In the previous article, we have implemented server-side pagination. Now we are gonna add a date field and use the Angular Material Datepicker with custom date format. Then, we will store the selected date in the backend, retrieve and format the date to display it on the list page.
What You’ll Build
Angular Frontend
Add/Edit Page
List Page
Spring Boot Backend
Modify the Product
and ProductRequest
classes to include the new date field as java.time.LocalDate
type.
What You’ll Need
Run the following checklist before you begin the implementation:
- Spring Tool Suite 4 or any other IDE of your choice
- Lombok
- JDK 11
- Maven
- Spring Boot + Angular Application Source Code
Angular Client Implementation
Add Angular Material Datepicker Dependencies
Angular Material Datepicker requires the material-moment-adapter
library in order to customize the date format. This library in turn depends on the moment
library. So let’s install both of them
Install Angular Material Moment Adapter
ng add @angular/material-moment-adapter
Output
i Using package manager: npm
√ Found compatible package version: @angular/[email protected].
√ Package information loaded.
The package @angular/[email protected] will be installed and executed.
Would you like to proceed? Yes
√ Package successfully installed.
Install Moment
ng add moment
Output
i Using package manager: npm
√ Found compatible package version: [email protected].
√ Package information loaded.
The package [email protected] will be installed and executed.
Would you like to proceed? Yes
√ Package successfully installed.
Modify Product Model Class
Lets add a date field validFrom
as type string
.
product.ts
export interface Product {
id: number;
name: string;
version: string;
edition: string;
description: string;
validFrom: string;
}
Modify Product Add/Edit Component
add-edit.component.ts
import { Component, OnInit } from '@angular/core';
import { ProductService } from '../_services/product.service';
import { Router, ActivatedRoute } from '@angular/router';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { first } from 'rxjs/operators';
import { DateAdapter, MAT_DATE_FORMATS, MAT_DATE_LOCALE } from '@angular/material/core';
import { MomentDateAdapter, MAT_MOMENT_DATE_ADAPTER_OPTIONS } from '@angular/material-moment-adapter';
export const APP_DATE_FORMATS = {
parse: {
dateInput: 'DD/MM/YYYY',
},
display: {
dateInput: 'DD/MM/YYYY',
monthYearLabel: 'MMMM YYYY',
dateA11yLabel: 'LL',
monthYearA11yLabel: 'MMMM YYYY'
},
};
@Component({
templateUrl: './add-edit.component.html',
providers: [
{ provide: MAT_DATE_FORMATS, useValue: APP_DATE_FORMATS },
{ provide: DateAdapter, useClass: MomentDateAdapter, deps: [MAT_DATE_LOCALE, MAT_MOMENT_DATE_ADAPTER_OPTIONS]},
{ provide: MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: { useUtc: true } }
]})
export class ProductAddEditComponent implements OnInit {
id: any;
form: FormGroup;
errorMessage;
constructor(public productService: ProductService, private router: Router, private fb: FormBuilder, private route: ActivatedRoute) {
this.createForm();
}
ngOnInit(): void {
this.id = this.route.snapshot.paramMap.get('id');
console.log(this.id);
if(this.id){
this.productService.find(this.id).subscribe(x => this.form.patchValue(x));
}
}
createForm() {
this.form = this.fb.group({
name: ['', [
Validators.required,
Validators.minLength(3),
Validators.maxLength(20),
] ],
description: ['', [
Validators.required,
Validators.minLength(3),
Validators.maxLength(100),
] ],
version: ['', Validators.required],
edition: ['', Validators.maxLength(20)],
validFrom: ['', Validators.required]
});
}
onSubmit(){
var response = this.id ? this.productService.update(this.id, this.form.value) : this.productService.create(this.form.value);
response.subscribe(
data => {
console.log('Product created / updated successfully!');
this.router.navigateByUrl('products/list');
}
,
err => {
this.errorMessage = err.error.message;
}
);
}
}
Let’s break it down to understand what each change does:
To create a Datepicker, we need a DateAdapter
that can be provided by MatNativeDateModule
, MatMomentDateModule
, or a custom implementation.
The MatNativeDateModule
uses native JavaScript Date and One of the biggest shortcomings of the native Date
object is the inability to set the parse format.
The MatMomentDateModule
uses moment
from moment.js
. So the MomentDateAdapter
provided by this module or a custom DateAdapter
can set parse format. It is recommended to use an adapter based on a more robust formatting and parsing library.
The MAT_DATE_FORMATS
object is a collection of formats that the datepicker uses when parsing and displaying dates. These formats are passed through to the DateAdapter
so we need to make sure that the format objects we’re using are compatible with the DateAdapter
used in the app.
If you want to use one of the DateAdapters
that ships with Angular Material, but use your own MAT_DATE_FORMATS
, you can import the NativeDateModule
or MomentDateModule
. These modules are identical to the “Mat”-prefixed versions (MatNativeDateModule
and MatMomentDateModule
) except they do not include the default formats. This is what we have done now:
- Imported
since we don’t want the default formats provided byMomentDateModule
MatMomentDateModule
- Defined our own
APP_DATE_FORMATS
withDD/MM/YYYY
format for parsing and displaying by Datepicker and configured it asMAT_DATE_FORMATS
provider. - Configured
MomentDateAdapter
as theDateAdapter
provider. - By default the
MomentDateAdapter
creates dates in our time zone specific locale. So we have changed the default behaviour to parse dates as UTC by providing theMAT_MOMENT_DATE_ADAPTER_OPTIONS
and setting it touseUtc: true
. - Added the date field into the form group.
add-edit.component.html
The Angular Material datepicker allows users to enter a date either through text input or by choosing a date from the calendar. It is composed of a text input and a calendar pop-up, connected via the matDatepicker
property on the text input.
<div class="row">
<div class="col s12">
<label for="validFrom">Valid From</label>
<mat-form-field appearance="outline"> <mat-label>Choose a date</mat-label> <input matInput [matDatepicker]="datepicker"
formControlName="validFrom" name="validFrom"> <mat-datepicker-toggle matSuffix [for]="datepicker" style='width: 1em'></mat-datepicker-toggle> <mat-datepicker
#datepicker> <mat-datepicker-actions>
<button mat-button matDatepickerCancel>Cancel</button>
<button mat-raised-button color="primary" matDatepickerApply>Apply</button>
</mat-datepicker-actions> </mat-datepicker> </mat-form-field>
<div class="alert-danger">
<div *ngIf="form.controls.validFrom.touched && form.controls.validFrom.errors?.required">* Required</div>
</div>
</div>
</div>
Modify Product List Component
list.component.html
The date is retrieved from the backend in ISO format. So we are formatting the date in dd/MM/yyyy
and displaying in the list here.
<table class="table table-bordered p-0 m-0">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Name</th>
<th scope="col">Description</th>
<th scope="col">Version</th>
<th scope="col">Edition</th>
<th scope="col">License Valid From</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
<tr scope="row" *ngFor="let product of products">
<td>{{ product?.id }}</td>
<td>{{ product?.name }}</td>
<td>{{ product?.description}}</td>
<td>{{ product?.version }}</td>
<td>{{ product?.edition }}</td>
<td>{{ product?.validFrom | date: 'dd/MM/yyyy'}}</td>
<td><a href="#" [routerLink]="['/products/', product.id, 'view']" class="btn btn-info m-1">View</a> <a href="#"
[routerLink]="['/products/', product.id, 'edit']" class="btn btn-primary m-1">Edit</a>
<button type="button" (click)="deleteProduct(product.id)" class="btn btn-danger m-1">Delete</button></td>
</tr>
<tr scope="row" *ngIf="products.length === 0">
<td colspan="6" class="text-center">No record found</td>
</tr>
</tbody>
</table>
Define Module
app.module.ts
Import and add MatDatepickerModule, MatInputModule
, and MomentDateModule
in the module declarations
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppRoutingModule } from './app-routing.module';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatInputModule } from '@angular/material/input';
import { MomentDateModule } from '@angular/material-moment-adapter';
import { AppComponent } from './app.component';
import { LoginComponent } from './login/login.component';
import { RegisterComponent } from './register/register.component';
import { HomeComponent } from './home/home.component';
import { ProfileComponent } from './profile/profile.component';
import { BoardAdminComponent } from './board-admin/board-admin.component';
import { BoardModeratorComponent } from './board-moderator/board-moderator.component';
import { BoardUserComponent } from './board-user/board-user.component';
import { TotpComponent } from './totp/totp.component';
import { OrderComponent } from './order/order.component';
import { TokenComponent } from './register/token.component';
import { ProductListComponent } from './products/list.component';
import { ProductViewComponent } from './products/view.component';
import { ProductAddEditComponent } from './products/add-edit.component';
import { authInterceptorProviders } from './_helpers/auth.interceptor';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
declarations: [
AppComponent,
LoginComponent,
RegisterComponent,
HomeComponent,
ProfileComponent,
BoardAdminComponent,
BoardModeratorComponent,
BoardUserComponent,
TotpComponent,
OrderComponent,
TokenComponent,
ProductListComponent,
ProductViewComponent,
ProductAddEditComponent
],
imports: [
BrowserModule,
AppRoutingModule,
FormsModule,
ReactiveFormsModule,
HttpClientModule,
MatPaginatorModule,
MatDatepickerModule,
MatInputModule,
MomentDateModule,
BrowserAnimationsModule
],
providers: [authInterceptorProviders],
bootstrap: [AppComponent]
})
export class AppModule { }
Spring Boot Backend Implementation
Add Date Field into Entity and DTO Classes
Product.java
package com.javachinna.model;
import java.time.LocalDate;
import javax.persistence.Entity;
import javax.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Setter;
import lombok.ToString;
@Entity
@Table(name = "product")
@Getter
@Setter
@ToString
@RequiredArgsConstructor
@NoArgsConstructor
public class Product extends BaseEntity {
/**
*
*/
private static final long serialVersionUID = 1493867573442690205L;
@NonNull
private String name;
@NonNull
private String version;
@NonNull
private String edition;
@NonNull
private String description;
@NonNull
private LocalDate validFrom;
}
ProductRequest.java
package com.javachinna.dto;
import java.time.LocalDate;
import javax.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class ProductRequest {
@NotBlank
private String name;
@NotBlank
private String version;
private String edition;
@NotBlank
private String description;
private LocalDate validFrom;
}
Run Spring Boot App with Maven
You can run the application with mvn clean spring-boot:run
and the REST API services can be accessed via http://localhost:8080
Run the Angular App
You can run this App with the below command and hit the URL http://localhost:8081/ in the browser.
ng serve --port 8081
Source Code
https://github.com/JavaChinna/angular-spring-boot-datepicker
Conclusion
That’s all folks. In this article, we have added a date field with datepicker support using Angular Material in our CRUD application.
Thank you for reading.