import { AbstractDirective } from "./abstract-directive"; import { Exception } from "./exception"; import { ERROR_CODES } from "./constants"; import { ReferenceToken } from "./expression/tokenizer"; import { ExpressionException } from "./expression/exception"; const DATA_ID_KEY = "id"; let counter: number = 0; //
export class NgFor extends AbstractDirective implements NgTemplate.Directive { nodes: NgTemplate.DirectiveNode[]; constructor( el: HTMLElement, reporter: NgTemplate.Reporter ){ super( el, reporter ); this.nodes = this.initNodes( el, "ng-for", ( node: HTMLElement, expr: string, compile: Function, cache: NgTemplate.Cache ) => { let parsed: NgTemplate.NgForExprVo = this.parseExpr( expr ), outerHTML: string, id: string = "id" + ( ++counter ); node.dataset[ "ngForScope" ] = id; outerHTML = node.outerHTML; // Do not process directives on the first level as all of them about elements generated by ngFor [ "ngSwitch", "ngSwitchCase", "ngSwitchCaseDefault", "ngIf", "ngClass", "ngData", "ngProp", "ngAttr", "ngEl", "ngText" ].forEach(( key ) => { if ( node.dataset[ key ] ) { delete node.dataset[ key ]; } }); return { el: node, parentNode: node.parentNode, outerHTML: outerHTML, id: id, indexable: false, variable: parsed.variable, items: >[], cache: cache, exp: function( data: NgTemplate.DataMap, cb: Function ): any[] { let it: any[] = []; try { it = ReferenceToken.findValue( parsed.iterable, data ); } catch ( err ) { if ( !( err instanceof ExpressionException ) ) { throw new Exception( `Invalid ng* expression ${expr}` ); } reporter.addLog( `${ERROR_CODES.NGT0003}: ` + ( err ).message ); } if ( !Array.isArray( it ) ) { it = []; } return it; } }; }); } parseExpr( strRaw: string ): NgTemplate.NgForExprVo{ let re = /(let|var)\s+([a-zA-Z0-9\_]+)\s+of\s+/, str = strRaw.trim(), varMatches = str.match( re ); if ( !varMatches || varMatches.length !== 3 ) { throw new Exception( "Cannot parse ng-for expression: " + strRaw ); } return { variable: varMatches[ 2 ], iterable: str.replace( re, "" ) }; } /** * Create for generated list elements a permitted parent elements */ private static createParentEl( tag: string ): HTMLElement { const map: NgTemplate.DataMap = { "TR": "tbody", "THEAD": "table", "TFOOT": "table", "TBODY": "table", "COLGROUP": "table", "CAPTION": "table", "TD": "tr", "TH": "tr", "COL": "colgroup", "FIGCAPTION": "figure", "LEGEND": "fieldset", "LI": "ul", "DT": "dl", "DD": "dl", }; let child: string = tag.toUpperCase(), parent = child in map ? map[ child ] : "div"; return document.createElement( parent ); } removeIndexable( node: NgTemplate.DirectiveNode, it: any[] ): NgTemplate.NgTemplate[] { return node.items.filter(( instance: NgTemplate.NgTemplate ) => { return it.find(( val ) => { return instance.id === val[ DATA_ID_KEY ]; }); }); } sync( data: NgTemplate.DataMap, Ctor: NgTemplate.NgTemplateCtor ){ this.nodes.forEach(( node: NgTemplate.DirectiveNode ) => { let it: any[] = node.exp( data ); if ( node.cache.match( JSON.stringify( it ) ) ) { return false; } // reduce: collection changed, it's a special case // if we have indexes (id) then we go still gacefully, we remove particular nodes from the list // if not, we updateth list if ( node.items.length > it.length ) { node.items = node.indexable ? this.removeIndexable( node, it ) : []; } // expand: update every item and add new ones if ( node.items.length < it.length ) { let num = it.length - node.items.length; while ( num-- ){ let el = NgFor.createEl( node.el.tagName, node.outerHTML ); node.items.push(new Ctor( el )); } } // sync it.forEach(( val, inx ) => { let item: NgTemplate.NgTemplate = node.items[ inx ]; data[ node.variable ] = val; item.sync( data ); if ( val && typeof val === "object" && DATA_ID_KEY in val ) { item.id = val[ DATA_ID_KEY ]; node.indexable = true; } }); this.buildDOM( node ); }); } static createEl( tag: string, html: string ): HTMLElement { let parent = NgFor.createParentEl( tag ); parent.innerHTML = html; return parent.firstElementChild as HTMLElement; } private buildDOM( node: NgTemplate.DirectiveNode ): void{ let items = Array.from( node.parentNode.querySelectorAll( `[data-ng-for-scope="${node.id}"]` ) ), anchor = document.createElement( "ng" ); node.parentNode.insertBefore( anchor, items[ 0 ] ); anchor.dataset[ "ngForScope" ] = node.id; items.forEach(( child ) => { node.parentNode.removeChild( child ); }); node.items.forEach(( item: NgTemplate.NgTemplate ) => { node.parentNode.insertBefore( item.el, anchor ); }); node.parentNode.removeChild( anchor ); } }