How to Use CSS Grid with Angular

John Au-Yeung
Sep 28 · 13 min read

CSS Grid is a great way to make a responsive layout without using any CSS library. For Angular apps, adding grid layout to it is even easier by using an add-on package which provides the grid layout as a series of directives.

To specify a grid layout with CSS, you put something like the following in your code:

.container {
display: grid;
grid-template-areas: "header header" "side content";
grid-template-rows: auto auto;
grid-row-gap: 16px;
grid-column-gap: 16px;
grid-column: 150px calc(90vw - 150px)
}

The code above specifies that the page has a header at the top and a div with 150px sidebar on the left and a container for the content on right taking up the remainder of the space. To specify a responsive layout, we use media queries, like so:

@media screen and (min-width: 600px) {
.container {
display: grid;
grid-template-areas: "header header" "side content";
grid-template-rows: auto auto;
grid-row-gap: 16px;
grid-column-gap: 16px;
grid-column: 150px calc(90vw - 150px)
}
}

@media screen and (max-width: 599px) {
.container {
display: grid;
grid-template-areas: "header" "menu" "content";
grid-template-rows: auto auto;
grid-row-gap: 16px;
grid-column-gap: 16px;
grid-column: 90vw;
}
}

The code in the @media screen and (max-width: 599px) block specifies that everything displays in one column in screens narrower than 600px wide.

In this story, will we build an Angular app that consumers the New York Times API. With one layout for mobile and another for another one for desktop. The desktop view has a left sidebar with the categories of news obtained from the New York Times API, and the user can click it to see the content on the right side, and the right show will show the content. In mobile view, the layout will be one column, with a menu to select the category and display the content. To build our layout, we use the @angular/flex-layout library.

The New York Times has a great API for developers to get, search, and display their news data. The API documentation is located at https://developer.nytimes.com/.

The API supports CORS, so front-end apps from domains outside of nytimes.com can access their APIs. This means that we can build an Angular app with it. To build our app, you have to go to the website and register for an API key, which is free.

We start by installing the Angular CLI. To do this, we run npm i -g @angular/cli. Once this is installed, we can run a command to scaffold the app. We run ng new nyt to make an app called nyt. Be sure to choose to include routing and use SCSS for styling when running the ng new.

In the app, we will add a home page to display the headlines from different sections and an article search page to search for articles by keyword and start and end dates. We do this by running the following:

$ ng g component homePage
$ ng g component articleSearchPage
$ ng g component articleSearchResults
$ ng g component toolBar
$ ng g pipe capitalizeCategory

The commands create the components that will be displayed to the user. Next we need to add some libraries that we will use to make our app look good and functional. To do we this we run:

npm install --save @angular/material @angular/cdk @angular/animations @angular/flex-layout

This installs Angular Material to make our app look appealing. Then we run:

npm install @ngrx/store moment

This builds our Flux store and manipulates time respectively.

Now we can include the library modules into our main-app module. To do this, we put the following into app.module.ts:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { HomePageComponent } from './home-page/home-page.component';
import { ArticleSearchPageComponent } from './article-search-page/article-search-page.component';
import { ArticleSearchResultsComponent } from './article-search-results/article-search-results.component';
import { StoreModule } from '@ngrx/store';
import { reducers } from './reducers';
import { NytService } from './nyt.service';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatInputModule } from '@angular/material/input';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { ToolBarComponent } from './tool-bar/tool-bar.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatButtonModule } from '@angular/material/button';
import { MatMomentDateModule } from '@angular/material-moment-adapter';
import { HttpClientModule } from '@angular/common/http';
import { MatSelectModule } from '@angular/material/select';
import { MatCardModule } from '@angular/material/card';
import { MatListModule } from '@angular/material/list';
import { MatMenuModule } from '@angular/material/menu';
import { MatIconModule } from '@angular/material/icon';
import { MatGridListModule } from '@angular/material/grid-list';
import { FlexLayoutModule } from '@angular/flex-layout';
import { CapitalizeCategoryPipe } from './capitalize-category.pipe';
import { TitleCasePipe } from '@angular/common';
@NgModule({
declarations: [
AppComponent,
HomePageComponent,
ArticleSearchPageComponent,
ArticleSearchResultsComponent,
ToolBarComponent,
CapitalizeCategoryPipe
],
imports: [
BrowserModule,
AppRoutingModule,
StoreModule.forRoot(reducers),
FormsModule,
MatSidenavModule,
MatToolbarModule,
MatInputModule,
MatFormFieldModule,
MatDatepickerModule,
BrowserAnimationsModule,
MatButtonModule,
MatMomentDateModule,
HttpClientModule,
MatSelectModule,
MatCardModule,
MatListModule,
MatMenuModule,
MatIconModule,
MatGridListModule,
FlexLayoutModule
],
providers: [
NytService,
TitleCasePipe
],
bootstrap: [AppComponent]
})
export class AppModule { }

