---
title: 'Guía de Desarrollo del Frontend'
---
Guía de Desarrollo - Frontend
===
* **Coordinador Tecnológico**: Rafael Palau
* **Consultores**:
* Ilse Grau
* Julio Mello
* Marco Aquino
* Marcos Benítez
* Lauro Segovia
## Contenido
[TOC]
## Introducción
---
En esta guía se describen los conceptos esenciales del framework Angular y los pasos necesarios para la creación de una vista de tipo Alta, Baja, Modificación (ABM) en el proyecto Frontend utilizando la estructura propuesta.
Para el caso práctico de la presente guía se utilizará la entidad de *FRECUENCIAS*.
## Objetivo
---
* Introducir al desarrollador en los conceptos fundamentales del framework Angular
* Guiar al desarrollador en la creación de los archivos necesarios para un ABM.
## Prerrequisito
---
:::warning
En esta guía se asume que el entorno de desarrollo se ha configurado.
- Visual Studio
- NodeJs
- Angular
- TypeScript
- Git
Nota: La versión esperada de NodeJs es 18.17.1
Para que la guía sea más util, el lector debe poder comprender y utilizar las siguientes tecnologías :
:::
* <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/A_re-introduction_to_JavaScript" target="_blank">JavaScript</a>
* <a href="https://developer.mozilla.org/es/docs/Learn/HTML/Introduction_to_HTML" target="_blank">HTML</a>
* <a href="https://developer.mozilla.org/es/docs/Learn/CSS/First_steps" target="_blank"> CSS</a>
## Concepto Angular
---
:::info
En esta sección se presentará un resumen de los conceptos fundamentales del framework Angular.
Para ahondar más al respecto es conveniente consultar las documentación oficial <a href="https://angular.io/docs" target="_blank"> Documentación Angular</a>
:::
### Introducción
<a href="https://angular.io/guide/architecture" target="_blank">Angular</a> es un framework que provee librerias para facilitar el proceso de la construcción de aplicaciones Web.
Angular es un framework open source desarrollador por el equipo de Google.
Es un framework que utiliza <a href="https://www.typescriptlang.org/"
target="_blank">TypeScript</a>, HTML e implementa el diseño de [Single Page Application (SPA)](https://developer.mozilla.org/en-US/docs/Glossary/SPA).
La base fundamental del framework Angular se sustentan en coleccion de bloques de código cohesivos y organizados, como ser `Components` que a su vez son parte de un módulo `NgModules` que los gestiona y expone para su utilización en toda la aplicación.
En una aplicación Angular existe un módulo principal llamado `root module` que tiene la responsabilidad de inicializar la aplicación, así también el de cargar las configuraciones de los otros módulos.
### Arquitectura
El esquema de la arquitectura del Angular son los [módulos](https://angular.io/guide/architecture-modules), [componentes](https://angular.io/guide/architecture-components#component-metadata), [plantillas](https://angular.io/guide/architecture-components#templates-and-views), [metadatos](https://angular.io/guide/architecture-components#component-metadata), [mapeo de datos](https://angular.io/guide/architecture-components#data-binding), [directivas](https://angular.io/guide/architecture-components#directives), [servicios](https://angular.io/guide/architecture-services) y la [inyección de dependencia](https://angular.io/guide/architecture-services).
* Esquema de la arquitectura

Figura: Esquema de la arquitectura del Angular
### Modulos (Modules)
En Angular se aplica el enfoque modular en la contrucción de la aplicación Web, Angular cuenta con su propio sistema de modulos llamado `NgModules`.
`NgModules` es un contenedor que gestiona bloque de código cohesivo enfocado en una solución especifica de la aplicación. Los módulos pueden contener **componentes**, **servicios** y otros códigos necesarios para el funcionamiento del módulo.
Para indicar al Angular que una clase sea tratada como un módulo es necesario usar <a href="https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841#.x5c2ndtx0" target="_blank">Decoradores</a>.
El decorador utilizado para los modulos es `@NgModule()`, el mismo se agregar por encima de la definicion de la clase.
:::info
Los decoradores son funciones que modifican una clase de JavaScript.
Angular define decoradores que permiten especificar metadatos a nivel de clases.
:::
El decorador `@NgModule()` define una funcion que permite especificar **metadatos** que agrega comportamientos adicionales para el módulo. Los metadastos más utilizados son :
* **declarations**: Listado de componentes, directivas y pipes que pertenecen al *NgModules*.
> Para poder utilizar un componente creado es necesario declararlo en este listado
* **exports**: Listado de componentes, directivas y pipes que serán visible para su utilización en otros componentes, plantillas o módulos.
* **imports**: Listado de módulos que son necesarios para el módulo desarrollado.
> En cada módulo se define un listado de componentes que pueden ser
> utilizados por otros módulos.
#### Ejemplo
---
```javascript=
import { CommonModule} from '@angular/common';
import { NgModule } from '@angular/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { CustomToastComponent }
from './custom-toast/custom-toast.component';
import { CustomSpinnerLoaderComponent }
from './loading/custom-spinner-loader.component';
import { CustomModalComponent }
from './custom-modal/custom-modal.component';
import { CustomFilterComponent }
from './custom-filter/custom-filter.component';
import { ReactiveFormsModule } from '@angular/forms';
import { CustomFormComponent }
from './custom-form/custom-form.component';
@NgModule({
declarations: [
CustomSpinnerLoaderComponent,
CustomToastComponent,
CustomModalComponent,
CustomFilterComponent,
CustomFormComponent
],
imports: [
NgbModule,
CommonModule,
ReactiveFormsModule,
],
exports: [
CustomSpinnerLoaderComponent,
CustomToastComponent,
CustomModalComponent,
CommonModule,
CustomFilterComponent,
ReactiveFormsModule,
NgbModule,
]
})
export class CustomCommonModule { }
```
* El decorador `NgModule` es importado desde el `@angular/core`
* En el metadato *declarations*, se agrega el listado de componentes que pertenecen al módulo **CustomCommonModule**
* En el metadato *imports*, se agrega el listado de módulos externos que son necesarios para los componentes del módulo **CustomCommonModule**
* En el metadato *exports*, se agrega el listado de componentes que serán reutilizados en otros módulos de la aplicación.
### Componentes (Components)
Los [Componentes](https://angular.io/guide/architecture-components) son los bloques fundamentales que sustentan la aplicación en Angular.
El Angular implementa una variante del patrón de diseño <a href="https://applecoding.com/guias/patrones-diseno-software-mvc" target="_blank">Modelo Vista Controlador (MVC)</a> el cual es el `Modelo VistaModelo Vista (MVVM)`.
* Representación del patrón **MVVM**
```graphviz
digraph graphname{
rankdir=LR;
A [label="Modelo"]
B [label="VistaModelo"]
C [label="Vista"]
A->B
B->A
B->C
C->B
}
```
* El *Modelo* en el esquema pueder ser una clase, un Objeto JSON o un Objeto XML.
* La *VistaModelo* en el esquema representa la gestión del mapeo de datos (data binding).
* La *Vista* en el esquema representa la plantilla que estructura la presentación de los datos.
Este esquema es utilizado en los componentes con la finalidad de construir código cohesivo y reutilizable.
La finalidad de cada componente varía según el problema a solucionar, pudiendo estos gestionar datos y mostrarlos en pantalla, escuchar eventos de un <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input" target="_blank">input</a> y realizar una acciones en consecuencia.
Cada aplicación en Angular cuenta mínimamente con un componente al cual se denomina *root component* que tiene la responsabilidad de coordinar la jerarquía de componentes y la interracción con el <a href="https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Introduction" target="_blank">Document Object Model (DOM)</a>.
Cada componente es definido vía clases empleando para ello <a href="https://www.typescriptlang.org/" target="_blank">Typescript</a> y la palabra reservada utilizada es `class`.
El componente contiene datos de la aplicación y lógica que permiten la asociación entre la clase en TypeScript y la plantilla HTML.
Para indicar al Angular que una clase sea tratada como un componente es necesario usar <a href="https://medium.com/google-developers/exploring-es7-decorators-76ecb65fb841#.x5c2ndtx0" target="_blank">Decoradores</a>.
El decorador utilizado para los componentes es `@Component()`, el mismo se agregar por encima de la definicion de la clase.
#### Metadato (Metadata)
Los metadatos para un componente indica al Angular cuales son las configuraciones necesarias a realizar o archivos externos a procesar para construir el componente.
##### Ejemplo
---
```javascript=
import { Component, OnInit } from "@angular/core";
@Component({
selector: 'app-frecuencias-form',
templateUrl: './frecuencias-form.component.html',
stylesUrl: ['./frecuencias-form.component.css']
})
export class FrecuenciasFormComponent implements OnInit {
/* . . . */
}
```
En el ejemplo se muestra algunos *metadatos* comunes utilizado a la hora de definir un componente
* El decorador `Component` es importado desde el `@angular/core`
* `seletor`: Es el selector CSS (identificador de componente), utilizado por el Angular para crear e insertar una instancia del componente donde sea utilizado. Por ejemplo, si el componente es utilizado por otro [Módulo](https://angular.io/guide/architecture-modules) dentro de la plantilla HTML `<app-frecuencias-form></app-frecuencias-form>`, entonces Angular inserta una instancia del componente `FrecuenciasFormComponent` en la posición del tags.
* `templateUrl`: Es la referencia relativa de un archivo de plantilla en HTML que será utilizada por el componente
* `stylesUrl`: Es la referencia relativa de un archivo de estilos en CSS que será utilizada por el componente
### Plantillas (Templates)
La plantilla es una combinación de HTML con tags de Angular que modifican los elementos HTML antes de ser mostrado en la pantalla.
Angular Provee [Directivas](https://angular.io/guide/architecture-components#directives) que permite aplicar logica y gestionar cambios en el DOM. Así también es posible utilizar [Pipes](https://angular.io/guide/architecture-components#pipes) para transformar los datos antes de mostrarlo y [Data binding ](https://angular.io/guide/architecture-components#data-binding) para coordinar y gestionar datos.
#### Ejemplo
---
```htmlembedded=
<div class="card-title">
<button type="button"
class="btn btn-outline-primary m-1"
(click)="open()">
<i class="fa fa-plus text-primary" aria-hidden="true"></i>
</button>
<button type="button"
class="btn btn-outline-primary m-1"
(click)="this.isShowFilter = !this.isShowFilter;">
<i class="fas fa-filter"></i>
</button>
<ng-container *ngIf="isShowFilter">
<app-custom-filter
[filterNames]="filterNames"
(formEvent)="search($event)">
</app-custom-filter>
</ng-container>
</div>
```
* Este ejemplo utiliza tags propios del HTML como :
* [div](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div)
* [button](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button)
* [i](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/i)
* Así también incluye tags conocidos por el Angular como :
* `<app-custom-spinner-loader>`
* `[filterNames]`
* `(formEvent)`
* `<ng-container>`
* `(click)`
* `*ngIf`
* `<app-custom-filter>`
* Estos tags indican al Angular como mostrará la plantilla HTML en pantalla y que acciones serán gestionadas.
* El tags `<app-custom-spinner-loader>`, indica al Angular que debe instanciar un componente que tenga el **selector** `app-custom-spinner-loader` e incluirlo como parte del DOM.
* El tags `[filterNames]`, es una forma de hacer data binding a nivel de atributo de componentes, el cual permite vincular datos expuesto por un componente como atributo.
* El tags `(formEvent)`, es una forma de hacer data binding a nivel de eventos de componentes, el cual permite captura eventos realizados por el usuario y realizar acciones en consecuencia.
* El tags `<ng-container>`, es un contenedor que aplicar directivas sin necesidad de utilizar tags HTML.
* El tags `(click)`, es una forma de hacer data binding a nivel de eventos de componentes, el cual permite captura eventos realizados por el usuario y realizar acciones en consecuencia.
* El tags `*ngIf`, es una directiva del Angular que aplica logica condicional para mostrar u ocultar elementos en la vista.
* El tags `<app-custom-filter>`, indica al Angular que debe instanciar un componente que tenga el **selector** `<app-custom-filter>`
### Mapeo de datos (Data binding)
El [mapeo de datos](https://angular.io/guide/binding-syntax) permite en forma automática gestionar el estado de la vista y mantenerlo actualizado en todo momento.
El Angular tiene la responsabilidad de agregar valores al elemento HTML, controlar los cambios de estos valores y realizar acciones en consecuencia para mantener actualizado la vista donde es referenciada. Así también trata con los eventos realizados por el usuario.
El angular soporta el mapeo de datos bidireccional, el cual permite coordinar elementos de la vista con atributos/eventos del componente, esto permite reflejar los cambios de ambos extremos en forma automatica.
En Angular existe cuatro formas de hacer mapeo de datos :
* **Interpolación de texto (Text interpolation)** : permite incorporar dinámicamente valores dentro de la plantilla HTML, la sintaxis utilizada es `{{ <valor a mostrar> }}`, por defecto como delimitador de la expresión el Angular utiliza doble llave.
* **Atributo (Property binding)** : permite agregar valor/dato a propiedades de los elementos HTML, de esta forma el angular puede gestionar estos valores entre la vista y el componente, la sintaxis utilizada es `[<propiedad>]`, por defecto como delimitador de la expresión el Angular utiliza corchete
* **Evento (Event binding)** : permite gestionar eventos que son aplicados a elementos HTML (clic, enter, movimiento de mouse, etc.) de tal forma a realizar cierta operación en consecuencia. La sintaxis utilizada es `(<evento>)`, por defecto como delimitador de la expresión el Angular utiliza el paréntesis
* **Mapeo bidireccional (Two-way binding)** : permite coordinar eventos o atributos de un componente que son compartidos. Angular gestiona en forma automática la actualización en ambos extremos. La sintaxis utilizada es `[(<evento>)] o [(<propiedad>)]`, por defecto como delimitador de la expresión el Angular utiliza el corchete y el paréntesis
* Esquema de interacción entre el DOM y el componente
<p style="text-align:center; ">
<img src="https://angular.io/generated/images/guide/architecture/databinding.png" />
</p>
Figura: Esquema de interacción entre el DOM y el componente
### Servicios (Services)
Son conjunto de código y lógica que se ejecuta en forma asíncrona y provee datos a los componentes.
Angular provee mecanismos para agrupar y crear código reutilizable gracias a los [servicios](https://angular.io/guide/architecture-services).
Angular distingue los servicios de los componentes para incrementar la reusabilidad y la modularidad.
Un componente delega ciertas tareas a los servicios, como ser obtención de datos, validación de datos, logging, etc. Esto es posible gracias a la `inyección de dependencia`, que pone a disposición de los componentes los servicios existentes.
El decorador utilizado para los servicios es `@Injectable()`, el mismo se agregar por encima de la definicion de la clase.
:::info
Los servicios no tienen interfaz de usuario o vista
:::
#### Ejemplo
---
```javascript=
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { Departamento } from '../model/departamento';
@Injectable({
providedIn: 'root'
})
export class DepartamentoService {
query = 'SELECT%20*%20FROM%20public.departamental_pais_precenso'
urlBase = `http://geo.stp.gov.py/user/stp/api/v2/sql?q=${query}`;
httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
constructor(private http: HttpClient) { }
get(): Observable<Departamento[]> {
return this.http.get<any>(this.urlBase);
}
}
```
* El decorador `Injectable` es importado desde el `@angular/core`
* El metadado `providedIn: 'root'`: indica al Angular que el servicio estará disponible en forma global para toda la aplicación
:::info
Por defecto al crear un servicio utilizando el Angular CLI se agrega el metadato `providedIn: 'root'` lo que significa que el servicio se instancia una vez y se comparte a nivel de aplicación.
:::
* Esquema de interacción
---
```sequence
Service->HttpCliente: ejecuta método HTTP (GET)
Note left of Service: ' utilización de SERVICE_URL '
HttpCliente->Backend Service: realiza petición HTTP
Note left of HttpCliente:' request/response via observable '
Backend Service-->HttpCliente:
HttpCliente-->Service:
```
#### Inyección de dependencia (Dependency Injection - DI)
La inyección de dependencia es una caracteristica implementada por el Angular para proveer a un nuevo componente las dependencias que sean requeridas.
Angular tiene la responsabilidad de crear y mantener la instancia de las clases de esta forma gestiona la reutilización de una instancia.
Angular al momento de generar un componente inspecciona las dependencias requeridas verificando los parámetros del constructor del componente.
:::info
Una dependencia no necesariamente debe ser un servicio, puede ser una función o un valor.
:::
* Esquema de trabajo de la inyección de dependencia
<p style="text-align:center; ">
<img src="https://angular.io/generated/images/guide/architecture/injector-injects.png">
</p>
</p>
### Formularios (Reactive Forms)
El enfoque de [Reactive Forms](https://angular.io/guide/reactive-forms) es el dirigido por modelo (model-driven) para gestionar los cambios de los diferentes atributos de un formulario.
Reactive forms implementa la gestión de estado mediante los `objetos inmutables del JavaScript`, lo que mantiene la integridad del modelo entre cambios
Reactive forms se construye utilizando [observables](https://angular.io/guide/glossary#observable) que permite detectar los cambios en los atributos y obtener los valores de cada cambio en forma asíncrona.
:::info
Angular utiliza una libreria de tercero para la gestión de llamadas asíncronas que es [Reactive Extensions (RxJs)](https://rxjs.dev/).
Angular utiliza la clase [Observable](https://angular.io/guide/observables) para la gestión de eventos asíncronos.
:::
#### Ejemplo
---
```javascript=
import { ReactiveFormsModule } from '@angular/forms';
@NgModule({
imports: [
ReactiveFormsModule
],
})
export class AppModule { }
```
* Para poder utilizar las caracteristicas del Reactive Forms, es necesario importar desde el `@angular/forms` el módulo `ReactiveFormsModule` e incluirlo dentro del nuestro módulo
#### Ejemplo
---
Para crear un control del formulario es necesario importar la clase [FormControl](https://angular.io/api/forms/FormControl) y crear una instancia del mismo y asignarlo a una variable de la clase.
```javascript=
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
@Component({
selector: 'app-name-editor',
templateUrl: './name-editor.component.html',
styleUrls: ['./name-editor.component.css']
})
export class NameEditorComponent {
name = new FormControl('');
constructor(){
}
}
```
* La linea `name = new FormControl('');`, inicializa la clase `FormControl` con un `string` vacio al crear este control dentro del componente `NameEditorComponent` se puede acceder a los cambios eventos y validaciones de un `input` del formulario.
* Para poder mostrar en la plantilla el atributo `name` es necesario registrar el `FormControl` a la misma. Ejemplo :
```htmlembedded=
<div>
<label for="name">Name: </label>
<input id="name" type="text" [formControl]="name">
</div>
<p>Value: {{ name.value }}</p>
```
* Utilizando el data binding (binding por atributo) del *input* `[formControl]="name"`, se registra en la plantilla el control con nombre *name*, desde allí el `FormControl` controla el elemento HTML `input` del DOM comunicando los cambios entre la vista y el componente.
* Para cambiar el valor del form control se puede utilizar el data binding (binding por evento). Ejemplo :
```javascript=
//Agregar el método al componente : NameEditorComponent
updateName() {
this.name.setValue('valor actualizado!');
}
```
* De esta forma se puede actualizar los datos del form control *name* para ello se utiliza el método `setValue()` del `FormControl`
* Este ejemplo require de un evento para llamar al método `updateName()` desde la plantilla HTML se puede agregar un botton para realizar esa operación.
```htmlembedded=
<button type="button" (click)="updateName()">Update Name</button>
```
* Con estos cambios la plantilla tendrá la funcionalidad de recibir datos del usuario via un *input* y mostralo en la vista.
#### El servicio FormBuilder
Para no instanciar cada control del formulario utilizando `FormControl`, el Angular provee el servicio [FormBuilder](https://angular.io/api/forms/FormBuilder), que contiene métodos que ayudan en el proceso de generación del formulario.
* Para poder utilizarlo es necesario importarlo desde el paquete
* `import { FormBuilder } from '@angular/forms';`.
* Al ser un servicio es una clase inyectable por lo tanto se puede agregar al constructor del componente como dependencia de esa forma el Angular pueda proveer una instancia del `FormBuilder` vía el constructor
* `constructor(private formBuilder: FormBuilder) { }`
* El `FormBuilder`, cuenta con los siguientes métodos `control(), group() y array()` que pueden ser utilizados.
##### Ejemplo
---
```javascript=
import { Component } from "@angular/core";
import { FormBuilder, FormGroup} from "@angular/forms";
@Component({
selector: 'app-frecuencias-form',
templateUrl: './frecuencias-form.component.html',
styles: [
]
})
export class FrecuenciasFormComponent {
//Se define el atributo de clase
formulario: FormGroup | null = null;
constructor(
// Se inyecta el servicio
private fromBuilder: FormBuilder,
) {
// Se inicializa los valores del FormGroup
this.buildForm();
}
buildForm() {
if (this.fromBuilder) {
//El método group del FormBuilder recibe un Objeto literal del
// JavaScript {key: value}, cada key
// representa el nombre FormControl
this.formulario = this.fromBuilder.group({
id: [''],
abreviatura: [''],
nombre: [''],
descripcion: [''],
estado: [''],
});
}
}
onSubmit(form: any): void{
console.log('Submit form :', form);
}
}
```
* El ejemplo utiliza el método `group()` del `FormBuilder` para crear los form control que posteriormente serán utilizados en la plantilla.
##### Ejemplo
---
```htmlembedded=
<form
#frecuenciaForm="ngForm"
(ngSubmit)="onSubmit(frecuenciaForm)"
[formGroup]="formulario">
<div class="form-group">
<label for="frecuenciaId">ID</label>
<input class="form-control" type="text" formControlName="id">
</div>
<div class="form-group">
<label for="nombreFrecuencia">Nombre</label>
<input type="text"
formControlName="nombre" placeholder="Nombre">
</div>
<div class="form-group">
<label for="descripcionFrecuencia">Descripcion</label>
<input type="text"
formControlName="descripcion"
placeholder="Descripcion" class="form-control" >
</div>
<div class="form-group">
<label for="abreviatura">Abreviatura</label>
<input type="text"
formControlName="abreviatura"
placeholder="Abreviatura" class="form-control">
</div>
<div class="form-group">
<label for="activo">Activo</label>
<select formControlName="activo">
<option value="true">ACTIVO</option>
<option value="false">INACTIVO</option>
</select>
</div>
<button>Submit</button>
</form>
```
* Para vincular el Form Group y el Form Control en la plantilla se emplea el binding por atributo `[formGroup]="formulario"`, con esta sintaxis se vincula el atributo formulario del componente con la plantilla HTML
* Para tener una referencia del formulario, es posible exportar como una variable local de la plantilla, para ese caso empleamos la sintaxis `#frecuenciaForm="ngForm"`, de esa forma podemos utilizar la referencia del form dentro de la plantilla o pasarlo a un método del componente como se ve en el ejemplo donde lo utilizamos para pasarlo como argumento del método *on Submit*`(ngSubmit)="onSubmit(frecuenciaForm)"`
* Por cada cada instancia de `FormControl` se van creando un input y agregando la vinculación al elemento HTML empleando binding de atributo `formControlName`
### Routing
Las aplicaciones Angular emplean una transición entre páginas, una aplicación que es (Single Page applications) SPA no recarga toda la página, solo la sección o página que requiere actualización. En general el header, el menú lateral, el footer y otros componentes cómunes se recarga solamente si son necesarias.
Angular utiliza *routing* para configurar y mapear las URLs de acceso a los componentes y mostrar las vistas asociadas.
#### Ejemplo
---
```javascript=
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PagesComponent } from './pages/pages.component';
const rootRoutes : Routes = [
{ path: '', component: PagesComponent},
]
@NgModule({
declarations: [],
imports: [
RouterModule.forRoot(rootRoutes)
],
exports:[
RouterModule
]
})
export class AppRoutingModule { }
```
* El array del `rootRoutes`, contendrá las configuraciones de rutas (route) en el ejemplo se agrega una entrada para mapear la ruta por defecto al componente `PagesComponent`.
* En la configuración del Routes es necesario dos atributos que son :
* **url** : la referencia relativa o URL
* **component**: la referencia del componente a cargar cuando el router vincule la url con la peticion actual
* Para configurar la ruta principal o por defecto de la aplicación es necesario agregar al módulo `RouterModule` vía el método `forRoot()` el listado de rutas definidas.
* El `RouterModule` es un módulo, que provee los servicios y las directivas que son necesarias para la navegación entre vistas.
* El `routing component` es un componente en el cual se define la referencia al `RouterOutlet`.
* Ejemplo del RouterOutlet
```htmlembedded=
<router-outlet></router-outlet>
```
### Decorador
El decorador es un tipo especial de declaración que puede adjuntarse a una declaración de clases, métodos, accesores, propiedades o parámetros. Los decoradores utilizan la forma @expresión, donde expresión debe evaluarse a una función que será llamada en tiempo de ejecución con información sobre la declaración decorada.
Ejemplo :
```javascript=
/**
* Valida la existencia de token antes de
* invocar al metodo original de la clase
* @returns EMPTY en caso de no tener token
*/
export const IsTokenPresente = () => {
return (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
const funcionOriginal = descriptor.value;
const cache = new CustomCacheService();
descriptor.value = function (...args: any) {
if (cache.obtenerItem('token')) {
return funcionOriginal.call(this, args);
} else {
return EMPTY;
}
};
return descriptor;
};
}
```
## Estructura de proyecto
Por defecto al utilizar el CLI del angular `ng new nombre-de-proyecto`, crea un directorio con el nombre `nombre-de-proyecto` y contiene la estructura básica de un proyecto Angular.
Dentro del directorio creado se agrega el subdirectorio `src/` el cual contiene los archivos fuentes de la aplicación.
:::warning
Ver nueva estructura : [Nueva estructura del front-end](https://hackmd.io/Bww3RFnzT9qfDwSJKpyh0w)
:::
### Ejemplo
* Estructura del proyecto Angular
---
<p style="text-align:center;">
<img src="https://i.imgur.com/9eU1uUO.png" />
</p>
---
* Dentro del directorio `src/` se encuentran los fuentes y configuraciones de la aplicación.
| Directorio/Archivo | Descripción |
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| *app/* | Contiene los archivos de los componentes y estructura de datos |
| *assets/* | Contiene las imagenes y recursos estáticos que son utilizados por la aplicación |
| *environments/* | Contiene las configuraciones de construcción por entorno |
| *index.html* | Es el archivo principal en formato HTML que es utilizado en conjunto con los componentes para mostrar vistas al usuario. El CLI del angular automáticamente agrega todas las librerias JavaScript y archivos CSS cuando se construye la aplicación. Generalmente no es necesario agregar `<script>` o `<link>` manualmente |
| *main.ts* | Es el archivo principal de la aplicación que tiene la tarea de levantar la aplicación |
| *polyfills.ts* | Es el archivo que contiene conjunto de código que es utilizado para proveer compatibilidad entre navegadores viejos |
| *styles.css* | Es el archivo que contiene conjunto de estilos a ser aplicado a nivel global en la aplicación |
| *test.ts* | Es el archivo principal para pruebas de la aplicación que tiene la tarea de inicializar las pruebas unitarias |
* Dentro del directorio `src/app` contiene los componentes, datos y lógica de la aplicación.
| Archivo | Descripción |
| -------- | -------- |
| *app/app.component.ts* | Define la lógica para la aplicación, es el componente principal de la aplicación `root component`, generalmente tiene el nombre de `AppComponent`. |
| *app/app.component.html* | Define la estructura HTML principal asociada al `AppComponent` |
| *app/app.component.css* | Define las reglas de estilo CSS a ser aplicadas al `AppComponent` |
| *app/app.module.ts* | Define el módulo principal `root module`, generalmente tiene el nombre de `AppModule`, tiene la tarea de ensamblar la aplicación. En este archivo se incluye todas las dependencias de la aplicación|
## Proceso inicial Git
Git es un sistema de control de versiones de código. Las características del Git son las siguientes :
* Código abierto.
* Gratuito.
* Diseñado para manejar proyectos grandes y pequeños.
* Fácil de aprender.
* Ocupa poco espacio con un rendimiento ultrarrápido.
## Importación del Proyecto desde GitLab
Para proceder a clonar el proyecto Frontend primeramente se debe instalar git, crear una rama propia del proyecto e importarlo como proyecto maven en el STS.
### Instalación de Git
Descargar el instalador del git (Windows installer).
https://git-scm.com/downloads
Realizar doble click sobre el instalador y seguir las instrucciones.
### Clonar el Proyecto Frontend
* Una vez instalado el git se procede ha ubicarse en la carpeta donde se desea clonar el proyecto y hacer **click derecho --> Git Bash Here** y despliega la ventana de comando de git.

**Figura.** Ejecutar git bash.

**Figura.** Editor vim de git.
* Configurar la variables globales del git
:::info
git config --global user.name "nombre de usuario"
git config --global user.email "user@hacienda.gov.py"
git config --global http.sslVerify false
:::

**Figura.** Configuración de variables.
* Clonar proyecto.
Antes de proceder a clonar el proyecto se debe acceder a GitLab del proyecto para copiar la url del proyecto.
1. Acceder al GitLab del proyecto mediante las credenciales válidas (username, password).
:::info
https://gitlab.siare.gov.py/
:::

**Figura.** Acceder al GitLab del proyecto.
2. Clonar el proyecto Siare-Front
``` shell=
$ git clone https://git.hacienda.gov.py/siare-dev/ngx-siare-front.git
```

**Figura.** Clonar el proyecto desde el repositorio en GitLab.
3. Acceder a la carpeta clonada y posicionarse en la rama develop
``` shell=
$ cd siare-front/
$ git checkout develop
```

**Figura.** Acceder a la carpeta clonada.
4. Crear una rama a partir de la rama develop
``` shell=
$ git checkout -b feature-frecuencias
```

**Figura.** Crear una nueva rama.
A continuación, podemos iniciar la creación de un nuevo ABM de frecuencias.
## Creación de ABM
## Resultado
---
:::info
A continuación se podrá visualizar la vista final del ABM correspondiente a la entidad de *FRECUENCIAS*.
:::
* Listado de Frecuencia, cuenta con las operaciones de *Agregar*, *Filtrar*, *Ordenar*, *Editar* y *Eliminar*.
<p style="text-align:center">
<img src="https://i.imgur.com/s08eYvj.png" style="border-radius:12px" />
</p>
* Formulario de Frecuencia, cuenta con las operaciones de *Guardar*, *Guardar y Crear* y *Cancelar*.
<p style="text-align:center; ">
<img src="https://i.imgur.com/xNyXiij.png" style="border-radius:12px" />
</p>
:::info
Se asume que el proyecto fue creado y configurado
:::
### Paso 1 - Creación de estructura
* Creación de la estructura de datos a utilizar
* Para la creación es necesario ir a la carpeta *app/model* del proyecto y agregar un archivo con el nombre de *frecuencia.ts* y copiar el siguiente contenido
---
```javascript=
import { BaseModel } from "./base-model";
export interface Frecuencia extends BaseModel{
id: number;
abreviatura: string;
nombre: string;
}
```
* La interfaz *BaseModel* contiene los campos comunes de las entidades
* Angular nos permite definir estructura de datos utilizando *CLASES* e *INTERFACES*.
* Nosotros optamos por utilizar *INTERFACES* para la definición de la estructura de datos de la entidad *Frecuencia*.
### Paso 2 - Creación de configuración
* Creación de la configuracion general para el acceso al componente y acceso al servicio
* Para la creación es necesario ir a la carpeta *app/util* del proyecto y editar el archivo con el nombre de *routes.ts* y copiar el siguiente contenido
---
```javascript=
FRECUENCIA: {
SERVICE_URL: 'frecuencias',
PATH: 'frecuencias',
TITLE: 'Frecuencias'
},
```
* Esta linea de código generalmente se agrega al final del listado de valores definido en el archivo.
* El key *SERVICE_URL*, contiene el path relativo del servicio backend a utilizar.
* El key *PATH*, contiene el path relativo para el acceso al componente a desarrollar.
* El key *TITLE*, contiene el nombre del menú, el mismo valor será utilizado para los títulos a ser mostrado en el listado, formulario y el breadcrumb.
### Paso 3 - Creación de Servicio
* Creación del servicio que proveerá los datos al listado y procesará los datos del formulario
> Para poder generar un servicio es necesario tener instalado el **Angular CLI**
> En el proyecto existe un servicio base del cual se debe extender los servicios creados
> El nombre del servicio base es **ServiceBase**, es una clase abstracta y genérica, la clase debe ser importado desde el paquete *util/service-base*
> La clase ServiceBase encapsula las operaciones comunes de un servicio como ser llamadas vía HTTP de los métodos GET, PUT, DELETE, POST y la transformación de parámetros requeridos por el servicio backend.
* Definición de Servicio Base :
---
```javascript=
import { HttpClient, HttpHeaders } from "@angular/common/http";
import { Observable, Subject } from "rxjs";
import { tap } from "rxjs/operators";
import { IsTokenPresente } from "../auth/auth-control-permiso";
import { ServiceResponse } from "../model/response";
import { GlobalMessage } from "./global-message";
import { GlobalService } from "./global.service";
export abstract class ServicioPersonalizadoBase<T> {
urlBase!: string;
httpClient: HttpClient | null = null;
globalServices: GlobalService | null = null;
urlServicio!: string;
editForm: boolean = false;
saveForm: boolean = false;
deleteForm: boolean = false;
privilegios: any = [];
camposFiltro = [
'filtros',
'permissionURL',
'filtrosPagina',
'filtrosCantidad',
'filtrosOrdenarPor',
'filtrosDireccion'
];
constructor(urlServicio: string) {
this.urlServicio = urlServicio;
this.obtenerPrivilegios();
}
/**
*
* @param id : identificador del registro
* @description : concatena el ruta base con la ruta relativa
* @returns string : path base + path relativo
*/
obtenerRutaServicio(id?: any): string {
........
}
/**
*
* @param data : El objeto que contiene los atributos a ser enviados al backend
* @param msg : El mensaje que se mostrará cuando se realice la acción.
* @returns Observable<T>
*/
@IsTokenPresente()
crear(data: any, msg?:string): Observable<T> {
........
}
/**
*
* @param data : El objeto que contiene los atributos a ser enviados al backend
* @param msg : El mensaje que se mostrará cuando se realice la acción.
* @returns Observable<T>
*/
@IsTokenPresente()
actualizar(data: any, msg?:string): Observable<T> {
........
}
/**
*
* @param data : El objeto que contiene el *ID* del registro para la eliminacion
* @param msg : El mensaje que se mostrará cuando se realice la acción.
* @returns Observable<T>
*/
@IsTokenPresente()
eliminar(data: any, msg?:string): Observable<any> {
........
}
/**
*
* @param data : objeto literal que contiene el *ID* del registro de busqueda
* @returns Observable<T>
*/
@IsTokenPresente()
obtenerPorId(data: any): Observable<ServiceResponse<T>> {
........
}
/**
*
* @param data : objeto literal que contiene los valores para la busqueda del registro
* @description: la peticion realizada sera via queryParams con nombre *filtros*
* @returns Observable<any>
*/
@IsTokenPresente()
obtenerPdfPorFiltroEnJson(data: any): Observable<any> {
........
}
/**
*
* @param data : objeto literal que contiene los valores para la busqueda del registro
* @description: la peticion realizada sera via queryParams
* @returns Observable<any>
*/
@IsTokenPresente()
obtenerPdfFiltrodoEnURL(data: any): Observable<any> {
........
}
/**
*
* @param data : objeto literal que contiene los valores para listar los registros desde el backend
* @description: la peticion realizada contendra los filtros en queryParams con nombre *filtros*
* @returns Observable<any>
*/
@IsTokenPresente()
obtenerListadoFiltradoEnJson(data: any): Observable<any> {
........
}
/**
*
* @param data : objeto literal que contiene los valores para listar los registros desde el backend
* @description: la peticion realizada utililizara todos los atributos como queryParams
* @returns Observable<any>
*/
@IsTokenPresente()
obtenerListadoFiltradoEnURL(data: any): Observable<ServiceResponse<T>> {
........
}
/**
*
* @param headers : objeto literal que contiene las propiedades para agregar a la cabecera de la peticion HTTP
* @returns Object : {headers: Object}
*/
protected crearParametrosCabeceraPeticionHTTP(headers: any = null): { headers: HttpHeaders } {
........
}
/**
*
* @param data : objeto literal que contiene los atributos para la creacion del objeto a enviar al backend
* @returns
*/
protected crearObjetoEntidadDesdeParametro(data: any) {
........
}
/**
*
* @param data : objeto literal que contiene los atributos para la creacion de la paginacion
* @returns string : pagina=0&cantida=10&ordenadoPor=nombre&direccion=asc
*/
public crearParametroPaginacion(data: any) {
........
}
/**
*
* @param data : objeto literal que contiene los atributos para la creacion del objeto filtro
* @param nombreFiltro : nombre del atributo donde se buscara el objeto literal
* @returns Object : {nombre:valor}
*/
crearObjetoFiltro(data: any, nombreFiltro: string): Object {
........
}
/**
*
* @param data : objeto literal que contiene los atributos para la creacion de queryParams con nombre *filtro*
* @param queryField : datos de paginacion y ordenamiento que seran agregados al filtro generado
* @returns string : pagina=0&cantidad=10&filtro={nombre:valor}
*/
crearParametroFiltro(data: any, queryField: any) {
........
}
/**
*
* @param data : objeto literal que contiene los atributos para la creacion de queryParams
* @returns string : nombre=valor&nombre=valor
*/
public crearParametroFiltroEnURL(data: any, queryField?: any) {
........
}
/**
*
* @param data : objeto literal que contiene los atributos a enviar al backend
* @returns : Observable<any>
*/
@IsTokenPresente()
obtenerArchivoPorId(data: any): Observable<any> {
........
}
/**
*
* @param data : objeto literal que contiene los atributos a enviar al backend
* @property data.archivo : objeto binario a enviar como multipart/form-data
* @returns : Observable<any>
*/
@IsTokenPresente()
enviarArchivoBinario(data: any) {
........
}
/***
* @description : agregar el valor del path relativo para el servicio
* @example : this.urlServicio = 'MENU_URL.MEDIDA.SERVICE_URL'
*/
agregarUrlRelativaServicio(url: string): void {
this.urlServicio = url;
}
/**
* @description: obtiene la referencia al servicio global
* @returns GlobalService
*/
getGlobalService(): GlobalService | null {
return this.globalServices;
}
/***
* @description : obtener el valor del path relativo del servicio
* @example : this.urlServicio = 'MENU_URL.MEDIDA.SERVICE_URL'
*/
obtenerUrlRelativaServicio(): string {
return this.urlServicio;
}
/**
* @description : obtiene el privilegio asociado a la url del servicio
*/
obtenerPrivilegios(): any {
const menu: MenuService = AppInjector.getServiceInjector("menuService");
this.privilegios = menu.obtenerPrivilegios().find(item => String(item.urlServicio).replace("/", "") == this.urlServicio);
return this.privilegios;
}
}
```
* Cada método definido en el servicio base utiliza el servicio [HttpClient](https://angular.io/api/common/http/HttpClient) para la comunicación con el recurso externo backend.
* Para crear el archivo *frecuencia.service.ts*, utilizamos el Angular CLI
``` shell=
$ ng generate service --project siare-front pages/presupuesto/services/frecuencia --skip-tests
```
o también:
``` shell=
$ ng generate service --project siare-front pages/presupuesto/services/frecuencia --skipTests
```
éste comando genera el archivo de servicio y la estructura base.
> el parámetro --skipTests o --skip-tests: excluye la creación del archivo de test
* El Resultado del comando anterior es :
```javascript=
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class FrecuenciaService {
constructor() { }
}
```
* A la estructura generada debemos agregarle lo siguiente :
* La referencia del servicio *HttpClient*
* La referencia del servicio *GlobalService*
* La herencia de la clase base *ServiceBase*
* Realizando eso cambios el resultado final queda de la siguiente manera :
```javascript=
// Importaciones de las clases utilizadas
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { GlobalService, ServicioPersonalizadoBase } from 'ngx-siare-common';
import { environment } from '../../environments/environment';
import { Frecuencia } from '../model/frecuencia';
import { MENU_URLS } from '../util/routes';
//Decorador de Servicio
@Injectable({
// metadato
providedIn: 'root'
})
export class FrecuenciaService extends
ServicioPersonalizadoBase<ServiceResponse<Frecuencia>>
{
// Inyección de instancia de HttpClient y GlobalService
constructor(
private http: HttpClient,
private globalSvc: GlobalService,
) {
//Se agrega URL del servicio backend
super(MENU_URLS.FRECUENCIA.SERVICE_URL);
//Dado que la clase abstracta no puede instanciarse
// se inicializan los atributos de la clase base
this.httpClient = http;
this.globalServices = globalSvc;
// Se agrega ruta base de navegacion
this.urlBase = environment.URL_BASE;
}
}
```
### Paso 4 - Creación de componente de Formulario
* En este pasó se mostrará las operaciones necesarias para la creación del componente de formulario.
* Esquema de dependencias del componente de formulario

> Los gráficos coloreados son los archivos requeridos para completar el proceso de desarrollo de un componente de formulario
:::info
Todos los componentes que serán mostrados al usuario final están agrupados dentro del módulo de `PageModule`, por lo tanto los componentes serán alojados dentro del directorio `src/app/pages` del proyecto.
Para poder generar un componente es necesario tener instalado el **Angular CLI**
En el proyecto existe una clase abstracta del la cual se debe extender.
El nombre de la clase abstracta es **TablaPersonalizadaAbstract**, la clase debe ser importado desde la librería *ngx-siare-common*.
La clase TablaPersonalizadaAbstract encapsula las operaciones comunes de un formulario como ser obtención de parámetros, agregar titulos, crear mensajes de alerta, procesar operaciones de creación y actualización.
:::
* Definición de la clase *FormularioPersonalizadoComponent* :
```javascript=
import { AfterViewInit, ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
import { BehaviorSubject } from "rxjs";
import { CustomModalService } from "../custom-modal/custom-modal.service";
import { CustomToastService } from "../custom-toast/custom-toast.service";
import { MODAL_OPERATION } from "../model/modal-data";
import { ModeloFormularioPersonalizado } from "../model/modelo-form-personalizado";
import { GlobalService } from "../util/global.service";
import { ServicioPersonalizadoBase } from "../util/servicio-base-personalizado";
import { FormularioDatoPersonalizadoService } from "./formulario-dato-personalizado.service";
import { FormularioPersonalizadoAbstractComponent } from "./formulario-personalizado-abstract";
@Component({
selector: 'app-formulario-personalizado',
templateUrl: './formulario-personalizado.component.html',
styleUrls: [],
})
export class FormularioPersonalizadoComponent extends FormularioPersonalizadoAbstractComponent implements OnInit, OnDestroy, AfterViewInit {
@Input() modelo: any | null = null;
@Input() nombreTitulo: any | null = null;
@Input() servicio: ServicioPersonalizadoBase<any> | null = null;
@Input() mensajeEmergenteService: CustomToastService | null = null;
@Input() controles: BehaviorSubject<any> = new BehaviorSubject([]);
@Output() inputChanged: EventEmitter<any> = new EventEmitter();
@Output() selectChanged: EventEmitter<any> = new EventEmitter();
formularioService: FormularioDatoPersonalizadoService;
mostrarBotonContinuar = true;
modalOperation = MODAL_OPERATION;
constructor(
public modalServiceInstance: CustomModalService,
public modalInstance: NgbActiveModal,
private detectorCambios: ChangeDetectorRef,
public globalServiceIntance: GlobalService,
public customToast: CustomToastService
) {
super();
this.formularioService = new FormularioDatoPersonalizadoService();
this.modalService = modalServiceInstance;
this.globalService = this.globalServiceIntance;
this.toasService = customToast;
}
ngAfterViewInit() {
.......
}
cambiarModoEdicion(controlesOrdenado: Array<any>) {
.......
}
inicializarValoresInternos() {
.......
}
ngOnInit(): void {
}
ngOnDestroy(): void {
this.restablecerValorIniciales();
}
guardar(cerrarFormulario: boolean = true) {
.......
}
crearContinuar(): void {
this.guardar(true);
}
actualizar(): void {
.......
}
crear(): void {
this.guardar(false);
}
obtenerDatosFormulario() {
.......
}
obtenerPropiedadRequerida() {
.......
}
cerrarFormulario() {
.......
}
eventoInput(event: any) {
this.inputChanged.next(event);
}
eventoSelect(event: any) {
this.selectChanged.next(event);
}
}
}
```
* Para crear el componente, utilizamos el Angular CLI
```javascript=
ng generate component --project siare-front pages/frecuenciasForm --skipTests --inline-style
```
* Este comando genera los siguientes archivos :
* *frecuencias-form.component.ts* : gestiona los datos del servicio y los expone como atributo o métodos de la clase
* *frecuencias-form.component.html* : gestiona la estructura de la presentación de datos
* *frecuencias-form.component.css* : gestiona la regla de estilos a ser aplicados en la presentación de los datos
* Estos archivos son parte del componente de `FrecuenciasForm`, por defecto el Angular CLI define la estructura base del componente y deja al desarrollador la responsabilidad de agregar la lógica correspondiente y la estructura de presentación.
* Los archivos generados tiene el siguiente contenido :
* frecuencias-form.component.ts
```javascript=
//Importaciones requeridas
import { Component, OnInit } from '@angular/core';
//Decorador de componente
@Component({
//Metadatos
selector: 'app-frecuencias-form',
templateUrl: './frecuencias-form.component.html',
styleUrls: ['./frecuencias-form.component.css']
})
export class FrecuenciasFormComponent implements OnInit {
constructor() { }
// Método de ciclo de vida del componente
ngOnInit(): void {
}
}
```
* frecuencias-form.component.html
```htmlembedded=
<p>frecuencias-form works!</p>
```
* frecuencias-form.component.css
```htmlembedded=
<archivo vacío>
```
* Posterior a la creación de los archivos del componente es necesario modificarlo según el siguiente ejemplo :
* Archivo *frecuencias-form.component.ts*
---
```javascript=
// Importaciones de las clases utilizadas
import { AfterViewInit, Component,
ViewChild, Inject, Optional } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { ControlPermiso,
CustomToastService,
CustomValidators,
FormularioPersonalizadoComponent,
ModeloFormularioPersonalizado,
MODAL_DIALOG_DATA } from 'ngx-siare-common';
import { FrecuenciaService } from '../../services/frecuencia.service';
import { MENU_URLS } from '../../util/routes';
//Decorador de componente
@Component({
//Metadato
selector: 'app-frecuencias-form',
templateUrl: './frecuencias-form.component.html',
styles: [
]
})
export class FrecuenciasFormComponent extends ControlPermiso
implements AfterViewInit {
//Variable de instancia
controles: any = {};
titulo: string = MENU_URLS.FRECUENCIA.TITLE;
params: any;
showSaveContinueButton = true;
@ViewChild(FormularioPersonalizadoComponent)
formulario!: FormularioPersonalizadoComponent;
//Inyección de dependencia
//Constructor de formulario
constructor(
// Servicio de mensaje emergente
public mensajeEmergenteService:
CustomToastService,
// Servicio de gestión de datos del Backend
public servicio: FrecuenciaService,
// Referencia de Modal activo
@Optional() @Inject(MODAL_DIALOG_DATA)
public modalInstance: NgbActiveModal,
) {
super();
this.permisos = this.servicio?.privilegios;
}
//Método utilizado para la creacion de los controles del formulario
buildForm() {
this.controles =
this.definicionFormulario(this.params);
}
construirFormularioSinParametro() {
if (!this.params) {
this.buildForm();
}
}
// Método de ciclo de vida del componente :
// se ejecuta cuando los elementos
//visuales fueron construidos
ngAfterViewInit(): void {
this.construirFormularioSinParametro();
this.formulario.modelo = this.controles;
this.formulario.mostrarBotonContinuar =
this.showSaveContinueButton;
this.formulario.inicializarValoresInternos();
}
definicionFormulario(datoEntidad?: any):
{
[key: string]:
ModeloFormularioPersonalizado<any>
}
{
return {
id: {
orden: 1,
valor: datoEntidad ? datoEntidad.id : 0,
etiqueta: 'ID',
},
abreviatura: {
orden: 3,
valor: datoEntidad ? datoEntidad.abreviatura : '',
etiqueta: 'ABREVIATURA',
},
nombre: {
orden: 2,
valor: datoEntidad ? datoEntidad.nombre : '',
etiqueta: 'NOMBRE',
requerido: true,
validadores: [ CustomValidators.required()]
},
descripcion: {
orden: 4,
valor: datoEntidad ? datoEntidad.descripcion : '',
etiqueta: 'DESCRIPCION',
},
activo: {
orden: 5,
tipoControl: 'combobox',
valor: datoEntidad ? String(datoEntidad.activo) : "true",
etiqueta: 'ACTIVO',
opciones: [
{
clave: 'true',
valor: 'SI'
},
{
clave: 'false',
valor: 'NO'
}
]
},
}
}
}
```
* Con estos cambios agregados ya se cuentan con la funcionalidad común de un formulario
* La acción siguiente consiste en actualizar la plantilla HTML a fin de crear la estructura los datos a mostrar y agregar el comportamiento deseado.
* Nos posicionamos dentro del archivo *frecuencias-form.component.html* y copiamos lo siguiente:
```htmlembedded=
<app-formulario-personalizado
[modelo]="controles"
[servicio]="servicio"
[nombreTitulo]="titulo"
[mensajeEmergenteService]="mensajeEmergenteService">
</app-formulario-personalizado>
```
* Con estos cambios ya se cuenta con la estructura y el comportamiento para mostar un formulario al usuario final
### Paso 5 - Creación del componente de Listado
* En este pasó se mostrará las operaciones necesarias para la creación del componente de listado.
* Esquema de dependencias del componente de listado

> Los gráficos coloreados son los archivos requeridos para completar el proceso de desarrollo de un componente de listado
:::info
Todos los componentes que serán mostrados al usuario final están agrupados dentro del módulo de `PagesModule`, por lo tanto los componentes serán alojados dentro del directorio `src/app/pages` del proyecto.
Para poder generar un componente es necesario tener instalado el **Angular CLI**
En el proyecto existe una clase abstracta de listado del cual se debe extender.
La clase TablaPersonalizadaAbstract encapsula las operaciones comunes de un listado como ser obtención de registros, ordenamiento, paginación, procesamiento de búsquedas, edición, creación y eliminación de registros.
:::
* Definición de la clase *PrimeTablePersonalizadoComponent* :
```javascript=
import { DatePipe } from '@angular/common';
import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { saveAs } from 'file-saver';
import { jsPDF } from 'jspdf';
import 'jspdf-autotable';
import { ColumnInput, UserOptions } from 'jspdf-autotable';
import { SortEvent } from 'primeng/api';
import { Paginator } from 'primeng/paginator';
import { Table } from 'primeng/table';
import { Observable } from 'rxjs';
import { first } from 'rxjs/operators';
import { ControlPermiso } from '../auth/auth-control-permiso';
import { CustomModalComponent } from '../custom-modal/custom-modal.component';
import { CustomModalService } from '../custom-modal/custom-modal.service';
import { CustomFilterName } from '../model/custom-filter-name';
import { DataTablePesonalizadoColumn } from '../model/data-table-personalizado-column';
import { defaultFilterParam, FilterParam } from '../model/filter-params';
import { defaultParam, MODAL_OPERATION } from '../model/modal-data';
import { ServiceResponse } from '../model/response';
import { DefinicionAccion, DefinicionOperacion } from '../tabla-personalizada/tabla-personalizada.component';
import { GlobalMessage } from '../util/global-message';
import { GlobalSettings } from '../util/global-settings';
import { GlobalService } from '../util/global.service';
export interface jsPDFCustom extends jsPDF {
autoTable: (options: UserOptions) => void;
}
@Component({
selector: 'app-prime-table-personalizado',
templateUrl: './prime-table-personalizado.component.html',
styleUrls: ['./prime-table-personalizado.component.scss'],
providers: [
GlobalService,
DatePipe
]
})
export class PrimeTablePersonalizadoComponent extends ControlPermiso implements OnInit, AfterViewInit {
@Input() operaciones: Array<DefinicionOperacion> = [];
@Input() nombreFiltros: Array<CustomFilterName> = [];
@Input() nombreColumnas: Array<DataTablePesonalizadoColumn> = [];
@Input() fnCallBack!: (params: any) => Observable<any>
@Input() ocultarFiltro: boolean = false;
@Input() ocultarBotonAgregar: boolean = false;
@Input() ocultarPaginacion: boolean = false;
@Input() ocultarBotonExtras: boolean = false;
@Input() ocultarSelector: boolean = true;
@Input() paginacionEnMemoria: boolean = false;
@Input() ocultarBotonVincular: boolean = true;
@Input() fnCallItems!: (params: any) => boolean;
@Input() responsiveLayout: string = 'stack';
@Input() tipoSelector: string = 'checkbox';
@Input() ocultarOpciones: boolean = false;
@Input() vistaEliminar: boolean = false;
@Output() filaChange: EventEmitter<DefinicionAccion> = new EventEmitter();
@Output() accionChange: EventEmitter<any> = new EventEmitter();
@Output() selectedChange: EventEmitter<any> = new EventEmitter();
@Output() filtroChange: EventEmitter<any> = new EventEmitter();
cantidadRegistroPagina = GlobalMessage.PAGINATOR_RANGE;
@ViewChild(Table) private dataTable!: Table;
@ViewChild(Paginator) paginator!: Paginator;
data: Array<any> = [];
acciones = GlobalMessage.LISTADO_ACCIONES;
gestionVisibilidadColumna: Array<boolean> = [];
isMostrarFiltro: boolean = false;
totalRegistro: number = 0;
cantidadRegistro: number = this.cantidadRegistroPagina[0];
selectedColumns: Array<any> = [];
selectedItems: Array<any> = [];
parameter: FilterParam = JSON.parse(JSON.stringify(defaultFilterParam));
selectedItem: any;
formularioMensajes = GlobalMessage.FORMS;
etiquetas = GlobalMessage.VIEW_LABELS;
constructor(
public globalService: GlobalService,
private datePipe: DatePipe,
private modalService: CustomModalService,
private route: ActivatedRoute
) {
super();
if (route?.snapshot) {
this.permisos = route.snapshot?.data?.permisos;
}
}
ngOnInit(): void {
this.nombreColumnas.forEach(col => {
this.gestionVisibilidadColumna.push(true);
});
this.parameter.filtrosCantidad = this.cantidadRegistroPagina[0];
if (this.paginacionEnMemoria) {
this.ocultarPaginacion = true;
}
this.obtenerRegistrosDesdeServicio();
}
ngAfterViewInit(): void {
setTimeout(() => {
this.dataTable.paginator = this.paginacionEnMemoria;
}, 10);
}
llamadaFormulario(data: any = null) {
const accion = data ? { operacion: data } : null;
this.accionChange.next(accion);
}
operacionFila(elemento: any, accion: any): void {
this.filaChange.next({ registro: elemento, operacion: accion });
}
operacionCampoBoolean(data: any, nombreCampo: any) {
const elemento: any = Object.assign({}, data);
elemento[nombreCampo] = !elemento[nombreCampo];
this.operacionFila(elemento, this.acciones[3]);
}
operacionSelect() {
this.selectedChange.next(this.selectedItems);
}
cambiarEstiloFilaPorBorrado(element: any) {
if (element?.borrado === true) {
return 'strike';
} else if (element?.activo === false) {
return 'row-gray';
} else {
return '';
}
}
obtenerValorDesdeFila(row: any, columna: any): any {
const nombresPropiedad: Array<string> = columna?.data?.split('.');
let valorCampo: any = '';
if (nombresPropiedad && nombresPropiedad.length > 1) {
valorCampo = this.obtenerCampoDesdeObjeto(row, nombresPropiedad);
} else {
if (row) {
valorCampo = `${row[columna?.data]}`;
}
}
return this.formatearValorPorTipoCampo(valorCampo, columna?.type);
}
formatearValorPorTipoCampo(valorCampo: any, tipo: string) {
......
return valorCampo;
}
obtenerRegistrosDesdeServicio() {
......
}
filtrar(datoFiltro: any) {
......
}
sort(data: SortEvent) {
......
}
page(pageEvent: any) {
......
}
obtenerCampoDesdeObjeto(obj: any, nombreCampos: Array<string>): string {
......
}
formatearRegistrosParaExportarPDF( ) {
......
})
return {
columns: nombreCabecera,
body: registros,
};
}
formatearRegistrosParaExportarEXCEL( ) {
......
})
return {
columns: nombreCabecera,
body: registros,
};
}
exportPdf() {
......
}
exportExcel() {
......
}
saveAsExcelFile(buffer: any, fileName: string): void {
......
}
copiarDatos() {
this.selectedChange.next(this.selectedItems);
}
findItemChecked(data: any): boolean {
if (this.fnCallItems != undefined) {
return this.fnCallItems(data)
} else {
return false;
}
}
operacionRadio() {
......
}
resetParamsConfig(): void {
this.parameter = {
filtrosPagina: 0,
filtrosCantidad: this.cantidadRegistro,
filtrosOrdenarPor: '',
filtrosDireccion: '',
filtros: this.parameter.filtros ?? ''
}
}
onSelectChange(event: any) {
this.selectedItems = event;
}
}
}
```
* Para crear el componente, utilizamos el Angular CLI
``` shell=
$ ng generate component --project siare-front pages/frecuenciasList --skipTests --inline-style
```
* Este comando genera los siguientes archivos :
* *frecuencias-list.component.ts* : gestiona los datos del servicio y los expone como atributo o métodos de la clase
* *frecuencias-list.component.html* : gestiona la estructura de la presentación de datos
* *frecuencias-list.component.css* : gestiona la regla de estilos a ser aplicadas en la presentación de los datos
* Estos archivos son parte del componente de `FrecuenciasList`, por defecto el Angular CLI define la estructura base del componente y deja al desarrollador la responsabilidad de agregar la lógica correspondiente y la estructura de presentación.
* Los archivos generados tiene el siguiente contenido :
* frecuencias-list.component.ts
```javascript=
//Importaciones requeridas
import { Component, OnInit } from '@angular/core';
//Decorador de componente
@Component({
//Metadatos
selector: 'app-frecuencias-list',
templateUrl: './frecuencias-list.component.html',
styleUrls: ['./frecuencias-list.component.css']
})
export class FrecuenciasListComponent implements OnInit {
constructor() { }
// Método de ciclo de vida del componente
ngOnInit(): void {
}
}
```
* frecuencias-list.component.html
```htmlembedded=
<p>frecuencias-list works!</p>
```
* frecuencias-list.component.css
```htmlembedded=
<archivo vacío>
```
* Posterior a la creación de los archivos del componente es necesario modificarlo según el siguiente ejemplo :
* Archivo frecuencias-list.component.ts
---
```javascript=
// Importaciones de las clases utilizadas
import { AfterViewInit, Component, Inject, OnInit, Optional, ViewChild } from "@angular/core";
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
import { CustomFilterName, CustomModalService, DataTablePesonalizadoColumn, FilterParam,
MODAL_DIALOG_DATA,
MODAL_OPERATION, PrimeTablePersonalizadoComponent,
TablaPersonalizadaAbstract,
TituloService } from "ngx-siare-common";
import { FrecuenciaService } from "../../services/frecuencia.service";
import { MENU_URLS } from "../../util/routes";
import { FrecuenciasFormComponent } from "../frecuencias-form/frecuencias-form.component";
//Decorador de Componente
@Component({
selector: 'app-frecuencias-list',
templateUrl: './frecuencias-list.component.html'
})
export class FrecuenciasListComponent extends TablaPersonalizadaAbstract implements OnInit, AfterViewInit {
// Variable de instancia que inicializa
@ViewChild(PrimeTablePersonalizadoComponent) referenciaTabla: PrimeTablePersonalizadoComponent | null = null;
ruta: Array<any> = ['Presupuesto', 'Catálogo'];
fnServiceCallBack: any;
modalOperation = MODAL_OPERATION;
fnListadoCallBack: any;
parametros?: FilterParam;
// Inyección de instancia de servicios
constructor(
private serviceInstance: FrecuenciaService,
private modalServiceInstance: CustomModalService,
private tituloServiceInstance: TituloService,
@Optional() public modalInstance: NgbActiveModal,
@Optional() @Inject(MODAL_DIALOG_DATA)
) {
super();
this.entityService = serviceInstance;
this.modalService = modalServiceInstance;
this.tituloService = tituloServiceInstance;
//Se obtiene permiso desde el servicio base
this.permisos = this.serviceInstance.privilegios;
//Se valida que el permiso este presente
//caso contrario se vuelve a cargar el
//permiso desde la clase base
if (!this.permisos) {
this.permisos = this.serviceInstance.obtenerPrivilegios();
}
// function to be executed to get data from service
this.fnServiceCallBack = (params: FilterParam) => {
this.parametros = params;
return this.serviceInstance.obtenerListadoFiltradoEnJson(params);
};
}
// Método de ciclo de vida del componente :
// se ejecuta cuando el componente fue construido
ngOnInit(): void {
if (this.dialogData) {
this.titulo = this.dialogData.titulo;
} else {
this.setTitle();
}
this.ruta.push(this.tituloService?.tituloData.getValue());
}
// Método de ciclo de vida del componente :
// se ejecuta cuando la vista fue construida
ngAfterViewInit(){
this.dataTable = this.referenciaTabla;
//Asignacion de permiso
// Esto se realiza para asegurar
// la correcta asignacion de permiso
// sin tener en cuenta desde donde es
//llamado el componente
if (this.referenciaTabla) {
this.referenciaTabla.permisos = this.serviceInstance.privilegios;
}
}
abirFormulario(element?: any) {
this.modalServiceInstance?.openForm(FrecuenciasFormComponent, {
element: element, data: {
titulo: MENU_URLS.FRECUENCIA.TITLE,
}
}).
result.then((operationResult) => {
this.referenciaTabla?.filtrar(this.parametros?.filtros);
},
reason => {
this.referenciaTabla?.filtrar(this.parametros?.filtros);
}
);
}
eliminar(element: any) {
const message = `${this.mensajeEliminacion} ${element.nombre}`;
this.configurarEliminacion(element.id, message);
}
obtenerDefinicionTabla(): Array<DataTablePesonalizadoColumn> {
return [
{ data: 'nombre', title: 'NOMBRE', colStyle:"text-align:left", orderable: true },
{ data: 'abreviatura', title: 'ABREVIATURA', colStyle:"text-align:left", orderable: true },
{ data: 'descripcion', title: 'DESCRIPCIÓN', colStyle:"text-align:left"},
{ data: 'activo', title: 'ACTIVO' },
];
}
obtenerDefinicionFiltro(): Array<CustomFilterName> {
return [
{ key: 'nombre', label: 'NOMBRE' },
{ key: 'abreviatura', label: 'ABREVIATURA' },
{ key: 'descripcion', label: 'DESCRIPCIÓN' },
{
key: 'activo', label: 'ACTIVO', type: 'select',
options: [
{
key: 'Todos',
value: '',
selected: 'y'
},
{
key: 'Si',
value: 'true'
},
{
key: 'No',
value: 'false'
}
]
},
{
key: 'borrado', label: 'BORRADO', type: 'select',
options: [
{
key: 'Todos',
value: ''
},
{
key: 'Si',
value: 'true',
},
{
key: 'No',
value: 'false',
selected: 'y'
}
]
},
];
}
}
```
:::info
Cuando se realiza la carga del listado desde el menú los permisos son obtenidos desde el objeto
**activedRoute**, en caso de que la navegación sea realizada vía modal los permisos son obtenidos desde el **servicio** de la entidad
:::
* Con estos cambios agregados ya se cuentan con la funcionalidad común de un listado.
* La acción siguiente consiste en actualizar la plantilla HTML a fin de estructura los datos a mostrar y agregar el comportamiento deseado.
* Nos posicionamos dentro del archivo frecuencias-list.component.html y copiamos lo siguiente:
```htmlembedded=
<app-camino-miga [ruta]=ruta></app-camino-miga>
<app-prime-table-personalizado
[titulo]=titulo
[fnCallBack]="fnServiceCallBack"
[nombreColumnas]="obtenerDefinicionTabla()"
[nombreFiltros]="obtenerDefinicionFiltro()"
(accionChange)="abirFormulario()"
(filaChange)="capturarOperacion($event)">
</app-prime-table-personalizado>
```
* Con estos cambios ya se cuenta con la estructura y el comportamiento para mostar un listado al usuario final
### Paso 6 - Asignación de ruta de navegación
* En este paso se culminará el proceso de creación del ABM de *Frecuencias*, para ello hay que ir al directorio `src/app/pages` y editar el archivo con nombre `pages-routing.module.ts`, la variable que se debe actualizar es la constante *pagesRoutes* en ella se debe agregando el siguiente código :
```javascript=
{
path: MENU_URLS.FRECUENCIA.PATH,
component: FrecuenciasListComponent,
data:{ titulo: MENU_URLS.FRECUENCIA.TITLE},
// Se agrega proveedor de permisos para el componente
resolve: { permisos: PermisoResolverService }
},
```
* Esta configuración debe ir dentro de la propiedad **children** del listado de rutas.
* Cada ruta del archivo `pages-routing.module.ts` define el mapeo de una URL con un componente, de esta forma se puede acceder a ellos.
* El orden de la definición de rutas sigue el critero del **path** más especifico al más general
* Una vez realizado ese cambio es momento de probar la ejecución del ABM creado.
:::warning
Tener en cuenta que el ABM creado requiere de la existencia del servicio backend.
:::
* Para ejecutar el proyecto ir a la consola y escriba el siguiente comando para deployar en el entorno local de nuestro equipo:
``` shell=
$ ng serve --project siare-front --open
```
:::info
Caso que sea la primera ejecución del proyecto es necesario construir la librería comun antes de ejecutar el proyecto principal
:::
:::info
La opción --open levanta un navegador y muestra la aplicación en ella, de otra forma se tendría que abrir una navegador y escribir la URL del la aplicación que por defecto es `http://localhost:4200`
El puerto por defecto utilizado por Angular CLI es el **4200**
:::
## Proceso final Git
:::info
Para culminar el proceso del desarrollo del ABM genérico es necesario ejecutar la siguiente secuencia de comandos
:::
``` shell=
$ git status
$ git add .
$ git commit -m "Creacion de frecuencias"
$ git fetch origin
$ git merge origin/develop
$ git push origin <nombre_rama>
```
## Vista Previa a la Eliminación
Para los casos que se requiera, es posible habilitar la funcionalidad de vista previa a la operación de eliminación.
El componente *PrimeTablePersonalizado* posee la opción booleana *vistaEliminar* (default *false*) que despliega el formulario de detalle en formato solo lectura con la operación de eliminar, de manera a poder visualizar los datos del regitro a ser eliminado.
Para habilitar la vista previa a la eliminación, ingrese el valor *true* en la opción *vistaEliminar*, como se muestra a continuación:
```htmlembedded=
<app-custom-spinner-loader></app-custom-spinner-loader>
<app-prime-table-personalizado
[titulo]=titulo
[fnCallBack]="fnServiceCallBack"
[nombreColumnas]="obtenerDefinicionTabla()"
[nombreFiltros]="obtenerDefinicionFiltro()"
[ocultarSelector]="true"
(accionChange)="abirFormulario()"
(filaChange)="capturarOperacion($event)"
[vistaEliminar]="true"> //Opción de vista previa a la eliminación
</app-prime-table-personalizado>
```
Con la opción *vistaEliminar* activada (*true*) en lugar del aviso de confirmación se mostrará el componente *Formulario Personalizado* en modo solo lectura con la opción de eliminar o cancelar la operación:

Para los formularios que no utilizan el componente *Formulario Personalizado* se deben realizar ajustes para incluir esta nueva funcionalidad, los pasos a ajustar serían:
1. Capturar variable que indica la operación de Vista previa a la eliminación
2. Incluir en el formulario la opción de *ELIMINAR*
3. Bloquear la edición de campos del formulario
**Capturar variable que indica la operación de Vista previa a la eliminación**
El componente *PrimeTablePersonalizado*, con la opción de vista previa a la eliminación activada, configura la instancia del servicio a utilizar ingresando valor *true* en la propiedad *deleteForm*.
En el método constructor del formulario se evalúa esta propiedad y se configura la variable local *modoEliminacion* de manera a utilizarla en los siguientes pasos:
```javascript=
constructor(
//...
// inyección de instancias en el constructor
//...
) {
super();
//... ...
// Instancia de servicio a utilizar
this.entityService = bienesService;
//... ...
// evaluar propiedad *deleteFrom* y configurar *modoEliminacion*
if (this.entityService.deleteForm) {
this.modoEliminacion = true;
} else {
this.modoEliminacion = false;
}
//... ...
this.buildForm(this.params);
}
```
**Incluir en el formulario la opción de *ELIMINAR***
En el código de la página html del formulario se ingresa la opción de *ELIMINAR*, utilizando la variable local *modoEliminacion* para mostrar/ocultar la opción según se requiera:
```htmlembedded=
<div class="modal-footer">
<button *ngIf="!params" type="button" class="btn btn-primary btn-md mr-1" [disabled]="entityForm.invalid"
(click)="save()">
{{ viewText.SAVE }}
</button>
<!-- condicionar la opción ACTUALIZAR según el modoEliminación -->
<button *ngIf="params && !modoEliminacion" (click)="update()" class="btn btn-outline-primary btn-md mr-1"
type="button" [disabled]="entityForm.invalid">
{{ viewText.UPDATE }}
</button>
<!-- condicionar la opción ELIMINAR según el modoEliminación -->
<button *ngIf="modoEliminacion" (click)="delete()" class="btn btn-primary btn-md mr-1" type="button">
{{ viewText.DELETE }}
</button>
<button type="button" class="btn btn-danger btn-md" aria-label="Close" (click)="closeEdit()">
{{ viewText.CANCEL }}
</button>
</div>
```
**Bloquear la edición de campos del formulario**
Cuando el formulario se despliega con la variable local *modoEliminacion* con el valor *true* se debe bloquear la edición de los campos del formulario, para ello se utiliza la configuración de la propiedad *disabled* del componente html *fieldset*:
```htmlembedded=
<div class="card-body">
<div class="content col-md-12 justify-content-center">
<form #bienForm="ngForm" nonvalidate [formGroup]="entityForm">
<!-- Configuración de propiedad *disabled* según *modoEliminación* -->
<fieldset [disabled]="modoEliminacion">
<!-- CAMPOS DEL FORMULARIO -->
</fieldset>
</form>
</div>
<div class="modal-footer">
<!-- BOTONES DE OPCIONES -->
</div>
</div>
```
## Mensaje Globales
```javascript=
export class GlobalMessage {
static APP_LABELS = {
APP_NAME: 'SIARE',
APP_NAME_SMALL: '',
APP_VERSION: 'Versión: ',
};
static ARTEFACTO_TIPOS = [
{ value: 1, text: 'PÁGINA' },
{ value: 2, text: 'SUB PÁGINA' }
];
static ACCOUNT_STATUS = [
{ value: '', text: 'TODOS' },
{ value: true, text: 'ACTIVO' },
{ value: false, text: 'INACTIVO' }
];
static GENERIC_SUCCESS_MSG = {
SUCCESS_CREATED: 'CREACIÓN EXITOSA',
SUCCESS_UPDATED: 'ACTUALIZACIÓN EXITOSA',
SUCCESS_DELETED: 'LA INFORMACIÓN SE BORRÓ CORRECTAMENTE',
};
static GENERIC_ERROR_MSG = {
PERMISSION_DENIED: 'PERMISOS INSUFICIENTES',
FAIL_OPERATION: 'OCURRIÓ UN INCONVENIENTE AL REALIZAR LA OPERACIÓN',
FAIL_NETWORK: 'OCURRIÓ UN INCONVENIENTE AL CONECTAR CON EL SERVIDOR',
TOKEN_INVALID: 'OCURRIÓ UN INCONVENIENTE AL PROCESAR EL TOKEN, FORMATO NO VÁLIDO',
RESOURCE_NOT_FOUND: 'NO SE ENCONTRO EL RECURSO SOLICITADO',
};
static PAGINATOR_RANGE = [15, 25, 50, 100];
static LISTADO_ACCIONES = ['EDITAR', 'ELIMINAR', 'CAMBIAR_ACTIVO', 'CAMBIAR_BOOLEAN', 'VINCULAR', 'EDITAR_EN_LINEA', 'RESTABLECER', 'VISTA_ELIMINAR'];
static ESTADO_ETIQUETA = {
ESTADO_COMITE: 1,
};
static VIEW_LABELS = {
ACCEPT: 'ACEPTAR',
CANCEL: 'CANCELAR',
CREATE: 'CREAR',
REGISTER: 'Agregar Registro',
SAVE: 'GUARDAR',
SAVE_CONTINUE: 'GUARDAR Y CREAR',
UPDATE: 'ACTUALIZAR',
DELETE: 'Eliminar',
CHANGE: 'CAMBIAR',
CONFIRM: 'CONFIRMAR',
EDIT: 'Editar',
ADD: 'AGREGAR',
CLEAR: 'LIMPIAR',
CLEAR_FORM: 'LIMPIAR FORMULARIO',
MODIFY: 'MODIFICAR',
EXIT: 'SALIR',
CLOSE: 'CERRAR',
RETURN: 'VOLVER',
ACTIVE: 'ACTIVO',
INACTIVE: 'INACTIVO',
ACTIVATED: 'ACTIVAR',
DEACTIVATED: 'DESACTIVAR',
CHANGE_PASS: 'CAMBIAR CONTRASEÑA',
CHANGE_MANAGEMENT: 'CAMBIAR GESTIÓN',
CHANGE_ADMIN: 'CAMBIAR ADMINISTRACIÓN',
ASOCIAR_OBJ: 'ES OBLIGATORIO ASOCIAR EL GRUPO Y EL SUB-GRUPO',
EXIS_OBJ: 'NO SE PUEDE ELIMINAR YA QUE EXISTEN REGISTROS ASOCIADOS',
LINK_REGISTER:'Vincular Registro',
SEARCH_FILTER:'Filtros de búsqueda',
EXPORT_PDF:'Exportar a PDF',
EXPORT_EXCEL:'Exportar a EXCEL',
SELECT:'Seleccionar',
LINK_SELECT:'Vincular registros seleccionados',
OPTIONS:'Opciones',
RESTORE:'Restablecer',
TOTAL_SHOW_RECORDS:'Total registros seleccionados {0}',
NOT_RECORDS:'Ningún dato disponible en esta tabla',
CLEAN:'Limpiar',
// titulos de dialogs
ATTENTION: 'ATENCIÓN',
IMPORTANT_INFO: 'INFORMACIÓN IMPORTANTE',
INVALID_FORM: 'FORMULARIO INVÁLIDO',
// inicio de mensaje de confirmacion generico
CONFIRM_CREATE: '¿CONFIRMA QUE DESEA REGISTRAR ',
CONFIRM_OPERATION: '¿CONFIRMA QUE DESEA REALIZAR LA OPERACIÓN ?',
CONFIRM_MIGRATE: '¿CONFIRMA QUE DESEA MIGRAR ?',
CONFIRM_MODIFY: '¿CONFIRMA QUE DESEA MODIFICAR ?',
CONFIRM_EDIT: '¿CONFIRMA QUE DESEA APLICAR LOS CAMBIOS PARA ',
CONFIRM_REMOVE: '¿REALMENTE DESEA BORRAR EL REGISTRO ?',
CONFIRM_RESTORE: '¿CONFIRMA QUE DESEA RESTAURAR EL REGISTRO ?',
RESTORE_SUCCESS: 'El registro fue restablecido correctamente ',
CONFIRM_UPDATE_PASSWORD: `¿CONFIRMA QUE DESEA CAMBIAR SU CLAVE?.
DEBERÁ INGRESAR AL SISTEMA CON SU NUEVA CLAVE LA PRÓXIMA VEZ QUE INICIE SESIÓN.`,
CONFIRM_NEW_PASSOWORD_RECOVERY: `¿CONFIRMA QUE LA CLAVE INGRESADA SERA SU NUEVA CLAVE DE ACCESO?`,
// operaciones
SUCCESS_OPERATION: 'OPERACIÓN EXITOSA',
SUCCESS_REMOVE: 'ÍTEM REMOVIDO',
//recovery
RECOVERY_CONFIRM: '¿CONFIRMA QUE DESEA ENVIAR UN CORREO DE RECUPERACIÓN PARA EL USUARIO ',
RECOVERY_SUCCESS_MSG: `HEMOS ENVIADO UN CORREO A SU CUENTA, SIGA LOS
PASOS DETALLADOS EN EL CORREO PARA RESTABLECER SU CLAVE.`,
RECOVERY_INFO: `PARA RECUPERAR SU CUENTA DEBE INGRESAR SU ALIAS DE USUARIO,
ESTAREMOS ENVIANDO UN LINK DE RECUPERACIÓN A SU CORREO ASOCIADO.`,
RECOVERY_BTN: 'SOLICITAR RECUPERACIÓN',
SECURE_PASS_TITLE: 'RESTRICCIONES Y RECOMENDACIONES PARA UNA CLAVE SEGURA',
SUCCESS_RECOVERY_CHANGE: 'CLAVE CAMBIADA, YA PUEDE INICIAR SESIÓN CON SU NUEVA CLAVE.',
RESTORE_PASSWORD_INFO: `INGRESE SU NUEVA CLAVE PARA CULMINAR EL PROCESO DE CREACIÓN/RESTAURACIÓN DE CLAVE,
ESTE ENLACE ES DE UN SOLO USO.`,
CHANGE_CURRENT_PASS: `ASEGURESE DE CREAR UNA CONTRASE FUERTE.`,
INVALID_RECOVERY_TOKEN_MSG: `NO SE REGISTRO RECUPERACIÓN DE CLAVE PARA EL ENLACE ACTUAL, EN ENLACE ES INVÁLIDO O HA EXPIRADO`,
SECURE_PASS: {
title: 'LOS ÍTEMS DEL LISTADO MARCADOS CON ASTERISCO (*) SON OBLIGATORIOS',
list: [
{ required: true, value: 'LONGITUD MÍNIMA 10 CARACTERES' },
{ required: true, value: 'DEBE CONTENER UNA O MÁS LETRAS MINÚSCULAS' },
{ required: true, value: 'DEBE CONTENER UNA O MÁS LETRAS MAYÚSCULAS' },
{ required: true, value: 'DEBE CONTENER UNO O MÁS NÚMEROS' },
{ required: true, value: 'DEBE CONTENER UNO O MÁS DE LOS SIGUIENTES SÍMBOLOS $@!%*?¿/~#.%^*()_-' },
{ required: true, value: 'SU NUEVA CLAVE DEBE SER DISTINTA A LAS ANTERIORES OCHO (8) CLAVES' },
{ required: false, value: 'EVITE QUE LA CLAVE TENGA SU NOMBRE, NOMBRE DE USUARIO O DATOS PERSONALES' },
{ required: false, value: 'EVITE QUE CONTENGA PALABRAS QUE PUEDAN APARECER EN UN DICCIONARIO' }
]
},
NOT_CONNECTION_FOUND: 'NO SE ENCONTRÓ CONEXIÓN ❌',
SESSION_EXPIRED: `SU SESIÓN HA EXPIRADO, FAVOR VUELVA A INGRESAR`
};
static ERROR_MSG = {
title: 'PÁGINA NO ENCONTRADA',
msg: 'LA PÁGINA A LA CUAL DESEA ACCEDER NO EXISTE O NO ESTÁ DISPONIBLE EN ESTE MOMENTO',
};
static FORMS = {
DEFAULT_DOESNT_MATCH_MESSAGE: "La propiedad [{0}] del objeto [{1}] con valor [{2}] no corresponde al patrón [{3}]",
DEFAULT_INVALID_URL_MESSAGE: "La propiedad [{0}] del objeto [{1}] con valor [{2}] no es una URL válida",
DEFAULT_INVALID_CREDIT_CARD_MESSAGE: "La propiedad [{0}] del objeto [{1}] con valor [{2}] no es un número de tarjeta de crédito válida",
DEFAULT_INVALID_RANGE_MESSAGE: "La propiedad [{0}] del objeto [{1}] con valor [{2}] no entra en el rango válido de [{3}] a [{4}]",
DEFAULT_INVALID_SIZE_MESSAGE: "La propiedad [{0}] del objeto [{1}] con valor [{2}] no entra en el tamaño válido de [{3}] a [{4}]",
DEFAULT_INVALID_MAX_MESSAGE: "La propiedad [{0}] del objeto [{1}] con valor [{2}] excede el valor máximo [{3}]",
DEFAULT_INVALID_MIN_MESSAGE: "La propiedad [{0}] del objeto [{1}] con valor [{2}] es menos que el valor mínimo [{3}]",
DEFAULT_INVALID_VALIDATOR_MESSAGE: "La propiedad [{0}] del objeto [{1}] con valor [{2}] no es válido",
DEFAULT_INVALID_INLIST_MESSAGE: "La propiedad [{0}] del objeto [{1}] con valor [{2}] no esta contenido dentro de la lista [{3}]",
DEFAULT_INVALID_BLANK_MESSAGE: "La propiedad [{0}] del objeto [{1}] no puede ser vacía",
DEFAULT_INVALID_NOT_EQUAL_MESSAGE: "La propiedad [{0}] del objeto [{1}] con valor [{2}] no puede igualar a [{3}]",
DEFAULT_INVALID_NULL_MESSAGE: "La propiedad [{0}] del objeto [{1}] no debe tener un valor nulo",
DEFAULT_INVALID_UNIQUE_MESSAGE: "La propiedad [{0}] del objeto [{1}] con valor [{2}] debe ser única",
DEFAULT_DATE_FORMAT: "dd/mm/yyyy",
DEFAULT_CREATED_MESSAGE: "{0} {1} creado correctamente",
DEFAULT_UPDATED_MESSAGE: "{0} {1} actualizado correctamente",
DEFAULT_DELETED_MESSAGE: "{0} {1} eliminado correctamente",
DEFAULT_NOT_FOUND_MESSAGE: "No se encuentra {0} con id {1}",
DEFAULT_LOCKING_MESSAGE: "Mientras usted editaba, otro usuario ha actualizado su {0}",
DEFAULT_ACTIVE_DISABLED_MESSAGE: "¿Realmente desea {0} el registro?",
};
}
```
## Validadores
```javascript=
import { AbstractControl, ValidationErrors, ValidatorFn } from "@angular/forms";
import { GlobalMessage } from "../util/global-message";
const EMAIL_REGEXP =
/^(?=.{1,254}$)(?=.{1,64}@)[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/;
/**
* Validator that requires the control have a non-empty value.
* See `Validators.required` for additional information.
*/
export function isEmptyInputValue(value: any): boolean {
/**
* Check if the object is a string or array before evaluating the length attribute.
* This avoids falsely rejecting objects that contain a custom length attribute.
* For example, the object {id: 1, length: 0, width: 0} should not be returned as empty.
*/
return value == null ||
((typeof value === 'string' || Array.isArray(value)) && value.length === 0);
}
export function hasValidLength(value: any): boolean {
// non-strict comparison is intentional, to check for both `null` and `undefined` values
return value != null && typeof value.length === 'number';
}
export class CustomValidators {
static trim(msg?: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (isEmptyInputValue(control.value)) {
return null; // don't validate empty values to allow optional controls
}
return String(control.value).trim() === '' ? { trim: { value: true, "msg": msg ?? GlobalMessage.FORMS.DEFAULT_INVALID_VALIDATOR_MESSAGE } } : null;
};
}
static required(msg?: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
return isEmptyInputValue(control.value) ? { 'required': { value: true, "msg": msg ?? GlobalMessage.FORMS.DEFAULT_INVALID_BLANK_MESSAGE } } : null
};
}
static email(msg?: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (isEmptyInputValue(control.value)) {
return null; // don't validate empty values to allow optional controls
}
return EMAIL_REGEXP.test(control.value) ? null : { 'email': { value: true, "msg": msg ?? GlobalMessage.FORMS.DEFAULT_INVALID_VALIDATOR_MESSAGE } };
};
}
static maxLength(maxLength: number, msg?: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (isEmptyInputValue(control.value)) {
return null; // don't validate empty values to allow optional controls
}
return hasValidLength(control.value) && control.value.length > maxLength ?
{ 'maxlength': { 'requiredLength': maxLength, 'actualLength': control.value.length, "msg": msg ?? GlobalMessage.FORMS.DEFAULT_INVALID_MAX_MESSAGE } } :
null;
};
}
static minLength(minLength: number, msg?: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (isEmptyInputValue(control.value) || !hasValidLength(control.value)) {
// don't validate empty values to allow optional controls
// don't validate values without `length` property
return null;
}
return control.value.length < minLength ?
{ 'minlength': { 'requiredLength': minLength, 'actualLength': control.value.length, "msg": msg ?? GlobalMessage.FORMS.DEFAULT_INVALID_MIN_MESSAGE } } :
null;
};
}
static min(min: number, msg?: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (isEmptyInputValue(control.value) || isEmptyInputValue(min)) {
return null; // don't validate empty values to allow optional controls
}
const value = parseFloat(control.value);
// Controls with NaN values after parsing should be treated as not having a
// minimum, per the HTML forms spec: https://www.w3.org/TR/html5/forms.html#attr-input-min
return !isNaN(value) && value < min ? { 'min': { 'min': min, 'actual': control.value, "msg": msg ?? GlobalMessage.FORMS.DEFAULT_INVALID_MIN_MESSAGE } } : null;
};
}
static max(max: number, msg?: string): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
if (isEmptyInputValue(control.value) || isEmptyInputValue(max)) {
return null; // don't validate empty values to allow optional controls
}
const value = parseFloat(control.value);
// Controls with NaN values after parsing should be treated as not having a
// maximum, per the HTML forms spec: https://www.w3.org/TR/html5/forms.html#attr-input-max
return !isNaN(value) && value > max ? { 'max': { 'max': max, 'actual': control.value, "msg": msg ?? GlobalMessage.FORMS.DEFAULT_INVALID_MAX_MESSAGE } } : null;
};
}
}
```
## Filtros genéricos
El componente de filtro genérico permite definir y crear formulario para realizar filtro de un listado.
> El nombre del componente es : **app-custom-filter**, que formate parte de la librería comun del proyecto **ngx-siare-common**
* Definición del componente de filtro
```htmlembedded
<div class="row">
<app-custom-filter [filterNames]="nombreFiltros"
(formEvent)="filtrar($event)">
</app-custom-filter>
</div>
```
* Propiedades
| Nombre | Tipo | Valor por Defecto | Descripción|
| -------- | -------- | -------- | -------- |
| filterNames | Array<CustomFilterName> | new Array() | Listado de definición de filtros |
* Eventos
| Nombre | Parámetro | Descripción |
| -------- | -------- | -------- |
| formEvent | value: Object | Función de callback que es llamado al emitir el evento de búsqueda |
* Modelo del filtro
```javascript
import { Observable } from "rxjs";
export interface CustomFilterName{
key:string;
label: string;
type?: string | 'input' | 'select' | 'date' | 'combobox' | 'number' | 'text';
options?: {key: string, value: string, selected?:string}[];
formatoFecha?: string;
selector?: {servicio:(params: any)=> Observable<any>, etiqueta: string};
}
export const filterExample: Array<CustomFilterName> = [
{key: 'nombre', label: 'Nombre Filter'},
{key: 'descripcion', label: 'Descripcion Filter'},
{key: 'tipo', label: 'Tipo Filter'},
];
```
| Nombre | Tipo | Valor por Defecto | Descripción|
| -------- | -------- | -------- | -------- |
| key | string | - | Clave del elemento html o componente a generar |
| label | string | - | Valor de etiqueta para el componente o elemento html |
| type | string | - | Valor de tipo de componente o elemento html |
| options | Array | - | Valores a utilizar para el elemento html select, es utilizado cuando el type es "select" |
| formatoFecha | string | - | Valor del formato de la fecha a utilizar para el componente "Date" |
| selector | funcion | - | Definición de la funcion de callback utilizado por el componente de autocomplete |
* Ejemplo de uso :
> Archivo : siare-front/src/app/bien-seguros-list/bien-seguros-list.component
```javascript
obtenerDefinicionFiltro(): Array<CustomFilterName> {
return [
{
key: 'aseguradora', // nombre del atributo asociado en el DTO
label: 'ASEGURADORA',
type: 'combobox',
selector: {
etiqueta: 'descripcion', //campo a mostrar en el combobox
servicio: (params: any) => {
if (params?.filtros?.nombre) {
params.filtros = { descripcion: params?.filtros?.nombre };
}
//filtro por defecto
params.filtros.borrado = false;
params.filtros.activo = true;
return this.aseguradorasService.obtenerListadoFiltradoEnJson(params)
}
}
},
{ key: 'codigoContratacion', label: 'CÓDIGO CONTRATACIÓN' },
{ key: 'numeroContrato', label: 'NÚMERO CONTRATO' },
{
key: 'activo', label: 'ACTIVO', type: 'select',
options: [
{
key: 'Todos',
value: '',
selected: 'y'
},
{
key: 'Si',
value: 'true'
},
{
key: 'No',
value: 'false'
}
]
},
{
key: 'borrado', label: 'BORRADO', type: 'select',
options: [
{
key: 'Todos',
value: '',
selected: 'y'
},
{
key: 'Si',
value: 'true',
},
{
key: 'No',
value: 'false'
}
]
},
];
}
```
## Conversión de mayúsculas para campos de formulario
La clase establecida en la hoja de estilo convierte todos los campos del formulario en mayúsculas, para poder ignorar este comportamiento predeterminado se debe agregar la siguiente notación:
* Definición de la clase upper
```htmlembedded=
<div class="col p-2">
<label for="nombre" class="col col-form-label">Nombre (*)</label>
<input name="nombre"
type="text"
formControlName="nombre"
placeholder="Nombre"
class="form-control no-upper"
autocomplete="off" [class.is-invalid]="
entityForm.controls['nombre']?.invalid &&
entityForm.controls['nombre']?.touched
" />
<small class="text-danger" [class.d-none]="
entityForm.controls['nombre']?.valid ||
entityForm.controls['nombre']?.pristine
">
Nombre del Artefacto es requerido</small>
</div>
```
:::info
Agregar la clase al **no-upper** al input especifico del formulario que no se verá afectado con la conversión a mayúscula
:::
## Gestion de cache
Para la capa de presentación se ha desarrollado un decorador que permite gestionar las llamadas a los servicios, lo cual tiene como finalidad la mejora de rendimiento tanto en los servicios de front end como en los de backend.
La utilización del cache se debe realizar de la siguiente manera:
```javascript=
import { HttpClient } from "@angular/common/http";
import { Injectable } from "@angular/core";
import { ServicioPersonalizadoBase, GlobalService } from "ngx-siare-common";
import { environment } from "projects/siare-front/src/environments/environment";
import { Bancos } from "../../../model/bancos";
import { TESORERIA_URLS } from "../routing";
import { Cache } from "ngx-siare-common";
@Injectable({
providedIn: 'root'
})
export class BancosService extends ServicioPersonalizadoBase<Bancos> {
constructor(
private http: HttpClient,
private globalSvc: GlobalService,
) {
super(TESORERIA_URLS.BANCOS.SERVICE_URL);
this.httpClient = http;
this.globalServices = globalSvc;
this.urlBase = environment.URL_BASE;
}
@Cache({maxAge: 2})
public obtenerPorId(data: any): Observable<ServiceResponse<Bancos>> {
super.obtenerPorId(data);
}
}
```
El decorador determinara basado en el entorno si el cache se guarda a nivel de sessionstorage o en memoria del browser, en caso de requerir un guardado no inmutable, se puede utilizar la siguiente configuración:
```javascript=
@Cache({tipo: SESSION|PERSISTENCE|MEMORY})
```
Si no se especifica el tipo, el decorador determinara basado en el entorno, siendo de preferencia la utilización de SESSION y MEMORY.