Clarity Server-Driven DataGrid
Overview
Clarity provides Server-Driven DataGrid. Using Query DSL we will fetch page by page data and render it in clarity server-driven data grid
Github: https://github.com/gitorko/project86
Quick Overview
To deploy the application in a single command, clone the project, make sure no conflicting docker containers or ports are running and then run
1git clone https://github.com/gitorko/project86
2cd project86
3docker-compose -f docker/docker-compose.yml up
Server-Driven DataGrid
When dealing with large amounts of data or heavy processing, a DataGrid often has to access the currently displayed data only, requesting only the necessary pieces of data from the server.
Implementation
Design
Code
1package com.demo.project86.controller;
2
3import com.demo.project86.domain.Customer;
4import com.demo.project86.domain.CustomerBinderCustomizer;
5import com.demo.project86.repo.CustomerRepository;
6import lombok.RequiredArgsConstructor;
7import lombok.extern.slf4j.Slf4j;
8import org.springframework.beans.factory.annotation.Autowired;
9import org.springframework.data.domain.Page;
10import org.springframework.data.domain.Pageable;
11import org.springframework.data.querydsl.binding.QuerydslPredicate;
12import org.springframework.data.web.PageableDefault;
13import org.springframework.web.bind.annotation.DeleteMapping;
14import org.springframework.web.bind.annotation.GetMapping;
15import org.springframework.web.bind.annotation.PathVariable;
16import org.springframework.web.bind.annotation.PostMapping;
17import org.springframework.web.bind.annotation.RequestBody;
18import org.springframework.web.bind.annotation.RestController;
19
20@RestController
21@Slf4j
22@RequiredArgsConstructor
23public class HomeController {
24
25 @Autowired
26 CustomerRepository customerRepo;
27
28 @GetMapping(value = "/api/customer")
29 public Page<Customer> getCustomers(@PageableDefault(size = 20) Pageable pageRequest,
30 @QuerydslPredicate(root = Customer.class, bindings = CustomerBinderCustomizer.class) com.querydsl.core.types.Predicate predicate) {
31 return customerRepo.findAll(predicate, pageRequest);
32 }
33
34 @PostMapping(value = "/api/customer")
35 public Customer saveCustomer(@RequestBody Customer customer) {
36 log.info("Saving customer!");
37 return customerRepo.save(customer);
38 }
39
40 @DeleteMapping(value = "/api/customer/{id}")
41 public void deleteCustomer(@PathVariable Long id) {
42 log.info("Deleting customer: {}", id);
43 customerRepo.deleteById(id);
44 }
45
46}
1package com.demo.project86.domain;
2
3import java.io.Serializable;
4import jakarta.persistence.Basic;
5import jakarta.persistence.Column;
6import jakarta.persistence.Entity;
7import jakarta.persistence.GeneratedValue;
8import jakarta.persistence.GenerationType;
9import jakarta.persistence.Id;
10import jakarta.persistence.Table;
11import jakarta.validation.constraints.Size;
12
13import lombok.AllArgsConstructor;
14import lombok.Builder;
15import lombok.Data;
16import lombok.NoArgsConstructor;
17
18@Entity
19@Table(name = "customer")
20@Data
21@Builder
22@AllArgsConstructor
23@NoArgsConstructor
24public class Customer implements Serializable {
25
26 private static final long serialVersionUID = 1L;
27 @Id
28 @GeneratedValue(strategy = GenerationType.AUTO)
29 @Basic(optional = false)
30 @Column(name = "id")
31 private Long id;
32 @Size(max = 45)
33 @Column(name = "first_name")
34 private String firstName;
35 @Size(max = 45)
36 @Column(name = "last_name")
37 private String lastName;
38 private String city;
39
40}
1package com.demo.project86.domain;
2
3import java.util.Collection;
4import java.util.Optional;
5
6import com.querydsl.core.BooleanBuilder;
7import com.querydsl.core.types.Predicate;
8import com.querydsl.core.types.dsl.StringPath;
9import org.springframework.data.querydsl.binding.MultiValueBinding;
10import org.springframework.data.querydsl.binding.QuerydslBinderCustomizer;
11import org.springframework.data.querydsl.binding.QuerydslBindings;
12
13public class CustomerBinderCustomizer implements QuerydslBinderCustomizer<QCustomer> {
14
15 @Override
16 public void customize(QuerydslBindings querydslBindings, QCustomer qCustomer) {
17 querydslBindings.including(
18 qCustomer.id,
19 qCustomer.firstName,
20 qCustomer.lastName,
21 qCustomer.city
22 );
23
24 StringPath[] multiPropertySearchPaths = new StringPath[]{qCustomer.firstName, qCustomer.lastName, qCustomer.city};
25
26 querydslBindings.bind(multiPropertySearchPaths).all(new MultiValueBinding<>() {
27 @Override
28 public Optional<Predicate> bind(StringPath path, Collection<? extends String> values) {
29 BooleanBuilder predicate = new BooleanBuilder();
30 values.forEach(value -> predicate.or(path.containsIgnoreCase(value)));
31 return Optional.of(predicate);
32 }
33 });
34
35 }
36}
1package com.demo.project86.repo;
2
3import com.demo.project86.domain.Customer;
4import org.springframework.data.jpa.repository.JpaRepository;
5import org.springframework.data.querydsl.QuerydslPredicateExecutor;
6
7public interface CustomerRepository extends JpaRepository<Customer, Long>, QuerydslPredicateExecutor<Customer> {
8}
1<div class="content-container">
2 <div class="content-area">
3 <div class="alert-section">
4 <app-alert></app-alert>
5 </div>
6
7 <div class="clr-row">
8 <div class="clr-col-12">
9 <h2 style="text-align: center">Customers</h2>
10 <clr-datagrid [clrDgLoading]="loading" (clrDgRefresh)="refresh($event)">
11 <clr-dg-placeholder class="content-center">No Customers!</clr-dg-placeholder>
12 <clr-dg-column [clrDgField]="'id'">ID</clr-dg-column>
13 <clr-dg-column [clrDgField]="'firstName'">
14 <ng-container *clrDgHideableColumn="{hidden: false}">First Name</ng-container>
15 </clr-dg-column>
16 <clr-dg-column [clrDgField]="'lastName'">
17 <ng-container *clrDgHideableColumn="{hidden: false}">Last Name</ng-container>
18 </clr-dg-column>
19 <clr-dg-column [clrDgField]="'city'">
20 <ng-container *clrDgHideableColumn="{hidden: false}">City</ng-container>
21 <clr-dg-filter [clrDgFilter]="cityFilter">
22 <app-checkbox-filter
23 #cityFilter
24 [filterValues]="cityFilterValues"
25 filterKey="city"></app-checkbox-filter>
26 </clr-dg-filter>
27 </clr-dg-column>
28 <clr-dg-column>Action</clr-dg-column>
29 <!-- structural directive -->
30 <clr-dg-row *ngFor="let customer of customerPage?.content">
31 <clr-dg-cell>{{customer.id}}</clr-dg-cell>
32 <clr-dg-cell>{{customer.firstName}}</clr-dg-cell>
33 <clr-dg-cell>{{customer.lastName}}</clr-dg-cell>
34 <clr-dg-cell>{{customer.city}}</clr-dg-cell>
35 <clr-dg-cell>
36 <cds-icon shape="trash" style="cursor: pointer; color: blue" (click)="deleteCustomer(customer)">
37 </cds-icon>
38 </clr-dg-cell>
39 </clr-dg-row>
40
41 <clr-dg-footer>
42 <clr-dg-pagination #pagination [clrDgPageSize]="10" [(clrDgPage)]="page"
43 [clrDgTotalItems]="total">
44 <clr-dg-page-size [clrPageSizeOptions]="[10,20,50,100]">Customers per page</clr-dg-page-size>
45 {{pagination.firstItem + 1}} - {{pagination.lastItem + 1}} of {{pagination.totalItems}} customers
46 </clr-dg-pagination>
47 </clr-dg-footer>
48
49 </clr-datagrid>
50 </div>
51 </div>
52
53 <div class="clr-row">
54 <div class="clr-col-12">
55 <form class="clr-form clr-form-horizontal">
56 <div class="clr-form-control">
57 <label for="firstName" class="clr-control-label">First Name</label>
58 <div class="clr-control-container">
59 <div class="clr-input-wrapper">
60 <!-- two way data binding -->
61 <input type="text" [(ngModel)]="customer.firstName" id="firstName" name="firstName"
62 placeholder="Placeholder" class="clr-input"/>
63 </div>
64 </div>
65 </div>
66 <div class="clr-form-control">
67 <label for="lastName" class="clr-control-label">Last Name</label>
68 <div class="clr-control-container">
69 <div class="clr-input-wrapper">
70 <input [(ngModel)]="customer.lastName" type="text" id="lastName" name="lastName"
71 placeholder="Placeholder" class="clr-input"/>
72 </div>
73 </div>
74 </div>
75 <div class="clr-form-control">
76 <div class="clr-control-container">
77 <!-- event binding -->
78 <button type="submit" class="btn btn-primary" (click)="saveCustomer()">Save</button>
79 </div>
80 </div>
81 </form>
82 </div>
83 </div>
84
85 </div>
86</div>
The debounceTime added to debounce the events so that rest api doesn't get called for every keystroke.
1import {Component, OnInit, ViewChild} from '@angular/core';
2import {Customer} from '../../models/customer';
3import {RestService} from '../../services/rest.service';
4import {Router} from '@angular/router';
5import {ClarityIcons, trashIcon} from '@cds/core/icon';
6import {ClrDatagridStateInterface} from '@clr/angular';
7import {CustomerPage} from "../../models/customer-page";
8import {AlertComponent} from "../alert/alert.component";
9import {Subject} from "rxjs";
10import {debounceTime} from "rxjs/operators";
11
12@Component({
13 selector: 'app-home',
14 templateUrl: './home.component.html',
15 styleUrls: []
16})
17export class HomeComponent implements OnInit {
18
19 customerPage: CustomerPage = new CustomerPage();
20 customer: Customer = new Customer();
21 loading = false;
22 page: number = 1;
23 total: number = 1;
24 cityFilterValues: string[] = [];
25 tableState: ClrDatagridStateInterface = {page: {current: 1, from: 1, size: 10, to: 10}};
26 debouncer: Subject<any> = new Subject<any>();
27
28 // @ts-ignore
29 @ViewChild(AlertComponent, {static: true}) private alert: AlertComponent;
30
31 constructor(private restService: RestService, private router: Router) {
32 ClarityIcons.addIcons(trashIcon);
33 this.cityFilterValues.push("Bangalore");
34 this.cityFilterValues.push("New York");
35 this.cityFilterValues.push("London");
36 }
37
38 ngOnInit(): void {
39 this.loading = true;
40 this.debouncer
41 .pipe(debounceTime(700))
42 .subscribe(state => {
43 this.tableState = state;
44 this.loading = true;
45 if (!state.page) {
46 state.page = {
47 from: 1,
48 to: 10,
49 size: 10,
50 };
51 }
52 // @ts-ignore
53 let pageStart = state.page.current - 1;
54 let pageSize = state.page.size;
55 this.restService.getCustomers(pageStart, pageSize, state.filters, state.sort).subscribe(data => {
56 this.customerPage = data;
57 this.total = this.customerPage?.totalElements;
58 this.loading = false;
59 },
60 error => {
61 this.loading = false;
62 });
63 }
64 );
65 }
66
67 saveCustomer(): void {
68 console.log('save customer!');
69 this.restService.saveCustomer(this.customer)
70 .subscribe(data => {
71 this.alert.showSuccess('Saved customer: ' + this.customer.firstName);
72 this.refresh(this.tableState);
73 });
74 }
75
76 deleteCustomer(customer: Customer): void {
77 console.log('deleting customer : ' + customer.id);
78 this.restService.deleteCustomer(customer.id)
79 .subscribe(data => {
80 this.alert.showSuccess('Deleted customer: ' + customer.id);
81 this.refresh(this.tableState);
82 });
83 }
84
85 refresh(state: ClrDatagridStateInterface) {
86 this.debouncer.next(state);
87 }
88
89}
Setup
1# Project 86
2
3Clarity - Server Driven Data Grid with QueryDSL
4
5[https://gitorko.github.io/clarity-server-driven-datagrid/](https://gitorko.github.io/clarity-server-driven-datagrid/)
6
7### Version
8
9Check version
10
11```bash
12$java --version
13openjdk version "21.0.3" 2024-04-16 LTS
14
15node --version
16v16.16.0
17
18yarn --version
191.22.18
20```
21
22### Postgres DB
23
24```
25docker run -p 5432:5432 --name pg-container -e POSTGRES_PASSWORD=password -d postgres:9.6.10
26docker ps
27docker exec -it pg-container psql -U postgres -W postgres
28CREATE USER test WITH PASSWORD 'test@123';
29CREATE DATABASE "test-db" WITH OWNER "test" ENCODING UTF8 TEMPLATE template0;
30grant all PRIVILEGES ON DATABASE "test-db" to test;
31
32docker stop pg-container
33docker start pg-container
34```
35
36### Dev
37
38To Run backend in dev mode
39
40```bash
41cd project86
42./gradlew bootRun
43```
44
45To Run UI in dev mode
46
47```bash
48cd ui
49yarn install
50yarn build
51yarn start
52```
53
54Open [http://localhost:4200/](http://localhost:4200/)
55
56### Prod
57
58To run as a single jar, both UI and backend are bundled to single uber jar.
59
60```bash
61./gradlew cleanBuild
62cd project86/build/libs
63java -jar project86-1.0.0.jar
64```
65
66Open [http://localhost:8080/](http://localhost:8080/)
67
68### Docker
69
70```bash
71./gradlew cleanBuild
72docker build -f docker/Dockerfile --force-rm -t project86:1.0.0 .
73docker images |grep project86
74docker tag project86:1.0.0 gitorko/project86:1.0.0
75docker push gitorko/project86:1.0.0
76docker-compose -f docker/docker-compose.yml up
77```
References
https://clarity.design/angular-components/datagrid/#server-driven-datagrid
comments powered by Disqus