47

Is there a short and simple way to pass an RxJS Subject or BehaviorSubject to an an Angular 2 directive for two-way binding? The long way to do it would be as follows:

@Component({
    template: `
        <input type="text" [ngModel]="subject | async" (ngModelChange)="subject.next($event)" />
    `
})

I'd like to be able to do something like this:

@Component({
    template: `
        <input type="text" [(ngModel)]="subject" />
    `
})

I believe the async pipe is only one-way, so that's not enough. Does Angular 2 provide a short and simple way to do this? Angular 2 uses RxJS too, so I expected there to be some inherent compatibility.

Could I perhaps create a new ngModel-like directive to make this possible?

mhelvens
  • 4,225
  • 4
  • 31
  • 55
  • 2
    What do you expect to happen when the value of the input changes? `Subject` is one-way. `[ngModel]="subject | async" (ngModelChange)="subject.next($event)"` might work – Günter Zöchbauer Jul 29 '16 at 15:15
  • 3
    @GünterZöchbauer [`Subject`](http://reactivex.io/rxjs/class/es6/Subject.js~Subject.html) is two-way, though. It's both an [`Observer`](http://reactivex.io/rxjs/class/es6/MiscJSDoc.js~ObserverDoc.html) and an [`Observable`](http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html). I'm also fine with this always being a [`BehaviorSubject`](http://reactivex.io/rxjs/class/es6/BehaviorSubject.js~BehaviorSubject.html), if it helps (because that has a method to access the current value). – mhelvens Jul 29 '16 at 15:17
  • Did you ever figure this out? – DarkNeuron Dec 16 '16 at 17:52
  • Afraid not. I guess I stopped caring at some point. :-) – mhelvens Dec 17 '16 at 19:19
  • Though now that I think about it, a custom attribute-based directive could probably be created for this. – mhelvens Dec 17 '16 at 20:13
  • Afaik there is no better way to bind it to `Subject`. I tried to create such directive but it looks its not possible to add `ngModel` directive to my directive's host. – Martin Nuc Apr 21 '17 at 23:49
  • I don't think it's doable. However [there was an attempt](https://github.com/angular/angular/issues/13248) by the angular team to solve part of this issue. – Quentin F Jun 20 '17 at 18:21

6 Answers6

23

This is a simple solution, as you said in your question. I think there's nothing simpler than what you already provided.

<input type="text" 
       [ngModel]="subject | async"
       (ngModelChange)="subject.next($event)" />
activedecay
  • 10,129
  • 5
  • 47
  • 71
9

A possible solution is a sublcass of BehaviorSubject:

class ModelSubject<T> extends BehaviorSubject<T> {

    constructor(initialValue: T) {
        super(initialValue);
    }

    set model(value: T) {
        this.next(value);
    }

    get model(): T {
        return this.value;
    }
}

Usage:

Component-Class:

name = new ModelSubject<string>('');

Component-Template:

<input [(ngModel)]="name.model">
Dieter Rehbein
  • 941
  • 8
  • 16
5

I've started looking into something like this to integrate form controls with my library ng-app-state. If you are the type who enjoys making very generic, library-like code then read on. But beware, this is long! In the end you should be able to use this in your templates:

<input [subjectModel]="subject">

I have made a proof-of-concept for the first half of this answer, and the second half I believe is correct, but be warned that none of actual code written in this answer is tested. I'm sorry, but that's the best I have to offer right now. :)

You can write your own directive called subjectModel to connect a subject to a form component. Following are the essential parts, minus things like cleanup. It relies on the ControlValueAccessor interface, so Angular includes the necessary adapters to hook this up to all the standard HTML form elements, and it will work with any custom form controls you find in the wild, as long as they use ControlValueAccessor (which is the recommended practice).

@Directive({ selector: '[subjectModel]' })
export class SubjectModelDirective {
    private valueAccesor: ControlValueAccessor;

    constructor(
        @Self() @Inject(NG_VALUE_ACCESSOR)
        valueAccessors: ControlValueAccessor[],
    ) {
        this.valueAccessor = valueAccessors[0]; // <- this can be fancier
    }

