⏳ 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
Top comments (13)
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 theoutlet
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:
Then in your routes, you just add the
outlet: 'contents'
where you want to specify what should be projected in thecontents
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.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:
You can have a first layout:
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 👍
No probs at all!
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 😊
Hi. Thanls for sharing. How could I login to see layout changes dynamically?
I mean whats the Email and Password?