Angular MatFormField doesn't render

871 views Asked by At

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 {}
0

There are 0 answers