How to build a full-page website in Angular

12. Add a custom directive

And now for the fun part! Our website is mostly functional except for the fact that our background image does not stretch to fit the full real estate of the browser. We are going to tackle this problem by creating a custom directive that will resize our image to make it full page height and width.

The CLI is back in our good graces as we can use it to generate a directive. We will generate a fullpage directive in the app/shared/directives directory.

mkdir src/app/shared/directives
ng g directive shared/directives/fullpage

This will generate a basic directive that looks like this.

ts
import { Directive } from '@angular/core';

@Directive({
  selector: '[appFullpage]'
})
export class FullpageDirective {
  constructor() { }
}

The generated selector is appFullpage, which feels odd to me, so I renamed it to fullpage. You are welcome to do it either way.

The fundamental difference between a component and a directive is that a directive does not have its own template; it is responsible for decorating or augmenting an existing element. In this case, we want it to take an existing element and make it fit the window.

13. Access the element

In order to resize an element, we need a reference to it, which is conveniently provided by ElementRef. This gives us access to the element our directive was declared on.

@Directive({
  selector: '[fullpage]'
})
export class FullpageDirective {
  constructor(private element: ElementRef) { }
}

Boring maths alert! We will create a resize function that measures the width and height of our element as well as the width and height of the window, and then proportionally resize the element accordingly. The basic idea is to fit the width or height to the window, based on which is smaller, and then scale the other property proportionally.

// app/shared/directives/fullpage.directive.ts
...
export class FullpageDirective {

  constructor(private element: ElementRef) { }

  resize() {
    let bgwidth = this.element.nativeElement.width;
    let bgheight = this.element.nativeElement.height;

    let winwidth = window.innerWidth;
    let winheight = window.innerHeight;

    let widthratio = winwidth / bgwidth;
    let heightratio = winheight / bgheight;

    let widthdiff = heightratio * bgwidth;
    let heightdiff = widthratio * bgheight;

    if (heightdiff > winheight) {
      this.element.nativeElement.width = winwidth;
      this.element.nativeElement.height = heightdiff;
    } else {
      this.element.nativeElement.width = widthdiff;
      this.element.nativeElement.height = winheight;
    }
  }
}

14. Introduce host listeners

This is all fascinating, but how do we know when to resize our element? Angular aside, we want to resize our element when it first loads, and every time the window is resized.

The next question is: How do we know when these events happen? Angular delivers an awesome way to do this in the form of `HostListener`. We can decorate a method with HostListener metadata that defines the event we want to listen for to fire the method we just decorated. 

We use window:resize to know when the window has been resized and load to know when the element has loaded.

// app/shared/directives/fullpage.directive.ts
import { Directive, ElementRef, HostListener } from '@angular/core';
...
export class FullpageDirective {

  constructor(private element: ElementRef) { }

  @HostListener('window:resize', ['$event'])
  onResize(event) {
    this.resize();
  }

  @HostListener('load', ['$event'])
  onLoad(event) {
    this.resize();
  }

  resize() {
    ...
  }
}

This is a really awesome way to attach methods to DOM events without having to talk to the DOM directly, manually write our listeners, and, most importantly, without needing to worry about destroying our events.

To add our directive to our application, we will go to our page.component.html file and add the fullpage attribute to our image.

<!-- app/page/page.component.html -->
<img class="fullBg" fullpage [src]="page.image">
...

15. Make things pretty with CSS

There are just a few things we are doing with the CSS to make this look pretty. For instance, we’re defining that we are using Flexbox for our layout and that we want our body tag to hide any overflow.

/* styles.css */
html, body, app-root {
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
}
body {
  overflow: hidden;
  margin:0;
}

Also, notice that we are creating a spacer class to push the logo and buttons apart in the toolbar. We also have an active class that we target with routerLinkActive.

/* app/app.component.css */
md-toolbar {
  position: relative;
  z-index: 1;
  overflow-x: auto;
}

md-toolbar-row {
  justify-content:space-between;
}

.spacer {
  flex: 1 1 auto;
}

a.active {
  background-color: rgba(0,0,0, 0.3);
}

h1 {
  margin: 0;
}

h1 .light {
  font-weight: 100;
}

Finally, we are setting using the :host pseudo-selector to target the component that our CSS is targeting.

/* app/page/page.component.css */
:host {
    display: flex;
    height: 100%;
    align-items: center;
}

md-card {
    width:500px;
    background: rgba(255,255,255,0.9);
}

md-card-header {
    color: #3f51b5;
}

md-card-content {
    padding: 8px;
}

.fullBg {
    z-index:0;
    position: fixed;
    top: 0;
    left: 0;
}

The interesting thing about the CSS is that it is defined at the appropriate component level. Application-wide styles generally go in the styles.css file, with each component-specific style going in its respective CSS files.

16. Review your work

We have covered quite a bit of ground in the first part of this series, but let's take a minute to review what we have accomplished:

  • We used the @angular/cli to generate a new project for us
  • We installed @angular/material and @angular/animations via NPM
  • We added @angular/material and @angular/animations to our Angular module
  • We created a PageComponent to represent the pages in our site
  • We created a ContentService to hold the content for our site
  • We created routes that mapped to the pages in our site and passed in custom data to help us determine which page we are on
  • We added a toolbar that linked to each page using routerLink and styled the button for the active page with routerLinkActive
  • We created a fullpage directive to resize the image in our PageComponent to fill the entire background
  • We used HostListener to listen for DOM events to know when to resize the element

Related articles: