5

I have a simple form with its input field associated to a directive:

<form id="scrollable-dropdown-menu">
  <input class="input-field" name="value" [someDirective] type="text"  [(ngModel)]="value" #query="ngModel" />
  <button (click)="onPost(query.value)" type="submit">Search</button>
</form>

The directive changes the input field through the use of a third party library. In my particular case, this is an autocomplete/typeahead Jquery plugin. This plugin provides options to the user and after selecting an option, it changes the value of the input field.

However, Angular doesn't update its property query.value and therefore, passes the old value to the onPost method.

The directive looks something like this:

@Directive({
    selector: '[someDirective]',
})
export class MyDirective  {
     constructor(private elRef: ElementRef, private renderer: Renderer) {
         // this changes the value of the field if the user selects an option
         $('.input-field').plugin(); 
     }

}

I was suggested to use UpdateValue, but I can't see how to use it inside a directive. That led me to look at @ViewChild, but it doesn't seem to work in directives, although I could be mistaken.

I also tried to force an update by injecting ChangeDetectorRef, but I didn't see any difference. This is what I did:

my.directive.ts

import {ChangeDetectorRef} from '@angular/core';
@Directive({
    selector: '[someDirective]',
})
export class MyDirective  {
    constructor(private elRef: ElementRef, private renderer: Renderer, private detectorRef: ChangeDetectorRef) {
       $('.input-field').plugin(); 

       $('.input-field').my-plugin(':selected', ()=>{ 
          // do something... 
          this.detectorRef.detectChanges();
       })
    }



}

AfterSelection is triggered when the user selects an option from the autocomplete plugin. For the plugin, it looks a bit different because it binds to some event, but I think this illustrates the idea.

I would appreciate any suggestions.

UPDATE:

This is a plnkr to show the main issue using the typeahead.js library. If you write a letter in the input field, it will show you some options. Select one of them and the value of the input field will change accordingly. However, when you submit the form (clicking on the input field or pressing enter), an alert will show that the value that was passed to the onPost function was the old value of the field and not the autocompleted value you selected.

r_31415
  • 8,752
  • 17
  • 74
  • 121
  • How do you use `ChangeDetectRef`? show it here. – micronyks Jul 10 '16 at 07:02
  • I will update my question. – r_31415 Jul 10 '16 at 07:05
  • In someDirective where do you put `$('.input-field').plugin();` after updating your question? – micronyks Jul 10 '16 at 07:19
  • Oh, sorry. Now it looks like I have two different directives. No, both snippets are the same directive. The part that includes `detectChanges` is after the $('.input-field').plugin() because the plugin needs to be instantiated and then it can detect events (e.g. AfterSelection in my example). I will correct it to avoid confusion. – r_31415 Jul 10 '16 at 07:23
  • I think now both snippets reflect better what I have. Notice that both calls (plugin and AfterSelection are inside the constructor). I'm not sure if that's a problem. – r_31415 Jul 10 '16 at 07:32
  • Still problem is there. how can you have `AfterSelection()` function within constructor? – micronyks Jul 10 '16 at 07:32
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/116912/discussion-between-micronyks-and-robert-smith). – micronyks Jul 10 '16 at 07:33
  • Because it is actually `$('.input-field').my-plugin(':selected', function(){ // do something... })` – r_31415 Jul 10 '16 at 07:34
  • I believe when values are modified outside Angular, you need to tell zone: will check it out... – Manube Jul 10 '16 at 07:44
  • I read something like that but I don't know how to do it, particularly after the recent releases of Angular. If you have an example with the correct import statements, I would appreciate the help. – r_31415 Jul 10 '16 at 07:46
  • I will check in a few hours. It is extremely late here. Thanks for the help. – r_31415 Jul 10 '16 at 07:48
  • see this: http://stackoverflow.com/questions/34381680/angular-2-how-to-get-angular-to-detect-changes-made-outside-angular good night... – Manube Jul 10 '16 at 07:48

5 Answers5

1

Provisional solution: Demo. Passing the form to the custom directive and using updateValue to manually change the value:

my.component.ts

<form *ngIf="active" #frm="ngForm">
    <input name="value" [myDirective]="frm" type="text" [(ngModel)]="value" #query="ngModel" />
    <button (click)="onPost(query.value)" type="submit">Search</button>
</form>

my.directive.ts

@Directive({
    selector: '[myDirective]',
})
export class MyDirective  {

    @Input('myDirectivev') query;
... 
$(this.elRef.nativeElement).my-plugin(':selected', (ev, suggestion) => {
        this.query.controls['value'].updateValue(suggestion);
      });

This works, but let me know if there is a standard approach to solve this kind of issue.

r_31415
  • 8,752
  • 17
  • 74
  • 121
1

I was told about another solution on Gitter. Unfortunately, I can't remember who told me about it and Gitter's search functionality doesn't help me. Since this person hasn't posted his solution, I will share it for the benefit of people who might be interested.

The idea is basically to pass the whole field as an argument to the onPost(input) method and access the value property of the input field (input.value).

@Component({
  selector: 'my-app',
  template: `
  <form id="scrollable-dropdown-menu" class="search-form">
      <input name="value" class="form-control typeahead" typeahead type="text" data-provide="typeahead" [(ngModel)]="thing" #query />
      <button (click)="onPost(query)" class="btn btn-default search-button" type="submit">Search</button>
  </form>
  `,
  directives: [TypeaheadDirective],
  providers: []
})
export class AppComponent { 
  onPost(input) {
    alert(`Your current value is ${input.value}`);
  }
}

Notice the [(ngModel)]="thing" and #query in the input field. I'm not entirely sure why this updates its value correctly and the native attempt doesn't, but this is far simpler than other alternatives. Here you can find a demo.

r_31415
  • 8,752
  • 17
  • 74
  • 121
1

Instead of

this.detectorRef.detectChanges();

use

$('.input-field').trigger('input');

ControlValueAccessor used by ngModel listens to input events for normal input fields.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
1

Günter Zöchbauer is right. You should dispatch 'input' event to update angular's model. But JQuery trigger didn't help. Only native event fixed this

@Directive({
    selector: '[myDirective]',
})
export class MyDirective  {

...

  $(this.elRef.nativeElement).my-plugin(':selected', (ev, suggestion) => {
    const inputEvent: any = document.createEvent('CustomEvent');
    inputEvent.initEvent('input', true, true);
    el.dispatchEvent(inputEvent);
  });
}
Vincente
  • 341
  • 3
  • 13
0

I think Zone is not aware of third-party variable changes, hence third-party variable changes are not reflected in Angular


You could try to place your jQuery code inside an angular zone, by implementing something like this:

  • import Zone: import {NgZone} from 'angular2/core' or import {NgZone} from '@angular/core' , depending on the Angular 2 version you are using;
  • add zone: NgZone to the constructor arguments of MyDirective;
  • replace $('.input-field').plugin(); by

    zone.run( () => {$('.input-field').plugin(); });
    

Here is a post about Change detection and Zone

Hope this will help you...

Manube
  • 5,110
  • 3
  • 35
  • 59
  • Thanks,. Probably this is on the right. Unfortunately, I still can't make it work. I wrapped the instantiation of the plugin (including the part that detects user selection) with `zone.run`, but that didn't help. I tried to use `zone.run` only for `$('.input-field').my-plugin(':selected'...`, but it doesn't change anything. I will try to use a Plunker demo to show the difference. – r_31415 Jul 10 '16 at 15:23
  • sorry to hear it did not solve it yet. Yes a plunker would be ideal to work on the isolated issue – Manube Jul 10 '16 at 15:51
  • I updated the question with a demo. I also wrote [this](http://plnkr.co/edit/X9v15FqLXt9GM3KetHlS?p=preview) version using `zone.run`. – r_31415 Jul 10 '16 at 16:15
  • great , will look into it when I have access to a proper computer (using a phone right now ) – Manube Jul 10 '16 at 16:48
  • Thank you! I appreciate your help. – r_31415 Jul 10 '16 at 17:51
  • I took a quick look yesterday night, but could not fix it yet. One more thing I just thought was to use the most relevant angular hook, like ngAfterViewInit instead of constructor, to be sure DOM is ready when applying the listener – Manube Jul 11 '16 at 09:51
  • Thank you. That's a good improvement. [This](http://plnkr.co/edit/hyssYnklc3opRD1Lq406?p=preview) is my updated version using your suggestion. I also updated the main plnkr without zone. Unfortunately, the issue persists. It feels suspicious to apply `zone.run` to the listener itself. Actually, in the link you posted, the solution is using `zone.run` inside the listener, not the other way around. – r_31415 Jul 11 '16 at 15:44
  • I asked in the Gitter of Angular about some way to pass the form values to a directive and was able to make it work like [this](http://plnkr.co/edit/Ot2tR2AMmu20LZ477A3G?p=preview). It is definitely not the best approach, but I guess that's part of the problem of having third-party libraries. What do you think? – r_31415 Jul 11 '16 at 22:29
  • well, it is working, well done! Whether you manage to trigger zone detection, or you manually change the values using the 'ngAfterViewInit' hook, it seems to amount to the same thing: providing glue between Angular and the third-party script. Not sure whether there is standard best approach in this case. But worth exploring... – Manube Jul 12 '16 at 19:04
  • Yes, a standard approach definitely would be good to know. By the way, thank you for your help! – r_31415 Jul 12 '16 at 22:52
  • you are welcome. Maybe you should provide your own answer, as you solved this yourself. I would then delete mine... – Manube Jul 13 '16 at 07:59
  • Okay, but please don't delete your answer. It is possible that there is working solution using NgZone. – r_31415 Jul 14 '16 at 16:18