Challenge 56 multi step reactive form and signals (#1030)

feat: challenge 55 form and signal
This commit is contained in:
Laforge Thomas
2024-07-08 17:11:41 +02:00
committed by GitHub
parent 45353aa55a
commit ddb5036af0
26 changed files with 505 additions and 17 deletions

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 @@
# forms and signal
> author: thomas-laforge
### Run Application
```bash
npx nx serve signal-forms-and-signal
```
### Documentation and Instruction
Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/signal/56-forms-and-signal/).

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,18 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
standalone: true,
imports: [RouterOutlet],
selector: 'app-root',
template: `
<h1 class="text-3xl">Shop</h1>
<div class="w-[500px] ">
<router-outlet />
</div>
`,
host: {
class: 'w-full flex justify-center flex-col items-center p-4 gap-10',
},
})
export class AppComponent {}

View File

@@ -0,0 +1,10 @@
import { ApplicationConfig, provideZoneChangeDetection } from '@angular/core';
import { provideRouter, withComponentInputBinding } from '@angular/router';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes, withComponentInputBinding()),
],
};

View File

@@ -0,0 +1,29 @@
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
redirectTo: 'dashboard',
pathMatch: 'full',
},
{
path: 'dashboard',
loadComponent: () => import('./dashboard.component'),
},
{
path: 'order',
loadComponent: () => import('./order.component'),
},
{
path: 'checkout',
loadComponent: () => import('./checkout.component'),
},
{
path: 'payment',
loadComponent: () => import('./payment.component'),
},
{
path: '**',
redirectTo: 'dashboard',
},
];

View File

@@ -0,0 +1,56 @@
import {
ChangeDetectionStrategy,
Component,
computed,
input,
} from '@angular/core';
import { RouterLink } from '@angular/router';
import { products } from './products';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [RouterLink],
template: `
<h2 class="mb-1 w-full bg-gray-400 p-2 text-white">Checkout</h2>
<button
routerLink="/order"
queryParamsHandling="merge"
class="mb-5 text-blue-400">
< back to order
</button>
<section class="mb-5 flex justify-between">
<div class="font-bold">Your order:</div>
<div>
{{ quantity() }} x {{ product()?.name }}: {{ product()?.price }}€
</div>
</section>
<div>Billing Information</div>
<div>...</div>
<div>...</div>
<div>...</div>
<div>...</div>
<div>...</div>
<button
routerLink="/payment"
[queryParams]="{ quantity: quantity() }"
queryParamsHandling="merge"
class="w-full rounded-full border bg-blue-500 p-2 text-white">
Pay
</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class DashboardComponent {
quantity = input(1);
productId = input('1');
product = computed(() =>
products.find((product) => product.id === this.productId()),
);
totalWithVAT = computed(
() => this.quantity() * (this.product()?.price ?? 0) * 1.21,
);
}

View File

@@ -0,0 +1,31 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterLink } from '@angular/router';
import { products } from './products';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [RouterLink],
template: `
<h2 class="mb-5 w-full bg-gray-400 p-2 text-white">List of Products</h2>
<ul class="w-full *:border-b *:border-l *:border-r *:p-4">
@for (product of products; track product.id) {
<li [class.border-t]="$first">
<div class="flex w-full justify-between">
{{ product.name }} ({{ product.price }}€)
<button
class="w-20 rounded-full border bg-blue-500 p-2 text-white"
routerLink="/order"
[queryParams]="{ productId: product.id }">
Buy
</button>
</div>
</li>
}
</ul>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class DashboardComponent {
products = products;
}

View File

