fix(core): host bindings and host listeners for animations
Host bindings / listeners for animation properties should use the renderer of the component view.
This commit is contained in:
@ -81,42 +81,41 @@ function declareTests({useJit}: {useJit: boolean}) {
|
||||
]);
|
||||
});
|
||||
|
||||
xit('should trigger a state change animation from void => state on the component host element',
|
||||
() => {
|
||||
@Component({
|
||||
selector: 'my-cmp',
|
||||
template: '...',
|
||||
animations: [trigger(
|
||||
'myAnimation',
|
||||
[transition(
|
||||
'a => b',
|
||||
[style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
|
||||
})
|
||||
class Cmp {
|
||||
@HostBinding('@myAnimation')
|
||||
get binding() { return this.exp ? 'b' : 'a'; }
|
||||
exp: any = false;
|
||||
}
|
||||
it('should trigger a state change animation from void => state on the component host element',
|
||||
() => {
|
||||
@Component({
|
||||
selector: 'my-cmp',
|
||||
template: '...',
|
||||
animations: [trigger(
|
||||
'myAnimation',
|
||||
[transition(
|
||||
'a => b', [style({'opacity': '0'}), animate(500, style({'opacity': '1'}))])])],
|
||||
})
|
||||
class Cmp {
|
||||
@HostBinding('@myAnimation')
|
||||
get binding() { return this.exp ? 'b' : 'a'; }
|
||||
exp: any = false;
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
cmp.exp = false;
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
expect(getLog().length).toEqual(0);
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
cmp.exp = false;
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
expect(getLog().length).toEqual(0);
|
||||
|
||||
cmp.exp = true;
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
expect(getLog().length).toEqual(1);
|
||||
cmp.exp = true;
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
expect(getLog().length).toEqual(1);
|
||||
|
||||
const data = getLog().pop();
|
||||
expect(data.element).toEqual(fixture.elementRef.nativeElement);
|
||||
expect(data.keyframes).toEqual([{offset: 0, opacity: '0'}, {offset: 1, opacity: '1'}]);
|
||||
});
|
||||
const data = getLog().pop();
|
||||
expect(data.element).toEqual(fixture.elementRef.nativeElement);
|
||||
expect(data.keyframes).toEqual([{offset: 0, opacity: '0'}, {offset: 1, opacity: '1'}]);
|
||||
});
|
||||
|
||||
it('should cancel and merge in mid-animation styles into the follow-up animation', () => {
|
||||
@Component({
|
||||
@ -516,42 +515,42 @@ function declareTests({useJit}: {useJit: boolean}) {
|
||||
expect(cmp.event2.triggerName).toBeTruthy('ani2');
|
||||
});
|
||||
|
||||
xit('should trigger a state change listener for when the animation changes state from void => state on the host element',
|
||||
() => {
|
||||
@Component({
|
||||
selector: 'my-cmp',
|
||||
template: `...`,
|
||||
animations: [trigger(
|
||||
'myAnimation2',
|
||||
[transition(
|
||||
'void => *',
|
||||
[style({'opacity': '0'}), animate(1000, style({'opacity': '1'}))])])],
|
||||
})
|
||||
class Cmp {
|
||||
event: AnimationEvent;
|
||||
it('should trigger a state change listener for when the animation changes state from void => state on the host element',
|
||||
() => {
|
||||
@Component({
|
||||
selector: 'my-cmp',
|
||||
template: `...`,
|
||||
animations: [trigger(
|
||||
'myAnimation2',
|
||||
[transition(
|
||||
'void => *',
|
||||
[style({'opacity': '0'}), animate(1000, style({'opacity': '1'}))])])],
|
||||
})
|
||||
class Cmp {
|
||||
event: AnimationEvent;
|
||||
|
||||
@HostBinding('@myAnimation2')
|
||||
exp: any = false;
|
||||
@HostBinding('@myAnimation2')
|
||||
exp: any = false;
|
||||
|
||||
@HostListener('@myAnimation2.start')
|
||||
callback = (event: any) => { this.event = event; };
|
||||
}
|
||||
@HostListener('@myAnimation2.start', ['$event'])
|
||||
callback = (event: any) => { this.event = event; };
|
||||
}
|
||||
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
TestBed.configureTestingModule({declarations: [Cmp]});
|
||||
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
cmp.exp = 'TRUE';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
const engine = TestBed.get(ɵAnimationEngine);
|
||||
const fixture = TestBed.createComponent(Cmp);
|
||||
const cmp = fixture.componentInstance;
|
||||
cmp.exp = 'TRUE';
|
||||
fixture.detectChanges();
|
||||
engine.flush();
|
||||
|
||||
expect(cmp.event.triggerName).toEqual('myAnimation2');
|
||||
expect(cmp.event.phaseName).toEqual('start');
|
||||
expect(cmp.event.totalTime).toEqual(1000);
|
||||
expect(cmp.event.fromState).toEqual('void');
|
||||
expect(cmp.event.toState).toEqual('TRUE');
|
||||
});
|
||||
expect(cmp.event.triggerName).toEqual('myAnimation2');
|
||||
expect(cmp.event.phaseName).toEqual('start');
|
||||
expect(cmp.event.totalTime).toEqual(1000);
|
||||
expect(cmp.event.fromState).toEqual('void');
|
||||
expect(cmp.event.toState).toEqual('TRUE');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -7,294 +7,306 @@
|
||||
*/
|
||||
|
||||
import {Injector, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation} from '@angular/core';
|
||||
import {ArgumentType, BindingType, NodeCheckFn, NodeDef, NodeFlags, RootData, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewState, ViewUpdateFn, anchorDef, asProviderData, directiveDef, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
|
||||
import {ArgumentType, BindingType, NodeCheckFn, NodeDef, NodeFlags, OutputType, RootData, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewState, ViewUpdateFn, anchorDef, asElementData, asProviderData, directiveDef, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
|
||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
|
||||
import {createRootView, isBrowser, removeNodes} from './helper';
|
||||
|
||||
export function main() {
|
||||
describe(`Component Views`, () => {
|
||||
function compViewDef(
|
||||
nodes: NodeDef[], updateDirectives?: ViewUpdateFn, updateRenderer?: ViewUpdateFn,
|
||||
viewFlags: ViewFlags = ViewFlags.None): ViewDefinition {
|
||||
return viewDef(viewFlags, nodes, updateDirectives, updateRenderer);
|
||||
}
|
||||
|
||||
function createAndGetRootNodes(viewDef: ViewDefinition): {rootNodes: any[], view: ViewData} {
|
||||
const view = createRootView(viewDef);
|
||||
const rootNodes = rootRenderNodes(view);
|
||||
return {rootNodes, view};
|
||||
}
|
||||
|
||||
it('should create and attach component views', () => {
|
||||
let instance: AComp;
|
||||
class AComp {
|
||||
constructor() { instance = this; }
|
||||
}
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||
directiveDef(
|
||||
NodeFlags.None, null, 0, AComp, [], null, null,
|
||||
() => compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 0, 'span'),
|
||||
])),
|
||||
]));
|
||||
|
||||
const compView = asProviderData(view, 1).componentView;
|
||||
|
||||
expect(compView.context).toBe(instance);
|
||||
expect(compView.component).toBe(instance);
|
||||
|
||||
const compRootEl = getDOM().childNodes(rootNodes[0])[0];
|
||||
expect(getDOM().nodeName(compRootEl).toLowerCase()).toBe('span');
|
||||
});
|
||||
|
||||
if (isBrowser()) {
|
||||
describe('root views', () => {
|
||||
let rootNode: HTMLElement;
|
||||
beforeEach(() => {
|
||||
rootNode = document.createElement('root');
|
||||
document.body.appendChild(rootNode);
|
||||
removeNodes.push(rootNode);
|
||||
});
|
||||
|
||||
it('should select root elements based on a selector', () => {
|
||||
const view = createRootView(
|
||||
compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 0, 'div'),
|
||||
]),
|
||||
{}, [], 'root');
|
||||
const rootNodes = rootRenderNodes(view);
|
||||
expect(rootNodes).toEqual([rootNode]);
|
||||
});
|
||||
|
||||
it('should select root elements based on a node', () => {
|
||||
const view = createRootView(
|
||||
compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 0, 'div'),
|
||||
]),
|
||||
{}, [], rootNode);
|
||||
const rootNodes = rootRenderNodes(view);
|
||||
expect(rootNodes).toEqual([rootNode]);
|
||||
});
|
||||
|
||||
it('should set attributes on the root node', () => {
|
||||
const view = createRootView(
|
||||
compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 0, 'div', [['a', 'b']]),
|
||||
]),
|
||||
{}, [], rootNode);
|
||||
expect(rootNode.getAttribute('a')).toBe('b');
|
||||
});
|
||||
|
||||
it('should clear the content of the root node', () => {
|
||||
rootNode.appendChild(document.createElement('div'));
|
||||
const view = createRootView(
|
||||
compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 0, 'div', [['a', 'b']]),
|
||||
]),
|
||||
{}, [], rootNode);
|
||||
expect(rootNode.childNodes.length).toBe(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('data binding', () => {
|
||||
it('should dirty check component views', () => {
|
||||
let value: any;
|
||||
class AComp {
|
||||
a: any;
|
||||
describe(
|
||||
`Component Views`, () => {
|
||||
function compViewDef(
|
||||
nodes: NodeDef[], updateDirectives?: ViewUpdateFn, updateRenderer?: ViewUpdateFn,
|
||||
viewFlags: ViewFlags = ViewFlags.None): ViewDefinition {
|
||||
return viewDef(viewFlags, nodes, updateDirectives, updateRenderer);
|
||||
}
|
||||
|
||||
const update =
|
||||
jasmine.createSpy('updater').and.callFake((check: NodeCheckFn, view: ViewData) => {
|
||||
check(view, 0, ArgumentType.Inline, value);
|
||||
function createAndGetRootNodes(viewDef: ViewDefinition):
|
||||
{rootNodes: any[], view: ViewData} {
|
||||
const view = createRootView(viewDef);
|
||||
const rootNodes = rootRenderNodes(view);
|
||||
return {rootNodes, view};
|
||||
}
|
||||
|
||||
it('should create and attach component views', () => {
|
||||
let instance: AComp;
|
||||
class AComp {
|
||||
constructor() { instance = this; }
|
||||
}
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(
|
||||
NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 0, 'span'),
|
||||
])),
|
||||
directiveDef(NodeFlags.IsComponent, null, 0, AComp, []),
|
||||
]));
|
||||
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
|
||||
expect(compView.context).toBe(instance);
|
||||
expect(compView.component).toBe(instance);
|
||||
|
||||
const compRootEl = getDOM().childNodes(rootNodes[0])[0];
|
||||
expect(getDOM().nodeName(compRootEl).toLowerCase()).toBe('span');
|
||||
});
|
||||
|
||||
if (isBrowser()) {
|
||||
describe('root views', () => {
|
||||
let rootNode: HTMLElement;
|
||||
beforeEach(() => {
|
||||
rootNode = document.createElement('root');
|
||||
document.body.appendChild(rootNode);
|
||||
removeNodes.push(rootNode);
|
||||
});
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(
|
||||
it('should select root elements based on a selector', () => {
|
||||
const view = createRootView(
|
||||
compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 0, 'div'),
|
||||
]),
|
||||
{}, [], 'root');
|
||||
const rootNodes = rootRenderNodes(view);
|
||||
expect(rootNodes).toEqual([rootNode]);
|
||||
});
|
||||
|
||||
it('should select root elements based on a node', () => {
|
||||
const view = createRootView(
|
||||
compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 0, 'div'),
|
||||
]),
|
||||
{}, [], rootNode);
|
||||
const rootNodes = rootRenderNodes(view);
|
||||
expect(rootNodes).toEqual([rootNode]);
|
||||
});
|
||||
|
||||
it('should set attributes on the root node', () => {
|
||||
const view = createRootView(
|
||||
compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 0, 'div', [['a', 'b']]),
|
||||
]),
|
||||
{}, [], rootNode);
|
||||
expect(rootNode.getAttribute('a')).toBe('b');
|
||||
});
|
||||
|
||||
it('should clear the content of the root node', () => {
|
||||
rootNode.appendChild(document.createElement('div'));
|
||||
const view = createRootView(
|
||||
compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 0, 'div', [['a', 'b']]),
|
||||
]),
|
||||
{}, [], rootNode);
|
||||
expect(rootNode.childNodes.length).toBe(0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe(
|
||||
'data binding', () => {
|
||||
it('should dirty check component views',
|
||||
() => {
|
||||
let value: any;
|
||||
class AComp {
|
||||
a: any;
|
||||
}
|
||||
|
||||
const update = jasmine.createSpy('updater').and.callFake(
|
||||
(check: NodeCheckFn, view: ViewData) => {
|
||||
check(view, 0, ArgumentType.Inline, value);
|
||||
});
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(
|
||||
compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||
directiveDef(NodeFlags.None, null, 0, AComp, [], null, null, () => compViewDef(
|
||||
elementDef(NodeFlags.None, null, null, 1, 'div', null, null, null, null, () => compViewDef(
|
||||
[
|
||||
elementDef(NodeFlags.None, null, null, 0, 'span', null, [[BindingType.ElementAttribute, 'a', SecurityContext.NONE]]),
|
||||
], null, update
|
||||
)),
|
||||
directiveDef(NodeFlags.IsComponent, null, 0, AComp, []),
|
||||
]));
|
||||
const compView = asProviderData(view, 1).componentView;
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
|
||||
value = 'v1';
|
||||
Services.checkAndUpdateView(view);
|
||||
value = 'v1';
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
expect(update.calls.mostRecent().args[1]).toBe(compView);
|
||||
expect(update.calls.mostRecent().args[1]).toBe(compView);
|
||||
|
||||
update.calls.reset();
|
||||
Services.checkNoChangesView(view);
|
||||
update.calls.reset();
|
||||
Services.checkNoChangesView(view);
|
||||
|
||||
expect(update.calls.mostRecent().args[1]).toBe(compView);
|
||||
expect(update.calls.mostRecent().args[1]).toBe(compView);
|
||||
|
||||
value = 'v2';
|
||||
expect(() => Services.checkNoChangesView(view))
|
||||
.toThrowError(
|
||||
`ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`);
|
||||
});
|
||||
value = 'v2';
|
||||
expect(() => Services.checkNoChangesView(view))
|
||||
.toThrowError(
|
||||
`ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked. Previous value: 'v1'. Current value: 'v2'.`);
|
||||
});
|
||||
|
||||
it('should support detaching and attaching component views for dirty checking', () => {
|
||||
class AComp {
|
||||
a: any;
|
||||
}
|
||||
it('should support detaching and attaching component views for dirty checking',
|
||||
() => {
|
||||
class AComp {
|
||||
a: any;
|
||||
}
|
||||
|
||||
const update = jasmine.createSpy('updater');
|
||||
const update = jasmine.createSpy('updater');
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||
directiveDef(
|
||||
NodeFlags.None, null, 0, AComp, [], null, null,
|
||||
() => compViewDef(
|
||||
[
|
||||
elementDef(NodeFlags.None, null, null, 0, 'span'),
|
||||
],
|
||||
update)),
|
||||
]));
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(
|
||||
NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => compViewDef(
|
||||
[
|
||||
elementDef(NodeFlags.None, null, null, 0, 'span'),
|
||||
],
|
||||
update)),
|
||||
directiveDef(NodeFlags.IsComponent, null, 0, AComp, [], null, null),
|
||||
]));
|
||||
|
||||
const compView = asProviderData(view, 1).componentView;
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
update.calls.reset();
|
||||
Services.checkAndUpdateView(view);
|
||||
update.calls.reset();
|
||||
|
||||
compView.state &= ~ViewState.ChecksEnabled;
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
compView.state &= ~ViewState.ChecksEnabled;
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
|
||||
compView.state |= ViewState.ChecksEnabled;
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).toHaveBeenCalled();
|
||||
});
|
||||
compView.state |= ViewState.ChecksEnabled;
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
if (isBrowser()) {
|
||||
it('should support OnPush components', () => {
|
||||
let compInputValue: any;
|
||||
class AComp {
|
||||
a: any;
|
||||
}
|
||||
if (isBrowser()) {
|
||||
it('should support OnPush components', () => {
|
||||
let compInputValue: any;
|
||||
class AComp {
|
||||
a: any;
|
||||
}
|
||||
|
||||
const update = jasmine.createSpy('updater');
|
||||
const update = jasmine.createSpy('updater');
|
||||
|
||||
const addListenerSpy = spyOn(HTMLElement.prototype, 'addEventListener').and.callThrough();
|
||||
const addListenerSpy =
|
||||
spyOn(HTMLElement.prototype, 'addEventListener').and.callThrough();
|
||||
|
||||
const {view} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||
directiveDef(
|
||||
NodeFlags.None, null, 0, AComp, [], {a: [0, 'a']}, null,
|
||||
() => compViewDef(
|
||||
[
|
||||
elementDef(NodeFlags.None, null, null, 0, 'span', null, null, ['click']),
|
||||
],
|
||||
update, null, ViewFlags.OnPush)),
|
||||
],
|
||||
(check, view) => { check(view, 1, ArgumentType.Inline, compInputValue); }));
|
||||
const {view} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
elementDef(
|
||||
NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => {
|
||||
return compViewDef(
|
||||
[
|
||||
elementDef(
|
||||
NodeFlags.None, null, null, 0, 'span', null, null,
|
||||
[[null, 'click']]),
|
||||
],
|
||||
update, null, ViewFlags.OnPush);
|
||||
}),
|
||||
directiveDef(NodeFlags.IsComponent, null, 0, AComp, [], {a: [0, 'a']}),
|
||||
],
|
||||
(check, view) => { check(view, 1, ArgumentType.Inline, compInputValue); }));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
// auto detach
|
||||
update.calls.reset();
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
// auto detach
|
||||
update.calls.reset();
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
|
||||
// auto attach on input changes
|
||||
update.calls.reset();
|
||||
compInputValue = 'v1';
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).toHaveBeenCalled();
|
||||
// auto attach on input changes
|
||||
update.calls.reset();
|
||||
compInputValue = 'v1';
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).toHaveBeenCalled();
|
||||
|
||||
// auto detach
|
||||
update.calls.reset();
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
// auto detach
|
||||
update.calls.reset();
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
|
||||
// auto attach on events
|
||||
addListenerSpy.calls.mostRecent().args[1]('SomeEvent');
|
||||
update.calls.reset();
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).toHaveBeenCalled();
|
||||
// auto attach on events
|
||||
addListenerSpy.calls.mostRecent().args[1]('SomeEvent');
|
||||
update.calls.reset();
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).toHaveBeenCalled();
|
||||
|
||||
// auto detach
|
||||
update.calls.reset();
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
// auto detach
|
||||
update.calls.reset();
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
|
||||
it('should stop dirty checking views that threw errors in change detection', () => {
|
||||
class AComp {
|
||||
a: any;
|
||||
}
|
||||
it('should stop dirty checking views that threw errors in change detection',
|
||||
() => {
|
||||
class AComp {
|
||||
a: any;
|
||||
}
|
||||
|
||||
const update = jasmine.createSpy('updater');
|
||||
const update = jasmine.createSpy('updater');
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||
directiveDef(
|
||||
NodeFlags.None, null, 0, AComp, [], null, null,
|
||||
() => compViewDef(
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'div', null, null, null, null, () => compViewDef(
|
||||
[
|
||||
elementDef(NodeFlags.None, null, null, 0, 'span', null, [[BindingType.ElementAttribute, 'a', SecurityContext.NONE]]),
|
||||
],
|
||||
null, update)),
|
||||
]));
|
||||
|
||||
const compView = asProviderData(view, 1).componentView;
|
||||
|
||||
update.and.callFake((check: NodeCheckFn, view: ViewData) => { throw new Error('Test'); });
|
||||
expect(() => Services.checkAndUpdateView(view)).toThrowError('Test');
|
||||
expect(update).toHaveBeenCalled();
|
||||
|
||||
update.calls.reset();
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('destroy', () => {
|
||||
it('should destroy component views', () => {
|
||||
const log: string[] = [];
|
||||
|
||||
class AComp {}
|
||||
|
||||
class ChildProvider {
|
||||
ngOnDestroy() { log.push('ngOnDestroy'); };
|
||||
}
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||
directiveDef(
|
||||
NodeFlags.None, null, 0, AComp, [], null, null,
|
||||
() => compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(NodeFlags.OnDestroy, null, 0, ChildProvider, [])
|
||||
])),
|
||||
NodeFlags.IsComponent, null, 0, AComp, [], null, null,
|
||||
),
|
||||
]));
|
||||
|
||||
Services.destroyView(view);
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
|
||||
update.and.callFake(
|
||||
(check: NodeCheckFn, view: ViewData) => { throw new Error('Test'); });
|
||||
expect(() => Services.checkAndUpdateView(view)).toThrowError('Test');
|
||||
expect(update).toHaveBeenCalled();
|
||||
|
||||
update.calls.reset();
|
||||
Services.checkAndUpdateView(view);
|
||||
expect(update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('destroy', () => {
|
||||
it('should destroy component views', () => {
|
||||
const log: string[] = [];
|
||||
|
||||
class AComp {}
|
||||
|
||||
class ChildProvider {
|
||||
ngOnDestroy() { log.push('ngOnDestroy'); };
|
||||
}
|
||||
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(
|
||||
NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(NodeFlags.OnDestroy, null, 0, ChildProvider, [])
|
||||
])),
|
||||
directiveDef(NodeFlags.IsComponent, null, 0, AComp, [], null, null, ),
|
||||
]));
|
||||
|
||||
Services.destroyView(view);
|
||||
|
||||
expect(log).toEqual(['ngOnDestroy']);
|
||||
});
|
||||
|
||||
it('should throw on dirty checking destroyed views', () => {
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
elementDef(NodeFlags.None, null, null, 0, 'div'),
|
||||
],
|
||||
(view) => {}));
|
||||
|
||||
Services.destroyView(view);
|
||||
|
||||
expect(() => Services.checkAndUpdateView(view))
|
||||
.toThrowError('ViewDestroyedError: Attempt to use a destroyed view: detectChanges');
|
||||
});
|
||||
});
|
||||
|
||||
expect(log).toEqual(['ngOnDestroy']);
|
||||
});
|
||||
|
||||
it('should throw on dirty checking destroyed views', () => {
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef(
|
||||
[
|
||||
elementDef(NodeFlags.None, null, null, 0, 'div'),
|
||||
],
|
||||
(view) => {}));
|
||||
|
||||
Services.destroyView(view);
|
||||
|
||||
expect(() => Services.checkAndUpdateView(view))
|
||||
.toThrowError('ViewDestroyedError: Attempt to use a destroyed view: detectChanges');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
|
||||
import {Injector, RenderComponentType, RootRenderer, Sanitizer, SecurityContext, ViewEncapsulation, WrappedValue, getDebugNode} from '@angular/core';
|
||||
import {getDebugContext} from '@angular/core/src/errors';
|
||||
import {ArgumentType, BindingType, DebugContext, NodeDef, NodeFlags, RootData, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
|
||||
import {ArgumentType, BindingType, DebugContext, NodeDef, NodeFlags, OutputType, RootData, Services, ViewData, ViewDefinition, ViewFlags, ViewHandleEventFn, ViewUpdateFn, anchorDef, asElementData, elementDef, rootRenderNodes, textDef, viewDef} from '@angular/core/src/view/index';
|
||||
import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter';
|
||||
|
||||
import {ARG_TYPE_VALUES, checkNodeInlineOrDynamic, createRootView, isBrowser, removeNodes} from './helper';
|
||||
@ -190,7 +190,8 @@ export function main() {
|
||||
const removeListenerSpy =
|
||||
spyOn(HTMLElement.prototype, 'removeEventListener').and.callThrough();
|
||||
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
|
||||
NodeFlags.None, null, null, 0, 'button', null, null, ['click'], handleEventSpy)]));
|
||||
NodeFlags.None, null, null, 0, 'button', null, null, [[null, 'click']],
|
||||
handleEventSpy)]));
|
||||
|
||||
rootNodes[0].click();
|
||||
|
||||
@ -256,7 +257,7 @@ export function main() {
|
||||
let preventDefaultSpy: jasmine.Spy;
|
||||
|
||||
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
|
||||
NodeFlags.None, null, null, 0, 'button', null, null, ['click'],
|
||||
NodeFlags.None, null, null, 0, 'button', null, null, [[null, 'click']],
|
||||
(view, eventName, event) => {
|
||||
preventDefaultSpy = spyOn(event, 'preventDefault').and.callThrough();
|
||||
return eventHandlerResult;
|
||||
@ -281,10 +282,9 @@ export function main() {
|
||||
|
||||
it('should report debug info on event errors', () => {
|
||||
const addListenerSpy = spyOn(HTMLElement.prototype, 'addEventListener').and.callThrough();
|
||||
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef(
|
||||
[elementDef(NodeFlags.None, null, null, 0, 'button', null, null, ['click'], () => {
|
||||
throw new Error('Test');
|
||||
})]));
|
||||
const {view, rootNodes} = createAndAttachAndGetRootNodes(compViewDef([elementDef(
|
||||
NodeFlags.None, null, null, 0, 'button', null, null, [[null, 'click']],
|
||||
() => { throw new Error('Test'); })]));
|
||||
|
||||
let err: any;
|
||||
try {
|
||||
|
@ -30,9 +30,10 @@ export function main() {
|
||||
const aCompViewDef = compViewDef(viewNodes);
|
||||
|
||||
return [
|
||||
elementDef(NodeFlags.None, null, null, 1 + contentNodes.length, 'acomp'),
|
||||
directiveDef(NodeFlags.None, null, 0, AComp, [], null, null, () => aCompViewDef),
|
||||
...contentNodes
|
||||
elementDef(
|
||||
NodeFlags.None, null, null, 1 + contentNodes.length, 'acomp', null, null, null, null,
|
||||
() => aCompViewDef),
|
||||
directiveDef(NodeFlags.IsComponent, null, 0, AComp, []), ...contentNodes
|
||||
];
|
||||
}
|
||||
|
||||
@ -110,7 +111,7 @@ export function main() {
|
||||
])),
|
||||
])));
|
||||
|
||||
const componentView = asProviderData(view, 1).componentView;
|
||||
const componentView = asElementData(view, 0).componentView;
|
||||
const view0 = Services.createEmbeddedView(componentView, componentView.def.nodes[1]);
|
||||
|
||||
attachEmbeddedView(asElementData(componentView, 1), 0, view0);
|
||||
|
@ -113,10 +113,10 @@ export function main() {
|
||||
try {
|
||||
createRootView(
|
||||
compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||
directiveDef(
|
||||
NodeFlags.None, null, 0, SomeService, [], null, null,
|
||||
() => compViewDef([textDef(null, ['a'])]))
|
||||
elementDef(
|
||||
NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => compViewDef([textDef(null, ['a'])])),
|
||||
directiveDef(NodeFlags.IsComponent, null, 0, SomeService, [])
|
||||
]),
|
||||
TestBed.get(Injector), [], getDOM().createElement('div'));
|
||||
} catch (e) {
|
||||
@ -174,13 +174,13 @@ export function main() {
|
||||
|
||||
it('should inject from a parent elment in a parent view', () => {
|
||||
createAndGetRootNodes(compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||
directiveDef(
|
||||
NodeFlags.None, null, 0, Dep, [], null, null,
|
||||
elementDef(
|
||||
NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(NodeFlags.None, null, 0, SomeService, [Dep])
|
||||
])),
|
||||
directiveDef(NodeFlags.IsComponent, null, 0, Dep, []),
|
||||
]));
|
||||
|
||||
expect(instance.dep instanceof Dep).toBeTruthy();
|
||||
@ -275,24 +275,24 @@ export function main() {
|
||||
|
||||
it('should inject ChangeDetectorRef for component providers', () => {
|
||||
const {view, rootNodes} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||
directiveDef(
|
||||
NodeFlags.None, null, 0, SomeService, [ChangeDetectorRef], null, null,
|
||||
elementDef(
|
||||
NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 0, 'span'),
|
||||
])),
|
||||
directiveDef(NodeFlags.IsComponent, null, 0, SomeService, [ChangeDetectorRef]),
|
||||
]));
|
||||
|
||||
const compView = asProviderData(view, 1).componentView;
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
expect(instance.dep._view).toBe(compView);
|
||||
});
|
||||
|
||||
it('should inject RendererV1', () => {
|
||||
createAndGetRootNodes(compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(
|
||||
NodeFlags.None, null, 0, SomeService, [Renderer], null, null,
|
||||
() => compViewDef([anchorDef(NodeFlags.None, null, null, 0)]))
|
||||
elementDef(
|
||||
NodeFlags.None, null, null, 1, 'span', null, null, null, null,
|
||||
() => compViewDef([anchorDef(NodeFlags.None, null, null, 0)])),
|
||||
directiveDef(NodeFlags.IsComponent, null, 0, SomeService, [Renderer])
|
||||
]));
|
||||
|
||||
expect(instance.dep.createElement).toBeTruthy();
|
||||
@ -300,10 +300,10 @@ export function main() {
|
||||
|
||||
it('should inject RendererV2', () => {
|
||||
createAndGetRootNodes(compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'span'),
|
||||
directiveDef(
|
||||
NodeFlags.None, null, 0, SomeService, [RendererV2], null, null,
|
||||
() => compViewDef([anchorDef(NodeFlags.None, null, null, 0)]))
|
||||
elementDef(
|
||||
NodeFlags.None, null, null, 1, 'span', null, null, null, null,
|
||||
() => compViewDef([anchorDef(NodeFlags.None, null, null, 0)])),
|
||||
directiveDef(NodeFlags.IsComponent, null, 0, SomeService, [RendererV2])
|
||||
]));
|
||||
|
||||
expect(instance.dep.createElement).toBeTruthy();
|
||||
|
@ -50,16 +50,17 @@ export function main() {
|
||||
];
|
||||
}
|
||||
|
||||
function viewQueryProviders(nodes: NodeDef[]) {
|
||||
function compViewQueryProviders(extraChildCount: number, nodes: NodeDef[]) {
|
||||
return [
|
||||
directiveDef(
|
||||
NodeFlags.None, null, 0, QueryService, [], null, null,
|
||||
elementDef(
|
||||
NodeFlags.None, null, null, 1 + extraChildCount, 'div', null, null, null, null,
|
||||
() => compViewDef([
|
||||
queryDef(
|
||||
NodeFlags.HasViewQuery | NodeFlags.HasDynamicQuery, someQueryId,
|
||||
{'a': QueryBindingType.All}),
|
||||
...nodes
|
||||
])),
|
||||
directiveDef(NodeFlags.IsComponent, null, 0, QueryService, [], null, null, ),
|
||||
];
|
||||
}
|
||||
|
||||
@ -110,27 +111,29 @@ export function main() {
|
||||
describe('view queries', () => {
|
||||
it('should query providers in the view', () => {
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||
...viewQueryProviders([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'span'),
|
||||
aServiceProvider(),
|
||||
]),
|
||||
...compViewQueryProviders(
|
||||
0,
|
||||
[
|
||||
elementDef(NodeFlags.None, null, null, 1, 'span'),
|
||||
aServiceProvider(),
|
||||
]),
|
||||
]));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
|
||||
const comp: QueryService = asProviderData(view, 1).instance;
|
||||
const compView = asProviderData(view, 1).componentView;
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
expect(comp.a.length).toBe(1);
|
||||
expect(comp.a.first).toBe(asProviderData(compView, 2).instance);
|
||||
});
|
||||
|
||||
it('should not query providers on the host element', () => {
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 2, 'div'),
|
||||
...viewQueryProviders([
|
||||
elementDef(NodeFlags.None, null, null, 0, 'span'),
|
||||
]),
|
||||
...compViewQueryProviders(
|
||||
1,
|
||||
[
|
||||
elementDef(NodeFlags.None, null, null, 0, 'span'),
|
||||
]),
|
||||
aServiceProvider(),
|
||||
]));
|
||||
|
||||
@ -225,13 +228,14 @@ export function main() {
|
||||
|
||||
it('should update view queries if embedded views are added or removed', () => {
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||
...viewQueryProviders([
|
||||
anchorDef(NodeFlags.HasEmbeddedViews, null, null, 0, null, embeddedViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||
aServiceProvider(),
|
||||
])),
|
||||
]),
|
||||
...compViewQueryProviders(
|
||||
0,
|
||||
[
|
||||
anchorDef(NodeFlags.HasEmbeddedViews, null, null, 0, null, embeddedViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||
aServiceProvider(),
|
||||
])),
|
||||
]),
|
||||
]));
|
||||
|
||||
Services.checkAndUpdateView(view);
|
||||
@ -239,7 +243,7 @@ export function main() {
|
||||
const comp: QueryService = asProviderData(view, 1).instance;
|
||||
expect(comp.a.length).toBe(0);
|
||||
|
||||
const compView = asProviderData(view, 1).componentView;
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
const childView = Services.createEmbeddedView(compView, compView.def.nodes[1]);
|
||||
attachEmbeddedView(asElementData(compView, 1), 0, childView);
|
||||
Services.checkAndUpdateView(view);
|
||||
|
@ -35,20 +35,20 @@ export function main() {
|
||||
|
||||
function createViewWithData() {
|
||||
const {view} = createAndGetRootNodes(compViewDef([
|
||||
elementDef(NodeFlags.None, null, null, 1, 'div'),
|
||||
directiveDef(
|
||||
NodeFlags.None, null, 0, AComp, [], null, null,
|
||||
elementDef(
|
||||
NodeFlags.None, null, null, 1, 'div', null, null, null, null,
|
||||
() => compViewDef([
|
||||
elementDef(NodeFlags.None, [['ref', QueryValueType.ElementRef]], null, 2, 'span'),
|
||||
directiveDef(NodeFlags.None, null, 0, AService, []), textDef(null, ['a'])
|
||||
])),
|
||||
directiveDef(NodeFlags.IsComponent, null, 0, AComp, []),
|
||||
]));
|
||||
return view;
|
||||
}
|
||||
|
||||
it('should provide data for elements', () => {
|
||||
const view = createViewWithData();
|
||||
const compView = asProviderData(view, 1).componentView;
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
|
||||
const debugCtx = Services.createDebugContext(compView, 0);
|
||||
|
||||
@ -65,7 +65,7 @@ export function main() {
|
||||
|
||||
it('should provide data for text nodes', () => {
|
||||
const view = createViewWithData();
|
||||
const compView = asProviderData(view, 1).componentView;
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
|
||||
const debugCtx = Services.createDebugContext(compView, 2);
|
||||
|
||||
@ -79,7 +79,7 @@ export function main() {
|
||||
|
||||
it('should provide data for other nodes based on the nearest element parent', () => {
|
||||
const view = createViewWithData();
|
||||
const compView = asProviderData(view, 1).componentView;
|
||||
const compView = asElementData(view, 0).componentView;
|
||||
|
||||
const debugCtx = Services.createDebugContext(compView, 1);
|
||||
|
||||
|
Reference in New Issue
Block a user