web-dev-qa-db-fra.com

S'abonner aux modifications sur la propriété du composant dans Angular 2 pour la sauvegarde automatique?

Supposons que j'ai le composant Angular 2 suivant, où foo est un objet non lié à un formulaire et ne provenant pas d'un _Input():

@Component({
    selector: 'foo',
    templateUrl: '/angular/views/foo.template.html',
    directives: [ContentEditableDirective, ROUTER_DIRECTIVES],
    providers: [FooService]
})
export class FooComponent implements OnInit {
    public foo: Foo = new Foo();

    constructor(
        private fooService: FooService,
        private route : ActivatedRoute,
        private router : Router) {}

    ngOnInit() {
        let id = +this.route.snapshot.params['id'];
        this.fooService.getFoo(id).subscribe(
            foo => {
                this.foo = foo;

                // I have this.foo, we should watch for changes here.

                Observable. // ???
                    .debounceTime(1000)
                    .subscribe(input => fooService.autoSave(this.foo));
            },
            error => console.log(error)
        );
    }
}

Comment puis-je m'abonner aux modifications apportées à l'objet Foo et l'envoyer à mon serveur?

Tous les exemples que j'ai vus jusqu'à présent impliquent soit de laisser foo sortir d'un Input(), soit d'être lié à un formulaire. Je souhaite simplement observer les modifications apportées à un ancien objet Javascript simple et réagir à cela.

Mettre à jour

J'ai de nouveau essayé et j'ai pu annuler une propriété interne primitive d'un composant ici .

Mais j'ai été incapable de faire fonctionner ce travail avec un objet complexe qui possède des propriétés qui lui sont propres ( ici ); le setter dans le plnkr fourni n'est pas appelé par ngModel lorsqu'il met à jour la propriété de l'objet foo.

Une réponse correcte montrera ce travail avec un objet complexe.

Mise à jour 2

_ { Je crois que je travaille avec un objet complexe } _; mais vous devez vous assurer que l'objet est immuable et que vous avez des setters pour chaque propriété, ce qui est une sorte de déception. Il semble également nécessiter le fractionnement de [(ngModel)] en [ngModel] et (ngModelChange) afin que vous puissiez spécifier une logique de personnalisation personnalisée.

Vous pouvez théoriquement résumer la fonctionnalité à un service public, mais j'aimerais voir si la quantité de passe-partout peut être réduite davantage. Créer de nouveaux objets chaque fois que vous souhaitez un changement d'état est assez frustrant.

7

Je semble avoir partiellement résolu ce problème pour des objets simples et complexes. Si vous n'avez pas besoin d'un détecteur de changement complet, vous pouvez le mettre en œuvre facilement avec simplement:

  • Un sujet et une observable sur votre composant.
  • Un getter et un setter pour la propriété suivie sur votre composant.

Vous trouverez ci-dessous un exemple très minime de la manière dont on pourrait y parvenir. 

Composant

import {Component} from '@angular/core'
import {Subject} from 'rxjs/Rx';

@Component({
  selector: 'my-app',
  providers: [],
  template: `
    <div>
      <input type="text" [(ngModel)]="foo"  />
      <div>current Foo: {{ foo }}</div>
      <div>debounced Foo: {{ debouncedFoo }}</div>
    </div>
  `,
  directives: []
})
export class App {
  private _foo: any;
  debouncedFoo: any;
  fooSubject : Subject<any> = new Subject<any>(this._foo);
  fooStream : Observable<any> = this.fooSubject.asObservable();

  get foo() {
      return this._foo;
  });

  set foo(value:any) {
    this._foo = value;
    this.fooSubject.next(value);
  });

  constructor() {
    // Manipulate your subscription stream here
    this.subscription = this.fooStream
      .debounceTime(2000)
      .subscribe(
        value => {
          this.debouncedFoo = value;
      });
  }
}

Exemple

http://plnkr.co/edit/HMJz6UWeZIBovMkXlR01?p=preview

Bien entendu, de manière réaliste, le composant ne doit avoir aucune connaissance du sujet ou être observable. Vous devez donc les extraire vers un service à votre guise.

Inconvénients

Cet exemple ne fonctionne qu'avec des primitives. Si vous souhaitez qu'il fonctionne avec des objets, vous avez besoin d'une logique de définition personnalisée pour chaque propriété et n'oubliez pas de créer de nouveaux objets chaque fois que vous modifiez une propriété de cet objet.

7

@vjawala est très proche :)

