fix(ivy): sync view with blueprint when necessary (#26263)

PR Close #26263
This commit is contained in:
Kara Erickson 2018-10-04 13:28:39 -07:00 committed by Jason Aden
parent fdaf573073
commit 51dfdd5dd1
7 changed files with 125 additions and 12 deletions

View File

@ -106,8 +106,9 @@ export function getOrCreateNodeInjectorForNode(
if (tView.firstTemplatePass) {
// TODO(kara): Store node injector with host bindings for that node (see VIEW_DATA.md)
tNode.injectorIndex = hostView.length;
tView.blueprint.push(0, 0, 0, 0, 0, 0, 0, 0, null); // foundation for cumulative bloom
tView.data.push(0, 0, 0, 0, 0, 0, 0, 0, tNode); // foundation for node bloom
setUpBloom(tView.data, tNode); // foundation for node bloom
setUpBloom(hostView, null); // foundation for cumulative bloom
setUpBloom(tView.blueprint, null);
tView.hostBindingStartIndex += INJECTOR_SIZE;
}
@ -118,16 +119,25 @@ export function getOrCreateNodeInjectorForNode(
const parentData = parentView[TVIEW].data as any;
const injectorIndex = tNode.injectorIndex;
for (let i = 0; i < PARENT_INJECTOR; i++) {
const bloomIndex = parentIndex + i;
hostView[injectorIndex + i] =
parentLoc === -1 ? 0 : parentView[bloomIndex] | parentData[bloomIndex];
// If a parent injector can't be found, its location is set to -1.
// In that case, we don't need to set up a cumulative bloom
if (parentLoc !== -1) {
for (let i = 0; i < PARENT_INJECTOR; i++) {
const bloomIndex = parentIndex + i;
// Creates a cumulative bloom filter that merges the parent's bloom filter
// and its own cumulative bloom (which contains tokens for all ancestors)
hostView[injectorIndex + i] = parentView[bloomIndex] | parentData[bloomIndex];
}
}
hostView[injectorIndex + PARENT_INJECTOR] = parentLoc;
return injectorIndex;
}
function setUpBloom(arr: any[], footer: TNode | null) {
arr.push(0, 0, 0, 0, 0, 0, 0, 0, footer);
}
export function getInjectorIndex(tNode: TNode, hostView: LViewData): number {
if (tNode.injectorIndex === -1 ||
// If the injector index is the same as its parent's injector index, then the index has been

View File

@ -335,6 +335,7 @@ export function leaveView(newView: LViewData, creationOnly?: boolean): void {
*/
function refreshDescendantViews() {
setHostBindings(tView.hostBindings);
const parentFirstTemplatePass = firstTemplatePass;
// This needs to be set before children are processed to support recursive components
tView.firstTemplatePass = firstTemplatePass = false;
@ -351,7 +352,7 @@ function refreshDescendantViews() {
executeHooks(directives !, tView.contentHooks, tView.contentCheckHooks, creationMode);
}
refreshChildComponents(tView.components);
refreshChildComponents(tView.components, parentFirstTemplatePass);
}
@ -388,10 +389,11 @@ function refreshContentQueries(tView: TView): void {
}
/** Refreshes child components in the current view. */
function refreshChildComponents(components: number[] | null): void {
function refreshChildComponents(
components: number[] | null, parentFirstTemplatePass: boolean): void {
if (components != null) {
for (let i = 0; i < components.length; i++) {
componentRefresh(components[i]);
componentRefresh(components[i], parentFirstTemplatePass);
}
}
}
@ -701,7 +703,7 @@ export function renderComponentOrTemplate<T>(
// Element was stored at 0 in data and directive was stored at 0 in directives
// in renderComponent()
setHostBindings(tView.hostBindings);
componentRefresh(HEADER_OFFSET);
componentRefresh(HEADER_OFFSET, false);
}
} finally {
if (rendererFactory.end) {
@ -2220,7 +2222,8 @@ export function embeddedViewEnd(): void {
*
* @param adjustedElementIndex Element index in LViewData[] (adjusted for HEADER_OFFSET)
*/
export function componentRefresh<T>(adjustedElementIndex: number): void {
export function componentRefresh<T>(
adjustedElementIndex: number, parentFirstTemplatePass: boolean): void {
ngDevMode && assertDataInRange(adjustedElementIndex);
const element = readElementValue(viewData[adjustedElementIndex]) as LElementNode;
ngDevMode && assertNodeType(tView.data[adjustedElementIndex] as TNode, TNodeType.Element);
@ -2230,10 +2233,44 @@ export function componentRefresh<T>(adjustedElementIndex: number): void {
// Only attached CheckAlways components or attached, dirty OnPush components should be checked
if (viewAttached(hostView) && hostView[FLAGS] & (LViewFlags.CheckAlways | LViewFlags.Dirty)) {
parentFirstTemplatePass && syncViewWithBlueprint(hostView);
detectChangesInternal(hostView, hostView[CONTEXT]);
}
}
/**
* Syncs an LViewData instance with its blueprint if they have gotten out of sync.
*
* Typically, blueprints and their view instances should always be in sync, so the loop here
* will be skipped. However, consider this case of two components side-by-side:
*
* App template:
* ```
* <comp></comp>
* <comp></comp>
* ```
*
* The following will happen:
* 1. App template begins processing.
* 2. First <comp> is matched as a component and its LViewData is created.
* 3. Second <comp> is matched as a component and its LViewData is created.
* 4. App template completes processing, so it's time to check child templates.
* 5. First <comp> template is checked. It has a directive, so its def is pushed to blueprint.
* 6. Second <comp> template is checked. Its blueprint has been updated by the first
* <comp> template, but its LViewData was created before this update, so it is out of sync.
*
* Note that embedded views inside ngFor loops will never be out of sync because these views
* are processed as soon as they are created.
*
* @param componentView The view to sync
*/
function syncViewWithBlueprint(componentView: LViewData) {
const componentTView = componentView[TVIEW];
for (let i = componentView.length; i < componentTView.blueprint.length; i++) {
componentView[i] = componentTView.blueprint[i];
}
}
/** Returns a boolean for whether the view is attached */
export function viewAttached(view: LViewData): boolean {
return (view[FLAGS] & LViewFlags.Attached) === LViewFlags.Attached;

View File

@ -965,6 +965,9 @@
{
"name": "setUpAttributes"
},
{
"name": "setUpBloom"
},
{
"name": "setValue"
},
@ -983,6 +986,9 @@
{
"name": "swapMultiContextEntries"
},
{
"name": "syncViewWithBlueprint"
},
{
"name": "template"
},

View File

@ -344,9 +344,15 @@
{
"name": "setUpAttributes"
},
{
"name": "setUpBloom"
},
{
"name": "stringify$2"
},
{
"name": "syncViewWithBlueprint"
},
{
"name": "text"
},

View File

@ -986,6 +986,9 @@
{
"name": "setUpAttributes"
},
{
"name": "setUpBloom"
},
{
"name": "setValue"
},
@ -1001,6 +1004,9 @@
{
"name": "stringify$2"
},
{
"name": "syncViewWithBlueprint"
},
{
"name": "template"
},

View File

@ -2339,6 +2339,9 @@
{
"name": "setUpAttributes"
},
{
"name": "setUpBloom"
},
{
"name": "setValue"
},
@ -2402,6 +2405,9 @@
{
"name": "symbolNames"
},
{
"name": "syncViewWithBlueprint"
},
{
"name": "tagSet"
},

View File

@ -8,7 +8,7 @@
import {EventEmitter} from '@angular/core';
import {AttributeMarker, defineComponent, defineDirective, tick} from '../../src/render3/index';
import {AttributeMarker, PublicFeature, defineComponent, defineDirective} from '../../src/render3/index';
import {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, listener, loadDirective, reference, text, textBinding} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {pureFunction1, pureFunction2} from '../../src/render3/pure_function';
@ -153,6 +153,48 @@ describe('elementProperty', () => {
expect(fixture.hostElement.id).toBe('other-id');
});
it('should support host bindings on second template pass', () => {
class HostBindingDir {
// @HostBinding()
id = 'foo';
static ngDirectiveDef = defineDirective({
type: HostBindingDir,
selectors: [['', 'hostBindingDir', '']],
factory: () => new HostBindingDir(),
hostVars: 1,
hostBindings: (directiveIndex: number, elementIndex: number) => {
elementProperty(
elementIndex, 'id', bind(loadDirective<HostBindingDir>(directiveIndex).id));
},
features: [PublicFeature]
});
}
/** <div hostBindingDir></div> */
const Parent = createComponent('parent', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
element(0, 'div', ['hostBindingDir', '']);
}
}, 1, 0, [HostBindingDir]);
/**
* <parent></parent>
* <parent></parent>
*/
const App = createComponent('app', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
element(0, 'parent');
element(1, 'parent');
}
}, 2, 0, [Parent]);
const fixture = new ComponentFixture(App);
const divs = fixture.hostElement.querySelectorAll('div');
expect(divs[0].id).toEqual('foo');
expect(divs[1].id).toEqual('foo');
});
it('should support component with host bindings and array literals', () => {
const ff = (v: any) => ['Nancy', v, 'Ned'];