Reputation: 809
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://stackoverflow.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)
Upvotes: 4
Views: 5304
Reputation: 2085
As of Angular 14 in 2022, the issue for the MatFormField has been closed against angular/angular#37319, without a solution. If you need this to work, the following seems to be the best solution that's possible now to use <ng-content>
with mat-form-field
:
@Component({
selector: 'my-input-wrapper',
template: `
<mat-form-field appearance="standard">
<ng-content></ng-content>
<!-- make sure this is destroyed so all bindings/subscriptions are removed-->
<input *ngIf="isBeforeViewInit$$ | async" hidden matInput />
</mat-form-field>
`,
});
class MyInputWrapper implement AfterViewInit {
isBeforeViewInit$$ = new BehaviorSubject(true);
@ContentChild(MatFormFieldControl) matFormControl: MatFormFieldControl<unknown>;
@ViewChild(MatFormField) matFormField: MatFormField;
ngAfterViewInit() {
// replace the reference to the dummy control
this.matFormField._control = this.matFormControl;
// force the form field to rebind everything to the actual control
this.matFormField.ngAfterContentInit();
this.isBeforeViewInit$$.next(false);
}
}
Upvotes: 1
Reputation: 496
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.
Upvotes: 1
Reputation: 4612
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.
Upvotes: 1