I am refactoring an Angular component developed by a predecessor.
Previously there was an edit pop up in the user profile component. I am extracting this functionality to a new component.
In profile directory so there was expert-profile component
, I added a new component named edit-profile component
which only supports the edit pop up. I used the same html implementation as seen in the expert-profile.component.html
file.
The problem is that none of the mat tags are rendering.
Here is a screenshot in which the edit popup was rendering correctly.
Here is a screenshot of the edit popup after refactoring.
On failure, the following error is thrown:
ERROR Error: mat-form-field must contain a MatFormFieldControl.
This error did not occur when the edit pop up was implemented in expert-profile.
So, where is the problem ?
My code:
edit-profile.component.html:
<!----------------------------------------------------------
POPUP EDIT PROFILE
----------------------------------------------------------->
<div class="editPopup" *ngIf="editMode">
<mat-card>
<div class="updateLoading" *ngIf="updatingProfile">
<mat-spinner></mat-spinner>
</div>
<h1>
{{
this.translateService.getTranslation(
this.translatePage,
'profileEdition'
)
}}
</h1>
<!----------------------------------------
F O R M
----------------------------------------->
<form [formGroup]="editProfileForm" (ngSubmit)="updateExpert(editProfileForm.value)">
<p>
<!----------------------------------------
PERSONAL INFORMATIONS
----------------------------------------->
<!-- title -->
<span>
{{
this.translateService.getTranslation(
this.translatePage,
'personalDatas'
)
}}
</span>
<mat-divider></mat-divider>
<!-- content & input -->
<!-- lastName -->
<mat-form-field>
<!-- <mat-label>
{{
this.translateService.getTranslation(
this.translatePage,
'lastName'
)
}}
</mat-label> -->
<label>
{{
this.translateService.getTranslation(
this.translatePage,
'lastName'
)
}}
</label>
<input matInput placeholder="{{
this.translateService.getTranslation(
this.translatePage,
'lastName'
)
}}" formControlName="lastName" value="{{ this.userInfos.lastName }}" />
</mat-form-field>
<!-- firstName -->
<mat-form-field>
<mat-label>
{{
this.translateService.getTranslation(
this.translatePage,
'firstName'
)
}}
</mat-label>
<input matInput placeholder="{{
this.translateService.getTranslation(
this.translatePage,
'firstName'
)
}}" formControlName="firstName" value="{{ this.userInfos.firstName }}" />
</mat-form-field>
<!-- language -->
<mat-form-field>
<mat-label>
{{
this.translateService.getTranslation(
this.translatePage,
'languages'
)
}}
</mat-label>
<mat-select multiple [(ngModel)]="this.userInfos.languages" [ngModelOptions]="{ standalone: true }">
<mat-option *ngFor="let language of this.allLanguages" value="{{ language }}">
{{
this.translateService.getTranslation(
this.languagePage,
language
)
}}
</mat-option>
</mat-select>
</mat-form-field>
<br /><br />
<!----------------------------------------
CONTACT INFORMATIONS
----------------------------------------->
<!-- title -->
<span>
{{
this.translateService.getTranslation(
this.translatePage,
'contactInfos'
)
}}
</span>
<mat-divider></mat-divider>
<mat-form-field>
<mat-label>
{{
this.translateService.getTranslation(
this.translatePage,
'email'
)
}}
</mat-label>
<input matInput placeholder="{{
this.translateService.getTranslation(
this.translatePage,
'email'
)
}}" type="email" formControlName="email" value="{{ this.userInfos.email }}" />
</mat-form-field>
<mat-form-field>
<mat-label>
{{
this.translateService.getTranslation(
this.translatePage,
'phone'
)
}}
</mat-label>
<input matInput placeholder="{{
this.translateService.getTranslation(
this.translatePage,
'phone'
)
}}" type="tel" formControlName="phone" value="{{ this.userInfos.phone }}" />
</mat-form-field>
<br /><br />
<!----------------------------------------
COMPANY
----------------------------------------->
<!-- title -->
<span>
{{
this.translateService.getTranslation(
this.translatePage,
'company'
)
}}
</span>
<mat-divider></mat-divider>
<mat-form-field>
<mat-label>
{{
this.translateService.getTranslation(
this.translatePage,
'company'
)
}}
</mat-label>
<input matInput placeholder="{{
this.translateService.getTranslation(
this.translatePage,
'company'
)
}}" formControlName="company" value="{{ this.userInfos.company }}" />
</mat-form-field>
<mat-form-field>
<mat-label>
{{
this.translateService.getTranslation(
this.translatePage,
'title'
)
}}
</mat-label>
<input matInput placeholder="{{
this.translateService.getTranslation(
this.translatePage,
'title'
)
}}" formControlName="title" value="{{ this.expert.title }}" />
</mat-form-field>
<mat-form-field appearance="legacy">
<mat-label>{{
this.translateService.getTranslation(
this.translatePage,
'expertWage'
)
}}</mat-label>
<input matInput placeholder="{{
this.translateService.getTranslation(
this.translatePage,
'expertWage'
)
}}" type="number" formControlName="wage" value="{{ this.expert.wage }}" />
</mat-form-field>
<!-- <mat-form-field style="width: 90%;">
<mat-chip-list #chipList aria-label="Software selection">
<mat-chip *ngFor="let software of this.currentSoftwares" [selectable]="true" [removable]="true"
(removed)="remove(software)" disabled="{{ this.updatingProfile }}">
{{ software }}
<mat-icon matChipRemove>cancel</mat-icon>
</mat-chip>
<input placeholder="{{
this.translateService.getTranslation(
this.translatePage,
'softwaresSelection'
)
}}" #softInput [formControl]="this.softwareCtrl" [matAutocomplete]="auto" [matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes" (matChipInputTokenEnd)="add($event)" />
</mat-chip-list>
<mat-autocomplete #auto="matAutocomplete" (optionSelected)="selected($event)">
<mat-option *ngFor="let software of this.filteredSoftwares | async" [value]="software">
{{ software }}
</mat-option>
</mat-autocomplete>
</mat-form-field> -->
<br /><br />
<!----------------------------------------
LOCATION
----------------------------------------->
<!-- title -->
<span>{{
this.translateService.getTranslation(this.translatePage, 'location')
}}</span>
<mat-divider></mat-divider>
<mat-form-field appearance="legacy">
<mat-label>{{
this.translateService.getTranslation(
this.translatePage,
'citizenship'
)
}}</mat-label>
<input matInput placeholder="{{
this.translateService.getTranslation(
this.translatePage,
'citizenship'
)
}}" formControlName="citizenship" value="{{ this.userInfos.citizenship }}" />
</mat-form-field>
<mat-form-field appearance="legacy">
<mat-label>{{
this.translateService.getTranslation(
this.translatePage,
'country'
)
}}</mat-label>
<input matInput placeholder="{{
this.translateService.getTranslation(
this.translatePage,
'country'
)
}}" formControlName="country" value="{{ this.userInfos.country }}" />
</mat-form-field>
<mat-form-field appearance="legacy">
<mat-label>{{
this.translateService.getTranslation(this.translatePage, 'city')
}}</mat-label>
<input matInput placeholder="{{
this.translateService.getTranslation(this.translatePage, 'city')
}}" formControlName="city" value="{{ this.userInfos.city }}" />
</mat-form-field>
<br />
<mat-form-field appearance="legacy">
<mat-label>{{
this.translateService.getTranslation(this.translatePage, 'postal')
}}</mat-label>
<input matInput placeholder="{{
this.translateService.getTranslation(
this.translatePage,
'postal'
)
}}" formControlName="address" value="{{ this.userInfos.address }}" />
</mat-form-field>
<mat-form-field appearance="legacy">
<mat-label>{{
this.translateService.getTranslation(
this.translatePage,
'zipCode'
)
}}</mat-label>
<input matInput placeholder="{{
this.translateService.getTranslation(
this.translatePage,
'zipCode'
)
}}" type="number" formControlName="zipCode" value="{{ this.userInfos.zipCode }}" />
</mat-form-field>
<br /><br />
<!----------------------------------------
COMPETENCIES
----------------------------------------->
<!-- title -->
<span>{{
this.translateService.getTranslation(
this.translatePage,
'competencies'
)
}}</span>
<mat-divider></mat-divider>
<mat-card class="competencies">
<mat-checkbox color="primary" class="competenciesCheck" formControlName="aerospace">
Aerospace
</mat-checkbox>
<mat-checkbox color="primary" class="competenciesCheck" formControlName="energy">
Energy
</mat-checkbox>
<mat-checkbox color="primary" class="competenciesCheck" formControlName="transport">
Transport
</mat-checkbox>
<mat-checkbox color="primary" class="competenciesCheck" formControlName="industries">
Industries
</mat-checkbox>
</mat-card>
</p>
<div class="submitButtons">
<button mat-button (click)="CloseDialog()" #cancelEditButton>
{{
this.translateService.getTranslation(this.translatePage, 'cancel')
}}
</button>
<button mat-button color="primary" type="submit" #applyEditButton>
{{
this.translateService.getTranslation(this.translatePage, 'modify')
}}
</button>
</div>
</form>
</mat-card>
</div>
edit-profile.component.ts:
// import from libraries angular & RxJS
import {
Component,
ElementRef,
Input,
OnInit,
Output,
ViewChild,
EventEmitter,
ViewEncapsulation,
ɵConsole,
Inject,
} from '@angular/core';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { Observable } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { Router } from '@angular/router';
import { FormBuilder, FormControl, FormGroup } from '@angular/forms';
// import from material
import {
MatAutocomplete,
MatAutocompleteSelectedEvent,
} from '@angular/material/autocomplete';
import {
MatDialog,
MatDialogRef,
MAT_DIALOG_DATA,
} from '@angular/material/dialog';
import { MatButton } from '@angular/material/button';
import { MatChipInputEvent } from '@angular/material/chips';
import { MatSnackBar } from '@angular/material/snack-bar';
import {MatFormFieldModule} from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
// import from app
import { Expert } from 'src/app/interfaces/expert/expert';
import {
ComponentsTrslt,
TranslationService,
} from 'src/app/services/translation/translation.service';
import { ExpertService } from 'src/app/services/expert/expert.service';
import { UserService } from 'src/app/services/user/user.service';
import { ProjectService } from 'src/app/services/project/project.service';
import { UserInformation } from 'src/app/interfaces/user-information/user-information';
import { Project } from 'src/app/interfaces/project/project';
import { LanguageService } from 'src/app/services/language/language.service';
import { SoftwareService } from 'src/app/sub-modules/profile/services/software/software.service';
import { ExpertProfileComponent } from 'src/app/sub-modules/profile/shared/expert-profile/expert-profile.component';
import { Globals } from 'src/app/globals/globals';
import { UserProject } from 'src/app/interfaces/user-projects/user-project';
import { environment } from 'src/environments/environment';
import { AreaEnum } from 'src/app/sub-modules/profile/interfaces/area-enum/area-enum.enum';
import { FileManipulationsService } from 'src/app/services/file-operations/file-manipulations.service';
import { FavoritesService } from 'src/app/sub-modules/profile/services/favorites/favorites.service';
import { ExpertiseService } from 'src/app/sub-modules/profile/services/expertise/expertise.service';
import { ExpertisesEnums } from 'src/app/sub-modules/profile/interfaces/expertises/expertises.enum';
@Component({
selector: 'app-edit-profile',
templateUrl: './edit-profile.component.html',
styleUrls: ['./edit-profile.component.less'],
})
export class EditProfileComponent implements OnInit {
// app translation management
public translatePage: ComponentsTrslt = ComponentsTrslt.PROFILE;
public languagePage: ComponentsTrslt = ComponentsTrslt.LANGUAGE;
// pop up buttons
private cancelInput: MatButton;
@ViewChild('cancelEditButton', { static: false }) set cancelContent(
cancelContent: MatButton
) {
if (cancelContent) {
this.cancelInput = cancelContent;
}
}
private applyInput: MatButton;
@ViewChild('applyEditButton', { static: false }) set applyContent(
applyContent: MatButton
) {
if (applyContent) {
this.applyInput = applyContent;
}
}
private softInput: ElementRef<HTMLInputElement>;
@ViewChild('softInput', { static: false }) set content(
content: ElementRef<HTMLInputElement>
) {
if (content) {
// initially setter gets called with undefined
this.softInput = content;
}
}
// user info
userInfos: UserInformation;
// @ViewChild('auto', { static: false }) matAutocomplete: MatAutocomplete;
// edit pop up
@Input() profileId: string;
public ownProfile: boolean = null; // Modify it to display edit/addFav button
public editMode = false; // Used to display edit popup
public updatingProfile = false; // Used to display spinner when updating profile after edit
public connectedIsExpert: boolean = null; // Modify it to display edit/addFav button
editProfileForm: FormGroup;
expert: Expert;
public allAreas = []; // Array to display all areas in profile Edition
public allLanguages = []; // Array to display all languages in profile Edition
separatorKeysCodes: number[] = [ENTER, COMMA];
public displayedSoft: string[] = [];
public allSoftwares: string[] = [];
softwareCtrl = new FormControl();
filteredSoftwares: Observable<string[]>;
// Used to show 'about' informations more simply
// Need to load data in async
public expertDatas = [
{ label: 'company', data: '-' },
{ label: 'phone', data: '-' },
{ label: 'email', data: '-' },
{ label: 'location', data: '-' },
{ label: 'citizenship', data: '-' },
{ label: 'softwares', data: [] },
{ label: 'languages', data: [] },
];
// 'Hardcoded' enum, need to fill the "data" field with db datas
public expertises = ExpertisesEnums;
public currentSoftwares: string[] = [];
constructor(
public dialogRef: MatDialogRef<EditProfileComponent>,
public translateService: TranslationService,
private userService: UserService,
private expertService: ExpertService,
public expertiseService: ExpertiseService,
public languageService: LanguageService,
private softwareService: SoftwareService,
private formBuilder: FormBuilder
) {
this.editProfileForm = this.formBuilder.group({
lastName: [{ value: '', disabled: true }],
firstName: '',
email: '',
phone: '',
company: '',
title: '',
wage: '',
citizenship: '',
country: '',
city: '',
address: '',
zipCode: '',
softwares: [],
aerospace: false,
energy: false,
transport: false,
industries: false,
});
}
ngOnInit(): void {
this.opClEditMode();
// this.editMode = true;
this.profileId = localStorage.getItem(Globals.USER_ID);
this.ownProfile = true;
// get data about user connected
// Replace by request to get only if user is expert
this.userService
.getPartial(localStorage.getItem(Globals.USER_ID))
.subscribe((user) => {
console.log('#1 user: ( this.userService.getPartial )', user);
this.connectedIsExpert = user.expertId != null;
console.log(
'#1 connectedIsExpert: ( this.userService.getPartial )',
this.connectedIsExpert
);
});
this.getProfileInfo();
// test
console.log(' #ONINIT - editmode: ', this.editMode);
// console.log('this.ownProfile: ', this.ownProfile);
console.log(' #ONINIT - this.profileId: ', this.profileId);
// console.log('localStorage.getItem(Globals.USER_ID) :', localStorage.getItem(Globals.USER_ID));
console.log(' #ONINIT - this.connectedIsExpert: ', this.connectedIsExpert);
console.log(' #ONINIT - this.translatePage: ', this.translatePage);
console.log(' #ONINIT - this.languagePage: ', this.languagePage);
}
// update data about expert connected
public updateExpert(formData) {
const areasArray = [];
if (formData.aerospace !== false) {
areasArray.push('AEROSPACE');
}
if (formData.energy !== false) {
areasArray.push('ENERGY');
}
if (formData.transport !== false) {
areasArray.push('TRANSPORT');
}
if (formData.industries !== false) {
areasArray.push('INDUSTRIES');
}
const str = formData.phone;
const matches = str.replace(/\D/g, '');
const newUserInfo: UserInformation = {
lastName: formData.lastName,
firstName: formData.firstName,
email: formData.email,
phone: matches,
company: formData.company,
citizenship: formData.citizenship,
country: formData.country,
city: formData.city,
address: formData.address,
zipCode: formData.zipCode,
image: this.userInfos.image,
softwares: formData.softwares,
languages: this.userInfos.languages,
areas: areasArray,
};
this.updatingProfile = true;
this.applyInput.disabled = true;
this.cancelInput.disabled = true;
this.userService.updateUser(this.profileId, newUserInfo).subscribe((_) => {
const newExpertInfo: Expert = {
title: formData.title,
wage: formData.wage,
rating: null,
};
this.expertService
.updateUser(this.expert.id, newExpertInfo)
.subscribe((expert) => {
this.opClEditMode();
this.updatingProfile = false;
this.applyInput.disabled = false;
this.cancelInput.disabled = false;
setTimeout(() => {
this.getProfileInfo();
}, 2);
});
});
}
// open - close dialog
CloseDialog(): void {
this.dialogRef.close();
}
opClEditMode() {
this.editMode = !this.editMode;
console.log('this.editMode: (opClEditMode()) ', this.editMode); // test
if (this.editMode) {
this.getProfileInfo();
}
}
// to fill input with data allready registered in db
getProfileInfo() {
console.log('this.profileId: ( getProfileInfo() )', this.profileId);
this.userService
.getPartial(this.profileId)
.subscribe((user: UserInformation) => {
this.userInfos = user;
console.log('this.userInfos: ( getProfileInfo() ) ', this.userInfos); // test
this.expertDatas[0].data = user.company;
this.expertDatas[1].data = user.phone;
this.expertDatas[2].data = user.email;
this.expertDatas[3].data = user.address;
this.expertDatas[4].data = user.citizenship;
this.editProfileForm.setValue({
lastName: this.userInfos.lastName,
firstName: this.userInfos.firstName,
email: this.userInfos.email,
phone: this.userInfos.phone,
company: this.userInfos.company,
citizenship: this.userInfos.citizenship,
country: this.userInfos.country,
city: this.userInfos.city,
address: this.userInfos.address,
zipCode: this.userInfos.zipCode,
title: '',
wage: '',
softwares: '',
aerospace: false,
energy: false,
transport: false,
industries: false,
});
// Get Expert Info
this.expertService
.getByUserId(this.profileId)
.subscribe((expert: Expert) => {
this.expert = expert;
this.expertises = this.expertiseService.getProfileExpertises(
this.expert.id
);
// Get User Languages info
this.languageService
.getLanguagesByUserId(this.profileId)
.subscribe((e) => {
this.userInfos.languages = e.map((elem) => elem.name);
this.expertDatas[6].data = this.userInfos.languages;
// Get USer Softwares Info
this.softwareService
.getSoftwaresByUserId(this.profileId)
.subscribe((k) => {
this.userInfos.softwares = k.map((elem) => elem.name);
this.expertDatas[5].data = this.userInfos.softwares;
this.currentSoftwares = this.userInfos.softwares;
// this.noSoftDuplicate();
// Get User "Areas" info.
this.expertService
.getAreasNames(this.profileId)
.subscribe((userAreas) => {
this.userInfos.areas = userAreas;
this.editProfileForm.patchValue({
title: this.expert.title,
wage: this.expert.wage,
softwares: this.userInfos.softwares,
});
// Here we prefill the form with user info, there are obviously better ways to do this, but the with the
// current implementation of user "Areas" this is easier for now. We would have to manually add areas here
// (if we increase the diversity of areas in the DB) and at several other places in this file.
if (userAreas.includes('AEROSPACE')) {
this.editProfileForm.patchValue({
aerospace: true,
});
}
if (userAreas.includes('ENERGY')) {
this.editProfileForm.patchValue({
energy: true,
});
}
if (userAreas.includes('TRANSPORT')) {
this.editProfileForm.patchValue({
transport: true,
});
}
if (userAreas.includes('INDUSTRIES')) {
this.editProfileForm.patchValue({
industries: true,
});
}
});
});
});
});
});
}
add(event: MatChipInputEvent): void {
const input = event.input;
const value = event.value;
// Add our software
if ((value || '').trim()) {
this.currentSoftwares.push(value.trim());
}
// Reset the input value
if (input) {
input.value = '';
}
this.softwareCtrl.setValue(null);
this.noSoftDuplicate();
}
selected(event: MatAutocompleteSelectedEvent): void {
this.currentSoftwares.push(event.option.viewValue);
this.softInput.nativeElement.value = '';
this.softwareCtrl.setValue(null);
this.noSoftDuplicate();
}
remove(software: string): void {
const index = this.currentSoftwares.indexOf(software);
if (index >= 0) {
this.currentSoftwares.splice(index, 1);
}
this.noSoftDuplicate();
}
public noSoftDuplicate() {
this.displayedSoft = [];
this.displayedSoft = this.allSoftwares.map((software) => {
if (
this.currentSoftwares.find((e: string) => e === software) === undefined
) {
return software;
}
});
}
}
profile.module.ts
contains :
import { MatInputModule } from '@angular/material/input';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatFormFieldModule } from '@angular/material/form-field';
[...]
imports: [
CommonModule,
ProfileRoutingModule,
MatProgressSpinnerModule,
MatSlideToggleModule,
MatToolbarModule,
MatIconModule,
MatButtonModule,
MatInputModule,
MatCardModule,
MatProgressBarModule,
ReactiveFormsModule,
MatDividerModule,
MatSelectModule,
FormsModule,
MatAutocompleteModule,
MatChipsModule,
ImgFallbackModule,
MatCheckboxModule,
MatTooltipModule,
MatFormFieldModule,
],
})
export class ProfileModule {}