3
votes

My goal:

I'm trying to build a reusable mat-form-field with a clear button.

How I tried achieving my goal:

I created a "mat-clearable-input" component and used it like this:

<mat-clearable-input>
        <mat-label>Put a Number here pls</mat-label>
        <input matInput formControlName="number_form_control">
    </mat-clearable-input>

mat-clearable-input.component.html

<mat-form-field>
    <ng-content></ng-content>
</mat-form-field>

Expected result:

the ng-content tag takes the label and the input and puts them inside the mat-form-field tag.

Actual result:

Error: mat-form-field must contain a MatFormFieldControl.
    at getMatFormFieldMissingControlError (form-field.js:226)
    at MatFormField._validateControlChild (form-field.js:688)
    at MatFormField.ngAfterContentChecked (form-field.js:558)
    at callHook (core.js:2926)
    at callHooks (core.js:2892)
    at executeInitAndCheckHooks (core.js:2844)
    at refreshView (core.js:7239)
    at refreshComponent (core.js:8335)
    at refreshChildComponents (core.js:6991)
    at refreshView (core.js:7248)

It looks like I'm missing something and I'm not using correctly the ng-content tag.

I wasn't able to locate the documentation for the ng-content tag on the angular website.

Thank you for any help.

EDIT AFTER ANSWER BELOW

So I tried this suggested method:

export class MatClearableInputComponent implements OnInit {
  @ContentChild(MatFormFieldControl) _control: MatFormFieldControl<any>;
  @ViewChild(MatFormField) _matFormField: MatFormField;
  // see https://stackguides.com/questions/63898533/angular-ng-content-not-working-with-mat-form-field/
  ngOnInit() {
    this._matFormField._control = this._control;
  }

}

unfortunately, when I try to use this in a form it still fails with the error "Error: mat-form-field must contain a MatFormFieldControl."

Code where i try to use this component in a form:

<mat-clearable-input>
    <mat-label>Numero incarico</mat-label>
    <buffered-input matInput formControlName="numero"></buffered-input>
</mat-clearable-input>

Repro on stackblitz: https://stackblitz.com/edit/angular-material-starter-xypjc5?file=app/clearable-form-field/clearable-form-field.component.html

notice how the mat-form-field features aren't working (no outline, no floating label), also open the console and you'll see the error Error: mat-form-field must contain a MatFormFieldControl.

EDIT AFTER OPTION 2 WAS POSTED

I tried doing this:

<mat-form-field>
  <input matInput hidden>
  <ng-content></ng-content>
</mat-form-field>

It works, but then when i added a mat-label to my form field, like this:

<mat-clearable-input>
        <mat-label>Numero incarico</mat-label>
        <buffered-input matInput formControlName="numero"></buffered-input>
    </mat-clearable-input>

the label is never floating and it's just staying there as a normal span the whole time.

So i tried assigning to the this._matFormField._control._label the content child with the label but that didn't work because _label is private and there is no setter for it.

It looks like I'm out of luck and this can't be done in Angular without going through a lot of effort.

If you have any further ideas feel free to fork the stackblitz and try!

Edit after @evilstiefel answer

the solution works only for native <input matInput>. When I try replacing that with my custom input component, it doesn't work anymore.

Working setup:

<mat-form-field appClearable>
    <mat-label>ID incarico</mat-label>
    <input matInput formControlName="id">
</mat-form-field>

