feat: challenge 54 pipe obs signal (#903)

This commit is contained in:
Laforge Thomas
2024-06-17 20:51:27 +02:00
committed by GitHub
parent e6fd5468e2
commit d49cbc0de8
26 changed files with 417 additions and 25 deletions

View File

@@ -24,7 +24,7 @@ If you would like to propose a challenge, this project is open source, so feel f
## Challenges ## Challenges
Check [all 53 challenges](https://angular-challenges.vercel.app/) Check [all 54 challenges](https://angular-challenges.vercel.app/)
## Contributors ✨ ## Contributors ✨

View File

@@ -0,0 +1,19 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts"],
"extends": [
"plugin:@nx/angular",
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {}
},
{
"files": ["*.html"],
"extends": ["plugin:@nx/angular-template"],
"rules": {}
}
]
}

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
import { TableComponent } from '@angular-challenges/shared/ui';
import { AsyncPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { CurrencyPipe } from './currency.pipe';
import { ProductRowComponent } from './product-row.component';
import { products } from './product.model';
@Component({
standalone: true,
imports: [AsyncPipe, CurrencyPipe, TableComponent, ProductRowComponent],
selector: 'app-root',
template: `
<table [items]="products">
<ng-template #header>
<tr>
@for (col of displayedColumns; track col) {
<th>{{ col }}</th>
}
</tr>
</ng-template>
<ng-template #body let-product>
<tr product-row [product]="product"></tr>
</ng-template>
</table>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
products = products;
displayedColumns = ['name', 'priceA', 'priceB', 'priceC'];
}

View File

@@ -0,0 +1,15 @@
import { inject, Pipe, PipeTransform } from '@angular/core';
import { map, Observable } from 'rxjs';
import { CurrencyService } from './currency.service';
@Pipe({
name: 'currency',
standalone: true,
})
export class CurrencyPipe implements PipeTransform {
currencyService = inject(CurrencyService);
transform(price: number): Observable<string> {
return this.currencyService.symbol$.pipe(map((s) => `${price}${s}`));
}
}

View File

@@ -0,0 +1,30 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, map } from 'rxjs';
export interface Currency {
name: string;
code: string;
symbol: string;
}
export const currency: Currency[] = [
{ name: 'Euro', code: 'EUR', symbol: '€' },
{ name: 'Dollar US', code: 'USD', symbol: 'US$' },
{ name: 'Dollar Autralien', code: 'AUD', symbol: 'AU$' },
{ name: 'Livre Sterling', code: 'GBP', symbol: '£' },
{ name: 'Dollar Canadien', code: 'CAD', symbol: 'CAD' },
];
@Injectable()
export class CurrencyService {
private code = new BehaviorSubject('EUR');
readonly code$ = this.code.asObservable();
readonly symbol$ = this.code$.pipe(
map((code) => currency.find((c) => c.code === code)?.symbol ?? code),
);
public updateCode(code: string) {
this.code.next(code);
}
}

View File

@@ -0,0 +1,34 @@
import { AsyncPipe } from '@angular/common';
import {
ChangeDetectionStrategy,
Component,
inject,
Input,
} from '@angular/core';
import { CurrencyPipe } from './currency.pipe';
import { CurrencyService } from './currency.service';
import { Product } from './product.model';
@Component({
standalone: true,
selector: 'tr[product-row]',
template: `
<td>{{ productInfo.name }}</td>
<td>{{ productInfo.priceA | currency | async }}</td>
<td>{{ productInfo.priceB | currency | async }}</td>
<td>{{ productInfo.priceC | currency | async }}</td>
`,
imports: [AsyncPipe, CurrencyPipe],
providers: [CurrencyService],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ProductRowComponent {
protected productInfo!: Product;
@Input({ required: true }) set product(product: Product) {
this.currencyService.updateCode(product.currencyCode);
this.productInfo = product;
}
currencyService = inject(CurrencyService);
}

View File

@@ -0,0 +1,55 @@
export interface Product {
name: string;
priceA: number;
priceB: number;
priceC: number;
currencyCode: string;
}
export const products: Product[] = [
{
name: 'bike',
priceA: 1000,
priceB: 2000,
priceC: 2200,
currencyCode: 'USD',
},
{ name: 'tent', priceA: 112, priceB: 120, priceC: 41, currencyCode: 'EUR' },
{
name: 'sofa',
priceA: 500,
priceB: 422,
priceC: 5000,
currencyCode: 'EUR',
},
{
name: 'watch',
priceA: 50,
priceB: 130,
priceC: 150,
currencyCode: 'AUD',
},
{
name: 'computer',
priceA: 1000,
priceB: 2200,
priceC: 3500,
currencyCode: 'GBP',
},
{ name: 'mug', priceA: 10, priceB: 15, priceC: 20, currencyCode: 'EUR' },
{
name: 'headset',
priceA: 100,
priceB: 150,
priceC: 220,
currencyCode: 'CAD',
},
{ name: 'cable', priceA: 5, priceB: 10, priceC: 15, currencyCode: 'EUR' },
{
name: 'table',
priceA: 100,
priceB: 20,
priceC: 500,
currencyCode: 'EUR',
},
];

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>signal-pipe-observable-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>

View File

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

View File

@@ -0,0 +1,26 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* You can add global styles to this file, and also import other style files */
table {
width: 100%;
}
table thead > tr > th {
text-align: left;
padding: 1rem 1rem;
border: 1px solid #dee2e6;
border-width: 0 0 1px 0;
font-weight: 700;
color: #343a40;
background: #f8f9fa;
transition: box-shadow 0.2s;
}
table tbody > tr > td {
text-align: left;
border: 1px solid #dee2e6;
border-width: 0 0 1px 0;
padding: 1rem 1rem;
}

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": 53, "total": 54,
"🟢": 21, "🟢": 21,
"🟠": 122, "🟠": 122,
"🔴": 209 "🔴": 210
} }

View File

@@ -9,7 +9,6 @@ challengeNumber: 53
command: signal-big-signal-performance command: signal-big-signal-performance
sidebar: sidebar:
order: 122 order: 122
badge: New
--- ---
## Information ## Information

View File

@@ -0,0 +1,22 @@
---
title: 🔴 Pipe Observable to Signal
description: Challenge 54 is about refactoring an application using observable to signals
author: thomas-laforge
contributors:
- tomalaforge
challengeNumber: 54
command: signal-pipe-observable-to-signal
sidebar:
order: 210
badge: New
---
## Information
We have a legacy application that is using observables to store a state. Signals are a very good fit for that.
## Statement
So the goal of this challenge is to refactor the following application to be a fully signal based application.
Be careful along the way, everything might not work as you wish.

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/signal/53-big-signal-performance/ link: /es/challenges/signal/54-pipe-observable-to-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="53 Desafíos"> <Card title="54 Desafíos">
Este repositorio contiene 53 Desafíos relacionados con <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> y <b>Typescript</b>. Este repositorio contiene 54 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/signal/53-big-signal-performance/ link: /fr/challenges/signal/54-pipe-observable-to-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="53 Défis"> <Card title="54 Défis">
Ce répertoire rassemble 53 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 54 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: Ir para o desafio mais recente - text: Ir para o desafio mais recente
link: /pt/challenges/signal/53-big-signal-performance/ link: /pt/challenges/signal/54-pipe-observable-to-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="53 Desafios"> <Card title="54 Desafios">
Este repositório possui 53 Desafios relacionados a <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, Este repositório possui 54 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/signal/53-big-signal-performance/ link: /ru/challenges/signal/54-pipe-observable-to-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="53 испытаний"> <Card title="54 испытаний">
Этот репозиторий содержит 53 испытаний, связанных с <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> и <b>Typescript</b>. Этот репозиторий содержит 54 испытаний, связанных с <b>Angular</b>, <b>Nx</b>, <b>RxJS</b>, <b>Ngrx</b> и <b>Typescript</b>.
Испытания основаны на реальных задачах или инструментах для того, чтобы прокачать вас. Испытания основаны на реальных задачах или инструментах для того, чтобы прокачать вас.
</Card> </Card>

View File

@@ -1,21 +1,20 @@
import { NgFor, NgTemplateOutlet } from '@angular/common'; import { NgTemplateOutlet } from '@angular/common';
import { Component, ContentChild, Input, TemplateRef } from '@angular/core'; import { Component, ContentChild, Input, TemplateRef } from '@angular/core';
@Component({ @Component({
selector: 'table', selector: 'table',
standalone: true, standalone: true,
imports: [NgTemplateOutlet, NgFor], imports: [NgTemplateOutlet],
template: ` template: `
<thead> <thead>
<ng-container *ngTemplateOutlet="headerTemplate"></ng-container> <ng-container *ngTemplateOutlet="headerTemplate"></ng-container>
</thead> </thead>
<tbody *ngFor="let item of items"> @for (item of items; track $index) {
<tbody>
<ng-container <ng-container
*ngTemplateOutlet=" *ngTemplateOutlet="bodyTemplate; context: { $implicit: item }" />
bodyTemplate;
context: { $implicit: item }
"></ng-container>
</tbody> </tbody>
}
`, `,
}) })
export class TableComponent<T> { export class TableComponent<T> {