Skip to content

Commit c07a973

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

8 files changed

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

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)