1

I'm working on a custom material component but while testing, the input value is not sent to the parent component. The form control value in the AppComponent is always null.

app.component.ts

import { Component, ChangeDetectionStrategy } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'ipmon-cartoppo-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
  title = 'test-carte';

  numCarteCtrl = new FormControl();

}

app.component.html

<h1>Welcome to test-cartes!</h1>
<label>Carte</label>
<cartoppy-saisie-numero-carte formControl="numCarteCtrl" required="false" placeholder="____ ____ ____ ____"></cartoppy-saisie-numero-carte>

<span
  >Valeur dans l'app :

  {{ numCarteCtrl.value | json }}
</span>

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { CartoppyLibCarteModule } from '@ipmon-cartoppo/cartoppy-lib-carte';
import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ReactiveFormsModule } from '@angular/forms';

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule, BrowserAnimationsModule, CartoppyLibCarteModule, ReactiveFormsModule],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

saisie-numero-carte.component.html

<mat-form-field>
  <input
    matInput
    cartoppyNumCarte
    type="text"
    [formControl]="numeroCarteCtrl"
    [id]="labelforId"
    [placeholder]="placeholder"
    [required]="required"
    maxlength="19"
  />
  <mat-error *ngIf="numeroCarteCtrl.hasError('required')">
    La saisie de ce champ est obligatoire.
  </mat-error>
</mat-form-field>
<span
  >Valeur du champs :
  <pre>
    {{ numeroCarteCtrl.value }}
    {{ required }}
  </pre>
</span>

saisie-numero-carte.component.ts

import { Component, OnInit, ElementRef, Input, OnDestroy, ChangeDetectionStrategy, HostBinding, Optional, Self } from '@angular/core';
import { FormControl, NgControl, ControlValueAccessor} from '@angular/forms';
import { tap, distinctUntilChanged, takeUntil } from 'rxjs/operators';
import { MatFormFieldControl } from '@angular/material';
import { Subject } from 'rxjs';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import { FocusMonitor } from '@angular/cdk/a11y';
/**
 * Composant pour saisir numéro carte
 */
@Component({
  selector: 'cartoppy-saisie-numero-carte',
  templateUrl: './saisie-numero-carte.component.html',
  styleUrls: ['./saisie-numero-carte.component.scss'],
  providers: [
    {
      provide: MatFormFieldControl,
      useExisting: SaisieNumeroCarteComponent
    }
  ],
  changeDetection: ChangeDetectionStrategy.OnPush
})

/**
 * Class Composant
 */
export class SaisieNumeroCarteComponent implements OnInit, ControlValueAccessor, MatFormFieldControl<string>, OnDestroy {
  static nextISaisieNumCarteComponent = 0;

  /**
   * Lié à l'attribut labelfor du label, il permet de donner le focus au champ
   */
  @Input()
  labelforId: string;

  /**
   * id du control
   */
  @HostBinding()
  id = `saisie-num-carte-${SaisieNumeroCarteComponent.nextISaisieNumCarteComponent++}`;

  /**
   * Form control du numéro carte
   */
  numeroCarteCtrl = new FormControl();

  stateChanges = new Subject<void>();

  describedBy = '';

  focused: boolean;

  shouldLabelFloat: boolean;

  private destroy$ = new Subject<void>();

  private _value = '';

  private _placeholder: string;

  private _required = false;

  private _readOnly = false;

  private _disabled = false;

  onChange = (_: any) => {};

  onTouched = () => {};

  get empty(): boolean {
    return !this.value;
  }

  /**
   * param disabled du control
   */
  @Input()
  get disabled(): boolean {
    return this._disabled;
  }
  set disabled(value: boolean) {
    this._disabled = coerceBooleanProperty(value);
    this._disabled ? this.numeroCarteCtrl.disable() : this.numeroCarteCtrl.enable();
    this.stateChanges.next();
  }

  /**
   * param placeholder du control
   */
  @Input()
  get placeholder(): string {
    return this._placeholder;
  }
  set placeholder(value: string) {
    this._placeholder = value;
    this.stateChanges.next();
  }

  /**
   * param required du control
   */
  @Input()
  get required(): boolean {
    return this._required;
  }
  set required(value: boolean) {
    this._required = coerceBooleanProperty(value);
    this.stateChanges.next();
  }

  /**
   * param readOnly du control
   */
  @Input()
  get readOnly(): boolean {
    return this._readOnly;
  }
  set readOnly(value: boolean) {
    this._readOnly = coerceBooleanProperty(value);
  }

  /**
   * param value du control
   */
  @Input()
  get value(): string | null {
    return this._value;
  }
  set value(numCarte: string | null) {
    console.log('numCarte: ', numCarte);
    if (numCarte) {
      this.numeroCarteCtrl.setValue(numCarte.replace(/\s+/g, ''));
      this.onChange(numCarte);
      this.stateChanges.next();
    }
  }

  get errorState() {
    return this.ngControl.errors !== null && !!this.ngControl.touched;
  }

  /**
   * constructor
   * @param _elementRef ElementRef<HTMLElement>
   */
  constructor(@Optional() @Self() public ngControl: NgControl, private fm: FocusMonitor, private _elementRef: ElementRef<HTMLElement>) {
    if (this.ngControl != null) {
      this.ngControl.valueAccessor = this;
    }
    fm.monitor(_elementRef.nativeElement, true).subscribe(origin => {
      this.focused = !!origin;
      if (!this.focused && typeof this.numeroCarteCtrl.value === 'string') {
        this.numeroCarteCtrl.setValue(undefined);
      }
      this.stateChanges.next();
    });
  }

  /**
   * Fonction appelée quand le composant est initié
   */
  ngOnInit() {
    this.numeroCarteCtrl.valueChanges
      .pipe(
        takeUntil(this.destroy$),
        distinctUntilChanged(),
        tap((numero: string) => {
          console.log('numero: ', numero);
          this.value = numero;
        })
      )
      .subscribe();
  }

  /**
   * Fonction de l'inteface ControlValueAccessor
   */
  writeValue(obj: string | null): void {
    console.log('----- In writeValue -----: ', obj);
    this.value = obj;
  }

  /**
   * Fonction de l'inteface ControlValueAccessor
   */
  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  /**
   * Fonction de l'inteface ControlValueAccessor
   */
  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  /**
   * Fonction de l'inteface ControlValueAccessor
   */
  setDisabledState(isDisabled: boolean): void {
    this.disabled = isDisabled;
  }

  /**
   * Fonction du class MatFormFieldControl
   * @param ids  tableau
   */
  setDescribedByIds(ids: string[]): void {
    this.describedBy = ids.join(' ');
  }

  /**
   *  Fonction du class MatFormFieldControl
   * @param event MouseEvent
   */
  onContainerClick(event: MouseEvent): void {
    if ((event.target as Element).tagName.toLowerCase() !== 'input') {
      const input: HTMLInputElement | null = this._elementRef.nativeElement.querySelector('input');
      if (input) {
        input.focus();
      }
    }
  }

  /**
   * Fonction appelée avant la destruction du component
   */
  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
    this.stateChanges.complete();
    this.fm.stopMonitoring(this._elementRef.nativeElement);
  }
}

cartoppy-lib-carte.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SaisieNumeroCarteComponent } from './components/saisie-numero-carte/saisie-numero-carte.component';
import { MatFormFieldModule, MatInputModule, MAT_LABEL_GLOBAL_OPTIONS } from '@angular/material';
import { ReactiveFormsModule } from '@angular/forms';
import { NumCarteDirective } from './directives/num-carte.directive';

/**
 * Module CartoppyLibCarteModule
 */
@NgModule({
  imports: [CommonModule, MatFormFieldModule, MatInputModule, ReactiveFormsModule],
  declarations: [SaisieNumeroCarteComponent, NumCarteDirective],
  exports: [SaisieNumeroCarteComponent],
  providers: [{ provide: MAT_LABEL_GLOBAL_OPTIONS, useValue: { float: 'never' } }]
})
export class CartoppyLibCarteModule {}
2
  • Any specific reason to use ChangeDetectionStrategy? Try removing it from both component & check. Commented Feb 6, 2020 at 11:05
  • @AnkurAkvaliya no specific usage. Removing it changed nothing Commented Feb 6, 2020 at 11:13

1 Answer 1

1

I think passing value changes to parent is missing

Change formControl="numCarteCtrl" to formControlName="numCarteCtrl"

add below in constructor

 this.numeroCarteCtrl.valueChanges.subscribe(val => {
   this.onChnage(val)
  });

Or use below

   ngOnChanges(inputs) {
        this.onChange(this.numeroCarteCtrl.value);
    }

Here is working stackblitz.

Sign up to request clarification or add additional context in comments.

8 Comments

@Sajus You have typo in passing form control to custom element. Use formControlName to give form-control-name as string. See updated answer
@Sajus I have added working stackblitz. I'll help you debug your issue.
Thanks for the help I appreciate ;)
I've tried using formControlName too but without sucess
May be you can replicate your code in stackblitz then i can see the issue
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.