diff --git a/goldens/size-tracking/integration-payloads.json b/goldens/size-tracking/integration-payloads.json index 1189e882466217..e7ba2b3bd1558e 100644 --- a/goldens/size-tracking/integration-payloads.json +++ b/goldens/size-tracking/integration-payloads.json @@ -41,7 +41,7 @@ "forms": { "uncompressed": { "runtime": 1060, - "main": 156626, + "main": 157136, "polyfills": 33915 } }, diff --git a/packages/core/src/render3/definition.ts b/packages/core/src/render3/definition.ts index 2359f5ad8ee7a5..7d216319108e34 100644 --- a/packages/core/src/render3/definition.ts +++ b/packages/core/src/render3/definition.ts @@ -326,7 +326,8 @@ export function ɵɵdefineComponent(componentDefinition: { setInput: null, schemas: componentDefinition.schemas || null, tView: null, - applyHostDirectives: null, + findHostDirectiveDefs: null, + hostDirectives: null, }; const dependencies = componentDefinition.dependencies; const feature = componentDefinition.features; diff --git a/packages/core/src/render3/features/host_directives_feature.ts b/packages/core/src/render3/features/host_directives_feature.ts index f563e7f37dbad3..ccb360f01b5d1f 100644 --- a/packages/core/src/render3/features/host_directives_feature.ts +++ b/packages/core/src/render3/features/host_directives_feature.ts @@ -5,14 +5,16 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.io/license */ +import {resolveForwardRef} from '../../di'; import {Type} from '../../interface/type'; import {EMPTY_OBJ} from '../../util/empty'; +import {getDirectiveDef} from '../definition'; import {DirectiveDef} from '../interfaces/definition'; import {TContainerNode, TElementContainerNode, TElementNode} from '../interfaces/node'; import {LView, TView} from '../interfaces/view'; /** Values that can be used to define a host directive through the `HostDirectivesFeature`. */ -type HostDirectiveDefiniton = Type|{ +type HostDirectiveConfig = Type|{ directive: Type; inputs?: string[]; outputs?: string[]; @@ -38,38 +40,54 @@ type HostDirectiveDefiniton = Type|{ * * @codeGenApi */ -export function ɵɵHostDirectivesFeature(rawHostDirectives: HostDirectiveDefiniton[]| - (() => HostDirectiveDefiniton[])) { - const unwrappedHostDirectives = - Array.isArray(rawHostDirectives) ? rawHostDirectives : rawHostDirectives(); - const hostDirectives = unwrappedHostDirectives.map( - dir => typeof dir === 'function' ? {directive: dir, inputs: EMPTY_OBJ, outputs: EMPTY_OBJ} : { - directive: dir.directive, - inputs: bindingArrayToMap(dir.inputs), - outputs: bindingArrayToMap(dir.outputs) - }); - +export function ɵɵHostDirectivesFeature(rawHostDirectives: HostDirectiveConfig[]| + (() => HostDirectiveConfig[])) { return (definition: DirectiveDef) => { - // TODO(crisbeto): implement host directive matching logic. - definition.applyHostDirectives = - (tView: TView, viewData: LView, tNode: TElementNode|TContainerNode|TElementContainerNode, - matches: any[]) => {}; + definition.findHostDirectiveDefs = findHostDirectiveDefs; + definition.hostDirectives = + (Array.isArray(rawHostDirectives) ? rawHostDirectives : rawHostDirectives()).map(dir => { + return typeof dir === 'function' ? + {directive: resolveForwardRef(dir), inputs: EMPTY_OBJ, outputs: EMPTY_OBJ} : + { + directive: resolveForwardRef(dir.directive), + inputs: bindingArrayToMap(dir.inputs), + outputs: bindingArrayToMap(dir.outputs) + }; + }); }; } +function findHostDirectiveDefs( + matches: DirectiveDef[], def: DirectiveDef, tView: TView, lView: LView, + tNode: TElementNode|TContainerNode|TElementContainerNode): void { + if (def.hostDirectives !== null) { + for (const hostDirectiveConfig of def.hostDirectives) { + const hostDirectiveDef = getDirectiveDef(hostDirectiveConfig.directive)!; + + // TODO(crisbeto): assert that the def exists. + + // Host directives execute before the host so that its host bindings can be overwritten. + findHostDirectiveDefs(matches, hostDirectiveDef, tView, lView, tNode); + } + } + + // Push the def itself at the end since it needs to execute after the host directives. + matches.push(def); +} + /** * Converts an array in the form of `['publicName', 'alias', 'otherPublicName', 'otherAlias']` into * a map in the form of `{publicName: 'alias', otherPublicName: 'otherAlias'}`. */ function bindingArrayToMap(bindings: string[]|undefined) { - if (!bindings || bindings.length === 0) { + if (bindings === undefined || bindings.length === 0) { return EMPTY_OBJ; } const result: {[publicName: string]: string} = {}; - for (let i = 1; i < bindings.length; i += 2) { - result[bindings[i - 1]] = bindings[i]; + for (let i = 0; i < bindings.length; i += 2) { + result[bindings[i]] = bindings[i + 1]; } return result; diff --git a/packages/core/src/render3/instructions/shared.ts b/packages/core/src/render3/instructions/shared.ts index 925b4856c11404..9e6f3f9d19c5dc 100644 --- a/packages/core/src/render3/instructions/shared.ts +++ b/packages/core/src/render3/instructions/shared.ts @@ -1062,7 +1062,10 @@ export function resolveDirectives( let hasDirectives = false; if (getBindingsEnabled()) { - const directiveDefs: DirectiveDef[]|null = findDirectiveDefMatches(tView, lView, tNode); + const directiveDefsMatchedBySelectors = findDirectiveDefMatches(tView, lView, tNode); + const directiveDefs = directiveDefsMatchedBySelectors ? + findHostDirectiveDefs(directiveDefsMatchedBySelectors, tView, lView, tNode) : + null; const exportsMap: ({[key: string]: number}|null) = localRefs === null ? null : {'': -1}; if (directiveDefs !== null) { @@ -1288,8 +1291,6 @@ function findDirectiveDefMatches( } else { matches.push(def); } - - def.applyHostDirectives?.(tView, viewData, tNode, matches); } } } @@ -1308,6 +1309,29 @@ export function markAsComponentHost(tView: TView, hostTNode: TNode): void { .push(hostTNode.index); } +/** + * Given an array of directives that were matched by their selectors, this function + * produces a new array that also includes any host directives that have to be applied. + * @param selectorMatches Directives matched in a template based on their selectors. + * @param tView Current TView. + * @param lView Current LView. + * @param tNode Current TNode that is being matched. + */ +function findHostDirectiveDefs( + selectorMatches: DirectiveDef[], tView: TView, lView: LView, + tNode: TElementNode|TContainerNode|TElementContainerNode): DirectiveDef[] { + const matches: DirectiveDef[] = []; + + for (const def of selectorMatches) { + if (def.findHostDirectiveDefs === null) { + matches.push(def); + } else { + def.findHostDirectiveDefs(matches, def, tView, lView, tNode); + } + } + + return matches; +} /** Caches local names and their matching directive indices for query and template lookups. */ function cacheMatchingLocalNames( diff --git a/packages/core/src/render3/interfaces/definition.ts b/packages/core/src/render3/interfaces/definition.ts index bb439bcd07a8f9..226f345a918eb4 100644 --- a/packages/core/src/render3/interfaces/definition.ts +++ b/packages/core/src/render3/interfaces/definition.ts @@ -207,12 +207,15 @@ export interface DirectiveDef { readonly features: DirectiveDefFeature[]|null; /** - * Function that will apply the host directives to the list of matches during directive matching. + * Function that will add the host directives to the list of matches during directive matching. * Patched onto the definition by the `HostDirectivesFeature`. */ - applyHostDirectives: - ((tView: TView, viewData: LView, tNode: TElementNode|TContainerNode|TElementContainerNode, - matches: any[]) => void)|null; + findHostDirectiveDefs: + ((matches: DirectiveDef[], def: DirectiveDef, tView: TView, lView: LView, + tNode: TElementNode|TContainerNode|TElementContainerNode) => void)|null; + + /** Additional directives to be applied whenever the directive has been matched. */ + hostDirectives: HostDirectiveDef[]|null; setInput: (( @@ -403,6 +406,18 @@ export interface DirectiveDefFeature { ngInherit?: true; } +/** Runtime information used to configure a host directive. */ +export interface HostDirectiveDef { + /** Class representing the host directive. */ + directive: Type; + + /** Directive inputs that have been exposed. */ + inputs: {[publicName: string]: string}; + + /** Directive outputs that have been exposed. */ + outputs: {[publicName: string]: string}; +} + export interface ComponentDefFeature { (componentDef: ComponentDef): void; /** diff --git a/packages/core/src/render3/jit/directive.ts b/packages/core/src/render3/jit/directive.ts index 0daa179fe3b884..f2b2dd623f4df9 100644 --- a/packages/core/src/render3/jit/directive.ts +++ b/packages/core/src/render3/jit/directive.ts @@ -416,7 +416,13 @@ export function directiveMetadata(type: Type, metadata: Directive): R3Direc providers: metadata.providers || null, viewQueries: extractQueriesMetadata(type, propMetadata, isViewQuery), isStandalone: !!metadata.standalone, - hostDirectives: null, + hostDirectives: + // TODO(crisbeto): remove the `as any` usage here and down in the `map` call once + // host directives are exposed in the public API. + (metadata as any) + .hostDirectives?.map( + (directive: any) => typeof directive === 'function' ? {directive} : directive) || + null }; } diff --git a/packages/core/test/acceptance/host_directives_spec.ts b/packages/core/test/acceptance/host_directives_spec.ts new file mode 100644 index 00000000000000..a8bca6071a5b7b --- /dev/null +++ b/packages/core/test/acceptance/host_directives_spec.ts @@ -0,0 +1,646 @@ +/*! + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {AfterViewChecked, AfterViewInit, Component, Directive, forwardRef, inject, Inject, InjectionToken, OnInit, ViewChild} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; + +/** + * Temporary `any` used for metadata until `hostDirectives` is enabled publicly. + * TODO(crisbeto): remove this once host directives are enabled in the public API. + */ +type HostDirectiveAny = any; + +describe('host directives', () => { + it('should apply a basic host directive', () => { + const logs: string[] = []; + + @Directive({ + standalone: true, + host: {'host-dir-attr': '', 'class': 'host-dir', 'style': 'height: 50px'} + }) + class HostDir { + constructor() { + logs.push('HostDir'); + } + } + + @Directive({ + selector: '[dir]', + host: {'host-attr': '', 'class': 'dir', 'style': 'width: 50px'}, + hostDirectives: [HostDir] + } as HostDirectiveAny) + class Dir { + constructor() { + logs.push('Dir'); + } + } + + @Component({template: '
'}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Dir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(logs).toEqual(['HostDir', 'Dir']); + expect(fixture.nativeElement.innerHTML) + .toBe( + '
'); + }); + + it('should apply a host directive referenced through a forwardRef', () => { + const logs: string[] = []; + + @Directive({ + selector: '[dir]', + hostDirectives: [forwardRef(() => HostDir), {directive: forwardRef(() => OtherHostDir)}] + } as HostDirectiveAny) + class Dir { + constructor() { + logs.push('Dir'); + } + } + + @Directive({standalone: true}) + class HostDir { + constructor() { + logs.push('HostDir'); + } + } + + @Directive({standalone: true}) + class OtherHostDir { + constructor() { + logs.push('OtherHostDir'); + } + } + + @Component({template: '
'}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Dir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(logs).toEqual(['HostDir', 'OtherHostDir', 'Dir']); + }); + + it('should apply a chain of host directives', () => { + const logs: string[] = []; + const token = new InjectionToken('message'); + let diTokenValue: string; + + @Directive({ + host: { + 'class': 'leaf', + 'id': 'leaf-id', + }, + providers: [{provide: token, useValue: 'leaf value'}], + standalone: true + }) + class Chain1_3 { + constructor(@Inject(token) tokenValue: string) { + diTokenValue = tokenValue; + logs.push('Chain1 - level 3'); + } + } + + @Directive({ + standalone: true, + hostDirectives: [Chain1_3], + } as HostDirectiveAny) + class Chain1_2 { + constructor() { + logs.push('Chain1 - level 2'); + } + } + + @Directive({ + standalone: true, + hostDirectives: [Chain1_2], + } as HostDirectiveAny) + class Chain1 { + constructor() { + logs.push('Chain1 - level 1'); + } + } + + @Directive({ + standalone: true, + host: { + 'class': 'middle', + 'id': 'middle-id', + }, + providers: [{provide: token, useValue: 'middle value'}], + }) + class Chain2_2 { + constructor() { + logs.push('Chain2 - level 2'); + } + } + + @Directive({ + standalone: true, + hostDirectives: [Chain2_2], + } as HostDirectiveAny) + class Chain2 { + constructor() { + logs.push('Chain2 - level 1'); + } + } + + @Directive({standalone: true}) + class Chain3_2 { + constructor() { + logs.push('Chain3 - level 2'); + } + } + + @Directive({standalone: true, hostDirectives: [Chain3_2]} as HostDirectiveAny) + class Chain3 { + constructor() { + logs.push('Chain3 - level 1'); + } + } + + @Component({ + selector: 'my-comp', + host: { + 'class': 'host', + 'id': 'host-id', + }, + template: '', + hostDirectives: [Chain1, Chain2, Chain3], + providers: [{provide: token, useValue: 'host value'}], + } as HostDirectiveAny) + class MyComp { + constructor() { + logs.push('host'); + } + } + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, MyComp]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(diTokenValue!).toBe('host value'); + expect(fixture.nativeElement.innerHTML) + .toBe(''); + expect(logs).toEqual([ + 'Chain1 - level 3', + 'Chain1 - level 2', + 'Chain1 - level 1', + 'Chain2 - level 2', + 'Chain2 - level 1', + 'Chain3 - level 2', + 'Chain3 - level 1', + 'host', + ]); + }); + + it('should be able to query for the host directives', () => { + let hostInstance!: Host; + let firstHostDirInstance!: FirstHostDir; + let secondHostDirInstance!: SecondHostDir; + + @Directive({standalone: true}) + class SecondHostDir { + constructor() { + secondHostDirInstance = this; + } + } + + @Directive({standalone: true, hostDirectives: [SecondHostDir]} as HostDirectiveAny) + class FirstHostDir { + constructor() { + firstHostDirInstance = this; + } + } + + @Directive({selector: '[dir]', hostDirectives: [FirstHostDir]} as HostDirectiveAny) + class Host { + constructor() { + hostInstance = this; + } + } + + @Component({template: '
'}) + class App { + @ViewChild(FirstHostDir) firstHost!: FirstHostDir; + @ViewChild(SecondHostDir) secondHost!: SecondHostDir; + } + + TestBed.configureTestingModule({declarations: [App, Host]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(hostInstance instanceof Host).toBe(true); + expect(firstHostDirInstance instanceof FirstHostDir).toBe(true); + expect(secondHostDirInstance instanceof SecondHostDir).toBe(true); + + expect(fixture.componentInstance.firstHost).toBe(firstHostDirInstance); + expect(fixture.componentInstance.secondHost).toBe(secondHostDirInstance); + }); + + it('should be able to reference exported host directives', () => { + @Directive({standalone: true, exportAs: 'secondHost'}) + class SecondHostDir { + name = 'SecondHost'; + } + + @Directive( + {standalone: true, hostDirectives: [SecondHostDir], exportAs: 'firstHost'} as + HostDirectiveAny) + class FirstHostDir { + name = 'FirstHost'; + } + + @Directive({selector: '[dir]', hostDirectives: [FirstHostDir]} as HostDirectiveAny) + class Host { + } + + @Component({ + template: ` +
{{firstHost.name}} | {{secondHost.name}}
+ ` + }) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Host]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.textContent).toContain('FirstHost | SecondHost'); + }); + + it('should invoke lifecycle hooks from the host directives', () => { + const logs: string[] = []; + + @Directive({standalone: true}) + class HostDir implements OnInit, AfterViewInit, AfterViewChecked { + ngOnInit() { + logs.push('HostDir - ngOnInit'); + } + + ngAfterViewInit() { + logs.push('HostDir - ngAfterViewInit'); + } + + ngAfterViewChecked() { + logs.push('HostDir - ngAfterViewChecked'); + } + } + + @Directive({standalone: true}) + class OtherHostDir implements OnInit, AfterViewInit, AfterViewChecked { + ngOnInit() { + logs.push('OtherHostDir - ngOnInit'); + } + + ngAfterViewInit() { + logs.push('OtherHostDir - ngAfterViewInit'); + } + + ngAfterViewChecked() { + logs.push('OtherHostDir - ngAfterViewChecked'); + } + } + + @Directive({selector: '[dir]', hostDirectives: [HostDir, OtherHostDir]} as HostDirectiveAny) + class Dir implements OnInit, AfterViewInit, AfterViewChecked { + ngOnInit() { + logs.push('Dir - ngOnInit'); + } + + ngAfterViewInit() { + logs.push('Dir - ngAfterViewInit'); + } + + ngAfterViewChecked() { + logs.push('Dir - ngAfterViewChecked'); + } + } + + @Component({template: '
'}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Dir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(logs).toEqual([ + 'HostDir - ngOnInit', + 'OtherHostDir - ngOnInit', + 'Dir - ngOnInit', + 'HostDir - ngAfterViewInit', + 'HostDir - ngAfterViewChecked', + 'OtherHostDir - ngAfterViewInit', + 'OtherHostDir - ngAfterViewChecked', + 'Dir - ngAfterViewInit', + 'Dir - ngAfterViewChecked', + ]); + }); + + describe('host bindings', () => { + it('should apply the host bindings from all host directives', () => { + const clicks: string[] = []; + + @Directive({standalone: true, host: {'host-dir-attr': 'true', '(click)': 'handleClick()'}}) + class HostDir { + handleClick() { + clicks.push('HostDir'); + } + } + + @Directive( + {standalone: true, host: {'other-host-dir-attr': 'true', '(click)': 'handleClick()'}}) + class OtherHostDir { + handleClick() { + clicks.push('OtherHostDir'); + } + } + + @Directive({ + selector: '[dir]', + host: {'host-attr': 'true', '(click)': 'handleClick()'}, + hostDirectives: [HostDir, OtherHostDir] + } as HostDirectiveAny) + class Dir { + handleClick() { + clicks.push('Dir'); + } + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Dir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + const host = fixture.nativeElement.querySelector('[dir]'); + + expect(host.outerHTML) + .toBe( + ''); + + host.click(); + fixture.detectChanges(); + + expect(clicks).toEqual(['HostDir', 'OtherHostDir', 'Dir']); + }); + + it('should have the host bindings take precedence over the ones from the host directives', + () => { + @Directive({standalone: true, host: {'id': 'host-dir'}}) + class HostDir { + } + + @Directive({standalone: true, host: {'id': 'other-host-dir'}}) + class OtherHostDir { + } + + @Directive( + {selector: '[dir]', host: {'id': 'host'}, hostDirectives: [HostDir, OtherHostDir]} as + HostDirectiveAny) + class Dir { + } + + @Component({template: '
'}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Dir]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.nativeElement.querySelector('[dir]').getAttribute('id')).toBe('host'); + }); + }); + + describe('dependency injection', () => { + it('should allow the host directives to inject their host', () => { + let hostInstance!: Host; + let firstHostDirInstance!: FirstHostDir; + let secondHostDirInstance!: SecondHostDir; + + @Directive({standalone: true}) + class SecondHostDir { + host = inject(Host); + + constructor() { + secondHostDirInstance = this; + } + } + + @Directive({standalone: true, hostDirectives: [SecondHostDir]} as HostDirectiveAny) + class FirstHostDir { + host = inject(Host); + + constructor() { + firstHostDirInstance = this; + } + } + + @Directive({selector: '[dir]', hostDirectives: [FirstHostDir]} as HostDirectiveAny) + class Host { + constructor() { + hostInstance = this; + } + } + + @Component({template: '
'}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Host]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(hostInstance instanceof Host).toBe(true); + expect(firstHostDirInstance instanceof FirstHostDir).toBe(true); + expect(secondHostDirInstance instanceof SecondHostDir).toBe(true); + + expect(firstHostDirInstance.host).toBe(hostInstance); + expect(secondHostDirInstance.host).toBe(hostInstance); + }); + + it('should give precedence to the DI tokens from the host over the host directive tokens', + () => { + const token = new InjectionToken('token'); + let hostInstance!: Host; + let firstHostDirInstance!: FirstHostDir; + let secondHostDirInstance!: SecondHostDir; + + @Directive({standalone: true, providers: [{provide: token, useValue: 'SecondDir'}]}) + class SecondHostDir { + tokenValue = inject(token); + + constructor() { + secondHostDirInstance = this; + } + } + + @Directive({ + standalone: true, + hostDirectives: [SecondHostDir], + providers: [{provide: token, useValue: 'FirstDir'}] + } as HostDirectiveAny) + class FirstHostDir { + tokenValue = inject(token); + + constructor() { + firstHostDirInstance = this; + } + } + + @Directive({ + selector: '[dir]', + hostDirectives: [FirstHostDir], + providers: [{provide: token, useValue: 'HostDir'}] + } as HostDirectiveAny) + class Host { + tokenValue = inject(token); + + constructor() { + hostInstance = this; + } + } + + @Component({template: '
'}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Host]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(hostInstance instanceof Host).toBe(true); + expect(firstHostDirInstance instanceof FirstHostDir).toBe(true); + expect(secondHostDirInstance instanceof SecondHostDir).toBe(true); + + expect(hostInstance.tokenValue).toBe('HostDir'); + expect(firstHostDirInstance.tokenValue).toBe('HostDir'); + expect(secondHostDirInstance.tokenValue).toBe('HostDir'); + }); + + it('should allow the host to inject tokens from the host directives', () => { + const firstToken = new InjectionToken('firstToken'); + const secondToken = new InjectionToken('secondToken'); + + @Directive({standalone: true, providers: [{provide: secondToken, useValue: 'SecondDir'}]}) + class SecondHostDir { + } + + @Directive({ + standalone: true, + hostDirectives: [SecondHostDir], + providers: [{provide: firstToken, useValue: 'FirstDir'}] + } as HostDirectiveAny) + class FirstHostDir { + } + + @Directive({selector: '[dir]', hostDirectives: [FirstHostDir]} as HostDirectiveAny) + class Host { + firstTokenValue = inject(firstToken); + secondTokenValue = inject(secondToken); + } + + @Component({template: '
'}) + class App { + @ViewChild(Host) host!: Host; + } + + TestBed.configureTestingModule({declarations: [App, Host]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(fixture.componentInstance.host.firstTokenValue).toBe('FirstDir'); + expect(fixture.componentInstance.host.secondTokenValue).toBe('SecondDir'); + }); + + it('should not give precedence to tokens from host directives over ones in viewProviders', + () => { + const token = new InjectionToken('token'); + let tokenValue: string|undefined; + + @Directive({standalone: true, providers: [{provide: token, useValue: 'host-dir'}]}) + class HostDir { + } + + @Component({ + selector: 'host', + hostDirectives: [HostDir], + providers: [{provide: token, useValue: 'host'}], + template: '', + } as HostDirectiveAny) + class Host { + } + + @Directive({selector: '[child]'}) + class Child { + constructor() { + tokenValue = inject(token); + } + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Host, Child]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(tokenValue).toBe('host'); + }); + + it('should not be able to access viewProviders from the host in the host directives', () => { + const token = new InjectionToken('token'); + let tokenValue: string|null = null; + + @Directive({standalone: true}) + class HostDir { + constructor() { + tokenValue = inject(token, {optional: true}); + } + } + + @Component({ + selector: 'host', + hostDirectives: [HostDir], + viewProviders: [{provide: token, useValue: 'host'}], + template: '', + } as HostDirectiveAny) + class Host { + } + + @Component({template: ''}) + class App { + } + + TestBed.configureTestingModule({declarations: [App, Host]}); + const fixture = TestBed.createComponent(App); + fixture.detectChanges(); + + expect(tokenValue).toBe(null); + }); + }); +});