Skip to content

Instantly share code, notes, and snippets.

@jbardon
Last active June 4, 2019 13:50
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jbardon/0482d48499ea91255cd73d5767258f1c to your computer and use it in GitHub Desktop.
Save jbardon/0482d48499ea91255cd73d5767258f1c to your computer and use it in GitHub Desktop.
Table component with Angular TemplateRef and ViewContainerRef
<!-- Test your customElement inside an angular environment here -->
<ld-test [data]="data">
<div slot="header">
<template column="position">
<th (click)="testClick($event)">Position {{coucou}}</th>
<td var="element">#%element.position%</td>
</template>
<template column="weight">
<th>Weight</th>
<td var="element">%element.weight% kg</td>
</template>
<template column="symbol">
<th>Symbol</th>
<td var="element">[%element.symbol%]</td>
</template>
</div>
</ld-test>
<ld-button (click)="update()">Update data</ld-button>
import { Component, ElementRef } from '@angular/core';
import { ChangeDetectorRef } from '@angular/core';
import { LemonadeComponent } from '@elements/base/lemonade-component';
@Component({
templateUrl: './angular-env.component.html',
styleUrls: ['../../scss/base.scss', './angular-env.component.scss']
})
export class AngularEnv extends LemonadeComponent {
data = [
{position: 1, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
{position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'},
{position: 3, name: 'Lithium', weight: 6.941, symbol: 'Li'},
{position: 4, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
{position: 5, name: 'Boron', weight: 10.811, symbol: 'B'},
{position: 6, name: 'Carbon', weight: 12.0107, symbol: 'C'},
{position: 7, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
{position: 8, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
{position: 9, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
{position: 10, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
];
constructor(protected $el: ElementRef, protected $cd: ChangeDetectorRef) { super($el, $cd); }
public update() {
this.data = [
{position: 10, name: 'Hydrogen', weight: 1.0079, symbol: 'H'},
{position: 20, name: 'Helium', weight: 4.0026, symbol: 'He'},
{position: 30, name: 'Lithium', weight: 6.941, symbol: 'Li'},
{position: 40, name: 'Beryllium', weight: 9.0122, symbol: 'Be'},
{position: 50, name: 'Boron', weight: 10.811, symbol: 'B'},
{position: 60, name: 'Carbon', weight: 12.0107, symbol: 'C'},
{position: 70, name: 'Nitrogen', weight: 14.0067, symbol: 'N'},
{position: 80, name: 'Oxygen', weight: 15.9994, symbol: 'O'},
{position: 90, name: 'Fluorine', weight: 18.9984, symbol: 'F'},
{position: 100, name: 'Neon', weight: 20.1797, symbol: 'Ne'},
];
}
public testClick($event) {
console.log($event);
}
}

Exemple (angular material): https://stackblitz.com/angular/ommgemmapdv?file=app%2Ftable-basic-example.html

@ViewChild/Children are references to the component itself @ContentChild/Children are references such as slots

Ex: @ViewChildren(TemplateRef) tmp1: QueryList<TemplateRef> // Request all ng-template @ViewChild('select') tmp1: TemplateRef // request any tag such as <h1 #select> (template reference variable) @ContentChildren('column', { read: ViewContainerRef }) templates: QueryList;

declare a template not compiled by Angular It got a special syntax to define template input variables: let-status="currentStatus"

includes by default the ng-template defined as children Can define any selector with the selection attribute

TemplateRef is a reference to the defining a template, or view ViewContainerRef is the which will host the view

Structural directives gives access to both TemplateRef and ViewContainerRef

World
Angular compiles to:
World
// [ngIf] for binding

Can avoid Angular to compile with either or ngNonBindable Problems:

  • avoid framework compile slots (but angular doesn't ignore , don't want to use framework specific syntax )
  • some frameworks outputs non standard HTML => DOMException (ex: (click)="handler()")
<!--<slot name="header" ngNonBindable></slot>-->
<ng-content ngNonBindable></ng-content>
<table>
<thead [innerHTML]="renderHeaders2()"></thead>
<tbody [innerHTML]="renderBody2()"></tbody>
<!--
<thead></thead>
<tbody></tbody>
-->
</table>
import {AfterViewInit, ChangeDetectorRef, Component, ElementRef, Input, ViewEncapsulation,} from '@angular/core';
import {LemonadeComponent} from "@elements/base/lemonade-component";
import {InputConverter} from "@elements/base/input-converter";
import {ElementReady} from "@elements/base/element-ready.interface";
import get from "lodash.get";
import {DomSanitizer} from "@angular/platform-browser";
@Component({
templateUrl: './test.component.html',
styleUrls: ['../../scss/base.scss', './test.component.scss'],
encapsulation: ViewEncapsulation.Emulated
})
export class Test extends LemonadeComponent implements ElementReady, AfterViewInit {
@Input() @InputConverter() data: any[];
private COLUMN_NAME_ATTRIBUTE: string = 'column';
private CELL_VARIABLE_ATTRIBUTE: string = 'var';
private columns: any[];
private isElementReady: boolean;
constructor(protected $el: ElementRef, protected $cd: ChangeDetectorRef, private _sanitizer: DomSanitizer) {
super($el, $cd);
this.isElementReady = false;
}
ngAfterViewInit(): void {
// console.log(`@ViewChildren: ${this.tmp1.toArray().length}, @ContentChildren: ${this.tmp2.toArray().length}`)
}
onElementReady() {
let columnDefs: HTMLElement[] = Array.from(
this.$el.nativeElement.querySelectorAll(`[${this.COLUMN_NAME_ATTRIBUTE}]`)
|| []);
columnDefs.forEach(a => console.log(a.outerHTML, a.querySelector('td')))
this.columns = columnDefs.map(columnDef => ({
name: columnDef.getAttribute('column'),
headerTemplate: columnDef.querySelector('th'),
cellTemplate: columnDef.querySelector('td')
}));
this.isElementReady = true;
/*
let headerContainer = this.$el.nativeElement.shadowRoot.querySelector('thead');
this.renderHeaders(headerContainer, columns);
let bodyContainer = this.$el.nativeElement.shadowRoot.querySelector('tbody');
this.renderBody(bodyContainer, columns);
*/
}
private renderHeaders2() {
if (this.isElementReady) {
let headers = this.columns.map(column => column.headerTemplate.outerHTML);
return this._sanitizer.bypassSecurityTrustHtml(`<tr>${headers.join('')}</tr>`);
}
}
private renderBody2() {
if (this.isElementReady) {
let rows = this.data.map(data => this.evaluateRow(this.columns, data));
return this._sanitizer.bypassSecurityTrustHtml(rows.join(''));
}
}
private renderHeaders(target, columns) {
let headers = columns.map(column => column.headerTemplate.outerHTML);
target.innerHTML = `<tr>${headers.join('')}</tr>`;
}
private renderBody(target, columns) {
let rows = this.data.map(data => this.evaluateRow(columns, data));
target.innerHTML = rows.join('');
}
/**
* Evaluate a full table row from columns templates
* and the row data
* @param columns All columns templates
* @param data Row data
*/
private evaluateRow(columns, data): string {
let cells = columns.map(column => {
let template = column.cellTemplate.outerHTML;
let templateVariable = {
name: column.cellTemplate.getAttribute(this.CELL_VARIABLE_ATTRIBUTE),
value: data
};
let bindings = this.findBindings(template, templateVariable);
// Replace bindings in the cell template
return bindings.reduce((evaluatedTemplate, binding) =>
evaluatedTemplate.replace(new RegExp(`%${binding.token}%`), binding.value),
template
);
});
return `<tr>${cells.join('')}</tr>`;
}
/**
* Find expressions token in the template
* and evaluate their value from the template variable
*
* The token syntax is anything accepted by lodash get function
* wrapped between percent signs (ex: %object.list[0].property%)
*
* @param template containing expressions tokens
* @param templateVariable
*/
private findBindings(template, templateVariable) {
let tokenPattern = /%([^%]+)%/g;
return [...template.matchAll(tokenPattern)]
.filter(matching => matching.length > 1)
.map(([, token, ]) => ({
token,
// When no value for a token: the value is the token itself
value: this.evaluateToken(token, templateVariable.name, templateVariable.value) || `%${token}%`
}));
}
/**
* Get token value from then given variable
*
* For a global variable: element = { message: 'hello' }
* the token "element.message" is evaluated to 'hello'
*
* @param token Expression token
* @param variableName Global variable name
* @param variableValue Global variable value
*/
private evaluateToken(token, variableName, variableValue) {
// Remove variable name from token
let tokenValuePath = token.replace(new RegExp(`${variableName}.`), '');
return get(variableValue, tokenValuePath);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment