Search This Blog

Tuesday, November 29, 2016

Using an Angular 2 Material Design component

In this post, we will utilize the material design library being developed as part of Angular 2 to build a simple set of cards. In previous posts, we already covered the following


We will build on past experiences to introduce ourselves to material design with Angular 2, which is still evolving. Overall process is as follows
  1. Start a new project and add the material design library to our project
  2. Create a new component using angular-cli
  3. Build a JSON REST service stub (we will simply emulate the service, not build it.)
  4. Use some material design components within our component
  5. Bind our component to the REST service
  6. View the final result.
At the end we will build a simple set of cards that look like the following:


Let's get started.

Adding Material Design to our project

Adding material design to the project is simple. Since I needed to carry out a few steps, I did the following steps
  • I first created a new project using "ng new"
  • Then added material design to the project node modules using the following command. 
  • Next I generated a component called mycard.
  • Finally I created a service called CardService.
The following steps document the script.

ng new ng2-3
cd ng2-3
npm install --save @angular/material
ng generate component mycard
ng generate service cards

Next we need to import the material design module within our primary application module. Access the application.module.ts and add the following lines at appropriate places.

import { MaterialModule } from '@angular/material';

@NgModule({
imports: [
    MaterialModule.forRoot(), ...
  ],

We also need the component definition added to app.module.ts

import { MycardComponent } from './mycard/mycard.component';

@NgModule({
  declarations: [
    MycardComponent...

Here is how the updated app.module.ts looks in my editor

app.module.ts

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { MaterialModule } from '@angular/material';
import { AppComponent } from './app.component';
import { MycardComponent } from './mycard/mycard.component';

@NgModule({
  declarations: [
    AppComponent,
    MycardComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    MaterialModule.forRoot()
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

A screenshot of the same..


Building a Service Stub

Next step is to edit the card.service.ts file to return a set of cards. I used the following JSON structure and code to create the service stub.

card.service.ts

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

@Injectable()
export class CardService {

    cards = [{
                "svc_cardid": 1,
                "svc_cardname": "Birds of Norfolk"
                ,"svc_cardlink": "https://commons.wikimedia.org/wiki/Category:Decorative_papers#/media/File:The_birds_of_Norfolk_-_with_remarks_on_their_habits,_migration,_and_local_distribution_(1866)_(14750215905).jpg"
                ,"svc_cardimg":"https://upload.wikimedia.org/wikipedia/commons/3/3f/The_birds_of_Norfolk_-_with_remarks_on_their_habits%2C_migration%2C_and_local_distribution_%281866%29_%2814750215905%29.jpg"
                ,"Identifier": "birdsofnorfolkwi01stev"
                ,"Title": "The birds of Norfolk : with remarks on their habits, migration, and local distribution"
                ,"Year": 1866
                ,"Authors": "Stevenson, Henry, 1833-1888 Southwell, Thomas, 1831-1909"
                ,"Subjects": "Birds -- England Norfolk"
                ,"Publisher": "London : J. Van Voorst"
                ,"Contributing_Library": "American Museum of Natural History Library"
                ,"Digitizing_Sponsor": "Biodiversity Heritage Library"
              }, {
                "svc_cardid": 2,
                "svc_cardname": "1902 Annual Report"
                ,"svc_cardlink": "https://commons.wikimedia.org/wiki/Category:Decorative_papers#/media/File:Annual_report_(1906)_(14745691021).jpg"
                ,"svc_cardimg":"https://upload.wikimedia.org/wikipedia/commons/7/7d/Annual_report_%281906%29_%2814745691021%29.jpg"
                ,"Identifier": "annualreport131415190newy"
                ,"Title": "Annual report"
                ,"Year": 1902
                ,"Authors": "Forest, Fish and Game Commission"
                ,"Subjects": "Forests and forestry Fisheries Game and game-birds"
                ,"Publisher": "(Albany, N.Y. : The Commission)"
                ,"Contributing_Library": "Smithsonian Libraries"
                ,"Digitizing_Sponsor": "Biodiversity Heritage Library"
              }, {
                "svc_cardid": 3,
                "svc_cardname": "Cooperative economic insects"
                ,"svc_cardlink": "https://commons.wikimedia.org/wiki/Category:Decorative_papers#/media/File:Cooperative_economic_insect_report_(1955)_(20671420826).jpg"
                ,"svc_cardimg":"https://upload.wikimedia.org/wikipedia/commons/9/9e/Cooperative_economic_insect_report_%281955%29_%2820671420826%29.jpg"
                ,"Identifier": "cooperativeecono51955unit"
                ,"Title": "Cooperative economic insect report"
                ,"Year": 1951
                ,"Authors": "Animal and Plant Health Service."
                ,"Subjects": "Beneficial insects; Insect pests"
                ,"Publisher": "Hyattsville, MD. (etc. )"
                ,"Contributing_Library": "Smithsonian Libraries"
                ,"Digitizing_Sponsor": "Biodiversity Heritage Library"
              }, {
                "svc_cardid": 4,
                "svc_cardname": "Histoire de Quillembois soldat"
                ,"svc_cardlink":"https://commons.wikimedia.org/wiki/File:Histoire_de_Quillembois_soldat_(1920)_(14750774484).jpg"
                ,"svc_cardimg": "https://upload.wikimedia.org/wikipedia/commons/f/f3/Histoire_de_Quillembois_soldat_%281920%29_%2814750774484%29.jpg"
                ,"Identifier": "histoiredequille001871"
                ,"Title": "Histoire de Quillembois soldat"
                ,"Year": 1920
                ,"Authors": "1871-1935"
                ,"Subjects":""
                ,"Publisher": "Nancy, Paris (etc.) : Librairie Berger-Levrault"
                ,"Contributing_Library": "New York Public Library"
                ,"Digitizing_Sponsor": "msn"
              }];


  constructor() { }

  getCards(): any {
    console.log("CardService.getCards() called");
    return this.cards;

   }
}

 Next, we want to add some behaviour to the cards, as follows
  • When a card is clicked, it goes in expanded mode. 
  • When it is clicked again, it resumes to its non-expanded mode.
  • We also want the card to raise an event that can be implemented by the host component if needed. For this we add an Output parameter that raises an event using EventEmitter. 
How the expanded mode differs from non-expanded, we will define in our style sheet later.

mycard.component.ts


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

@Component({
  selector: 'mycard',
  templateUrl: './mycard.component.html',
  styleUrls: ['./mycard.component.css']
})
export class MycardComponent implements OnInit {
  @Input() cardid = -1;
  @Input() cardname="";
  @Input() cardimage="";
  @Input() cardyear;
  @Input() cardauthor;
  @Input() cardpublisher;
  @Input() cardlink;
  @Input() isExpanded = false;
  @Output() cardClicked = new EventEmitter();

  constructor() {
      console.log("MycardComponent.constructor() called");
  }

  ngOnInit() {
  }

  onDivClick()
  {
      this.isExpanded = !this.isExpanded;
      console.log("MycardComponent.onDivClick("+this.cardid+").isExpanded(): "+this.isExpanded);
      this.cardClicked.emit(this.cardid);
  }

  onDivHover()
  {
      console.log("MycardComponent.onDivHover("+this.cardid+") called");
  } 
}

Here is a screenshot of my editor screen.



Next, we add the edit the mycard.component.html to add the rendering logic as follows

  • An outer div tag to capture click events that calls onDivClick() in the component class
  • A md-card that has a ternary operator for loading appropriate css class based on the component's state isExpanded. The md-card contains the following sub-elements.
    • An image with md-card-image directive and an image sourced from a component property
    • A md-card-title based on the cardname property of the class
    • A md-card-content containing a description obtained by concatenating several properties of the card.
    • A md-raised-button with accent color. 

mycard.component.html 

<div (click)="onDivClick()" (hover)="onDivHover()">
  <md-card class="my-card {{isExpanded?'my-card-expanded':''}}">
      <img class="md-card-image" src="{{cardimage}}" md-card-image/>
    <div >
      <md-card-title>{{cardname}}</md-card-title>
    </div>
    <div >
      <md-card-content>Authors: {{cardauthor}}, Published: {{cardyear}} by {{cardpublisher}}</md-card-content>
    </div>
    <button md-raised-button color="accent">View</button>
  </md-card> 
</div>

Here is the screenshot of the same..


mycard.component.css

The css contains the style definition for the cards. Here are some important observations

  • The default css for my-card simply sets 300px as width and height, sets a background color and a top and bottom margin.
  • The expanded card increases the height to 600px.
  • md-card-image restricts the height of the image to 180px, and sets object-fit to cover. This ensures that image is cropped to fit the placeholder
  • Finally md-card-title overrides the default font size to 20px, to save some space.
.my-card {
    width: 300px;
    height:300px;
    margin-left: 0;
    margin-right:0;
    background-color:#eaeaea;
    margin-top:10px;
    margin-bottom:10px;
}

.my-card-expanded {
   width: 300px;
    height:600px;
    margin-left: 0;
    margin-right:0;
    background-color:#eaeaea;
    margin-top:10px;
    margin-bottom:10px;
}

.md-card-image {
    height: 180px;
    object-fit: cover;
}

.md-card-title {
    font-size: 20px;
    font-weight: 400;
}

Here is a screenshot of the same..


Binding the service to mycard component

Next step is to bind the data received from the CardService to mycard component. This is done by the component initialising the mycard component. We also need to add the component to our primary app component. This is done in app.component.ts

app.component.ts

Here we do a few things.
  • We import the CardService that we had defined earlier
  • We define a variable called cards;
  • We add the cardService to the constructor using dependency injection. Here we also assign the return type from getCards to the AppComponent's cards variable
  • We also add an event handler for the component's click event, simply to log which card was clicked.

import { Component } from '@angular/core';
import {CardService} from './card.service';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
   providers: [CardService]
})
export class AppComponent {
  title = 'Material Design Tiles!';
  cards;
  
  constructor(cardService: CardService) {
     console.log("app.component.constructor() called");
     this.cards=cardService.getCards();
  }

  compCardClicked($event) {
    console.log("app.component.compCardClicked() called for:"+ $event);
  }
}

Here is a screenshot of the same:



Finally, we need to add our component to the app.component.html.

app.component.html

Here we do the following things
  • We check if cards is populated, then we instantiate a div container with class=container (we will discuss this part next).
  • Within the div container, we create a for loop using ngFor directive, that iterates over the cards property of the app.component class (app.component.ts), and puts it within a my-card tag. 
  • The html code also sets various properties of the mycard component using data received from the service.
  • The html markup also looks for cardClicked event raised by each card and calls the compCardClicked() method in the app.component class.

<h1>
</h1>
<div *ngIf="cards" class="container">
  <div *ngFor="let card of cards, let i = index">
    <mycard [cardname]="card.svc_cardname" [cardid]="card.svc_cardid" 
              [cardimage]="card.svc_cardimg" [cardauthor]="card.Authors" 
              [cardyear]="card.Year" [cardpublisher]="card.Publisher" 
              [cardlink]="card.svc_cardlink"
              (cardClicked)="compCardClicked($event)"></mycard>
  </div>
</div>

Here is the html markup on my screen editor..



Finally, we tackle the styling within app.component.css.

app.component.css

The only definition is for the container class that sets the following

  • A flex layout oriented by rows with row wrap to ensure mycards are wrapped based on space available.
  • A justify content (based on rows) and across "align-content" based on space-around.

.container {
  display: flex; /* or inline-flex */
  flex-flow: row wrap;
  justify-content: space-around;
  align-content: space-around;
  min-height: 110%;
}

Here is the screenshot, once again..


Running it

Finally we can use ng serve command to view the configured page on localhost:4200

This is the normal mode


and now the expanded mode after clicking on the card..


Here is the same site with console logs visible


That's it!

No comments: