😲 Angular pages with Dynamic layouts!

Michael "lampe" Lazarski - Apr 19 '20 - - Dev Community

⏳ A few months ago I wrote an article about dynamic layouts in Vue.
Currently, I have the same problem but with Angular. I could not find one satisfying solution online. Most of them for me were not clear and a little bit messy.

😄 So here is a solution I'm satisfied with.

➡ Btw the Vue Article can be found here

Intro

We first need to set up a new Angular project. For that, we will use the Angular CLI. If you don't have Angular CLI installed you can do it with the following command:

npm install -g @angular/cli

We will now create our project with:

ng new dynamicLayouts

Now the CLI will ask if you want to add the Angular router and you need to say Yes by pressing Y.

Chose CSS for your stylesheet format.
After pressing enter Angular CLI will install all NPM packages. this can take some time.

We will also need the following package:

  • @angular/material
  • @angular/cdk
  • @angular/flex-layout

@angular/material is a component library that has a lot of material component based on the similar named Google design system.

We also want to use flexbox. @angular/flex-layout will help us with that.

We can install all of these packages with:

npm i -s @angular/cdk @angular/flex-layout @angular/material

Now we can start our dev server.

npm start

One thing I like to do first is to add the following lines to your tsconfig.json under the "compilerOptions".

  "compilerOptions": {
    "baseUrl": "src",
    "paths": {
      "@app/*": ["app/*"],
      "@layout/*": ["app/layout/*"]
    }
  }

With that we can import components and modules way easier then remembering the actual path.

We need to setup @angular/material a little bit more.
First, add the following to the src/style.css

html,
body {
  height: 100%;
}

body {
  margin: 0;
  font-family: Roboto, "Helvetica Neue", sans-serif;
}

Second, the src/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>DynamicLayouts</title>
    <base href="/" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <link rel="icon" type="image/x-icon" href="favicon.ico" />
    <link
      href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap"
      rel="stylesheet"
    />
    <link
      href="https://fonts.googleapis.com/icon?family=Material+Icons"
      rel="stylesheet"
    />
  </head>
  <body class="mat-typography">
    <app-root></app-root>
  </body>
</html>

I also like to create a single file for all the needed material components.
We need to create a new file in the src/app folder called material-modules.ts.

import { NgModule } from '@angular/core';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatCardModule } from '@angular/material/card';

@NgModule({
  exports: [
    MatToolbarModule,
    MatSidenavModule,
    MatButtonModule,
    MatIconModule,
    MatListModule,
    MatInputModule,
    MatFormFieldModule,
    MatCardModule,
  ],
})
export class MaterialModule {}

Now we can start to generate the modules and components we will need for this project.

The first component will be the dashboard.

ng g c dashboard
ng g m dashboard

Following this, we can create the login module and component.

ng g c login
ng g m login

We need one last module and two components.

ng g m layout
ng g c layout/main-layout
ng g c layout/centred-content-layout

app.module.ts now needs to be updated

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule, Routes } from '@angular/router';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { LoginModule } from './login/login.module';
import { RegisterModule } from './register/register.module';
import { DashboardModule } from './dashboard/dashboard.module';
import { LayoutModule } from './layout/layout.module';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    LayoutModule,
    AppRoutingModule,
    BrowserAnimationsModule,
    LoginModule,
    RegisterModule,
    DashboardModule,
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

This just stitches everything together.

We also need to create a app-routing.module.ts to set make our router work.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    redirectTo: '/dashboard',
    pathMatch: 'full',
  },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

The important lines here are the Routes lines.
Here we are defining that if you enter in your browser localhost:4200/ you will be redirected to the dashboard page.

The next file we need to update is app.component.ts

import { Component } from '@angular/core';
import { Router, RoutesRecognized } from '@angular/router';

export enum Layouts {
  centredContent,
  Main,
}

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
})
export class AppComponent {
  Layouts = Layouts;
  layout: Layouts;

  constructor(private router: Router) {}

  // We can't use `ActivatedRoute` here since we are not within a `router-outlet` context yet.
  ngOnInit() {
    this.router.events.subscribe((data) => {
      if (data instanceof RoutesRecognized) {
        this.layout = data.state.root.firstChild.data.layout;
      }
    });
  }
}

We are creating a enum for the different Layouts here and in the ngOnInit() we will set the right layout we want to use. Thats it!

The last file in the app folder we need to update is the app.component.html.

<ng-container [ngSwitch]="layout">
  <!-- Alternativerly use the main layout as the default switch case -->
  <app-main-layout *ngSwitchCase="Layouts.Main"></app-main-layout>
  <app-centred-content-layout
    *ngSwitchCase="Layouts.centredContent"
  ></app-centred-content-layout>
</ng-container>

This file is the placeholder for all of our layouts and we are usuing the ngSwitch/ngSwitchCase functinality to set the correct layout. In the actuall HTML we need to set the correct value from the enum. Thats it for the main app files.

We can now start to implement the layouts themself.
The src/app/layout/layout.module.ts file needs to look like this

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MainLayoutComponent } from './main-layout/main-layout.component';
import { CentredContentLayoutComponent } from './centred-content-layout/centred-content-layout.component';
import { RouterModule } from '@angular/router';
import { MaterialModule } from '@app/material-modules';
import { FlexLayoutModule } from '@angular/flex-layout';

@NgModule({
  imports: [
    CommonModule,
    RouterModule.forChild([]),
    MaterialModule,
    FlexLayoutModule,
  ],
  exports: [MainLayoutComponent, CentredContentLayoutComponent],
  declarations: [MainLayoutComponent, CentredContentLayoutComponent],
})
export class LayoutModule {}

The biggest take away here is that we need to declare and export the layouts themself. The rest is standard Angular boilerplate code.

Lets now implement the layouts HTML.
The src/app/layout/main-layout/main-layout.component.html should look like this

<div fxFlex fxLayout="column" fxLayoutGap="10px" style="height: 100vh;">
  <mat-sidenav-container class="sidenav-container">
    <mat-sidenav
      #sidenav
      mode="over"
      [(opened)]="opened"
      (closed)="events.push('close!')"
    >
      <mat-nav-list>
        <a mat-list-item [routerLink]="'/dashboard'"> Dashboard </a>
        <a mat-list-item [routerLink]="'/login'"> Login </a>
      </mat-nav-list>
    </mat-sidenav>

    <mat-sidenav-content style="height: 100vh;">
      <mat-toolbar color="primary">
        <button
          aria-hidden="false"
          aria-label="sidebar toogle button"
          mat-icon-button
          (click)="sidenav.toggle()"
        >
          <mat-icon>menu</mat-icon>
        </button>
      </mat-toolbar>
      <div fxLayout="column">
        App Content
        <router-outlet></router-outlet>
      </div>
    </mat-sidenav-content>
  </mat-sidenav-container>
</div>

This layout is your typical material app layout. With a navigation drawer that slides out and a topbar. The navigation also has a route to the login page.
We are usuing here @angular/material for all the components and @angular/flex-layout to layout our components. Nothing fance here.

The second layout called centred-content-layout. The only file we need to change here is centred-content-layout.component.html.

<div fxFlex fxLayout="row" fxLayoutAlign="center center" style="height: 100vh;">
  <router-outlet></router-outlet>
</div>

A very short layout since the only thing it has to do is to vertical and horizontal centre the content it will receive.

That's it! we have set up our layouts and we can use them now.

Now lets setup the dashboard first. In the dashboard component folder, we need to create a new file called dashboard-routing.module.ts

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { Layouts } from '@app/app.component';

