How to two-way bind my own RxJS Subject to an [(ng

2020-05-18 05:39发布

问题:

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?

回答1:

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


回答2:

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.



回答3:

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'))
    }
}


回答4:

I tried this and it worked

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

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



回答5:

This is a simple solution, as you said in your question. (Nothing simpler than what you already provided)

<input type="text" [ngModel]="subject | async" (ngModelChange)="subject.next($event)" />