    @Input() set subjectModel(subject: Subject) {
        // <-- cleanup here if this was already set before
        subject.subscribe((newValue) => {
            // <-- skip if this is already the value
            this.valueAccessor.writeValue(newValue);
        });
        this.valueAccessor.registerOnChange((newValue) => {
            subject.next(newValue);
        });
    }
}

We could stop here, and you'll be able to write this in your templates:

<input [subjectModel]="subject" [ngDefaultControl]>

That extra [ngDefaultControl] exists to manually cause angular to provide the needed ControlValueAccessor to our directive. Other kinds of inputs (like radio buttons and selects) would need a different extra directive. This is because Angular does not automatically attached value accessors to every form component, only those that also have an ngModel, formControl, or formControlName.

If you want to go the extra mile to eliminate the need for those extra directives, you'll have to essentially copy them into your code, but modify their selectors to activate for your new subjectModel. This is the totally untested part, but I believe you could do this:

// This is copy-paste-tweaked from
// https://angular.io/api/forms/DefaultValueAccessor
@Directive({
    selector: 'input:not([type=checkbox])[subjectModel],textarea[subjectModel]',
    host: {
        '(input)': '_handleInput($event.target.value)',
        '(blur)': 'onTouched()',
        '(compositionstart)': '_compositionStart()',
        '(compositionend)': '_compositionEnd($event.target.value)'
    },
    providers: [DEFAULT_VALUE_ACCESSOR]
})
export class DefaultSubjectModelValueAccessor extends DefaultValueAccessor {}

Credit for my understanding of this goes to ngrx-forms, which employes this technique.

Eric Simonton
  • 5,702
  • 2
  • 37
  • 54
  • Looks interesting. Was this solution available back when I first asked the question? --- I'm afraid I won't have time to verify this. I use React in my new job, and my mind really isn't in Angular space right now. --- If someone else could verify, I'd be happy to accept the answer. – mhelvens Jan 07 '18 at 15:10
  • I'm not sure when this would have become possible. – Eric Simonton Jan 07 '18 at 15:39
5

'If the mountain will not come to Muhammad, then Muhammad must go to the mountain'

Lets approach this from RxJS side instead of the NgModel side.

This solution limits us to only use BehaviorSubject's but I think this is a fair trade for having such an easy solution.

Slap this piece of code into your polyfills.ts. This enables you to bind the .value of a BehaviorSubject to an ngModel

import { BehaviorSubject } from 'rxjs';

Object.defineProperty(BehaviorSubject.prototype, 'value', {
    set: function(v) {
        return this.next(v);
    }
});

And just use it like this.

<ng5-slider [(value)]="fooBehaviorSubject.value" ...
Győri Sándor
  • 592
  • 5
  • 10
  • 1
    Adding this to `polyfills.ts` does the trick in development builds. This solution gives a build error (`Attempt to assign to const or readonly variable`) in production, because `value` property is still seen as readonly by the compiler. I had to use Mario's solution. – Daniels Šatcs Apr 22 '20 at 22:00
  • who/what is muhamamannad? i don't understand the quote here – activedecay Jan 17 '23 at 18:38
2

I tried this and it worked

<div>
    <input
        #searchInput
        type="search"
        [ngModel]="searchTerm | async"
        (ngModelChange)="searchTerm.next(searchInput.value)"
    />
    {{ searchTerm | async }}
</div>

enter image description here

I'm not sure it this is breaking any rules and if it's buggy or hacky but seems to work for me. I wish Angular had a built it subject directive like they do with forms.

Hope this helps

Mario Subotic
  • 131
  • 1
  • 5
1

The closest I can think of is to use a FormControl:

import { FormControl } from '@angular/forms';

@Component({
    template: '<input [formControl]="control">'
})
class MyComponent {
    control = new FormControl('');
    constructor(){
        this.control.valueChanges.subscribe(()=> console.log('tada'))
    }
}
Quentin F
  • 738
  • 8
  • 16