@fagon/ngx-intellitoolx 16.0.4 → 16.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +582 -13
- package/esm2022/lib/form-changes/can-component-deactivate.interface.mjs +2 -0
- package/esm2022/lib/form-changes/confirm-handler.type.mjs +2 -0
- package/esm2022/lib/form-changes/form-changes-tracker.service.mjs +58 -0
- package/esm2022/lib/form-changes/form-update-message-config.interface.mjs +2 -0
- package/esm2022/lib/form-changes/form-update-message.component.mjs +53 -0
- package/esm2022/lib/form-changes/unsaved-changes.guard.mjs +29 -0
- package/esm2022/lib/helpers/intellitoolx.helper.mjs +2 -3
- package/esm2022/public-api.mjs +15 -14
- package/fesm2022/fagon-ngx-intellitoolx.mjs +197 -134
- package/fesm2022/fagon-ngx-intellitoolx.mjs.map +1 -1
- package/lib/form-changes/can-component-deactivate.interface.d.ts +5 -0
- package/lib/form-changes/form-changes-tracker.service.d.ts +15 -0
- package/lib/{form-update → form-changes}/unsaved-changes.guard.d.ts +5 -5
- package/lib/helpers/intellitoolx.helper.d.ts +1 -1
- package/package.json +1 -1
- package/public-api.d.ts +11 -9
- package/esm2022/lib/form-update/confirm-handler.type.mjs +0 -2
- package/esm2022/lib/form-update/form-update-message-config.interface.mjs +0 -2
- package/esm2022/lib/form-update/form-update-message.component.mjs +0 -53
- package/esm2022/lib/form-update/unsaved-changes.guard.mjs +0 -19
- /package/lib/{form-update → form-changes}/confirm-handler.type.d.ts +0 -0
- /package/lib/{form-update → form-changes}/form-update-message-config.interface.d.ts +0 -0
- /package/lib/{form-update → form-changes}/form-update-message.component.d.ts +0 -0
package/README.md
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
# IntelliToolx
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
3
6
|

|
|
4
|
-

|
|
8
|
+

|
|
6
9
|

|
|
7
10
|

|
|
11
|
+

|
|
12
|
+

