62 votes

Comment créer un composant d'entrée personnalisé avec ngModel fonctionnant dans angular 6 ?

Comme j'utilise des entrées avec beaucoup de directives et de classes .css identiques, je veux extraire le code répété vers un composant comme celui-ci :

  @Component({
  selector: "app-input",
  template: `
    <div class="...">
      <input type="..." name="..." class="..." [(ngModel)]="value" someDirectives...>
      <label for="...">...</label>
    </div>
  `,
  ...
  })
  export class InputComponent implements OnInit {
    // some implementation connecting external ngModel with internal "value" one
  }

Le problème ici est de créer un composant de manière à ce qu'il puisse être utilisé avec ngModel comme une entrée ordinaire :

<app-input [(ngModel)]="externalValue" ... ></app-input>

J'ai trouvé plusieurs solutions sur internet qui peuvent être partiellement ou complètement dépassées maintenant comme : Entrée de formulaire personnalisé en Angular 2 Peut-on faire cela d'une meilleure manière dans angular 6 ?

1 votes

Cela pourrait Aide

1 votes

@Vikas Oui, il y a une démo qui fonctionne, merci.

113voto

andreas Points 1168

J'ai rencontré le même problème il y a quelque temps et je souhaite partager un exemple minimal qui fonctionne avec Angular 2+.

Pour les versions plus récentes d'Angular, il existe une approche simplifiée (faites défiler vers le bas) !


Angular 2

Supposons que vous souhaitiez utiliser le code suivant n'importe où dans votre application :

<app-input-slider [(ngModel)]="inputSliderValue"></app-input-slider>
  1. Créez maintenant un composant appelé InputSlider .

  2. En el input-slider.component.html ajoutez ce qui suit :

    <input type="range" [(ngModel)]="value" (ngModelChange)="updateChanges()">
  3. Maintenant nous devons faire un peu de travail dans le input-slider.component.ts fichier :

    import {Component, forwardRef, OnInit} from "@angular/core";
    import {ControlValueAccessor, NG_VALUE_ACCESSOR} from "@angular/forms";
    
    @Component({
        selector: "app-input-slider",
        templateUrl: "./input-slider.component.html",
        styleUrls: ["./input-slider.component.scss"],
        providers: [{
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => InputSliderComponent),
            multi: true
        }]
    
    })
    export class InputSliderComponent implements ControlValueAccessor {
    
     /**
      * Holds the current value of the slider
      */
     value: number = 0;
    
     /**
      * Invoked when the model has been changed
      */
     onChange: (_: any) => void = (_: any) => {};
    
     /**
      * Invoked when the model has been touched
      */
     onTouched: () => void = () => {};
    
     constructor() {}
    
     /**
      * Method that is invoked on an update of a model.
      */
     updateChanges() {
         this.onChange(this.value);
     }
    
     ///////////////
     // OVERRIDES //
     ///////////////
    
     /**
      * Writes a new item to the element.
      * @param value the value
      */
     writeValue(value: number): void {
         this.value = value;
         this.updateChanges();
     }
    
     /**
      * Registers a callback function that should be called when the control's value changes in the UI.
      * @param fn
      */
     registerOnChange(fn: any): void {
         this.onChange = fn;
     }
    
     /**
      * Registers a callback function that should be called when the control receives a blur event.
      * @param fn
      */
     registerOnTouched(fn: any): void {
         this.onTouched = fn;
     }

    }

Bien sûr, vous pourriez ajouter plus de fonctionnalités et de contrôles de valeur en utilisant cette classe, mais j'espère que cela vous donnera quelques idées.

Explication rapide :

L'astuce consiste à ajouter le fournisseur NG_VALUE_ACCESSOR sur le décorateur de la classe et implémenter ControlValueAccessor .

Ensuite, nous devons définir les fonctions writeValue , registerOnChange y registerOnTouched . Ces deux derniers sont directement appelés lors de la création du composant. C'est pourquoi nous avons besoin de variables (par exemple onChange y onTouched - mais vous pouvez les nommer comme vous le souhaitez.

Enfin, nous devons définir une fonction qui permet au composant de savoir qu'il doit mettre à jour le ngModel sous-jacent. Je l'ai fait avec la fonction updateChanges . Il doit être invoqué chaque fois que la valeur change, soit de l'extérieur (c'est pourquoi il est appelé en writeValue ), ou de l'intérieur (c'est pourquoi il est appelé depuis le fichier html ngModelChange ).


Angular 7+ (en anglais)

Bien que la première approche fonctionne toujours pour les versions les plus récentes, vous préférerez peut-être la version suivante qui nécessite moins de saisie.

Auparavant, vous pouviez obtenir une liaison bidirectionnelle en ajoutant quelque chose comme ceci dans le composant externe :

<app-input-slider [inputSliderValue]="inputSliderValue" (inputSliderValueChange)="inputSliderValue = $event"></app-input-slider>

Angular a implémenté un sucre syntaxique pour cela, donc vous pouvez maintenant écrire

<app-input-slider [(inputSliderValue)]="inputSliderValue"></app-input-slider>

si vous suivez les étapes ci-dessous.

  1. Créez un composant appelé InputSlider .

  2. En el input-slider.component.html ajoutez ce qui suit :

     <input type="range" [(ngModel)]="inputSliderValue (ngModelChange)="inputSliderValueChange.emit(inputSliderValue)">
  3. Maintenant nous devons faire un peu de travail dans le input-slider.component.ts fichier :

    import {Component, forwardRef, OnInit} from "@angular/core";
    
    @Component({
        selector: "app-input-slider",
        templateUrl: "./input-slider.component.html",
        styleUrls: ["./input-slider.component.scss"],
        providers: []
    })
    export class InputSliderComponent {
    
        /**
         * Holds the current value of the slider
         */
        @Input() inputSliderValue: string = "";
    
        /**
         * Invoked when the model has been changed
         */
        @Output() inputSliderValueChange: EventEmitter<string> = new EventEmitter<string>();
    
     }

Il est important que la propriété de sortie (EventEmitter) ait le même nom que la propriété d'entrée avec la chaîne ajoutée Change .


Si nous comparons les deux approches, nous constatons ce qui suit :

  • La première approche vous permet d'utiliser [(ngModel)]="propertyNameOutsideTheComponent" comme si le composant était un élément de formulaire quelconque.
  • Seule la première approche vous permet d'utiliser directement la validation (pour les formulaires).
  • Mais la première approche nécessite plus de codage dans la classe du composant que la deuxième approche.
  • La deuxième approche vous permet d'utiliser une liaison bidirectionnelle sur votre propriété avec la syntaxe suivante [(propertyNameInsideTheComponent)]="propertyNameOutsideTheComponent"

1 votes

D'après d'autres exemples que j'ai vus, cela devrait fonctionner. La solution actuelle que j'utilise présente les différences suivantes en html : (ngModelChange)="onChangeCallback($event)" [ngModel]="innerValue" et dans le langage ts : registerOnChange(fn : any) : void { this.onChangeCallback = fn ; } writeValue(value : any) { if(value !== this.innerValue) { this.innerValue = value ; } }. } Donc, en fait, je n'appelle pas onChange manuellement. J'ai rencontré une situation où le modèle n'a pas été mis à jour, peut-être est-ce lié. Qu'en pensez-vous ?

0 votes

Pour la version Angular 7 : J'utilise cette solution, mais cela ne fonctionne pas dans le cas où vous voulez l'utiliser de cette façon : <input type="range" [(ngModel)]="someObject.val" ...> et vous changez someObject entre-temps (mise à jour de l'appel de service).

0 votes

En effet ! Cela ne fonctionne jamais, parce que la référence à l'instance originale de l'objet sera conservée et la propriété val sera utilisée à la place.

67voto

omer Points 597

Vous pouvez également procéder de la manière suivante : lorsque vous créez une liaison bidirectionnelle [()], vous pouvez la lier à une fonction portant le même nom + "change" (dans notre cas, inputModel et inputModelChange). De cette manière, le ngModel sera mis à jour lorsque vous déclencherez inputModelChange.emit('updatedValue') et vous ne devrez le déclarer qu'une seule fois dans votre composant.

app-input.component.ts

import { Component, OnInit, Output, Input, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-input',
  template: `  <input type="text" [(ngModel)]="inputModel" (ngModelChange)="inputModelChange.emit(inputModel)"/>`,
  styleUrls: ['./app-input.component.scss']
})
export class AppInputComponent {
  @Input() inputModel: string;
  @Output() inputModelChange = new EventEmitter<string>();
}

app.component.html

<app-input [(inputModel)]="externalValue"></app-input>

4 votes

Actuellement, j'utilise exactement cette solution. Le seul problème est qu'elle n'utilise pas ngModel dans l'interface, elle utilise un autre champ spécifique ("inputModel" dans votre cas). Mais j'ai trouvé cet inconvénient beaucoup moins inquiétant que d'implémenter correctement l'ensemble du ngModel pour chaque nouvelle entrée shell ou autre composant que je crée.

1 votes

Fonctionne pour moi, Angular.io Version 7.1.5

0 votes

Comment suivre le changement de modèle du parent ?

21voto

Si vous ne vous souciez pas de lier votre variable par [ngModel] dans le modèle ou [formControl] en forme réactive, vous pouvez utiliser réponse de l'omer .

Autrement :

  1. Ajouter NG_VALUE_ACCESSOR dans la définition de votre composant :

    import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
    
    @Component({
       ...,
       providers: [
         {
            provide: NG_VALUE_ACCESSOR,
            useExisting: forwardRef(() => AppInputComponent),
            multi: true
         }
       ]
    })
  2. Mettre en œuvre ControlValueAccessor interface :

    export class AppInputComponent implements ControlValueAccessor {
    
      writeValue(obj: any): void {
        // Step 3
      }
    
      registerOnChange(fn: any): void {
        this.onChange = fn;
      }
    
      registerOnTouched(fn: any): void {
        this.onTouched = fn;
      }
    
      setDisabledState?(isDisabled: boolean): void {
      }
    
      onChange: any = () => { };
    
      onTouched: any = () => { };
    
    }
  3. Gérer value quand elle change :

    private _value;
    
    public get value(){
      return this._value;
    }
    
    public set value(v){
      this._value = v;
      this.onChange(this._value);
      this.onTouched();
    }
    
    writeValue(obj: any): void {
      this._value = obj;
    }
    
    // Optional
    onSomeEventOccured(newValue){
      this.value = newValue;
    }

Vous pouvez maintenant utiliser <app-input [(ngModel)]="externalValue" ... ></app-input>

1 votes

Vous pouvez utiliser getter y setter fonction comme une variable. lien

-4voto

Prachi Points 2354

Vous pouvez utiliser la directive @Input pour faire passer la valeur externe dans le composant et la lier avec elle.

Voici un code :

  @Component({
  selector: "app-input",
  template: `
    <div class="...">
      <input type="..." name="..." class="..." [(ngModel)]="externalValue" someDirectives...>
      <label for="...">...</label>
    </div>
  `,
  })

  export class InputComponent implements OnInit {
     @Input('externalValue') externalValue : any;
  }

Et dans votre composant parent, vous pouvez l'utiliser comme :

<app-input [externalValue]="externalValue" ... ></app-input>

0 votes

On dirait que vous n'avez pas saisi l'idée. Passer une valeur est très simple. La question est de savoir comment renvoyer la valeur mise à jour en utilisant uniquement [(ngModel)]. Je pourrais retourner la valeur mise à jour en utilisant Output et EventEmitter, mais je devrais passer la valeur d'entrée et la fonction de sortie à chaque composant d'entrée personnalisé que j'ai, ce qui augmente beaucoup le boilerplate et n'est pas acceptable.

1 votes

Êtes-vous passé par este poste ? Je pense que c'est ce que vous recherchez.

1 votes

Ce site peut également être consulté. Voir le dernier exemple de l'article.

-6voto

mittal bhatt Points 464

Vous pouvez utiliser un service partagé qui communique entre deux composants sans utiliser d'entrée ou de sortie comme ci-dessous

Service

import {Injectable} from '@angular/core';

@Injectable()
export class ShareService {
   public data: string;

   setData(newdata : string){
       this.data = newdata;
   }

   clearData(){
       this.data = '';
   }
}

Composant qui définit la valeur

export class PageA {
    constructor(private shareService: ShareService, private router: Router){
    }
    gotoPageB(){
        this.shareService.setData("Sample data");
        this.router.navigate(['pageb']);  
    }

}

Composant qui obtient la valeur

export class PageB {
    constructor(private shareService: ShareService){ }

    get somedata() : string {
      return this.shareService.data;
    }
}

La clé ici est d'utiliser une propriété getter dans le composant qui obtient la valeur (PageB dans cet exemple) afin qu'elle soit mise à jour chaque fois que la valeur du service de données change.

0 votes

Désolé, c'est une toute autre solution. J'ai besoin d'utiliser [(ngModel)] sur un composant personnalisé de la même manière que sur un <input> ordinaire, et pas seulement de chercher des possibilités de partage de données.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X