Dashboard -- Tailwind + Angular

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

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

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

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

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

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

 


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

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

.........

------------------------------------
Artículo Anterior Artículo Siguiente