mframejs
Version:
simple framework
406 lines (320 loc) • 15.1 kB
text/typescript
import { ContainerElements } from '../container/exported';
import { ContainerAttributes } from '../container/exported';
import { Cache } from '../utils/exported';
import { AttributeController } from './attributeController';
import { ElementController } from './elementController';
import { InterpolateController } from './interpolateController';
import { ViewController } from './viewController';
import { IElement, IControllerArray, IBindingContext, ITemplateCache } from '../interface/exported';
import { DOM } from '../utils/exported';
/**
* This class have methods to parse the string/template and initiate our custom elements/attributes/binding
*
*/
export class View {
/**
* creates element and parses template string
* just used insternally for creating root
* @internal
* @param _class - class to use
* @param element - element node
* @param bindingContext - binding Context
* @param templateString - template string
* @param viewController - viewController
*/
public static parseAndCreateElement(
_class: IElement,
element: Node,
bindingContext: IBindingContext,
templateString: string,
viewController: ViewController
): ElementController {
const elementController = new ElementController(bindingContext, element, _class, null, templateString, viewController);
const controller = elementController.init();
return controller;
}
/**
* cleans a html template
* @param template - template element to clean up
*/
public static cleanTemplate(template: HTMLElement): void {
const loopNodes = (node: Node) => {
for (let n = 0; n < node.childNodes.length; n++) {
let child = node.childNodes[n];
if (child.nodeType === 8 || (child.nodeType === 3 && !/\S/.test(child.nodeValue))) {
// 8 Represents a comment
// Represents textual content in an element or attribute, but check if empty
node.removeChild(child);
n--;
} else if (child.nodeType === 1) {
// 1 = Element, Text, Comment, ProcessingInstruction, CDATASection, EntityReference
loopNodes(child);
}
child = null;
}
};
loopNodes(template);
}
/**
* creates a template from string markup
* @param markup - string markup to add- remember to wrap inside <template>
*/
public static createTemplate(markup: string): Element {
let template: any;
if (!Cache.templateMap.has(markup)) {
const container = DOM.document.createElement('div');
container.innerHTML = (<any>markup).default || markup;
const fragment = container.firstElementChild;
if (!(<any>fragment).content) {
// ie11 fix
(<any>fragment).content = DOM.document.createDocumentFragment();
while (fragment.childNodes[0]) {
(<any>fragment).content.appendChild(fragment.childNodes[0]);
}
}
template = DOM.document.createElement('mf-template');
View.cleanTemplate((fragment as any).content);
template.appendChild((fragment as any).content);
Cache.templateMap.set(markup, template);
} else {
template = Cache.templateMap.get(markup);
}
const x = template.cloneNode(true);
template = null;
return x;
}
/**
* adds template and calls attached
* @param template - template to attach
* @param toNode - node you want to attach it to
* @param controllers - controllers to attach
*/
public static attachTemplate(template: any, toNode: Element, controllers: IControllerArray): Element[] {
const el = [];
while (template.firstChild) {
el.push(template.firstChild);
toNode.appendChild(template.firstChild);
}
controllers.forEach((x) => {
if (x.attached) {
x.attached();
}
});
return el;
}
/**
* helper for clearing views on array of elements
* optional viewController you can pass in too
* if you are to lazy for calling viewController.clearView() :-)
* @param elements - elements to remove
* @param viewController - viewcontrolelrs to remove
*/
public static clearViews(elements: Element[], viewController?: ViewController) {
if (viewController) {
viewController.clearView();
}
elements.forEach((el) => {
// this will traverse down to every child and tell them to detach
if (el.parentNode) {
el.parentNode.removeChild(el);
}
});
}
/**
* parses nodes and adds initiates elements/interpolate and attributes
* use cache version when generating many
*
* @param template - template to use
* @param bindingContext - binding context
* @param viewController - viewcontroller
*/
public static parseTemplate(template: Node, bindingContext: IBindingContext, viewController: ViewController): IControllerArray {
const templateCache = View.createTemplateCache(template);
const controllers = View.parseTemplateCache(template, bindingContext, viewController, templateCache);
return controllers;
}
/**
* parses nodes and adds initiates elements/interpolate and attributes
* this uses cache to loop, use createTemplateCache to generate cache
*
* @internal
* @param template - template to use
* @param bindingContext - bindingcontext to use
* @param viewController - viewController
* @param cacheX - template cache
*/
public static parseTemplateCache(
template: Node,
bindingContext: IBindingContext,
viewController: ViewController,
cacheX: ITemplateCache[]): IControllerArray {
const arr: IControllerArray = [];
const cache = cacheX.slice().reverse();
// loops and pushes into our node map/array
const loopNodes = function (_node: Node) {
// loop children
for (let n = 0; n < _node.childNodes.length; n++) {
const htmlNode = _node.childNodes[n];
const curcach = cache.pop();
curcach.attributes.forEach((attr) => {
let controller: any;
let attrNode: any;
switch (attr.type) {
case 'controller':
attrNode = (htmlNode as Element).getAttributeNode(attr.name);
controller = new AttributeController(bindingContext, htmlNode, attrNode, attr.container, viewController);
arr.push(controller);
break;
case 'value':
attrNode = (htmlNode as Element).getAttributeNode(attr.name);
controller = new InterpolateController(bindingContext, attrNode, viewController, true);
arr.push(controller);
break;
case 'attribute':
attrNode = (htmlNode as Element).getAttributeNode(attr.name);
controller = new AttributeController(bindingContext, htmlNode, attrNode, attr.container, viewController);
arr.push(controller);
break;
}
});
switch (curcach.type) {
case 'element':
const elementController = new ElementController(bindingContext, htmlNode, null, curcach.container, null, viewController);
arr.push(elementController);
break;
case 'text':
const interpolateController = new InterpolateController(bindingContext, htmlNode, viewController, false);
arr.push(interpolateController);
break;
default:
if (curcach.gotoChild) {
loopNodes(htmlNode);
}
}
}
};
const tempTemplate = ({ childNodes: [template] } as any);
loopNodes(tempTemplate);
arr.forEach((instance) => {
instance.init();
});
return arr;
}
/**
* parses nodes and creates cache to be used in repeat
* @param template - template to use
*/
public static createTemplateCache(template: Node): ITemplateCache[] {
const arr: ITemplateCache[] = [];
// loops and pushes into our node map/array
const loopNodes = function (_node: Node) {
// loop children
for (let n = 0; n < _node.childNodes.length; n++) {
const currentNode = _node.childNodes[n] as Element;
const instance: ITemplateCache = {
type: 'blank',
attributes: ([] as any),
container: null,
gotoChild: false
};
arr.push(instance);
// check controllers first
let isControllerAttribute = false;
if (currentNode.getAttribute) {
// TODO: need to add option to add own here
['repeat.for', 'if.bind'].forEach((attribute: string) => {
if (!isControllerAttribute) { // only 1 controller per element
const attributeNode = currentNode.getAttributeNode(attribute);
if (attributeNode) {
isControllerAttribute = true;
const customAttribute = ContainerAttributes.findAttribute(attributeNode.name);
instance.attributes.push({
type: 'controller',
name: attribute,
container: customAttribute
});
}
}
});
}
if (!isControllerAttribute) {
const childAttributes: Attr[] = [];
const length = currentNode.attributes && currentNode.attributes.length || 0;
for (let i = 0; i < length; i++) {
childAttributes.push(currentNode.attributes[i]);
}
for (let i = 0; i < childAttributes.length; i++) {
const attributeNode = childAttributes[i];
let customAttribute = ContainerAttributes.findAttribute(attributeNode.name);
if (!customAttribute && attributeNode.name) {
customAttribute = ContainerAttributes.findAttribute(attributeNode.name.replace('.bind', ''));
if (!customAttribute && attributeNode.name) {
customAttribute = ContainerAttributes.findAttribute(View.getAttributeExpression(attributeNode.name));
}
}
if (customAttribute) {
instance.attributes.push({
type: 'attribute',
name: attributeNode.name,
container: customAttribute
});
} else {
// attribute value
if (attributeNode.value.indexOf('${') !== -1 || attributeNode.value.indexOf('@{') !== -1) {
if (attributeNode.name.indexOf('.bind') === -1) {
instance.attributes.push({
type: 'value',
name: attributeNode.name,
container: null
});
}
}
}
}
}
if (!isControllerAttribute) {
if (currentNode.nodeType === 1) {
const customElement = ContainerElements.findElement((<Element>currentNode).localName);
if (customElement) {
instance.type = 'element';
instance.container = customElement;
} else {
if (currentNode.childNodes) {
instance.gotoChild = true;
loopNodes(currentNode);
}
}
} else {
if ((<any>currentNode).textContent) {
if ((<any>currentNode).textContent.indexOf('${') !== -1 || (<any>currentNode).textContent.indexOf('@{') !== -1) {
instance.type = 'text';
}
}
}
}
}
};
const tempTemplate = ({ childNodes: [template] } as any);
loopNodes(tempTemplate);
return arr;
}
/**
* Gets custom attibute search expressions form string
* @param stringAttribute - string attibute to check
* @internal
*
* click.trigger === #VARIABLE#.trigger
* some-custom-event.bind === #VARIABLE#.bind
* style@background-color.bind === style#VARIABLE#.bind
* style@background-color === style#VARIABLE#
*
* NB! I first check for just value, and just value + '.bind'
* This is just if 2 two checks fails
*/
public static getAttributeExpression(stringAttribute: string) {
const atIndex = stringAttribute.indexOf('@') !== -1 ? stringAttribute.indexOf('@') : 0;
const dotIndex = stringAttribute.lastIndexOf('.') !== -1 ? stringAttribute.lastIndexOf('.') : stringAttribute.length;
const toReplace = stringAttribute.substring(atIndex, dotIndex);
return stringAttribute.replace(toReplace, '#VARIABLE#');
}
}