loading...
Cover image for 😲 Angular pages with Dynamic layouts!

😲 Angular pages with Dynamic layouts!

lampewebdev profile image Michael "lampe" Lazarski ・8 min read

⏳ 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

Posted on by:

lampewebdev profile

Michael "lampe" Lazarski

@lampewebdev

I'm a full-stack web developer. I love to help people.

Discussion

markdown guide
 

Thanks for your approach!
Also you should realize it prevents any lazy loading, and could end up with a big switch in your main component.

I usually rather go with sub-routing: basically you are re-implementing a routing facility. You could instead use <ng-router name="contents"></ng-router> and your routes would define the routes with the outlet property. Then you get best of both worlds and don't have to centralize a list of all possible layout components.

Hope it helps!

 

I did not know about that!

Do you maybe have a repo with an example implementation?

 

Nope unfortunately, only on a private project. But the official doc is pretty extensive, you'll figure things out very quickly. Might be a good follow-up article!

Basically, you do:

The "container" layout:

<my-layout...>
<ng-router name="contents"></ng-router>
...
</my-layout>

Then in your routes, you just add the outlet: 'contents' where you want to specify what should be projected in the contents placeholders.

I can have a look at that :D

As far as I remember I don't see a problem with lazy loading and my solution πŸ€”.
You just need to change the routes definitions.

  {
    path: 'SomePath',
    loadChildren: () => import('./something/someThing.module').then(m => m.someThingModule)
  },

I mean, your layouts cannot be lazy-loaded (nor cascaded, which is a nice feature).

In my app, I have several levels of such layouts. Let's say you have a layout:

--------- topbar ----------------------------
left menu  |  page_contents                      |

You can have a first layout:

topbar
router "contents"

and then in contents you can inject a layout with two router-outlets, left-menu & page_contents.

And the left menu + contents layout does not have to be loaded if you don't navigate to this part.

I hope it's clear :)

Ahh okay yeah :D
Sure that's also not bad.

My idea was more when you have completely different layouts like in the example.
You have your app layout and then you have a layout for login/register.
Which have not much in common

And yes then if you don't need the topbar it could be configured via routes too :)

Yup, especially in that case: I don't want to pull in the code for the login page module when the user is already logged in and won't hit the login page, if you see what I mean.

But anyway you're right, the cost is usually not that high with simple components like that.

 

Heya! Just a heads up that I think the link to the Vue article up top is acting a bit screwy. Cool article though! πŸ™‚

 

Thanks, and yeah the link was broken.
It is now fixed! Thanks πŸ‘

 
 

Is this not possible, if you put login component outside of main components... Means, one shell component having all other components and login component is alone..

 

I'm not sure if I understand your comment correctly πŸ€”

In general this is just an example.
Imagine you would have a login, register, reset password and new password component. Now you would use the same layout for all of them.

Now you want to have your logo in all 4 of them on the top right. You could change this simple in the layout file.

The same goes for the other component. In the example, we just have the dashboard but almost every other page could be in this layout.

Maybe you will need another layout for fullscreen pages without the menu.

So this is more like a starter for your app/page 😊