How to two-way bind my own RxJS Subject to an [(ngModel)]? How to two-way bind my own RxJS Subject to an [(ngModel)]? angular angular

How to two-way bind my own RxJS Subject to an [(ngModel)]?


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)" />


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.


'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" ...