Beautiful Angular Apps with Bootstrap
I’ve been a fan of CSS frameworks since 2005. I led an open-source project called AppFuse at the time and wanted a way to provide themes for our users. We used Mike Stenhouse’s CSS Framework and held a design contest to gather some themes we liked for our users. A couple of other CSS frameworks came along in the next few years, namely Blueprint in 2007 and Compass in 2008.
However, no CSS frameworks took the world by storm like Bootstrap. Back then, it was called Twitter Bootstrap. Mark Otto and Jacob Thornton invented it in mid-2010 while they worked at Twitter. As they wrote in “Building Twitter Bootstrap” in Issue 324 of A List Apart:
Our goal is to provide a refined, well-documented, and extensive library of flexible design components built with HTML, CSS, and JavaScript for others to build and innovate on.
They released Bootstrap on August 19, 2011, and it quickly became the most popular project on GitHub. Developers like myself all over the world started using it. Bootstrap differed from previous CSS frameworks because it embraced mobile-first design and made responsiveness the norm for web design. Before Bootstrap, we built UIs for mobile apps with specialized frameworks like jQuery Mobile.
Another web framework took the world by storm the following year: AngularJS. AngularJS (v0.9) first appeared on GitHub in October 2010. The creators released version 1.0 on June 14, 2012.
Together, these frameworks have had quite a run. It’s hard to believe they’ve lasted so long, especially considering both projects have had major rewrites!
I’ve heard many developers say that Angular is dead. As a veteran Java developer, I’ve heard this said about Java many times over the years as well. Yet it continues to thrive. Angular is similar in that it’s become somewhat boring. Some people call boring frameworks “legacy.” Others call them “revenue-generating.”
Whatever you want to call it, Angular is far from dead.
Angular Loves Bootstrap
You might think that Angular Material is more popular than Bootstrap these days. You may be right, but who you follow on Twitter shapes your popularity perspective. Bootstrap and Angular Material were quite popular among the fabulous folks that answered my recent poll. In 2019; 53% answered Bootstrap, and 33% answered Angular Material.
Integrate Bootstrap with Angular
Integrating Bootstrap into an Angular application is fairly easy, thanks to NG Bootstrap. I’ll start with the note-taking example from the last section. If you follow along, you’ll convert the app to use Sass (because CSS is more fun with Sass), make the app look good, add form validation, and write some code to develop a searchable, sortable, and pageable data table. The last part sounds complex, but it only requires < 10 lines of code on the Spring Boot side of things. Kotlin and Spring Data JPA—FTW!
If you’re following along, you should have an angular-spring-boot
directory containing an Angular and a Spring Boot app.
If you’d rather start from this point, download the examples for this book from InfoQ. The angular-spring-boot
directory has the previous section’s completed example. Copy it to angular-bootstrap
in your favorite code location.
Navigate into this new directory and its notes
folder in a terminal. Then install the dependencies for the Angular app.
cd angular-bootstrap/notes
npm install
Add Bootstrap and NG Bootstrap:
rm package-lock.json
ng update @angular/cli @angular/core
ng add @ng-bootstrap/ng-bootstrap@14
This process will import NgbModule
in app.module.ts
and configure your app to use Bootstrap by adding a reference to bootstrap.min.css
in angular.json
.
If you run ng serve -o
, you’ll see it’s pretty simple. And kinda ugly.
Let’s fix that!
Begin by changing app.component.html
to use Bootstrap classes.
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand text-light" href="#">{{ title }} app is running!</a>
</div>
</nav>
<div class="container-fluid pt-3">
<router-outlet></router-outlet>
</div>
Now we’re getting somewhere!
Enter a note, and you’ll see it in the list.
You’ll notice it looks pretty good, but things aren’t quite beautiful. Yet…
Use Sass to Customize Bootstrap
Before you make things awesome, I’d like to show you how to convert from CSS with Angular to Sass. Why? Because Sass is completely compatible with CSS, and it makes CSS more like programming. It also allows you to customize Bootstrap by overriding its variables.
If you’re not into Sass, you can skip this section. Everything will still work without it. |
If you run the following find
command in the notes
project…
find . -name "*.css" -not -path "./node_modules/*"
…you’ll see three files have a .css
extension.
./src/app/home/home.component.css
./src/app/app.component.css
./src/styles.css
You can manually rename these to have a .scss
extension or do it programmatically.
find . -name "*.css" -not -path "./node_modules/*" | rename -v "s/css/scss/g"
I had to brew install rename on my Mac for this command to work.
|
Then, replace all references to .css
files.
find ./src/app -type f -exec sed -i '' -e 's/.css/.scss/g' {} \;
Modify angular.json
to reference src/styles.scss
(in the build
and test
sections) and remove bootstrap.min.css
.
"styles": [
"src/styles.scss"
],
And change styles.scss
to import Bootstrap’s Sass.
@import 'bootstrap/scss/bootstrap.scss';
To demonstrate how to override Bootstrap’s variables, create a src/_variables.scss
and override the colors. You can see the default variables in Bootstrap’s GitHub repo.
$primary: orange;
$secondary: blue;
$light: lighten($primary, 20%);
$dark: darken($secondary, 10%);
Then import this file at the top of src/styles.scss
:
@import 'variables', 'bootstrap/scss/bootstrap.scss';
You’ll see the colors change after these updates.
Comment out (or remove) the variables in _variables.scss
to revert to Bootstrap’s default colors.
Make Your Angular App Beautiful with Bootstrap
You can see from the screenshots above that angular-crud
generates screens with some styling, but it’s not quite right. Let’s add a
Navbar
in app.component.html
. Change its HTML to have a collapsible navbar (for mobile devices), add links to useful sites, and add login/logout buttons. While you’re at it, display a message to the user when they aren’t authenticated.
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">
<img src="/assets/images/angular.svg" width="30" height="30"
class="d-inline-block align-top" alt="Angular">
{{ title }}
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse"
data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav ms-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://twitter.com/mraible">@mraible</a>
</li>
<li class="nav-item">
<a class="nav-link" href="https://github.com/mraible">GitHub</a>
</li>
</ul>
<form class="d-flex">
<button *ngIf="(auth.isAuthenticated$ | async) === false"
(click)="login()" type="button"
class="btn btn-outline-primary">Login</button>
<button *ngIf="auth.isAuthenticated$ | async"
(click)="logout()" type="button"
class="btn btn-outline-secondary">Logout</button>
</form>
</div>
</div>
</nav>
<div class="container-fluid pt-3">
<a *ngIf="(auth.isAuthenticated$ | async) === false">Please log in to manage your notes.</a>
<router-outlet></router-outlet>
</div>
Download the angular.svg
file from
angular.io/presskit
and add it to your project. You can do this quickly by running the following command from the notes
directory.
wget https://angular.io/assets/images/logos/angular/angular.svg -P src/assets/images/
Add AuthService
and DOCUMENT
as imports to AppComponent
, inject them into the constructor, and add login()
and logout()
methods.
import { Component, Inject } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';
import { DOCUMENT } from '@angular/common';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
title = 'notes';
constructor(public auth: AuthService, @Inject(DOCUMENT) private doc: Document) {
}
login(): void {
this.auth.loginWithRedirect();
}
logout(): void {
this.auth.logout({
logoutParams: {
returnTo: this.doc.location.origin
}
});
}
}
Remove the login and logout buttons from home.component.html
:
<p><a routerLink="/notes" *ngIf="auth.isAuthenticated$ | async">View Notes</a></p>
You can also remove the login()
and logout()
methods from home.component.ts
.
import { Component, Inject } from '@angular/core';
import { AuthService } from '@auth0/auth0-angular';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent {
constructor(public auth: AuthService) {
}
}
Run ng serve
and you’ll be able to see your stylish app at http://localhost:4200
.
Fix Bootstrap’s Responsive Menu
If you reduce the width of your browser window, you’ll see the menu collapse to take up less real estate.
However, if you click on it, the menu doesn’t expand. To fix that, you must use the ngbCollapse
directive from NG Bootstrap. Modify app.component.html
to have a click handler on the navbar toggle and add ngbCollapse
to the menu.
<button (click)="isCollapsed = !isCollapsed" class="navbar-toggler" ...>
...
</button>
<div [ngbCollapse]="isCollapsed" class="collapse navbar-collapse" ...>
...
</div>
Then add isCollapsed
in app.component.ts
and change the title
to be capitalized.
export class AppComponent {
title = 'Notes';
isCollapsed = true;
...
}
Now, you’ll be able to toggle the menu!
Refactor Unit Tests to Pass
You changed some elements and values that will cause tests in app.component.spec.ts
to fail. Update the tests to look for uppercase “Note” and import NgbModule
.
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
describe('AppComponent', () => {
...
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
...
NgbModule
],
...
}).compileComponents();
}));
...
it(`should have as title 'notes'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('Notes');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('nav')?.textContent).toContain('Notes');
});
});
Update the Note List Angular Template
Modify the note-list.component.html
so the search form is all on one line.
...
<h2>Notes List</h2>
<form #f="ngForm" class="row g-2">
<div class="col-auto">
<input [(ngModel)]="filter.title" type="search" name="query"
placeholder="Title" class="form-control ml-2 mr-2">
</div>
<div class="col-auto">
<button (click)="search($event)" [disabled]="!f?.valid"
class="btn btn-primary">Search</button>
<a [routerLink]="['../notes', 'new' ]"
class="btn btn-default ml-2">New</a>
</div>
</form>
...
That looks better!
Add Validation and Bootstrap to the Note Edit Template
If you click the New button, you’ll see the form needs some work, too. Bootstrap has excellent support for
stylish forms
using its form-label
and form-control
classes. note-edit.component.html
already uses these classes, but there are updates needed for Bootstrap 5.
The following HTML will add floating labels to your form using the form-floating
class and add more spacing with mb-3
.
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a routerLink="/">Home</a></li>
<li class="breadcrumb-item active">Notes</li>
</ol>
</nav>
<h2>Notes Detail</h2>
<div *ngIf="feedback.message"
class="alert alert-{{feedback.type}}">{{ feedback.message }}</div>
<form *ngIf="note" #editForm="ngForm" (ngSubmit)="save()"
class="form-floating">
<div class="mb-3">
<label>Id</label>
{{note.id || 'n/a'}}
</div>
<div class="form-floating mb-3">
<input [(ngModel)]="note.title" id="title" name="title"
class="form-control" placeholder="title">
<label for="title">Title</label>
</div>
<div class="form-floating mb-3">
<input [(ngModel)]="note.text" id="text" name="text"
class="form-control" placeholder="text">
<label for="text">Text</label>
</div>
<div class="btn-group mt-3" role="group">
<button type="submit" class="btn btn-primary"
[disabled]="!editForm.form.valid">Save</button>
<button type="button" class="btn btn-secondary ml-2"
(click)="cancel()">Cancel</button>
</div>
</form>
That’s an improvement!
To make the title
field required, add a required
attribute to its <input>
tag, along with a name so that it can be referenced in an error message.
<div class="form-floating mb-3">
<input [(ngModel)]="note.title" id="title" name="title" #name="ngModel"
class="form-control" placeholder="title" required
[ngClass]="{'is-invalid': name.touched && name.invalid,
'is-valid': name.touched && name.valid}">
<div [hidden]="name.valid" style="display: block" class="invalid-feedback">
Title is required
</div>
<label for="title">Title</label>
</div>
You might notice the expression on the [ngClass]
attribute. This adds CSS classes to the element as validation rules pass and fail. It’s a cool feature that web developers love!
When you add a new note, it’ll let you know it requires a title.
If you give it focus and leave, it’ll add a red border around the field.
Add a Data Table with Searching, Sorting, and Pagination
At the beginning of this section, I said I’d show you how to develop a searchable, sortable, and pageable data table. NG Bootstrap has a complete example I used to build the section below. The major difference is you’ll be using a real server, not a simulated one. Spring Data JPA has some slick features that make this possible, namely its query methods and paging/sorting.
Add Search by Title with Spring Data JPA
Adding search functionality requires the fewest code modifications. Change the UserController#notes()
method in your Spring Boot app to accept a title parameter and return notes with the parameter’s value in their title.
@GetMapping("/user/notes")
fun notes(principal: Principal, title: String?): List<Note> {
println("Fetching notes for user: ${principal.name}")
return if (title.isNullOrEmpty()) {
repository.findAllByUsername(principal.name)
} else {
println("Searching for title: ${title}")
repository.findAllByUsernameAndTitleContainingIgnoreCase(principal.name, title)
}
}
Add the new repository method to the NotesRepository
in DemoApplication.kt
.
@RepositoryRestResource
interface NotesRepository : JpaRepository<Note, Long> {
fun findAllByUsername(name: String): List<Note>
fun findAllByUsernameAndTitleContainingIgnoreCase(name: String, term: String): List<Note>
}
Restart your server and add a few notes, and you should be able to search for them by title in your Angular app. I love how Spring Data JPA makes this so easy!
Add Sort Functionality with Angular and Bootstrap
To begin, create a sortable.directive.ts
directive to show a direction indicator.
import { Directive, EventEmitter, Input, Output } from '@angular/core';
export type SortDirection = 'asc' | 'desc' | '';
const rotate: { [key: string]: SortDirection } = {asc: 'desc', desc: '', '': 'asc'};
export interface SortEvent {
column: string;
direction: SortDirection;
}
@Directive({
selector: 'th[sortable]',
host: {
'[class.asc]': 'direction === "asc"',
'[class.desc]': 'direction === "desc"',
'(click)': 'rotate()'
}
})
export class SortableHeaderDirective {
@Input() sortable!: string;
@Input() direction: SortDirection = '';
@Output() sort = new EventEmitter<SortEvent>();
rotate() {
this.direction = rotate[this.direction];
this.sort.emit({column: this.sortable, direction: this.direction});
}
}
Add it as a declaration in note.module.ts
.
import { SortableHeaderDirective } from './note-list/sortable.directive';
@NgModule({
...
declarations: [
...
SortableHeaderDirective
],
...
}
Add a headers
variable to note-list.component.ts
and an onSort()
method.
import { Component, OnInit, QueryList, ViewChildren } from '@angular/core';
import { SortableHeaderDirective, SortEvent } from './sortable.directive';
export class NoteListComponent implements OnInit {
@ViewChildren(SortableHeaderDirective) headers!: QueryList<SortableHeaderDirective>;
...
onSort({column, direction}: SortEvent) {
// reset other headers
this.headers.forEach(header => {
if (header.sortable !== column) {
header.direction = '';
}
});
this.filter.column = column;
this.filter.direction = direction;
this.search();
}
...
}
Update the note-filter.ts
to have column
and direction
properties.
export class NoteFilter {
title = '';
column!: string;
direction!: string;
}
Modify the find()
method in NoteService
to pass a sort
parameter when appropriate.
import { map } from 'rxjs/operators';
...
find(filter: NoteFilter): Observable<Note[]> {
const params: any = {
title: filter.title,
sort: `${filter.column},${filter.direction}`,
};
if (!filter.direction) { delete params.sort; }
const userNotes = 'http://localhost:8080/user/notes';
return this.http.get(userNotes, {params, headers}).pipe(
map((response: any) => {
return response.content;
})
);
}
Update note-list.component.html
so it uses the sortable
directive and calls onSort()
when a user clicks it.
<thead>
<tr>
<th class="border-top-0" scope="col">#</th>
<th class="border-top-0" scope="col" sortable="title"
(sort)="onSort($event)">Title</th>
<th class="border-top-0" scope="col" sortable="text"
(sort)="onSort($event)">Text</th>
<th class="border-top-0" scope="col" style="width:120px"></th>
</tr>
</thead>
Add CSS in styles.scss
to show a sort indicator when a user sorts a column.
th[sortable] {
cursor: pointer;
user-select: none;
-webkit-user-select: none;
}
th[sortable].desc:before, th[sortable].asc:before {
content: '';
display: block;
background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAmxJREFUeAHtmksrRVEUx72fH8CIGQNJkpGUUmakDEiZSJRIZsRQmCkTJRmZmJgQE0kpX0D5DJKJgff7v+ru2u3O3vvc67TOvsdatdrnnP1Y///v7HvvubdbUiIhBISAEBACQkAICAEhIAQ4CXSh2DnyDfmCPEG2Iv9F9MPlM/LHyAecdyMzHYNwR3fdNK/OH9HXl1UCozD24TCvILxizEDWIEzA0FcM8woCgRrJCoS5PIwrANQSMAJX1LEI9bqpQo4JYNFFKRSvIgsxHDVnqZgIkPnNBM0rIGtYk9YOOsqgbgepRCfdbmFtqhFkVEDVPjJp0+Z6e6hRHhqBKgg6ZDCvYBygVmUoEGoh5JTRvIJwhJo1aUOoh4CLPMyvxxi7EWOMgnCGsXXI1GIXlZUYX7ucU+kbR8NW8lh3O7cue0Pk32MKndfUxQFAwxdirk3fHappAnc0oqDPzDfGTBrCfHP04dM4oTV8cxr0SVzH9FF07xD3ib6xCDE+M+aUcVygtWzzbtGX2rPBrEUYfecfQkaFzYi6HjVnGBdtL7epqAlc1+jRdAap74RrnPc4BCijttY2tRcdN0g17w7HqZrXhdJTYAuS3hd8z+vKgK3V1zWPae0mZDMykadBn1hTQBLnZNwVrJpSe/NwEeDsEwCctEOsJTsgxLvCqUl2ACftEGvJDgjxrnBqkh3ASTvEWrIDQrwrnJpkB3DSDrGW7IAQ7wqnJtkBnLRztejXXVu4+mxz/nQ9jR1w5VB86ejLTFcnnDwhzV+F6T+CHZlx6THSjn76eyyBIOPHyDakhBAQAkJACAgBISAEhIAQYCLwC8JxpAmsEGt6AAAAAElFTkSuQmCC') no-repeat;
background-size: 22px;
width: 22px;
height: 22px;
float: left;
margin-left: -22px;
}
th[sortable].desc:before {
transform: rotate(180deg);
-ms-transform: rotate(180deg);
}
Add Sorting and Paging in Spring Boot with Spring Data JPA
On the server, you can use
Spring Data’s support for paging and sorting.
Add a Pageable
argument to UserController#notes()
and return a Page
instead of a List
.
package com.okta.developer.notes
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.security.oauth2.core.oidc.user.OidcUser
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import java.security.Principal
@RestController
class UserController(val repository: NotesRepository) {
@GetMapping("/user/notes")
fun notes(principal: Principal, title: String?, pageable: Pageable): Page<Note> {
println("Fetching notes for user: ${principal.name}")
return if (title.isNullOrEmpty()) {
repository.findAllByUsername(principal.name, pageable)
} else {
println("Searching for title: ${title}")
repository.findAllByUsernameAndTitleContainingIgnoreCase(principal.name, title, pageable)
}
}
@GetMapping("/user")
fun user(@AuthenticationPrincipal user: OidcUser): OidcUser {
return user
}
}
Modify NotesRepository
to add a Pageable
argument to its methods and return a Page
.
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
...
@RepositoryRestResource
interface NotesRepository : JpaRepository<Note, Long> {
fun findAllByUsername(name: String, pageable: Pageable): Page<Note>
fun findAllByUsernameAndTitleContainingIgnoreCase(name: String, term: String, pageable: Pageable): Page<Note>
}
While you’re updating the Spring Boot side of things, modify DataInitializer
to create a thousand notes for your user.
@Component
class DataInitializer(val repository: NotesRepository) : ApplicationRunner {
@Throws(Exception::class)
override fun run(args: ApplicationArguments) {
for (x in 0..1000) {
repository.save(Note(title = "Note ${x}", username = "<your email>"))
}
repository.findAll().forEach { println(it) }
}
}
Make sure to replace <your email>
with the email address you use to log in to Auth0.
The principal.name
will not default to the user’s email address. To fix this, you need to add an Action in Auth0 that will add the email address to the access token. Log in to your Auth0 management dashboard and go to Actions > Library > Build Custom.
Name it "Add email claim" and click Create. Replace the code with the following:
exports.onExecutePostLogin = async (event, api) => {
const namespace = 'https://angular-book.org';
if (event.authorization) {
api.accessToken.setCustomClaim(`${namespace}/email`, event.user.email);
}
};
Select Save Draft and then Deploy.
Now go to Actions > Flows > Login and add the action to the flow from the Custom panel on the right. Click Apply. The results should look as follows:
When you log in, the email address will be added to the access token.
Adjust the UserController
to use the email address from the access token instead of the principal.name
.
@GetMapping("/user/notes")
fun notes(principal: Principal, title: String?, pageable: Pageable): Page<Note> {
val jwt: JwtAuthenticationToken = principal as JwtAuthenticationToken
val email = jwt.tokenAttributes
.getOrDefault("https://angular-book.org/email", principal.name).toString()
println("Fetching notes for user: ${email}")
return if (title.isNullOrEmpty()) {
repository.findAllByUsername(email, pageable)
} else {
println("Searching for title: ${title}")
repository.findAllByUsernameAndTitleContainingIgnoreCase(email, title, pageable)
}
}
Modify the AddUserToNote
class in DemoApplication
too.
class AddUserToNote {
@HandleBeforeCreate
fun handleCreate(note: Note) {
val auth = SecurityContextHolder.getContext().authentication
val email = (auth as JwtAuthenticationToken).tokenAttributes
.getOrDefault("https://angular-book.org/email", auth.name).toString()
note.username = email
println("Creating note: $note")
}
}
Restart your Spring Boot app to make the data available for searching. Click on the Title column to see sorting in action!
Add Pagination with Angular and Bootstrap
The last feature to add is pagination with NG Bootstrap’s <ngb-pagination>
component. Begin by adding page
and size
variables (with default values) to note-filter.ts
.
export class NoteFilter {
title = '';
column!: string;
direction!: string;
page = 0;
size = 20;
}
At the bottom of note-list.component.html
(just after </table>
), add the pagination component, along with a page-size selector.
<div class="d-flex justify-content-between p-2">
<ngb-pagination [maxSize]="10" [collectionSize]="total$ | async"
[(page)]="filter.page" [pageSize]="filter.size"
(pageChange)="onPageChange(filter.page)">
</ngb-pagination>
<select class="custom-select" style="width: auto" name="pageSize"
[(ngModel)]="filter.size" (ngModelChange)="onChange(filter.size)">
<option [ngValue]="10">10 items per page</option>
<option [ngValue]="20">20 items per page</option>
<option [ngValue]="100">100 items per page</option>
</select>
</div>
You might notice "total$ | async"
in this code and wonder what it means. This is an async
pipe that subscribes to an Observable
or Promise
and returns the last value produced. It’s a handy way to subscribe to real-time updates.
Add NgbModule
as an import to note.module.ts
.
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
@NgModule({
imports: [
...
NgbModule
],
...
}
In note-list.component.ts
, add a total$
observable and set it from the search()
method. Then add an onPageChange()
method and an onChange()
method, and modify onSort()
to set the page to 0.
import { Observable } from 'rxjs';
export class NoteListComponent implements OnInit {
total$!: Observable<any>;
...
search(event?: Event): void {
if (event) {
this.filter.page = 0;
}
this.noteService.load(this.filter);
this.total$ = this.noteService.size$;
}
onChange(pageSize: number) {
this.filter.size = pageSize;
this.filter.page = 0;
this.search();
}
onPageChange(page: number) {
this.filter.page = page - 1;
this.search();
this.filter.page = page;
}
onSort({column, direction}: SortEvent) {
// reset other headers
this.headers.forEach(header => {
if (header.sortable !== column) {
header.direction = '';
}
});
this.filter.column = column;
this.filter.direction = direction;
this.filter.page = 0;
this.search();
}
}
Then update notes.service.ts
to add a size$
observable and parameters for the page size and page number.
import { BehaviorSubject } from 'rxjs';
...
export class NoteService {
...
size$ = new BehaviorSubject<number>(0);
...
find(filter: NoteFilter): Observable<Note[]> {
const params: any = {
title: filter.title,
sort: `${filter.column},${filter.direction}`,
size: filter.size,
page: filter.page
};
if (!filter.direction) { delete params.sort; }
const userNotes = 'http://localhost:8080/user/notes';
return this.http.get(userNotes, {params, headers}).pipe(
map((response: any) => {
this.size$.next(response.totalElements);
return response.content;
})
);
}
...
}
Now your note list should have a working pagination feature at the bottom. Pretty slick, eh?
Angular + Bootstrap + Spring Boot = JHipster
Phew! That was a lot of code. I hope this section has helped you see how powerful Angular and Spring Boot with Bootstrap can be!
I also wanted to let you know you can get a lot of this functionality for free with JHipster. It even has Kotlin support. You can generate a Notes CRUD app that uses Angular, Bootstrap, Spring Boot, and Kotlin in just three steps.
-
Install Node 16 for JHipster 7. If you’re using nvm, run
nvm use 16
. -
Install JHipster and KHipster:
npm install -g generator-jhipster generator-jhipster-kotlin
-
Create an
easy-notes
directory and anotes.jdl
file in it:application { config { baseName notes authenticationType oauth2 buildTool gradle searchEngine elasticsearch testFrameworks [cypress] } entities * } entity Note { title String required text TextBlob } relationship ManyToOne { Note{user(login)} to User } paginate Note with pagination
-
In a terminal, navigate to the
easy-notes
directory and run:khipster jdl notes.jdl
That’s it!
Of course, you probably want to see it running. Run the following commands to start Keycloak (as a local OAuth 2.0 server) and Elasticsearch, and launch the app.
docker-compose -f src/main/docker/keycloak.yml up -d
docker-compose -f src/main/docker/elasticsearch.yml up -d
./gradlew
Then, run npm run e2e
in another terminal window to verify everything works. Here’s a screenshot of the app’s Notes form with validation.
Want to make JHipster work with Auth0? See JHipster’s security documentation. |
Summary
In this chapter, I showed you how to use Bootstrap to make your Angular app look good, configure form validation, and add a searchable, sortable, and pageable data table feature.
I used the following resources to gather historical information about Angular and Bootstrap.
In the next section, I’ll show you how to deploy your Angular app to production. Buckle up!
You can download the code for this book’s examples from InfoQ. The angular-bootstrap directory has this chapter’s completed example.
|