diff --git a/packages/core/src/render3/component.ts b/packages/core/src/render3/component.ts index 4d3bf089c1..c2cceba0c1 100644 --- a/packages/core/src/render3/component.ts +++ b/packages/core/src/render3/component.ts @@ -12,8 +12,8 @@ import {Injector} from '../di/injector'; import {ComponentRef as viewEngine_ComponentRef} from '../linker/component_factory'; import {assertNotNull} from './assert'; -import {queueLifecycleHooks} from './hooks'; -import {CLEAN_PROMISE, _getComponentHostLElementNode, createLView, createTView, directiveCreate, enterView, getDirectiveInstance, getRootView, hostElement, initChangeDetectorIfExisting, locateHostElement, renderComponentOrTemplate} from './instructions'; +import {queueInitHooks, queueLifecycleHooks} from './hooks'; +import {CLEAN_PROMISE, _getComponentHostLElementNode, baseDirectiveCreate, createLView, createTView, enterView, getRootView, hostElement, initChangeDetectorIfExisting, locateHostElement, renderComponentOrTemplate} from './instructions'; import {ComponentDef, ComponentType} from './interfaces/definition'; import {LElementNode} from './interfaces/node'; import {RElement, RendererFactory3, domRendererFactory3} from './interfaces/renderer'; @@ -22,6 +22,7 @@ import {stringify} from './util'; import {createViewRef} from './view_ref'; + /** Options that control how the component should be bootstrapped. */ export interface CreateComponentOptions { /** Which renderer factory to use. */ @@ -135,7 +136,7 @@ export function renderComponent( // Create element node at index 0 in data array elementNode = hostElement(hostNode, componentDef); // Create directive instance with n() and store at index 1 in data array (el is 0) - component = rootContext.component = directiveCreate(1, componentDef.factory(), componentDef) as T; + component = rootContext.component = baseDirectiveCreate(1, componentDef.factory(), componentDef) as T; initChangeDetectorIfExisting(elementNode.nodeInjector, component); } finally { // We must not use leaveView here because it will set creationMode to false too early, @@ -164,6 +165,9 @@ export function renderComponent( */ export function LifecycleHooksFeature(component: any, def: ComponentDef): void { const elementNode = _getComponentHostLElementNode(component); + + // Root component is always created at dir index 1, after host element at 0 + queueInitHooks(1, def.onInit, def.doCheck, elementNode.view.tView); queueLifecycleHooks(elementNode.flags, elementNode.view); } diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index c92cc33e1e..18422aabae 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -1096,6 +1096,34 @@ export function textBinding(index: number, value: T | NO_CHANGE): void { export function directiveCreate( index: number, directive: T, directiveDef: DirectiveDef, localNames?: (string | number)[] | null): T { + const instance = baseDirectiveCreate(index, directive, directiveDef); + + ngDevMode && assertNotNull(previousOrParentNode.tNode, 'previousOrParentNode.tNode'); + const tNode: TNode|null = previousOrParentNode.tNode !; + + if (currentView.tView.firstTemplatePass && localNames) { + tNode.localNames = tNode.localNames ? tNode.localNames.concat(localNames) : localNames; + } + + if (tNode && tNode.attrs) { + setInputsFromAttrs(instance, directiveDef !.inputs, tNode); + } + + // Init hooks are queued now so ngOnInit is called in host components before + // any projected components. + queueInitHooks(index, directiveDef.onInit, directiveDef.doCheck, currentView.tView); + + return instance; +} + +/** + * A lighter version of directiveCreate() that is used for the root component + * + * This version does not contain features that we don't already support at root in + * current Angular. Example: local refs and inputs on root component. + */ +export function baseDirectiveCreate( + index: number, directive: T, directiveDef: DirectiveDef): T { let instance; ngDevMode && assertNull(currentView.bindingStartIndex, 'directives should be created before any bindings'); @@ -1117,11 +1145,6 @@ export function directiveCreate( if (index >= tData.length) { tData[index] = directiveDef !; - if (localNames) { - ngDevMode && assertNotNull(previousOrParentNode.tNode, 'previousOrParentNode.tNode'); - const tNode = previousOrParentNode !.tNode !; - tNode.localNames = tNode.localNames ? tNode.localNames.concat(localNames) : localNames; - } } const diPublic = directiveDef !.diPublic; @@ -1135,15 +1158,6 @@ export function directiveCreate( (previousOrParentNode as LElementNode).native, directiveDef !.attributes as string[]); } - const tNode: TNode|null = previousOrParentNode.tNode !; - if (tNode && tNode.attrs) { - setInputsFromAttrs(instance, directiveDef !.inputs, tNode); - } - - // Init hooks are queued now so ngOnInit is called in host components before - // any projected components. - queueInitHooks(index, directiveDef.onInit, directiveDef.doCheck, currentView.tView); - return instance; } diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index 3409017e0e..e46681f06a 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -41,6 +41,9 @@ { "name": "appendChild" }, + { + "name": "baseDirectiveCreate" + }, { "name": "bindingUpdated" }, @@ -74,9 +77,6 @@ { "name": "detectChangesInternal" }, - { - "name": "directiveCreate" - }, { "name": "domRendererFactory3" }, @@ -98,9 +98,6 @@ { "name": "findNextRNodeSibling" }, - { - "name": "generateInitialInputs" - }, { "name": "getDirectiveInstance" }, @@ -146,9 +143,6 @@ { "name": "locateHostElement" }, - { - "name": "queueInitHooks" - }, { "name": "refreshChildComponents" }, @@ -179,9 +173,6 @@ { "name": "setHostBindings" }, - { - "name": "setInputsFromAttrs" - }, { "name": "setUpAttributes" }, diff --git a/packages/core/test/render3/change_detection_spec.ts b/packages/core/test/render3/change_detection_spec.ts index 076d722a24..e2238bbe07 100644 --- a/packages/core/test/render3/change_detection_spec.ts +++ b/packages/core/test/render3/change_detection_spec.ts @@ -10,7 +10,7 @@ import {withBody} from '@angular/core/testing'; import {ChangeDetectionStrategy, ChangeDetectorRef, DoCheck} from '../../src/core'; import {getRenderedText, whenRendered} from '../../src/render3/component'; -import {defineComponent, defineDirective, injectChangeDetectorRef} from '../../src/render3/index'; +import {LifecycleHooksFeature, defineComponent, defineDirective, injectChangeDetectorRef} from '../../src/render3/index'; import {bind, container, containerRefreshEnd, containerRefreshStart, detectChanges, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, listener, markDirty, text, textBinding, tick} from '../../src/render3/instructions'; import {containerEl, renderComponent, requestAnimationFrame} from './render_util'; @@ -39,7 +39,7 @@ describe('change detection', () => { } it('should mark a component dirty and schedule change detection', withBody('my-comp', () => { - const myComp = renderComponent(MyComponent); + const myComp = renderComponent(MyComponent, {hostFeatures: [LifecycleHooksFeature]}); expect(getRenderedText(myComp)).toEqual('works'); myComp.value = 'updated'; markDirty(myComp); @@ -49,7 +49,7 @@ describe('change detection', () => { })); it('should detectChanges on a component', withBody('my-comp', () => { - const myComp = renderComponent(MyComponent); + const myComp = renderComponent(MyComponent, {hostFeatures: [LifecycleHooksFeature]}); expect(getRenderedText(myComp)).toEqual('works'); myComp.value = 'updated'; detectChanges(myComp); @@ -58,7 +58,7 @@ describe('change detection', () => { it('should detectChanges only once if markDirty is called multiple times', withBody('my-comp', () => { - const myComp = renderComponent(MyComponent); + const myComp = renderComponent(MyComponent, {hostFeatures: [LifecycleHooksFeature]}); expect(getRenderedText(myComp)).toEqual('works'); expect(myComp.doCheckCount).toBe(1); myComp.value = 'ignore'; @@ -72,7 +72,7 @@ describe('change detection', () => { })); it('should notify whenRendered', withBody('my-comp', async() => { - const myComp = renderComponent(MyComponent); + const myComp = renderComponent(MyComponent, {hostFeatures: [LifecycleHooksFeature]}); await whenRendered(myComp); myComp.value = 'updated'; markDirty(myComp); @@ -347,7 +347,7 @@ describe('change detection', () => { it('should check the component view when called by component (even when OnPush && clean)', () => { - const comp = renderComponent(MyComp); + const comp = renderComponent(MyComp, {hostFeatures: [LifecycleHooksFeature]}); expect(getRenderedText(comp)).toEqual('Nancy'); comp.name = 'Bess'; // as this is not an Input, the component stays clean @@ -356,7 +356,7 @@ describe('change detection', () => { }); it('should NOT call component doCheck when called by a component', () => { - const comp = renderComponent(MyComp); + const comp = renderComponent(MyComp, {hostFeatures: [LifecycleHooksFeature]}); expect(comp.doCheckCount).toEqual(1); // NOTE: in current Angular, detectChanges does not itself trigger doCheck, but you @@ -367,7 +367,7 @@ describe('change detection', () => { }); it('should NOT check the component parent when called by a child component', () => { - const parentComp = renderComponent(ParentComp); + const parentComp = renderComponent(ParentComp, {hostFeatures: [LifecycleHooksFeature]}); expect(getRenderedText(parentComp)).toEqual('1 - Nancy'); parentComp.doCheckCount = 100; @@ -378,7 +378,7 @@ describe('change detection', () => { it('should check component children when called by component if dirty or check-always', () => { - const parentComp = renderComponent(ParentComp); + const parentComp = renderComponent(ParentComp, {hostFeatures: [LifecycleHooksFeature]}); expect(parentComp.doCheckCount).toEqual(1); myComp.name = 'Bess'; @@ -390,7 +390,7 @@ describe('change detection', () => { }); it('should not group detectChanges calls (call every time)', () => { - const parentComp = renderComponent(ParentComp); + const parentComp = renderComponent(ParentComp, {hostFeatures: [LifecycleHooksFeature]}); expect(myComp.doCheckCount).toEqual(1); parentComp.cdr.detectChanges(); @@ -524,7 +524,7 @@ describe('change detection', () => { }); } - const comp = renderComponent(DetectChangesComp); + const comp = renderComponent(DetectChangesComp, {hostFeatures: [LifecycleHooksFeature]}); expect(getRenderedText(comp)).toEqual('1'); }); @@ -553,7 +553,7 @@ describe('change detection', () => { }); } - const comp = renderComponent(DetectChangesComp); + const comp = renderComponent(DetectChangesComp, {hostFeatures: [LifecycleHooksFeature]}); expect(getRenderedText(comp)).toEqual('1'); }); @@ -940,7 +940,7 @@ describe('change detection', () => { } it('should throw if bindings in current view have changed', () => { - const comp = renderComponent(NoChangesComp); + const comp = renderComponent(NoChangesComp, {hostFeatures: [LifecycleHooksFeature]}); expect(() => comp.cdr.checkNoChanges()).not.toThrow(); diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index e6fb60e50a..74f22b736b 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -9,7 +9,7 @@ import {defineComponent, defineDirective} from '../../src/render3/index'; import {NO_CHANGE, bind, container, containerRefreshEnd, containerRefreshStart, elementAttribute, elementClassNamed, elementEnd, elementProperty, elementStart, elementStyleNamed, embeddedViewEnd, embeddedViewStart, interpolation1, interpolation2, interpolation3, interpolation4, interpolation5, interpolation6, interpolation7, interpolation8, interpolationV, load, projection, projectionDef, text, textBinding} from '../../src/render3/instructions'; -import {containerEl, renderToHtml} from './render_util'; +import {ComponentFixture, containerEl, renderToHtml} from './render_util'; describe('render3 integration test', () => { @@ -269,6 +269,21 @@ describe('render3 integration test', () => { expect(renderToHtml(Template, {})).toEqual('two'); }); + it('should support root component with host attribute', () => { + class HostAttributeComp { + static ngComponentDef = defineComponent({ + type: HostAttributeComp, + tag: 'host-attr-comp', + factory: () => new HostAttributeComp(), + template: (ctx: HostAttributeComp, cm: boolean) => {}, + attributes: ['role', 'button'] + }); + } + + const fixture = new ComponentFixture(HostAttributeComp); + expect(fixture.hostElement.getAttribute('role')).toEqual('button'); + }); + it('should support component with bindings in template', () => { /**

{{ name }}

*/ class MyComp {