Skip to content

Commit 9601331

Browse files
WEB-894 | Ng-busy lazy loader + no-data null pipe8
1 parent f82ad09 commit 9601331

8 files changed

Lines changed: 211 additions & 14 deletions

File tree

src/app/centers/create-center/create-center.component.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ <h3 matSubheader>{{ 'labels.heading.Selected Groups' | translate }}</h3>
167167
mat-raised-button
168168
color="primary"
169169
[disabled]="!centerForm.valid"
170+
[ngBusy]="loading"
171+
[busyText]="'labels.buttons.Creating...' | translate"
170172
(click)="submit()"
171173
*mifosxHasPermission="'CREATE_CENTER'"
172174
>

src/app/centers/create-center/create-center.component.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
2828
import { MatNavList, MatListSubheaderCssMatStyler } from '@angular/material/list';
2929
import { MatLine } from '@angular/material/grid-list';
3030
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
31+
import { NgBusyDirective } from '../../shared/directives/ng-busy.directive';
3132

3233
/**
3334
* Create Center component.
@@ -40,6 +41,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
4041
...STANDALONE_SHARED_IMPORTS,
4142
MatCheckbox,
4243
MatIconButton,
44+
NgBusyDirective,
4345
FaIconComponent,
4446
MatNavList,
4547
MatListSubheaderCssMatStyler,
@@ -71,6 +73,8 @@ export class CreateCenterComponent implements OnInit {
7173
groupMembers: any[] = [];
7274
/** Group Choice. */
7375
groupChoice = new UntypedFormControl('');
76+
/** True if loading. */
77+
loading = false;
7478

7579
/**
7680
* Retrieves the offices data from `resolve`.
@@ -194,8 +198,15 @@ export class CreateCenterComponent implements OnInit {
194198
};
195199
data.groupMembers = [];
196200
this.groupMembers.forEach((group: any) => data.groupMembers.push(group.id));
197-
this.centerService.createCenter(data).subscribe((response: any) => {
198-
this.router.navigate(['../centers']);
201+
202+
this.loading = true;
203+
this.centerService.createCenter(data).subscribe({
204+
next: (response: any) => {
205+
this.router.navigate(['../centers']);
206+
},
207+
error: () => {
208+
this.loading = false;
209+
}
199210
});
200211
}
201212
}

src/app/groups/create-group/create-group.component.html

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,14 @@ <h3 matSubheader>{{ 'labels.heading.Selected Clients' | translate }}</h3>
165165
<button type="button" mat-raised-button [routerLink]="['../']">
166166
{{ 'labels.buttons.Cancel' | translate }}
167167
</button>
168-
<button mat-raised-button color="primary" [disabled]="!groupForm.valid" (click)="submit()">
168+
<button
169+
mat-raised-button
170+
color="primary"
171+
[disabled]="!groupForm.valid"
172+
[ngBusy]="loading"
173+
[busyText]="'labels.buttons.Creating...' | translate"
174+
(click)="submit()"
175+
>
169176
{{ 'labels.buttons.Submit' | translate }}
170177
</button>
171178
</mat-card-actions>

src/app/groups/create-group/create-group.component.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,14 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
2929
import { MatNavList, MatListSubheaderCssMatStyler } from '@angular/material/list';
3030
import { MatLine } from '@angular/material/grid-list';
3131
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
32+
import { NgBusyDirective } from '../../shared/directives/ng-busy.directive';
3233

3334
/**
3435
* Create Group component.
3536
*/
3637
@Component({
3738
selector: 'mifosx-create-group',
39+
standalone: true,
3840
templateUrl: './create-group.component.html',
3941
styleUrls: ['./create-group.component.scss'],
4042
imports: [
@@ -43,6 +45,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
4345
MatAutocompleteTrigger,
4446
MatAutocomplete,
4547
MatIconButton,
48+
NgBusyDirective,
4649
FaIconComponent,
4750
MatNavList,
4851
MatListSubheaderCssMatStyler,
@@ -74,6 +77,8 @@ export class CreateGroupComponent implements OnInit, AfterViewInit {
7477
clientMembers: any[] = [];
7578
/** ClientChoice. */
7679
clientChoice = new UntypedFormControl('');
80+
/** True if loading. */
81+
loading = false;
7782

7883
/**
7984
* Retrieves the offices data from `resolve`.
@@ -104,12 +109,14 @@ export class CreateGroupComponent implements OnInit, AfterViewInit {
104109
*/
105110
ngAfterViewInit() {
106111
this.clientChoice.valueChanges.subscribe((value: string) => {
107-
if (value.length >= 2) {
112+
if (value && value.length >= 2) {
108113
this.clientsService
109114
.getFilteredClients('displayName', 'ASC', true, value, this.groupForm.get('officeId').value)
110115
.subscribe((data: any) => {
111116
this.clientsData = data.pageItems;
112117
});
118+
} else {
119+
this.clientsData = [];
113120
}
114121
});
115122
}
@@ -214,12 +221,19 @@ export class CreateGroupComponent implements OnInit, AfterViewInit {
214221
};
215222
data.clientMembers = [];
216223
this.clientMembers.forEach((client: any) => data.clientMembers.push(client.id));
217-
this.groupService.createGroup(data).subscribe((response: any) => {
218-
this.router.navigate([
219-
'../groups',
220-
response.resourceId,
221-
'general'
222-
]);
224+
225+
this.loading = true;
226+
this.groupService.createGroup(data).subscribe({
227+
next: (response: any) => {
228+
this.router.navigate([
229+
'../groups',
230+
response.resourceId,
231+
'general'
232+
]);
233+
},
234+
error: () => {
235+
this.loading = false;
236+
}
223237
});
224238
}
225239
}

src/app/groups/groups.component.html

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,17 @@
2929
<table mat-table [dataSource]="dataSource" matSort class="bordered-table">
3030
<ng-container matColumnDef="name">
3131
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.name' | translate }}</th>
32-
<td mat-cell *matCellDef="let group">{{ group.name }}</td>
32+
<td mat-cell *matCellDef="let group">{{ group.name ? group.name : ('labels.text.no-data' | translate) }}</td>
3333
</ng-container>
3434

3535
<ng-container matColumnDef="accountNo">
3636
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Account' | translate }} #</th>
37-
<td mat-cell *matCellDef="let group">{{ group.accountNo }}</td>
37+
<td mat-cell *matCellDef="let group">{{ group.accountNo ? group.accountNo : ('labels.text.no-data' | translate) }}</td>
3838
</ng-container>
3939

4040
<ng-container matColumnDef="externalId">
4141
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.External Id' | translate }}</th>
42-
<td mat-cell *matCellDef="let group">{{ group.externalId }}</td>
42+
<td mat-cell *matCellDef="let group">{{ group.externalId ? group.externalId : ('labels.text.no-data' | translate) }}</td>
4343
</ng-container>
4444

4545
<ng-container matColumnDef="status">
@@ -53,7 +53,7 @@
5353

5454
<ng-container matColumnDef="officeName">
5555
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Office Name' | translate }}</th>
56-
<td mat-cell *matCellDef="let group">{{ group.officeName }}</td>
56+
<td mat-cell *matCellDef="let group">{{ group.officeName ? group.officeName : ('labels.text.no-data' | translate) }}</td>
5757
</ng-container>
5858

5959
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>

src/app/shared/_busy.scss

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* Copyright since 2025 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
7+
*/
8+
9+
// ng-busy directive styles
10+
.busy-loading {
11+
position: relative;
12+
pointer-events: none;
13+
14+
.css-spinner {
15+
display: inline-block;
16+
vertical-align: middle;
17+
}
18+
}
19+
20+
// Button specific busy styles
21+
button.busy-loading {
22+
cursor: not-allowed;
23+
}
24+
25+
// Dots animation for loading indicator
26+
@keyframes dots {
27+
0%, 20% { opacity: 0; transform: scale(0.8); }
28+
50% { opacity: 1; transform: scale(1); }
29+
80%, 100% { opacity: 0; transform: scale(0.8); }
30+
}
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import { Directive, Input, ElementRef, Renderer2, OnInit, OnChanges, SimpleChanges } from '@angular/core';
2+
3+
@Directive({
4+
selector: '[ngBusy]',
5+
standalone: true
6+
})
7+
export class NgBusyDirective implements OnInit, OnChanges {
8+
@Input('ngBusy') isBusy = false;
9+
@Input() busyText = '';
10+
@Input() busyClass = 'busy-loading';
11+
12+
private originalContent: string | null = null;
13+
private originalDisabled: boolean | null = null;
14+
private spinnerElement: HTMLElement | null = null;
15+
private styleElement: HTMLStyleElement | null = null;
16+
17+
constructor(
18+
private el: ElementRef,
19+
private renderer: Renderer2
20+
) {}
21+
22+
ngOnInit() {
23+
// Store original content and state
24+
this.originalContent = this.el.nativeElement.innerHTML;
25+
this.originalDisabled = this.el.nativeElement.disabled;
26+
27+
// Add global styles for animation
28+
this.addGlobalStyles();
29+
}
30+
31+
ngOnChanges(changes: SimpleChanges) {
32+
if (changes['isBusy']) {
33+
this.updateBusyState();
34+
}
35+
}
36+
37+
private updateBusyState() {
38+
if (this.isBusy) {
39+
this.showBusyState();
40+
} else {
41+
this.hideBusyState();
42+
}
43+
}
44+
45+
private showBusyState() {
46+
// Add busy class
47+
this.renderer.addClass(this.el.nativeElement, this.busyClass);
48+
49+
// Disable element
50+
this.renderer.setAttribute(this.el.nativeElement, 'disabled', 'true');
51+
52+
// Create busy content
53+
const busyContent = this.createBusyContent();
54+
55+
// Clear and set busy content
56+
this.el.nativeElement.innerHTML = '';
57+
this.el.nativeElement.appendChild(busyContent);
58+
}
59+
60+
private hideBusyState() {
61+
// Remove busy class
62+
this.renderer.removeClass(this.el.nativeElement, this.busyClass);
63+
64+
// Restore original disabled state
65+
if (this.originalDisabled) {
66+
this.renderer.setAttribute(this.el.nativeElement, 'disabled', 'true');
67+
} else {
68+
this.renderer.removeAttribute(this.el.nativeElement, 'disabled');
69+
}
70+
71+
// Restore original content
72+
if (this.originalContent) {
73+
this.el.nativeElement.innerHTML = this.originalContent;
74+
}
75+
}
76+
77+
private createBusyContent(): HTMLElement {
78+
const container = this.renderer.createElement('span');
79+
this.renderer.setStyle(container, 'display', 'inline-flex');
80+
this.renderer.setStyle(container, 'align-items', 'center');
81+
this.renderer.setStyle(container, 'gap', '8px');
82+
83+
// Create CSS spinner with proper rotation
84+
this.spinnerElement = this.renderer.createElement('span');
85+
this.renderer.addClass(this.spinnerElement, 'css-spinner');
86+
87+
// Create spinner using CSS border technique
88+
this.renderer.setStyle(this.spinnerElement, 'display', 'inline-block');
89+
this.renderer.setStyle(this.spinnerElement, 'width', '16px');
90+
this.renderer.setStyle(this.spinnerElement, 'height', '16px');
91+
this.renderer.setStyle(this.spinnerElement, 'border', '2px solid rgba(255, 255, 255, 0.3)');
92+
this.renderer.setStyle(this.spinnerElement, 'border-top', '2px solid #ffffff');
93+
this.renderer.setStyle(this.spinnerElement, 'border-radius', '50%');
94+
this.renderer.setStyle(this.spinnerElement, 'animation', 'spin 1s linear infinite');
95+
this.renderer.setStyle(this.spinnerElement, 'vertical-align', 'middle');
96+
97+
// Create text
98+
const text = this.busyText || 'Loading...';
99+
const textNode = this.renderer.createText(text);
100+
101+
// Assemble
102+
container.appendChild(this.spinnerElement);
103+
container.appendChild(textNode);
104+
105+
return container;
106+
}
107+
108+
private addGlobalStyles() {
109+
// Add proper rotation animation
110+
if (!this.styleElement) {
111+
this.styleElement = this.renderer.createElement('style');
112+
this.renderer.setProperty(this.styleElement, 'innerHTML', `
113+
@keyframes spin {
114+
0% { transform: rotate(0deg); }
115+
100% { transform: rotate(360deg); }
116+
}
117+
118+
.css-spinner {
119+
border: 2px solid rgba(255, 255, 255, 0.3) !important;
120+
border-top: 2px solid #ffffff !important;
121+
}
122+
123+
button .css-spinner {
124+
border: 2px solid rgba(255, 255, 255, 0.3) !important;
125+
border-top: 2px solid #ffffff !important;
126+
}
127+
`);
128+
this.renderer.appendChild(document.head, this.styleElement);
129+
}
130+
}
131+
}

src/assets/translations/en-US.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,7 @@
571571
"Show less": "Show less",
572572
"Show more": "Show more",
573573
"Signing in...": "Signing in...",
574+
"Creating...": "Creating...",
574575
"Staff": "Staff",
575576
"Staff Assignment History": "Staff Assignment History",
576577
"Subledger Account": "Subledger Account",
@@ -3094,6 +3095,7 @@
30943095
"Add Role": "Add Role",
30953096
"Add customized reports and edit core reports": "You may add customized reports and edit core reports for your organization.",
30963097
"Loading data": "Loading data",
3098+
"no-data": "-",
30973099
"No repayment schedule available": "No repayment schedule available",
30983100
"Add Job Step to Workflow": "Add Job Step to Workflow",
30993101
"Add new extra fields to any entity": "Add new extra fields to any entity in the form of data table",

0 commit comments

Comments
 (0)