Hola a todos
Todos los que escribieron en el marco angular de alguna manera se encontraron (o incluso trabajaron) con la biblioteca de
material angular . Esta es una biblioteca de componentes muy bien escrita capaz de un estilo flexible, que se implementa a través de la capacidad de crear varios temas para su aplicación, con un gran conjunto de componentes para todas las ocasiones.
En mi trabajo diario, ni un solo proyecto puede prescindir de él.
Pero además de todas las ventajas de la flexibilidad de esta biblioteca, también es posible aprovechar la experiencia de los creadores al escribir sus propios componentes, y este es para mí el mejor manual sobre el desarrollo de mejores prácticas en
Angular .
En este artículo quiero compartir con ustedes cómo puede implementar el enfoque con una plantilla compleja que se implementa en el módulo
MatTableModule .
Como ejemplo, quiero mostrar cómo hacer una
lista de tarjetas con la capacidad de agregar paginación y filtros , y como base tomamos el modelo modelo del componente MatTable.
Plantilla (
fuente ):
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8"> <ng-container matColumnDef="position"> <th mat-header-cell *matHeaderCellDef> No. </th> <td mat-cell *matCellDef="let element"> {{element.position}} </td> </ng-container> <ng-container matColumnDef="name"> <th mat-header-cell *matHeaderCellDef> Name </th> <td mat-cell *matCellDef="let element"> {{element.name}} </td> </ng-container> <ng-container matColumnDef="weight"> <th mat-header-cell *matHeaderCellDef> Weight </th> <td mat-cell *matCellDef="let element"> {{element.weight}} </td> </ng-container> <ng-container matColumnDef="symbol"> <th mat-header-cell *matHeaderCellDef> Symbol </th> <td mat-cell *matCellDef="let element"> {{element.symbol}} </td> </ng-container> <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> <tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr> </table>
Después de estudiar la plantilla, queda claro que indicamos en las etiquetas
ng-container el marcado para una columna específica de la tabla, pero ¿cómo funciona dentro? Fue esta pregunta la que hice cuando vi este diseño, en parte debido al hecho de que no trabajaba con componentes dinámicos. Y así, comencemos
(código fuente) .
Estructura
Un conjunto de entidades que necesitamos crear. Este diagrama de bloques ilustra su interacción.

