mframejs
Version:
simple framework
653 lines (532 loc) • 22.6 kB
text/typescript
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
*
*/
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();
}
}
}
}