Tag: Terminal

  • Angular 19 con Signals y Effects: Reactividad moderna sin RxJS

    Angular 19 con Signals y Effects: Reactividad moderna sin RxJS

    Con la llegada de Angular 16, y su consolidación en Angular 17, 18 y ahora 19, el equipo de Angular ha introducido una nueva forma de trabajar con la reactividad que se aleja de la complejidad de RxJS en muchos casos comunes: los Signals y Effects.

    En este post, exploraremos cómo crear un proyecto moderno con Angular 19 usando esta nueva API reactiva, y compararemos la solución con un enfoque clásico basado en BehaviorSubject. También presentaremos herramientas como Volta para controlar versiones de Node.js de forma consistente.

    Requisitos previos

    Versiones que usaremos:

    • Angular: 19.0.0 (estable en junio 2025)
    • Node.js: 20.x LTS
    • NPM: 10.x
    • Volta: última estable

    ¿Qué es Volta y por qué usarlo?

    Volta es un gestor de herramientas para entornos JavaScript que permite instalar y fijar versiones de herramientas como Node.js, npm, Yarn o pnpm. A diferencia de NVM, es más rápido, automático y confiable, ideal para entornos de desarrollo modernos y consistentes.

    Instalación de Volta

    En macOS con Homebrew:

    brew install volta

    O con el script oficial:

    curl https://get.volta.sh | bash

    Configuración del entorno

    Una vez instalado:

    volta install node@20
    volta install npm
    volta pin node@20

    Esto asegura que todos los comandos dentro del proyecto usen siempre Node.js 20.x, sin necesidad de .nvmrc o pasos manuales.

    Volta detecta automáticamente la configuración guardada en el package.json y la aplica en CLI, IDEs o entornos de CI/CD.

    Crear proyecto Angular

    Durante la creación del proyecto, Angular puede preguntarte si deseas habilitar SSR (Server-Side Rendering) o SSG (Static Site Generation). Para este tutorial, responde “No” a ambas, ya que estamos construyendo una SPA simple que consume una API pública desde el navegador.

    Instalación del Angular CLI

    Si aún no tienes Angular CLI instalado globalmente, puedes hacerlo con:

    npm install -g @angular/cli@19

    Esto te permitirá usar el comando ng para crear y administrar proyectos Angular.

    npm create @angular@latest
    # o si ya tienes Angular CLI
    ng new pokemon-signals-demo --standalone --routing --style=css
    cd pokemon-signals-demo

    Agregar Tailwind CSS al proyecto

    Desde Tailwind CSS v4, la forma recomendada de integración con Angular ha cambiado. Ya no se utiliza @tailwind en archivos SCSS ni se genera postcss.config.js, sino que se usa un archivo .postcssrc.json.

    Paso 1: Instalar Tailwind CSS

    npm install tailwindcss @tailwindcss/postcss postcss --force

    Paso 2: Crear el archivo .postcssrc.json

    En la raíz del proyecto, crea un archivo llamado .postcssrc.json con el siguiente contenido:

    {
      "plugins": {
        "@tailwindcss/postcss": {}
      }
    }

    Paso 3: Crear y configurar el archivo de estilos global

    Asegúrate de usar un archivo styles.css (no SCSS) dentro de src/, y en él escribe:

    Luego actualiza el archivo angular.json para asegurarte de que src/styles.css sea la hoja de estilo principal.

    Importante: Tailwind v4 no es compatible con preprocesadores CSS como SCSS. Usa solo .css para los estilos globales.

    Paso 4: Ejecutar la aplicación

    ng serve

    Qué son Signals y Effects

    signal es una forma de declarar un valor reactivo sin depender de RxJS. Funciona como una señal reactiva que notifica automáticamente a Angular para actualizar la vista o ejecutar efectos cuando cambia su valor.

    • signal: crea un valor observable reactivo.
    • computed: deriva valores de otros signals.
    • effect: ejecuta efectos secundarios (como peticiones HTTP o logs) en respuesta a cambios.

    Ejemplo real: Buscador de Pokémon con Angular Signals

    Vamos a crear un componente que consulte la API pública de PokeAPI y filtre los primeros 151 Pokémon por nombre.

    Crear el componente, servicio, tipos y configurar HttpClient

    Primero, generamos un componente standalone:

    ng generate component PokemonSearch --standalone

    También generamos un servicio:

    ng generate service pokemon --flat --path=src/app/pokemon

    Y creamos una carpeta para los tipos e interfaces del dominio:

    mkdir src/app/pokemon/types

    Dentro creamos el archivo de modelo:

    // src/app/pokemon/types/pokemon.model.ts
    export interface Pokemon {
      name: string;
      url: string;
    }

    Este servicio manejará la comunicación con la API. La interfaz Pokemon se separa en un archivo tipo para mantener una estructura limpia:

    import { Injectable, signal } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { Pokemon } from '../types/pokemon.model';
    
    @Injectable({ providedIn: 'root' })
    export class PokemonService {
      private readonly apiUrl = 'https://pokeapi.co/api/v2/pokemon?limit=151';
    
      readonly pokemons = signal<Pokemon[]>([]);
    
      constructor(private http: HttpClient) {
        this.fetchAll();
      }
    
      fetchAll() {
        this.http.get<any>(this.apiUrl).subscribe(response => {
          this.pokemons.set(response.results);
        });
      }
    }
    

    Configurar main.ts para standalone con HttpClient

    Edita main.ts para usar PokemonSearchComponent como raíz:

    import { bootstrapApplication } from '@angular/platform-browser';
    import { provideHttpClient } from '@angular/common/http';
    import { PokemonSearchComponent } from './app/pokemon-search/pokemon-search.component';
    
    bootstrapApplication(PokemonSearchComponent, {
      providers: [
        provideHttpClient(),
      ]
    });

    Ver el resultado en el navegador

    Dado que PokemonSearchComponent es un componente standalone y está siendo bootstrapped directamente en main.ts, debes asegurarte de que su selector esté presente en index.html.

    Por defecto, el selector del componente es:

    selector: 'app-pokemon-search'

    Entonces, en tu archivo src/index.html, dentro del <body>, agrega:

    <body>
      <app-pokemon-search></app-pokemon-search>
    </body>

    Esto le dice a Angular dónde montar el componente en el DOM. Si no colocas ese selector, obtendrás un error como:

    NG05104: The selector "app-pokemon-search" did not match any elements

    También podrías cambiar el selector a 'app-root' si prefieres mantener la convención típica de Angular y usar <app-root> en el HTML.

    Dado que PokemonSearchComponent es un componente standalone y está siendo bootstrapped directamente en main.ts, el contenido se mostrará en el index.html por defecto de Angular, en el elemento <app-root> (o en su reemplazo si se cambia el selector).

    Asegúrate de que el selector usado sea <app-pokemon-search> o lo que corresponda, y que esté vinculado directamente desde main.ts como se muestra arriba.

    Vamos a construir una aplicación que cargue la lista de los primeros 151 Pokémon desde la API pública PokeAPI y permita filtrarlos por nombre con un input reactivo.

    import { Component, computed, signal, effect } from '@angular/core';
    import { CommonModule } from '@angular/common';
    import { PokemonService } from '../pokemon/pokemon.service';
    
    @Component({
      selector: 'app-pokemon-search',
      standalone: true,
      imports: [CommonModule],
      templateUrl: './pokemon-search.component.html',
    })
    export class PokemonSearchComponent {
      // Signal que almacena el valor actual del input
      searchTerm = signal('');
    
      // Valor computado que depende del signal searchTerm y del signal del servicio
      filteredPokemons = computed(() => {
        const term = this.searchTerm().toLowerCase();
        return this.pokemonService.pokemons().filter(p =>
          p.name.toLowerCase().includes(term)
        );
      });
    
      // Inyectamos el servicio donde vive el signal con la data
      constructor(public pokemonService: PokemonService) {
        // Opcional: efecto secundario al cambiar el valor del input
        effect(() => {
          console.log('Término de búsqueda:', this.searchTerm());
        });
      }
    
      // Función que actualiza el signal searchTerm al escribir en el input
      updateSearch(term: string) {
        this.searchTerm.set(term);
      }
    }
    

    ¿Qué son signal, computed y effect?

    • signal(initialValue): Crea un valor reactivo que notifica automáticamente cuando cambia. Similar a un BehaviorSubject, pero más simple y sin necesidad de subscribe.

    • computed(() => ...): Deriva un valor a partir de uno o varios signals. Solo se recalcula cuando cambian sus dependencias. Ideal para filtros, cálculos derivados, etc.

    • effect(() => ...): Ejecuta un bloque de código cada vez que cambian los signals usados dentro del efecto. Útil para side-effects como logging, tracking, llamadas a APIs, etc.

    En este tutorial solo usamos effect como demostración, pero podrías aprovecharlo para:

    • Guardar búsquedas recientes.

    • Sincronizar con el localStorage.

    • Disparar una petición HTTP cada vez que cambie el filtro (aunque no es necesario en este caso porque ya precargamos los 151 Pokémon).

    Template HTML

    <div class="flex flex-col items-center justify-center h-screen w-screen">
      <div class="w-4/5 min-h-[500px] bg-gray-100 rounded-lg p-4 overflow-y-auto my-8">
        <input
          type="text"
          class="w-full p-2 rounded-md border border-gray-300 bg-white"
          placeholder="Buscar Pokémon..."
          (input)="updateSearch($any($event).target.value)"
        />
        <div class="grid grid-cols-6 gap-4 mt-4">
          <div *ngFor="let pokemon of filteredPokemons()" 
               class="bg-white p-4 rounded-lg border border-gray-200">
            <p class="text-center capitalize">{{ pokemon.name }}</p>
          </div>
        </div>
      </div>
    </div>
    

    Comparativa: Cómo se hacía antes con RxJS

    A continuación, vemos cómo implementar el mismo comportamiento usando RxJS y el enfoque clásico basado en observables:

    // Importamos decoradores y operadores necesarios
    import { Component, OnInit } from '@angular/core';
    import { HttpClient } from '@angular/common/http';
    import { BehaviorSubject, combineLatest, map } from 'rxjs';
    
    @Component({
      selector: 'app-pokemon-search-rxjs',
      standalone: true,
      templateUrl: './pokemon-search.component.html',
    })
    export class PokemonSearchRxjsComponent implements OnInit {
      // URL de la API
      private apiUrl = 'https://pokeapi.co/api/v2/pokemon?limit=151';
    
      // Observable que representa el término de búsqueda
      private searchTerm$ = new BehaviorSubject<string>('');
    
      // Observable que obtiene todos los Pokémon desde la API
      private allPokemons$ = this.http.get<any>(this.apiUrl).pipe(
        map(res => res.results)
      );
    
      // Combina ambos observables y filtra los resultados según el término
      filteredPokemons$ = combineLatest([this.allPokemons$, this.searchTerm$]).pipe(
        map(([pokemons, term]) =>
          pokemons.filter((p: any) =>
            p.name.toLowerCase().includes(term.toLowerCase())
          )
        )
      );
    
      constructor(private http: HttpClient) {}
    
      // Método de Angular, aunque no es necesario en este ejemplo
      ngOnInit(): void {}
    
      // Función para actualizar el término de búsqueda
      updateSearch(term: string) {
        this.searchTerm$.next(term);
      }
    }

    Comparación línea por línea

    Angular Signals RxJS clásico
    signal('') new BehaviorSubject('')
    computed(() => ...) combineLatest([...]).pipe(map(...))
    effect(() => ...) subscribe(...) (si hiciera falta)
    .set() .next()
    Reactividad declarativa integrada Reactividad con suscripción explícita
    Sin necesidad de unsubscribe Debes cuidar las suscripciones manuales

    Ventajas de usar Signals

    • No necesitas suscripciones ni unsubscribe.
    • Menos boilerplate.
    • Integración directa con Angular y detección de cambios optimizada.
    • Código más declarativo y legible.

    Conclusión

    Angular 19 marca una evolución en la forma de trabajar con reactividad. Signals y Effects ofrecen una experiencia más directa, clara y moderna, ideal para la mayoría de los casos donde antes usábamos RxJS por obligación. Aunque RxJS sigue siendo útil en escenarios complejos, Signals resuelven el 80% de los casos comunes con menos código y más claridad.

    En futuros posts podríamos combinar Signals con rutas, inputs y servicios para crear apps más complejas, o explorar cómo coexistir con código existente basado en RxJS.

  • Arquitectura moderna Microfrontends: Caso real TCG Pocket – II

    Arquitectura moderna Microfrontends: Caso real TCG Pocket – II

    Ahora comenzamos la parte práctica del proyecto:

    ✅ Creamos el monorepo con Nx.
    ✅ Generamos la app shell, que servirá de host para los microfrontends.
    ✅ Configuramos next-i18next para traducción en español e inglés.
    ✅ Verificamos que el servidor funcione localmente y muestra el texto traducido correctamente.

    Paso 1: Crear el monorepo con Nx

    Usaremos Nx por su robustez y soporte para múltiples frameworks (Next.js, NestJS, React, etc.).

    npx create-nx-workspace@latest tcg-pocket --preset=apps --package-manager=npm --workspaceType=integrated

    Explicación:

    • tcg-pocket: nombre del workspace.

    • --preset=apps: crearemos un monorepo sin proyectos iniciales.

    • --package-manager=npm: usaremos npm (puedes usar pnpm o yarn si prefieres).

     

    Paso 2: Instalar plugins para Next.js y NestJS

    npm install --save-dev @nx/next @nx/nest
    • @nx/next: para apps Next.js.

    • @nx/nest: para el backend BFF con NestJS.

    Paso 3: Generar la aplicación shell (Next.js)

    npx nx g @nx/next:app apps/shell --style=tailwind

    Esto crea:

    /apps/shell

    Con todos los archivos de un proyecto Next.js básico.

    Paso 4: Añadir soporte para next-i18next

    Instalamos la librería:

    npm install next-i18next
    

    Creamos la configuración base de i18n en la raíz del shell:

    // next-i18next.config.js
    module.exports = {
      i18n: {
        defaultLocale: 'es',
        locales: ['es', 'en'],
      },
    };
    

    Paso 5: Configurar Next.js para usar i18n

    Edita /apps/shell/next.config.js:

    //@ts-check
    
     
    const { composePlugins, withNx } = require('@nx/next');
    const { i18n } = require('./next-i18next.config');
    
    /**
     * @type {import('@nx/next/plugins/with-nx').WithNxOptions}
     **/
    const nextConfig = {
      reactStrictMode: true,
      i18n,
      nx: {},
    };
    
    const plugins = [
      withNx,
    ];
    
    module.exports = composePlugins(...plugins)(nextConfig);
    
    

    Paso 6: Crear archivos de traducción

    Creamos carpetas de traducción dentro de la app shell:

    /apps/shell/public/locales
      /es
        common.json
      /en
        common.json
    

    Ejemplo de /apps/shell/public/locales/es/common.json:

    {
      "title": "Bienvenido a TCG Pocket Explorer",
      "description": "Explora cartas de Pokémon TCG Pocket con arquitectura moderna"
    }

    Y para /en/common.json:

    {
      "title": "Welcome to TCG Pocket Explorer",
      "description": "Explore Pokémon TCG Pocket cards with modern architecture"
    }
    

    Paso 7: Usar traducción en la app

    Edita /apps/shell/pages/index.tsx:

    import { useTranslation } from 'next-i18next';
    import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
    
    export default function Home() {
      const { t } = useTranslation('common');
    
      return (
        <main className="flex flex-col items-center justify-center min-h-screen">
          <h1 className="text-3xl font-bold">{t('title')}</h1>
          <p className="mt-2 text-gray-600">{t('description')}</p>
        </main>
      );
    }
    
    // Para que Next.js cargue las traducciones en SSR:
    export async function getStaticProps({ locale }: { locale: string }) {
      return {
        props: {
          ...(await serverSideTranslations(locale, ['common'])),
        },
      };
    }
    

    Editar el archivo pages/_app.tsx:

    import { AppProps } from 'next/app';
    import Head from 'next/head';
    import './styles.css';
    import { appWithTranslation } from 'next-i18next';
    
    function CustomApp({ Component, pageProps }: AppProps) {
      return (
        <>
          <Head>
            <title>Welcome to shell!</title>
          </Head>
          <main className="app">
            <Component {...pageProps} />
          </main>
        </>
      );
    }
    
    export default appWithTranslation(CustomApp);

    Esto nos permitirá usar la traducción en toda la web.

    Paso 8: Levantar el servidor y probar

    Arrancamos la app shell:

    nx run shell:dev

    Esto corre en http://localhost:3000.

    Probar el cambio de idioma

    Para ver cómo funciona la traducción:

    • Accede a http://localhost:3000/es → ves la versión en español.

    • Accede a http://localhost:3000/en → ves la versión en inglés.

    ¡La traducción ya está activa y funcionando!

    ¿Qué logramos?

    ✅ Monorepo base creado y estructurado con Nx.
    ✅ App shell Next.js lista como host de microfrontends.
    ✅ Configuración de i18n con next-i18next y traducciones en español e inglés.
    ✅ Servidor corriendo y funcionando correctamente.

    ¿Qué sigue?

    En el próximo post:

    • Agregaremos los microfrontends sets y cards.

    • Configuraremos Module Federation para que el shell cargue sus módulos remotos.

    • Documentaremos la configuración y primeras integraciones.

  • ¡Configura tu terminal con OhMyZSH como los más capos!

    ¡Configura tu terminal con OhMyZSH como los más capos!

    ¿Cansado del terminal por defecto en macOS? Personalizarlo no solo mejora la estética, sino que también incrementa tu productividad.

    Z Shell (ZSH) es una poderosa shell de Unix basada en Bash (la shell predeterminada de macOS), pero con muchísimas mejoras.

    En esta guía, configuraremos iTerm2 junto con ZSH y algunos complementos esenciales para lograr un terminal espectacular.

    Temas que cubriremos:

    • Instalación de Homebrew

    • Instalación de iTerm2

    • Instalación de ZSH y Oh My ZSH

    • Configuración de temas, fuentes y plugins para un terminal potente

    Paso 1: Instalar Homebrew

    Homebrew es un gestor de paquetes para macOS que facilita la instalación de software.

    Primero, instala las herramientas de línea de comandos de Xcode ejecutando:

    xcode-select --install

    Si ves un error, reinicia la configuración de Xcode con:

    xcode-select -r

    Luego instala Homebrew con:

    /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

    Paso 2: Instalar iTerm2

    iTerm2 es una alternativa moderna al Terminal de macOS, repleta de funcionalidades avanzadas.

    Instálalo usando Homebrew:

    brew install --cask iterm2

    Paso 3: Instalar ZSH

    macOS ya trae ZSH instalado en /bin/zsh, pero conviene actualizarlo con Brew:

    brew install zsh

    Paso 4: Instalar Oh My Zsh

    Oh My Zsh es un marco de configuración de ZSH impulsado por la comunidad. Hace que la personalización de ZSH sea simple y poderosa.

    Instálalo con:

    sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

    Verifica la versión instalada:

    zsh --version

    Y, si deseas actualizar en el futuro:

    upgrade_oh_my_zsh

    ¡Reinicia iTerm2 para comenzar a usar tu nueva configuración de ZSH!

    Paso 5: Cambiar el tema por defecto

    Oh My Zsh incluye muchos temas. El tema predeterminado es robbyrussell, pero puedes cambiarlo fácilmente.

    Para editar tu configuración:

    nano ~/.zshrc

    O usa tu editor favorito:

    cursor ~/.zshrc

    Busca la línea que dice:

    ZSH_THEME="robbyrussell"

    y cámbiala, por ejemplo, a:

    ZSH_THEME="agnoster"

    Guarda los cambios y recarga la configuración:

    source ~/.zshrc

    Usar un tema personalizado (Ejemplo: Powerlevel9k)

    Si deseas un tema no preinstalado:

    git clone https://github.com/bhilburn/powerlevel9k.git ~/.oh-my-zsh/custom/themes/powerlevel9k
    

    Luego actualiza tu .zshrc:

    ZSH_THEME="powerlevel9k/powerlevel9k"
    

    Y recarga:

    source ~/.zshrc

    Paso 6: Instalar fuentes para Powerline

    Algunos temas (como Powerlevel9k) requieren fuentes especiales (Powerline Fonts).

    Instálalas ejecutando:

    git clone https://github.com/powerline/fonts.git --depth=1
    cd fonts
    ./install.sh
    

    Luego, en iTerm2 ve a: Preferences > Profiles > Text > Change Font
    y selecciona una fuente como Inconsolata o FiraCode (esta última soporta ligaduras).

    Paso 7: Instalar esquemas de colores

    Personaliza aún más tu terminal instalando un esquema de colores:

    1. Descarga los esquemas desde iTerm2 Color Schemes.

    2. Extrae el ZIP.

    3. En iTerm2, ve a:
      Preferences > Profiles > Colors > Color Presets > Import

    4. Importa los archivos .itermcolors desde la carpeta descargada.

    Paso 8: Instalar plugins para ZSH

    Oh My Zsh viene con varios plugins (como git), pero puedes agregar más.

    Por ejemplo, para agregar soporte a Docker:

    git clone https://github.com/zsh-users/zsh-docker.git ${ZSH_CUSTOM:-~/.oh-my-zsh/custom}/plugins/zsh-docker
    

    Luego edita ~/.zshrc y añade zsh-docker en la sección de plugins:

    plugins=(git zsh-docker)

    Recarga:

    source ~/.zshrc

    Paso 9: Agregar alias personalizados

    Los alias son comandos abreviados. Puedes agregarlos en tu .zshrc.

    Por ejemplo:

    alias dckimgs="docker images"
    

    Así, cada vez que escribas dckimgs verás el listado de imágenes de Docker.

    ¡Listo!

    Ahora tienes un terminal hermoso, funcional y altamente productivo en macOS, listo para acompañarte en todos tus proyectos.

    Si conoces otros trucos o configuraciones útiles para ZSH, ¡Contáctame!