Extending Angular 2 ngModel directive to use observables
Asked Answered
A

4

29

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" />
Ashes answered 9/8, 2016 at 7:41 Comment(5)
Sounds like you are looking for something like github.com/angular/angular/issues/4062. I'm sure this will come to Angular2 but only after release.Osana
@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.Ashes
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.Quinsy
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.Ashes
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.Aundrea
R
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

Respiratory answered 28/2, 2019 at 15:50 Comment(2)
Do not link to external resources; instead, explain how to solve the problem hereSheedy
The op's question already has an explanation. The "external" solution is code converted into a directive.Viscacha
D
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>
Divan answered 3/8, 2019 at 19:17 Comment(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.Respiratory
R
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

Respiratory answered 28/2, 2019 at 15:50 Comment(2)
Do not link to external resources; instead, explain how to solve the problem hereSheedy
The op's question already has an explanation. The "external" solution is code converted into a directive.Viscacha
A
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

Aphelion answered 3/8, 2019 at 23:27 Comment(2)
Does this update the view value when the value changes elsewhere such as in the component typescript? Struggling with it not updating the view.Binocular
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)"Aphelion
N
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
}
Nace answered 28/7, 2022 at 22:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.