Creating an Angular Component Library - Card Component


A card component is something that you might use quite a bit in a project. We are going to build a very basic card component that doesn’t really do anything fancy to start. It will be made up of a header, body, and footer. In subsequent post will expand on our card component and create a couple fancier forms of cards using animations.

A full screen demo of what we’re going to build

 

Prerequisites

 

Generating Our Card Component

For our card we are going to need a module and a component.

ng generate module components/card --project=foo
ng generate component components/card --project=foo

We are also going to generate three additional components. These components are simply wrappers around an <ng-content> tag. There won’t be any logic or styling so we will generate them with an inline style and template.

ng generate component components/card/card-body --project=foo --inlineStyle=true --inlineTemplate=true --flat=true
ng generate component components/card/card-footer --project=foo --inlineStyle=true --inlineTemplate=true --flat=true
ng generate component components/card/card-header --project=foo --inlineStyle=true --inlineTemplate=true --flat=true

Now we can update our module to export our components to make them accessible to other modules importing our card module.

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

import { CardComponent } from './card.component';
import { CardBodyComponent } from './card-body.component';
import { CardFooterComponent } from './card-footer.component';
import { CardHeaderComponent } from './card-header.component';

@NgModule({
  declarations: [
    CardComponent,
    CardBodyComponent,
    CardFooterComponent,
    CardHeaderComponent,
  ],
  imports: [
    CommonModule
  ],
  exports: [
    CardComponent,
    CardBodyComponent,
    CardFooterComponent,
    CardHeaderComponent
  ]
})
export class CardModule { }

We are also going to need an enum value to model our different card types.

touch projects/foo/src/lib/components/card/card-type.enum.ts

And enum will look something like this…

export enum CardType {
  BLANK = 'card-blank',
  DEFAULT = 'card-default',
  PRIMARY = 'card-primary',
  SECONDARY = 'card-secondary',
  SUCCESS = 'card-success',
  INFO = 'card-info',
  WARNING = 'card-warning',
  DANGER = 'card-danger'
}

We will have quite a few different card styles. A BLANK card will simple be a blank white card, while the others will follow our color scheme we already setup. We will need to update our _variables.scss file with a couple addtional values as well, the default color and card background color.

// COLORS
$default: #AAAAAC;
$primary: #3B4D63;
$secondary: #B676BC;
$success: #5aaa95;
$info: #61adb5;
$warning: #d3c160;
$danger: #db9c7c;

// CONTAINERS
$border-radius: 4px;
$card-background: #FFF;

And finally update our components directory’s public_api.ts file…

// Card
export * from './card/card.module';
export * from './card/card.component';
export * from './card/card-body.component';
export * from './card/card-footer.component';
export * from './card/card-header.component';
export * from './card/card-type.enum';

 

Creating Our Card Child Components

In the previous step, I had us generate three components that essentially act as a wrapper around an <ng-content> element. We generated these with inline styles and inline templates. We are going to update our template to render an <ng-content> element and thats its. There will be no styles or logic. We could have chosen to use a directive for these as well and would have had a similar effect, but I find using components with element selectors easier to read.

Lets update our card-body component…

import { Component } from '@angular/core';

@Component({
  selector: 'foo-card-body',
  template: `<ng-content></ng-content>`
})
export class CardBodyComponent {}

Our card-footer component…

import { Component } from '@angular/core';

@Component({
  selector: 'foo-card-footer',
  template: `<ng-content></ng-content>`
})
export class CardFooterComponent {}

And finally our card-header component…

import { Component } from '@angular/core';

@Component({
  selector: 'foo-card-header',
  template: `<ng-content></ng-content>`
})
export class CardHeaderComponent {}

 

Creating Our Card Component

Our root card component is very simple. Our card is essentially a dummy component with no logic and a single @Input to configure the card type. By default going to set the type to blank. The major part of the card component is the styling.

For our HTML will use a single div with nested ng-content elements. The content elements will select our card-header, card-body, and card-footer components we created.

Lets update our card component’s HTML file…

