npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@becaskurtces/gaia-ng-front-core

v1.0.1

Published

Librería de Angular con configuraciones, funcionalidades comunes y componentes UI básicos para todos los proyectos de front. No es obligatorio usar esta librería mientras se cumpla con lo escrito en la sección "[Comunicación entre marco global e iframes](

Downloads

270

Readme

@becaskurtces/gaia-ng-front-core

Librería de Angular con configuraciones, funcionalidades comunes y componentes UI básicos para todos los proyectos de front. No es obligatorio usar esta librería mientras se cumpla con lo escrito en la sección "Comunicación entre marco global e iframes". Si no usas la librería, puedes copiar y modificar todo el código que te pueda ser útil.

La versión mayor de esta librería sigue la versión mayor de Angular. Es decir, si la librería tiene la versión 18.x.x, entonces está desarrollada sobre Angular 18.x.x.

Es compatible con Angular 17+. Si tu proyecto usa una versión más antigua, la única opción es copiar el código que necesites o actualizar la app a Angular 17+. El código es compatible con las versiones anteriores excepto porque se está usando la nueva forma de organizar el código en Angular, con componentes standalone y sin NgModules. La librería ha sido probada en aplicaciones con Angular 17, 18, 19 y 20.

Los cambios que se vayan haciendo en el código de la librería se irán anotando en el fichero CHANGELOG

En el caso de encontrar errores o querer hacer alguna sugerencia, contactar con la Oficina Técnica o crear un merge/pull request.

Puedes usar el proyecto gaia-fe-angular-base-project como referencia de uso de esta librería.

Índice

Sobre la librería

Esta librería trata de solucionar los siguientes aspectos:

  • Comunicación entre el marco global e iframes usando postMessage.
  • Configuración común del usuario (idioma, formato de fecha, formato numérico) y sincronización con el marco global.
  • Sincronización de rutas entre marco global e iframes.
  • Sincronización de las opciones del menú entre marco global e iframes.
  • Interfaces sugeridas para definir la configuración por entornos.
  • Configuración sugerida para autenticación y sincronización del usuario logueado en el marco global con la app cargada en el iframe.
  • Configuración sugerida para i18n con sincronización con el marco global y utilidades.
  • Cargar módulos remotos (por ejemplo, el Web Component de notificaciones).
  • Script para generar un fichero versions.json dentro del compilado de Angular con las versiones de la aplicación, información del sistema operativo, versiones de Node.js y npm, la rama de Git y el hash del commit, y la fecha de generación del fichero.
  • Componentes básicos de UI con el diseño definido por el equipo de UX.

Recomendaciones al empezar un proyecto

Al empezar un nuevo proyecto de Angular, se recomienda usar la última versión estable, y como mínimo la versión 17. Se recomienda también crear todos los componentes como 'standalone', pues es la forma en la que el equipo de Angular ha decidido que usemos el framework desde las últimas versiones. A partir de la versión 19 de Angular, todos los componentes pasarán a ser standalone por defecto.

Instalar en tu proyecto

La librería se publica como paquete público en npmjs.com. No necesitas token para instalarla.

La librería tiene las siguientes dependencias:

  • @jsverse/transloco@7+
  • angular-auth-oidc-client@17+
  • @angular/material@17+

Instala la librería:

npm install @becaskurtces/gaia-ng-front-core

Si te aparece algún error al usar la librería, es posible que necesites modificar el fichero tsconfig.json de tu proyecto y cambiar el valor de moduleResolution por Bundler. En proyectos nuevos generados con el CLI de Angular, ya viene configurado así.

{
  "compilerOptions": {
    "moduleResolution": "Bundler"
  }
}

Componentes UI

Los componentes de UI se han definido dentro de un subpaquete: @becaskurtces/gaia-ng-front-core/ui.

Puedes ver todos los componentes creados hasta el momento en la documentación dedicada solo a estos componentes de UI:

Ver documentación de @becaskurtces/gaia-ng-front-core/ui.

El marco global

Todas las aplicaciones se han pensando para que sean lo más independientes posibles unas de otras. Cada aplicación se encarga de un contexto diferente (siniestros, contabilidad, etc.) y cada una tiene su frontend y su backend.

El marco global es la aplicación que carga todas estás aplicaciones dentro de una misma app. Cuenta solo con una barra superior y un menú lateral de navegación. En el centro carga el resto de apps dentro de un iframe. En la barra superior puede haber iconos que abren otros menús, como algunos ajustes del usuario o las notificaciones.

Todas las aplicaciones también deben poder abrirse sin necesidad del marco global, funcionando de forma independiente, con su autenticación y todo.

En la siguiente imagen se muestra una captura de pantalla del marco global actual. En el centro se está cargando la aplicación de bandeja de tareas (una versión no actualizada usada para hacer pruebas):

Marco global

En principio, el marco global será la única app que mostrará las notificaciones. Se encargará de abrir los links de las notificaciones y ajustar la URL según a que app pertenezcan.

Marco global

El marco global será el encargado de permitir al usuario cambiar el idioma, formato de fecha y formato numérico. Estos datos se comunicarán al resto de apps cargadas dentro del iframe (Comunicación entre marco global e iframes).

Marco global

En la barra superior también aparecerá el usuario logueado:

Marco global

El menú izquierdo por ahora es fijo y sus opciones se configuran en el fichero "{env}.properties.js" del marco global:

Marco global

Notificaciones

Las notificaciones pueden contener links. Los links deben ser propios del front de la app que las generó, no deben ser links del marco global. El marco global parsea las URLs para cargar la app correspodiente e iniciarla con la URL de la notificación. Por ahora, las aplicaciones de los iframes deben configurarse en la configuración por entorno del marco global, y en caso de que alguna aplicación no esté definida y llegué una notificación con un link de la misma, se mostrará una página 404.

Comunicación entre marco global e iframes

Debido a limitaciones de seguridad por parte de los navegadores, la mejor opción para comunicar la aplicación del marco global y el resto de aplicaciones cargadas en iframes, es usando la API postMessage.

Se necesita compartir la siguiente información entre el marco global y la app cargada en el iframe:

  • Configuración común del usuario.
    • Idioma
    • Formato de fecha
    • Formato numérico
  • Sincronizar la ruta del iframe con la del marco global.
  • Sincronizar el usuario logueado en el marco global con la del iframe.

Los mensajes que se envían a través de postMessage deben tener el siguiente formato para que los entienda el marco global:

export interface IframeMessage {
  type: string;
  data: any;
}

Se puede usar el código de src/iframe-messaging/services/iframe-messaging.service.ts como referencia para ver como enviar y recibir mensajes con postMessage.

Para la configuración común del usuario

type será "ENT_DOMAIN_COMMON_USER_CONFIG"

y data sigue la interfaz CommonUserConfig:

export interface CommonUserConfigMessage extends IframeMessage {
  type: "ENT_DOMAIN_COMMON_USER_CONFIG";
  data: CommonUserConfig;
}
export interface CommonUserConfig {
  locale: Locale;
  dateFormat: DateFormat;
  numberFormat: NumberFormat;
}

El marco global envía este mensaje al iframe al terminarse de cargar la app en el iframe y cada vez que cambia un valor de la configuración.

Los valores de los enumerados Locale, DateFormat y NumberFormat son los siguientes:

