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

前端 未结 6 622
一生所求
一生所求 2021-01-31 15:42

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:



        
相关标签:
6条回答
  • 2021-01-31 16:17

    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

    0 讨论(0)
  • 2021-01-31 16:18

    '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" ...
    
    0 讨论(0)
  • 2021-01-31 16:25

    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'))
        }
    }
    
    0 讨论(0)
  • 2021-01-31 16:28

    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)]="firstName.model">
    
    0 讨论(0)
  • 2021-01-31 16:30

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

    0 讨论(0)
  • 2021-01-31 16:40

    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.

    0 讨论(0)
提交回复
热议问题