Merge branch 'main' into all-contributors/add-kabrunko-dev

This commit is contained in:
Laforge Thomas
2024-01-31 21:38:40 +01:00
committed by GitHub
426 changed files with 10426 additions and 6262 deletions

View File

@@ -26,9 +26,9 @@
"symbol": "🇵🇹",
"description": "Translate in Portuguese"
},
"translation-pt-BR": {
"symbol": "🇧🇷",
"description": "Translate in Brazilian Portuguese"
"translation-ru": {
"symbol": "🇷🇺",
"description": "Translate in Russian"
}
},
"contributors": [
@@ -47,6 +47,38 @@
"translation-fr"
]
},
{
"login": "jdegand",
"name": "J. Degand",
"avatar_url": "https://avatars.githubusercontent.com/u/70610011?v=4",
"profile": "https://github.com/jdegand",
"contributions": [
"doc",
"content",
"code"
]
},
{
"login": "DeveshChau",
"name": "Devesh Chaudhari",
"avatar_url": "https://avatars.githubusercontent.com/u/9509673?v=4",
"profile": "https://github.com/DeveshChau",
"contributions": [
"code",
"bug",
"challenge"
]
},
{
"login": "stillst",
"name": "stillst",
"avatar_url": "https://avatars.githubusercontent.com/u/1463098?v=4",
"profile": "https://github.com/stillst",
"contributions": [
"challenge",
"translation-ru"
]
},
{
"login": "alan-bio",
"name": "Alan Dragicevich",
@@ -94,26 +126,6 @@
"code"
]
},
{
"login": "jdegand",
"name": "J. Degand",
"avatar_url": "https://avatars.githubusercontent.com/u/70610011?v=4",
"profile": "https://github.com/jdegand",
"contributions": [
"doc"
]
},
{
"login": "DeveshChau",
"name": "Devesh Chaudhari",
"avatar_url": "https://avatars.githubusercontent.com/u/9509673?v=4",
"profile": "https://github.com/DeveshChau",
"contributions": [
"code",
"bug",
"challenge"
]
},
{
"login": "dmmishchenko",
"name": "Dmitriy Mishchenko",
@@ -157,9 +169,28 @@
"avatar_url": "https://avatars.githubusercontent.com/u/142346548?v=4",
"profile": "https://github.com/kabrunko-dev/",
"contributions": [
"translation-pt-BR",
"code",
"translation-pt",
"doc"
]
},
{
"login": "ErickRodrCodes",
"name": "Erick Rodriguez",
"avatar_url": "https://avatars.githubusercontent.com/u/1978642?v=4",
"profile": "http://www.streamoverlaypro.com",
"contributions": [
"translation-es"
]
},
{
"login": "eduardoRoth",
"name": "Eduardo Roth",
"avatar_url": "https://avatars.githubusercontent.com/u/5419161?v=4",
"profile": "https://eduardoroth.dev",
"contributions": [
"doc",
"code"
"translation-es"
]
}
],

View File

@@ -16,10 +16,12 @@ jobs:
days-before-issue-close: -1
stale-issue-label: 'stale'
stale-issue-message: 'This issue is stale because it has been open for 20 days with no activity.'
exempt-issue-labels: 'long-term'
days-before-pr-stale: 20
days-before-pr-close: 7
stale-pr-label: 'stale'
stale-pr-message: 'This pull request is stale because it has been open for 20 days with no activity.'
close-pr-message: 'This pull request was closed because it has been inactive for 7 days since being marked as stale.'
only-pr-labels: 'answer'
exempt-pr-labels: 'challenge-creation, long-term'
repo-token: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@@ -19,7 +19,6 @@ node_modules
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

View File

@@ -1,3 +1,6 @@
{
"eslint.validate": ["json"]
"eslint.validate": [
"json"
],
"cSpell.language": "en,es-ES"
}

View File

