mirror of
https://github.com/Raghu-Ch/angular-challenges.git
synced 2026-02-10 12:53:03 -05:00
refactor: move libs
This commit is contained in:
36
apps/signal/30-interop-rxjs-signal/.eslintrc.json
Normal file
36
apps/signal/30-interop-rxjs-signal/.eslintrc.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"extends": ["../../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts"],
|
||||
"rules": {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "app",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "app",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
]
|
||||
},
|
||||
"extends": [
|
||||
"plugin:@nx/angular",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
]
|
||||
},
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"extends": ["plugin:@nx/angular-template"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
13
apps/signal/30-interop-rxjs-signal/README.md
Normal file
13
apps/signal/30-interop-rxjs-signal/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Interoperability Rxjs/Signal
|
||||
|
||||
> author: thomas-laforge
|
||||
|
||||
### Run Application
|
||||
|
||||
```bash
|
||||
npx nx serve signal-interop-rxjs-signal
|
||||
```
|
||||
|
||||
### Documentation and Instruction
|
||||
|
||||
Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/30-interop-rxjs-signal/).
|
||||
22
apps/signal/30-interop-rxjs-signal/jest.config.ts
Normal file
22
apps/signal/30-interop-rxjs-signal/jest.config.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/* eslint-disable */
|
||||
export default {
|
||||
displayName: 'signal-interop-rxjs-signal',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
coverageDirectory: '../../../coverage/apps/signal/30-interop-rxjs-signal',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
'jest-preset-angular/build/serializers/html-comment',
|
||||
],
|
||||
};
|
||||
84
apps/signal/30-interop-rxjs-signal/project.json
Normal file
84
apps/signal/30-interop-rxjs-signal/project.json
Normal file
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"name": "signal-interop-rxjs-signal",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"sourceRoot": "apps/signal/30-interop-rxjs-signal/src",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@angular-devkit/build-angular:browser",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/apps/signal/30-interop-rxjs-signal",
|
||||
"index": "apps/signal/30-interop-rxjs-signal/src/index.html",
|
||||
"main": "apps/signal/30-interop-rxjs-signal/src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "apps/signal/30-interop-rxjs-signal/tsconfig.app.json",
|
||||
"assets": [
|
||||
"apps/signal/30-interop-rxjs-signal/src/favicon.ico",
|
||||
"apps/signal/30-interop-rxjs-signal/src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"apps/signal/30-interop-rxjs-signal/src/styles.scss",
|
||||
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
"optimization": false,
|
||||
"vendorChunk": true,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true,
|
||||
"namedChunks": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "signal-interop-rxjs-signal:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "signal-interop-rxjs-signal:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"executor": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "signal-interop-rxjs-signal:build"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
},
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "apps/signal/30-interop-rxjs-signal/jest.config.ts"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
apps/signal/30-interop-rxjs-signal/src/app/app.component.ts
Normal file
13
apps/signal/30-interop-rxjs-signal/src/app/app.component.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [RouterOutlet],
|
||||
selector: 'app-root',
|
||||
template: `
|
||||
<router-outlet />
|
||||
`,
|
||||
styles: [''],
|
||||
})
|
||||
export class AppComponent {}
|
||||
29
apps/signal/30-interop-rxjs-signal/src/app/app.config.ts
Normal file
29
apps/signal/30-interop-rxjs-signal/src/app/app.config.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
import { provideAnimations } from '@angular/platform-browser/animations';
|
||||
import { provideRouter, withComponentInputBinding } from '@angular/router';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
provideAnimations(),
|
||||
provideRouter(
|
||||
[
|
||||
{
|
||||
path: '',
|
||||
pathMatch: 'full',
|
||||
loadComponent: () => import('./list/photos.component'),
|
||||
},
|
||||
{
|
||||
path: 'detail',
|
||||
loadComponent: () => import('./detail/detail.component'),
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: '',
|
||||
},
|
||||
],
|
||||
withComponentInputBinding(),
|
||||
),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { Component, Input as RouterInput } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { Photo } from '../photo.model';
|
||||
|
||||
@Component({
|
||||
selector: 'app-photos',
|
||||
standalone: true,
|
||||
imports: [DatePipe, RouterLink],
|
||||
template: `
|
||||
<img src="{{ photo.url_m }}" alt="{{ photo.title }}" class="image" />
|
||||
<p>
|
||||
<span class="font-bold">Title:</span>
|
||||
{{ photo.title }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-bold">Owner:</span>
|
||||
{{ photo.ownername }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-bold">Date:</span>
|
||||
{{ photo.datetaken | date }}
|
||||
</p>
|
||||
<p>
|
||||
<span class="font-bold">Tags:</span>
|
||||
{{ photo.tags }}
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="mt-10 rounded-md border border-black px-4 py-2"
|
||||
routerLink="">
|
||||
Back
|
||||
</button>
|
||||
`,
|
||||
host: {
|
||||
class: 'p-5 block',
|
||||
},
|
||||
})
|
||||
export default class DetailComponent {
|
||||
@RouterInput({
|
||||
required: true,
|
||||
transform: (value: string) => JSON.parse(decodeURIComponent(value)),
|
||||
})
|
||||
photo!: Photo;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { NgFor, NgIf } from '@angular/common';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { RouterLinkWithHref } from '@angular/router';
|
||||
import { LetDirective } from '@ngrx/component';
|
||||
import { provideComponentStore } from '@ngrx/component-store';
|
||||
import { debounceTime, distinctUntilChanged, skipWhile, tap } from 'rxjs';
|
||||
import { Photo } from '../photo.model';
|
||||
import { PhotoStore } from './photos.store';
|
||||
|
||||
@Component({
|
||||
selector: 'app-photos',
|
||||
standalone: true,
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
MatFormFieldModule,
|
||||
MatProgressBarModule,
|
||||
NgIf,
|
||||
NgFor,
|
||||
MatInputModule,
|
||||
LetDirective,
|
||||
RouterLinkWithHref,
|
||||
],
|
||||
template: `
|
||||
<h2 class="mb-2 text-xl">Photos</h2>
|
||||
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>Search</mat-label>
|
||||
<input
|
||||
type="text"
|
||||
matInput
|
||||
[formControl]="search"
|
||||
placeholder="find a photo" />
|
||||
</mat-form-field>
|
||||
|
||||
<ng-container *ngrxLet="vm$ as vm">
|
||||
<section class="flex flex-col">
|
||||
<section class="flex items-center gap-3">
|
||||
<button
|
||||
[disabled]="vm.page === 1"
|
||||
[class.bg-gray-400]="vm.page === 1"
|
||||
class="rounded-md border p-3 text-xl"
|
||||
(click)="store.previousPage()">
|
||||
<
|
||||
</button>
|
||||
<button
|
||||
[disabled]="vm.endOfPage"
|
||||
[class.bg-gray-400]="vm.endOfPage"
|
||||
class="rounded-md border p-3 text-xl"
|
||||
(click)="store.nextPage()">
|
||||
>
|
||||
</button>
|
||||
Page :{{ vm.page }} / {{ vm.pages }}
|
||||
</section>
|
||||
<mat-progress-bar
|
||||
mode="query"
|
||||
*ngIf="vm.loading"
|
||||
class="mt-5"></mat-progress-bar>
|
||||
<ul
|
||||
class="flex flex-wrap gap-4"
|
||||
*ngIf="vm.photos && vm.photos.length > 0; else noPhoto">
|
||||
<li *ngFor="let photo of vm.photos; trackBy: trackById">
|
||||
<a routerLink="detail" [queryParams]="{ photo: encode(photo) }">
|
||||
<img
|
||||
src="{{ photo.url_q }}"
|
||||
alt="{{ photo.title }}"
|
||||
class="image" />
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ng-template #noPhoto>
|
||||
<div>No Photos found. Type a search word.</div>
|
||||
</ng-template>
|
||||
<footer class="text-red-500">
|
||||
{{ vm.error }}
|
||||
</footer>
|
||||
</section>
|
||||
</ng-container>
|
||||
`,
|
||||
providers: [provideComponentStore(PhotoStore)],
|
||||
host: {
|
||||
class: 'p-5 block',
|
||||
},
|
||||
})
|
||||
export default class PhotosComponent implements OnInit {
|
||||
store = inject(PhotoStore);
|
||||
readonly vm$ = this.store.vm$.pipe(
|
||||
tap(({ search }) => {
|
||||
if (!this.formInit) {
|
||||
this.search.setValue(search);
|
||||
this.formInit = true;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
private formInit = false;
|
||||
search = new FormControl();
|
||||
|
||||
ngOnInit(): void {
|
||||
this.store.search(
|
||||
this.search.valueChanges.pipe(
|
||||
skipWhile(() => !this.formInit),
|
||||
debounceTime(300),
|
||||
distinctUntilChanged(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
trackById(index: number, photo: Photo) {
|
||||
return photo.id;
|
||||
}
|
||||
|
||||
encode(photo: Photo) {
|
||||
return encodeURIComponent(JSON.stringify(photo));
|
||||
}
|
||||
}
|
||||
135
apps/signal/30-interop-rxjs-signal/src/app/list/photos.store.ts
Normal file
135
apps/signal/30-interop-rxjs-signal/src/app/list/photos.store.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import {
|
||||
ComponentStore,
|
||||
OnStateInit,
|
||||
OnStoreInit,
|
||||
tapResponse,
|
||||
} from '@ngrx/component-store';
|
||||
import { pipe } from 'rxjs';
|
||||
import { filter, mergeMap, tap } from 'rxjs/operators';
|
||||
import { Photo } from '../photo.model';
|
||||
import { PhotoService } from '../photos.service';
|
||||
|
||||
const PHOTO_STATE_KEY = 'photo_search';
|
||||
|
||||
export interface PhotoState {
|
||||
photos: Photo[];
|
||||
search: string;
|
||||
page: number;
|
||||
pages: number;
|
||||
loading: boolean;
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
const initialState: PhotoState = {
|
||||
photos: [],
|
||||
search: '',
|
||||
page: 1,
|
||||
pages: 1,
|
||||
loading: false,
|
||||
error: '',
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class PhotoStore
|
||||
extends ComponentStore<PhotoState>
|
||||
implements OnStoreInit, OnStateInit
|
||||
{
|
||||
private photoService = inject(PhotoService);
|
||||
|
||||
private readonly photos$ = this.select((s) => s.photos);
|
||||
private readonly search$ = this.select((s) => s.search);
|
||||
private readonly page$ = this.select((s) => s.page);
|
||||
private readonly pages$ = this.select((s) => s.pages);
|
||||
private readonly error$ = this.select((s) => s.error);
|
||||
private readonly loading$ = this.select((s) => s.loading);
|
||||
|
||||
private readonly endOfPage$ = this.select(
|
||||
this.page$,
|
||||
this.pages$,
|
||||
(page, pages) => page === pages,
|
||||
);
|
||||
|
||||
readonly vm$ = this.select(
|
||||
{
|
||||
photos: this.photos$,
|
||||
search: this.search$,
|
||||
page: this.page$,
|
||||
pages: this.pages$,
|
||||
endOfPage: this.endOfPage$,
|
||||
loading: this.loading$,
|
||||
error: this.error$,
|
||||
},
|
||||
{ debounce: true },
|
||||
);
|
||||
|
||||
ngrxOnStoreInit() {
|
||||
const savedJSONState = localStorage.getItem(PHOTO_STATE_KEY);
|
||||
if (savedJSONState === null) {
|
||||
this.setState(initialState);
|
||||
} else {
|
||||
const savedState = JSON.parse(savedJSONState);
|
||||
this.setState({
|
||||
...initialState,
|
||||
search: savedState.search,
|
||||
page: savedState.page,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngrxOnStateInit() {
|
||||
this.searchPhotos(
|
||||
this.select({
|
||||
search: this.search$,
|
||||
page: this.page$,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
readonly search = this.updater(
|
||||
(state, search: string): PhotoState => ({
|
||||
...state,
|
||||
search,
|
||||
page: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
readonly nextPage = this.updater(
|
||||
(state): PhotoState => ({
|
||||
...state,
|
||||
page: state.page + 1,
|
||||
}),
|
||||
);
|
||||
|
||||
readonly previousPage = this.updater(
|
||||
(state): PhotoState => ({
|
||||
...state,
|
||||
page: state.page - 1,
|
||||
}),
|
||||
);
|
||||
|
||||
readonly searchPhotos = this.effect<{ search: string; page: number }>(
|
||||
pipe(
|
||||
filter(({ search }) => search.length >= 3),
|
||||
tap(() => this.patchState({ loading: true, error: '' })),
|
||||
mergeMap(({ search, page }) =>
|
||||
this.photoService.searchPublicPhotos(search, page).pipe(
|
||||
tapResponse(
|
||||
({ photos: { photo, pages } }) => {
|
||||
this.patchState({
|
||||
loading: false,
|
||||
photos: photo,
|
||||
pages,
|
||||
});
|
||||
localStorage.setItem(
|
||||
PHOTO_STATE_KEY,
|
||||
JSON.stringify({ search, page }),
|
||||
);
|
||||
},
|
||||
(error: unknown) => this.patchState({ error, loading: false }),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
22
apps/signal/30-interop-rxjs-signal/src/app/photo.model.ts
Normal file
22
apps/signal/30-interop-rxjs-signal/src/app/photo.model.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export interface Photo {
|
||||
id: string;
|
||||
owner: string;
|
||||
secret: string;
|
||||
server: string;
|
||||
farm: number;
|
||||
title: string;
|
||||
ispublic: number;
|
||||
isfriend: number;
|
||||
isfamily: number;
|
||||
datetaken: string;
|
||||
datetakengranularity: number;
|
||||
datetakenunknown: string;
|
||||
ownername: string;
|
||||
tags: string;
|
||||
url_q: string;
|
||||
height_q: number;
|
||||
width_q: number;
|
||||
url_m: string;
|
||||
height_m: number;
|
||||
width_m: number;
|
||||
}
|
||||
43
apps/signal/30-interop-rxjs-signal/src/app/photos.service.ts
Normal file
43
apps/signal/30-interop-rxjs-signal/src/app/photos.service.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Photo } from './photo.model';
|
||||
|
||||
export interface FlickrAPIResponse {
|
||||
photos: {
|
||||
page: number;
|
||||
pages: number;
|
||||
perpage: number;
|
||||
total: number;
|
||||
photo: Photo[];
|
||||
};
|
||||
stat: string;
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class PhotoService {
|
||||
private http = inject(HttpClient);
|
||||
|
||||
public searchPublicPhotos(
|
||||
searchTerm: string,
|
||||
page: number,
|
||||
): Observable<FlickrAPIResponse> {
|
||||
return this.http.get<FlickrAPIResponse>(
|
||||
'https://www.flickr.com/services/rest/',
|
||||
{
|
||||
params: {
|
||||
tags: searchTerm,
|
||||
method: 'flickr.photos.search',
|
||||
format: 'json',
|
||||
nojsoncallback: '1',
|
||||
tag_mode: 'all',
|
||||
media: 'photos',
|
||||
per_page: '30',
|
||||
page,
|
||||
extras: 'tags,date_taken,owner_name,url_q,url_m',
|
||||
api_key: 'c3050d39a5bb308d9921bef0e15c437d',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
BIN
apps/signal/30-interop-rxjs-signal/src/favicon.ico
Normal file
BIN
apps/signal/30-interop-rxjs-signal/src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
13
apps/signal/30-interop-rxjs-signal/src/index.html
Normal file
13
apps/signal/30-interop-rxjs-signal/src/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>rxjs-to-signal</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
7
apps/signal/30-interop-rxjs-signal/src/main.ts
Normal file
7
apps/signal/30-interop-rxjs-signal/src/main.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { appConfig } from './app/app.config';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
||||
console.error(err),
|
||||
);
|
||||
5
apps/signal/30-interop-rxjs-signal/src/styles.scss
Normal file
5
apps/signal/30-interop-rxjs-signal/src/styles.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
2
apps/signal/30-interop-rxjs-signal/src/test-setup.ts
Normal file
2
apps/signal/30-interop-rxjs-signal/src/test-setup.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import 'jest-preset-angular/setup-jest';
|
||||
14
apps/signal/30-interop-rxjs-signal/tailwind.config.js
Normal file
14
apps/signal/30-interop-rxjs-signal/tailwind.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind');
|
||||
const { join } = require('path');
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
|
||||
...createGlobPatternsForDependencies(__dirname),
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
10
apps/signal/30-interop-rxjs-signal/tsconfig.app.json
Normal file
10
apps/signal/30-interop-rxjs-signal/tsconfig.app.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"types": []
|
||||
},
|
||||
"files": ["src/main.ts"],
|
||||
"include": ["src/**/*.d.ts"],
|
||||
"exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
|
||||
}
|
||||
7
apps/signal/30-interop-rxjs-signal/tsconfig.editor.json
Normal file
7
apps/signal/30-interop-rxjs-signal/tsconfig.editor.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"types": ["jest", "node"]
|
||||
}
|
||||
}
|
||||
32
apps/signal/30-interop-rxjs-signal/tsconfig.json
Normal file
32
apps/signal/30-interop-rxjs-signal/tsconfig.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"useDefineForClassFields": false,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.editor.json"
|
||||
}
|
||||
],
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
15
apps/signal/30-interop-rxjs-signal/tsconfig.spec.json
Normal file
15
apps/signal/30-interop-rxjs-signal/tsconfig.spec.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["jest", "node", "@testing-library/jest-dom"]
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
36
apps/signal/43-signal-input/.eslintrc.json
Normal file
36
apps/signal/43-signal-input/.eslintrc.json
Normal file
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"extends": ["../../../.eslintrc.json"],
|
||||
"ignorePatterns": ["!**/*"],
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.ts"],
|
||||
"extends": [
|
||||
"plugin:@nx/angular",
|
||||
"plugin:@angular-eslint/template/process-inline-templates"
|
||||
],
|
||||
"rules": {
|
||||
"@angular-eslint/directive-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "attribute",
|
||||
"prefix": "app",
|
||||
"style": "camelCase"
|
||||
}
|
||||
],
|
||||
"@angular-eslint/component-selector": [
|
||||
"error",
|
||||
{
|
||||
"type": "element",
|
||||
"prefix": "app",
|
||||
"style": "kebab-case"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.html"],
|
||||
"extends": ["plugin:@nx/angular-template"],
|
||||
"rules": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
13
apps/signal/43-signal-input/README.md
Normal file
13
apps/signal/43-signal-input/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Signal Input
|
||||
|
||||
> author: thomas-laforge
|
||||
|
||||
### Run Application
|
||||
|
||||
```bash
|
||||
npx nx serve signal-signal-input
|
||||
```
|
||||
|
||||
### Documentation and Instruction
|
||||
|
||||
Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/43-signal-input/).
|
||||
73
apps/signal/43-signal-input/project.json
Normal file
73
apps/signal/43-signal-input/project.json
Normal file
@@ -0,0 +1,73 @@
|
||||
{
|
||||
"name": "signal-signal-input",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"sourceRoot": "apps/signal/43-signal-input/src",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@angular-devkit/build-angular:application",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/apps/signal/43-signal-input",
|
||||
"index": "apps/signal/43-signal-input/src/index.html",
|
||||
"browser": "apps/signal/43-signal-input/src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "apps/signal/43-signal-input/tsconfig.app.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
"apps/signal/43-signal-input/src/favicon.ico",
|
||||
"apps/signal/43-signal-input/src/assets"
|
||||
],
|
||||
"styles": ["apps/signal/43-signal-input/src/styles.scss"],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"executor": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "signal-signal-input:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "signal-signal-input:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"executor": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "signal-signal-input:build"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint",
|
||||
"outputs": ["{options.outputFile}"]
|
||||
}
|
||||
}
|
||||
}
|
||||
45
apps/signal/43-signal-input/src/app/app.component.ts
Normal file
45
apps/signal/43-signal-input/src/app/app.component.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { JsonPipe } from '@angular/common';
|
||||
import { Component } from '@angular/core';
|
||||
import { UserComponent } from './user.component';
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
imports: [UserComponent, JsonPipe],
|
||||
selector: 'app-root',
|
||||
template: `
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex gap-2 ">
|
||||
Name:
|
||||
<input #name class="border" />
|
||||
@if (showUser && !name.value) {
|
||||
<div class="text-sm text-red-500">name required</div>
|
||||
}
|
||||
</div>
|
||||
<div class="flex gap-2 ">
|
||||
LastName:
|
||||
<input #lastName class="border" />
|
||||
</div>
|
||||
<div class="flex gap-2 ">
|
||||
Age:
|
||||
<input type="number" #age class="border" />
|
||||
</div>
|
||||
<button
|
||||
(click)="showUser = true"
|
||||
class="w-fit rounded-md border border-blue-500 bg-blue-200 px-4 py-2">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
@if (showUser && !!name.value) {
|
||||
<app-user
|
||||
[name]="name.value"
|
||||
[lastName]="lastName.value"
|
||||
[age]="age.value" />
|
||||
}
|
||||
`,
|
||||
host: {
|
||||
class: 'p-10 block flex flex-col gap-10',
|
||||
},
|
||||
})
|
||||
export class AppComponent {
|
||||
showUser = false;
|
||||
}
|
||||
5
apps/signal/43-signal-input/src/app/app.config.ts
Normal file
5
apps/signal/43-signal-input/src/app/app.config.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [],
|
||||
};
|
||||
41
apps/signal/43-signal-input/src/app/user.component.ts
Normal file
41
apps/signal/43-signal-input/src/app/user.component.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { TitleCasePipe } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
} from '@angular/core';
|
||||
|
||||
type Category = 'Youth' | 'Junior' | 'Open' | 'Senior';
|
||||
const ageToCategory = (age: number): Category => {
|
||||
if (age < 10) return 'Youth';
|
||||
else if (age < 18) return 'Junior';
|
||||
else if (age < 35) return 'Open';
|
||||
return 'Senior';
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'app-user',
|
||||
standalone: true,
|
||||
imports: [TitleCasePipe],
|
||||
template: `
|
||||
{{ fullName | titlecase }} plays tennis in the {{ category }} category!!
|
||||
`,
|
||||
host: {
|
||||
class: 'text-xl text-green-800',
|
||||
},
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class UserComponent implements OnChanges {
|
||||
@Input({ required: true }) name!: string;
|
||||
@Input() lastName?: string;
|
||||
@Input() age?: string;
|
||||
|
||||
fullName = '';
|
||||
category: Category = 'Junior';
|
||||
|
||||
ngOnChanges(): void {
|
||||
this.fullName = `${this.name} ${this.lastName ?? ''}`;
|
||||
this.category = ageToCategory(Number(this.age) ?? 0);
|
||||
}
|
||||
}
|
||||
0
apps/signal/43-signal-input/src/assets/.gitkeep
Normal file
0
apps/signal/43-signal-input/src/assets/.gitkeep
Normal file
BIN
apps/signal/43-signal-input/src/favicon.ico
Normal file
BIN
apps/signal/43-signal-input/src/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
13
apps/signal/43-signal-input/src/index.html
Normal file
13
apps/signal/43-signal-input/src/index.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>angular-signal-input</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
7
apps/signal/43-signal-input/src/main.ts
Normal file
7
apps/signal/43-signal-input/src/main.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { appConfig } from './app/app.config';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
||||
console.error(err),
|
||||
);
|
||||
5
apps/signal/43-signal-input/src/styles.scss
Normal file
5
apps/signal/43-signal-input/src/styles.scss
Normal file
@@ -0,0 +1,5 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* You can add global styles to this file, and also import other style files */
|
||||
14
apps/signal/43-signal-input/tailwind.config.js
Normal file
14
apps/signal/43-signal-input/tailwind.config.js
Normal file
@@ -0,0 +1,14 @@
|
||||
const { createGlobPatternsForDependencies } = require('@nx/angular/tailwind');
|
||||
const { join } = require('path');
|
||||
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
join(__dirname, 'src/**/!(*.stories|*.spec).{ts,html}'),
|
||||
...createGlobPatternsForDependencies(__dirname),
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
10
apps/signal/43-signal-input/tsconfig.app.json
Normal file
10
apps/signal/43-signal-input/tsconfig.app.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"types": []
|
||||
},
|
||||
"files": ["src/main.ts"],
|
||||
"include": ["src/**/*.d.ts"],
|
||||
"exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
|
||||
}
|
||||
7
apps/signal/43-signal-input/tsconfig.editor.json
Normal file
7
apps/signal/43-signal-input/tsconfig.editor.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"types": []
|
||||
}
|
||||
}
|
||||
30
apps/signal/43-signal-input/tsconfig.json
Normal file
30
apps/signal/43-signal-input/tsconfig.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"useDefineForClassFields": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.editor.json"
|
||||
}
|
||||
],
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user