😲 Angular pages with Dynamic layouts!
Tags: angular, typescript, webdev, beginners
Published: 2020-04-19 16:57:17

⏳ 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](https://dev.to/lampewebdev/vuejs-pages-with-dynamic-layouts-problems-and-a-solution-4460)

### 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:
```bash
npm install -g @angular/cli
```

We will now create our project with:
```bash
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:
```bash
npm i -s @angular/cdk @angular/flex-layout @angular/material
```

Now we can start our dev server.
```bash
npm start
```
One thing I like to do first is to add the following lines to your `tsconfig.json` under the `"compilerOptions"`.
```json
  "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`
```css
html,
body {
  height: 100%;
}

body {
  margin: 0;
  font-family: Roboto, "Helvetica Neue", sans-serif;
}
```
Second, the `src/index.html`
```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`.

```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`.
```bash
ng g c dashboard
ng g m dashboard
```

Following this, we can create the `login` module and component.
```bash
ng g c login
ng g m login
```

We need one last module and two components.
```bash
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

```ts
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.

```ts
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`

```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`.

```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
```ts
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
```HTML
<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`.

```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`

```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`.
```ts
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`

```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`
```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`.

```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`.

```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](https://github.com/lampewebdev/angular-dynamic-layout)

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](https://www.instagram.com/lampewebdev/) | [Twitter](https://twitter.com/lampewebdev) | [LinkedIn](https://www.linkedin.com/in/michael-lazarski-25725a87) | [Medium](https://medium.com/@lampewebdevelopment) | [Twitch](https://dev.to/twitch_live_streams/lampewebdev) | [YouTube](https://www.youtube.com/channel/UCYCe4Cnracnq91J0CgoyKAQ)