28

Angular 2 ngModel directive works with variables and functions like

<input [ngModel]="myVar" (ngModelChange)="myFunc($event)" />

Instead of variables and functions, I would like to use BehaviorSubjects instead

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

Is there a safe way to extend ngModel or use some kind of macro to reduce repetition in my templates?

<input [myNewNgModel]="mySubject" />
BinaryButterfly
  • 18,137
  • 13
  • 50
  • 91
nwarp
  • 731
  • 4
  • 8
  • 17
  • 1
    Sounds like you are looking for something like https://github.com/angular/angular/issues/4062. I'm sure this will come to Angular2 but only after release. – Günter Zöchbauer Aug 09 '16 at 10:08
  • @GünterZöchbauer Good to know there are others trying to Rx everything. The main difference is that I'm trying to extend/reuse ngModel with observables while the proposal is focusing on binding events to observables. – nwarp Aug 10 '16 at 03:46
  • Is this something that was eventually solved? I am happy to write up a response but currently I struggle to understand whether you actually want to use these inputs as part of a form? If so, have you thought of listening to the form observable? If not, can you please give some background as to how you are using your BehaviorSubject? There is certainly use cases for it, but when new to RxJS, we tend to overuse Subjects a little. – Ben Dadsetan Mar 25 '17 at 20:51
  • There are 2 goals here: 1. Reduce boilerplate code used when dealing with observables such as `myObservable.subscribe((myVar) => this.myVar = myVar)` and `myObserable | async` 2. Learn how to extend Angular objects. I don't understand how decorators work and how to extend decorated objects. I'm picking ngModel to illustrate the problem I'm trying to solve but it's not really about ngModel at all. – nwarp Mar 27 '17 at 07:41
  • Would a pipe be the ideal solution here? You'd keep the concerns separated in regards to updating a property and emitting a value. Otherwise, it would cause you to have to subscribe to the event to set the value back on the original property... Work experiment: try creating a simple component that will give you the best of both worlds without having to wire up more than you'd wire up using a ngModel. – Jacob Roberts May 18 '17 at 21:32

4 Answers4

7

I came up with a similar approached to @Adbel. Not sure about the inner implications of this, but it will be awesome to have some feedback. Stackbliz code

Your.component.ts

export class AppComponent  {
  email = new BehaviorSubject("UnwrappedMe ");

  emailHandler(input) {
    this.email.next(input);
  }
}

Your.component.html

 <form class="mx-3">
     <input [ngModel]="email | async" 
            (ngModelChange)="emailHandler($event)" 
            name="email" type="email" 
            id="email" placeholder="Enter email">
 </form>

 <p class="mx-3"> {{ email | async }} </p>

A little variation in case you need to get a ref to your input value and you do not want to make a second subscription (use template vars).

Your.component.html

 <form class="mx-3">
     <input [ngModel]="email | async" #emailref
            (ngModelChange)="emailHandler($event)" 
            name="email" type="email" 
            id="email" placeholder="Enter email">
 </form>

 <p class="mx-3"> {{ emailref.value }} </p>
Luillyfe
  • 6,183
  • 8
  • 36
  • 46
  • 1
    This will work great! Only problem is that it is not as re-usable. Imagine you had 5 more inputs, that would mean 5 more behavior subjects you need to add to `AppComponent` and 5 other methods to handle their changes. You could potentially improve this method to become a bit more efficient, but my approach encapsulates all of that way in a directive for you. – realappie Aug 05 '19 at 08:31
4

I don't know why you wouldn't just use reactive forms, but this was a fun puzzle. I created a directive that will alter the model value to the BehaviorSubject's value. And any changes will call .next on the BehaviorSubject for you.

Usage will look like this

<input type="text" [ngModel]="ngModelValue" appRxModel> 

Here is the stackblitz, enjoy

realappie
  • 4,656
  • 2
  • 29
  • 38
1

Do you really want to create an observable for each input field in your form? The pattern I use is to have one observable for the model of the whole form, clone it for a view variable that you can then bind to and then have the submit handler of the form push the new model back to the service.

user$ = this.userService.user$;

save(user: User) {
  this.userService.save(user);
}

and in the view

<form *ngIf="user$ | async | clone as user" #userForm="ngForm" (submit)="userForm.form.valid && save(user)">
  <label>
    Firstname
    <input name="firstname" [(ngModel)]="user.firstname" required>
  </label>
  <label>
    Lastname
    <input name="lastname" [(ngModel)]="user.lastname" required>
  </label>
  <button>Save</button>
</form>

The clone pipe looks like this

export const clone = (obj: any) =>
  Array.isArray(obj)
    ? obj.map(item => clone(item))
    : obj instanceof Date
    ? new Date(obj.getTime())
    : obj && typeof obj === 'object'
    ? Object.getOwnPropertyNames(obj).reduce((o, prop) => {
        o[prop] = clone(obj[prop]);
        return o;
      }, {})
    : obj;

import { Pipe, PipeTransform } from '@angular/core';

import { clone } from './clone';

@Pipe({
  name: 'clone'
})
export class ClonePipe implements PipeTransform {

  transform(value: any): any {
    return clone(value);
  }
}

I have done a write up on this pattern with my state management library here. https://medium.com/@adrianbrand/angular-state-management-with-rxcache-468a865fc3fb

Adrian Brand
  • 20,384
  • 4
  • 39
  • 60
  • Does this update the view value when the value changes elsewhere such as in the component typescript? Struggling with it not updating the view. – BeniaminoBaggins Nov 01 '21 at 20:44
  • The point of cloning the value that is unwaraped by the async pipe is so we don't mutate the contents of the observable. If we hit cancel then the only changes are to a disposable view variable. When we hit save it is up to the save method to persist the mutated object. If you want the mutated values to be reflected in your view as you edit then you should use the view variable. There are change events you can use to pass the value to TypeScript like (ngModelChange)="yourCompomentFunction(user)" – Adrian Brand Nov 02 '21 at 22:03
1

Answering late because none of these is what I found successful.

// View input
<input-check [ngModel]="(bool$ | async)!" (ngModelChange)="startNextHop($event)"></input-check>

// component
public bool$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);

startNextHop(event: any) {
    console.log(event); // on and off with the switch
}
Ben Racicot
  • 5,332
  • 12
  • 66
  • 130