Marcos Benitez
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    --- 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 ![Arquitecuta](https://angular.io/generated/images/guide/architecture/overview2.png) 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. ![](https://i.imgur.com/UAxPJbI.png) **Figura.** Ejecutar git bash. ![](https://i.imgur.com/opGaVZI.png) **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 ::: ![](https://i.imgur.com/EUlmbkS.png) **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/ ::: ![](https://i.imgur.com/HxoM7iZ.png) **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 ``` ![](https://i.imgur.com/yIvuuF3.png) **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 ``` ![](https://i.imgur.com/6oEHYfs.png) **Figura.** Acceder a la carpeta clonada. 4. Crear una rama a partir de la rama develop ``` shell= $ git checkout -b feature-frecuencias ``` ![](https://i.imgur.com/9qWT0Qp.png) **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 ![](https://i.imgur.com/ZP3cYj3.png) > 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 ![](https://i.imgur.com/k0pVbmM.png) > 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: ![](https://hackmd.io/_uploads/BktIseAD3.png) 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.

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully