@microsoft/mgt
Version:
The Microsoft Graph Toolkit
210 lines • 8.9 kB
JavaScript
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
export class TemplateHelper {
/**
* Render a template into a HTMLElement with the appropriate data context
*
* Ex:
* ```
* <template>
* <div>{{myObj.someStr}}</div>
* <div data-for="key in myObj.list">
* <div>{{key.anotherStr}}</div>
* </div>
* </template>
* ```
*
* @param template the template to render
* @param context the data context to be applied
* @param converters the converter functions used to transform the data
*/
static renderTemplate(template, context, converters) {
// inherit context from parent template
if (template.$parentTemplateContext) {
context = Object.assign(Object.assign({}, context), { $parent: template.$parentTemplateContext });
}
if (template.content && template.content.childNodes.length) {
const templateContent = template.content.cloneNode(true);
return this.renderNode(templateContent, context, converters);
}
else if (template.childNodes.length) {
const div = document.createElement('div');
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < template.childNodes.length; i++) {
div.appendChild(template.childNodes[i].cloneNode(true));
}
return this.renderNode(div, context, converters);
}
}
/**
* Gets the value of an expanded key in an object
*
* Ex:
* ```
* let value = getValueFromObject({d: 3, a: {b: {c: 5}}}, 'a.b.c')
* ```
* @param obj the object holding the value (ex: {d: 3, a: {b: {c: 5}}})
* @param key the key of the value we need (ex: 'a.b.c')
*/
static getValueFromObject(obj, key) {
key = key.trim();
if (key === 'this') {
return obj;
}
const keys = key.split('.');
let value = obj;
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < keys.length; i++) {
const currentKey = keys[i];
value = value[currentKey];
if (!value) {
return null;
}
}
return value;
}
static replaceExpression(str, context, converters) {
return str
.replace(this._converterExpression, match => {
if (!converters) {
return '';
}
return this.evalInContext(match.substring(3, match.length - 3).trim(), Object.assign(Object.assign({}, converters), context));
})
.replace(this._expression, match => {
const key = match.substring(2, match.length - 2);
const value = this.getValueFromObject(context, key);
if (value) {
if (typeof value === 'object') {
return JSON.stringify(value);
}
else {
return value.toString();
}
}
return '';
});
}
static renderNode(node, context, converters) {
if (node.nodeName === '#text') {
node.textContent = this.replaceExpression(node.textContent, context, converters);
return node;
}
else if (node.nodeName === 'TEMPLATE') {
node.$parentTemplateContext = context;
return node;
}
// tslint:disable-next-line: prefer-const
let nodeElement = node;
// replace attribute values
if (nodeElement.attributes) {
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < nodeElement.attributes.length; i++) {
const attribute = nodeElement.attributes[i];
nodeElement.setAttribute(attribute.name, this.replaceExpression(attribute.value, context, converters));
}
}
// don't process nodes that will loop yet, but
// keep a reference of them
const loopChildren = [];
// list of children to remove (ex, when data-if == false)
const removeChildren = [];
let previousChildWasIfAndTrue = false;
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < node.childNodes.length; i++) {
const childNode = node.childNodes[i];
const childElement = childNode;
let previousChildWasIfAndTrueSet = false;
if (childElement.dataset) {
let childWillBeRemoved = false;
if (childElement.dataset.if) {
const expression = childElement.dataset.if;
if (!this.evalBoolInContext(expression, context)) {
removeChildren.push(childElement);
childWillBeRemoved = true;
}
else {
childElement.removeAttribute('data-if');
previousChildWasIfAndTrue = true;
previousChildWasIfAndTrueSet = true;
}
}
else if (typeof childElement.dataset.else !== 'undefined') {
if (previousChildWasIfAndTrue) {
removeChildren.push(childElement);
childWillBeRemoved = true;
}
else {
childElement.removeAttribute('data-else');
}
}
if (childElement.dataset.for && !childWillBeRemoved) {
loopChildren.push(childElement);
}
else if (!childWillBeRemoved) {
this.renderNode(childNode, context, converters);
}
}
else {
this.renderNode(childNode, context, converters);
}
// clear the flag if the current node wasn't data-if
// or if it was data-if but it wasn't true
if (!previousChildWasIfAndTrueSet && childNode.nodeName !== '#text') {
previousChildWasIfAndTrue = false;
}
}
// now handle nodes that need to be removed
for (const child of removeChildren) {
nodeElement.removeChild(child);
}
// now handle nodes that should loop
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < loopChildren.length; i++) {
const childElement = loopChildren[i];
const loopExpression = childElement.dataset.for;
const loopTokens = loopExpression.split(' ');
if (loopTokens.length > 1) {
// don't really care what's in the middle at this point
const itemName = loopTokens[0];
const listKey = loopTokens[loopTokens.length - 1];
const list = this.getValueFromObject(context, listKey);
if (Array.isArray(list)) {
// first remove the child
// we will need to make copy of the child for
// each element in the list
childElement.removeAttribute('data-for');
for (let j = 0; j < list.length; j++) {
const newContext = Object.assign({ $index: j }, context);
newContext[itemName] = list[j];
const clone = childElement.cloneNode(true);
this.renderNode(clone, newContext, converters);
nodeElement.insertBefore(clone, childElement);
}
nodeElement.removeChild(childElement);
}
}
}
return node;
}
static evalBoolInContext(expression, context) {
return new Function('with(this) { return !!(' + expression + ')}').call(context);
}
static evalInContext(expression, context) {
const func = new Function('with(this) { return ' + expression + ';}');
let result;
try {
result = func.call(context);
// tslint:disable-next-line: no-empty
}
catch (e) { }
return result;
}
}
TemplateHelper._expression = /{{\s*([$\w]+)(\.[$\w]+)*\s*}}/g;
TemplateHelper._converterExpression = /{{{\s*[$\w\.()]+\s*}}}/g;
//# sourceMappingURL=templateHelper.js.map