Si vous souhaitez suivre les propriétés d'entrée (annotées avec @Input), utilisez OnChanges:

  ngOnChanges(changes: SimpleChanges) {
    if(changes['foo']){
        triggerAutoSave();
    }
  }

Si vous souhaitez suivre un objet clé/valeur, implémentez DoCheck listener et utilisez KeyValueDiffer, un exemple est fourni dans la documentation angulaire.

Il pourrait être beaucoup plus efficace/simple de vous abonner directement pour modifier/saisir (probablement les deux) des événements: 

<form (input)="triggerAutoSave()" (change)="triggerAutoSave()">
   <input>
   <input>
   <input>
</form>

Debounce est relativement facile avec RxJS:

triggerAutoSave(){
     subject.next(this.foo);
}

subject.debounceTime(500).subscribe(...);

Si vous enregistrez via http, exhaustMap peut vous aider.

3
kemsky

C'est quelque chose que je viens de proposer. Cela nécessite probablement un peu de polissage, mais cela fonctionne bien malgré tout.

Travailler Plunker par exemple usage


export class ChangeTracker {
  private _object: any;
  private _objectRetriever: () => any;
  private _objectSubject: Subject<any> = new Subject<any>();
  private _onChange: (obj: any) => void;
  private _fnCompare: (a: any, b: any) => boolean;

  constructor(objRetriever: () => any, 
              onChange: (obj: any) => void, 
              fnCompare?: (a: any, b: any) => boolean) {

    this._object = objRetriever();
    this._objectRetriever = objRetriever;
    this._onChange = onChange;
    this._fnCompare = fnCompare ? fnCompare : this.defaultComparer;

    this._objectSubject
      .debounceTime(1000)
      .subscribe((data: any) => {
          this._onChange(data);
      });

    setInterval(() => this.detectChanges(), 500);
  }

  private defaultComparer(a: any, b: any) {
    return JSON.stringify(a) == JSON.stringify(b);
  }

  private detectChanges() {
    let currentObject = this._objectRetriever();

    if (!this._fnCompare(this._object, currentObject)) {
      this._object = currentObject;

      this._objectSubject.next(currentObject);
    }
  }
}

Utilisation dans votre exemple de code:

@Component({
    selector: 'foo',
    templateUrl: '/angular/views/foo.template.html',
    directives: [ContentEditableDirective, ROUTER_DIRECTIVES],
    providers: [FooService]
})
export class FooComponent implements OnInit {
    public foo: Foo = new Foo();

    constructor(
        private fooService: FooService,
        private route : ActivatedRoute,
        private router : Router) {}

    ngOnInit() {
        let id = +this.route.snapshot.params['id'];
        this.fooService.getFoo(id).subscribe(
            foo => {
                this.foo = foo;

                /* just create a new ChangeTracker with a reference
                 * to the local foo variable and define the action to be
                 * taken on a detected change
                 */
                new ChangeTracker(
                    () => this.foo, 
                    (value) => {
                        this.fooService.autoSave(value);
                    }
                );
            },
            error => console.log(error)
        );
    }
}
2
rinukkusu

Que faire si vous utilisez la fonction ngDoCheck()? Quelque chose comme:

@Component({
    selector: 'foo',
    templateUrl: '/angular/views/foo.template.html',
    directives: [ContentEditableDirective, ROUTER_DIRECTIVES],
    providers: [FooService]
})
export class FooComponent implements OnInit, DoCheck {
    public foo: Foo = new Foo();

    constructor(
        private fooService: FooService,
        private route : ActivatedRoute,
        private router : Router) {}

    ngOnInit() {
        let id = +this.route.snapshot.params['id'];
        this.foo = this.fooService.getFoo(id);
    }
    ngDoCheck() {
        fooService.autoSave(this.foo);
    }
}
1
vjawala

Vous pouvez utiliser des flux dans angular2 . J'avais la même exigence en tant que telle, j'avais implémenté l'utilisation de flux dans un service comme suit:

user.service.ts

selectedUserInstance:user = new User();

// Observable selectUser source
private selectUserSource = new Subject<any>();
// Observable selectUser stream
selectUser$ = this.selectUserSource.asObservable();

// service command
selectUser(user:User) {
    //console.log(user);
    this.selectedUserInstance=user;    
    this.selectUserSource.next(this.selectedUserInstance);
}

composant intérieur

this.subscription = this.userService.selectUser$.subscribe(
  selectedUser => {
    this.selectedUser = selectedUser;
    console.log("In component : " + this.selectedUser.name);
  });
0
Bhushan Gadekar