fix(compiler): always wrap views into an own `<template>` element

This is needed to allow view instantiation also in browsers that
don’t support the `<template>` element and because of this would
return elements from the content of `<template>` elements when
using `element.querySelectorAll`.

Also stores the `elementBinder.nestedProtoView` correctly
when a template directive was used on a `<template>` element.
This commit is contained in:
Tobias Bosch 2014-12-01 15:18:55 -08:00
parent 95d86d151a
commit 63053438ea
5 changed files with 116 additions and 43 deletions

View File

@ -1,5 +1,6 @@
import {isBlank} from 'facade/lang';
import {List, ListWrapper} from 'facade/collection'; import {List, ListWrapper} from 'facade/collection';
import {DOM} from 'facade/dom'; import {DOM, Element} from 'facade/dom';
import {CompileElement} from './compile_element'; import {CompileElement} from './compile_element';
import {CompileStep} from './compile_step'; import {CompileStep} from './compile_step';
@ -13,12 +14,14 @@ export class CompileControl {
_parent:CompileElement; _parent:CompileElement;
_current:CompileElement; _current:CompileElement;
_results; _results;
_additionalChildren;
constructor(steps) { constructor(steps) {
this._steps = steps; this._steps = steps;
this._currentStepIndex = 0; this._currentStepIndex = 0;
this._parent = null; this._parent = null;
this._current = null; this._current = null;
this._results = null; this._results = null;
this._additionalChildren = null;
} }
// only public so that it can be used by compile_pipeline // only public so that it can be used by compile_pipeline
@ -39,15 +42,21 @@ export class CompileControl {
this._currentStepIndex = previousStepIndex; this._currentStepIndex = previousStepIndex;
this._parent = previousParent; this._parent = previousParent;
var localAdditionalChildren = this._additionalChildren;
this._additionalChildren = null;
return localAdditionalChildren;
} }
addParent(newElement:CompileElement) { addParent(newElement:CompileElement) {
var currEl = this._current.element;
var newEl = newElement.element;
DOM.parentElement(currEl).insertBefore(newEl, currEl);
DOM.appendChild(newEl, currEl);
this.internalProcess(this._results, this._currentStepIndex+1, this._parent, newElement); this.internalProcess(this._results, this._currentStepIndex+1, this._parent, newElement);
this._parent = newElement; this._parent = newElement;
} }
addChild(element:CompileElement) {
if (isBlank(this._additionalChildren)) {
this._additionalChildren = ListWrapper.create();
}
ListWrapper.push(this._additionalChildren, element);
}
} }

View File