Primer paso
Necesitamos un servicio para registrar nuestras micro-plantillas.
@Injectable() export class RegisterPropertyDef<T> {
Segundo paso
Cree una directiva para registrar plantillas:
@Directive({ selector: '[providePropertyDefValue]' }) export class ProvidePropertyDefValueDirective<T> implements OnInit { @Input() providePropertyDefValueId: string; constructor( private container: ViewContainerRef, private template: TemplateRef<any>, // private registerPropertyDefService: RegisterPropertyDefService<any>, // @Optional() private parent: Alias<T[]> // ) {} ngOnInit(): void { this.container.clear();
Paso tres
Crea un componente:
@Component({ selector: 'lib-card-list', template: ` <mat-card *ngFor="let source of sources"> <ul> <li *ngFor="let key of displayedColumns"> <span>{{ findColumnByKey(key)?.label }}</span> <span> <ng-container [ngTemplateOutlet]="findColumnByKey(key)?.template || default" [ngTemplateOutletContext]="{ $implicit: source }" ></ng-container> </span> </li> </ul> </mat-card> <ng-template #default></ng-template> `, styles: [ 'mat-card { margin: 10px; }' ] }) export class CardListComponent<T> implements OnInit, AfterViewInit { @Input() defaultColumns: DefaultColumn[]; @Input() source$: Observable<T[]>; displayedColumns = []; sources: T[] = []; constructor(private readonly registerPropertyDefService: RegisterPropertyDefService<T>, private readonly parent: Alias<T[]>) { } ngOnInit() { this.source$.subscribe((data: T[]) => this.sources = data); this.displayedColumns = this.defaultColumns.map(c => c.id); } findColumnByKey(key: string): DefaultColumn { return this.defaultColumns.find(column => column.id === key); } ngAfterViewInit(): void { this.defaultColumns = this.defaultColumns.map(column => Object.assign(column, { template: this.registerPropertyDefService.getTemplate(this.parent as ComponentInstance, column.id) }) ); } }
Una pequeña explicación, el trabajo principal del componente es enriquecer la definición de la estructura de datos en el método
ngAfterViewInit . Aquí, después de inicializar las plantillas, actualizamos los modelos de
columnas predeterminadas con plantillas.
En el marcado, puede prestar atención a las siguientes líneas:
<ng-container [ngTemplateOutlet]="findColumnByKey(key)?.template || default" [ngTemplateOutletContext]="{ $implicit: source }"></ng-container>
aquí
se usa una
característica para pasar el alcance (como en AngularJS) al marcado. Esto hace que sea conveniente declarar una variable en nuestras micro plantillas a través de la construcción
let-my-var en la que se ubicarán los datos.
Uso
// app.component.html <lib-card-list [defaultColumns]="defaultColumns" [source$]="sources$"></lib-card-list> <ng-container *libProvidePropertyDefValue="let element; id: 'id'"> {{ element.id }} </ng-container> <ng-container *libProvidePropertyDefValue="let element; id: 'title'"> {{ element.title }} </ng-container>
Inicializando nuestro nuevo componente y pasándole parámetros.
Definición de plantilla a través de
ng-container y nuestra directiva
libProvidePropertyDefValue .
Lo más importante aquí es
"Dejar elemento; id: 'id' »
donde
elemento es el alcance de la plantilla que es igual al objeto con los datos de la lista,
id es el identificador de la micro-plantilla.
Ahora quiero volver a la directiva
providePropertyDefValue , al método
ngOnInit ngOnInit(): void { this.container.clear(); ... }
Puede colocar micro-plantillas como se muestra en el ejemplo y "limpiarlas" en la directiva, o transferir completamente su definición dentro del componente
lib-card-list , por lo tanto, el marcado se verá así:
<lib-card-list [defaultColumns]="defaultColumns" [source$]="sources$"> <ng-container *libProvidePropertyDefValue="let element; id: 'id'"> {{ element.id }} </ng-container> <ng-container *libProvidePropertyDefValue="let element; id: 'title'"> {{ element.title }} </ng-container> </lib-card-list>
Objetivamente: el segundo caso de uso es más productivo.
@Component({ selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.css'], providers: [{ provide: Alias, useExisting: forwardRef(() => AppComponent) }] }) export class AppComponent extends Alias<any> { title = 'card-list-example'; defaultColumns: DefaultColumn[] = [ { id: 'id', label: 'ID' }, { id: 'title', label: 'Title' } ]; sources$ = of([ { id: 1, title: 'Hello' }, { id: 2, title: 'World' } ]); }
Aquí todo es bastante elemental, lo único a considerar es:
proveedores: [{proporcionar: Alias, useExisting: forwardRef (() => AppComponent)}]
Este diseño es necesario para vincular la plantilla y el componente que las usa.
En nuestro servicio, el constructor recibirá una instancia del componente AppComponent del inyector.
Opcional
En este ejemplo, vimos cómo hacer un componente, para uso repetido en sus proyectos, para el cual puede transferir diferentes plantillas con datos, estas plantillas definitivamente pueden ser cualquier cosa.
¿Cómo mejorar?
Puede agregar paginación de material angular y filtrado.
// card-list.component.html <mat-paginator [pageSize]="5"showFirstLastButton></mat-paginator>
El filtrado se puede implementar a través de mat-form-field y, de manera similar a cambiar de página durante la paginación, actualizar los datos.
Eso es todo. Recomiendo consultar periódicamente el código fuente de la biblioteca
angular / de material, en mi opinión, esta es una buena oportunidad para mejorar su conocimiento en la creación de componentes flexibles y productivos. Gracias por su atencion