<div class="card" [ngClass]="type">
  <ng-content select="foo-card-header"></ng-content>
  <ng-content select="foo-card-body"></ng-content>
  <ng-content select="foo-card-footer"></ng-content>
</div>

Our card styles…

@import '../_shared/scss/variables';
@import '../_shared/scss/mixins';

:host ::ng-deep .card {
  box-sizing: border-box;
  background: $card-background;
  display: flex;
  flex-direction: column;
  height: 100%;
  @include box-shadow;
  
  foo-card-header,
  foo-card-body,
  foo-card-footer {
    padding: 1rem;
  }

  foo-card-body {
    flex-grow: 1;
  }

  &.card-default {
    foo-card-header {
      color: $card-background;
      background: $default;
    }
  }

  &.card-primary {
    foo-card-header {
      color: $card-background;
      background: $primary;
    }
  }

  &.card-secondary {
    foo-card-header {
      color: $card-background;
      background: $secondary;
    }
  }

  &.card-success {
    foo-card-header {
      color: $card-background;
      background: $success;
    }
  }

  &.card-info {
    foo-card-header {
      color: $card-background;
      background: $info;
    }
  }

  &.card-warning {
    foo-card-header {
      color: $card-background;
      background: $warning;
    }
  }

  &.card-danger {
    foo-card-header {
      color: $card-background;
      background: $danger;
    }
  }
}

We need to create one additional file for our card component. If you look at our SCSS you will see an additional @import at the top and an @include under the .card class for a box-shadow. We are going to create a mixin scss file so we can reuse some of these mixin values.

We will create our _mixins.scss file in the same directory as our _variables.scss file…

touch projects/foo/src/lib/components/_shared/scss/_mixins.scss

And our box-shadow mixin…

@mixin box-shadow {
  box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
  -webkit-box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
  -moz-box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
}

Finally, our card component class file…

import { Component, OnInit, Input } from '@angular/core';
import { CardType } from './card-type.enum';

@Component({
  selector: 'foo-card',
  templateUrl: './card.component.html',
  styleUrls: ['./card.component.scss']
})
export class CardComponent implements OnInit {
  @Input()
  public type: CardType;

  constructor() {
    this.type = CardType.BLANK;
  }

  ngOnInit(): void {
  }
}

 

Using Our Card Component

Lets update our demo application to use our card component.

<div style="width: 500px; margin: 2rem;">
  <foo-card [type]="CardType.BLANK">
    <foo-card-header>
      <h1>Hello</h1>
    </foo-card-header>
    <foo-card-body>
      <p>Testing</p>
    </foo-card-body>
    <foo-card-footer>
      <p>Footer</p>
    </foo-card-footer>
  </foo-card>
</div>
<div style="width: 500px; margin: 2rem;">
  <foo-card [type]="CardType.DEFAULT">
    <foo-card-header>
      <h1>Hello</h1>
    </foo-card-header>
    <foo-card-body>
      <p>Testing</p>
    </foo-card-body>
    <foo-card-footer>
      <p>Footer</p>
    </foo-card-footer>
  </foo-card>
</div>
<div style="width: 500px; margin: 2rem;">
  <foo-card [type]="CardType.PRIMARY">
    <foo-card-header>
      <h1>Hello</h1>
    </foo-card-header>
    <foo-card-body>
      <p>Testing</p>
    </foo-card-body>
    <foo-card-footer>
      <p>Footer</p>
    </foo-card-footer>
  </foo-card>
</div>

Our app component class file…

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

import { CardType } from 'foo';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  public CardType = CardType;

  constructor() {}

  ngOnInit() {
  }
}

And update our app module to import our card module from our library…

import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule } from '@angular/core';

import { CardModule } from 'foo';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';

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

Now we can simply build our library and start our demo application

npm run build
npm start

 

Stackblitz Result

I’ve also embeded a Stackblitz showing our cards with our dashabord layout we create a few posts back. I’ve added a card for each card type we created.

A full screen demo can also be viewed here

 

 

The completed github repository can be found here

 


comments powered by Disqus