Same setup but with my custom "buffered-input" component (not working :( )

<mat-form-field appClearable>
    <mat-label>ID incarico</mat-label>
    <buffered-input matInput formControlName="id"></buffered-input>
</mat-form-field>

The console logs this error when I click on the clear button:

TypeError: Cannot read property 'ngControl' of undefined
    at ClearableDirective.clear (clearable.directive.ts:33)
    at ClearButtonComponent.clearHost (clearable.directive.ts:55)
    at ClearButtonComponent_Template_button_click_0_listener (clearable.directive.ts:47)
    at executeListenerWithErrorHandling (core.js:14293)
    at wrapListenerIn_markDirtyAndPreventDefault (core.js:14328)
    at HTMLButtonElement.<anonymous> (platform-browser.js:582)
    at ZoneDelegate.invokeTask (zone-evergreen.js:399)
    at Object.onInvokeTask (core.js:27126)
    at ZoneDelegate.invokeTask (zone-evergreen.js:398)
    at Zone.runTask (zone-evergreen.js:167)
2
If you want to use a component inside a mat-form-field (in this case, to put mat-clearable-input inside it) it must implement MatFormFieldControl interface. Take a look at the docs to see how to do it.julianobrasil
@julianobrasil thanks, but that's not the problem: in the code snippet in the question, I'm using an input tag with the matInput directive, so that should work inside mat-form-field. I'm not creating custom components. Also keep in mind that this code works if I don't use ng-content and I just put the input tag there by handFrancesco Manicardi
Maybe a stackblitz would be helpfulStPaulis
one question: what should it look like?Andre Elrico
@AndreEirico like the first example of this page material.angular.io/components/input/examplesFrancesco Manicardi

2 Answers

1
votes

Another solution is using a directive to implement the behaviour.

import {
  AfterViewInit,
  Component,
  ComponentFactory,
  ComponentFactoryResolver,
  ContentChild,
  Directive,
  Injector,
  Input,
  Optional,
  SkipSelf,
  TemplateRef,
  ViewContainerRef,
} from '@angular/core';
import { MatFormFieldControl } from '@angular/material/form-field';


@Directive({
  selector: '[appClearable]'
})
export class ClearableDirective implements AfterViewInit {

  @ContentChild(MatFormFieldControl) matInput: MatFormFieldControl<any>;
  @Input() appClearable: TemplateRef<any>;
  private factory: ComponentFactory<ClearButtonComponent>;

  constructor(
    private vcr: ViewContainerRef,
    resolver: ComponentFactoryResolver,
    private injector: Injector,
  ) {
    this.factory = resolver.resolveComponentFactory(ClearButtonComponent);
  }

  ngAfterViewInit(): void {
    if (this.appClearable) {
      this.vcr.createEmbeddedView(this.appClearable);
    } else {
      this.vcr.createComponent(this.factory, undefined, this.injector);
    }
  }

  /**
   * This is used to clear the formControl oder HTMLInputElement
   */
  clear(): void {
    if (this.matInput.ngControl) {
      this.matInput.ngControl.control.reset();
    } else {
      this.matInput.value = '';
    }
  }
}

/**
 * This is the markup/component for the clear-button that is shown to the user.
 */
@Component({
  selector: 'app-clear-button',
  template: `
  <button (click)="clearHost()">Clear</button>
  `
})
export class ClearButtonComponent {
  constructor(@Optional() @SkipSelf() private clearDirective: ClearableDirective) { }

  clearHost(): void {
    if (this.clearDirective) {
      this.clearDirective.clear();
    }
  }
}

This creates a directive called appClearable and an optional Component for a fallback-layout. Make sure to add the component and the directive to the declarations-array of your module. You can either specify a template to use for providing the user-interface or just use the ClearButtonComponent as a one-size-fits-all solution. The markup looks like this:

<!-- Use it with a template reference -->
<mat-form-field [appClearable]="clearableTmpl">
  <input type="text" matInput [formControl]="exampleInput">
</mat-form-field>

<!-- use it without a template reference -->
<mat-form-field appClearable>
  <input type="text" matInput [formControl]="exampleInput2">
</mat-form-field>

<ng-template #clearableTmpl>
  <button (click)="exampleInput.reset()">Marked-Up reference template</button>
</ng-template>

This works with and without a ngControl/FormControl, but you might need to adjust it to your use-case.

1
votes

Update:

Option 1 does not work for new angular versions because @ViewChild() returns undefined in ngOnInit() hook. Another hack is to use a dummy MatFormFieldControl -

Option 2

<mat-form-field>
  <input matInput hidden>
  <ng-content></ng-content>
</mat-form-field>

Edit:

That error is thrown because MatFormField component queries the child content using @ContentChild(MatFormFieldControl) which does not work if you use nested ng-content (MatFormField also uses content projection).

Option 1 (deprecated)

Below is how you can make it work -

@Component({
  selector: 'mat-clearable-input',
  template: `
    <mat-form-field>
      <ng-content></ng-content>
    </mat-form-field>
  `
})
export class FieldComponent implements OnInit { 
    @ContentChild(MatFormFieldControl) _control: MatFormFieldControl<any>;
    @ViewChild(MatFormField) _matFormField: MatFormField;

    ngOnInit() {
        this._matFormField._control = this._control;
    }
}

Please checkout this stackBlitz. Also, there is this issue created in github already.