@@ -24,7 +24,7 @@ If you would like to propose a challenge, this project is open source, so feel f
## Challenges
Check [all 40 challenges](https://angular-challenges.vercel.app/)
Check [all 43 challenges](https://angular-challenges.vercel.app/)
## Contributors ✨
@@ -35,20 +35,25 @@ Check [all 40 challenges](https://angular-challenges.vercel.app/)
<tbody>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://thomaslaforge.dev/home"><img src="https://avatars.githubusercontent.com/u/30832608?v=4?s=100" width="100px;" alt="Laforge Thomas"/><br /><sub><b>Laforge Thomas</b></sub></a><br /><a href="#challenge-tomalaforge" title="Create a challenge">🧩</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=tomalaforge" title="Code">💻</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=tomalaforge" title="Documentation">📖</a> <a href="#content-tomalaforge" title="Content">🖋</a> <a href="#ideas-tomalaforge" title="Ideas, Planning, & Feedback">🤔</a> <a href="#design-tomalaforge" title="Design">🎨</a> <a href="#translation-fr-tomalaforge" title="Translate in French">🇫🇷</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jdegand"><img src="https://avatars.githubusercontent.com/u/70610011?v=4?s=100" width="100px;" alt="J. Degand"/><br /><sub><b>J. Degand</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=jdegand" title="Documentation">📖</a> <a href="#content-jdegand" title="Content">🖋</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=jdegand" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DeveshChau"><img src="https://avatars.githubusercontent.com/u/9509673?v=4?s=100" width="100px;" alt="Devesh Chaudhari"/><br /><sub><b>Devesh Chaudhari</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=DeveshChau" title="Code">💻</a> <a href="https://github.com/tomalaforge/angular-challenges/issues?q=author%3ADeveshChau" title="Bug reports">🐛</a> <a href="#challenge-DeveshChau" title="Create a challenge">🧩</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/stillst"><img src="https://avatars.githubusercontent.com/u/1463098?v=4?s=100" width="100px;" alt="stillst"/><br /><sub><b>stillst</b></sub></a><br /><a href="#challenge-stillst" title="Create a challenge">🧩</a> <a href="#translation-ru-stillst" title="Translate in Russian">🇷🇺</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/alan-bio"><img src="https://avatars.githubusercontent.com/u/31838230?v=4?s=100" width="100px;" alt="Alan Dragicevich"/><br /><sub><b>Alan Dragicevich</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=alan-bio" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/edimitchel"><img src="https://avatars.githubusercontent.com/u/2922851?v=4?s=100" width="100px;" alt="Michel EDIGHOFFER"/><br /><sub><b>Michel EDIGHOFFER</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=edimitchel" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/gsgonzalez88"><img src="https://avatars.githubusercontent.com/u/39884678?v=4?s=100" width="100px;" alt="Gerardo Sebastian Gonzalez"/><br /><sub><b>Gerardo Sebastian Gonzalez</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=gsgonzalez88" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marryday"><img src="https://avatars.githubusercontent.com/u/57489315?v=4?s=100" width="100px;" alt="Evseev Yuriy"/><br /><sub><b>Evseev Yuriy</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/issues?q=author%3Amarryday" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tomer953"><img src="https://avatars.githubusercontent.com/u/1807493?v=4?s=100" width="100px;" alt="Tomer953"/><br /><sub><b>Tomer953</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/issues?q=author%3Atomer953" title="Bug reports">🐛</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=tomer953" title="Documentation">📖</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=tomer953" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/jdegand"><img src="https://avatars.githubusercontent.com/u/70610011?v=4?s=100" width="100px;" alt="J. Degand"/><br /><sub><b>J. Degand</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=jdegand" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/DeveshChau"><img src="https://avatars.githubusercontent.com/u/9509673?v=4?s=100" width="100px;" alt="Devesh Chaudhari"/><br /><sub><b>Devesh Chaudhari</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=DeveshChau" title="Code">💻</a> <a href="https://github.com/tomalaforge/angular-challenges/issues?q=author%3ADeveshChau" title="Bug reports">🐛</a> <a href="#challenge-DeveshChau" title="Create a challenge">🧩</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/marryday"><img src="https://avatars.githubusercontent.com/u/57489315?v=4?s=100" width="100px;" alt="Evseev Yuriy"/><br /><sub><b>Evseev Yuriy</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/issues?q=author%3Amarryday" title="Bug reports">🐛</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/tomer953"><img src="https://avatars.githubusercontent.com/u/1807493?v=4?s=100" width="100px;" alt="Tomer953"/><br /><sub><b>Tomer953</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/issues?q=author%3Atomer953" title="Bug reports">🐛</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=tomer953" title="Documentation">📖</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=tomer953" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/dmmishchenko"><img src="https://avatars.githubusercontent.com/u/51910160?v=4?s=100" width="100px;" alt="Dmitriy Mishchenko"/><br /><sub><b>Dmitriy Mishchenko</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=dmmishchenko" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="http://www.sagardev.com.np"><img src="https://avatars.githubusercontent.com/u/30800393?v=4?s=100" width="100px;" alt="Sagar Devkota"/><br /><sub><b>Sagar Devkota</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=Sagardevkota" title="Documentation">📖</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=Sagardevkota" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://nelsonguti.dev/"><img src="https://avatars.githubusercontent.com/u/62297014?v=4?s=100" width="100px;" alt="Nelson Gutierrez"/><br /><sub><b>Nelson Gutierrez</b></sub></a><br /><a href="#translation-es-nelsongutidev" title="Translate in Spanish">🇪🇸</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/ho-ssain"><img src="https://avatars.githubusercontent.com/u/61125174?v=4?s=100" width="100px;" alt="Hossain K. M."/><br /><sub><b>Hossain K. M.</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=ho-ssain" title="Documentation">📖</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kabrunko-dev/"><img src="https://avatars.githubusercontent.com/u/142346548?v=4?s=100" width="100px;" alt="Diogo Nishikawa"/><br /><sub><b>Diogo Nishikawa</b></sub></a><br /><a href="#translation-pt-BR-kabrunko-dev" title="Translate in Brazilian Portuguese">🇧🇷</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=kabrunko-dev" title="Documentation">📖</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=kabrunko-dev" title="Code">💻</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://github.com/kabrunko-dev/"><img src="https://avatars.githubusercontent.com/u/142346548?v=4?s=100" width="100px;" alt="Diogo Nishikawa"/><br /><sub><b>Diogo Nishikawa</b></sub></a><br /><a href="#translation-pt-kabrunko-dev" title="Translate in Portuguese">🇵🇹</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=kabrunko-dev" title="Documentation">📖</a> <a href="https://github.com/tomalaforge/angular-challenges/commits?author=kabrunko-dev" title="Code">💻</a></td>
</tr>
<tr>
<td align="center" valign="top" width="14.28%"><a href="http://www.streamoverlaypro.com"><img src="https://avatars.githubusercontent.com/u/1978642?v=4?s=100" width="100px;" alt="Erick Rodriguez"/><br /><sub><b>Erick Rodriguez</b></sub></a><br /><a href="#translation-es-ErickRodrCodes" title="Translate in Spanish">🇪🇸</a></td>
<td align="center" valign="top" width="14.28%"><a href="https://eduardoroth.dev"><img src="https://avatars.githubusercontent.com/u/5419161?v=4?s=100" width="100px;" alt="Eduardo Roth"/><br /><sub><b>Eduardo Roth</b></sub></a><br /><a href="https://github.com/tomalaforge/angular-challenges/commits?author=eduardoRoth" title="Documentation">📖</a> <a href="#translation-es-eduardoRoth" title="Translate in Spanish">🇪🇸</a></td>
</tr>
</tbody>
<tfoot>

View File

@@ -2,7 +2,7 @@
export default {
displayName: 'angular-anchor-scrolling',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
setupFilesAfterEnv: ['<rootDir>/src/subscription-setup.ts'],
coverageDirectory: '../../../coverage/apps/angular/anchor-scrolling',
transform: {
'^.+\\.(ts|mjs|js|html)$': [

View File

@@ -5,6 +5,8 @@ import { RouterOutlet } from '@angular/router';
standalone: true,
imports: [RouterOutlet],
selector: 'app-root',
template: ` <router-outlet></router-outlet> `,
template: `
<router-outlet></router-outlet>
`,
})
export class AppComponent {}

View File

@@ -7,7 +7,7 @@ import { NavButtonComponent } from './nav-button.component';
selector: 'app-foo',
template: `
Welcome to foo page
<nav-button href="home" class="fixed top-3 left-1/2">Home Page</nav-button>
<nav-button href="home" class="fixed left-1/2 top-3">Home Page</nav-button>
<div class="h-screen bg-blue-200">section 1</div>
<div class="h-screen bg-red-200">section 2</div>
`,

View File

@@ -6,7 +6,7 @@ import { NavButtonComponent } from './nav-button.component';
imports: [NavButtonComponent],
selector: 'app-home',
template: `
<nav-button href="/foo" class="fixed top-3 left-1/2">Foo Page</nav-button>
<nav-button href="/foo" class="fixed left-1/2 top-3">Foo Page</nav-button>
<div id="top" class="h-screen bg-gray-500">
Empty
<nav-button href="#bottom">Scroll Bottom</nav-button>

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -1,8 +1,8 @@
import { appConfig } from './app/app.config';
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
console.error(err),
);

View File

@@ -2,7 +2,7 @@
export default {
displayName: 'angular-bug-cd',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
setupFilesAfterEnv: ['<rootDir>/src/subscription-setup.ts'],
coverageDirectory: '../../../coverage/apps/angular/bug-cd',
transform: {
'^.+\\.(ts|mjs|js|html)$': [

View File

@@ -6,7 +6,7 @@ import { RouterOutlet } from '@angular/router';
imports: [RouterOutlet],
selector: 'app-root',
template: `
<h1 class="text-xl px-4 py-2">My Application</h1>
<h1 class="px-4 py-2 text-xl">My Application</h1>
<section class="flex">
<router-outlet name="side" />
<div class="border p-4">

View File

@@ -2,6 +2,8 @@ import { Component } from '@angular/core';
@Component({
selector: 'app-bar',
standalone: true,
template: ` BarComponent `,
template: `
BarComponent
`,
})
export class BarComponent {}

View File

@@ -2,6 +2,8 @@ import { Component } from '@angular/core';
@Component({
selector: 'app-foo',
standalone: true,
template: `Foo Component `,
template: `
Foo Component
`,
})
export class FooComponent {}

View File

@@ -15,7 +15,7 @@ interface MenuItem {
template: `
<ng-container *ngFor="let menu of menus">
<a
class="border px-4 py-2 rounded-md"
class="rounded-md border px-4 py-2"
[routerLink]="menu.path"
routerLinkActive="isSelected">
{{ menu.name }}

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -1,7 +1,7 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
console.error(err),
);

View File

@@ -17,11 +17,10 @@ import {
*ngTemplateOutlet="
listTemplateRef || emptyRef;
context: { $implicit: item, appList: item, index: i }
">
</ng-container>
"></ng-container>
</div>
<ng-template #emptyRef> No Template </ng-template>
<ng-template #emptyRef>No Template</ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})

View File

@@ -17,7 +17,7 @@ interface Person {
context: { $implicit: person.name, age: person.age }
"></ng-container>
<ng-template #emptyRef> No Template </ng-template>
<ng-template #emptyRef>No Template</ng-template>
`,
})
export class PersonComponent {

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -2,7 +2,7 @@
export default {
displayName: 'angular-crud',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
setupFilesAfterEnv: ['<rootDir>/src/subscription-setup.ts'],
globals: {},
coverageDirectory: '../../../coverage/apps/angular/crud',
transform: {

View File

@@ -42,7 +42,7 @@ export class AppComponent implements OnInit {
headers: {
'Content-type': 'application/json; charset=UTF-8',
},
}
},
)
.subscribe((todoUpdated: any) => {
this.todos[todoUpdated.id - 1] = todoUpdated;

View File

@@ -1,6 +1,5 @@
import { ApplicationConfig } from '@angular/core';
import { enableProdMode, importProvidersFrom } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [importProvidersFrom(HttpClientModule)],
};

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -4,5 +4,5 @@ import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
console.error(err),
);

View File

@@ -6,6 +6,8 @@ import { Component } from '@angular/core';
standalone: true,
imports: [BtnDisabledDirective, BtnHelmetDirective],
selector: 'app-root',
template: ` <button btnDisabled hlm>Coucou</button> `,
template: `
<button btnDisabled hlm>Coucou</button>
`,
})
export class AppComponent {}

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -16,7 +16,7 @@ interface ProductContext {
export class ProductDirective {
static ngTemplateContextGuard(
dir: ProductDirective,
ctx: unknown
ctx: unknown,
): ctx is ProductContext {
return true;
}

View File

@@ -11,7 +11,7 @@ export class CurrencyPipe implements PipeTransform {
transform(price: number) {
return this.currencyService.symbol$.pipe(
map((s) => `${String(price)}${s}`)
map((s) => `${String(price)}${s}`),
);
}
}

View File

@@ -20,7 +20,7 @@ export const currency: Currency[] = [
export class CurrencyService extends ComponentStore<{ code: string }> {
readonly code$ = this.select((state) => state.code);
readonly symbol$ = this.code$.pipe(
map((code) => currency.find((c) => c.code === code)?.symbol ?? code)
map((code) => currency.find((c) => c.code === code)?.symbol ?? code),
);
constructor() {

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -2,7 +2,7 @@
export default {
displayName: 'angular-injection-token',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
setupFilesAfterEnv: ['<rootDir>/src/subscription-setup.ts'],
coverageDirectory: '../../../coverage/apps/angular/injection-token',
transform: {
'^.+\\.(ts|mjs|js|html)$': [

View File

@@ -5,15 +5,17 @@ import { RouterLink, RouterOutlet } from '@angular/router';
standalone: true,
imports: [RouterOutlet, RouterLink],
selector: 'app-root',
template: ` <div class="flex gap-4 mb-5">
<button class="border rounded-md px-4 py-2" routerLink="video">
template: `
<div class="mb-5 flex gap-4">
<button class="rounded-md border px-4 py-2" routerLink="video">
Video
</button>
<button class="border rounded-md px-4 py-2" routerLink="phone">
<button class="rounded-md border px-4 py-2" routerLink="phone">
Phone
</button>
</div>
<router-outlet />`,
<router-outlet />
`,
host: {
class: 'p-10 flex flex-col',
},

View File

@@ -5,10 +5,12 @@ import { TimerContainerComponent } from './timer-container.component';
selector: 'app-phone',
standalone: true,
imports: [TimerContainerComponent],
template: `<div class="flex gap-2">
template: `
<div class="flex gap-2">
Phone Call Timer:
<p class="italic">(should be 2000s)</p>
</div>
<timer-container />`,
<timer-container />
`,
})
export default class PhoneComponent {}

View File

@@ -6,7 +6,9 @@ import { DEFAULT_TIMER } from './data';
@Component({
selector: 'timer',
standalone: true,
template: ` Timer running {{ timer() }} `,
template: `
Timer running {{ timer() }}
`,
})
export class TimerComponent {
timer = toSignal(interval(DEFAULT_TIMER));

View File

@@ -5,10 +5,12 @@ import { TimerContainerComponent } from './timer-container.component';
selector: 'app-video',
standalone: true,
imports: [TimerContainerComponent],
template: `<div class="flex gap-2">
template: `
<div class="flex gap-2">
Video Call Timer:
<p class="italic">(should be the default 1000s)</p>
</div>
<timer-container />`,
<timer-container />
`,
})
export default class VideoComponent {}

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -1,7 +1,7 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
console.error(err),
);

View File

@@ -2,7 +2,7 @@
export default {
displayName: 'angular-interop-rxjs-signal',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
setupFilesAfterEnv: ['<rootDir>/src/subscription-setup.ts'],
coverageDirectory: '../../../coverage/apps/angular/interop-rxjs-signal',
transform: {
'^.+\\.(ts|mjs|js|html)$': [

View File

@@ -5,7 +5,9 @@ import { RouterOutlet } from '@angular/router';
standalone: true,
imports: [RouterOutlet],
selector: 'app-root',
template: `<router-outlet />`,
template: `
<router-outlet />
`,
styles: [''],
})
export class AppComponent {}

View File

@@ -23,7 +23,7 @@ export const appConfig: ApplicationConfig = {
redirectTo: '',
},
],
withComponentInputBinding()
withComponentInputBinding(),
),
],
};

View File

@@ -9,13 +9,25 @@ import { Photo } from '../photo.model';
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>
<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="border border-black rounded-md px-4 py-2 mt-10"
class="mt-10 rounded-md border border-black px-4 py-2"
routerLink="">
Back
</button>

View File

@@ -25,7 +25,7 @@ import { PhotoStore } from './photos.store';
RouterLinkWithHref,
],
template: `
<h2 class="text-xl mb-2">Photos</h2>
<h2 class="mb-2 text-xl">Photos</h2>
<mat-form-field appearance="fill">
<mat-label>Search</mat-label>
@@ -33,23 +33,23 @@ import { PhotoStore } from './photos.store';
type="text"
matInput
[formControl]="search"
placeholder="write an article" />
placeholder="find a photo" />
</mat-form-field>
<ng-container *ngrxLet="vm$ as vm">
<section class="flex flex-col">
<section class="flex gap-3 items-center">
<section class="flex items-center gap-3">
<button
[disabled]="vm.page === 1"
[class.bg-gray-400]="vm.page === 1"
class="text-xl border rounded-md p-3"
class="rounded-md border p-3 text-xl"
(click)="store.previousPage()">
<
</button>
<button
[disabled]="vm.endOfPage"
[class.bg-gray-400]="vm.endOfPage"
class="text-xl border rounded-md p-3"
class="rounded-md border p-3 text-xl"
(click)="store.nextPage()">
>
</button>
@@ -93,7 +93,7 @@ export default class PhotosComponent implements OnInit {
this.search.setValue(search);
this.formInit = true;
}
})
}),
);
private formInit = false;
@@ -104,8 +104,8 @@ export default class PhotosComponent implements OnInit {
this.search.valueChanges.pipe(
skipWhile(() => !this.formInit),
debounceTime(300),
distinctUntilChanged()
)
distinctUntilChanged(),
),
);
}

View File

@@ -47,7 +47,7 @@ export class PhotoStore
private readonly endOfPage$ = this.select(
this.page$,
this.pages$,
(page, pages) => page === pages
(page, pages) => page === pages,
);
readonly vm$ = this.select(
@@ -60,7 +60,7 @@ export class PhotoStore
loading: this.loading$,
error: this.error$,
},
{ debounce: true }
{ debounce: true },
);
ngrxOnStoreInit() {
@@ -82,7 +82,7 @@ export class PhotoStore
this.select({
search: this.search$,
page: this.page$,
})
}),
);
}
@@ -91,21 +91,21 @@ export class PhotoStore
...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 }>(
@@ -123,13 +123,13 @@ export class PhotoStore
});
localStorage.setItem(
PHOTO_STATE_KEY,
JSON.stringify({ search, page })
JSON.stringify({ search, page }),
);
},
(error: unknown) => this.patchState({ error, loading: false })
)
)
)
)
(error: unknown) => this.patchState({ error, loading: false }),
),
),
),
),
);
}

View File

@@ -1,10 +1,22 @@
export interface Photo {
id: string;
title: string;
tags: string;
owner: string;
ownername: 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;
}

View File

@@ -5,9 +5,13 @@ import { Photo } from './photo.model';
export interface FlickrAPIResponse {
photos: {
page: number;
pages: number;
perpage: number;
total: number;
photo: Photo[];
};
stat: string;
}
@Injectable({ providedIn: 'root' })
@@ -16,7 +20,7 @@ export class PhotoService {
public searchPublicPhotos(
searchTerm: string,
page: number
page: number,
): Observable<FlickrAPIResponse> {
return this.http.get<FlickrAPIResponse>(
'https://www.flickr.com/services/rest/',
@@ -33,7 +37,7 @@ export class PhotoService {
extras: 'tags,date_taken,owner_name,url_q,url_m',
api_key: 'c3050d39a5bb308d9921bef0e15c437d',
},
}
},
);
}
}

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -1,7 +1,7 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
console.error(err),
);

View File

@@ -2,24 +2,26 @@ import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: ` <div class="flex gap-2">
template: `
<div class="flex gap-2">
<button
routerLink="home"
class="border px-4 py-2 border-blue-400 rounded-md">
class="rounded-md border border-blue-400 px-4 py-2">
Home
</button>
<button
routerLink="admin"
class="border px-4 py-2 border-blue-400 rounded-md">
class="rounded-md border border-blue-400 px-4 py-2">
Admin
</button>
<button
routerLink="user"
class="border px-4 py-2 border-blue-400 rounded-md">
class="rounded-md border border-blue-400 px-4 py-2">
User
</button>
</div>
<router-outlet></router-outlet>`,
<router-outlet></router-outlet>
`,
host: {
class: 'flex flex-col p-4 gap-3',
},

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -5,7 +5,9 @@ import { RouterOutlet } from '@angular/router';
standalone: true,
imports: [RouterOutlet],
selector: 'app-root',
template: ` <router-outlet></router-outlet> `,
template: `
<router-outlet></router-outlet>
`,
styles: [],
})
export class AppComponent {}

View File

@@ -4,7 +4,9 @@ import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
standalone: true,
selector: 'button[app-button]',
template: `<ng-content></ng-content>`,
template: `
<ng-content></ng-content>
`,
host: {
class: 'border border-blue-700 bg-blue-400 p-2 rounded-sm text-white',
},

View File

@@ -18,7 +18,7 @@ import { UserStore } from './user.store';
imports: [InformationComponent, RouterLink, ButtonComponent],
selector: 'app-login',
template: `
<header class="flex gap-3 items-center">
<header class="flex items-center gap-3">
Log as :
<button app-button (click)="admin()">Admin</button>
<button app-button (click)="manager()">Manager</button>
@@ -26,12 +26,12 @@ import { UserStore } from './user.store';
<button app-button (click)="writer()">Writer</button>
<button app-button (click)="readerWriter()">Reader and Writer</button>
<button app-button (click)="client()">Client</button>
<button app-button (click)="everyone()">Client</button>
<button app-button (click)="everyone()">Everyone</button>
</header>
<app-information></app-information>
<button app-button class=" mt-10" routerLink="enter">
<button app-button class="mt-10" routerLink="enter">
Enter application
</button>
`,

View File

@@ -8,7 +8,7 @@ export const APP_ROUTES = [
path: 'enter',
loadComponent: () =>
import('./dashboard/admin.component').then(
(m) => m.AdminDashboardComponent
(m) => m.AdminDashboardComponent,
),
},
];

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -4,5 +4,5 @@ import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
console.error(err),
);

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -2,7 +2,7 @@
export default {
displayName: 'angular-projection',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
setupFilesAfterEnv: ['<rootDir>/src/subscription-setup.ts'],
globals: {},
coverageDirectory: '../../../coverage/apps/angular/projection',
transform: {

View File

@@ -7,10 +7,12 @@ import { CardComponent } from '../../ui/card/card.component';
@Component({
selector: 'app-student-card',
template: `<app-card
[list]="students"
[type]="cardType"
customClass="bg-light-green"></app-card>`,
template: `
<app-card
[list]="students"
[type]="cardType"
customClass="bg-light-green"></app-card>
`,
standalone: true,
styles: [
`
@@ -25,7 +27,10 @@ export class StudentCardComponent implements OnInit {
students: Student[] = [];
cardType = CardType.STUDENT;
constructor(private http: FakeHttpService, private store: StudentStore) {}
constructor(
private http: FakeHttpService,
private store: StudentStore,
) {}
ngOnInit(): void {
this.http.fetchStudents$.subscribe((s) => this.store.addAll(s));

View File

@@ -7,10 +7,12 @@ import { CardComponent } from '../../ui/card/card.component';
@Component({
selector: 'app-teacher-card',
template: `<app-card
[list]="teachers"
[type]="cardType"
customClass="bg-light-red"></app-card>`,
template: `
<app-card
[list]="teachers"
[type]="cardType"
customClass="bg-light-red"></app-card>
`,
styles: [
`
::ng-deep .bg-light-red {
@@ -25,7 +27,10 @@ export class TeacherCardComponent implements OnInit {
teachers: Teacher[] = [];
cardType = CardType.TEACHER;
constructor(private http: FakeHttpService, private store: TeacherStore) {}
constructor(
private http: FakeHttpService,
private store: TeacherStore,
) {}
ngOnInit(): void {
this.http.fetchTeachers$.subscribe((t) => this.store.addAll(t));

View File

@@ -6,7 +6,7 @@ import { CardType } from '../../model/card.model';
@Component({
selector: 'app-list-item',
template: `
<div class="border border-grey-300 py-1 px-2 flex justify-between">
<div class="border-grey-300 flex justify-between border px-2 py-1">
{{ name }}
<button (click)="delete(id)">
<img class="h-5" src="assets/svg/trash.svg" />
@@ -22,7 +22,7 @@ export class ListItemComponent {
constructor(
private teacherStore: TeacherStore,
private studentStore: StudentStore
private studentStore: StudentStore,
) {}
delete(id: number) {

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -6,17 +6,19 @@ import { RouterLink, RouterModule } from '@angular/router';
standalone: true,
imports: [RouterLink, RouterModule, ReactiveFormsModule],
selector: 'app-root',
template: ` <label for="userName">UserName</label>
template: `
<label for="userName">UserName</label>
<input id="userName" type="text" [formControl]="userName" />
<label for="testId">TestId</label>
<input id="testId" type="number" [formControl]="testId" />
<button
[routerLink]="'test/' + testId.value"
[routerLink]="'subscription/' + testId.value"
[queryParams]="{ user: userName.value }">
Test
</button>
<button routerLink="/">HOME</button>
<router-outlet></router-outlet>`,
<router-outlet></router-outlet>
`,
})
export class AppComponent {
userName = new FormControl();

View File

@@ -6,7 +6,7 @@ export const appRoutes: Route[] = [
loadComponent: () => import('./home.component'),
},
{
path: 'test/:testId',
path: 'subscription/:testId',
loadComponent: () => import('./test.component'),
data: {
permission: 'admin',

View File

@@ -3,6 +3,8 @@ import { Component } from '@angular/core';
selector: 'app-home',
standalone: true,
imports: [],
template: `<div>Home</div>`,
template: `
<div>Home</div>
`,
})
export default class HomeComponent {}

View File

@@ -2,8 +2,9 @@ import { AsyncPipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { map } from 'rxjs';
@Component({
selector: 'app-test',
selector: 'app-subscription',
standalone: true,
imports: [AsyncPipe],
template: `

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@@ -1,7 +1,7 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig).catch((err) =>
console.error(err)
console.error(err),
);

View 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": {}
}
]
}

View File

@@ -0,0 +1,13 @@
# Signal Input
> author: thomas-laforge
### Run Application
```bash
npx nx serve angular-signal-input
```
### Documentation and Instruction
Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/angular/43-signal-input/).

View File

@@ -0,0 +1,73 @@
{
"name": "angular-signal-input",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"prefix": "app",
"sourceRoot": "apps/angular/signal-input/src",
"tags": [],
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:application",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/angular/signal-input",
"index": "apps/angular/signal-input/src/index.html",
"browser": "apps/angular/signal-input/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "apps/angular/signal-input/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"apps/angular/signal-input/src/favicon.ico",
"apps/angular/signal-input/src/assets"
],
"styles": ["apps/angular/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": "angular-signal-input:build:production"
},
"development": {
"buildTarget": "angular-signal-input:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "angular-signal-input:build"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
}
}
}

View 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;
}

View File

@@ -0,0 +1,5 @@
import { ApplicationConfig } from '@angular/core';
export const appConfig: ApplicationConfig = {
providers: [],
};

View 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);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View 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>

View 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),
);

View 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 */

View 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: [],
};

View 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"]
}

View File

@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"include": ["src/**/*.ts"],
"compilerOptions": {
"types": []
}
}

View 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
}
}

View File

@@ -11,7 +11,7 @@ import { TextComponent } from './text.component';
<static-text></static-text>
<static-text type="error"></static-text>
<static-text type="warning"></static-text>
<text [font]="15" color="blue">This a a blue text</text>
<text [font]="15" color="blue">This is a blue text</text>
`,
})
export class PageComponent {}

View File

@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View 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": {}
}
]
}

View File

@@ -0,0 +1,13 @@
# Control Value Accessor
> author: stanislav-gavrilov
### Run Application
```bash
npx nx serve forms-control-value-accessor
```
### Documentation and Instruction
Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/forms/41-control-value-accessor/).

View File

@@ -0,0 +1,22 @@
/* eslint-disable */
export default {
displayName: 'forms-control-value-accessor',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/subscription-setup.ts'],
coverageDirectory: '../../../coverage/apps/forms/control-value-accessor',
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',
],
};

View File

@@ -0,0 +1,86 @@
{
"name": "forms-control-value-accessor",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"prefix": "app",
"sourceRoot": "apps/forms/control-value-accessor/src",
"tags": [],
"targets": {
"build": {
"executor": "@angular-devkit/build-angular:application",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/forms/control-value-accessor",
"index": "apps/forms/control-value-accessor/src/index.html",
"browser": "apps/forms/control-value-accessor/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "apps/forms/control-value-accessor/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
"apps/forms/control-value-accessor/src/favicon.ico",
"apps/forms/control-value-accessor/src/assets"
],
"styles": ["apps/forms/control-value-accessor/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": "forms-control-value-accessor:build:production"
},
"development": {
"buildTarget": "forms-control-value-accessor:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"executor": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "forms-control-value-accessor:build"
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": [
"apps/forms/control-value-accessor/**/*.ts",
"apps/forms/control-value-accessor/**/*.html"
]
}
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/forms/control-value-accessor/jest.config.ts"
}
}
}
}

View File

@@ -0,0 +1,16 @@
import { Component } from '@angular/core';
import { FeedbackFormComponent } from './feedback-form/feedback-form.component';
@Component({
standalone: true,
imports: [FeedbackFormComponent],
selector: 'app-root',
template: `
<app-feedback-form (feedBackSubmit)="apiCall($event)"></app-feedback-form>
`,
})
export class AppComponent {
apiCall(event: Record<string, string | null>): void {
console.log(event);
}
}

View File

@@ -0,0 +1,29 @@
<form
[formGroup]="feedbackForm"
class="feedback-form"
(ngSubmit)="submitForm()">
<legend class="feedback-form-title">Tell us what you think</legend>
<input
class="feedback-form-control"
[formControl]="feedbackForm.controls.name"
placeholder="Name"
type="text" />
<input
class="feedback-form-control"
[formControl]="feedbackForm.controls.email"
placeholder="Email"
type="email" />
<app-rating-control (ratingUpdated)="rating = $event"></app-rating-control>
<textarea
class="feedback-form-control"
[formControl]="feedbackForm.controls.comment"
placeholder="Сomment text"></textarea>
<button
class="feedback-form-submit"
type="submit"
[disabled]="
!feedbackForm.touched || rating === null || feedbackForm.invalid
">
Submit
</button>
</form>

View File

@@ -0,0 +1,50 @@
* {
box-sizing: border-box;
}
:host {
display: block;
padding: 20px;
}
.feedback-form {
display: flex;
flex-direction: column;
width: 500px;
padding: 20px;
border: 1px solid #000000;
}
.feedback-form-title {
margin-bottom: 20px;
font-size: 24px;
}
.feedback-form-control {
max-height: 200px;
margin-bottom: 20px;
padding: 12px 12px 12px 20px;
border-radius: 0;
background-color: #fbfbfb;
color: #3c3c3c;
font-size: 18px;
&:focus {
padding: 10px 10px 10px 18px;
border: 2px solid #054ada;
outline: none;
background: #fff;
}
}
.feedback-form-submit {
padding: 10px;
background-color: #054ada;
color: #ffffff;
font-size: 18px;
&[disabled] {
background-color: #cccccc;
color: #ffffff;
}
}

View File

@@ -0,0 +1,42 @@
import { Component, EventEmitter, Output } from '@angular/core';
import {
FormControl,
FormGroup,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
import { RatingControlComponent } from '../rating-control/rating-control.component';
@Component({
standalone: true,
imports: [RatingControlComponent, ReactiveFormsModule],
selector: 'app-feedback-form',
templateUrl: 'feedback-form.component.html',
styleUrls: ['feedback-form.component.scss'],
})
export class FeedbackFormComponent {
@Output()
readonly feedBackSubmit: EventEmitter<Record<string, string | null>> =
new EventEmitter<Record<string, string | null>>();
readonly feedbackForm = new FormGroup({
name: new FormControl('', {
validators: Validators.required,
}),
email: new FormControl('', {
validators: Validators.required,
}),
comment: new FormControl(),
});
rating: string | null = null;
submitForm(): void {
this.feedBackSubmit.emit({
...this.feedbackForm.value,
rating: this.rating,
});
this.feedbackForm.reset();
}
}

View File

@@ -0,0 +1,16 @@
<svg width="0" height="0" xmlns="http://www.w3.org/2000/svg">
<symbol id="star" width="50" height="50" xmlns="http://www.w3.org/2000/svg">
<path
d="M31.77 11.857H19.74L15.99.5l-3.782 11.357H0l9.885 6.903-3.692 11.21 9.736-7.05 9.796 6.962-3.722-11.18 9.766-6.845z" />
</symbol>
</svg>
<div class="rating">
@for (item of [].constructor(5); track item) {
<svg
class="star"
[class.star-active]="isStarActive($index, value)"
(click)="setRating($index)">
<use xlink:href="#star"></use>
</svg>
}
</div>

After

Width:  |  Height:  |  Size: 547 B

View File

@@ -0,0 +1,26 @@
.rating {
display: flex;
justify-content: center;
padding: 0 10px;
&:hover {
.star {
fill: #ffd055;
}
}
}
.star {
width: 50px;
height: 50px;
fill: #cccccc;
cursor: pointer;
&:hover ~ .star {
fill: #d8d8d8;
}
}
.star-active {
fill: #ffd055 !important;
}

View File

@@ -0,0 +1,23 @@
import { Component, EventEmitter, Output } from '@angular/core';
@Component({
standalone: true,
selector: 'app-rating-control',
templateUrl: 'rating-control.component.html',
styleUrls: ['rating-control.component.scss'],
})
export class RatingControlComponent {
@Output()
readonly ratingUpdated: EventEmitter<string> = new EventEmitter<string>();
value: number | null = null;
setRating(index: number): void {
this.value = index + 1;
this.ratingUpdated.emit(`${this.value}`);
}
isStarActive(index: number, value: number | null): boolean {
return value ? index < value : false;
}
}

Some files were not shown because too many files have changed in this diff Show More