Angular ng-content not working with mat-form-field - javascript

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)

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.

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.

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);
}
}

Related

Is there a way to control how pressing the TAB key works on angular?

Can i change the behaviour when moving through elements in my html page with tab? Like which element is the first one to be focused when pressing TAB key?
FYI the SPA app is created using Angular as a framework.
Well:
I know the existence of tabindex but it does not work when using a custom UI like ng-zorro which is the one i am using.
I think that ng-zorro is overriding tabindex or tab property behavior.(there is something in their github file...)
I could get the element and from my ts do something with onFocus() ?
I honeslty do not know any help?
EDIT
If you want there is ng-zorro-select with which you can try to use any of the would work they have a stackblitz for every example... So say you have three ng-zorro select how do you set the tab order?
stackblitz
Just create a viewChild and trigger focus method, if you want it inbuilt, raise a request in the ngzorro github repo.
ts
import { ChangeDetectorRef, Component, ViewChild } from '#angular/core';
import { NzSelectComponent } from 'ng-zorro-antd/select';
#Component({
selector: 'nz-demo-select-search',
template: `
<nz-select #firstFocus nzShowSearch nzAllowClear nzPlaceHolder="Select a person" [(ngModel)]="selectedValue">
<nz-option nzLabel="Jack" nzValue="jack"></nz-option>
<nz-option nzLabel="Lucy" nzValue="lucy"></nz-option>
<nz-option nzLabel="Tom" nzValue="tom"></nz-option>
</nz-select>
`,
styles: [
`
nz-select {
width: 200px;
}
`,
],
})
export class NzDemoSelectSearchComponent {
#ViewChild('firstFocus', { static: true }) firstFocus: NzSelectComponent;
selectedValue = null;
constructor(private cdr: ChangeDetectorRef) {}
ngAfterViewInit() {
this.firstFocus.focus();
this.cdr.detectChanges();
}
}
forked stackblitz

How to make a Value to lowercase in mat-form

I have a mat-form from where I want user to enter some values and then I want to submit those values.However I want one of the field to be converted to lowercase and then be submitted.How can I achieve that.
HTML Code:
<mat-form-field appearance="fill">
<mat-label>Hive Table</mat-label>
<input
matInput
formControlName="hiveTable"
/>
</mat-form-field>
Typescript Code:
this.generalInfoForm = new FormGroup({
hiveTable: new FormControl('', [Validators.required]),
});
What I have tried so far
<mat-label>Hive Table</mat-label>
<input
matInput
oninput="this.value = this.value.toLowerCase()"
(keyup.enter)="sendit($event.target.value)"
formControlName="hiveTable"
/>
</mat-form-field>
I used oninout and using keyup I did console.log and in console the values are getting converted tolowercase however on submitting it the value changes back to the way the user typed.
What am I doing wrong.
why not use in input
style="text-transform: lowercase"
Then, before submit you can (I imagine you has a function submit())
submit(form:FormGroup)
{
if (form.valid)
{
form.value.yourField=form.value.yourField.toLowerCase()
console.log(form.value)
}
}
You can use a Directive as shown below:
import { Directive, HostListener, ElementRef, Renderer2 } from '#angular/core';
#Directive({
selector: '[lowercase]'
})
export class LowercaseDirective {
constructor(private renderer: Renderer2, private elementRef: ElementRef) {}
#HostListener('keyup') onKeyUp() {
const nativeElement = this.elementRef.nativeElement;
nativeElement.value = nativeElement.value.toLowerCase();
}
}
And in your template, this needs to be defined as follows:
<input matInput ... lowercase/>
Please have a look at this StackBlitz