/**
 * Todos los códigos de idioma que deberían soportar todas las aplicaciones.  
 * De momento solo español e inglés. Esto no restringe a que cada app pueda
 * añadir otros idiomas aunque este enumerado no se modifique.
 */
export enum Locale {
  ES = "es",
  EN = "en",
}
/**
 * Los valores se han definido siguiendo la documentación
 * de DatePipe de Angular
 * https://angular.dev/api/common/DatePipe?tab=usage-notes
 */
export enum DateFormat {
  /**
   * MM/dd/yyyy, hh:mm a  
   * (09/25/2024, 09:15 PM)
   */
  MM_DD_YYYY_hhmma = "MM/dd/yyyy, hh:mm a",
  /**
   * dd/MM/yyyy HH:mm  
   * (25/09/2024 21:15)
   */
  DD_MM_YYYY_HHmm = "dd/MM/yyyy HH:mm",
  /**
   * yyyy/MM/dd HH:mm  
   * (2024/09/25 21:15)
   */
  YYYY_MM_DD_HHmm = "yyyy/MM/dd HH:mm"
}
export enum NumberFormat {
  /**
   * Usar puntos para los enteros y coma para separar los decimales.
   * Ejemplo: 100.000.000,00
   */
  DOTS_COMMA = "dots_comma",
  /**
   * Usar commas para los enteros y punto para separar los decimales.
   * Ejemplo: 100,000,000.00
   */
  COMMAS_DOT = "commas_dot"
}

Para la sincronización de rutas

type será "ENT_DOMAIN_ROUTE_CHANGE"

y data sigue la interfaz RouteChange:

export interface RouteChangeMessage extends IframeMessage {
  type: "ENT_DOMAIN_ROUTE_CHANGE";
  data: RouteChange;
}
export interface RouteChange {
  path: string;
}

El iframe indicará al marco global cuando cambia su ruta y el marco global indicará a los iframe si quiere que carguen una ruta en específico, por ejemplo, al pinchar en las opciones del menú de navegación.

Si la app del iframe cambia de ruta, debe enviar un mensaje "ENT_DOMAIN_ROUTE_CHANGE" al marco global con su nuevo path. Si, estando la app cargada en el iframe, el marco global quiere mostrar otra ruta, por ejemplo, al pulsar una sub-opción del menú, enviará un mensaje "ENT_DOMAIN_ROUTE_CHANGE" al iframe con el path deseado.

Para la sincronización del usuario

type será "ENT_DOMAIN_CURRENT_AUTH"

y data sigue la interfaz CurrentAuth:

export interface CurrentAuthMessage extends IframeMessage {
  type: "ENT_DOMAIN_CURRENT_AUTH";
  data?: CurrentAuth;
}
export interface CurrentAuth {
  email: string;
}

Para que la app del iframe obtenga el usuario logueado del marco global, debe enviar un mensaje "ENT_DOMAIN_CURRENT_AUTH" al marco global, sin necesidad de especificar la propiedad data. Cuando el marco global recibe este mensaje, responde al iframe con otro mensaje con el mismo tipo, "ENT_DOMAIN_CURRENT_AUTH", con la propiedad data con el email del usuario logueado. Este email se puede usar con la librería angular-auth-oidc-client para pasarle al servicio de Microsoft la propiedad login_hint con el email del usuario y la propiedad prompt con el valor none. De esta forma, se autentica directamente con ese usuario sin mostrar la pantalla de login. Se puede ver un ejemplo de esto en el fichero src/auth/provide-auth-app-initializer.ts.

Es importante que en la configuración de angular-auth-oidc-cliente NO se use la siguiente configuración:

// No poner esto
customParamsAuthRequest: {
  prompt: "select_account"
}

Esta opción provoca que siempre se muestre la página de login de Microsoft y esta página no funciona dentro de un iframe, por lo que la app no se cargaría correctamente.

Obtener la configuración de usuario actual

import { ReadonlyUserConfigService } from "@becaskurtces/gaia-ng-front-core";

@Component()
export class MyComponent implements OnInit {

  private readonlyUserConfigService = inject(ReadonlyUserConfigService);

  ngOnInit() {
    // Suscribirse a toda la configuración
    this.readonlyUserConfigService.config$.subscribe(config => ...);

    // Suscribirse a partes de la configuración
    this.readonlyUserConfigService.locale$.subscribe(locale => ...);
    this.readonlyUserConfigService.dateFormat$.subscribe(dateFormat => ...);
    this.readonlyUserConfigService.numberFormat$.subscribe(numberFormat => ...);

    // Obtener la configuración sin necesidad de suscribirse
    const config = this.readonlyUserConfigService.getCurrentConfig();
  }
}

Cómo saber si mi app está dentro de un iframe

window !== window.parent // dentro de un iframe

window === window.parent // ejecución normal sin iframe

Puedes usar la utilidad isInsideAnIframe() de esta librería si no te acuerdas de la condición de arriba.

Recargar la aplicación con los cambios en la configuración

Si quieres que tu app se recargue automáticamente cada vez que cambia alguna de las configuraciones del usuario en el marco global, importa la siguiente configuración:

import { provideCommonUserConfig } from "@becaskurtces/gaia-ng-front-core";

export const appConfig: ApplicationConfig = {
  providers: [
    provideCommonUserConfig({
      // Recargar con cualquier cambio
      reloadWindowOnAnyChange: true
    })
  ]
};
import { provideCommonUserConfig, CommonUserConfigProperty } from "@becaskurtces/gaia-ng-front-core";

export const appConfig: ApplicationConfig = {
  providers: [
    provideCommonUserConfig({
      reloadWindowWithChangesIn: [
        // Elegir con que cambios recargar
        CommonUserConfigProperty.LOCALE,
        CommonUserConfigProperty.DATE_FORMAT,
        CommonUserConfigProperty.NUMBER_FORMAT
      ]
    })
  ]
};

Inicializar y sincronizar el idioma (i18n)

Para escuchar los eventos del marco global y actualizar automáticamente el idioma en el servicio de Transloco, solo es necesario importar e inicializar los servicios usando provideI18n(config?: I18nConfig). Esta función crea un servicio que llama a translocoService.setActiveLang(lang) cuando recibe un evento de configuración del marco global, además de inicializar Transloco con los datos proporcionados con el parámetro I18nConfig.

Si nuestra app tiene el fichero app.config.ts:

import { provideI18n } from '@becaskurtces/gaia-ng-front-core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideI18n()
  ]
};

O importándolo en un NgModule:

import { provideI18n } from '@becaskurtces/gaia-ng-front-core';

@NgModule({
  providers: [
    provideI18n()
  ]
})
export class MyModule {}

La función provideI18n() usa la siguiente configuración por defecto:

const defaultConfig: I18nConfig = {
  assetsPath: `${window.location.origin}/assets/i18n`,
  availableLocales: availableLocales,
  defaultLocale: DEFAULT_LOCALE,
  fallbackLocale: DEFAULT_LOCALE,
  reRenderOnLangChange: false,
  logMissingKeys: true,
  loader: undefined // usa uno por defecto si no se define
}

// availableLocales son los valores del enumerado Locale
const availableLocales = Object.values(Locale);

// DEFAULT_LOCALE se elige según el idioma del navegador y
// los idiomas de availableLocales
const DEFAULT_LOCALE = availableLocales.includes(browserLang) ? browserLang : Locale.ES;

Los ficheros de traducciones tendrán el formato {{código-de-idioma}}.json dentro de la ruta assetsPath.

Loader alternativo

Si el "loader" por defecto no cumple con todas nuestras necesidades, podemos usar uno personalizado:

import { inject, Injectable } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { Translation, TranslocoLoader } from "@jsverse/transloco";
import { Observable } from "rxjs";
import { I18N_CONFIG_TOKEN } from '@becaskurtces/gaia-ng-front-core';

@Injectable({ providedIn: 'root' })
export class MyCustomTranslocoLoaderService implements TranslocoLoader {

  private http = inject(HttpClient);
  private config = inject(I18N_CONFIG_TOKEN);

  /**
   * Descarga el JSON con las traducciones usando la ruta
   * especificada en la configuración
   */
  getTranslation(lang: string): Observable<Translation> {
    return this.http.get<Translation>(`${this.config.assetsPath}/${lang}.json`);
  }

}
import { provideI18n } from '@becaskurtces/gaia-ng-front-core';
import { MyCustomTranslocoLoaderService } from './my-custom-transloco-loader.service';

@NgModule({
  providers: [
    provideI18n({ loader: MyCustomTranslocoLoaderService })
  ]
})
export class MyModule {}

I18n en Angular Material

Tanto en Angular como en Angular Material, la configuración de idioma y fechas, se suele tratar como estática en muchos componentes y servicios. No se puede actualizar dinámicamente mientras se usa la aplicación. La librería NgxMask tampoco permite cambiar el formato a posteriori. Por tanto, para asegurar que todo se muestra con el mismo idioma y formato, se recomienda recargar la página como se indica en un uno de los apartados anteriores: "Recargar la aplicación con los cambios en la configuración". Si tu aplicación no usa Datepickers de Material, NgxMask u otra librería que no permita actualizarse dinámicamente su configuración, no es necesario hacer esta recarga.

Para obtener la configuración del usuario al cargarse la app, se han creado las siguientes funciones:

  • getInitUserConfig. Devuelve toda la configuración del usuario (locale, dateFormat y numberFormat).
  • getInitNumberConfig. Devuelve la configuración numberFormat preparada para usar con NgxMask. (También se puede usar Maskito con un componente ya preconfigurado. Ver la documentación de README-UI.md, busca en el índice: "Maskito")
  • getInitMatDateFormats. Devuelve la configuración dateFormat preparada para importarse como MatDateFormats.

A continuación se deja un ejemplo de como usar estas funciones:

import { provideDateFnsAdapter } from "@angular/material-date-fns-adapter";
import { provideMomentDateAdapter } from "@angular/material-moment-adapter";
import { es } from "date-fns/locale";
import { enUS } from "date-fns/locale";
import { provideNgxMask } from "ngx-mask";
import {
  getInitUserConfig,
  getInitNumberConfig,
  getInitMatDateFormats,
  Locale
} from "@becaskurtces/gaia-ng-front-core";

const initLang = getInitUserConfig().locale;

const initNumberConfig = getInitNumberConfig();

export const appConfig: ApplicationConfig = {
  providers: [
    // Inicializar el idioma en Material
    {
      provide: MAT_DATE_LOCALE,
      useValue: initLang == Locale.ES ? es : enUS
    },

    // Importa solo un DateAdapter: provideDateFnsAdapter o provideMomentDateAdapter

    // Inicializar el DateAdapter de DateFns con los
    // formatos de fecha según la configuración de usuario
    provideDateFnsAdapter(getInitMatDateFormats()),

    // Inicializar el DateAdapter de Momentjs  con los
    // formatos de fecha según la configuración de usuario
    provideMomentDateAdapter(getInitMatDateFormats("moment"), { useUtc: true }),

    // Inicializar NgxMask con la configuración de usuario
    provideNgxMask({
      thousandSeparator: initNumberConfig.thousandSeparator,
      decimalMarker: initNumberConfig.decimalMarker
    })
  ]
}

Si no aparecen las fechas en el formato esperado, comprueba que no estés importando más de una vez un DateAdapter o un módulo que inicialice un DateAdapter.

Directivas y Pipes de ayuda

El módulo de i18n incluye varias directivas y pipes de ayuda:

@Component({
  selector: 'my-component',
  standalone: true,
  imports: [
    I18nRerenderDirective,
    I18nRerenderLocaleDirective,
    I18nRerenderDateDirective,
    I18nRerenderTimeDirective,
    I18nRerenderNumberDirective,
    I18nDatePipe,
    I18nDateDirective,
    I18nNumberPipe,
    I18nNumberDirective,
    I18nCurrencyPipe,
    I18nConcatDateFormatKeyPipe
  ],
  template: `...`
})
export class MyComponent {}

Las directivas I18nRerender* solo pueden ser útiles en el caso de que no se recargue la aplicación automáticamente con los cambios en la configuración del usuario.

I18nRerenderDirective

Directiva estructural para re-renderizar componentes o elementos (y sus componentes y elementos hijo) al cambiar el locale, dateFormat o numberFormat.

Acepta un string con los valores separados por comas. Los valores se convierten a minúsculas internamente, por lo que es case insensitive. Los posibles valores son: locale, dateFormat y numberFormat.

Solo re-renderiza los componentes o elementos si cambia alguno de los valores especificados o en el caso de no especificar ninguno, re-renderiza con cualquier cambio en los 3 valores.

<ng-container *i18nRerender></ng-container>

<div *i18nRerender></div>

<div *i18nRerender="'locale,dateFormat,numberFormat'"></div>

<div *i18nRerender="'dateFormat'"></div>

I18nRerenderLocaleDirective

Directiva estructural para re-renderizar componentes o elementos (y sus componentes y elementos hijo) al cambiar el locale.

<div *i18nRerenderLocale></div>

I18nRerenderDateDirective

Directiva estructural para re-renderizar componentes o elementos (y sus componentes y elementos hijo) al cambiar el dateFormat. Está pensada para componentes o elementos que muestran una fecha sin incluir la hora.

<div *i18nRerenderDate></div>

I18nRerenderTimeDirective

Directiva estructural para re-renderizar componentes o elementos (y sus componentes y elementos hijo) al cambiar el locale o dateFormat. El locale puede afectar en algún caso a la hora mostrada (AM/PM, a. m./p. m.). Está pensada para componentes o elementos que muestren una fecha con la hora.

<div *i18nRerenderTime></div>

I18nRerenderNumberDirective

Directiva estructural para re-renderizar componentes o elementos (y sus componentes y elementos hijo) al cambiar el numberFormat.

<div *i18nRerenderNumber></div>

I18nDateDirective

Directiva para renderizar una fecha según el idioma de la configuración de usuario dateFormat. Si cambia la configuración vuelve a renderizar la fecha.

El valor por defecto del input format es "shortDate", el de tooltipFormat es undefined por defecto y en ese caso no se renderiza el tooltip. El tooltip se crea usando la propiedad name de HTML.

<span [i18nDate]="notification.createdOn" format="shortDate" tooltipFormat="short"></span>

<span [i18nDate]="notification.createdOn"></span>  

<span i18nDate="{{ notification.createdOn }}"></span>

