88import { resolveForwardRef } from '../../di' ;
99import { RuntimeError , RuntimeErrorCode } from '../../errors' ;
1010import { assertEqual } from '../../util/assert' ;
11- import { EMPTY_OBJ } from '../../util/empty' ;
1211import { getComponentDef , getDirectiveDef } from '../def_getters' ;
1312import { isComponentDef } from '../interfaces/type_checks' ;
1413import type {
@@ -94,7 +93,7 @@ function resolveHostDirectives(matches: DirectiveDef<unknown>[]): HostDirectiveR
9493 hostDirectiveRanges ??= new Map ( ) ;
9594
9695 // TODO(pk): probably could return matches instead of taking in an array to fill in?
97- findHostDirectiveDefs ( def , allDirectiveDefs , hostDirectiveDefs ) ;
96+ findHostDirectiveDefs ( def , allDirectiveDefs , hostDirectiveDefs , matches ) ;
9897
9998 // Note that these indexes are within the offset by `directiveStart`. We can't do the
10099 // offsetting here, because `directiveStart` hasn't been initialized on the TNode yet.
@@ -113,23 +112,37 @@ function resolveHostDirectives(matches: DirectiveDef<unknown>[]): HostDirectiveR
113112 allDirectiveDefs . push ( matches [ i ] ) ;
114113 }
115114
115+ // We need to patch the `declaredInputs` so that `ngOnChanges` can map the properties correctly.
116+ // Note that we do this at the end so that all host directive inputs have been merged.
117+ if ( hostDirectiveDefs !== null ) {
118+ hostDirectiveDefs . forEach ( ( def , hostDirectiveDef ) => {
119+ patchDeclaredInputs ( hostDirectiveDef . declaredInputs , def . inputs ) ;
120+ } ) ;
121+ }
122+
116123 return [ allDirectiveDefs , hostDirectiveDefs , hostDirectiveRanges ] ;
117124}
118125
119126function findHostDirectiveDefs (
120127 currentDef : DirectiveDef < unknown > ,
121128 matchedDefs : DirectiveDef < unknown > [ ] ,
122129 hostDirectiveDefs : HostDirectiveDefs ,
130+ templateMatches : readonly DirectiveDef < unknown > [ ] ,
123131) : void {
124132 if ( currentDef . hostDirectives !== null ) {
125133 for ( const configOrFn of currentDef . hostDirectives ) {
126134 if ( typeof configOrFn === 'function' ) {
127135 const resolved = configOrFn ( ) ;
128136 for ( const config of resolved ) {
129- trackHostDirectiveDef ( createHostDirectiveDef ( config ) , matchedDefs , hostDirectiveDefs ) ;
137+ trackHostDirectiveDef (
138+ createHostDirectiveDef ( config ) ,
139+ matchedDefs ,
140+ hostDirectiveDefs ,
141+ templateMatches ,
142+ ) ;
130143 }
131144 } else {
132- trackHostDirectiveDef ( configOrFn , matchedDefs , hostDirectiveDefs ) ;
145+ trackHostDirectiveDef ( configOrFn , matchedDefs , hostDirectiveDefs , templateMatches ) ;
133146 }
134147 }
135148 }
@@ -138,29 +151,56 @@ function findHostDirectiveDefs(
138151/** Tracks a single host directive during directive matching. */
139152function trackHostDirectiveDef (
140153 def : HostDirectiveDef ,
141- matchedDefs : DirectiveDef < unknown > [ ] ,
154+ finalMatches : DirectiveDef < unknown > [ ] ,
142155 hostDirectiveDefs : HostDirectiveDefs ,
156+ templateMatches : readonly DirectiveDef < unknown > [ ] ,
143157) {
144158 const hostDirectiveDef = getDirectiveDef ( def . directive ) ! ;
145159
146160 if ( typeof ngDevMode === 'undefined' || ngDevMode ) {
147161 validateHostDirective ( def , hostDirectiveDef ) ;
148162 }
149163
150- // We need to patch the `declaredInputs` so that
151- // `ngOnChanges` can map the properties correctly.
152- patchDeclaredInputs ( hostDirectiveDef . declaredInputs , def . inputs ) ;
153-
154164 // Host directives execute before the host so that its host bindings can be overwritten.
155- findHostDirectiveDefs ( hostDirectiveDef , matchedDefs , hostDirectiveDefs ) ;
156- hostDirectiveDefs . set ( hostDirectiveDef , def ) ;
157- matchedDefs . push ( hostDirectiveDef ) ;
165+ findHostDirectiveDefs ( hostDirectiveDef , finalMatches , hostDirectiveDefs , templateMatches ) ;
166+
167+ if ( hostDirectiveDefs . has ( hostDirectiveDef ) ) {
168+ const existing = hostDirectiveDefs . get ( hostDirectiveDef ) ! ;
169+ mergeBindingMaps ( existing , def . inputs , 'input' ) ;
170+ mergeBindingMaps ( existing , def . outputs , 'output' ) ;
171+ } else if ( ! templateMatches . includes ( hostDirectiveDef ) ) {
172+ hostDirectiveDefs . set ( hostDirectiveDef , def ) ;
173+ finalMatches . push ( hostDirectiveDef ) ;
174+ }
175+ }
176+
177+ function mergeBindingMaps (
178+ existingDef : HostDirectiveDef ,
179+ newMap : HostDirectiveBindingMap ,
180+ kind : 'input' | 'output' ,
181+ ) {
182+ // Note: we don't do something like `existingDef[kind]` to avoid property renaming issues.
183+ const targetMap = kind === 'input' ? existingDef . inputs : existingDef . outputs ;
184+
185+ Object . keys ( newMap ) . forEach ( ( publicName ) => {
186+ const alias = newMap [ publicName ] ;
187+
188+ if ( ! targetMap . hasOwnProperty ( publicName ) || targetMap [ publicName ] === alias ) {
189+ targetMap [ publicName ] = alias ;
190+ } else if ( typeof ngDevMode === 'undefined' || ngDevMode ) {
191+ const message =
192+ `${ kind === 'input' ? 'Input' : 'Output' } "${ publicName } " from ${ existingDef . directive . name } ` +
193+ `is exposed under the following conflicting names: "${ targetMap [ publicName ] } " and "${ alias } ". ` +
194+ `An ${ kind } can only be exposed under a single name.` ;
195+ throw new RuntimeError ( RuntimeErrorCode . HOST_DIRECTIVE_CONFLICTING_ALIAS , message ) ;
196+ }
197+ } ) ;
158198}
159199
160200/** Creates a `HostDirectiveDef` from a used-defined host directive configuration. */
161201function createHostDirectiveDef ( config : HostDirectiveConfig ) : HostDirectiveDef {
162202 return typeof config === 'function'
163- ? { directive : resolveForwardRef ( config ) , inputs : EMPTY_OBJ , outputs : EMPTY_OBJ }
203+ ? { directive : resolveForwardRef ( config ) , inputs : { } , outputs : { } }
164204 : {
165205 directive : resolveForwardRef ( config . directive ) ,
166206 inputs : bindingArrayToMap ( config . inputs ) ,
@@ -173,14 +213,12 @@ function createHostDirectiveDef(config: HostDirectiveConfig): HostDirectiveDef {
173213 * a map in the form of `{publicName: 'alias', otherPublicName: 'otherAlias'}`.
174214 */
175215function bindingArrayToMap ( bindings : string [ ] | undefined ) : HostDirectiveBindingMap {
176- if ( bindings === undefined || bindings . length === 0 ) {
177- return EMPTY_OBJ ;
178- }
179-
180216 const result : HostDirectiveBindingMap = { } ;
181217
182- for ( let i = 0 ; i < bindings . length ; i += 2 ) {
183- result [ bindings [ i ] ] = bindings [ i + 1 ] ;
218+ if ( bindings !== undefined && bindings . length > 0 ) {
219+ for ( let i = 0 ; i < bindings . length ; i += 2 ) {
220+ result [ bindings [ i ] ] = bindings [ i + 1 ] ;
221+ }
184222 }
185223
186224 return result ;
0 commit comments