Error: Template parse errors: There is no directive with "exportAs" set to "matAutocomplete" (""auto" [formControl] ="..."

I am writing unit test to check if component is getting created successfully. I see the following error
Error: Template parse errors: There is no directive with "exportAs" set to "matAutocomplete" (""auto" [formControl
This is my template.html which contains auto-complete directive
<mat-form-field >
<input matInput [matAutocomplete]="auto" [formControl]="customerFilterControl">
<mat-autocomplete panelWidth ="450px" #auto="matAutocomplete" [displayWith] = "displayFn" style="width:750px;">
<mat-option *ngFor="let customer of filteredOptions | async" [value] ="customer.AccountID + '('+ customer.AccountName + ')'" (click)="onCustomerChange(customer)">
{{customer.AccountID}} ({{customer.AccountName}})
</mat-option>
</mat-autocomplete>
</mat-form-field>
This is the unit test spec.file, I have tried the following things,
import { ActualComponent } from './ActualComponent';
import { NO_ERRORS_SCHEMA,CUSTOM_ELEMENTS_SCHEMA,Directive } from '#angular/core';
#Directive({
selector:'<matAutocomplete>',
})
export class matAutocomplete{}
beforeEach(()=>{
TestBed.configureTestingModule({
imports:[....],
declarations:[...matAutocomplete],(1)
...
schemas:[NO_ERRORS_SCHEMA,CUSTOM_ELEMENTS_SCHEMA] //this didn't fix (2)
})
it('should create', () => {
expect(component).toBeTruthy();
})
I expected test to pass for (1) defining directive "matAutocomplete" and declaring in spec file (2) decalring schemas in testbed config But still test is not passing! Does anyone have suggestion for me?
The selector <matAutocomplete> will not work. Try to use [matAutoComplete] and add a #Input() matAutocomplete to the class. Next step would be to add exportAs to the directive decorator:
#Directive({
selector:'[matAutocomplete]',
exportAs: 'matAutocomplete'
})
export class matAutocomplete {
#Input() matAutocomplete: any;
}
UPDATE
May be it would be better to test this with the Angular Material Modules imported. Otherwise the test does not really test anything relevant.
Can you try the following:
1)
import {ReactiveFormsModule} from '#angular/forms';
import {MatAutocompleteModule} from '#angular/material/autocomplete';
import {MatInputModule} from '#angular/material/input';
import {MatSelectModule} from '#angular/material/select';
import {MatFormFieldModule} from '#angular/material/form-field';
...
// inside beforeEach:
TestBed.configureTestingModule({
imports:[ReactiveFormsModule,
MatAutocompleteModule, MatInputModule,
MatSelectModule, MatFormFieldModule],
declarations:[AppComponent], // add your component instead of AppComponent
// schemas:[NO_ERRORS_SCHEMA,CUSTOM_ELEMENTS_SCHEMA]
});
2) Remove the custom matAutocomplete directive.

How to set auto focus in mat-select?

In my angular project have angular material and use mat-select. Mat-select is the first element for my form in my case set auto focus while page was loaded successfully but I wasn't able to set auto focus on mat-select. Anyone can help me to find the way to set auto focus in mat-select.
#ViewChild("name") nameField: ElementRef;
ngOninit() {
this.nameField.nativeElement.focus();
}
html
<div>
<mat-select [(ngModel)]="nameField" #name>
<mat-option *ngFor="let option of options2" [value]="option.id">
{{ option.name }}
</mat-option>
</mat-select>
</div>
HTML :
<mat-select #someRef >
<mat-option *ngFor="let item of items;" [value]="item">
{{item.name}}
</mat-option>
</mat-select>
.ts :
make sure you import MatSelect
import { MatSelect } from '#angular/material';
#ViewChild('someRef') someRef: MatSelect;
ngOnInit() {
if(this.someRef) this.someRef.focus();
}
Hope this helps.
If I understand it correctly, you want to focus select element on load. If this is the case, your code is perfectly fine, you just need to move focus logic in to another life cycle event which is
ngAfterViewInit
HTML:
<mat-select #fff>
<mat-option *ngFor="let food of foods" [value]="food.value">
{{food.viewValue}}
</mat-option>
</mat-select>
TS:
export class SelectOverviewExample implements AfterViewInit{
foods: Food[] = [
{value: 'steak-0', viewValue: 'Steak'},
{value: 'pizza-1', viewValue: 'Pizza'},
{value: 'tacos-2', viewValue: 'Tacos'}
];
#ViewChild("fff", {static: false}) nameField: ElementRef;
ngAfterViewInit() {
this.nameField.focused = true;
}
}
Find working demo here. You can see select is highlighted. comment code inside ngAfterViewInit() and see this difference.
As this is the First hit that shows up on Google I'll provide what I found:
Note that I did this specifically for a mat-select as there is no real inner html element that the reference could be attached to.
What I found works is getting a reference to the element through view-child and then calling
reference._elementRef.nativeElement.focus();
Hope this helps at least someone :)
We can use default angular attribute for autofocus
<mat-form-field>
<mat-select formControlName="xyz" cdkFocusInitial>
<mat-option value="abc">Abc</mat-option>
</mat-select>
</mat-form-field>
Try using MatSelect on viewChild to access focused attribute, then onInit set it to true.
<mat-form-field>
<mat-select #mySelect [(ngModel)]="nameField">
<mat-option *ngFor="let option of options2" [value]="option.id">{{ option.name }}
</mat-option>
</mat-select>
</mat-form-field>
and ts file import import { MatSelect } from '#angular/material';
import { MatSelect } from '#angular/material';
export class SelectExample implements OnInit {
#ViewChild(MatSelect) mySelect: MatSelect;
ngOnInit() {
this.mySelect.focused = true;
}
}
You can call the focus on OnInit
ts:
options2 = ['A', 'B'];
#ViewChild('name')
nameField: MdSelect;
ngOnInit() {
setTimeout(() => {
this.nameField.open();
}, 0);
}
html:
<div>
<md-select [(ngModel)]="nameField" #name>
<md-option *ngFor="let option of options2" [value]="option.id">{{ option }}</md-option>
</md-select>
EDIT: Sorry, I think you can not get the nativeElement from mat-select and md-select. You need to get the object and call open().
Workning project here in stackblitz
First, let’s create the Directive
auto-focus.directive.ts
import { AfterContentInit, Directive, ElementRef, Input } from '#angular/core';
#Directive({
selector: '[autoFocus]' }) export class AutofocusDirective implements AfterContentInit {
public constructor(private el: ElementRef) {
}
public ngAfterContentInit() {
setTimeout(() => {
this.el.nativeElement.focus();
}, 500);
}
}
Next we need to tell our AppModule that this new directive exists and to declare it for availability by updating our app.module.ts :
#NgModule({
declarations: [
AutoFocusDirective
]
})
Now you can use it in a component template:
app.component.html
<div> Autofocus? <input appAutoFocus> </div>
You can adapt this example to your own project. Clicking on the button becomes focus.
focusing on form elements the Angular way
show more

Angular Reactive form rendering issue breaking the form

So I am having a very weird problem with Angular reactive form. I asked my instructor, he couldn't figure it out so I have only one place I am wishing to get some help from. It's here.
So I am using a Angular form and the signup.component.html code snippet is:
<form [formGroup]="form" (submit)="onSaveUser()" *ngIf="!isLoading">
<mat-accordion class="accordion-headers-align">
<mat-expansion-panel [expanded]="1">
<mat-expansion-panel-header>
<mat-panel-title>Personal data</mat-panel-title>
<mat-panel-description>Type your personal details</mat-panel-description>
</mat-expansion-panel-header>
<mat-form-field>
<input matInput type="text" formControlName="name" placeholder="Name">
<mat-error *ngIf="form.get('name').invalid">Please enter your name</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput type="date" formControlName="dob" placeholder="DD/MM/YYYY">
<mat-error *ngIf="form.get('dob').invalid">Please enter your valid date of birth in form of DD/MM/YYYY</mat-error>
</mat-form-field>
and continue like that, ignore the accordion part pls.
Then my signup.component.ts file is:
import { Component, OnInit } from '#angular/core';
import { FormGroup, FormControl, Validators } from '#angular/forms';
import { ActivatedRoute, ParamMap } from '#angular/router';
#Component({
selector: 'app-signup',
templateUrl: './signup.component.html',
styleUrls: ['./signup.component.css']
})
export class SignupComponent implements OnInit {
isLoading = false;
form: FormGroup;
imagePreview: string;
constructor(public userService: UsersService, public route: ActivatedRoute) { }
ngOnInit() {
this.form = new FormGroup({
name: new FormControl(null, {validators: [
Validators.required,
Validators.minLength(3),
// tslint:disable-next-line:quotemark
Validators.pattern(some regex not of concern)
]}),
dob: new FormControl(null, {validators: [
Validators.required,
// tslint:disable-next-line:max-line-length quotemark
Validators.pattern(some regex not of concern)
]}),
and continued like this, nothing special. Just trying to map the form fields. So the form renders like this: The error I am getting in console is:
ERROR TypeError: "this.form is undefined, can't access property "get" of it".
ERROR Error: "formGroup expects a FormGroup instance. Please pass one in.
ERROR TypeError: "_co.form is undefined, can't access property "get" of it".
and I don't understand what is going wrong, I checked the documentation and everything, no help. I am guessing it's a possible bug so wanted to make sure.
I resolve with *ngIf="form", in this way form tag will be rendered only when form is ready.
<form [formGroup]="form" (submit)="onSaveUser()" *ngIf="form">

Categories

Resources