-4

I am working on project that requires to access the metadata of the field that is bound to the component (I can access that metadata information if I know what the developer bound to my component), therefore I need to be able to read the expression that other developer pass to my component in string value.

<mycomponent [(value)]="user.Name" />

Template:
<input type="text" [(ngModel)]="value"/>

In TS file:
@Input() value:string;

ngOnInit() {
   var v = this.value; //=> David

   //need to access the string value of the bound expression
   var expr = this.?; //=> user.Name
}
sam360
  • 1,121
  • 3
  • 18
  • 37
  • [OnChange](https://angular.io/api/core/OnChanges) – Nicholas K Aug 25 '21 at 16:59
  • Does OnChanges give me access to developer's binding expression? note that I am not interested to monitor the changes in the value. For example I am not trying to track the the value was "David" and now is "John". I am interested to access the string "user.Name" binding expression that the developer passed to my component. – sam360 Aug 25 '21 at 17:20
  • No it doesn't. Just curios, why do you need that binding info? – Nicholas K Aug 26 '21 at 06:41
  • Using the binding info, I can retrieve the metadata of the field in my code, the metadata describes the input field label, validations and give more details to specialize my component based on metadata information. The two binding provide me with just the value of field bound. – sam360 Aug 26 '21 at 14:01
  • @NicholasK any thoughts? – sam360 Aug 27 '21 at 19:46
  • @sam360 you need to pass the whole user object to your component, then you will have both, the meta and the value ! – Zulqarnain Jalil Sep 01 '21 at 12:39
  • @ZulqarnainJalil that won't work, even if I pass the whole object, I won't be able to figure out what type of data it is, in our system all data models are Entity/Object driven & for each object/entity we have metadata that describes it and give me more info. I need to be able to know what the developer expression is, like: "user.name". The key "user" leads me to the entity name that I need to get the metadata for & "name" leads to the field bound which I need to also read the field metadata. Not to mention that the expression can be like: "contact.Account.Name" due to table relationships. – sam360 Sep 02 '21 at 14:04

2 Answers2

0

When we has a component with two-binding we has a pair of Inputs/Outputs. One is called "property" and the another one "propertyChange"

  @Input()  value!: any;
  @Output() valueChange = new EventEmitter<any>();

Just we can use the [(ngModel)] but not in "bannana sintax" else separate the [ngModel]and the (ngModelChange)

<input type="text" [ngModel]="value" 
                   (ngModelChange)="valueChange.next($event)"/>

See that I write directly in ngModelChange the valueChange.next($event), if we need make a more elaborate function create it

<input type="text" [ngModel]="value" 
                   (ngModelChange)="makeSomething($event)"/>

makeSomething(data:any){
    ..make something more..
    this.valueChange.next(data)
}
Eliseo
  • 50,109
  • 4
  • 29
  • 67
  • note that I am not interested to monitor the changes in the value. For example I am not trying to track the the value was "David" and now is "John". I am interested to access the string "user.Name" binding expression that the developer passed to my component – sam360 Aug 25 '21 at 20:22
0

Going from less intrusive to more intrusive:

  • Angular Lifecycle Hooks:

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.

  • The @Input Decorator:

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:

  1. 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

  1. 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.

IDK4real
  • 757
  • 6
  • 14
  • Would you be able to provide a sample for me to see how you would approach this? – sam360 Sep 02 '21 at 13:56
  • I updated my answer but the feeling is still the same, it can be done, but requires lots of work and the problem is probably being approached wrong. Without further details and what we want to achieve there is little more I can do. – IDK4real Sep 05 '21 at 11:18