I18nDatePipe

Pipe para renderizar una fecha según la configuración de usuario dateFormat.

Si cambia la configuración NO vuelve a renderizar el número. Se puede combinar con la directiva *i18nRerenderDate o *i18nRerenderTime para añadir la funcionalidad de refresco cuando cambia la configuración.

La propiedad format es por defecto "shortDate". Acepta los mimos valores que la DatePipe de Angular.

<span>{{ date | i18nDate }}</span>

<span>{{ date | i18nDate:"{format}" }}</span>

<span *i18nRerenderDate>{{ date | i18nDate }}</span>

<span *i18nRerenderTime>{{ date | i18nDate:"short" }}</span>

I18nNumberDirective

Directiva para renderizar un número según la configuración de usuario numberFormat. Si cambia la configuración vuelve a renderizar el número. La propiedad digitsInfo acepta el mismo formato que la DecimalPipe de Angular.

<span [i18nNumber]="cost"></span>

<span [i18nNumber]="cost" digitsInfo="{minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}"></span>

<span i18nNumber="{{ cost }}"></span> 

I18nNumberPipe

Pipe para renderizar un número según la configuración de usuario numberFormat.

Si cambia la configuración NO vuelve a renderizar el número. Se puede combinar con la directiva *i18nRerenderNumber para añadir la funcionalidad de refresco cuando cambia la configuración.

Acepta el parámetro opcional digitsInfo con el mismo formato que la DecimalPipe de Angular.

<span>{{ cost | i18nNumber }}\</span>

<span>{{ cost | i18nNumber:"{minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}" }}</span>

<span *i18nRerenderNumber>{{ cost | i18nNumber }}\</span>

I18nCurrencyPipe

Pipe para renderizar un número según la configuración de usuario numberFormat.

Si cambia la configuración NO vuelve a renderizar el número. Se puede combinar con la directiva *i18nRerenderNumber para añadir la funcionalidad de refresco cuando cambia la configuración.

Los parámetros son los siguientes:

i18nCurrency:"currencyCode_ISO4217":"narrow|wide":"digitsInfo"

<span>{{ cost | i18nCurrency }}</span>

<span>{{ cost | i18nCurrency:"EUR" }}</span>

<span>{{ cost | i18nCurrency:"EUR":"wide" }}</span>

<span>{{ cost | i18nCurrency:"EUR":"narrow":"{minIntegerDigits}.{minFractionDigits}-{maxFractionDigits}" }}</span>
 
<span *i18nRerenderNumber>{{ cost | i18nCurrency }}</span>

I18nConcatDateFormatKeyPipe

Pipe que concatena la clave en formato string del DateFormat seleccionado por el usuario.

// Recordatorio de como es el enumerado DateFormat
export enum DateFormat {
  MM_DD_YYYY_hhmma = "MM/dd/yyyy, hh:mm a",
  DD_MM_YYYY_HHmm = "dd/MM/yyyy HH:mm",
  YYYY_MM_DD_HHmm = "yyyy/MM/dd HH:mm"
}

Si cambia la configuración NO vuelve a procesar el string. Se puede combinar con la directiva *i18nRerenderDate o *i18nRerenderTime para añadir la funcionalidad de refresco cuando cambia la configuración.

<span>
  {{ "date-format.shortDate." | i18nConcatDateFormatKey | transloco }}
</span>

<span *i18nRerenderDate>
  {{ "date-format.shortDate." | i18nConcatDateFormatKey | transloco }}
</span>

<span *i18nRerenderTime>
  {{ "date-format.short." | i18nConcatDateFormatKey | transloco }}
</span>

El fichero en.json tendría el siguiente aspecto:

{
  "date-format.shortDate.MM_DD_YYYY_hhmma": "MM/DD/YYYY",
  "date-format.shortDate.DD_MM_YYYY_HHmm": "DD/MM/YYYY",
  "date-format.shortDate.YYYY_MM_DD_HHmm": "YYYY/MM/DD",
  "date-format.short.MM_DD_YYYY_hhmma": "MM/DD/YYYY, hh:mm AM/PM",
  "date-format.short.DD_MM_YYYY_HHmm": "DD/MM/YYYY hh:mm",
  "date-format.short.YYYY_MM_DD_HHmm": "YYYY/MM/DD hh:mm"
}

Y el fichero es.json:

{
  "date-format.shortDate.MM_DD_YYYY_hhmma": "MM/DD/AAAA",
  "date-format.shortDate.DD_MM_YYYY_HHmm": "DD/MM/AAAA",
  "date-format.shortDate.YYYY_MM_DD_HHmm": "AAAA/MM/DD",
  "date-format.short.MM_DD_YYYY_hhmma": "MM/DD/AAAA, hh:mm a.m./p.m.",
  "date-format.short.DD_MM_YYYY_HHmm": "DD/MM/AAAA hh:mm",
  "date-format.short.YYYY_MM_DD_HHmm": "AAAA/MM/DD hh:mm"
}

Funciones de ayuda

shortDate

Devuelve el formato de fecha definido en el enumerado DateFormat pero solo con el año, mes y día, quitando las horas y minutos.

shortDate(dateFormat: DateFormat): string

import { DateFormat, shortDate } from "@becaskurtces/gaia-ng-front-core";

shortDate(DateFormat.MM_DD_YYYY_hhmma); // "MM/dd/yyyy"
shortDate(DateFormat.DD_MM_YYYY_HHmm);  // "dd/MM/yyyy"
shortDate(DateFormat.YYYY_MM_DD_HHmm);  // "yyyy/MM/dd"

Sincronización de rutas

Las apps cargadas en los iframes enviarán al marco global su ruta actual cada vez que cambie, sin incluir el dominio. También pueden recibir del marco global el mismo tipo de mensaje para que la app en el iframe cargue otra ruta.

Si nuestra app tiene el fichero app.config.ts:

import { provideIframeSyncRoute } from '@becaskurtces/gaia-ng-front-core';

export const appConfig: ApplicationConfig = {
  providers: [
    provideIframeSyncRoute()
  ]
};

O importándolo en un NgModule:

import { provideIframeSyncRoute } from '@becaskurtces/gaia-ng-front-core';

@NgModule({
  providers: [
    provideIframeSyncRoute()
  ]
})
export class MyModule {}

No es necesario hacer nada más.

Sincronización de las opciones del menú

Para poder sincronizar el menú de nuestra aplicación con el marco global, debemos hacer los siguientes cambios:

  • Crear la configuración del menú (AppMenuSyncConfig).
  • Añadir JSONs con las traducciones del menú.
  • Cambiar la función "boostrap" del fichero main.ts.
  • Actualizar la configuración del marco global (gaia-fe-home) para habilitar la sincronización.
  • Asegurarnos que el servicio desplegado con la aplicación permite los CORS a la aplicación gaia-fe-home.

Para que el menú se sincronice, no es necesario abrir la aplicación dentro del marco global. La sincronización del menú se realiza internamente usando un iframe temporal (y no visible para el usuario) y postMessage. Se inicia una app con la menor lógica posible (solo con la autenticación en iframe y lógica de sincronización del menú) para que la sincronización sea lo más rápida posible. Cuando termina la sincronización o salta un timeout definido en el marco global, el iframe se destruye.

Crear la configuración del menú

La interfaz AppMenuSyncConfig consta de la siguientes propiedades:

/**
 * Configuración para la sincronización del menú de la aplicación
 */
export interface AppMenuSyncConfig {
  /**
   * Configuración de autenticación.  
   * Puede ser un objeto o una función que devuelva una promesa u observable con la configuración.
   * Usa la misma interfaz que la función "provideAuth".
   */
  auth: ProvideAuthConfig;
  /**
   * Función para construir el menú de la aplicación
   */
  buildMenuFn: BuildMenuFn;
}

ProvideAuthConfig es un tipo con la siguiente definición:

// AuthConfig o función que devuelve Promise<AuthConfig> o Observable<AuthConfig>
export type ProvideAuthConfig = AuthConfig | (() => Promise<AuthConfig>) | (() => Observable<AuthConfig>);

La función con la interfaz BuildMenuFn es la siguiente:

/**
 * Interfaz para la función que construye el menú.
 */
export type BuildMenuFn = (ctx: BuildMenuFnContext) => AppMenu;

/**
 * Contexto para la función que construye el menú.
 */
export interface BuildMenuFnContext {
  /**
   * Devuelve true si el usuario tiene TODOS los roles indicados.
   */
  userHasRoles: (roles: string[]) => boolean;
  /**
   * Devuelve true si el usuario NO TIENE TODOS los roles indicados.
   */
  userDoesntHaveRoles: (roles: string[]) => boolean;
  /**
   * Devuelve true si el usuario tiene ALGUNO de los roles indicados.
   */
  userHasAnyRole: (roles: string[]) => boolean;
}

Interfaces para definir el menú (AppMenu, AppMenuTranslations y AppMenuItem):

/**
 * Menú de la aplicación que se mostrará dentro
 * del marco global.
 */
export interface AppMenu {
  /**
   * Idioma a usar por defecto si en "translations"
   * no se encuentra el idioma elegido por el usuario.
   */
  defaultLocale: Locale | string;
  /**
   * Paths para obtener las traducciones.
   * 
   * Ejemplo:
   * ```json
   * {
   *   "en": "/assets/i18n/en.json",
   *   "es": "/assets/i18n/es.json"
   * }
   * ```
   */
  translations: AppMenuTranslations;
  /**
   * Item principal del menú.
   */
  rootItem: AppMenuItem;
}
/**
 * Clave: Código de idioma.  
 * Valor: Path al JSON con las traducciones.
 * 
 * Ejemplo:
 * {
 *   "en": "/assets/i18n/en.json",
 *   "es": "/assets/i18n/es.json"
 * }
 */
export interface AppMenuTranslations {
  [key: string]: string;
}
/**
 * Elemento del menú.
 */
export interface AppMenuItem {
  /**
   * Clave de las traducciones para usar como título/label del elemento
   */
  titleI18n: string;
  /**
   * Path al recurso asociado al elemento.  
   * La ruta es relativa al AppMenuItem padre.
   */
  path: string;
  /**
   * Indica si el elemento está deshabilitado.
   */
  disabled?: boolean;
  /**
   * Elementos hijo. Por ahora solo se muestran hasta
   * 2 niveles de menú sin contar con el nivel raíz.
   */
  children?: AppMenuItem[];
}

Ejemplo de configuración de menú

app-menu.config.ts

import { AppMenuSyncConfig } from "@becaskurtces/gaia-ng-front-core/appmenu";
import { environment } from '@env';

export const appMenuConfig: AppMenuSyncConfig = {
  // En este ejemplo pasamos los valores para la
  // autenticación usando la configuración de entorno.
  // La propiedad usa la misma interfaz de configuración que "provideAuth".
  // Más abajo hay un ejemplo para devolver la configuración
  // dentro de una promesa.
  auth: environment.auth,
  // Función para construir el menú
  buildMenuFn: (ctx) => {
    return {
      // Idioma por defecto a usar si el usuario ha elegido un idioma
      // que no se encuentra en "translations", por ejemplo "fr".
      defaultLocale: 'en',
      // Rutas de los ficheros con las traducciones del menú
      translations: {
        en: '/assets/i18n/appmenu/en.json',
        es: '/assets/i18n/appmenu/es.json'
      },
      rootItem: {
        titleI18n: 'menu.root-item',
        // Path raíz de la aplicación
        path: '',
        // Ocultar elemento raíz (y por tanto todo el menú)
        // si el usuario no tiene los roles especificados:
        // disabled: ctx.userDoesntHaveRoles(['my-app-role'])
        children: [
          {
            titleI18n: 'menu.child.admin',
            path: '/admin',
            // Ocultar item del menú si no tiene los roles "admin" y "manager"
            disabled: ctx.userDoesntHaveRoles(['admin', 'manager'])
          },
          {
            titleI18n: "menu.child.option-a",
              // Los paths son relativos al path padre
            path: "/option-a",
            children: [
              {
                titleI18n: "menu.child.option-a.option-1",
                // Los paths son relativos al path padre.
                // Para esta opción el path resultante sería:
                // "/option-a/option-1"
                path: "/option-1"
              },
              {
                titleI18n: "menu.child.option-a.option-2",
                path: "/option-2",
                // Ocultar item del menú si no tiene el rol "second-level-option-a2"
                disabled: ctx.userDoesntHaveRoles(['second-level-option-a2'])
              }
            ]
          },
          {
            titleI18n: "menu.child.option-b",
            path: "/option-b"
          },
          {
            titleI18n: "menu.child.option-c",
            path: "/option-c"
          }
        ]
      }
    };
  }
};

En el ejemplo se devuelve un objecto directamente y se usan las funciones del contexto para ocultar opciones del menú. Al ser una función, se pueden usar ifs, fors, etc para contruir el menú como se quiera.

Más ejemplos de la propiedad "auth"

Ejemplo cargando la configuración de un fichero JSON usando el fetch nativo y devolviendo un Promise<AuthConfig>:

import { AppMenuSyncConfig } from "@becaskurtces/gaia-ng-front-core/appmenu";
import { findFirstUUID } from "@becaskurtces/gaia-ng-front-core";
import { environment } from "@/environment";
import { MyAppConfig } from "./my-app-config";

export const appMenuConfig: AppMenuSyncConfig = {
  auth: async () => {
    const response = await fetch(environment.configFile);
    const config = response.ok ? await response.json() as MyAppConfig : null;
    return {
      clientId: config.security.clientId,
      scope: config.security.scopes,
      // Podemos usar la función "findFirstUUID" para extraer el
      // "tenantId" de la URL usada como "authority"
      tenantId: findFirstUUID(config.security.authority)
    };
  },
  buildMenuFn: (ctx) => { ... }
};

Ejemplo usando HttpClient de Angular y devolviendo un Observable<AuthConfig>:

import { inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { AppMenuSyncConfig } from "@becaskurtces/gaia-ng-front-core/appmenu";
import { environment } from "@/environment";
import { MyAppConfig } from "./my-app-config";

export const appMenuConfig: AppMenuSyncConfig = {
  auth: () => {
    const httpClient = inject(HttpClient);

    return httpClient.get('/assets/config.json').pipe(
      map((config: MyAppConfig) => {
        return {
          clientId: config.clientId,
          scope: config.scope,
          tenantId: config.tenantId,
          // etc...
        } as AuthConfig;
      })
    );
  },
  buildMenuFn: (ctx) => { ... }
};

Ejemplo cargando la configuración desde otra variable:

import { AppMenuSyncConfig } from "@becaskurtces/gaia-ng-front-core/appmenu";
import { config } from "./config";

export const appMenuConfig: AppMenuSyncConfig = {
  auth: {
    clientId: config.auth.clientId,
    scope: config.auth.scopes,
    tenantId: config.auth.tenantId
  },
  buildMenuFn: (ctx) => { ... }
};

Añadir JSONs con las traducciones del menú

Crea todos los ficheros json que hayas definido en la propiedad translations. No es necesario añadir un prefijo único a las traducciones, el marco global se encarga de añadir uno para evitar conflictos con otras aplicaciones.

Ejemplo de fichero /assets/i18n/appmenu/en.json:

{
  "menu.root-item": "My App",
  "menu.child.admin": "Admin",
  "menu.child.option-a": "Option A",
  ...
}

Cambiar la función "boostrap" del fichero main.ts

Para iniciar la app con la menor lógica posible, como se ha comentado más arriba, se ha creado una nueva función bootstrap. Esta función inicia la app en modo sincronización del menú si se cumple una condición y si esta condición no se cumple, se inicia la app de forma normal, al igual que el código original.

Si usamos ApplicationConfig (app.config.ts)

Cambiar bootstrapApplication por bootstrapApplicationWithMenuSync:

Original:

import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';

bootstrapApplication(AppComponent, appConfig)
  .catch((err) => console.error(err));

Nuevo:

import { bootstrapApplicationWithMenuSync } from '@becaskurtces/gaia-ng-front-core/appmenu';
import { appConfig } from './app/app.config';
import { AppComponent } from './app/app.component';
import { appMenuConfig } from './app/app-menu.config';

bootstrapApplicationWithMenuSync(AppComponent, appConfig, appMenuConfig)
  .catch((err) => console.error(err));

Si usamos NgModule

Cambiar platformBrowserDynamic().bootstrapModule(...) por bootstrapModuleWithMenuSync:

Original:

import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { AppModule } from './app/app.module';

platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .catch(err => console.error(err));

Nuevo:

import { bootstrapModuleWithMenuSync } from '@becaskurtces/gaia-ng-front-core/appmenu';
import { AppModule } from './app/app.module';
import { appMenuConfig } from './app/app-menu.config';

bootstrapModuleWithMenuSync(AppModule, appMenuConfig)
  .catch(err => console.error(err));

Actualizar la configuración del marco global (gaia-fe-home) para habilitar la sincronización

El marco global debe habilitar para cada app la sincronización del menú. Para ello, las apps que tengan implementada esta funcionalidad, deben añadir la propiedad hasAppMenuSync con el valor true en el item raíz de cada app en la configuración del marco global.

En el caso de que ocurra un error de sincronización, el marco global usará el menú definido en su configuración de entorno (como si no existiera la sincronización).

Permitir CORS al Marco Global (gaia-fe-home)

El marco global debe tener acceso a los JSON con las traducciones del menú. Para ello debemos asegurarnos que no se bloquean los CORS al Marco Global.

La forma fácil usando Nginx es añadir la cabecera Access-Control-Allow-Origin con el valor *. Para más complejas, buscar en Internet como sería la configuración correcta.

server {
  ...


  location / {
    ... 

    add_header Access-Control-Allow-Origin "*" always;
  }
}

Solicitar al marco global abrir una URL (en la misma u otra pestaña)

Solicita al marco global abrir una URL de una de las aplicaciones pero dentro del marco global y no de forma independiente. La URL de la aplicación que se quiere abrir, debe estar configurada en el menú del marco global.

Para abrir otra aplicación diferente a la actual, se debe usar el dominio de esa app que se quiere abrir y NO la URL del marco global. El marco global transforma la URL de cada dominio en el URL correcta del marco global. De esta forma esta funcionalidad funciona tanto estando dentro como fuera del marco global. Si la aplicación no se está ejecutando dentro del marco global, la URL se abre sin modificar.

Si se solicita abrir una URL que solo es un path (no empieza por http:// o https://), la URL se transform en location.origin + URL.

A partir de la versión 1.20.0 de la librería, este servicio funciona también sin abrir la aplicación dentro del marco global.

import { OpenUrlRequestService } from '@becaskurtces/gaia-ng-front-core';

@Component({ ... })
export class MyComponent {

  private openUrlRequestService = inject(OpenUrlRequestService);

  onUrlClick(url: string): void {

    // En la misma pestaña
    this.openUrlRequestService.openUrl(url);

    // En una pestaña nueva
    this.openUrlRequestService.openUrlInNewTab(url);
  }

}

A partir de la versión 1.20.0 también, se puede añadir un parámetro a la URL para que la aplicación destino pueda conocer que aplicación la ha abierto.

import { OpenUrlRequestService } from '@becaskurtces/gaia-ng-front-core';

@Component({ ... })
export class MyComponent {

  private openUrlRequestService = inject(OpenUrlRequestService);

  onUrlClick(url: string): void {

    // En la misma pestaña
    this.openUrlRequestService.openUrl(url, { informOriginUrl: true });

    // En una pestaña nueva
    this.openUrlRequestService.openUrlInNewTab(url, { informOriginUrl: true });
  }

}

El parámetro se llama entOpenUrlOrigin y al iniciar la app se puede obtener fácilmente con este método:

import { OpenUrlRequestService } from '@becaskurtces/gaia-ng-front-core';

@Component({ ... })
export class MyComponent implements OnInit {

  private openUrlRequestService = inject(OpenUrlRequestService);

  ngOnInit(): void {

    const originUrl: string | null = this.openUrlRequestService.captureOpenUrlOriginParam();

  }

}

Solicitar al marco global cerrar la ventana actual

Importante: Solo funciona si la ventana ha sido abierta antes por la propia app. No puedes cerrar una ventana que ha sido creada manualmente por el usuario.

A partir de la versión 1.20.0 de la librería, este servicio funciona también sin abrir la aplicación dentro del marco global.

import { CloseTabRequestService } from '@becaskurtces/gaia-ng-front-core';

@Component({ ... })
export class MyComponent {

  private closeTabRequestService = inject(CloseTabRequestService);

  closeCurrentTab(): void {
    this.closeTabRequestService.close();
  }

}

Solicitar al marco global que muestre una configuración propia de mi app

Importante: Los valores de los "labels", deben enviarse traducidos al idioma activo. El marco global no conoce las traducciones de otras aplicaciones y por tanto no puede realizar la traducción.

import { CustomConfigRequestService, CustomConfigRequest } from '@becaskurtces/gaia-ng-front-core';

@Component({ ... })
export class MyComponent implements OnInit {

  private customConfigRequestService = inject(CustomConfigRequestService);

  private ngOnDestroy$ = new Subject();

  ngOnInit() {
    this.listentoCustomConfigChanges();
    this.sendCustomConfig();
  }

  ngOnDestroy() {
    this.ngOnDestroy$.next(null);
    this.ngOnDestroy$.complete();
  }

  // Envía la configuración con los campos deseados.
  private sendCustomConfig(): void {
    const config: CustomConfigRequest = {
      items: [
        {
          id: "id1",
          label: "NombreCampo1",
          value: "valor1",
          options: [
            {
              label: "NombreValor1",
              value: "valor1"
            },
            {
              label: "NombreValor2",
              value: "valor2"
            },
            {
              label: "NombreValor3",
              value: "valor3"
            }
          ]
        },
        {
          id: "id2",
          label: "NombreCampo2",
          placeholder: "Elige un valor",
          enableFilter: true,
          filterPlaceholder: "Filtra los valores",
          options: [
            {
              label: "NombreValor1",
              value: "valor1"
            },
            {
              label: "NombreValor2",
              value: "valor2"
            }
          ]
        }
      ]
    };

    this.customConfigRequestService.sendConfig(config);
  }

  // Escucha los eventos de cambio de valor.
  // Se devuelve el "id" y el "value" del campo.
  private listentoCustomConfigChanges() {
    this.customConfigRequestService.onValueChange()
      .pipe(takeUntil(ngOnDestroy$))
      .subscribe(valueChange => {
        console.log(valueChange.id, "-", valueChange.value);
      });
  }

}

La interfaz de CustomConfigRequest es la siguiente:

export interface CustomConfigRequest {
  items: CustomConfigRequestItem[];
}

export interface CustomConfigRequestItem {
  /** Identificador de la configuración */
  id: string;
  /** Etiqueta del selector */
  label: string;
  /** Valor actual si tiene */
  value?: any;
  /** Opciones a elegir */
  options: CustomConfigRequestItemOption[];
  /** Placeholder */
  placeholder?: string;
  /** Habilitar un filtro de texto para filtrar las opciones */
  enableFilter?: boolean;
  /** Placeholder del filtro */
  filterPlaceholder?: string;
}

export interface CustomConfigRequestItemOption {
  /** Lo que se va a mostrar en la UI como valor */
  label: string;
  /** El valor real del elemento */
  value: any;
}

Autenticación

En este módulo se ha reunido la configuración mínima de autenticación. Se usa la dependencia angular-auth-oidc-client. En principio la configuración de este módulo debería ser suficiente para todas las aplicaciones.

Solo necesitamos importar provideAuth(config: ProvideAuthConfig) y authInterceptor que añade la cabecera "Authentication" a todas las peticiones HTTP realizadas con el cliente HTTP de Angular. Este modulo se encarga de sincronizar el usuario del marco global con el iframe.

La configuración acepta las siguientes propiedades:

// AuthConfig o función que devuelve Promise<AuthConfig> o Observable<AuthConfig>
export type ProvideAuthConfig = AuthConfig | (() => Promise<AuthConfig>) | (() => Observable<AuthConfig>);
export interface AuthConfig {
  clientId: string;
  tenantId: string;
  scope: string;
  /**
   * URL a la que redirigir al hacer un login correcto.  
   * Si no se informa el valor, se usa por defecto: `window.location.origin`.  
   * Si el valor NO empieza por `http(s)://` se usa `${window.location.origin}/?${redirectUrl}`.  
   * En cualquier otro caso, se usa el valor indicado.
   */
  redirectUrl?: string;
  /**
   * Dominio de correo permitido. Si no se especifica, la
   * ventana de login permite usar cualquier correo. En el
   * caso de tener varios usuarios con la sesión iniciada
   * en Microsoft, se filtran las cuentas usando este dominio.
   */
  domainHint?: string;
  /**
   * Rutas a las que añadir la cabecera Authorization. Recuerda importar el
   * interceptor en los providers. Puedes usar el interceptor de "@becaskurtces/gaia-ng-front-core"
   * o el de "angular-auth-oidc-client". El interceptor de "@becaskurtces/gaia-ng-front-core"
   * añade la cabecera a todas las peticiones si no se especifica ninguna ruta.  
   * Las rutas que no empiecen por `http(s)://`, se les añadirá el valor de
   * `location.origin` al principio de la ruta.
   */
  secureRoutes?: string[];
  /**
   * Claves para acceder a las propiedas del token JWT y obtener el nombre,
   * nombre de usuario e email del usuario logueado. Por defecto:  
   * 
   * { nameKey: "name", emailKey: "email", usernameKey: "preferred_username" }
   * 
   * Se usa en el servicio AuthHelperService.
   */
  jwtUserDataKeys?: JwtUserDataKeys;
  /**
   * Lista de roles mínimos requeridos por el usuario para usar la app.
   * Si la lista es vacía o nula, no se requiere ningún rol.
   * 
   * También puede ser una función que recibe un contexto
   * con métodos para comprobar los roles del usuario.  
   * Si la función devuelve `true`, se considera que el usuario
   * tiene los roles correctos, `false` en caso contrario.  
   * `CheckUserRolesFn = (ctx: CheckUserRolesFnContext) => boolean`
   */
  userMinimuentquiredRoles?: string[] | CheckUserRolesFn;
  /**
   * Lanzar un error si el proceso de login no se completa con éxito.  
   * El error provoca que la app no termine de inicializarse.  
   * 
   * Por defecto: true.
   */
  throwErrorWhenUnauthenticated?: boolean;
  /**
   * Lanzar un error si el usuario se autentica con éxito pero
   * no tiene los roles mínimos requeridos.  
   * El error provoca que la app no termine de inicializarse.  
   * 
   * Por defecto: true.
   */
  throwErrorWhenUserDoesNotHaveRequiredRoles?: boolean;
    /**
   * Indica si se debe deshabilitar el login automático al iniciar la aplicación.
   * Si se establece a true, la aplicación no intentará iniciar sesión automáticamente al arrancar.
   * Esto puede ser útil en escenarios donde se desea iniciar el proceso de autenticación desde un componente específico.
   *
   * Por defecto: false.
   */
  disableAutoLoginOnStartup?: boolean;
  /**
   * Indica si se debe forzar el uso de un popup para el login.
   * 
   * Por defecto: false.
   */
  forceLoginWithPopup?: boolean;
  /**
   * Indica si se debe deshabilitar la comprobación para saber si el usuario
   * permite abrir popups. Si se establece a true, la aplicación no verificará
   * si el usuario ha permitido abrir popups antes de intentar iniciar sesión
   * con un popup.
   * 
   * Si se deshabilita esta comprobación, se asume que el usuario permite abrir popups,
   * lo que puede provocar errores en el proceso de autenticación si el usuario tiene
   * bloqueados los popups en su navegador.
   * 
   * La verificación de permisos implica abrir un popup de prueba.
   * 
   * Por defecto: false.
   */
  disableCheckPopupPermission?: boolean;
}
/**
 * Contexto para la función que comprueba los roles del usuario.
 */
export interface CheckUserRolesFnContext {
  /**
   * Devuelve true si el usuario tiene TODOS los roles indicados.
   */
  userHasRoles: (roles: string[]) => boolean;
  /**
   * Devuelve true si el usuario NO TIENE TODOS los roles indicados.
   */
  userDoesntHaveRoles: (roles: string[]) => boolean;
  /**
   * Devuelve true si el usuario tiene ALGUNO de los roles indicados.
   */
  userHasAnyRole: (roles: string[]) => boolean;
}

/**
 * Interfaz para la función que comprueba los roles del usuario.
 */
export type CheckUserRolesFn = (ctx: CheckUserRolesFnContext) => boolean;

Con domainHint podemos hacer que Azure AD solo nos muestre cuentas con un dominio especifico, por ejemplo, si domainHint: "entitynopro.onmicrosoft.com", entonces solo las cuentas con un email con el dominio "entitynopro.onmicrosoft.com", podrán hacer login.

Por ejemplo, si dejamos domainHint vacío y hemos iniciado sesión en Microsoft con más de una cuenta, aparecerá una pantalla como la siguiente, en la que hay 3 usuarios:

Selección de usuario en el login de Microsoft

Si especificamos domainHint: "entitynopro.onmicrosoft.com", entonces solo se mostraría la primera cuenta.

Si nuestra app tiene el fichero app.config.ts:

import { ApplicationConfig } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor, provideAuth, provideI18n } from '@becaskurtces/gaia-ng-front-core';
import { environment } from '@env';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([
        authInterceptor
      ])
    ),
    provideAuth(environment.auth)
  ]
};

O importándolo en un NgModule:

import { NgModule } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor, provideAuth, provideI18n } from '@becaskurtces/gaia-ng-front-core';
import { environment } from '@env';

@NgModule({
  providers: [
    provideHttpClient(
      withInterceptors([
        authInterceptor
      ])
    ),
    provideAuth(environment.auth)
  ]
})
export class MyModule {}

Para securizar solo los endpoints de la API, se pueden especificar las rutas en la configuración por entornos o añadir directamente la URL de la API en "secureRoutes". Las rutas que no empiecen por "http(s)://", se les añadirá el valor de location.origin al principio de la ruta.

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([
        authInterceptor
      ])
    ),
    provideAuth({
      ...environment.auth,
      secureRoutes: [environment.apiUrl]
    })
  ]
};

IMPORTANTE: Si no configuras las "secureRoutes", puedes tener problemas al obtener los ficheros de traducciones de los "/assets" una vez esté la app desplegada en AWS, porque no se permite usar la cabecera Authorization para obtener ficheros estáticos (sólo si se sirve la app desde S3).

Cargar configuración de forma asíncrona

Como provideAuth acepta funciones que devuelven la configuración en una promesa u observable, podemos cargar la configuración de forma asíncrona, por ejemplo:

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(
      withInterceptors([
        authInterceptor
      ])
    ),
    provideAuth(authConfigLoader)
  ]
};

// Función que carga la configuración
// y la mapea a la interfaz AuthConfig
const authConfigLoader = () => {
  const configService = inject(MyConfigService);

  return configService.initialized().pipe(
    map(config => {
      return {
        clientId: config.clientId,
        scope: config.scope,
        tenantId: config.tenantId,
        // etc...
      } as AuthConfig;
    }),
    take(1)
  );
}

Otro ejemplo de función que carga la configuración:

const authConfigJsonLoader = () => {
  const httpClient = inject(HttpClient);

  return httpClient.get<MyAppConfig>('/assets/config.json').pipe(
    map(config => {
      return {
        clientId: config.clientId,
        scope: config.scope,
        tenantId: config.tenantId,
        // etc...
      } as AuthConfig;
    })
  );
}

Mostrar errores de autenticación

En el caso de que ocurra un error en la autenticación del usuario, lo ideal es informar al usuario de ello.

Método rápido

Añade provideAuthErrorDialog a tu módulo principal o app.config.ts para mostrar un diálogo informativo cuando ocurran errores de autenticación o por no cumplir con los roles mínimos:

import { provideAuthErrorDialog } from "@becaskurtces/gaia-ng-front-core/ui";

export const appConfig: ApplicationConfig = {
  providers: [
    provideAuthErrorDialog()
  ]
};

Añade las traducciones de los diálogos a tu app copiando los assets de @becaskurtces/gaia-ng-front-core a los assets de tu app.

Para ello copia lo siguiente en el fichero angular.json, en el listado de assets:

{
  "glob": "**/*",
  "input": "node_modules/@becaskurtces/gaia-ng-front-core/assets/i18n",
  "output": "assets/i18n"
}

Quedaría así:

angular.json

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "my-app": {
      "architect": {
        "build": {
          "options": {
            "assets": [
              {
                "glob": "**/*",
                "input": "node_modules/@becaskurtces/gaia-ng-front-core/assets/i18n",
                "output": "assets/i18n"
              }
            ]
          }
        }
      }
    }
  }
}

Método personalizado

Para poder saber si ha ocurrido algún error, podemos suscribirnos al Observable AuthService.checkAuth$, que guarda el último resultado del método AuthService.checkAuth().

Como la autenticación se hace en un "initializer", debemos suscribirnos al observable en otro "initializer" y usar dialogs o snackbars para mostrar el error. Si falla algún initializer (como cuando falla la autenticación, debido a que se devuelve una excepción), el resto del código de la app no se ejecuta (por ejemplo, el app.component.ts, no se carga), por tanto no podemos poner la lógica en un componente.

El primer valor que puede devolver AuthService.checkAuth$ es undefined. Esto significa que AuthService.checkAuth() todavía no se ha llamado. Una vez se llama a AuthService.checkAuth(), se obtiene el resultado de la autenticación.

provide-auth-error-dialog.ts

import { APP_INITIALIZER, EnvironmentProviders, makeEnvironmentProviders } from "@angular/core";
import { TranslocoService } from "@jsverse/transloco";
import { AuthService, CheckAuthResult, getInitUserConfig, getPostLoginRoute } from "@becaskurtces/gaia-ng-front-core";
import { EntDialogService, EntDialogType } from "@becaskurtces/gaia-ng-front-core/ui";
import { Observable, filter, map, take } from "rxjs";


/**
 * Muestra un diálogo de error en caso de que ocurra algún
 * error en el proceso de autenticación.
 */
export function provideAuthErrorDialog(): EnvironmentProviders {
  return makeEnvironmentProviders([
    {
      provide: APP_INITIALIZER,
      useFactory: authErrorCheck,
      deps: [AuthService, EntDialogService, TranslocoService],
      multi: true
    }
  ]);
}

function authErrorCheck(
  authService: AuthService,
  dialogService: EntDialogService,
  translocoService: TranslocoService
): () => Observable<void> {
  return () => authService.checkAuth$.pipe(
    filter(res => !!res),
    take(1),
    map(result => {
      if (result == CheckAuthResult.ERROR)
        openGenericErrorDialog(dialogService, translocoService);

      if (result == CheckAuthResult.POPUP_BLOCKED)
        openPopupsBlockedErrorDialog(dialogService, translocoService);
    })
  );
}

function openGenericErrorDialog(
  dialogService: EntDialogService,
  translocoService: TranslocoService
) {
  const locale = getInitUserConfig().locale;

  translocoService.load(locale).subscribe(() => {
    dialogService.open({
      type: EntDialogType.ERROR,
      title: translocoService.translate("auth-error.dialog.common.title", undefined, locale),
      message: translocoService.translate("auth-error.dialog.unknown-error.message", undefined, locale),
    });
  });
}

function openPopupsBlockedErrorDialog(
  dialogService: EntDialogService,
  translocoService: TranslocoService
) {
  const locale = getInitUserConfig().locale;

  translocoService.load(locale).subscribe(() => {
    dialogService
      .open({
        type: EntDialogType.ERRO