Dashboard -- Tailwind + Angular
Un dashboard es una herramienta de gestión de la información que monitoriza, analiza y muestra de manera visual los indicadores clave de desempeño (KPI), métricas y datos fundamentales para hacer un seguimiento del estado de una empresa, un departamento, una campaña o un proceso específico. https://tailwindcss.com/docs/guides/angular
videotutorial
- Creación de archivos y directorios https://youtu.be/8djEbTdB_ZQ
- Angular: - Configuración de rutas https://youtu.be/C0D4xhhrf_U
- Angular:- Configuración del dashboard | Tailwind https://youtu.be/S8hdW6rz4jY
- Angular:- Configurar alias de importaciones en TypeScript https://youtu.be/ijhimFUSApc
- Angular: - @for & @if https://youtu.be/Pc7Ep2AkO1A
- Angular: - Control Flow | @if https://youtu.be/yWxp1VCKysM
Tailwind en Angular
// Crear proyecto App = my-dashboard
ng new my-dashboard
// Entrando y abriendo el proyecto en con editor: Visual Studio Code
cd my-dashboard
code .
Install Tailwind CSS with Angular: https://tailwindcss.com/docs/guides/angular
Instalar tailwindcss a través de npm, y luego ejecute el comando init para generar un archivo tailwind.config.js .
// Instalando Tailwind CSS
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init
Configure las rutas de su plantilla: Agregue las rutas a todos sus archivos de plantilla en su archivo: tailwind.config.js
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/**/*.{html,ts}",
],
theme: {
extend: {},
},
plugins: [],
}
Añade las directivas Tailwind a tu CSS Añade el @tailwind directivas para cada una de las capas de Tailwind en su archivo ./src/styles.css.
// estilos.css
@tailwind base;
@tailwind components;
@tailwind utilities;
Comienza a usar Tailwind en tu proyecto
<!-- app.component.html -->
<h1 class="text-3xl font-bold underline">
Hello world! with Tailwind
</h1>
<router-outlet/>
Comience su proceso de construcción Ejecute su proceso de construcción con ng serve.
// Iniciar el servidor
ng serve
Angular: Creación de archivos y directorios
Creación de archivos y directorios https://youtu.be/8djEbTdB_ZQ
// ng g c dashboard --skip-selector --inline-template --inline-style --skip-tests
// creando componentes de archivos y directorios
ng g c --inline-style --skip-tests dashboard
ng g c --inline-style --skip-tests interfaces
ng g interface interfaces/req-response
ng g c --inline-style --skip-tests services
ng g service services/users
ng g c --inline-style --skip-tests shared
Creando los directorios y subdirectorios del dashboard
// Creando los directorios del dashboard
ng g c --skip-tests dashboard/pages
// Creando sub-directorios del dashboard/pages
ng g c --inline-style --skip-tests dashboard/pages/change-detection
ng g c --inline-style --skip-tests dashboard/pages/control-flow
ng g c --inline-style --skip-tests dashboard/pages/defer-options
ng g c --inline-style --skip-tests dashboard/pages/defer-views
// usuario admin
ng g c --inline-style --skip-tests dashboard/pages/user
// usuario publico
ng g c --inline-style --skip-tests dashboard/pages/users
ng g c --inline-style --skip-tests dashboard/pages/view-transition
Creando los directorios y subdirectorios de shared
ng g c --skip-tests shared/sidemenu
ng g c --inline-style --skip-tests shared/title
ng g c --inline-template --inline-style --skip-tests shared/heavy-loaders
ng g c --inline-template --inline-style --skip-tests shared/heavy-loaders/heavy-loaders-fast
ng g c --inline-template --inline-style --skip-tests shared/heavy-loaders/heavy-loaders-slow
ng g c --inline-template --inline-style --skip-tests shared/heavy-loaders/users-loader
Angular: Configuración de rutas
Angular: - Configuración de rutas https://youtu.be/C0D4xhhrf_U
app.routes.ts
Declarando la ruta PADRE por defecto = default: carga diferida (lazy loading) loadComponent: () => import( './dashboard/dashboard.component'),
Archivo: // app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'dashboard',
// Declarando la ruta padre por defecto = default
loadComponent: () => import( './dashboard/dashboard.component'),
children: [
{
path: 'change—detection',
title: 'Change Detection',
// Declarando la ruta hijo por defecto = default
loadComponent: () => import('./dashboard/pages/change-detection/change-detection.component'),
},
]
},
{
path: '',
redirectTo:' /dashboard',
pathMatch: 'full'
}
];
Archivo: dashboard.component.ts
Declarando la ruta PADRE por defecto = default: carga diferida (lazy loading) default class DashboardComponent
// dashboard.component.ts
import { Component } from '@angular/core';
// Agregando el RouterModule, para reconocer las rutas hijas
import { RouterModule } from '@angular/router';
// Agregando el componente para el menu
import { SidemenuComponent } from '../shared/sidemenu/sidemenu.component';
@Component({
selector: 'app-dashboard',
standalone: true,
// Declarando RouterModule, para reconocer las rutas hijas
imports: [RouterModule, SidemenuComponent],
templateUrl: './dashboard.component.html',
styles: ``
})
// Declarando la ruta padre por defecto = default
export default class DashboardComponent {
}
Archivo: change-detection.component.ts
Declarando la ruta HIJO = children: []
por defecto (default): carga diferida (lazy loading) default class ChangeDetectionComponent
// change-detection.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-change-detection',
standalone: true,
imports: [],
templateUrl: './change-detection.component.html',
styles: ``
})
// Declarando la ruta por defecto = default
export default class ChangeDetectionComponent {
}
archivo: dashboard.component.html
<!-- dashboard.component.html -->
<h3>dashboard</h3>
<router-outlet/>
Agregando el resto de rutas hijas y declarandolas por defecto para la carga diferida
// app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: 'dashboard',
// Declarando la ruta padre por defecto = default
loadComponent: () => import( './dashboard/dashboard.component'),
children: [
{
path: 'change—detection',
title: 'Change Detection',
// Declarando la ruta hijo por defecto = default
loadComponent: () => import('./dashboard/pages/change-detection/change-detection.component'),
},
{
path: 'control-flow',
title: 'Control Flow',
// Declarando la ruta hijo por defecto = default
loadComponent: () => import('./dashboard/pages/control-flow/control-flow.component'),
},
{
path: 'defer-options',
title: 'Defer Options',
// Declarando la ruta hijo por defecto = default
loadComponent: () => import('./dashboard/pages/defer-options/defer-options.component'),
},
{
path: 'defer-views',
title: 'Defer Views',
// Declarando la ruta hijo por defecto = default
loadComponent: () => import('./dashboard/pages/defer-views/defer-views.component'),
},
{
path: 'user/:id',
title: 'User View',
// Declarando la ruta hijo por defecto = default
loadComponent: () => import('./dashboard/pages/user/user.component'),
},
{
path: 'user-list',
title: 'User List',
// Declarando la ruta hijo por defecto = default
loadComponent: () => import('./dashboard/pages/users/users.component'),
},
{
path: 'view-transition',
title: 'View Transition',
// Declarando la ruta hijo por defecto = default
loadComponent: () => import('./dashboard/pages/view-transition/view-transition.component'),
},
{
path: '',
redirectTo: './control-flow',
pathMatch:'full'
}
]
},
{
path: '',
redirectTo:' /dashboard',
pathMatch: 'full'
}
];
Angular: - Configurar alias "@shared/*"
Angular:- Configurar alias de importaciones en TypeScript https://youtu.be/ijhimFUSApc
Archivo: tsconfig.json
Configurar alias: "@shared/*"
, "@interfaces/*"
, "@services/*"
de importaciones en TypeScript: https://youtu.be/ijhimFUSApc
/* Archivo: tsconfig.json */
{
"compileOnSave": false,
"compilerOptions": {
/** Angular: - Configurar alias "@shared/*", "@interfaces/*", "@services/*" de importaciones en TypeScript */
/* Archivo: tsconfig.json */
"paths":{
"@shared/*": ["./src/app/shared/*"],
"@interfaces/*": ["./src/app/interfaces/*"],
"@services/*": ["./src/app/services/*"],
},
/* --- coninua el contenido del Archivo: tsconfig.json ----*/
}
Archivo: dashboard.component.ts
// dashboard.component.ts
import { Component } from '@angular/core';
// Agregando el RouterModule, para reconocer las rutas hijas
import { RouterModule } from '@angular/router';
/**---- Importando por alias: "@shared/*" = "../shared/" ----*/
import {SidemenuComponent} from '@shared/sidemenu/sidemenu.component';
// import { SidemenuComponent } from '../shared/sidemenu/sidemenu.component';
@Component({
selector: 'app-dashboard',
standalone: true,
// Declarando RouterModule, para reconocer las rutas hijas
imports: [RouterModule, SidemenuComponent],
templateUrl: './dashboard.component.html',
styles: ``
})
// Declarando la ruta padre por defecto = default
export default class DashboardComponent {
}
Configuración del dashboard
- Dashboard-navigation https://www.creative-tim.com/twcomponents/component/dashboard-navigation
- Angular:- Configuración del dashboard | Tailwind https://youtu.be/S8hdW6rz4jY
- Angular:- Configuración https://devtalles.com/files/angular-cheat-sheet-v2.pdf
- Angular: - @for & @if https://youtu.be/Pc7Ep2AkO1A
Modificando el Archivo: dashboard.component.html
<!-- dashboard.component.html -->
<div class="flex bg-slate-100 text-black overflow-y-scroll w-screen h-screen antialiased selection:bg-blue-600 selection:text-white">
<div class="flex relative w-screen">
<app-sidemenu/>
<div class="text-black px-2 mt-2 w-full">
<router-outlet/>
</div>
</div>
</div>
Modificando el archivo: sidemenu.component.ts
// sidemenu.component.ts
import { Component } from '@angular/core';
/** importando y Definiendo la dirección de las rutas */
import { routes } from '../../app.routes';
import { RouterLink, RouterLinkActive, RouterModule } from '@angular/router';
@Component({
selector: 'app-sidemenu',
standalone: true,
// Declarando que usaremos los modulos de Rutas
imports: [RouterModule, RouterLink, RouterLinkActive],
templateUrl: './sidemenu.component.html',
styleUrl: './sidemenu.component.css'
})
export class SidemenuComponent {
public menuItems = routes
.map(route => route.children ?? [])
.flat()
.filter(route => route && route.path )
.filter(route => !route.path?.includes(':'))
constructor(){
// const dashboardRoutes = routes
// .map(route => route.children ?? [])
// .flat()
// .filter(route => route && route.path )
// .filter(route => !route.path?.includes(':'))
// console.log(dashboardRoutes);
}
}
Modificando el archivo: <!-- sidemenu.component.html -->
<!-- sidemenu.component.html -->
<div id="menu" class="bg-gray-900 min-h-screen z-10 text-slate-300 w-64 left-0 h-screen overflow-y-scroll">
<div id="logo" class="my-4 px-6">
<h1 class="text-lg md:text-2xl font-bold text-white">Dash<span class="text-blue-500"> board </span>.</h1>
<p class="text-slate-300 text-sm font-bold"> Tailwind + Angular </p>
</div>
<div id="profile" class="px-6 py-10">
<p class="text-slate-500">Welcome back,</p>
<a href="#" class="inline-flex space-x-2 items-center">
<span>
<img class="rounded-full w-8 h-8" src="https://lh3.googleusercontent.com/a/ACg8ocLflXgu1xFxbsama0KSfh6benDjEvGelGZjVs4SkG66BusC1LM=s288-c-no" alt="">
</span>
<span class="text-sm md:text-base font-bold">
Milton Rodriguez
</span>
</a>
</div>
<div id="nav" class="w-full px-6">
@for (item of menuItems; track $index) {
<a [routerLink]="item.path"
routerLinkActive="bg-blue-800"
class="w-full px-2 inline-flex space-x-2 items-center border-b border-slate-700 py-3
hover:bg-white/5 transition ease-linear duration-150">
<div class="flex flex-col">
<span class="text-lg font-bold leading-5 text-white">{{item.title}}</span>
<span class="text-sm text-white/50 hidden md:block">{{item.path}}</span>
</div>
</a>
}
</div>
</div>
Contoles de flujo
- @if https://angular.dev/api/core/@if
- @For https://angular.dev/api/core/@for
- @Switch https://angular.dev/api/core/@switch
- [S28/L10]Angular: - Control Flow | @if https://youtu.be/yWxp1VCKysM
- [S28/L11] Angular: - Control Flow | @Switch https://youtu.be/N6GXtm7wO8w
- [S28/L12] Angular: - Control Flow | @For https://youtu.be/P6O5BHoX63Y
Archivo: control-flow.component.ts
// control-flow.component.ts
import { Component, signal } from '@angular/core';
type Grade = 'A'|'B'|'C'|'E';
@Component({
selector: 'app-control-flow',
standalone: true,
imports: [],
templateUrl: './control-flow.component.html',
styles: ``
})
// Declarando la ruta hijo por defecto = default
export default class ControlFlowComponent {
public showContent = signal(false);
public toggleContent(){
this.showContent.update(value => !value);
}
public grade = signal<Grade>('B')
public frameworks = signal(['angular','Vie', 'Svelte', 'Quik', 'React']);
}
Archivo: control-flow.component.html
<!-- control-flow.component.html -->
<h2 class="font-bold text-3xl mb-5">Controles de flujo</h2>
<section class="grid grid-cols-1 md:grid-cols-2 gap-3">
<!-- ngInf @if -->
<div class="bg-white rounded shadow p-10">
<h2 class="text-2xl font-bold mb-5"> if: {{showContent()}} </h2>
<button class="p-2 bg-blue-500 rounded text-white" (click)="toggleContent()" >
click me
</button>
@if (showContent()) {
<p>****** Es correcto ********</p>
}@else { <p> ---- Es falsa, mi gente---- </p> }
</div>
<!-- @switch -->
<div class="bg-white rounded shadow p-10">
<h2 class="text-3xl">{{ grade() }}</h2>
@switch (grade()) {
@case ('A') { <p>Mayor de 90%</p> }
@case ('B') { <p>81 a 90 % </p> }
@case ('C') { <p>70 a 80 % </p> }
@default { <p>Reprobado (menor de 70%)</p> }
}
</div>
</section>
<br>
<section class="grid grid-cols-1 md:grid-cols-2 gap-3">
<!-- ngFor @For -->
<div class="bg-white rounded shadow p-10">
<ul>
@for ( framework of frameworks(); track framework;
let i = $index, first = $first, last = $last, even = $even, odd = $odd, count = $count ) {
<!-- " && !first && !last " Significa que No es el primero, ni tampoco el ultimo -->
<!-- " first || last " Significa que es el primero ó el ultimo -->
<li [class]="{
'bg-red-100': even && !first && !last,
'bg-purple-100': odd && !first && !last,
'bg-blue-100': first || last,
}">
{{ i + 1 }} / {{ count }} - {{ framework }}
</li>
}
</ul>
</div>
<div class="bg-white rounded shadow p-10">
</div>
</section>
Change Detection = Detección de cambios
- @Input https://angular.dev/tutorials/learn-angular/8-input#
- OnPush https://angular.dev/best-practices/skipping-subtrees#using-onpush
- [S28/L13] Angular: - Required | @Input https://youtu.be/G9bghx5CBYw
- [S28/L14] Angular:- ChangeDetection | OnPush https://youtu.be/5ia54sIDTGA
Archivo: title.component.ts
// title.component.ts
import { CommonModule } from '@angular/common';
import { booleanAttribute, Component, Input } from '@angular/core';
@Component({
selector: 'app-title',
standalone: true,
imports: [CommonModule],
template: `
<h2 class="text-3xl mb-5"> {{ title }}</h2>
`,
styles: ``
})
export class TitleComponent {
@Input({ required:true }) title!:string;
@Input({ transform: booleanAttribute }) withShadow:boolean = false;
}
Archivo: control-flow.component.html
<!-- control-flow.component.html -->
<!-- <h2 class="font-bold text-3xl mb-5" >Controles de flujo</h2> -->
<app-title title= "Control Flow = Controles de flujo" withShadow />
<!-- Sigue el resto del contenido -->
Arcivo: control-flow.component.ts , solo se agregan e importa componente title.
// control-flow.component.ts
import { Component, signal } from '@angular/core';
import { TitleComponent } from '@shared/title/title.component';
@Component({
selector: 'app-control-flow',
standalone: true,
// se agrega el componente titulo
imports: [TitleComponent,],
templateUrl: './control-flow.component.html',
styles: ``
})
// Declarando la ruta hijo por defecto = default
export default class ControlFlowComponent {
// Mismo contenido de antes
}
Archivo: change-detection.component.ts
// change-detection.component.ts
import { CommonModule, JsonPipe } from '@angular/common';
import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';
import { TitleComponent } from '@shared/title/title.component';
@Component({
selector: 'app-change-detection',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, TitleComponent, JsonPipe],
template:`
<app-title [title]="currentFramework()" />
<pre> {{ frameworkAsSignal() | json }} </pre>
<pre> {{ frameworkAsProperty | json }} </pre>
`,
styles: ``
})
// Declarando la ruta hijo por defecto = default
export default class ChangeDetectionComponent {
public currentFramework = computed(
() => `Change detection - ${ this.frameworkAsSignal().name }`
);
public frameworkAsSignal = signal({
name: 'Angular', releaseDate: 2016,
});
public frameworkAsProperty = {
name: 'Angular Material', releaseDate: 2018,
};
// Cambiar el nombre a los 3 segundos de angular material
constructor(){
setTimeout(() => {
this.frameworkAsSignal.update( value => ({
... value, name: 'Bootstrap', releaseDate: 2020,
}))
// this.frameworkAsProperty.name = ' React ';
// this.frameworkAsProperty.releaseDate = 2014;
console.log('Hecho, el cambio de datos y propiedades, depues de 3 segundos');
}, 3000);
}
}
Defer Views = Aplazar vistas
- @defer https://angular.dev/api/core/@defer#mat-tab-content-105-0
- [S28/L15] Angular: - Defer Blocks https://youtu.be/633SsJjnjkQ
- [S28/L16] Angular: - Defer Triggers https://youtu.be/ZS8Hpdc-lPc
- [S28/L17] Angular: - Defer Triggers | Interacciones https://youtu.be/e_ESSR9lTKs
Archivo: heavy-loaders-slow.component.ts
// heavy-loaders-slow.component.ts
import { Component, input } from '@angular/core';
import {CommonModule} from '@angular/common';
@Component({
selector: 'app-heavy-loaders-slow',
standalone: true,
imports: [CommonModule, ],
template: `
<p class="text-2xl font-bold mt-2">heavy loaders slow, con Bloques de aplazamiento !</p>
<br>
<p> heavy (pesado) -loaders (cargadores) -slow (lentos) </p>
`,
styles: ``
})
export class HeavyLoadersSlowComponent {
constructor(){
console.log('HeaveLoader Component, agregado correctamente');
const start = Date.now();
// iniciar en 3000 milisegundos
while( Date.now() - start < 3000 ) { }
console.log('cargando, porfavor espere 3 segundos');
}
}
Archivo: defer-views.component.ts
// defer-views.component.ts
import { Component } from '@angular/core';
import { HeavyLoadersSlowComponent } from '@shared/heavy-loaders/heavy-loaders-slow/heavy-loaders-slow.component';
import { TitleComponent } from '@shared/title/title.component';
@Component({
selector: 'app-defer-views',
standalone: true,
imports: [HeavyLoadersSlowComponent, TitleComponent],
templateUrl: './defer-views.component.html',
styles: ``
})
// Declarando la ruta hijo por defecto = default
export default class DeferViewsComponent {
}
Archivo: defer-views.component.html
<!-- defer-views.component.html -->
<h3>defer-views! = Aplazar vistas </h3>
<app-title title="Defer Views / Blocs = Bloques de aplazamiento"> </app-title>
<section class="grid grid-cols-1">
@defer () {
<app-heavy-loaders-slow class="h-[100px] w-fullbg-blue-300 bg-green-300" />
}@placeholder {
<p class="h-[100px] w-full bg-gradient-to-r from-green-200 to-green-500 animate-pulse">
Cargando defer-views # 1 , Bloques de aplazamiento nº 1
</p>
}
@defer (on idle) {
<app-heavy-loaders-slow class="h-[100px] w-full bg-red-300" />
}@placeholder {
<p class="h-[100px] w-full bg-gradient-to-r from-red-300 to-red-500 animate-pulse">
Cargando defer-views # 2 , Bloques de aplazamiento nº 2
</p>
}
@defer (on viewport) {
<app-heavy-loaders-slow class="h-[100px] w-full bg-yellow-300" />
}@placeholder {
<p class="h-[100px] w-full bg-gradient-to-r from-yellow-200 to-yellow-500 animate-pulse">
Cargando defer-views # 3 , Bloques de aplazamiento nº 3
</p>
}
Defer Triggers = Aplazar activadores
Arcivo: heavy-loaders-fast.component.ts
@defer blocks https://angular.dev/guide/defer#on-interaction
// heavy-loaders-fast.component.ts
import { Component } from '@angular/core';
@Component({
selector: 'app-heavy-loaders-fast',
standalone: true,
imports: [],
template: `
<p class="h-[50px] mt-3" > heavy-loaders-fast, inicializado! </p>
`,
styles: ``
})
export class HeavyLoadersFastComponent {
// @input({ required: true }) cssClass!= string;
constructor(){
console.log('heavy-loaders-fast, se ha creado correctamente')
}
}
Archivo: defer-options.component.ts
// defer-options.component.ts
import { Component } from '@angular/core';
import {HeavyLoadersFastComponent} from '@shared/heavy-loaders/heavy-loaders-fast/heavy-loaders-fast.component';
import { TitleComponent } from '@shared/title/title.component';
@Component({
selector: 'app-defer-options',
standalone: true,
imports: [HeavyLoadersFastComponent, TitleComponent, ],
templateUrl: './defer-options.component.html',
styles: ``
})
// Declarando la ruta hijo por defecto = default
export default class DeferOptionsComponent {
}
Archivo: defer-options.component.html
<!-- defer-options.component.html -->
<app-title title="Defer Triggers = Aplazar activadores" />
<h1 class="text-xl"> Interacciones </h1>
<hr class="my-2">
<section>
<div class="h-[50px] w-fullbg-blue-300 bg-blue-300">
@defer (on interaction) {
<app-heavy-loaders-fast />
}@placeholder { <button class=" h-[50px] bg-blue-500">
click me! = On interaction </button>
}
</div>
<br>
<div class="h-[50px] w-fullbg-green-400 bg-green-300" >
@defer (on interaction (mostrar)) {
<app-heavy-loaders-fast />
}@placeholder { <button type="button" #mostrar class=" h-[50px] bg-green-500">
click me! = on interaction (mostrar) </button>
}
</div>
</section>
<br>
<section>
<div class="h-[50px] w-fullbg-blue-300 bg-yellow-300">
@defer (on timer(3000ms)) {
<app-heavy-loaders-fast />
}@placeholder { <button class=" h-[50px] bg-yellow-500">
click me! = on timer(3000ms) </button>
}
</div>
<br>
<div class="h-[50px] w-fullbg-green-400 bg-purple-300" >
@defer (on immediate) {
<app-heavy-loaders-fast />
}@placeholder { <button type="button" #mostrar class=" h-[50px] bg-purple-500">
click me! = on immediate </button>
}
</div>
</section>
Servicios y Peticiones HTTP
- [S28/L21] Angular: - Servicios con señales https://youtu.be/-kCeeRNcejQ
- [S28/L22] Angular: - Peticiones HTTP | importProvidersFrom https://youtu.be/Ypybm5Dq_VQ
Archivo: app.config.ts
// app.config.ts
import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideClientHydration } from '@angular/platform-browser';
// Importando el servicio que necesitamos: Http
import { HttpClient, HttpClientModule } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideClientHydration(),
// Declarando el servicio que necesitamos: Http
importProvidersFrom( HttpClient, HttpClientModule,),
]
};
Archivo: users.service.ts
Los datos bd.json usados estan disponibles en: https://reqres.in/api/users
// users.service.ts
import { HttpClient } from '@angular/common/http';
import { computed, inject, Injectable, signal } from '@angular/core';
// import {User} from '../interfaces/req-response'
import { User, UsersResponse } from '@interfaces/req-response';
import { delay } from 'rxjs/internal/operators/delay';
interface State{ users: User[]; loading: boolean; }
@Injectable({ providedIn: 'root' })
export class UsersService {
// Agregando el servicio HttpClient
private http = inject(HttpClient);
//
#state = signal<State>({ loading: true, users:[], });
// Declarando funciones computadas
public users = computed( () => this.#state().users );
public loading = computed( () => this.#state().loading );
// Generando el servicio a utilizar
constructor() {
this.http.get<UsersResponse>('https://reqres.in/api/users')
.pipe( delay(2000) ) .subscribe( res => {
this.#state.set({ loading:false, users: res.data, })
})
console.log('cargando Datos');
}
}
Archivo: req-response.ts
Los datos bd.json usados estan disponibles en: https://reqres.in/ , asi como en: https://reqres.in/api/users?page=2
// req-response.ts
export interface UsersResponse {
page: number;
per_page: number;
total: number;
total_pages: number;
data: User[];
support: Support;
}
export interface User {
id: number;
email: string;
first_name: string;
last_name: string;
avatar: string;
}
export interface Support {
url: string;
text: string;
}
Archivo: users.component.ts
// users.component.ts
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
// import { UsersService } from '../../../services/users.service';
import { UsersService } from '@services/users.service';
import { TitleComponent } from '@shared/title/title.component';
@Component({
selector: 'app-users',
standalone: true,
imports: [TitleComponent, RouterLink, CommonModule, ],
templateUrl: './users.component.html',
styles: ``
})
// Declarando la ruta hijo por defecto = default
export default class UsersComponent {
public usersService = inject(UsersService);
}
Archivo: users.component.html
<!-- users.component.html -->
<app-title title="Lista de Usuarios" />
<ul>
@for ( user of usersService.users(); track user.id) {
<li class="flex place-items-center my-2 cursor-pointer">
<img [srcset]="user.avatar" [alt]="user.first_name"
class="rounded-r w-14"
/>
<a [routerLink]="['dashboard/pages/user/', user.id ]"
class="mx-5 hover:underline">
{{user.first_name}} {{user.last_name}}
</a>
</li>
} @empty{
<p>Espere un momento porfavor... <br> la infomación esta cargando. </p>
}
</ul>
Observable a Señal
- Observable: https://angular.dev/guide/pipes/unwrapping-data-observables#
- Using RxJS observables as source https://angular.dev/guide/components/output-fn#using-rxjs-observables-as-source
- [S28/L23] Angular: - De observable a Señal | toSignal https://youtu.be/kCATxUtogrY
- [S28/L24] Angular: - Computed Signals https://youtu.be/1ahe97pHT54
Archivo: app.config.ts
// app.config.ts
import { ApplicationConfig, importProvidersFrom, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideClientHydration } from '@angular/platform-browser';
// Importando el servicio que necesitamos: Http
import { HttpClient, provideHttpClient, withFetch } from '@angular/common/http';
export const appConfig: ApplicationConfig = {
providers: [
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideClientHydration(),
// Declarando el servicio que necesitamos: Http
importProvidersFrom( HttpClient ),
// provideHttpClient(), /* angular with standalone v14+ */
provideHttpClient( withFetch() ), /* angular with standalone v14+ and allow withFetch */
]
};
Archivo: req-response.ts
/** req-response.ts */
export interface UsersResponse {
page: number;
per_page: number;
total: number;
total_pages: number;
data: User[];
support: Support;
}
/** Para mostrar Observables */
export interface UserResponse {
data: User;
support: Support;
}
/** Interfaces o datos de cada ususrio */
export interface User {
id: number;
email: string;
first_name: string;
last_name: string;
avatar: string;
}
export interface Support {
url: string;
text: string;
}
Archivo: users.service.ts
// users.service.ts
import { HttpClient } from '@angular/common/http';
import { computed, inject, Injectable, signal } from '@angular/core';
// import {User} from '../interfaces/req-response'
import type { User, UserResponse, UsersResponse } from '@interfaces/req-response';
import { map } from 'rxjs';
import { delay } from 'rxjs/internal/operators/delay';
interface State{ users: User[]; loading: boolean; }
@Injectable({ providedIn: 'root' })
export class UsersService {
// Agregando el servicio HttpClient
private http = inject(HttpClient);
#state = signal<State>({ loading: true, users:[], });
// Declarando funciones computadas
public users = computed( () => this.#state().users );
public loading = computed( () => this.#state().loading );
// Generando el servicio a utilizar
constructor() {
this.http.get<UsersResponse>('https://reqres.in/api/users')
.pipe( delay(2000) ) .subscribe( res => {
this.#state.set({ loading:false, users: res.data, })
})
console.log('cargando Datos');
}
}
/** Funcion para observar datos specificos de un usuario */
getUserById( id: string ){
return this.http.gett<UserResponse>(`https://reqres.in/api/users/${id}`)
.pipe(
delay(2000),
map( resp => resp.data )
)
}
}
Archivo user.component.ts
// user.component.ts
import { CommonModule } from '@angular/common';
import { Component, inject, signal } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { User, } from '@interfaces/req-response';
import { TitleComponent } from '@shared/title/title.component';
import { toSignal } from '@angular/core/rxjs-interop';
import { switchMap } from 'rxjs';
import { UsersService } from '@services/users.service';
@Component({
selector: 'app-user',
standalone: true,
imports: [CommonModule, TitleComponent, RouterLink],
templateUrl: './user.component.html',
styles: ``
})
// Declarando la ruta hijo por defecto = default
export default class UserComponent {
// Funcion para observar datos specificos de un usuario
private route = inject( ActivatedRoute );
private usersService = inject( UsersService );
// public user = signal < User | undefined >( undefined );
public user = toSignal(
this.route.params.pipe(
switchMap(({ id }) => this.usersService.getUserById( id ))
)
)
// constructor(){
// this.route.params.subscribe(params => {
// console.log({params})
// })
// }
}
archivo: user.component.html
<!-- user.component.html -->
<p>user works!</p>
<app-title title="User" />
@if ( user() ) {
<section>
<img [srcset]="user()!.avatar" [alt]="user()!.first_name" />
<div>
<h3>{{ user()?.first_name }} {{user()?.last_name}}</h3>
<p>{{user()?.email}}</p>
</div>
</section>
} @else {
<p>Cargando información </p>
}
.........
------------------------------------