const routes: Routes = [
  {
    path: 'dashboard',
    component: DashboardComponent,
    data: { layout: Layouts.Main },
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class DashboardRoutingModule {}

We are setting up the route for the dashboard. We are telling our app to use the Main layout.

In the dashboard.module.ts we need to import the DashboardRoutingModule.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { DashboardComponent } from './dashboard.component';
import { DashboardRoutingModule } from './dashboard-routing.module';

@NgModule({
  imports: [CommonModule, DashboardRoutingModule],
  declarations: [DashboardComponent],
})
export class DashboardModule {}

Now we just have to implement our login page.
lets first update the login.module.ts

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { LoginComponent } from './login.component';
import { LoginRoutingModule } from './login-routing.module';
import { MaterialModule } from '@app/material-modules';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { FlexLayoutModule } from '@angular/flex-layout';

@NgModule({
  declarations: [LoginComponent],
  imports: [
    CommonModule,
    LoginRoutingModule,
    MaterialModule,
    FormsModule,
    ReactiveFormsModule,
    FlexLayoutModule,
  ],
})
export class LoginModule {}

Again nothing special here just our standard angular boilerplate code.
The one new thing here is that we will be usuing the FormModule and ReactiveFormsModule. We need this for our form and validation. Which we will implement now.

The next file to change will be the login.component.html

<mat-card>
  <mat-card-content>
    <form>
      <h2>Log In</h2>
      <mat-form-field>
        <mat-label>Enter your email</mat-label>
        <input
          matInput
          placeholder="pat@example.com"
          [formControl]="email"
          required
        />
        <mat-error *ngIf="email.invalid">
          {{ getEmailErrorMessage() }}
        </mat-error>
      </mat-form-field>
      <mat-form-field>
        <mat-label>Enter your password</mat-label>
        <input
          matInput
          placeholder="My Secret password"
          [formControl]="password"
          required
        />
        <mat-error *ngIf="password.invalid">
          {{ getPasswordErrorMessage() }}
        </mat-error>
      </mat-form-field>
      <button mat-raised-button color="primary">Login</button>
    </form>
  </mat-card-content>
</mat-card>

This is a standard Form for a login interface. Again nothing special here. We will have some form validation and error messages. To make the validation work we need to update the login.component.ts.

import { Component, OnInit } from '@angular/core';
import { FormControl, Validators } from '@angular/forms';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css'],
})
export class LoginComponent implements OnInit {
  constructor() {}

  email = new FormControl('', [Validators.required, Validators.email]);
  password = new FormControl('', [
    Validators.required,
    Validators.minLength(8),
  ]);
  getEmailErrorMessage() {
    if (this.email.hasError('required')) {
      return 'You must enter a email';
    }

    return this.email.hasError('email') ? 'Not a valid email' : '';
  }

  getPasswordErrorMessage() {
    if (this.password.hasError('required')) {
      return 'You must enter a password';
    }

    return this.password.hasError('password') ? 'Not a valid password' : '';
  }

  ngOnInit(): void {}
}

We are setting up email validation. The user now needs to enter a valid e-mail.
Also, the password must be at least 8 characters. The rest is just boilerplate code where we are setting up the message.

One last thing we need to do is to create a login-routing.module.ts.

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { Layouts } from '@app/app.component';
import { LoginComponent } from './login.component';
const routes: Routes = [
  {
    path: 'login',
    component: LoginComponent,
    data: { layout: Layouts.centeredContent },
  },
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class LoginRoutingModule {}

This is almost the same file as in our dashboard example but it will now use the centredContent layout. If you have a copy and paste the login.module.ts then the LoginRoutingModule will be already imported.

That's it! We now have a way to create as many layouts as we want. We can also extend them and add more functionality without touching our page components.

I have also created a GitHub repo with the code. LINK

If you have any questions just ask down below in the comments!

Would you like to see a Video Tutorial for this tutorial?
Is there anything you want to know more of?
Should I go somewhere into details?
If yes please let me know!

That was fun!

👋Say Hello! Instagram | Twitter | LinkedIn | Medium | Twitch | YouTube

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player