Most of the modules in the import array are Angular Material modules. We will use them throughout the app.

In capitalize-category.pipe.ts , we put:

import { Pipe, PipeTransform } from '@angular/core';
import { TitleCasePipe } from '@angular/common';
@Pipe({
name: 'capitalizeCategory'
})
export class CapitalizeCategoryPipe implements PipeTransform {
constructor(private titlecasePipe: TitleCasePipe) { }
transform(value: any, ...args: any[]): any {
if (typeof value == 'string') {
if (value.toLowerCase() == 'nyregion') {
return 'NY Region';
}
if (value.toLowerCase() == 'realestate') {
return 'Real Estate';
}
else if (value.toLowerCase() == 'sundayreview') {
return 'Sunday Review';
}
else if (value.toLowerCase() == 'tmagazine') {
return 'T Magazine';
}
return this.titlecasePipe.transform(value);
}
return value;
}}

This will capitalize the categories correctly. We use the titlecase pipe for most strings, except for a few that are multiple words joined together.

Now we have to create the part of the app where we get and store data. To do this, run:

$ ng g service nyt

This is where we make our HTTP calls to the New York Times API. Now we should have a file called nyt.service.ts. In there, we put:

import { Injectable } from '@angular/core';
import { HttpClient, HttpParams } from '@angular/common/http';
import { environment } from 'src/environments/environment';
@Injectable({
providedIn: 'root'
})
export class NytService {
constructor(
private http: HttpClient
) { }
search(data) {
let params: HttpParams = new HttpParams();
params = params.set('api-key', environment.apikey);
if (data.q !== undefined) {
params = params.set('q', data.q);
}
if (data.begin_date !== undefined) {
params = params.set('begin_date', data.begin_date);
}
if (data.end_date !== undefined) {
params = params.set('end_date', data.end_date);
}
if (data.sort !== undefined) {
params = params.set('sort', data.sort);
}
return this.http.get(`${environment.apiUrl}/search/v2/articlesearch.json`, { params });
}
getArticles(section: string = 'home') {
let params: HttpParams = new HttpParams();
params = params.set('api-key', environment.apikey);
return this.http.get(`${environment.apiUrl}/topstories/v2/${section}.json`, { params });
}
}

The search function takes the data we will pass in, and if it’s defined, then it will be included in the GET request’s query string. The second parameter in the this.http.get function takes a variety of options, including headers and query parameters. HttpParams objects are converted to query strings when the code is executed. getArticles does similar things as the search function except with a different URL.

Then in environment.ts, we put:

export const environment = {
production: false,
apikey: 'your api key',
apiUrl: 'https://api.nytimes.com/svc'
};

This makes the URL and API key referenced in the service file available.

Next, we need to add a Flux data store to store our menu state and search results. First we have to run:

$ ng add @ngrx/store

This adds the boilerplate code to the Flux store. Then, we run:

$ ng g class menuReducer
$ ng g class searchResultsReducer

We run this in the src\app\reducers folder, which was created after running ng add @ngrx/store to make the files for our reducers.

Then in menu-reducer.ts, we put:

export const SET_MENU_STATE = 'SET_MENU_STATE';export function MenuReducer(state: boolean, action) {
switch (action.type) {
case SET_MENU_STATE:
return action.payload;
default:
return state;
}
}

And in search-result-reducer.ts, we put:

export const SET_SEARCH_RESULT = 'SET_SEARCH_RESULT';export function SearchResultReducer(state, action) {
switch (action.type) {
case SET_SEARCH_RESULT:
return action.payload;
default:
return state;
}
}

These twopieces of code will allow the menu and search results to be stored in memory and be propagated to components that subscribe to the data.

Next in src\app\reducers\index.ts, we put:

import { SearchResultReducer } from './search-results-reducer';
import { MenuReducer } from './menu-reducer';
export const reducers = {
searchResults: SearchResultReducer,
menuState: MenuReducer
};

This will allow our module to access our reducers since we have StoreModule.forRoot(reducers) in app.module.ts.

Now we’ll work on our app’s toolbar. To make the toolbar, we put the following in tool-bar.component.ts:

import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
import { SET_MENU_STATE } from '../reducers/menu-reducer';
@Component({
selector: 'app-tool-bar',
templateUrl: './tool-bar.component.html',
styleUrls: ['./tool-bar.component.scss']
})
export class ToolBarComponent implements OnInit {
menuOpen: boolean;
constructor(
private store: Store<any>
) {
store.pipe(select('menuState'))
.subscribe(menuOpen => {
this.menuOpen = menuOpen;
})
}
ngOnInit() {
}
toggleMenu() {
this.store.dispatch({ type: SET_MENU_STATE, payload: !this.menuOpen });
}
}this.store.dispatch({ type: SET_MENU_STATE, payload: !this.menuOpen });

This sends the state of the menu to the rest of the app.

store.pipe(select('menuState'))
.subscribe(menuOpen => {
this.menuOpen = menuOpen;
})

The above gets the state of the menu and is used for displaying and toggling the menu state.

In the corresponding template, tool-bar.component.html, we put:

<mat-toolbar>
<a (click)='toggleMenu()' class="menu-button">
<i class="material-icons">
menu
</i>
</a>
New York Times App
</mat-toolbar>

And in tool-bar.component.scss, we put:

.menu-button {
margin-top: 6px;
margin-right: 10px;
cursor: pointer;
}
.mat-toolbar {
background: #009688;
color: white;
}

In app.component.scss, we put:

#content {
padding: 20px;
min-height: 100vh;
}
ul {
list-style-type: none;
margin: 0;
li {
padding: 20px 5px;
}
}

This changes the color of the toolbar.

Then in app.component.ts, we put:

import { Component, HostListener } from '@angular/core';
import { SET_MENU_STATE } from './reducers/menu-reducer';
import { Store, select } from '@ngrx/store';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
menuOpen: boolean;
constructor(
private store: Store<any>,
) {
store.pipe(select('menuState'))
.subscribe(menuOpen => {
this.menuOpen = menuOpen;
})
}
@HostListener('document:click', ['$event'])
public onClick(event) {
const isOutside = !event.target.className.includes("menu-button") &&
!event.target.className.includes("material-icons") &&
!event.target.className.includes("mat-drawer-inner-container")
if (isOutside) {
this.menuOpen = false;
this.store.dispatch({ type: SET_MENU_STATE, payload: this.menuOpen });
}
}
}

This makes it so when we click outside of the left side menu, it’ll be closed.

In app.component.html, we put:

<mat-sidenav-container class="example-container">
<mat-sidenav mode="side" [opened]='menuOpen'>
<ul>
<li>
<b>
New York Times
</b>
</li>
<li>
<a routerLink='/'>Home</a>
</li>
<li>
<a routerLink='/search'>Search</a>
</li>
</ul>
</mat-sidenav>
<mat-sidenav-content>
<app-tool-bar></app-tool-bar>
<div id='content'>
<router-outlet></router-outlet>
</div>
</mat-sidenav-content>
</mat-sidenav-container>

This displays our left side menu and routes.

In style.scss, we put:

/* You can add global styles to this file, and also import other style files */
@import "~@angular/material/prebuilt-themes/indigo-pink.css";
body {
font-family: "Roboto", sans-serif;
margin: 0;
}
form {
mat-form-field {
width: 95vw;
margin: 0 auto;
}
}
.center {
text-align: center;
}

This imports the Material Design styles and sets the width of our forms.

Then in app-routing.module.ts, we put the following so we can see the pages we made when we go the specified URLs:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { HomePageComponent } from './home-page/home-page.component';
import { ArticleSearchPageComponent } from './article-search-page/article-search-page.component';
const routes: Routes = [
{ path: '', component: HomePageComponent },
{ path: 'search', component: ArticleSearchPageComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }

In index.html, we put:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>New York Time</title>
<base href="/">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root></app-root>
</body>
</html>

This includes the Roboto font commonly used with Material Design and Material Icons.

Now we build the logic for our two pages. First we start with our home page. In home-page.component.html, we put:

import { Component, OnInit } from '@angular/core';
import { NytService } from '../nyt.service';
@Component({
selector: 'app-home-page',
templateUrl: './home-page.component.html',
styleUrls: ['./home-page.component.scss']
})
export class HomePageComponent implements OnInit {
sections: string[] =
`arts, automobiles, books, business, fashion, food, health,
home, insider, magazine, movies, national, nyregion, obituaries,
opinion, politics, realestate, science, sports, sundayreview,
technology, theater, tmagazine, travel, upshot, world`
.replace(/ /g, '')
.split(',');
results: any[] = [];
selectedSection: string = 'home';
constructor(
private nytService: NytService
) { }
ngOnInit() {
this.getArticles();
}
getArticles() {
this.nytService.getArticles(this.selectedSection)
.subscribe(res => {
this.results = (res as any).results;
})
}
}

We get the articles on first load with the ngOnInit function, and then once the page is loaded, we can choose which section to load.

In home-page.component.html, we put:

<div gdAreas="header header | side content" gdColumns="150px calc(90vw - 150px)" gdGap="16px"
gdAreas.lt-md="header | menu | content" gdRows.lt-md="auto auto auto auto" gdColumns.lt-md="90vw">
<div gdArea="header">
<div class="center">
<h1>{{selectedSection | capitalizeCategory }}</h1>
</div>
</div>
<div gdArea='menu'>
<button mat-raised-button [matMenuTriggerFor]="appMenu">
Sections
</button>
<mat-menu #appMenu="matMenu">
<button mat-menu-item *ngFor='let s of sections'
(click)='selectedSection = s; getArticles()'>{{s | capitalizeCategory }}
</button>
</mat-menu>
</div>
<div gdArea="side">
<mat-list role="list">
<mat-list-item role="listitem" *ngFor='let s of sections'>
<a class="sidebar-links" (click)='selectedSection = s; getArticles()'>{{s | capitalizeCategory }}</a>
</mat-list-item>
</mat-list>
</div>
<div gdArea="content">
<mat-card *ngFor='let r of results'>
<mat-list role="list">
<mat-list-item>
<mat-card-title>
{{r.title}}
</mat-card-title>
</mat-list-item>
</mat-list>
<mat-card-subtitle>
<mat-list role="list">
<mat-list-item>Published Date: {{r.published_date | date: 'full' }}</mat-list-item>
<mat-list-item><a href='{{r.url}}'>Link</a></mat-list-item>
<mat-list-item *ngIf='r.byline'>{{r.byline}}</mat-list-item>
</mat-list>
</mat-card-subtitle>
<mat-card-content>
<mat-list role="list">
<mat-list-item>{{r.abstract}}</mat-list-item>
</mat-list>
<img *ngIf='r.multimedia[r.multimedia.length - 1]?.url'
[src]='r.multimedia[r.multimedia.length - 1]?.url'
[alt]='r.multimedia[r.multimedia.length - 1]?.caption' class="image">
</mat-card-content>
</mat-card>
</div>
</div>

This is where we display the results from the New York Times API, including headline titles, pictures, publication date, and other data. capitalizeCategory is called a pipe. It maps the object to the left of the pipe symbol by calling the function on the right.

We make use of the @angular/flex-layout to build our grid layout. We specify the desktop layout with gdAreas=”header header | side content” gdColumns=”150px calc(90vw — 150px)” . header header is what is displayed on top and side content is what is displayed below the header. gdColumns=”150px calc(90vw — 150px)” specifies the column widths of each of the items. Anything on the left side is 150px and anything on the right is 90 percent of the screen’s width minus 150px. The | separates the rows. gdAreas.lt-md=”header | menu | content” gdRows.lt-md=”auto auto auto auto” gdColumns.lt-md=”90vw” specifies the layout for mobile. Everything is display in one row. Each item takes up 90 percents of the screen’s width.

The code on the top of the file is where we enable users to select the section they want to load by letting them choose the section they want to load. It’s here:

<div class="center">
<h1>{{selectedSection | capitalizeCategory }}</h1>
<mat-menu #appMenu="matMenu">
<button mat-menu-item *ngFor='let s of sections' (click)='selectedSection = s; getArticles()'>{{s | capitalizeCategory}}
</button>
</mat-menu>
<button mat-raised-button [matMenuTriggerFor]="appMenu">
Sections
</button>
</div>
<br>

This block splits the string into an array of strings with those names and without the spaces:

sections: string[] =
`arts, automobiles, books, business, fashion, food, health,
home, insider, magazine, movies, national, nyregion, obituaries,
opinion, politics, realestate, science, sports, sundayreview,
technology, theater, tmagazine, travel, upshot, world`
.replace(/ /g, '')
.split(',');

Then in home-page.component.scss, we put:

.image {
width: 100%;
margin-top: 30px;
}

This styles the pictures displayed.

Next we build the page to search for articles. It has a form and a space to display the results.

In article-search.component.ts, we put:

import { Component, OnInit } from '@angular/core';
import { SearchData } from '../search-data';
import { NgForm } from '@angular/forms';
import { NytService } from '../nyt.service';
import * as moment from 'moment';
import { Store } from '@ngrx/store';
import { SET_SEARCH_RESULT } from '../reducers/search-results-reducer';
@Component({
selector: 'app-article-search-page',
templateUrl: './article-search-page.component.html',
styleUrls: ['./article-search-page.component.scss']
})
export class ArticleSearchPageComponent implements OnInit {
searchData: SearchData = <SearchData>{
sort: 'newest'
};
today: Date = new Date();
constructor(
private nytService: NytService,
private store: Store<any>
) {
}ngOnInit() {
}
search(searchForm: NgForm) {
if (searchForm.invalid) {
return;
}
const data: any = {
begin_date: moment(this.searchData.begin_date).format('YYYYMMDD'),
end_date: moment(this.searchData.end_date).format('YYYYMMDD'),
q: this.searchData.q
}
this.nytService.search(data)
.subscribe(res => {
this.store.dispatch({ type: SET_SEARCH_RESULT, payload: (res as any).response.docs });
})
}
}

This gets the data when we click search and propagates the results to the Flux store, which will be used to display the data at the end.

In article-search.component.html, we put:

<div class="center">
<h1>Search</h1>
</div>
<br>
<form #searchForm='ngForm' (ngSubmit)='search(searchForm)'>
<mat-form-field>
<input matInput placeholder="Keyword" required #keyword='ngModel' name='keyword' [(ngModel)]='searchData.q'>
<mat-error *ngIf="keyword.invalid && (keyword.dirty || keyword.touched)">
<div *ngIf="keyword.errors.required">
Keyword is required.
</div>
</mat-error>
</mat-form-field>
<br>
<mat-form-field>
<input matInput [matDatepicker]="startDatePicker" placeholder="Start Date" [max]="today" #startDate='ngModel'
name='startDate' [(ngModel)]='searchData.begin_date'>
<mat-datepicker-toggle matSuffix [for]="startDatePicker"></mat-datepicker-toggle>
<mat-datepicker #startDatePicker></mat-datepicker>
</mat-form-field>
<br>
<mat-form-field>
<input matInput [matDatepicker]="endDatePicker" placeholder="End Date" [max]="today" #endDate='ngModel'
name='endDate' [(ngModel)]='searchData.end_date'>
<mat-datepicker-toggle matSuffix [for]="endDatePicker"></mat-datepicker-toggle>
<mat-datepicker #endDatePicker></mat-datepicker>
</mat-form-field>
<br>
<mat-form-field>
<mat-label>Sort By</mat-label>
<mat-select required [(value)]="searchData.sort">
<mat-option value="newest">Newest</mat-option>
<mat-option value="oldest">Oldest</mat-option>
<mat-option value="relevance">Relevance</mat-option>
</mat-select>
</mat-form-field>
<br>
<button mat-raised-button type='submit'>Search</button>
</form>
<br>
<app-article-search-results></app-article-search-results>

This is the search form for the articles. It includes a keyword field, start and end date datepickers, and a drop-down to select the way to sort. These are all Angular Material components. <app-article-search-results></app-article-search-results> is the article search result component which we generated but have not built yet.

Note that the [( in [(ngModel)] denotes two-way data binding between the component or directive, and the current component and [ denote one-way binding from the current component to the directive or component.

Next in article-search.results.ts, we put:

import { Component, OnInit } from '@angular/core';
import { Store, select } from '@ngrx/store';
@Component({
selector: 'app-article-search-results',
templateUrl: './article-search-results.component.html',
styleUrls: ['./article-search-results.component.scss']
})
export class ArticleSearchResultsComponent implements OnInit {
searchResults: any[] = [];
constructor(
private store: Store<any>
) {
store.pipe(select('searchResults'))
.subscribe(searchResults => {
this.searchResults = searchResults;
})
}
ngOnInit() {
}
}

This block gets the article search results stored in our Flux store and sends it to our template for displaying:

store.pipe(select('searchResults'))
.subscribe(searchResults => {
this.searchResults = searchResults;
})

In article-search.results.html, we put:

<mat-card *ngFor='let s of searchResults'>
<mat-list role="list">
<mat-list-item>
<mat-card-title>
{{s.headline.main}}
</mat-card-title>
</mat-list-item>
</mat-list>
<mat-card-subtitle>
<mat-list role="list">
<mat-list-item>Date: {{s.pub_date | date: 'full' }}</mat-list-item>
<mat-list-item><a href='{{s.web_url}}'>Link</a></mat-list-item>
<mat-list-item *ngIf='s.byline.original'>{{s.byline.original}}</mat-list-item>
</mat-list>
</mat-card-subtitle>
<mat-card-content>
<div class="content">
<p>{{s.lead_paragraph}}</p>
<p>{{s.snippet}}</p>
</div>
</mat-card-content>
</mat-card>

This just displays the results from the store.

In article-search-results.component.scss, we add:

.content {
padding: 0px 15px;
}

This adds some padding to the paragraphs.

At the end, we have the following:

The Startup

Medium's largest active publication, followed by +539K people. Follow to join our community.

John Au-Yeung

Written by

Web developer. Subscribe to my email list now at http://jauyeung.net/subscribe/ . Follow me on Twitter at https://twitter.com/AuMayeung

The Startup

Medium's largest active publication, followed by +539K people. Follow to join our community.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade