UNPKG

mframejs

Version:
653 lines (532 loc) 22.6 kB
import { customAttribute } from '../decorator/exported'; import { IAttribute, IBindingContext, ITemplateCache } from '../interface/exported'; import { View, AttributeController } from '../view/exported'; import { ViewController } from '../view/exported'; import { BindingEngine } from '../binding/exported'; import { ArrayMethodCallHandler, ArrayPropertyChange, PropertyChangeSimple } from './repeatAttributeSubscriberHelpers'; import { DOM } from '../utils/exported'; import { createBindingContext } from '../binding/createBindingContext'; export interface ITemplateArray { template: Node | Element | Node[] | Element[]; ctx: IBindingContext; $view: ViewController; } /** * repeat for attribute * */ @customAttribute('repeat.for') export class RepeatAttribute implements IAttribute { // added by template engine public $element: HTMLElement; public $attribute: Attr; public $bindingContext: IBindingContext; public $controller: AttributeController; // internal vars public value: any; public isAttached: boolean; private elementClone: Node; private anchor: Node; public arrayVariableName: string; public arrayExpression: string; public rowInstanceName: string; public arrayMethodCallHandler: ArrayMethodCallHandler; public arrayPropertyChangeHandler: ArrayPropertyChange; private propertyChangeHandlerSimple: PropertyChangeSimple; public templateArray: ITemplateArray[] = []; public loopBinded: Function; private repeatForArray: string[]; private $view: ViewController; public arrayType = 'object'; public arrayPropertyChangeHandlerLocal: ArrayPropertyChange; public $array: any; private templateElement: boolean; private cache: ITemplateCache[]; /** * sets local variables in the context of each row * */ public setArrayLocalVariables(ctx: any, i: number) { ctx.$index = i; ctx.$even = i % 2 === 0 ? true : false; ctx.$odd = i % 2 === 0 ? false : true; ctx.$last = i === this.templateArray.length - 1 ? true : false; ctx.$first = i === 0 ? true : false; } /** * created * */ public created() { this.value = this.$attribute.value; this.loopBinded = this.loopArray.bind(this); // get view instance, will need this when we create rows this.$view = this.$controller.getView(); // remove repeat attribute, so its not initiated again this.$element.attributes.removeNamedItem('repeat.for'); // clone of our row dom this.elementClone = this.$element.cloneNode(true); this.templateElement = (this.elementClone as any).tagName === 'TEMPLATE' ? true : false; if ((this.elementClone as Element).getAttribute('if.bind') && this.templateElement) { // if we have if.bind we need to pass entire template over this.templateElement = false; } // hide it for now, we will remove it when we are attached to parent this.$element.style.display = 'None'; // create dummy element, so viewparser can look at children const x = DOM.document.createElement('div'); // this is not needed if template tag (when I add) const template = this.elementClone.cloneNode(true); if (this.templateElement) { // IE11 if (!(<any>template).content) { (<any>template).content = DOM.document.createDocumentFragment(); while (template.childNodes[0]) { (<any>template).content.appendChild(template.childNodes[0]); } } } x.appendChild(this.templateElement ? (<any>template).content : template); // anchor so we know where to insert it, also add view to anchor, needed for viewparser to call detached this.anchor = DOM.document.createComment('mf-repeat-for'); // need something better... but good enough for now this.repeatForArray = this.value.split(' of '); this.repeatForArray = this.repeatForArray.map(x => x.trim()); if (this.repeatForArray.length !== 2) { console.error('unknown expression in repeat:' + this.value); } else { this.rowInstanceName = this.repeatForArray[0]; this.arrayVariableName = this.repeatForArray[1]; this.arrayExpression = this.repeatForArray[1]; if (this.arrayVariableName.indexOf('|')) { const split = this.arrayVariableName.split('|').map(x => x.trim()); this.arrayVariableName = split[0]; } this.$array = BindingEngine.evaluateExpression(this.arrayExpression, this.$bindingContext); const propertyType = this.$array; if (!Array.isArray(propertyType)) { // not array, this can be a number or a string. // do I want to support object or date ? // map and set Im not going to bother atm.. if (typeof propertyType === 'number') { this.arrayType = 'number'; this.subscribePropSimple(); } else { if (typeof propertyType === 'string') { this.arrayType = 'string'; this.subscribePropSimple(); } else { // console.error('unknown type, only support array, string and number'); // object array this.subscribeArray(); this.subscribePropArray(); // todo, add option for repeat.string, repeat.number } } } else { // object array this.subscribeArray(); this.subscribePropArray(); } } } /** * loops array and updates values/context * */ public loopArray(changed?: boolean, remove?: number, add?: number) { const array = this.$array; if (array) { if (changed) { if (remove) { const syncLength = this.templateArray.length - remove; for (let i = 0; i < this.templateArray.length; i++) { if (i < syncLength) { if (this.templateArray[i].ctx.$context[this.rowInstanceName] !== array[i]) { this.templateArray[i].ctx.$context[this.rowInstanceName] = array[i]; } } else { const temp = this.templateArray.pop(); i--; // this isnt very elegant... this.clearInRow(temp); } } } if (add) { const syncLength = array.length - add; for (let i = 0; i < array.length; i++) { if (i < syncLength) { if (this.templateArray[i].ctx.$context[this.rowInstanceName] !== this.$array[i]) { this.templateArray[i].ctx.$context[this.rowInstanceName] = this.$array[i]; } } else { this.push(array[i]); } } } } else { for (let i = 0; i < this.$array.length; i++) { if (this.templateArray[i].ctx.$context[this.rowInstanceName] !== array[i]) { this.templateArray[i].ctx.$context[this.rowInstanceName] = array[i]; } } } this.updateInternals(); } } /** * loops array/string and updates values/context * */ public loopArrayNumber(changed?: boolean, remove?: number, add?: number) { const num = this.templateArray.length + 1; if (num) { if (changed) { if (remove) { const syncLength = this.templateArray.length - remove; for (let i = 0; i < this.templateArray.length; i++) { if (i >= syncLength) { const temp = this.templateArray.pop(); i--; // this isnt very elegant... this.clearInRow(temp); } } } if (add) { const syncLength = num; for (let i = 0; i < num + add; i++) { if (i >= syncLength) { this.push(i); } } } } else { for (let i = 0; i < this.$array; i++) { if (this.templateArray[i].ctx.$context[this.rowInstanceName] !== i) { this.templateArray[i].ctx.$context[this.rowInstanceName] = i; } } } this.updateInternals(); } } /** * detached, unsubscribes events * */ public detached() { this.clearTemplateArray(); if (this.arrayMethodCallHandler) { BindingEngine.unSubscribeClassArray(this.$bindingContext, this.arrayMethodCallHandler); } if (this.arrayPropertyChangeHandler) { BindingEngine.unSubscribeClassProperty(this.$bindingContext, this.arrayPropertyChangeHandler); } if (this.arrayPropertyChangeHandlerLocal) { BindingEngine.unSubscribeClassProperty(this.$bindingContext, this.arrayPropertyChangeHandlerLocal); } if (this.propertyChangeHandlerSimple) { BindingEngine.unSubscribeClassProperty(this.$bindingContext, this.propertyChangeHandlerSimple); } if (this.anchor) { this.anchor.parentNode.removeChild(this.anchor); this.anchor = null; } this.$view = null; this.$array = null; this.arrayMethodCallHandler = null; this.arrayPropertyChangeHandler = null; this.arrayPropertyChangeHandlerLocal = null; this.propertyChangeHandlerSimple = null; } /** * attached, update rows * */ public attached() { // replace dom with anchor this.remove(); // var to set state this.isAttached = true; this.$array = BindingEngine.evaluateExpression(this.arrayExpression, this.$bindingContext); const array = this.$array; if (Array.isArray(array)) { array.forEach((ctx: any) => { this.push(ctx); }); } else { if (this.arrayType === 'number') { if (this.templateArray.length !== array) { this.clearTemplateArray(); for (let i = 0; i < array; i++) { this.push(i + 1); } } } if (this.arrayType === 'string') { const stringLength = typeof array === 'string' ? array.length : 0; if (this.templateArray.length !== stringLength) { this.clearTemplateArray(); for (let i = 0; i < stringLength; i++) { this.push(array[i]); } } } } this.updateInternals(); } /** * subscribe array in parent * */ public subscribeArray() { if (!this.arrayMethodCallHandler) { this.arrayMethodCallHandler = new ArrayMethodCallHandler(this); } BindingEngine.subscribeClassArray(this.$bindingContext, this.arrayVariableName, this.arrayMethodCallHandler); } /** * subscribe array prop in parent * */ public subscribePropArray() { if (!this.arrayPropertyChangeHandler) { this.arrayPropertyChangeHandler = new ArrayPropertyChange(this); } else { console.error('subscribePropArray fail, called when set', this.arrayExpression, this.arrayVariableName); } BindingEngine.subscribeClassProperty(this.$bindingContext, this.arrayVariableName, this.arrayPropertyChangeHandler); if (this.arrayVariableName !== this.arrayExpression) { // if expression is used we need to have a local handler // why local handler ? // - if we dont have a local handler we will be editing the main array, we need this for later // - if we do filter on main array and set it to the filtered array then we lose the original data if (!this.arrayPropertyChangeHandlerLocal) { this.arrayPropertyChangeHandlerLocal = new ArrayPropertyChange(this); } // replace original expression variable name with local "$array" used in this class // this will be the array we use for data const classKey = this.arrayExpression.replace(this.arrayVariableName, '$array'); BindingEngine.subscribeClassProperty(createBindingContext(this), classKey, this.arrayPropertyChangeHandlerLocal); } } /** * subscribe array in parent * */ public subscribePropSimple() { if (!this.propertyChangeHandlerSimple) { this.propertyChangeHandlerSimple = new PropertyChangeSimple(this); } BindingEngine.subscribeClassProperty(this.$bindingContext, this.arrayVariableName, this.propertyChangeHandlerSimple); } /** * pushes new row * */ public push(ctx: any, i?: number) { const template = this.elementClone.cloneNode(true); if (this.templateElement) { // IE11 if (!(<any>template).content) { (<any>template).content = DOM.document.createDocumentFragment(); while (template.childNodes[0]) { (<any>template).content.appendChild(template.childNodes[0]); } } } // parse node for custom elements and attributes with context object const context = createBindingContext({ [this.rowInstanceName]: ctx }, createBindingContext(this.$bindingContext, this.$bindingContext)); // append new child const temp = DOM.document.createElement('div'); temp.appendChild(this.templateElement ? (<any>template).content : template); const $view = new ViewController(template, this.$view); if (!this.cache) { this.cache = View.createTemplateCache(temp); } const controllers = View.parseTemplateCache(temp, context, $view, this.cache); let childNodes: undefined | any[]; if (this.templateElement) { childNodes = []; let anchor = DOM.document.createComment('repeat-template-row-anchor-start'); childNodes.push(anchor); for (let i = 0, ii = temp.childNodes.length; i < ii; i++) { childNodes.push(temp.childNodes[i]); } anchor = DOM.document.createComment('repeat-template-row-anchor-end'); childNodes.push(anchor); } if (i === undefined) { const length = this.templateArray.length; this.templateArray.push({ ctx: context, template: this.templateElement ? childNodes : template, $view: $view }); this.setArrayLocalVariables(context.$context, this.templateArray.length - 1); if (this.templateElement) { if (length) { const u = (this.templateArray[length - 1].template as Element[] | Node[]).length - 1; this.anchor.parentNode.insertBefore(childNodes[0], this.templateArray[length - 1].template[u].nextSibling); for (let i = 1, ii = childNodes.length; i < ii; i++) { this.anchor.parentNode.insertBefore(childNodes[i], childNodes[i - 1].nextSibling); } } else { this.anchor.parentNode.insertBefore(childNodes[0], this.anchor.nextSibling); for (let i = 1, ii = childNodes.length; i < ii; i++) { this.anchor.parentNode.insertBefore(childNodes[i], childNodes[i - 1].nextSibling); } } } else { if (length) { this.anchor.parentNode.insertBefore(template, (this.templateArray[length - 1].template as Element | Node).nextSibling); } else { this.anchor.parentNode.insertBefore(template, this.anchor.nextSibling); } } } else { this.templateArray.splice(i, 0, { ctx: context, template: this.templateElement ? childNodes : template, $view: $view }); if (this.templateElement) { if (this.templateArray[i + 1]) { const u = (this.templateArray[length - 1].template as Element[] | Node[]).length - 1; this.anchor.parentNode.insertBefore(childNodes[0], this.templateArray[length - 1].template[u].nextSibling); for (let i = 1, ii = childNodes.length; i < ii; i++) { this.anchor.parentNode.insertBefore(childNodes[i], childNodes[i - 1].nextSibling); } } else { this.anchor.parentNode.insertBefore(childNodes[0], this.anchor.nextSibling); for (let i = 1, ii = childNodes.length; i < ii; i++) { this.anchor.parentNode.insertBefore(childNodes[i], childNodes[i - 1].nextSibling); } } } else { if (this.templateArray[i + 1]) { this.anchor.parentNode.insertBefore(template, (this.templateArray[i + 1].template as Element | Node)); } else { this.anchor.parentNode.appendChild(template); } } } // call attached controllers.forEach((contr: any) => { // if it have attached method if (contr.attached) { contr.attached(); } }); } /** * pops row * */ public pop() { if (this.templateArray.length > 0) { const temp = this.templateArray.pop(); this.clearInRow(temp); this.updateInternals(); } } public updateInternals() { this.templateArray.forEach((context: any, i: number) => { this.setArrayLocalVariables(context.ctx.$context, i); }); } /** * shift row * */ public shift() { if (this.templateArray.length > 0) { const temp = this.templateArray.shift(); this.clearInRow(temp); this.updateInternals(); } } /** * splice row * */ public splice(args: any) { if (this.templateArray.length > 0) { const index = args[0]; const deleted = args[0] === 0 && args[1] === undefined ? this.templateArray.length : args[1]; const added = []; for (let i = 2; i < args.length; i++) { if (args[i]) { added.push(args[i]); } } if (deleted) { const newIndex = index + deleted - 1; for (let i = newIndex; i > index - 1; i--) { const temp = this.templateArray.splice(i, 1)[0]; this.clearInRow(temp); } } added.forEach((ctx) => { this.push(ctx, index); }); this.updateInternals(); // get our queue } } /** * clear template array * */ public clearTemplateArray() { // create temp div let x = DOM.document.createElement('div'); this.templateArray.forEach((temp: any) => { if (this.templateElement) { for (let i = 0, ii = temp.template.length; i < ii; i++) { x.appendChild(temp.template[i]); } } else { x.appendChild(temp.template); } }); // now clear all views this.templateArray.forEach((temp: any) => { temp.$view.clearView(); }); // clear all rows x = null; // reset template array this.templateArray = []; } /** * remove all * */ private remove() { // clear if any this.clearTemplateArray(); // my repeat fail fast if it have no parent, could this happend? this.$element.parentNode.replaceChild(this.anchor, this.$element); } /** * clear 1 row * */ private clearInRow(rowData: any) { if (rowData) { if (this.templateElement) { for (let i = 0, ii = rowData.template.length; i < ii; i++) { rowData.$view.clearView(); if (rowData.template[i].parentNode) { rowData.template[i].parentNode.removeChild(rowData.template[i]); } } } else { if (rowData.template.parentNode) { rowData.template.parentNode.removeChild(rowData.template); } rowData.$view.clearView(); } } } }