|
|
8
13
|
|
|
9
14
|
## Overview
|
|
10
15
|
|
|
@@ -17,7 +22,7 @@ Built with scalability, accessibility, and maintainability in mind, IntelliToolx
|
|
|
17
22
|
### Installation
|
|
18
23
|
|
|
19
24
|
```bash
|
|
20
|
-
npm
|
|
25
|
+
npm intall @fagon/ngx-intellitoolx
|
|
21
26
|
```
|
|
22
27
|
|
|
23
28
|
# IntelliToolxHelper
|
|
@@ -562,7 +567,7 @@ The component includes built-in messages for common validators:
|
|
|
562
567
|
| maxMonthYear | Date is later than allowed |
|
|
563
568
|
| minMonthYear | Date is earlier than allowed |
|
|
564
569
|
| exceededAllowedDateDifference | Date difference can only be one month |
|
|
565
|
-
|
|
|
570
|
+
| startDateAfterEndDate | Start date cannot be greater than end date |
|
|
566
571
|
|
|
567
572
|
### Adding Control Labels (User-Friendly Messages)
|
|
568
573
|
|
|
@@ -675,12 +680,6 @@ The component intelligently handles different validator outputs:
|
|
|
675
680
|
- String { customError: 'Invalid value provided' } → displays string directly
|
|
676
681
|
- Object { minlength: { requiredLength: 5 } } → dynamic message rendering
|
|
677
682
|
|
|
678
|
-
Built-in Smart Messages
|
|
679
|
-
|
|
680
|
-
- Pattern Errors Display: `Please provide a valid {controlLabel}`
|
|
681
|
-
- Min / Max Validators Display: `Age cannot be less than 18` / `Price cannot be greater than 100`
|
|
682
|
-
- Ngb Date Errors Display: `Invalid date format provided`
|
|
683
|
-
|
|
684
683
|
Best Practices
|
|
685
684
|
|
|
686
685
|
- Always provide controlLabel for better UX
|
|
@@ -1124,7 +1123,7 @@ These validators return error keys compatible with the error component:
|
|
|
1124
1123
|
| passwordMismatchValidator | passwordMismatch |
|
|
1125
1124
|
| uniqueEmailsValidator | duplicateEmail |
|
|
1126
1125
|
|
|
1127
|
-
Best Practices
|
|
1126
|
+
** Best Practices **
|
|
1128
1127
|
|
|
1129
1128
|
- Use with Reactive Forms only
|
|
1130
1129
|
- Combine with required validators when necessary
|
|
@@ -1141,5 +1140,575 @@ this.form = new FormGroup({
|
|
|
1141
1140
|
});
|
|
1142
1141
|
```
|
|
1143
1142
|
|
|
1144
|
-
|
|
1145
|
-
|
|
1143
|
+
# Unsaved Changes Protection
|
|
1144
|
+
|
|
1145
|
+
Protect Angular routes and modals from accidental navigation or closing when there are unsaved changes.
|
|
1146
|
+
Supports:
|
|
1147
|
+
|
|
1148
|
+
1. Angular route navigation
|
|
1149
|
+
2. Browser back button
|
|
1150
|
+
3. Browser refresh / tab close
|
|
1151
|
+
4. Modal close (NG Bootstrap / Material / custom)
|
|
1152
|
+
5. Parent + child component setups
|
|
1153
|
+
6. Nested forms
|
|
1154
|
+
|
|
1155
|
+
### The system is built around three core pieces:
|
|
1156
|
+
|
|
1157
|
+
1. FormChangesTrackerService
|
|
1158
|
+
2. UnsavedChangesGuard
|
|
1159
|
+
3. CanComponentDeactivate interface (for route protection)
|
|
1160
|
+
|
|
1161
|
+
Architecture:
|
|
1162
|
+
|
|
1163
|
+
```
|
|
1164
|
+
Form → TrackerService → Guard → Confirmation → Continue / Block
|
|
1165
|
+
```
|
|
1166
|
+
|
|
1167
|
+
The library does NOT depend on your UI layer.
|
|
1168
|
+
Your app controls the confirmation modal.
|
|
1169
|
+
Core Exports
|
|
1170
|
+
|
|
1171
|
+
```ts
|
|
1172
|
+
import { FormChangesTrackerService, UnsavedChangesGuard, CanComponentDeactivate } from "ngx-intellitoolx";
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
## FormChangesTrackerService API
|
|
1176
|
+
|
|
1177
|
+
The FormChangesTrackerService tracks changes in Angular forms and provides methods for managing form state across your application.
|
|
1178
|
+
|
|
1179
|
+
1. Single Form Operations
|
|
1180
|
+
|
|
1181
|
+
### register(form)
|
|
1182
|
+
|
|
1183
|
+
Registers a single form for change tracking.
|
|
1184
|
+
|
|
1185
|
+
```ts
|
|
1186
|
+
register(form: AbstractControl): void
|
|
1187
|
+
```
|
|
1188
|
+
|
|
1189
|
+
Behavior:
|
|
1190
|
+
|
|
1191
|
+
- Captures the current form value as the initial state
|
|
1192
|
+
- Marks the form as pristine
|
|
1193
|
+
- Stores the form reference for change detection
|
|
1194
|
+
|
|
1195
|
+
Example:
|
|
1196
|
+
|
|
1197
|
+
```ts
|
|
1198
|
+
ngOnInit() {
|
|
1199
|
+
this.form = this.fb.group({
|
|
1200
|
+
name: [''],
|
|
1201
|
+
email: ['']
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
this.tracker.register(this.form);
|
|
1205
|
+
}
|
|
1206
|
+
```
|
|
1207
|
+
|
|
1208
|
+
### reset(form)
|
|
1209
|
+
|
|
1210
|
+
Resets the tracked initial value to the current form value.
|
|
1211
|
+
|
|
1212
|
+
```ts
|
|
1213
|
+
reset(form: AbstractControl): void
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
Use Case: After patching form data from an API, reset the tracker to prevent false positives.
|
|
1217
|
+
Example:
|
|
1218
|
+
|
|
1219
|
+
```ts
|
|
1220
|
+
loadData() {
|
|
1221
|
+
this.api.getData().subscribe(data => {
|
|
1222
|
+
this.form.patchValue(data);
|
|
1223
|
+
// Reset tracker after patching to track only future changes
|
|
1224
|
+
this.tracker.reset(this.form);
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
```
|
|
1228
|
+
|
|
1229
|
+
### unregister(form)
|
|
1230
|
+
|
|
1231
|
+
Removes a form from the tracker.
|
|
1232
|
+
|
|
1233
|
+
```ts
|
|
1234
|
+
unregister(form: AbstractControl): void
|
|
1235
|
+
```
|
|
1236
|
+
|
|
1237
|
+
Example:
|
|
1238
|
+
|
|
1239
|
+
```ts
|
|
1240
|
+
ngOnDestroy() {
|
|
1241
|
+
this.tracker.unregister(this.form);
|
|
1242
|
+
}
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
### hasChanges()
|
|
1246
|
+
|
|
1247
|
+
Checks if any tracked form has unsaved changes.
|
|
1248
|
+
|
|
1249
|
+
```ts
|
|
1250
|
+
hasChanges(): boolean
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
Returns: true if any tracked form has changes, false otherwise.
|
|
1254
|
+
Example:
|
|
1255
|
+
|
|
1256
|
+
```ts
|
|
1257
|
+
if (this.tracker.hasChanges()) {
|
|
1258
|
+
// Show confirmation modal
|
|
1259
|
+
}
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1262
|
+
### clearAll()
|
|
1263
|
+
|
|
1264
|
+
Removes all forms from the tracker.
|
|
1265
|
+
|
|
1266
|
+
```ts
|
|
1267
|
+
clearAll(): void
|
|
1268
|
+
```
|
|
1269
|
+
|
|
1270
|
+
Example:
|
|
1271
|
+
|
|
1272
|
+
```ts
|
|
1273
|
+
ngOnDestroy() {
|
|
1274
|
+
this.tracker.clearAll();
|
|
1275
|
+
}
|
|
1276
|
+
```
|
|
1277
|
+
|
|
1278
|
+
2. Batch Operations
|
|
1279
|
+
For applications with multiple forms that need to be managed together, the service provides batch methods.
|
|
1280
|
+
|
|
1281
|
+
### registerForms(forms)
|
|
1282
|
+
|
|
1283
|
+
Registers multiple forms at once for change tracking.
|
|
1284
|
+
|
|
1285
|
+
```ts
|
|
1286
|
+
registerForms(forms: AbstractControl[]): void
|
|
1287
|
+
```
|
|
1288
|
+
|
|
1289
|
+
Use Case:
|
|
1290
|
+
|
|
1291
|
+
- Multi-step forms
|
|
1292
|
+
- Forms in tabs
|
|
1293
|
+
- Parent component managing multiple child forms
|
|
1294
|
+
|
|
1295
|
+
Example:
|
|
1296
|
+
|
|
1297
|
+
```ts
|
|
1298
|
+
ngOnInit() {
|
|
1299
|
+
this.personalInfoForm = this.fb.group({
|
|
1300
|
+
firstName: [''],
|
|
1301
|
+
lastName: ['']
|
|
1302
|
+
});
|
|
1303
|
+
|
|
1304
|
+
this.contactForm = this.fb.group({
|
|
1305
|
+
email: [''],
|
|
1306
|
+
phone: ['']
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
this.addressForm = this.fb.group({
|
|
1310
|
+
street: [''],
|
|
1311
|
+
city: ['']
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1314
|
+
// Register all forms at once
|
|
1315
|
+
this.tracker.registerForms([
|
|
1316
|
+
this.personalInfoForm,
|
|
1317
|
+
this.contactForm,
|
|
1318
|
+
this.addressForm
|
|
1319
|
+
]);
|
|
1320
|
+
}
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
### resetForms(forms)
|
|
1324
|
+
|
|
1325
|
+
Resets multiple forms to their current values, useful after batch updates or API calls.
|
|
1326
|
+
|
|
1327
|
+
```ts
|
|
1328
|
+
resetForms(forms: AbstractControl[]): void
|
|
1329
|
+
```
|
|
1330
|
+
|
|
1331
|
+
Use Case:
|
|
1332
|
+
|
|
1333
|
+
- After loading data into multiple forms
|
|
1334
|
+
- After bulk form updates
|
|
1335
|
+
- Resetting multiple wizard steps
|
|
1336
|
+
|
|
1337
|
+
Example:
|
|
1338
|
+
|
|
1339
|
+
```ts
|
|
1340
|
+
loadAllData() {
|
|
1341
|
+
this.api.getUserData().subscribe(data => {
|
|
1342
|
+
this.personalInfoForm.patchValue(data.personal);
|
|
1343
|
+
this.contactForm.patchValue(data.contact);
|
|
1344
|
+
this.addressForm.patchValue(data.address);
|
|
1345
|
+
|
|
1346
|
+
// Reset all forms after patching
|
|
1347
|
+
this.tracker.resetForms([
|
|
1348
|
+
this.personalInfoForm,
|
|
1349
|
+
this.contactForm,
|
|
1350
|
+
this.addressForm
|
|
1351
|
+
]);
|
|
1352
|
+
});
|
|
1353
|
+
}
|
|
1354
|
+
```
|
|
1355
|
+
|
|
1356
|
+
### unregisterForms(forms)
|
|
1357
|
+
|
|
1358
|
+
Removes multiple forms from the tracker at once.
|
|
1359
|
+
|
|
1360
|
+
```ts
|
|
1361
|
+
unregisterForms(forms: AbstractControl[]): void
|
|
1362
|
+
```
|
|
1363
|
+
|
|
1364
|
+
Use Case:
|
|
1365
|
+
|
|
1366
|
+
- Component cleanup with multiple forms
|
|
1367
|
+
- Dynamically removing form sections
|
|
1368
|
+
- Batch cleanup in parent components
|
|
1369
|
+
|
|
1370
|
+
Example:
|
|
1371
|
+
|
|
1372
|
+
```ts
|
|
1373
|
+
ngOnDestroy() {
|
|
1374
|
+
// Unregister all forms at once
|
|
1375
|
+
this.tracker.unregisterForms([
|
|
1376
|
+
this.personalInfoForm,
|
|
1377
|
+
this.contactForm,
|
|
1378
|
+
this.addressForm
|
|
1379
|
+
]);
|
|
1380
|
+
}
|
|
1381
|
+
```
|
|
1382
|
+
|
|
1383
|
+
### Complete Multi-Form Example
|
|
1384
|
+
|
|
1385
|
+
```ts
|
|
1386
|
+
import { Component, OnInit, OnDestroy } from "@angular/core";
|
|
1387
|
+
import { FormBuilder, FormGroup } from "@angular/forms";
|
|
1388
|
+
import { FormChangesTrackerService } from "ngx-intellitoolx";
|
|
1389
|
+
|
|
1390
|
+
@Component({
|
|
1391
|
+
selector: "app-user-profile",
|
|
1392
|
+
templateUrl: "./user-profile.component.html",
|
|
1393
|
+
})
|
|
1394
|
+
export class UserProfileComponent implements OnInit, OnDestroy {
|
|
1395
|
+
personalForm!: FormGroup;
|
|
1396
|
+
contactForm!: FormGroup;
|
|
1397
|
+
preferencesForm!: FormGroup;
|
|
1398
|
+
|
|
1399
|
+
constructor(
|
|
1400
|
+
private fb: FormBuilder,
|
|
1401
|
+
private tracker: FormChangesTrackerService,
|
|
1402
|
+
private api: UserApiService,
|
|
1403
|
+
) {}
|
|
1404
|
+
|
|
1405
|
+
ngOnInit() {
|
|
1406
|
+
// Create multiple forms
|
|
1407
|
+
this.personalForm = this.fb.group({
|
|
1408
|
+
firstName: [""],
|
|
1409
|
+
lastName: [""],
|
|
1410
|
+
dateOfBirth: [""],
|
|
1411
|
+
});
|
|
1412
|
+
|
|
1413
|
+
this.contactForm = this.fb.group({
|
|
1414
|
+
email: [""],
|
|
1415
|
+
phone: [""],
|
|
1416
|
+
address: [""],
|
|
1417
|
+
});
|
|
1418
|
+
|
|
1419
|
+
this.preferencesForm = this.fb.group({
|
|
1420
|
+
newsletter: [false],
|
|
1421
|
+
notifications: [false],
|
|
1422
|
+
theme: ["light"],
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
// Register all forms at once
|
|
1426
|
+
this.tracker.registerForms([this.personalForm, this.contactForm, this.preferencesForm]);
|
|
1427
|
+
|
|
1428
|
+
// Load data
|
|
1429
|
+
this.loadUserData();
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
loadUserData() {
|
|
1433
|
+
this.api.getUserProfile().subscribe((data) => {
|
|
1434
|
+
this.personalForm.patchValue(data.personal);
|
|
1435
|
+
this.contactForm.patchValue(data.contact);
|
|
1436
|
+
this.preferencesForm.patchValue(data.preferences);
|
|
1437
|
+
|
|
1438
|
+
// Reset all trackers after loading
|
|
1439
|
+
this.tracker.resetForms([this.personalForm, this.contactForm, this.preferencesForm]);
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
saveAll() {
|
|
1444
|
+
if (!this.tracker.hasChanges()) {
|
|
1445
|
+
console.log("No changes to save");
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
const allData = {
|
|
1450
|
+
personal: this.personalForm.value,
|
|
1451
|
+
contact: this.contactForm.value,
|
|
1452
|
+
preferences: this.preferencesForm.value,
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1455
|
+
this.api.updateProfile(allData).subscribe(() => {
|
|
1456
|
+
// Reset all forms after successful save
|
|
1457
|
+
this.tracker.resetForms([this.personalForm, this.contactForm, this.preferencesForm]);
|
|
1458
|
+
});
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
ngOnDestroy() {
|
|
1462
|
+
// Clean up all forms at once
|
|
1463
|
+
this.tracker.unregisterForms([this.personalForm, this.contactForm, this.preferencesForm]);
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
### Best Practices
|
|
1469
|
+
|
|
1470
|
+
1. Always unregister forms in ngOnDestroy() to prevent memory leaks
|
|
1471
|
+
2. Use clearAll() when component manages all tracked forms
|
|
1472
|
+
3. Reset after API loads to establish the correct baseline for change detection
|
|
1473
|
+
4. Batch operations improve code readability when managing multiple forms
|
|
1474
|
+
5. Call markAsPristine() after successful saves (handled automatically by `reset()` and `resetForms()`)
|
|
1475
|
+
|
|
1476
|
+
## Protecting Angular Routes
|
|
1477
|
+
|
|
1478
|
+
### Add Guard to Route
|
|
1479
|
+
|
|
1480
|
+
```ts
|
|
1481
|
+
{
|
|
1482
|
+
path: 'add-item',
|
|
1483
|
+
component: AddItemPageComponent,
|
|
1484
|
+
canDeactivate: [UnsavedChangesGuard]
|
|
1485
|
+
}
|
|
1486
|
+
```
|
|
1487
|
+
|
|
1488
|
+
### Implement Route Component
|
|
1489
|
+
|
|
1490
|
+
Your routed component must implement CanComponentDeactivate. This is not related to the library, as the modal trigger logic exists entirely outside the library's scope.
|
|
1491
|
+
|
|
1492
|
+
```ts
|
|
1493
|
+
import { FormChangesTrackerService } from "ngx-intellitoolx";
|
|
1494
|
+
import { UtilityService } from "../services/utility.service";
|
|
1495
|
+
|
|
1496
|
+
@Component({
|
|
1497
|
+
selector: "app-add-item-page",
|
|
1498
|
+
template: `<app-item-form></app-item-form>`,
|
|
1499
|
+
})
|
|
1500
|
+
export class AddItemPageComponent implements CanComponentDeactivate {
|
|
1501
|
+
constructor(private utils: UtilityService) {}
|
|
1502
|
+
|
|
1503
|
+
confirmUnsavedChanges(): Promise<boolean> {
|
|
1504
|
+
return this.utils.confirmUnsavedChanges();
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
```
|
|
1508
|
+
|
|
1509
|
+
### Track Form Changes (Child Component)
|
|
1510
|
+
|
|
1511
|
+
```ts
|
|
1512
|
+
@Component({
|
|
1513
|
+
selector: "app-item-form",
|
|
1514
|
+
templateUrl: "./item-form.component.html",
|
|
1515
|
+
})
|
|
1516
|
+
export class ItemFormComponent implements OnInit {
|
|
1517
|
+
form!: FormGroup;
|
|
1518
|
+
|
|
1519
|
+
constructor(
|
|
1520
|
+
private fb: FormBuilder,
|
|
1521
|
+
private tracker: FormChangesTrackerService,
|
|
1522
|
+
) {}
|
|
1523
|
+
|
|
1524
|
+
ngOnInit(): void {
|
|
1525
|
+
this.form = this.fb.group({
|
|
1526
|
+
name: [""],
|
|
1527
|
+
description: [""],
|
|
1528
|
+
});
|
|
1529
|
+
|
|
1530
|
+
this.tracker.register(this.form);
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
patchForm() {
|
|
1534
|
+
this.form.patchValue({
|
|
1535
|
+
name: this.data.name,
|
|
1536
|
+
description: this.data.description,
|
|
1537
|
+
});
|
|
1538
|
+
// reset form after patching to track changes afterwards.
|
|
1539
|
+
// Very useful during form update
|
|
1540
|
+
this.tracker.reset(this.form);
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
save(): void {
|
|
1544
|
+
// API call
|
|
1545
|
+
this.form.markAsPristine();
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
ngOnDestroy(): void {
|
|
1549
|
+
// This is very important to not keep track of forms from other component
|
|
1550
|
+
this.tracker.clearAll();
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
```
|
|
1554
|
+
|
|
1555
|
+
## Protecting Browser Refresh / Tab Close
|
|
1556
|
+
|
|
1557
|
+
### Add this inside the routed component:
|
|
1558
|
+
|
|
1559
|
+
```ts
|
|
1560
|
+
// Declare cleanup callback
|
|
1561
|
+
private unregister?: () => void;
|
|
1562
|
+
|
|
1563
|
+
constructor(
|
|
1564
|
+
private tracker: FormChangesTrackerService,
|
|
1565
|
+
public utilservice: UtilityService
|
|
1566
|
+
) {
|
|
1567
|
+
super();
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
ngOnInit() {
|
|
1571
|
+
// Register a beforeunload handler to prevent or warn about navigation when there are unsaved changes.
|
|
1572
|
+
this.unregister = IntelliToolxHelper.registerBeforeUnload(() =>
|
|
1573
|
+
this.tracker.hasChanges(),
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
confirmUnsavedChanges(): Promise<boolean> {
|
|
1578
|
+
return this.utilservice.confirmUnsavedChanges();
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
ngOnDestroy() {
|
|
1582
|
+
// Safely invokes the teardown/unsubscribe callback if it exists.
|
|
1583
|
+
this.unregister?.();
|
|
1584
|
+
}
|
|
1585
|
+
```
|
|
1586
|
+
|
|
1587
|
+
## Protecting Modals (NG Bootstrap Example)
|
|
1588
|
+
|
|
1589
|
+
Route guards do NOT protect modals.
|
|
1590
|
+
You must intercept modal close manually.
|
|
1591
|
+
|
|
1592
|
+
Modal Form Component
|
|
1593
|
+
|
|
1594
|
+
```ts
|
|
1595
|
+
import { NgbActiveModal } from "@ng-bootstrap/ng-bootstrap";
|
|
1596
|
+
import { FormChangesTrackerService } from "ngx-intellitoolx";
|
|
1597
|
+
import { UtilityService } from "../utility.service";
|
|
1598
|
+
|
|
1599
|
+
@Component({
|
|
1600
|
+
selector: "app-item-modal",
|
|
1601
|
+
templateUrl: "./item-modal.component.html",
|
|
1602
|
+
})
|
|
1603
|
+
export class ItemModalComponent implements OnInit {
|
|
1604
|
+
form!: FormGroup;
|
|
1605
|
+
|
|
1606
|
+
constructor(
|
|
1607
|
+
private fb: FormBuilder,
|
|
1608
|
+
private tracker: FormChangesTrackerService,
|
|
1609
|
+
private utils: UtilityService,
|
|
1610
|
+
public activeModal: NgbActiveModal,
|
|
1611
|
+
) {}
|
|
1612
|
+
|
|
1613
|
+
ngOnInit(): void {
|
|
1614
|
+
this.form = this.fb.group({
|
|
1615
|
+
name: [""],
|
|
1616
|
+
description: [""],
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
this.tracker.register(this.form);
|
|
1620
|
+
}
|
|
1621
|
+
|
|
1622
|
+
async closeModal() {
|
|
1623
|
+
if (this.tracker.hasChanges()) {
|
|
1624
|
+
const allowClose = await IntelliToolxHelper.confirmIfChanged(this.tracker.hasChanges(), () => this.utilsService.confirmUnsavedChanges());
|
|
1625
|
+
if (!allowClose) return;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
this.activeModal.dismiss();
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
save(): void {
|
|
1632
|
+
this.form.markAsPristine();
|
|
1633
|
+
this.activeModal.close("saved");
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
ngOnDestroy(): void {
|
|
1637
|
+
// This is very important to not keep track of forms from other component
|
|
1638
|
+
this.tracker.clearAll();
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
```
|
|
1642
|
+
|
|
1643
|
+
Modal Template
|
|
1644
|
+
|
|
1645
|
+
```html
|
|
1646
|
+
<div class="modal-header">
|
|
1647
|
+
<h5 class="modal-title">Add Item</h5>
|
|
1648
|
+
<button type="button" class="btn-close" (click)="closeModal()"></button>
|
|
1649
|
+
</div>
|
|
1650
|
+
|
|
1651
|
+
<div class="modal-body">
|
|
1652
|
+
<form [formGroup]="form">
|
|
1653
|
+
<input formControlName="name" />
|
|
1654
|
+
<textarea formControlName="description"></textarea>
|
|
1655
|
+
</form>
|
|
1656
|
+
</div>
|
|
1657
|
+
|
|
1658
|
+
<div class="modal-footer">
|
|
1659
|
+
<button class="btn btn-secondary" (click)="closeModal()">Cancel</button>
|
|
1660
|
+
|
|
1661
|
+
<button class="btn btn-primary" (click)="save()">Save</button>
|
|
1662
|
+
</div>
|
|
1663
|
+
```
|
|
1664
|
+
|
|
1665
|
+
## How It Works
|
|
1666
|
+
|
|
1667
|
+
| Action | What Happens |
|
|
1668
|
+
| -------------------- | --------------------- |
|
|
1669
|
+
| User edits form | Tracker marks dirty |
|
|
1670
|
+
| User navigates route | Guard checks tracker |
|
|
1671
|
+
| User refreshes page | beforeunload triggers |
|
|
1672
|
+
| User closes modal | Manual interception |
|
|
1673
|
+
| User saves form | Tracker resets |
|
|
1674
|
+
|
|
1675
|
+
### Parent + Child Structure
|
|
1676
|
+
|
|
1677
|
+
If your form is nested:
|
|
1678
|
+
|
|
1679
|
+
```
|
|
1680
|
+
RouteComponent
|
|
1681
|
+
└── Modal
|
|
1682
|
+
└── Form
|
|
1683
|
+
|
|
1684
|
+
Child form → marks tracker dirty
|
|
1685
|
+
|
|
1686
|
+
Route component → implements guard interface
|
|
1687
|
+
|
|
1688
|
+
Modal → manually checks tracker before close
|
|
1689
|
+
```
|
|
1690
|
+
|
|
1691
|
+
### Confirmation Modal Example (App Layer)
|
|
1692
|
+
|
|
1693
|
+
```ts
|
|
1694
|
+
@Injectable({ providedIn: "root" })
|
|
1695
|
+
export class UtilityService {
|
|
1696
|
+
constructor(private modal: NgbModal) {}
|
|
1697
|
+
|
|
1698
|
+
confirmUnsavedChanges(): Promise<boolean> {
|
|
1699
|
+
const modalRef = this.modal.open(UnsavedConfirmComponent, {
|
|
1700
|
+
backdrop: "static",
|
|
1701
|
+
});
|
|
1702
|
+
|
|
1703
|
+
return modalRef.result.then(() => true).catch(() => false);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
```
|
|
1707
|
+
|
|
1708
|
+
📄 License
|
|
1709
|
+
|
|
1710
|
+
```
|
|
1711
|
+
MIT © Fagon Technologies
|
|
1712
|
+
```
|
|
1713
|
+
|
|
1714
|
+
[REPO]: https://github.com/Nana-Darko-Ampem/ngx-intellitoolx
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export {};
|
|
2
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2FuLWNvbXBvbmVudC1kZWFjdGl2YXRlLmludGVyZmFjZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9saWIvZm9ybS1jaGFuZ2VzL2Nhbi1jb21wb25lbnQtZGVhY3RpdmF0ZS5pbnRlcmZhY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IiIsInNvdXJjZXNDb250ZW50IjpbImV4cG9ydCBpbnRlcmZhY2UgQ2FuQ29tcG9uZW50RGVhY3RpdmF0ZSB7XG4gIGhhc1Vuc2F2ZWRDaGFuZ2VzOiAoKSA9PiBib29sZWFuO1xuICBjb25maXJtVW5zYXZlZENoYW5nZXM/OiAoKSA9PiBQcm9taXNlPGJvb2xlYW4+IHwgYm9vbGVhbjtcbiAgZ2V0VW5zYXZlZE1lc3NhZ2U/OiAoKSA9PiBzdHJpbmc7XG59XG4iXX0=
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export {};
|
|
2
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY29uZmlybS1oYW5kbGVyLnR5cGUuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi9zcmMvbGliL2Zvcm0tY2hhbmdlcy9jb25maXJtLWhhbmRsZXIudHlwZS50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIiwic291cmNlc0NvbnRlbnQiOlsiZXhwb3J0IHR5cGUgQ29uZmlybUhhbmRsZXIgPSAoKSA9PiBib29sZWFuIHwgUHJvbWlzZTxib29sZWFuPjtcbiJdfQ==
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Injectable } from '@angular/core';
|
|
2
|
+
import { IntelliToolxHelper } from '../helpers/intellitoolx.helper';
|
|
3
|
+
import * as i0 from "@angular/core";
|
|
4
|
+
export class FormChangesTrackerService {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.trackedForms = new Map();
|
|
7
|
+
}
|
|
8
|
+
register(form) {
|
|
9
|
+
const currentRawValue = form.getRawValue?.() ?? form.value;
|
|
10
|
+
this.trackedForms.set(form, {
|
|
11
|
+
initialValue: IntelliToolxHelper.clone(currentRawValue),
|
|
12
|
+
});
|
|
13
|
+
form.markAsPristine();
|
|
14
|
+
}
|
|
15
|
+
registerForms(forms) {
|
|
16
|
+
forms.forEach((form) => this.register(form));
|
|
17
|
+
}
|
|
18
|
+
hasChanges() {
|
|
19
|
+
// We iterate over entries [form, metadata]
|
|
20
|
+
for (const [form, { initialValue }] of this.trackedForms.entries()) {
|
|
21
|
+
if (IntelliToolxHelper.formHasChanges(initialValue, form)) {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
unregister(form) {
|
|
28
|
+
this.trackedForms.delete(form);
|
|
29
|
+
}
|
|
30
|
+
unregisterForms(forms) {
|
|
31
|
+
forms.forEach((form) => this.unregister(form));
|
|
32
|
+
}
|
|
33
|
+
reset(form) {
|
|
34
|
+
const entry = this.trackedForms.get(form);
|
|
35
|
+
if (!entry) {
|
|
36
|
+
this.register(form);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const currentRawValue = form.getRawValue?.() ?? form.value;
|
|
40
|
+
entry.initialValue = IntelliToolxHelper.clone(currentRawValue);
|
|
41
|
+
form.markAsPristine();
|
|
42
|
+
}
|
|
43
|
+
resetForms(forms) {
|
|
44
|
+
forms.forEach((form) => this.reset(form));
|
|
45
|
+
}
|
|
46
|
+
clearAll() {
|
|
47
|
+
this.trackedForms.clear();
|
|
48
|
+
}
|
|
49
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FormChangesTrackerService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
|
|
50
|
+
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FormChangesTrackerService, providedIn: 'root' }); }
|
|
51
|
+
}
|
|
52
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: FormChangesTrackerService, decorators: [{
|
|
53
|
+
type: Injectable,
|
|
54
|
+
args: [{
|
|
55
|
+
providedIn: 'root',
|
|
56
|
+
}]
|
|
57
|
+
}] });
|
|
58
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZm9ybS1jaGFuZ2VzLXRyYWNrZXIuc2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uLy4uL3NyYy9saWIvZm9ybS1jaGFuZ2VzL2Zvcm0tY2hhbmdlcy10cmFja2VyLnNlcnZpY2UudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxFQUFFLFVBQVUsRUFBRSxNQUFNLGVBQWUsQ0FBQztBQUMzQyxPQUFPLEVBQUUsa0JBQWtCLEVBQUUsTUFBTSxnQ0FBZ0MsQ0FBQzs7QUFNcEUsTUFBTSxPQUFPLHlCQUF5QjtJQUh0QztRQUlVLGlCQUFZLEdBQUcsSUFBSSxHQUFHLEVBSzNCLENBQUM7S0FzREw7SUFwREMsUUFBUSxDQUFDLElBQXFCO1FBQzVCLE1BQU0sZUFBZSxHQUFJLElBQVksQ0FBQyxXQUFXLEVBQUUsRUFBRSxJQUFJLElBQUksQ0FBQyxLQUFLLENBQUM7UUFFcEUsSUFBSSxDQUFDLFlBQVksQ0FBQyxHQUFHLENBQUMsSUFBSSxFQUFFO1lBQzFCLFlBQVksRUFBRSxrQkFBa0IsQ0FBQyxLQUFLLENBQUMsZUFBZSxDQUFDO1NBQ3hELENBQUMsQ0FBQztRQUVILElBQUksQ0FBQyxjQUFjLEVBQUUsQ0FBQztJQUN4QixDQUFDO0lBRUQsYUFBYSxDQUFDLEtBQXdCO1FBQ3BDLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztJQUMvQyxDQUFDO0lBRUQsVUFBVTtRQUNSLDJDQUEyQztRQUMzQyxLQUFLLE1BQU0sQ0FBQyxJQUFJLEVBQUUsRUFBRSxZQUFZLEVBQUUsQ0FBQyxJQUFJLElBQUksQ0FBQyxZQUFZLENBQUMsT0FBTyxFQUFFLEVBQUU7WUFDbEUsSUFBSSxrQkFBa0IsQ0FBQyxjQUFjLENBQUMsWUFBWSxFQUFFLElBQUksQ0FBQyxFQUFFO2dCQUN6RCxPQUFPLElBQUksQ0FBQzthQUNiO1NBQ0Y7UUFDRCxPQUFPLEtBQUssQ0FBQztJQUNmLENBQUM7SUFFRCxVQUFVLENBQUMsSUFBcUI7UUFDOUIsSUFBSSxDQUFDLFlBQVksQ0FBQyxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUM7SUFDakMsQ0FBQztJQUVELGVBQWUsQ0FBQyxLQUF3QjtRQUN0QyxLQUFLLENBQUMsT0FBTyxDQUFDLENBQUMsSUFBSSxFQUFFLEVBQUUsQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLElBQUksQ0FBQyxDQUFDLENBQUM7SUFDakQsQ0FBQztJQUVELEtBQUssQ0FBQyxJQUFxQjtRQUN6QixNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsWUFBWSxDQUFDLEdBQUcsQ0FBQyxJQUFJLENBQUMsQ0FBQztRQUMxQyxJQUFJLENBQUMsS0FBSyxFQUFFO1lBQ1YsSUFBSSxDQUFDLFFBQVEsQ0FBQyxJQUFJLENBQUMsQ0FBQztZQUNwQixPQUFPO1NBQ1I7UUFFRCxNQUFNLGVBQWUsR0FBSSxJQUFZLENBQUMsV0FBVyxFQUFFLEVBQUUsSUFBSSxJQUFJLENBQUMsS0FBSyxDQUFDO1FBQ3BFLEtBQUssQ0FBQyxZQUFZLEdBQUcsa0JBQWtCLENBQUMsS0FBSyxDQUFDLGVBQWUsQ0FBQyxDQUFDO1FBRS9ELElBQUksQ0FBQyxjQUFjLEVBQUUsQ0FBQztJQUN4QixDQUFDO0lBRUQsVUFBVSxDQUFDLEtBQXdCO1FBQ2pDLEtBQUssQ0FBQyxPQUFPLENBQUMsQ0FBQyxJQUFJLEVBQUUsRUFBRSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLENBQUMsQ0FBQztJQUM1QyxDQUFDO0lBRUQsUUFBUTtRQUNOLElBQUksQ0FBQyxZQUFZLENBQUMsS0FBSyxFQUFFLENBQUM7SUFDNUIsQ0FBQzsrR0EzRFUseUJBQXlCO21IQUF6Qix5QkFBeUIsY0FGeEIsTUFBTTs7NEZBRVAseUJBQXlCO2tCQUhyQyxVQUFVO21CQUFDO29CQUNWLFVBQVUsRUFBRSxNQUFNO2lCQUNuQiIsInNvdXJjZXNDb250ZW50IjpbImltcG9ydCB7IEluamVjdGFibGUgfSBmcm9tICdAYW5ndWxhci9jb3JlJztcbmltcG9ydCB7IEludGVsbGlUb29seEhlbHBlciB9IGZyb20gJy4uL2hlbHBlcnMvaW50ZWxsaXRvb2x4LmhlbHBlcic7XG5pbXBvcnQgeyBBYnN0cmFjdENvbnRyb2wgfSBmcm9tICdAYW5ndWxhci9mb3Jtcyc7XG5cbkBJbmplY3RhYmxlKHtcbiAgcHJvdmlkZWRJbjogJ3Jvb3QnLFxufSlcbmV4cG9ydCBjbGFzcyBGb3JtQ2hhbmdlc1RyYWNrZXJTZXJ2aWNlIHtcbiAgcHJpdmF0ZSB0cmFja2VkRm9ybXMgPSBuZXcgTWFwPFxuICAgIEFic3RyYWN0Q29udHJvbCxcbiAgICB7XG4gICAgICBpbml0aWFsVmFsdWU6IGFueTtcbiAgICB9XG4gID4oKTtcblxuICByZWdpc3Rlcihmb3JtOiBBYnN0cmFjdENvbnRyb2wpIHtcbiAgICBjb25zdCBjdXJyZW50UmF3VmFsdWUgPSAoZm9ybSBhcyBhbnkpLmdldFJhd1ZhbHVlPy4oKSA/PyBmb3JtLnZhbHVlO1xuXG4gICAgdGhpcy50cmFja2VkRm9ybXMuc2V0KGZvcm0sIHtcbiAgICAgIGluaXRpYWxWYWx1ZTogSW50ZWxsaVRvb2x4SGVscGVyLmNsb25lKGN1cnJlbnRSYXdWYWx1ZSksXG4gICAgfSk7XG5cbiAgICBmb3JtLm1hcmtBc1ByaXN0aW5lKCk7XG4gIH1cblxuICByZWdpc3RlckZvcm1zKGZvcm1zOiBBYnN0cmFjdENvbnRyb2xbXSkge1xuICAgIGZvcm1zLmZvckVhY2goKGZvcm0pID0+IHRoaXMucmVnaXN0ZXIoZm9ybSkpO1xuICB9XG5cbiAgaGFzQ2hhbmdlcygpOiBib29sZWFuIHtcbiAgICAvLyBXZSBpdGVyYXRlIG92ZXIgZW50cmllcyBbZm9ybSwgbWV0YWRhdGFdXG4gICAgZm9yIChjb25zdCBbZm9ybSwgeyBpbml0aWFsVmFsdWUgfV0gb2YgdGhpcy50cmFja2VkRm9ybXMuZW50cmllcygpKSB7XG4gICAgICBpZiAoSW50ZWxsaVRvb2x4SGVscGVyLmZvcm1IYXNDaGFuZ2VzKGluaXRpYWxWYWx1ZSwgZm9ybSkpIHtcbiAgICAgICAgcmV0dXJuIHRydWU7XG4gICAgICB9XG4gICAgfVxuICAgIHJldHVybiBmYWxzZTtcbiAgfVxuXG4gIHVucmVnaXN0ZXIoZm9ybTogQWJzdHJhY3RDb250cm9sKSB7XG4gICAgdGhpcy50cmFja2VkRm9ybXMuZGVsZXRlKGZvcm0pO1xuICB9XG5cbiAgdW5yZWdpc3RlckZvcm1zKGZvcm1zOiBBYnN0cmFjdENvbnRyb2xbXSkge1xuICAgIGZvcm1zLmZvckVhY2goKGZvcm0pID0+IHRoaXMudW5yZWdpc3Rlcihmb3JtKSk7XG4gIH1cblxuICByZXNldChmb3JtOiBBYnN0cmFjdENvbnRyb2wpIHtcbiAgICBjb25zdCBlbnRyeSA9IHRoaXMudHJhY2tlZEZvcm1zLmdldChmb3JtKTtcbiAgICBpZiAoIWVudHJ5KSB7XG4gICAgICB0aGlzLnJlZ2lzdGVyKGZvcm0pO1xuICAgICAgcmV0dXJuO1xuICAgIH1cblxuICAgIGNvbnN0IGN1cnJlbnRSYXdWYWx1ZSA9IChmb3JtIGFzIGFueSkuZ2V0UmF3VmFsdWU/LigpID8/IGZvcm0udmFsdWU7XG4gICAgZW50cnkuaW5pdGlhbFZhbHVlID0gSW50ZWxsaVRvb2x4SGVscGVyLmNsb25lKGN1cnJlbnRSYXdWYWx1ZSk7XG5cbiAgICBmb3JtLm1hcmtBc1ByaXN0aW5lKCk7XG4gIH1cblxuICByZXNldEZvcm1zKGZvcm1zOiBBYnN0cmFjdENvbnRyb2xbXSkge1xuICAgIGZvcm1zLmZvckVhY2goKGZvcm0pID0+IHRoaXMucmVzZXQoZm9ybSkpO1xuICB9XG5cbiAgY2xlYXJBbGwoKSB7XG4gICAgdGhpcy50cmFja2VkRm9ybXMuY2xlYXIoKTtcbiAgfVxufVxuIl19
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export {};
|
|
2
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZm9ybS11cGRhdGUtbWVzc2FnZS1jb25maWcuaW50ZXJmYWNlLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vLi4vc3JjL2xpYi9mb3JtLWNoYW5nZXMvZm9ybS11cGRhdGUtbWVzc2FnZS1jb25maWcuaW50ZXJmYWNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiIiLCJzb3VyY2VzQ29udGVudCI6WyJleHBvcnQgaW50ZXJmYWNlIEludGVsbGl0b29seEZvcm1VcGRhdGVNZXNzYWdlQ29uZmlnIHtcbiAgbWVzc2FnZT86IHN0cmluZztcbiAgYmFja2dyb3VuZENvbG9yPzogc3RyaW5nO1xuICB0ZXh0Q29sb3I/OiBzdHJpbmc7XG4gIGJvcmRlckNvbG9yPzogc3RyaW5nO1xuICBmb250V2VpZ2h0PzogbnVtYmVyO1xuICBib3JkZXJSYWRpdXM/OiBzdHJpbmc7XG4gIHBhZGRpbmc/OiBzdHJpbmc7XG4gIGljb25BbmRNZXNzYWdlR2FwPzogc3RyaW5nO1xuICBpY29uU2l6ZT86IHN0cmluZztcbn1cbiJdfQ==
|