@ -1,3 +1,4 @@
import {isPresent} from 'facade/lang';
import {List, ListWrapper} from 'facade/collection'; import {List, ListWrapper} from 'facade/collection';
import {Element, Node, DOM} from 'facade/dom'; import {Element, Node, DOM} from 'facade/dom';
import {CompileElement} from './compile_element'; import {CompileElement} from './compile_element';
@ -17,18 +18,24 @@ export class CompilePipeline {
process(rootElement:Element):List { process(rootElement:Element):List {
var results = ListWrapper.create(); var results = ListWrapper.create();
this._process(results, null, rootElement); this._process(results, null, new CompileElement(rootElement));
return results; return results;
} }
_process(results, parent:CompileElement, element:Element) { _process(results, parent:CompileElement, current:CompileElement) {
var current = new CompileElement(element); var additionalChildren = this._control.internalProcess(results, 0, parent, current);
this._control.internalProcess(results, 0, parent, current);
var childNodes = DOM.templateAwareRoot(element).childNodes; var childNodes = DOM.templateAwareRoot(current.element).childNodes;
for (var i=0; i<childNodes.length; i++) { for (var i=0; i<childNodes.length; i++) {
var node = childNodes[i]; var node = childNodes[i];
if (node.nodeType === Node.ELEMENT_NODE) { if (node.nodeType === Node.ELEMENT_NODE) {
this._process(results, current, node); this._process(results, current, new CompileElement(node));
}
}
if (isPresent(additionalChildren)) {
for (var i=0; i<additionalChildren.length; i++) {
this._process(results, current, additionalChildren[i]);
} }
} }
} }

View File

@ -1,6 +1,6 @@
import {isBlank, isPresent} from 'facade/lang'; import {isBlank, isPresent} from 'facade/lang';
import {DOM, TemplateElement} from 'facade/dom'; import {DOM, TemplateElement} from 'facade/dom';
import {MapWrapper, StringMapWrapper} from 'facade/collection'; import {MapWrapper, ListWrapper} from 'facade/collection';
import {Parser} from 'change_detection/parser/parser'; import {Parser} from 'change_detection/parser/parser';
@ -9,9 +9,20 @@ import {CompileElement} from './compile_element';
import {CompileControl} from './compile_control'; import {CompileControl} from './compile_control';
/** /**
* Splits views at template directives: * Splits views at `<template>` elements or elements with `template` attribute:
* Replaces the element with an empty <template> element that contains the * For `<template>` elements:
* template directive and all property bindings needed for the template directive. * - moves the content into a new and disconnected `<template>` element
* that is marked as view root.
*
* For elements with a `template` attribute:
* - replaces the element with an empty `<template>` element,
* parses the content of the `template` attribute and adds the information to that
* `<template>` element. Marks the elements as view root.
*
* Note: In both cases the root of the nested view is disconnected from its parent element.
* This is needed for browsers that don't support the `<template>` element
* as we want to do locate elements with bindings using `getElementsByClassName` later on,
* which should not descend into the nested view.
* *
* Fills: * Fills:
* - CompileElement#isViewRoot * - CompileElement#isViewRoot
@ -26,19 +37,41 @@ export class ViewSplitter extends CompileStep {
process(parent:CompileElement, current:CompileElement, control:CompileControl) { process(parent:CompileElement, current:CompileElement, control:CompileControl) {
var element = current.element; var element = current.element;
if (isBlank(parent) || (current.element instanceof TemplateElement)) { if (isBlank(parent)) {
current.isViewRoot = true; current.isViewRoot = true;
} else {
if (current.element instanceof TemplateElement) {
if (!current.isViewRoot) {
var viewRoot = new CompileElement(DOM.createTemplate(''));
this._moveChildNodes(current.element.content, viewRoot.element.content);
viewRoot.isViewRoot = true;
control.addChild(viewRoot);
}
} else { } else {
var templateBindings = MapWrapper.get(current.attrs(), 'template'); var templateBindings = MapWrapper.get(current.attrs(), 'template');
if (isPresent(templateBindings)) { if (isPresent(templateBindings)) {
var newParent = new CompileElement(DOM.createTemplate(''));
current.isViewRoot = true; current.isViewRoot = true;
var templateElement = DOM.createTemplate(''); this._parseTemplateBindings(templateBindings, newParent);
var newParentElement = new CompileElement(templateElement); this._addParentElement(current.element, newParent.element);
this._parseTemplateBindings(templateBindings, newParentElement);
control.addParent(newParentElement); control.addParent(newParent);
current.element.remove();
} }
} }
} }
}
_moveChildNodes(source, target) {
while (isPresent(source.firstChild)) {
DOM.appendChild(target, source.firstChild);
}
}
_addParentElement(currentElement, newParentElement) {
DOM.parentElement(currentElement).insertBefore(newParentElement, currentElement);
DOM.appendChild(newParentElement, currentElement);
}
_parseTemplateBindings(templateBindings:string, compileElement:CompileElement) { _parseTemplateBindings(templateBindings:string, compileElement:CompileElement) {
var bindings = this._parser.parseTemplateBindings(templateBindings); var bindings = this._parser.parseTemplateBindings(templateBindings);

View File

@ -1,7 +1,7 @@
import {describe, beforeEach, it, expect, iit, ddescribe} from 'test_lib/test_lib'; import {describe, beforeEach, it, expect, iit, ddescribe} from 'test_lib/test_lib';
import {ListWrapper, List} from 'facade/collection'; import {ListWrapper, List} from 'facade/collection';
import {DOM} from 'facade/dom'; import {DOM} from 'facade/dom';
import {isPresent, NumberWrapper} from 'facade/lang'; import {isPresent, NumberWrapper, StringWrapper} from 'facade/lang';
import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline'; import {CompilePipeline} from 'core/compiler/pipeline/compile_pipeline';
import {CompileElement} from 'core/compiler/pipeline/compile_element'; import {CompileElement} from 'core/compiler/pipeline/compile_element';
@ -21,16 +21,6 @@ export function main() {
}); });
describe('control.addParent', () => { describe('control.addParent', () => {
it('should wrap the underlying DOM element', () => {
var element = createElement('<div id="1"><span wrap0="1" id="2"><b id="3"></b></span></div>');
var pipeline = new CompilePipeline([
createWrapperStep('wrap0', [])
]);
pipeline.process(element);
expect(DOM.getOuterHTML(element)).toEqual('<div id="1"><a id="wrap0#0"><span wrap0="1" id="2"><b id="3"></b></span></a></div>');
});
it('should report the new parent to the following processor and the result', () => { it('should report the new parent to the following processor and the result', () => {
var element = createElement('<div id="1"><span wrap0="1" id="2"><b id="3"></b></span></div>'); var element = createElement('<div id="1"><span wrap0="1" id="2"><b id="3"></b></span></div>');
var step0Log = []; var step0Log = [];
@ -95,6 +85,26 @@ export function main() {
}); });
describe('control.addChild', () => {
it('should report the new child to all processors and the result', () => {
var element = createElement('<div id="1"><div id="2"></div></div>');
var resultLog = [];
var newChild = new CompileElement(createElement('<div id="3"></div>'));
var pipeline = new CompilePipeline([
new MockStep((parent, current, control) => {
if (StringWrapper.equals(current.element.id, '1')) {
control.addChild(newChild);
}
}),
createLoggerStep(resultLog)
]);
var result = pipeline.process(element);
expect(result[2]).toBe(newChild);
expect(resultLog).toEqual(['1', '1<2', '1<3']);
expect(resultIdLog(result)).toEqual(['1', '2', '3']);
});
});
}); });
} }

View File

@ -22,21 +22,35 @@ export function main() {
expect(results[0].isViewRoot).toBe(true); expect(results[0].isViewRoot).toBe(true);
}); });
it('should mark <template> elements as viewRoot', () => { describe('<template> elements', () => {
var rootElement = createElement('<div><template></template></div>');
it('should move the content into a new <template> element and mark that as viewRoot', () => {
var rootElement = createElement('<div><template if="true">a</template></div>');
var results = createPipeline().process(rootElement); var results = createPipeline().process(rootElement);
expect(results[1].isViewRoot).toBe(true); expect(DOM.getOuterHTML(results[1].element)).toEqual('<template if="true"></template>');
expect(results[1].isViewRoot).toBe(false);
expect(DOM.getOuterHTML(results[2].element)).toEqual('<template>a</template>');
expect(results[2].isViewRoot).toBe(true);
});
it('should not wrap a root <template> element', () => {
var rootElement = createElement('<div></div>');
var results = createPipeline().process(rootElement);
expect(results.length).toBe(1);
expect(DOM.getOuterHTML(rootElement)).toEqual('<div></div>');
});
}); });
describe('elements with template attribute', () => { describe('elements with template attribute', () => {
it('should insert an empty <template> element', () => { it('should replace the element with an empty <template> element', () => {
var rootElement = createElement('<div><div template></div></div>'); var rootElement = createElement('<div><span template=""></span></div>');
var originalChild = rootElement.childNodes[0]; var originalChild = rootElement.childNodes[0];
var results = createPipeline().process(rootElement); var results = createPipeline().process(rootElement);
expect(results[0].element).toBe(rootElement); expect(results[0].element).toBe(rootElement);
expect(results[1].element instanceof TemplateElement).toBe(true); expect(DOM.getOuterHTML(results[0].element)).toEqual('<div><template></template></div>');
expect(DOM.getInnerHTML(results[1].element)).toEqual(''); expect(DOM.getOuterHTML(results[2].element)).toEqual('<span template=""></span>')
expect(results[2].element).toBe(originalChild); expect(results[2].element).toBe(originalChild);
}); });