+/**
+ * AngularJS-style minimal template parser.
+ * Original use case is supporting AngularJS style print templates.
+ *
+ * Supports the following template constructs:
+ * {{variables}}
+ * <element ng-if="expression">
+ * <element ng-repeat="expression">
+ * <element ng-switch="expression">
+ * <child-element ng-switch-when="string">
+ */
import {Injectable, EventEmitter} from '@angular/core';
import {FormatService} from '@eg/share/util/format.service';
// process the HTML template declarations, apply variable
// replacements, and return the restulting HTML string.
parse(html: string, context: any): string {
- this.context = context;
- const parser = new DOMParser();
+ // Shallow copy the context since we modify the keys internally
+ this.context = Object.assign({}, context);
+
+ const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
// Parsing as html wraps the content in an <html><body> wrapper
return this.domNode.innerHTML;
}
- // Process each node in the source document.
+ // Process each node in the in-progress document.
traverse(node: Node) {
if (!node) return;
switch (node.nodeType) {
case Node.ELEMENT_NODE:
- this.processElement(node as Element);
+ const complete = this.processElementNode(node as Element);
+ if (complete) {
+ return;
+ }
break;
case Node.TEXT_NODE:
- this.replaceText(node as Text);
+ this.processTextNode(node as Text);
break;
}
}
// Process expressions found on each element.
- processElement(node: Element) {
+ // Returns true if the node was processed within and needs no
+ // further processing, false otherwise.
+ processElementNode(node: Element): boolean {
+
+ const switchExp = node.getAttribute('ng-switch');
+ if (switchExp) {
+ node.removeAttribute('ng-switch');
+ if (!this.processSwitchExpression(node, switchExp)) {
+ // We removed all child nodes in the switch
+ return true;
+ }
+ }
const ifExp = node.getAttribute('ng-if');
- if (ifExp && !this.testIfExpression(ifExp)) {
- // A failing IF expression means the node does not render.
- node.remove();
+ if (ifExp) {
+ node.removeAttribute('ng-if');
+ if (!this.testIfExpression(ifExp)) {
+ // A failing IF expression means the node does not render.
+ node.remove();
+ return true;
+ }
}
const loopExp = node.getAttribute('ng-repeat');
if (loopExp) {
+ node.removeAttribute('ng-repeat');
this.processLoopExpression(node, loopExp);
+ return true;
}
+
+ return false;
+ }
+
+ // Returns true if any of the switch expressions resulted
+ // in true, thus allowing a child node to remain and require
+ // future processing.
+ processSwitchExpression(node: Element, expr: string): boolean {
+
+ // ng-switch only works on string values
+ const targetVal = this.getContextStringValue(expr);
+ let matchNode: Node;
+
+ // Find the switch-matching child node
+ Array.from(node.childNodes).forEach(child => {
+ if (!matchNode && child.nodeType === Node.ELEMENT_NODE) {
+ const elm: Element = child as Element;
+ const val = elm.getAttribute('ng-switch-when');
+ if (val === null || val === undefined) { return; }
+ if (val + '' === targetVal) {
+ matchNode = child;
+ }
+ }
+ });
+
+ // Remove all child nodes
+ while (node.firstChild) {
+ node.removeChild(node.firstChild);
+ }
+
+ // Add the matching node back if found
+ if (matchNode) {
+ node.appendChild(matchNode);
+ return true;
+ }
+
+ return false; // no matching switch values
}
processLoopExpression(node: Element, expr: string) {
const parts = expr.split('in');
- const key = parts[0].trim();
+ const listItemKey = parts[0].trim();
const listPath = parts[1].trim();
-
- this.generateEvalEnv();
const list = this.getContextValueAt(listPath);
if (!Array.isArray(list)) {
throw new Error(`Template value ${listPath} is not an Array`);
}
+
+ // Loop over the repeat array and for each iteration, add the
+ // loop variable as a new value in the (temporary) context so
+ // it can be referenced in the eval context.
+ const origContext = this.context;
+ let prevNode: Element = node;
+
+ list.forEach(listItem => {
+ const listCtx = {};
+ listCtx[listItemKey] = listItem;
+ this.context = Object.assign(this.context, listCtx);
+ const newNode = node.cloneNode(true) as Element;
+ // appendChild used internally when needed.
+ prevNode.parentNode.insertBefore(newNode, prevNode.nextSibling);
+ this.traverse(newNode);
+ prevNode = newNode;
+ });
+
+ // recover the original context sans any loop data.
+ this.context = origContext;
+
+ // get rid of the source/template node
+ node.remove();
}
// To evaluate free-form expressions, create an environment
// For example: ng-if="foo.bar" -- the 'foo' key from the
// context must be defined in advance for evaluation to succeed.
generateEvalEnv() {
- if (this.evalEnv) return;
- this.evalEnv = '';
+ let env = '';
Object.keys(this.context).forEach(key => {
- this.evalEnv += `var ${key} = this.context['${key}'];\n`;
+ env += `var ${key} = this.context['${key}'];\n`;
});
+ return env;
}
// Returns true of the IF expression evaluates to true,
// false otherwise.
testIfExpression(expr: string): boolean {
- this.generateEvalEnv();
- const evalStr = `${this.evalEnv}; Boolean(${expr})`;
+ const env = this.generateEvalEnv();
+ const evalStr = `${env}; Boolean(${expr})`;
//console.debug('ng-if eval string: ', evalStr);
try {
// Replace variable {{...}} instances in a given Text node
// with the matching data from the context.
- replaceText(node: Text) {
+ processTextNode(node: Text) {
if (!node || !node.data) { return; }
const matches = node.data.match(/{{.*?}}/g);