@@ -0,0 +1,72 @@
import {
ChangeDetectionStrategy,
Component,
computed,
input,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { RouterLink } from '@angular/router';
import { products } from './products';
@Component({
selector: 'app-order',
standalone: true,
imports: [RouterLink, ReactiveFormsModule],
template: `
<h2 class="mb-5 w-full bg-gray-400 p-2 text-white">Order</h2>
<section class="flex flex-col gap-5">
<form class="flex items-center justify-between gap-5" [formGroup]="form">
<label for="countries" class="mb-2 block text-nowrap text-gray-900">
Select a quantity
</label>
<select
formControlName="quantity"
id="countries"
class="block w-32 rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
<option value="4">4</option>
<option value="5">5</option>
</select>
</form>
<div class="flex justify-between">
<div>SubTotal</div>
<div>{{ totalWihoutVat() }} €</div>
</div>
<div class="flex justify-between">
<div>VAT (21%)</div>
<div>{{ vat() }} €</div>
</div>
<div class="flex justify-between">
<div>Total</div>
<div>{{ total() }} €</div>
</div>
<button
routerLink="/checkout"
[queryParams]="{ quantity: quantity() }"
queryParamsHandling="merge"
class="w-full rounded-full border bg-blue-500 p-2 text-white">
Checkout
</button>
</section>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class OrderComponent {
form = new FormGroup({
quantity: new FormControl(1, { nonNullable: true }),
});
productId = input('1');
price = computed(
() => products.find((p) => p.id === this.productId())?.price ?? 0,
);
quantity = toSignal(this.form.controls.quantity.valueChanges, {
initialValue: this.form.getRawValue().quantity,
});
totalWihoutVat = computed(() => Number(this.price()) * this.quantity());
vat = computed(() => this.totalWihoutVat() * 0.21);
total = computed(() => this.totalWihoutVat() + this.vat());
}

View File

@@ -0,0 +1,19 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-dashboard',
standalone: true,
imports: [RouterLink],
template: `
<h2 class="mb-1 w-full bg-green-700 p-2 text-white">Payment Success</h2>
<button
routerLink="/dashboard"
class="w-full rounded-full border bg-blue-500 p-2 text-white">
Back to Shop
</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export default class DashboardComponent {}

View File

@@ -0,0 +1,17 @@
export const products = [
{
id: '1',
name: 'Computer',
price: 2000,
},
{
id: '2',
name: 'Mouse',
price: 40,
},
{
id: '3',
name: 'Keyboard',
price: 80,
},
];

View File

@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>signal-forms-and-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>

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,6 @@
{
"extends": "./tsconfig.json",
"include": ["src/**/*.ts"],
"compilerOptions": {},
"exclude": ["jest.config.ts", "src/**/*.test.ts", "src/**/*.spec.ts"]
}

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.editor.json"
},
{
"path": "./tsconfig.app.json"
}
],
"extends": "../../../tsconfig.base.json",
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"strictTemplates": true
}
}

View File

@@ -1,6 +1,6 @@
{ {
"total": 55, "total": 56,
"🟢": 21, "🟢": 21,
"🟠": 123, "🟠": 123,
"🔴": 210 "🔴": 211
} }

View File

@@ -0,0 +1,28 @@
---
title: 🔴 forms and signal
description: Challenge 56 is about working with reactive forms and signals
author: thomas-laforge
contributors:
- tomalaforge
challengeNumber: 56
command: signal-forms-and-signal
sidebar:
order: 211
badge: New
---
## Information
We are working within a large e-commerce codebase that utilizes a substantial number of forms. The team predominantly uses reactive forms, and since the release of signals, we have been integrating them extensively.
The current feature in development is a multi-step form process. The steps include: selecting a product, choosing the quantity, and finally proceeding to the checkout step to complete the billing details. However, an issue has been identified: when a user navigates back from the checkout step to the quantity step, the previously selected quantity is not retained. This needs to be fixed.
## Challenge Statement
The objective of this challenge is to make sure that the selected quantity is preserved when navigating back from the checkout step to the quantity step.
## Constraints
The solution must use reactive forms and signals.
Additionally, as an optional side challenge, you may refactor the code to use template-driven forms.

View File

@@ -13,7 +13,7 @@ hero:
icon: right-arrow icon: right-arrow
variant: primary variant: primary
- text: Ir al Desafío más reciente - text: Ir al Desafío más reciente
link: /es/challenges/angular/55-back-button-navigation/ link: /es/challenges/signal/56-forms-and-signal/
icon: rocket icon: rocket
- text: Dar una estrella - text: Dar una estrella
link: https://github.com/tomalaforge/angular-challenges link: https://github.com/tomalaforge/angular-challenges
@@ -26,8 +26,8 @@ import MyIcon from '../../../components/MyIcon.astro';
import SubscriptionForm from '../../../components/SubscriptionForm.astro'; import SubscriptionForm from '../../../components/SubscriptionForm.astro';
<CardGrid> <CardGrid>
<Card title="55 Desafíos"> <Card title="56 Desafíos">
Este repositorio contiene 55 Desafíos relacionados con <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> y <b>Typescript</b>. Este repositorio contiene 56 Desafíos relacionados con <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> y <b>Typescript</b>.
Estos desafíos se resuelven en torno a problemas de la vida real o características específicas para mejorar tus habilidades. Estos desafíos se resuelven en torno a problemas de la vida real o características específicas para mejorar tus habilidades.
</Card> </Card>

View File

@@ -13,7 +13,7 @@ hero:
icon: right-arrow icon: right-arrow
variant: primary variant: primary
- text: Aller au dernier Challenge - text: Aller au dernier Challenge
link: /fr/challenges/angular/55-back-button-navigation/ link: /fr/challenges/signal/56-forms-and-signal/
icon: rocket icon: rocket
- text: Donne une étoile - text: Donne une étoile
link: https://github.com/tomalaforge/angular-challenges link: https://github.com/tomalaforge/angular-challenges
@@ -26,8 +26,8 @@ import MyIcon from '../../../components/MyIcon.astro';
import SubscriptionForm from '../../../components/SubscriptionForm.astro'; import SubscriptionForm from '../../../components/SubscriptionForm.astro';
<CardGrid> <CardGrid>
<Card title="55 Défis"> <Card title="56 Défis">
Ce répertoire rassemble 55 Défis liés à <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> et <b>Typescript</b>. Ces défis portent sur des problèmes réels ou des fonctionnalités spécifiques pour améliorer vos compétences. Ce répertoire rassemble 56 Défis liés à <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> et <b>Typescript</b>. Ces défis portent sur des problèmes réels ou des fonctionnalités spécifiques pour améliorer vos compétences.
</Card> </Card>
<Card title="Subscribe to get notify of latest challenges"> <Card title="Subscribe to get notify of latest challenges">

View File

@@ -13,7 +13,7 @@ hero:
icon: right-arrow icon: right-arrow
variant: primary variant: primary
- text: Go to the latest Challenge - text: Go to the latest Challenge
link: /challenges/angular/55-back-button-navigation/ link: /challenges/signal/56-forms-and-signal/
icon: rocket icon: rocket
- text: Give a star - text: Give a star
link: https://github.com/tomalaforge/angular-challenges link: https://github.com/tomalaforge/angular-challenges
@@ -27,8 +27,8 @@ import MyIcon from '../../components/MyIcon.astro';
import SubscriptionForm from '../../components/SubscriptionForm.astro'; import SubscriptionForm from '../../components/SubscriptionForm.astro';
<CardGrid> <CardGrid>
<Card title="53 Challenges"> <Card title="56 Challenges">
This repository gathers 53 Challenges related to <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> and <b>Typescript</b>. This repository gathers 56 Challenges related to <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> and <b>Typescript</b>.
These challenges resolve around real-life issues or specific features to elevate your skills. These challenges resolve around real-life issues or specific features to elevate your skills.
</Card> </Card>

View File

@@ -13,7 +13,7 @@ hero:
icon: right-arrow icon: right-arrow
variant: primary variant: primary
- text: Ir para o desafio mais recente - text: Ir para o desafio mais recente
link: /pt/challenges/angular/55-back-button-navigation/ link: /pt/challenges/signal/56-forms-and-signal/
icon: rocket icon: rocket
- text: Dar uma estrela - text: Dar uma estrela
link: https://github.com/tomalaforge/angular-challenges link: https://github.com/tomalaforge/angular-challenges
@@ -26,8 +26,8 @@ import MyIcon from '../../../components/MyIcon.astro';
import SubscriptionForm from '../../../components/SubscriptionForm.astro'; import SubscriptionForm from '../../../components/SubscriptionForm.astro';
<CardGrid> <CardGrid>
<Card title="55 Desafios"> <Card title="56 Desafios">
Este repositório possui 55 Desafios relacionados a <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, Este repositório possui 56 Desafios relacionados a <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>,
<b>Ngrx</b> e <b>Typescript</b>. <b>Ngrx</b> e <b>Typescript</b>.
Esses desafios são voltados para problemas reais ou funcionalidades específicas afim de Esses desafios são voltados para problemas reais ou funcionalidades específicas afim de
melhorar suas habilidades. melhorar suas habilidades.

View File

@@ -13,7 +13,7 @@ hero:
icon: right-arrow icon: right-arrow
variant: primary variant: primary
- text: Перейти к последней задаче - text: Перейти к последней задаче
link: /ru/challenges/angular/55-back-button-navigation/ link: /ru/challenges/signal/56-forms-and-signal/
icon: rocket icon: rocket
- text: Добавить звезду - text: Добавить звезду
link: https://github.com/tomalaforge/angular-challenges link: https://github.com/tomalaforge/angular-challenges
@@ -26,8 +26,8 @@ import MyIcon from '../../../components/MyIcon.astro';
import SubscriptionForm from '../../../components/SubscriptionForm.astro'; import SubscriptionForm from '../../../components/SubscriptionForm.astro';
<CardGrid> <CardGrid>
<Card title="54 испытаний"> <Card title="56 испытаний">
Этот репозиторий содержит 54 испытаний, связанных с <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> и <b>Typescript</b>. Этот репозиторий содержит 56 испытаний, связанных с <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> и <b>Typescript</b>.
Испытания основаны на реальных задачах или инструментах для того, чтобы прокачать вас. Испытания основаны на реальных задачах или инструментах для того, чтобы прокачать вас.
</Card> </Card>