Going from less intrusive to more intrusive:
Can we use the lifecycle hooks of MyComponent
to obtain the expression that passed to the binding in:
<mycomponent [(value)]="user.Name" />
The answer is no. When the lifecycle hooks are called the binding user.Name
has already been resolved to the proper value. This applies to all lifecycle hooks.
Can we modify the @Input
decorator to give us the value of the original expression?
The answer is no.
Quoting Angular:
Decorator that marks a class field as an input property and supplies
configuration metadata. The input property is bound to a DOM property
in the template. During change detection, Angular automatically
updates the data property with the DOM property's value.
This means that we would need to go very deep into the Angular's change detection to be able to obtain the original expression. To do this we would need to practically copy the entire project since we are unable to override very specific functions from the outside.
The above two approaches have only thing in common, the action is being done on the MyComponent
side. Unfortunately we can't solve the issue with this restriction.
A solution therefor must require some type of action on the place where the binding occurs. In this case, it's in the HTML so a solution should attempt to take place there to avoid spilling into more files.
- A
Directive
to obtain the expression:
Can we create an directive to obtain the expression that is being binded?
The answer is no and yes.
Consider the following parent component:
@Component({
selector: 'app-parent',
template: `<app-child [qux]="foo.bar"></app-child>`
})
export class ParentComponent {
foo = { bar: 'qux' };
}
Consider the following child component:
@Component({
selector: 'app-child',
template: `<p> {{ baz }} ></p>`
})
export class ParentComponent {
@Input() baz = '';
}
We can create the following directive and obtain access to the HTML generated at run time with the bindings:
@Directive({ selector: '[appInputListener]' })
export class InputListenerDirective implements OnInit {
constructor(private _el: ElementRef) { }
ngOnInit(): void {
console.log(this._el.nativeElement.outerHTML);
}
This would print the followoing:
<app-child _ngcontent-nef-c19="" appinputlistener="" _nghost-nef-c16="" ng-reflect-baz="foo.bar"><p _ngcontent-nef-c16="">qux</p></app-child>
Notice the following in the expression: ng-reflect-baz="foo.bar"
.
So we can access the binding here. But there are two issues that make this solution not viable:
- Parsing HTML:
Parsing HTML is a tricky thing, it can work but is something that should be avoided if possible. You can read more on this topic here. I quote the following for simplification:
Regular expressions can only match regular languages but HTML is a context-free language and not a regular language
- Production:
While we managed to obtain the expressions in development, if we enable production mode, we lose them. You can read more on this topic here. I quote the following for simplification
ng-reflect-${name} attributes are added for debugging purposes and show the input bindings that a component/directive has declared in its class.
This is to say that while we reached a possible solution, it is not a valid one. Parsing HTML can become problematic and furthermore into wont work in production.
While the previously solutions did not work there is a possible solution using a @Directive
and a Singleton
The downside to this solution is that it requires explicit adoption and practice.
We can create the following service to store the information of the bindings:
@Injectable({ providedIn: 'root' })
export class BindingStoreService {
private _bindings: { [k: string] /* hostSelector */: {
selector: string,
input: string,
expression: string
}[] } = { };
getBindings(hostSelector: string, childSelector: string): {
selector: string, input: string, expression: string
} {
return this._bindings[hostSelector].filter(binding => {
bindings.selector === childSelector
})
};
addBindings(
hostSelector: string,
childSelector: string,
input: string,
expression: string
) : void {
this._bindings[hostSelector].push({
selector: childSelector, input: input, expression: expression
});
}
}
We can create the following directive to store the information in the service:
@Directive({ selector: '[appInputListener]' })
export class InputListenerDirective implements OnInit {
@Input() hostSelector: string;
@Input() childSelector: string;
@Input() inputs: string[] = [];
@Input() expressions: string[] = [];
constructor(private _bindingsStoreService: BindingsStoreService) { }
ngOnInit(): void {
if (this.inputs.length !== this.expressions.length) {
// Throw an error since we have a mismatch of inputs and expressions
};
for(let i = 0; i < this.inputs.length; i++) {
this._bindingsStoreService.addBindings(
hostSelector: this.hostSelector,
childSelector: this.childSelector,
input: this.inputs[i];
expression: this.expressions[i]
)
}
}
}
Applying this to my previous parent component HTML would lead to the following:
<app-child
appInputListener
[hostSelector]="app-parent"
[childSelector]="app-child"
[inputs]="['qux']"
[expressions]="['foo.bar']"
[qux]="foo.bar"
></app-child>
I personally dislike this approach since we are manually writing everything. We could improve the directive or the service to not require so many inputs but the reality is that we would always perform the majority of work manually.
This answer works. However it is very bulky and problematic to apply. It would only work has a starting point.
My thoughts on this issue.
What is wanted can be done, but should be avoided.
There is probably a better approach to whatever problem you are trying to solve. Going for the expressions that are passed in the bindings is probably not the best way to go.
If you don't need to know the values at runtime and only require them during a build, consider creating a script that is run beforehand and perform some scraping there.