osl-base-extended 1.0.27 → 1.0.29
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
CHANGED
|
@@ -1,656 +1,656 @@
|
|
|
1
|
-
# osl-base-extended
|
|
2
|
-
|
|
3
|
-
> **Enterprise Angular UI toolkit** — HTTP layer, CRUD UI, dynamic forms, skeleton loading, and 200+ utilities. Build data-driven pages in minutes, not days.
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/osl-base-extended)
|
|
6
|
-
[](https://angular.dev)
|
|
7
|
-
[](https://opensource.org/licenses/ISC)
|
|
8
|
-
[](https://github.com/bilalraza052)
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## What's Inside
|
|
13
|
-
|
|
14
|
-
| Module | What it does |
|
|
15
|
-
|--------|-------------|
|
|
16
|
-
| **`Httpbase`** | Abstract HTTP service — extend once per domain, get CRUD + auth + error handling free |
|
|
17
|
-
| **`<osl-setup>`** | Zero-config CRUD page: grid + add/edit dialog + delete confirm + search + pagination |
|
|
18
|
-
| **`<osl-dynamic-form>`** | Declarative forms — 12+ field types from a JSON config array |
|
|
19
|
-
| **`<osl-grid>`** | Data table with server-side or auto pagination, sorting, enum display |
|
|
20
|
-
| **`<osl-form-grid>`** | Inline editable grid with dynamic form elements per cell |
|
|
21
|
-
| **`[oslSkeleton]`** | GPU-accelerated skeleton directive — 8 layouts, 4 animations, 1 attribute |
|
|
22
|
-
| **`baseComponent`** | Inject once: `showSuccess()`, `showError()`, `openDialog()`, `openDeleteDialog()` |
|
|
23
|
-
| **`ArrayUtil`** | 25+ array helpers: chunk, groupBy, sortBy, paginate, intersection… |
|
|
24
|
-
| **`DateUtil`** | 40+ date helpers: format, diff, add/subtract, timeAgo, getAge… |
|
|
25
|
-
| **`NumberUtil`** | 30+ number helpers: formatCurrency, abbreviate, clamp, isPrime… |
|
|
26
|
-
| **`ObjectUtil`** | 25+ object helpers: deepClone, deepMerge, pick, omit, flattenObject… |
|
|
27
|
-
| **`StringUtil`** | 35+ string helpers: camelCase, slugify, truncate, mask, escapeHtml… |
|
|
28
|
-
| **`StorageUtil`** | localStorage, sessionStorage, and cookie helpers with JSON serialization |
|
|
29
|
-
| **`ValidationUtil`** | 40+ validators: email, URL, phone, password strength, credit card (Luhn)… |
|
|
30
|
-
|
|
31
|
-
---
|
|
32
|
-
|
|
33
|
-
## Installation
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
npm install osl-base-extended
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### Peer dependencies
|
|
40
|
-
|
|
41
|
-
```bash
|
|
42
|
-
npm install @angular/material
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
### Add `HttpClientModule` to your app
|
|
46
|
-
|
|
47
|
-
```typescript
|
|
48
|
-
// app.config.ts
|
|
49
|
-
import { provideHttpClient } from '@angular/common/http';
|
|
50
|
-
|
|
51
|
-
export const appConfig: ApplicationConfig = {
|
|
52
|
-
providers: [provideHttpClient()]
|
|
53
|
-
};
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
---
|
|
57
|
-
|
|
58
|
-
## API Calling — Httpbase
|
|
59
|
-
|
|
60
|
-
The core of the library. Create one service per backend controller by extending `Httpbase`.
|
|
61
|
-
|
|
62
|
-
### 1. Create a service
|
|
63
|
-
|
|
64
|
-
```typescript
|
|
65
|
-
// user.service.ts
|
|
66
|
-
import { Injectable } from '@angular/core';
|
|
67
|
-
import { Httpbase } from 'osl-base-extended';
|
|
68
|
-
|
|
69
|
-
@Injectable({ providedIn: 'root' })
|
|
70
|
-
export class UserService extends Httpbase {
|
|
71
|
-
constructor() {
|
|
72
|
-
super('User'); // maps to /api/User/...
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### 2. Call built-in CRUD methods — no extra code needed
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
// user.component.ts
|
|
81
|
-
export class UserComponent {
|
|
82
|
-
private userSvc = inject(UserService);
|
|
83
|
-
|
|
84
|
-
async loadAll() {
|
|
85
|
-
const res = await this.userSvc.getAll<User[]>();
|
|
86
|
-
if (res.isSuccessful) this.users = res.result;
|
|
87
|
-
else this.base.showError(res.error);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async loadOne(id: number) {
|
|
91
|
-
const res = await this.userSvc.getById<User>(id);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async create(user: User) {
|
|
95
|
-
const res = await this.userSvc.save<User>(user);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async edit(user: User) {
|
|
99
|
-
const res = await this.userSvc.update<User>(user);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
async remove(id: number) {
|
|
103
|
-
const res = await this.userSvc.remove(id);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
### Built-in public methods
|
|
109
|
-
|
|
110
|
-
| Method | HTTP verb | Endpoint hit |
|
|
111
|
-
|--------|-----------|-------------|
|
|
112
|
-
| `getAll<T>()` | GET | `/api/{controller}/GetAll` |
|
|
113
|
-
| `getById<T>(id)` | GET | `/api/{controller}/GetById?id=…` |
|
|
114
|
-
| `save<T>(body)` | POST | `/api/{controller}/Save` |
|
|
115
|
-
| `update<T>(body)` | PUT | `/api/{controller}/Update` |
|
|
116
|
-
| `remove<T>(id)` | DELETE | `/api/{controller}/Delete?id=…` |
|
|
117
|
-
| `search(body)` | POST | `/api/{controller}/Search` |
|
|
118
|
-
| `getConfig()` | GET | `/api/{controller}/getConfig` |
|
|
119
|
-
|
|
120
|
-
### Custom endpoints — protected verb wrappers
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
@Injectable({ providedIn: 'root' })
|
|
124
|
-
export class UserService extends Httpbase {
|
|
125
|
-
constructor() { super('User'); }
|
|
126
|
-
|
|
127
|
-
// Custom POST
|
|
128
|
-
async changePassword(body: ChangePasswordDto) {
|
|
129
|
-
return this.post<void>('ChangePassword', body);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
// Custom PUT
|
|
133
|
-
async updateRole(body: UpdateRoleDto) {
|
|
134
|
-
return this.put<User>('UpdateRole', body);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Custom PATCH
|
|
138
|
-
async toggleActive(id: number) {
|
|
139
|
-
return this.patch<User>('ToggleActive', { id });
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Custom DELETE with query params
|
|
143
|
-
async bulkDelete(ids: number[]) {
|
|
144
|
-
return this.delete<void>('BulkDelete', [{ property: 'ids', value: ids }]);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// File upload (multipart/form-data)
|
|
148
|
-
async uploadAvatar(file: File) {
|
|
149
|
-
const fd = new FormData();
|
|
150
|
-
fd.append('file', file);
|
|
151
|
-
return this.upload<{ url: string }>('UploadAvatar', fd);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
### Protected verb wrappers
|
|
157
|
-
|
|
158
|
-
| Method | HTTP verb | Notes |
|
|
159
|
-
|--------|-----------|-------|
|
|
160
|
-
| `post<T>(method, body)` | POST | JSON body |
|
|
161
|
-
| `get<T>(method, params?)` | GET | query params via `myParams[]` |
|
|
162
|
-
| `put<T>(method, body)` | PUT | JSON body |
|
|
163
|
-
| `patch<T>(method, body)` | PATCH | JSON body |
|
|
164
|
-
| `delete<T>(method, params?)` | DELETE | query params |
|
|
165
|
-
| `upload<T>(method, formData)` | POST multipart | 60 s timeout |
|
|
166
|
-
|
|
167
|
-
### `HttpResponse<T>` — every method returns this
|
|
168
|
-
|
|
169
|
-
```typescript
|
|
170
|
-
interface HttpResponse<T> {
|
|
171
|
-
isSuccessful: boolean; // true for 2xx
|
|
172
|
-
statusCode: number;
|
|
173
|
-
error: string; // human-readable error message
|
|
174
|
-
result: T; // response body
|
|
175
|
-
headers?: HttpHeaders;
|
|
176
|
-
}
|
|
177
|
-
```
|
|
178
|
-
|
|
179
|
-
### Authentication
|
|
180
|
-
|
|
181
|
-
The service reads `token` from `localStorage` and sends it as `Authorization: Bearer <token>` on every request automatically.
|
|
182
|
-
|
|
183
|
-
```typescript
|
|
184
|
-
localStorage.setItem('token', 'your-jwt-here');
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
---
|
|
188
|
-
|
|
189
|
-
## CRUD UI — `<osl-setup>`
|
|
190
|
-
|
|
191
|
-
The flagship component. One tag replaces a full page of boilerplate.
|
|
192
|
-
|
|
193
|
-
### Template
|
|
194
|
-
|
|
195
|
-
```html
|
|
196
|
-
<osl-setup
|
|
197
|
-
title="Users"
|
|
198
|
-
[columns]="columns"
|
|
199
|
-
[datasource]="users"
|
|
200
|
-
[formElements]="formElements"
|
|
201
|
-
[loading]="loading"
|
|
202
|
-
[isPaginated]="true"
|
|
203
|
-
[totalRecords]="totalRecords"
|
|
204
|
-
(onAdd)="onAdd()"
|
|
205
|
-
(onEdit)="onEdit($event)"
|
|
206
|
-
(onDelete)="onDelete($event)"
|
|
207
|
-
(onSave)="onSave($event)"
|
|
208
|
-
(onSearch)="onSearch($event)"
|
|
209
|
-
(pageChange)="onPageChange($event)"
|
|
210
|
-
/>
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
### Component
|
|
214
|
-
|
|
215
|
-
```typescript
|
|
216
|
-
export class UsersComponent {
|
|
217
|
-
loading = false;
|
|
218
|
-
users: User[] = [];
|
|
219
|
-
totalRecords = 0;
|
|
220
|
-
|
|
221
|
-
columns: OslGridColumn[] = [
|
|
222
|
-
{ key: 'name', label: 'Name' },
|
|
223
|
-
{ key: 'email', label: 'Email' },
|
|
224
|
-
{ key: 'status', label: 'Status', enums: { 1: 'Active', 0: 'Inactive' } },
|
|
225
|
-
];
|
|
226
|
-
|
|
227
|
-
formElements: elements[] = [
|
|
228
|
-
{ key: 'name', label: 'Full Name', elementType: 'textbox', columns: 6, required: true },
|
|
229
|
-
{ key: 'email', label: 'Email', elementType: 'textbox', columns: 6, required: true,
|
|
230
|
-
inputType: 'email' },
|
|
231
|
-
{ key: 'status', label: 'Status', elementType: 'select', columns: 6,
|
|
232
|
-
datasource: [{ id: 1, name: 'Active' }, { id: 0, name: 'Inactive' }],
|
|
233
|
-
displayField: 'name', valueField: 'id' },
|
|
234
|
-
];
|
|
235
|
-
|
|
236
|
-
async onSave(e: { model: User; mode: 'add' | 'edit' }) {
|
|
237
|
-
const res = e.mode === 'add'
|
|
238
|
-
? await this.userSvc.save(e.model)
|
|
239
|
-
: await this.userSvc.update(e.model);
|
|
240
|
-
if (res.isSuccessful) this.loadUsers();
|
|
241
|
-
else this.base.showError(res.error);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
async onDelete(user: User) {
|
|
245
|
-
const res = await this.userSvc.remove(user.id);
|
|
246
|
-
if (res.isSuccessful) this.loadUsers();
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
### `<osl-setup>` inputs
|
|
252
|
-
|
|
253
|
-
| Input | Type | Default | Description |
|
|
254
|
-
|-------|------|---------|-------------|
|
|
255
|
-
| `title` | `string` | — | Entity name shown in heading and dialog |
|
|
256
|
-
| `columns` | `OslGridColumn[]` | — | Column definitions |
|
|
257
|
-
| `datasource` | `any[]` | — | Table row data |
|
|
258
|
-
| `formElements` | `elements[]` | — | Dynamic form configuration |
|
|
259
|
-
| `loading` | `boolean` | `false` | Shows skeleton rows while loading |
|
|
260
|
-
| `isPaginated` | `boolean` | `false` | Enable pagination footer |
|
|
261
|
-
| `pageSize` | `number` | `25` | Rows per page |
|
|
262
|
-
| `totalRecords` | `number` | — | Total count for server-side pagination |
|
|
263
|
-
| `autoMode` | `boolean` | `true` | Library handles client-side sort/page |
|
|
264
|
-
| `tableHeight` | `string` | — | CSS height for scrollable table body |
|
|
265
|
-
| `dialogWidth` | `string` | `'50vw'` | Width of add/edit dialog |
|
|
266
|
-
| `isLister` | `boolean` | `false` | Hides actions column |
|
|
267
|
-
| `beforeDisplay` | `(model) => any` | — | Transform model before opening dialog |
|
|
268
|
-
| `onAddEditFn` | `(model, mode) => void` | — | Override default add/edit dialog |
|
|
269
|
-
|
|
270
|
-
### `<osl-setup>` outputs
|
|
271
|
-
|
|
272
|
-
| Output | Payload | When |
|
|
273
|
-
|--------|---------|------|
|
|
274
|
-
| `onAdd` | — | Add button clicked |
|
|
275
|
-
| `onEdit` | row object | Edit button clicked |
|
|
276
|
-
| `onDelete` | row object | Delete confirmed |
|
|
277
|
-
| `onSave` | `{ model, mode }` | Dialog save button clicked |
|
|
278
|
-
| `onSearch` | `string` | Search input changes |
|
|
279
|
-
| `pageChange` | `OslPageEvent` | Page number changes |
|
|
280
|
-
| `pageSizeChange` | `number` | Page size changes |
|
|
281
|
-
| `sortChange` | `OslSortEvent` | Column header clicked |
|
|
282
|
-
| `onRowClick` | row object | Row clicked |
|
|
283
|
-
|
|
284
|
-
---
|
|
285
|
-
|
|
286
|
-
## Dynamic Forms — `<osl-dynamic-form>`
|
|
287
|
-
|
|
288
|
-
Build any form from a plain array — no template code needed.
|
|
289
|
-
|
|
290
|
-
```html
|
|
291
|
-
<osl-dynamic-form
|
|
292
|
-
[elements]="formElements"
|
|
293
|
-
[(model)]="formModel"
|
|
294
|
-
[skeletonLoading]="loading"
|
|
295
|
-
/>
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
### Element types
|
|
299
|
-
|
|
300
|
-
| `elementType` | Renders |
|
|
301
|
-
|---------------|---------|
|
|
302
|
-
| `textbox` | `<osl-input>` — text, password, email, number, tel, url |
|
|
303
|
-
| `textarea` | `<osl-textarea>` — resizable, character counter |
|
|
304
|
-
| `select` | `<osl-select>` — static or API datasource |
|
|
305
|
-
| `autocomplete` | `<osl-autocomplete>` — local or API search with lister |
|
|
306
|
-
| `radio` | `<osl-radio>` — horizontal/vertical layout |
|
|
307
|
-
| `checkbox` | `<osl-checkbox>` — with indeterminate support |
|
|
308
|
-
| `slide-toggle` | `<osl-slide-toggle>` — custom true/false labels |
|
|
309
|
-
| `datepicker` | `<osl-datepicker>` — date, datetime-local, time, month, week |
|
|
310
|
-
| `file-uploader` | `<osl-file-upload>` — drag & drop, size validation |
|
|
311
|
-
| `button` | `<osl-button>` — all variants and sizes |
|
|
312
|
-
| `fieldset` | Nested group of elements |
|
|
313
|
-
| `templateRef` | Inject a custom `TemplateRef` |
|
|
314
|
-
|
|
315
|
-
### Common element options
|
|
316
|
-
|
|
317
|
-
```typescript
|
|
318
|
-
{
|
|
319
|
-
key: 'fieldName', // maps to model property
|
|
320
|
-
label: 'Display Label',
|
|
321
|
-
elementType: 'textbox',
|
|
322
|
-
columns: 6, // 1–12 grid columns (Bootstrap-style)
|
|
323
|
-
required: true,
|
|
324
|
-
requiredIf: (m) => m.type === 'enterprise',
|
|
325
|
-
hideIf: (m) => !m.showField,
|
|
326
|
-
disabledIf: () => !hasPermission,
|
|
327
|
-
change: (model) => { /* react to value change */ },
|
|
328
|
-
|
|
329
|
-
// select / autocomplete
|
|
330
|
-
datasource: [],
|
|
331
|
-
displayField: 'name',
|
|
332
|
-
valueField: 'id',
|
|
333
|
-
|
|
334
|
-
// API-backed datasource
|
|
335
|
-
apiService: inject(CategoryService),
|
|
336
|
-
apiMethod: 'getAll',
|
|
337
|
-
|
|
338
|
-
// textbox extras
|
|
339
|
-
inputType: 'email',
|
|
340
|
-
placeholder: 'Enter email',
|
|
341
|
-
mask: '(000) 000-0000',
|
|
342
|
-
prefixIcon: 'person',
|
|
343
|
-
suffixIcon: 'clear',
|
|
344
|
-
maxLength: 100,
|
|
345
|
-
}
|
|
346
|
-
```
|
|
347
|
-
|
|
348
|
-
---
|
|
349
|
-
|
|
350
|
-
## Data Grid — `<osl-grid>`
|
|
351
|
-
|
|
352
|
-
```html
|
|
353
|
-
<osl-grid
|
|
354
|
-
[columns]="columns"
|
|
355
|
-
[datasource]="rows"
|
|
356
|
-
[isPaginated]="true"
|
|
357
|
-
[totalRecords]="total"
|
|
358
|
-
[loading]="loading"
|
|
359
|
-
tableHeight="400px"
|
|
360
|
-
(editClick)="onEdit($event)"
|
|
361
|
-
(deleteClick)="onDelete($event)"
|
|
362
|
-
(pageChange)="loadPage($event)"
|
|
363
|
-
(sortChange)="onSort($event)"
|
|
364
|
-
/>
|
|
365
|
-
```
|
|
366
|
-
|
|
367
|
-
### `OslGridColumn` interface
|
|
368
|
-
|
|
369
|
-
```typescript
|
|
370
|
-
interface OslGridColumn {
|
|
371
|
-
key: string; // model property
|
|
372
|
-
label: string; // header text
|
|
373
|
-
enums?: Record<any, string>; // value → display label map
|
|
374
|
-
displayFn?: (row: any) => string; // custom cell renderer
|
|
375
|
-
isActions?: boolean; // marks the edit/delete column
|
|
376
|
-
}
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
---
|
|
380
|
-
|
|
381
|
-
## Skeleton Loading — `[oslSkeleton]`
|
|
382
|
-
|
|
383
|
-
Drop one attribute on any element.
|
|
384
|
-
|
|
385
|
-
```html
|
|
386
|
-
<!-- Auto-detects child structure -->
|
|
387
|
-
<div [oslSkeleton]="loading">...</div>
|
|
388
|
-
|
|
389
|
-
<!-- Explicit types -->
|
|
390
|
-
<table [oslSkeleton]="loading" oslSkeletonType="table"
|
|
391
|
-
[oslSkeletonTableRows]="8" [oslSkeletonTableCols]="4">
|
|
392
|
-
</table>
|
|
393
|
-
|
|
394
|
-
<ul [oslSkeleton]="loading" oslSkeletonType="list" [oslSkeletonListItems]="5"></ul>
|
|
395
|
-
|
|
396
|
-
<div [oslSkeleton]="loading" oslSkeletonType="card"></div>
|
|
397
|
-
|
|
398
|
-
<img [oslSkeleton]="loading" oslSkeletonType="circle" oslSkeletonCircleSize="48px">
|
|
399
|
-
```
|
|
400
|
-
|
|
401
|
-
### Key inputs
|
|
402
|
-
|
|
403
|
-
| Input | Type | Default | Description |
|
|
404
|
-
|-------|------|---------|-------------|
|
|
405
|
-
| `oslSkeleton` | `boolean` | — | Toggle on/off |
|
|
406
|
-
| `oslSkeletonType` | `'auto'│'text'│'rect'│'circle'│'card'│'list'│'table'│'avatar-text'` | `'auto'` | Layout preset |
|
|
407
|
-
| `oslSkeletonAnimation` | `'shimmer'│'pulse'│'wave'│'none'` | `'shimmer'` | Animation style |
|
|
408
|
-
| `oslSkeletonTheme` | `'light'│'dark'` | `'light'` | Color theme |
|
|
409
|
-
| `oslSkeletonRows` | `number` | `3` | Text rows count |
|
|
410
|
-
| `oslSkeletonTableRows` | `number` | `5` | Table row count |
|
|
411
|
-
| `oslSkeletonTableCols` | `number` | `4` | Table column count |
|
|
412
|
-
| `oslSkeletonListItems` | `number` | `4` | List item count |
|
|
413
|
-
| `oslSkeletonDuration` | `number` | `1500` | Animation ms |
|
|
414
|
-
| `oslSkeletonDelay` | `number` | `0` | Delay before showing |
|
|
415
|
-
|
|
416
|
-
### Global theme
|
|
417
|
-
|
|
418
|
-
```typescript
|
|
419
|
-
// Set once in a root component
|
|
420
|
-
inject(OslSkeletonThemeService).setTheme('dark');
|
|
421
|
-
```
|
|
422
|
-
|
|
423
|
-
---
|
|
424
|
-
|
|
425
|
-
## `baseComponent` — UI Utilities
|
|
426
|
-
|
|
427
|
-
```typescript
|
|
428
|
-
export class MyComponent {
|
|
429
|
-
private base = inject(baseComponent);
|
|
430
|
-
|
|
431
|
-
showFeedback(res: HttpResponse<any>) {
|
|
432
|
-
if (res.isSuccessful) this.base.showSuccess('Saved!');
|
|
433
|
-
else this.base.showError(res.error);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
openCustomDialog() {
|
|
437
|
-
this.base.openDialog(
|
|
438
|
-
'Edit User',
|
|
439
|
-
MyFormComponent, // formBody
|
|
440
|
-
MyFooterComponent, // formFooter
|
|
441
|
-
'40vw',
|
|
442
|
-
{ userId: 1 } // data passed to dialog
|
|
443
|
-
);
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
confirmDelete(item: any) {
|
|
447
|
-
this.base.openDeleteDialog(
|
|
448
|
-
`Delete "${item.name}"?`,
|
|
449
|
-
'Confirm Delete',
|
|
450
|
-
'Yes, Delete',
|
|
451
|
-
'Cancel',
|
|
452
|
-
item
|
|
453
|
-
);
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
```
|
|
457
|
-
|
|
458
|
-
---
|
|
459
|
-
|
|
460
|
-
## Utility Namespaces
|
|
461
|
-
|
|
462
|
-
Import by namespace — tree-shakable pure functions, no external dependencies.
|
|
463
|
-
|
|
464
|
-
```typescript
|
|
465
|
-
import { ArrayUtil, DateUtil, NumberUtil, ObjectUtil,
|
|
466
|
-
StringUtil, StorageUtil, ValidationUtil } from 'osl-base-extended';
|
|
467
|
-
```
|
|
468
|
-
|
|
469
|
-
### ArrayUtil
|
|
470
|
-
|
|
471
|
-
```typescript
|
|
472
|
-
ArrayUtil.chunk([1,2,3,4,5], 2) // [[1,2],[3,4],[5]]
|
|
473
|
-
ArrayUtil.unique([1,1,2,3,2]) // [1,2,3]
|
|
474
|
-
ArrayUtil.uniqueBy(users, 'email')
|
|
475
|
-
ArrayUtil.groupBy(orders, 'status')
|
|
476
|
-
ArrayUtil.sortBy(items, 'price', 'asc')
|
|
477
|
-
ArrayUtil.filterBy(items, 'category', 'Electronics')
|
|
478
|
-
ArrayUtil.paginate(items, 2, 10) // page 2, 10 per page
|
|
479
|
-
ArrayUtil.flatten([[1,[2]],3])
|
|
480
|
-
ArrayUtil.sumBy(cart, 'total')
|
|
481
|
-
ArrayUtil.intersection([1,2,3],[2,3,4]) // [2,3]
|
|
482
|
-
ArrayUtil.difference([1,2,3],[2,3]) // [1]
|
|
483
|
-
ArrayUtil.toggle(selected, item) // add if missing, remove if present
|
|
484
|
-
ArrayUtil.shuffle(deck)
|
|
485
|
-
ArrayUtil.sample(pool, 3) // random 3
|
|
486
|
-
ArrayUtil.zip(['a','b'],[1,2]) // [['a',1],['b',2]]
|
|
487
|
-
```
|
|
488
|
-
|
|
489
|
-
### DateUtil
|
|
490
|
-
|
|
491
|
-
```typescript
|
|
492
|
-
DateUtil.formatDate(new Date(), 'YYYY-MM-DD')
|
|
493
|
-
DateUtil.timeAgo(pastDate) // '3 hours ago'
|
|
494
|
-
DateUtil.getAge(birthDate) // 28
|
|
495
|
-
DateUtil.addDays(date, 7)
|
|
496
|
-
DateUtil.addMonths(date, 3)
|
|
497
|
-
DateUtil.diffInDays(dateA, dateB)
|
|
498
|
-
DateUtil.diffInMinutes(dateA, dateB)
|
|
499
|
-
DateUtil.isBefore(dateA, dateB)
|
|
500
|
-
DateUtil.isToday(date)
|
|
501
|
-
DateUtil.isWeekend(date)
|
|
502
|
-
DateUtil.startOfMonth(date)
|
|
503
|
-
DateUtil.endOfMonth(date)
|
|
504
|
-
DateUtil.nextWorkday(date)
|
|
505
|
-
DateUtil.getWeekNumber(date)
|
|
506
|
-
DateUtil.isLeapYear(2024) // true
|
|
507
|
-
```
|
|
508
|
-
|
|
509
|
-
### NumberUtil
|
|
510
|
-
|
|
511
|
-
```typescript
|
|
512
|
-
NumberUtil.formatCurrency(1500.5, 'USD') // '$1,500.50'
|
|
513
|
-
NumberUtil.abbreviate(1500000) // '1.5M'
|
|
514
|
-
NumberUtil.toOrdinal(3) // '3rd'
|
|
515
|
-
NumberUtil.clamp(value, 0, 100)
|
|
516
|
-
NumberUtil.percentage(part, total)
|
|
517
|
-
NumberUtil.round(3.14159, 2) // 3.14
|
|
518
|
-
NumberUtil.randomInt(1, 100)
|
|
519
|
-
NumberUtil.isPrime(17) // true
|
|
520
|
-
NumberUtil.fibonacci(10) // 55
|
|
521
|
-
NumberUtil.lerp(0, 100, 0.5) // 50
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
### ObjectUtil
|
|
525
|
-
|
|
526
|
-
```typescript
|
|
527
|
-
ObjectUtil.deepClone(obj)
|
|
528
|
-
ObjectUtil.deepMerge(defaults, overrides)
|
|
529
|
-
ObjectUtil.pick(user, ['name', 'email'])
|
|
530
|
-
ObjectUtil.omit(user, ['password'])
|
|
531
|
-
ObjectUtil.flattenObject({ a: { b: 1 } }) // { 'a.b': 1 }
|
|
532
|
-
ObjectUtil.unflattenObject({ 'a.b': 1 }) // { a: { b: 1 } }
|
|
533
|
-
ObjectUtil.getPath(obj, 'user.address.city')
|
|
534
|
-
ObjectUtil.setPath(obj, 'user.role', 'admin')
|
|
535
|
-
ObjectUtil.toQueryString({ page: 1, q: 'test' }) // 'page=1&q=test'
|
|
536
|
-
ObjectUtil.diff(objA, objB) // changed keys
|
|
537
|
-
ObjectUtil.isEqual(a, b) // deep equality
|
|
538
|
-
```
|
|
539
|
-
|
|
540
|
-
### StringUtil
|
|
541
|
-
|
|
542
|
-
```typescript
|
|
543
|
-
StringUtil.camelCase('hello world') // 'helloWorld'
|
|
544
|
-
StringUtil.pascalCase('hello world') // 'HelloWorld'
|
|
545
|
-
StringUtil.snakeCase('helloWorld') // 'hello_world'
|
|
546
|
-
StringUtil.kebabCase('Hello World') // 'hello-world'
|
|
547
|
-
StringUtil.titleCase('hello world') // 'Hello World'
|
|
548
|
-
StringUtil.toSlug('Hello World!') // 'hello-world'
|
|
549
|
-
StringUtil.truncate('Long text...', 20)
|
|
550
|
-
StringUtil.mask('4111111111111111', 4) // '************1111'
|
|
551
|
-
StringUtil.initials('Bilal Raza') // 'BR'
|
|
552
|
-
StringUtil.escapeHtml('<script>') // '<script>'
|
|
553
|
-
StringUtil.stripHtml('<p>Hello</p>') // 'Hello'
|
|
554
|
-
StringUtil.randomString(16)
|
|
555
|
-
StringUtil.isPalindrome('racecar') // true
|
|
556
|
-
```
|
|
557
|
-
|
|
558
|
-
### StorageUtil
|
|
559
|
-
|
|
560
|
-
```typescript
|
|
561
|
-
// localStorage
|
|
562
|
-
StorageUtil.setLocal('user', { id: 1, name: 'Bilal' });
|
|
563
|
-
StorageUtil.getLocal<User>('user');
|
|
564
|
-
StorageUtil.removeLocal('user');
|
|
565
|
-
|
|
566
|
-
// sessionStorage
|
|
567
|
-
StorageUtil.setSession('draft', formData);
|
|
568
|
-
StorageUtil.getSession<Draft>('draft');
|
|
569
|
-
|
|
570
|
-
// Cookies
|
|
571
|
-
StorageUtil.setCookie('lang', 'en', 30); // expires in 30 days
|
|
572
|
-
StorageUtil.getCookie('lang');
|
|
573
|
-
StorageUtil.removeCookie('lang');
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
### ValidationUtil
|
|
577
|
-
|
|
578
|
-
```typescript
|
|
579
|
-
ValidationUtil.isEmail('user@example.com') // true
|
|
580
|
-
ValidationUtil.isUrl('https://example.com') // true
|
|
581
|
-
ValidationUtil.isPhone('+1-800-555-0100')
|
|
582
|
-
ValidationUtil.isCreditCard('4111111111111111') // Luhn check
|
|
583
|
-
ValidationUtil.isIPv4('192.168.1.1')
|
|
584
|
-
ValidationUtil.isStrongPassword('P@ssw0rd!', {
|
|
585
|
-
minLength: 8,
|
|
586
|
-
requireUppercase: true,
|
|
587
|
-
requireSpecial: true,
|
|
588
|
-
})
|
|
589
|
-
ValidationUtil.passwordStrength('P@ssw0rd!') // 0–5 score
|
|
590
|
-
ValidationUtil.isJSON('{"key":"val"}')
|
|
591
|
-
ValidationUtil.isHexColor('#ff5500')
|
|
592
|
-
ValidationUtil.matchesPattern(value, /^\d{4}$/)
|
|
593
|
-
```
|
|
594
|
-
|
|
595
|
-
---
|
|
596
|
-
|
|
597
|
-
## Module Import
|
|
598
|
-
|
|
599
|
-
```typescript
|
|
600
|
-
// app.module.ts (or standalone app)
|
|
601
|
-
import { FormStructureModule, OslSkeletonModule } from 'osl-base-extended';
|
|
602
|
-
|
|
603
|
-
@NgModule({
|
|
604
|
-
imports: [
|
|
605
|
-
FormStructureModule,
|
|
606
|
-
OslSkeletonModule,
|
|
607
|
-
]
|
|
608
|
-
})
|
|
609
|
-
export class AppModule {}
|
|
610
|
-
```
|
|
611
|
-
|
|
612
|
-
---
|
|
613
|
-
|
|
614
|
-
## Interfaces Reference
|
|
615
|
-
|
|
616
|
-
```typescript
|
|
617
|
-
// HTTP
|
|
618
|
-
interface HttpResponse<T> {
|
|
619
|
-
isSuccessful: boolean;
|
|
620
|
-
statusCode: number;
|
|
621
|
-
error: string;
|
|
622
|
-
result: T;
|
|
623
|
-
headers?: HttpHeaders;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
interface myParams {
|
|
627
|
-
property: string;
|
|
628
|
-
value: any;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// Grid
|
|
632
|
-
interface OslGridColumn {
|
|
633
|
-
key: string;
|
|
634
|
-
label: string;
|
|
635
|
-
enums?: Record<any, string>;
|
|
636
|
-
displayFn?: (row: any) => string;
|
|
637
|
-
isActions?: boolean;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
interface OslPageEvent { page: number; pageSize: number; }
|
|
641
|
-
interface OslSortEvent { key: string; direction: 'asc' | 'desc'; }
|
|
642
|
-
|
|
643
|
-
// Form
|
|
644
|
-
type InputType = 'text' | 'password' | 'email' | 'number' | 'tel' | 'url';
|
|
645
|
-
type DateInputType = 'date' | 'datetime-local' | 'time' | 'month' | 'week';
|
|
646
|
-
type TextareaResize = 'none' | 'both' | 'horizontal' | 'vertical';
|
|
647
|
-
type ButtonVariant = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info'
|
|
648
|
-
| 'outline-primary' | 'outline-secondary' | 'icon';
|
|
649
|
-
type ButtonSize = 'sm' | 'md' | 'lg';
|
|
650
|
-
```
|
|
651
|
-
|
|
652
|
-
---
|
|
653
|
-
|
|
654
|
-
## License
|
|
655
|
-
|
|
656
|
-
ISC — © Bilal Raza
|
|
1
|
+
# osl-base-extended
|
|
2
|
+
|
|
3
|
+
> **Enterprise Angular UI toolkit** — HTTP layer, CRUD UI, dynamic forms, skeleton loading, and 200+ utilities. Build data-driven pages in minutes, not days.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/osl-base-extended)
|
|
6
|
+
[](https://angular.dev)
|
|
7
|
+
[](https://opensource.org/licenses/ISC)
|
|
8
|
+
[](https://github.com/bilalraza052)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## What's Inside
|
|
13
|
+
|
|
14
|
+
| Module | What it does |
|
|
15
|
+
|--------|-------------|
|
|
16
|
+
| **`Httpbase`** | Abstract HTTP service — extend once per domain, get CRUD + auth + error handling free |
|
|
17
|
+
| **`<osl-setup>`** | Zero-config CRUD page: grid + add/edit dialog + delete confirm + search + pagination |
|
|
18
|
+
| **`<osl-dynamic-form>`** | Declarative forms — 12+ field types from a JSON config array |
|
|
19
|
+
| **`<osl-grid>`** | Data table with server-side or auto pagination, sorting, enum display |
|
|
20
|
+
| **`<osl-form-grid>`** | Inline editable grid with dynamic form elements per cell |
|
|
21
|
+
| **`[oslSkeleton]`** | GPU-accelerated skeleton directive — 8 layouts, 4 animations, 1 attribute |
|
|
22
|
+
| **`baseComponent`** | Inject once: `showSuccess()`, `showError()`, `openDialog()`, `openDeleteDialog()` |
|
|
23
|
+
| **`ArrayUtil`** | 25+ array helpers: chunk, groupBy, sortBy, paginate, intersection… |
|
|
24
|
+
| **`DateUtil`** | 40+ date helpers: format, diff, add/subtract, timeAgo, getAge… |
|
|
25
|
+
| **`NumberUtil`** | 30+ number helpers: formatCurrency, abbreviate, clamp, isPrime… |
|
|
26
|
+
| **`ObjectUtil`** | 25+ object helpers: deepClone, deepMerge, pick, omit, flattenObject… |
|
|
27
|
+
| **`StringUtil`** | 35+ string helpers: camelCase, slugify, truncate, mask, escapeHtml… |
|
|
28
|
+
| **`StorageUtil`** | localStorage, sessionStorage, and cookie helpers with JSON serialization |
|
|
29
|
+
| **`ValidationUtil`** | 40+ validators: email, URL, phone, password strength, credit card (Luhn)… |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install osl-base-extended
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Peer dependencies
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm install @angular/material
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Add `HttpClientModule` to your app
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
// app.config.ts
|
|
49
|
+
import { provideHttpClient } from '@angular/common/http';
|
|
50
|
+
|
|
51
|
+
export const appConfig: ApplicationConfig = {
|
|
52
|
+
providers: [provideHttpClient()]
|
|
53
|
+
};
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## API Calling — Httpbase
|
|
59
|
+
|
|
60
|
+
The core of the library. Create one service per backend controller by extending `Httpbase`.
|
|
61
|
+
|
|
62
|
+
### 1. Create a service
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
// user.service.ts
|
|
66
|
+
import { Injectable } from '@angular/core';
|
|
67
|
+
import { Httpbase } from 'osl-base-extended';
|
|
68
|
+
|
|
69
|
+
@Injectable({ providedIn: 'root' })
|
|
70
|
+
export class UserService extends Httpbase {
|
|
71
|
+
constructor() {
|
|
72
|
+
super('User'); // maps to /api/User/...
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 2. Call built-in CRUD methods — no extra code needed
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
// user.component.ts
|
|
81
|
+
export class UserComponent {
|
|
82
|
+
private userSvc = inject(UserService);
|
|
83
|
+
|
|
84
|
+
async loadAll() {
|
|
85
|
+
const res = await this.userSvc.getAll<User[]>();
|
|
86
|
+
if (res.isSuccessful) this.users = res.result;
|
|
87
|
+
else this.base.showError(res.error);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async loadOne(id: number) {
|
|
91
|
+
const res = await this.userSvc.getById<User>(id);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async create(user: User) {
|
|
95
|
+
const res = await this.userSvc.save<User>(user);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async edit(user: User) {
|
|
99
|
+
const res = await this.userSvc.update<User>(user);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async remove(id: number) {
|
|
103
|
+
const res = await this.userSvc.remove(id);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Built-in public methods
|
|
109
|
+
|
|
110
|
+
| Method | HTTP verb | Endpoint hit |
|
|
111
|
+
|--------|-----------|-------------|
|
|
112
|
+
| `getAll<T>()` | GET | `/api/{controller}/GetAll` |
|
|
113
|
+
| `getById<T>(id)` | GET | `/api/{controller}/GetById?id=…` |
|
|
114
|
+
| `save<T>(body)` | POST | `/api/{controller}/Save` |
|
|
115
|
+
| `update<T>(body)` | PUT | `/api/{controller}/Update` |
|
|
116
|
+
| `remove<T>(id)` | DELETE | `/api/{controller}/Delete?id=…` |
|
|
117
|
+
| `search(body)` | POST | `/api/{controller}/Search` |
|
|
118
|
+
| `getConfig()` | GET | `/api/{controller}/getConfig` |
|
|
119
|
+
|
|
120
|
+
### Custom endpoints — protected verb wrappers
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
@Injectable({ providedIn: 'root' })
|
|
124
|
+
export class UserService extends Httpbase {
|
|
125
|
+
constructor() { super('User'); }
|
|
126
|
+
|
|
127
|
+
// Custom POST
|
|
128
|
+
async changePassword(body: ChangePasswordDto) {
|
|
129
|
+
return this.post<void>('ChangePassword', body);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Custom PUT
|
|
133
|
+
async updateRole(body: UpdateRoleDto) {
|
|
134
|
+
return this.put<User>('UpdateRole', body);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Custom PATCH
|
|
138
|
+
async toggleActive(id: number) {
|
|
139
|
+
return this.patch<User>('ToggleActive', { id });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Custom DELETE with query params
|
|
143
|
+
async bulkDelete(ids: number[]) {
|
|
144
|
+
return this.delete<void>('BulkDelete', [{ property: 'ids', value: ids }]);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// File upload (multipart/form-data)
|
|
148
|
+
async uploadAvatar(file: File) {
|
|
149
|
+
const fd = new FormData();
|
|
150
|
+
fd.append('file', file);
|
|
151
|
+
return this.upload<{ url: string }>('UploadAvatar', fd);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### Protected verb wrappers
|
|
157
|
+
|
|
158
|
+
| Method | HTTP verb | Notes |
|
|
159
|
+
|--------|-----------|-------|
|
|
160
|
+
| `post<T>(method, body)` | POST | JSON body |
|
|
161
|
+
| `get<T>(method, params?)` | GET | query params via `myParams[]` |
|
|
162
|
+
| `put<T>(method, body)` | PUT | JSON body |
|
|
163
|
+
| `patch<T>(method, body)` | PATCH | JSON body |
|
|
164
|
+
| `delete<T>(method, params?)` | DELETE | query params |
|
|
165
|
+
| `upload<T>(method, formData)` | POST multipart | 60 s timeout |
|
|
166
|
+
|
|
167
|
+
### `HttpResponse<T>` — every method returns this
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
interface HttpResponse<T> {
|
|
171
|
+
isSuccessful: boolean; // true for 2xx
|
|
172
|
+
statusCode: number;
|
|
173
|
+
error: string; // human-readable error message
|
|
174
|
+
result: T; // response body
|
|
175
|
+
headers?: HttpHeaders;
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Authentication
|
|
180
|
+
|
|
181
|
+
The service reads `token` from `localStorage` and sends it as `Authorization: Bearer <token>` on every request automatically.
|
|
182
|
+
|
|
183
|
+
```typescript
|
|
184
|
+
localStorage.setItem('token', 'your-jwt-here');
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## CRUD UI — `<osl-setup>`
|
|
190
|
+
|
|
191
|
+
The flagship component. One tag replaces a full page of boilerplate.
|
|
192
|
+
|
|
193
|
+
### Template
|
|
194
|
+
|
|
195
|
+
```html
|
|
196
|
+
<osl-setup
|
|
197
|
+
title="Users"
|
|
198
|
+
[columns]="columns"
|
|
199
|
+
[datasource]="users"
|
|
200
|
+
[formElements]="formElements"
|
|
201
|
+
[loading]="loading"
|
|
202
|
+
[isPaginated]="true"
|
|
203
|
+
[totalRecords]="totalRecords"
|
|
204
|
+
(onAdd)="onAdd()"
|
|
205
|
+
(onEdit)="onEdit($event)"
|
|
206
|
+
(onDelete)="onDelete($event)"
|
|
207
|
+
(onSave)="onSave($event)"
|
|
208
|
+
(onSearch)="onSearch($event)"
|
|
209
|
+
(pageChange)="onPageChange($event)"
|
|
210
|
+
/>
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Component
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
export class UsersComponent {
|
|
217
|
+
loading = false;
|
|
218
|
+
users: User[] = [];
|
|
219
|
+
totalRecords = 0;
|
|
220
|
+
|
|
221
|
+
columns: OslGridColumn[] = [
|
|
222
|
+
{ key: 'name', label: 'Name' },
|
|
223
|
+
{ key: 'email', label: 'Email' },
|
|
224
|
+
{ key: 'status', label: 'Status', enums: { 1: 'Active', 0: 'Inactive' } },
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
formElements: elements[] = [
|
|
228
|
+
{ key: 'name', label: 'Full Name', elementType: 'textbox', columns: 6, required: true },
|
|
229
|
+
{ key: 'email', label: 'Email', elementType: 'textbox', columns: 6, required: true,
|
|
230
|
+
inputType: 'email' },
|
|
231
|
+
{ key: 'status', label: 'Status', elementType: 'select', columns: 6,
|
|
232
|
+
datasource: [{ id: 1, name: 'Active' }, { id: 0, name: 'Inactive' }],
|
|
233
|
+
displayField: 'name', valueField: 'id' },
|
|
234
|
+
];
|
|
235
|
+
|
|
236
|
+
async onSave(e: { model: User; mode: 'add' | 'edit' }) {
|
|
237
|
+
const res = e.mode === 'add'
|
|
238
|
+
? await this.userSvc.save(e.model)
|
|
239
|
+
: await this.userSvc.update(e.model);
|
|
240
|
+
if (res.isSuccessful) this.loadUsers();
|
|
241
|
+
else this.base.showError(res.error);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async onDelete(user: User) {
|
|
245
|
+
const res = await this.userSvc.remove(user.id);
|
|
246
|
+
if (res.isSuccessful) this.loadUsers();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### `<osl-setup>` inputs
|
|
252
|
+
|
|
253
|
+
| Input | Type | Default | Description |
|
|
254
|
+
|-------|------|---------|-------------|
|
|
255
|
+
| `title` | `string` | — | Entity name shown in heading and dialog |
|
|
256
|
+
| `columns` | `OslGridColumn[]` | — | Column definitions |
|
|
257
|
+
| `datasource` | `any[]` | — | Table row data |
|
|
258
|
+
| `formElements` | `elements[]` | — | Dynamic form configuration |
|
|
259
|
+
| `loading` | `boolean` | `false` | Shows skeleton rows while loading |
|
|
260
|
+
| `isPaginated` | `boolean` | `false` | Enable pagination footer |
|
|
261
|
+
| `pageSize` | `number` | `25` | Rows per page |
|
|
262
|
+
| `totalRecords` | `number` | — | Total count for server-side pagination |
|
|
263
|
+
| `autoMode` | `boolean` | `true` | Library handles client-side sort/page |
|
|
264
|
+
| `tableHeight` | `string` | — | CSS height for scrollable table body |
|
|
265
|
+
| `dialogWidth` | `string` | `'50vw'` | Width of add/edit dialog |
|
|
266
|
+
| `isLister` | `boolean` | `false` | Hides actions column |
|
|
267
|
+
| `beforeDisplay` | `(model) => any` | — | Transform model before opening dialog |
|
|
268
|
+
| `onAddEditFn` | `(model, mode) => void` | — | Override default add/edit dialog |
|
|
269
|
+
|
|
270
|
+
### `<osl-setup>` outputs
|
|
271
|
+
|
|
272
|
+
| Output | Payload | When |
|
|
273
|
+
|--------|---------|------|
|
|
274
|
+
| `onAdd` | — | Add button clicked |
|
|
275
|
+
| `onEdit` | row object | Edit button clicked |
|
|
276
|
+
| `onDelete` | row object | Delete confirmed |
|
|
277
|
+
| `onSave` | `{ model, mode }` | Dialog save button clicked |
|
|
278
|
+
| `onSearch` | `string` | Search input changes |
|
|
279
|
+
| `pageChange` | `OslPageEvent` | Page number changes |
|
|
280
|
+
| `pageSizeChange` | `number` | Page size changes |
|
|
281
|
+
| `sortChange` | `OslSortEvent` | Column header clicked |
|
|
282
|
+
| `onRowClick` | row object | Row clicked |
|
|
283
|
+
|
|
284
|
+
---
|
|
285
|
+
|
|
286
|
+
## Dynamic Forms — `<osl-dynamic-form>`
|
|
287
|
+
|
|
288
|
+
Build any form from a plain array — no template code needed.
|
|
289
|
+
|
|
290
|
+
```html
|
|
291
|
+
<osl-dynamic-form
|
|
292
|
+
[elements]="formElements"
|
|
293
|
+
[(model)]="formModel"
|
|
294
|
+
[skeletonLoading]="loading"
|
|
295
|
+
/>
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Element types
|
|
299
|
+
|
|
300
|
+
| `elementType` | Renders |
|
|
301
|
+
|---------------|---------|
|
|
302
|
+
| `textbox` | `<osl-input>` — text, password, email, number, tel, url |
|
|
303
|
+
| `textarea` | `<osl-textarea>` — resizable, character counter |
|
|
304
|
+
| `select` | `<osl-select>` — static or API datasource |
|
|
305
|
+
| `autocomplete` | `<osl-autocomplete>` — local or API search with lister |
|
|
306
|
+
| `radio` | `<osl-radio>` — horizontal/vertical layout |
|
|
307
|
+
| `checkbox` | `<osl-checkbox>` — with indeterminate support |
|
|
308
|
+
| `slide-toggle` | `<osl-slide-toggle>` — custom true/false labels |
|
|
309
|
+
| `datepicker` | `<osl-datepicker>` — date, datetime-local, time, month, week |
|
|
310
|
+
| `file-uploader` | `<osl-file-upload>` — drag & drop, size validation |
|
|
311
|
+
| `button` | `<osl-button>` — all variants and sizes |
|
|
312
|
+
| `fieldset` | Nested group of elements |
|
|
313
|
+
| `templateRef` | Inject a custom `TemplateRef` |
|
|
314
|
+
|
|
315
|
+
### Common element options
|
|
316
|
+
|
|
317
|
+
```typescript
|
|
318
|
+
{
|
|
319
|
+
key: 'fieldName', // maps to model property
|
|
320
|
+
label: 'Display Label',
|
|
321
|
+
elementType: 'textbox',
|
|
322
|
+
columns: 6, // 1–12 grid columns (Bootstrap-style)
|
|
323
|
+
required: true,
|
|
324
|
+
requiredIf: (m) => m.type === 'enterprise',
|
|
325
|
+
hideIf: (m) => !m.showField,
|
|
326
|
+
disabledIf: () => !hasPermission,
|
|
327
|
+
change: (model) => { /* react to value change */ },
|
|
328
|
+
|
|
329
|
+
// select / autocomplete
|
|
330
|
+
datasource: [],
|
|
331
|
+
displayField: 'name',
|
|
332
|
+
valueField: 'id',
|
|
333
|
+
|
|
334
|
+
// API-backed datasource
|
|
335
|
+
apiService: inject(CategoryService),
|
|
336
|
+
apiMethod: 'getAll',
|
|
337
|
+
|
|
338
|
+
// textbox extras
|
|
339
|
+
inputType: 'email',
|
|
340
|
+
placeholder: 'Enter email',
|
|
341
|
+
mask: '(000) 000-0000',
|
|
342
|
+
prefixIcon: 'person',
|
|
343
|
+
suffixIcon: 'clear',
|
|
344
|
+
maxLength: 100,
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
---
|
|
349
|
+
|
|
350
|
+
## Data Grid — `<osl-grid>`
|
|
351
|
+
|
|
352
|
+
```html
|
|
353
|
+
<osl-grid
|
|
354
|
+
[columns]="columns"
|
|
355
|
+
[datasource]="rows"
|
|
356
|
+
[isPaginated]="true"
|
|
357
|
+
[totalRecords]="total"
|
|
358
|
+
[loading]="loading"
|
|
359
|
+
tableHeight="400px"
|
|
360
|
+
(editClick)="onEdit($event)"
|
|
361
|
+
(deleteClick)="onDelete($event)"
|
|
362
|
+
(pageChange)="loadPage($event)"
|
|
363
|
+
(sortChange)="onSort($event)"
|
|
364
|
+
/>
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### `OslGridColumn` interface
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
interface OslGridColumn {
|
|
371
|
+
key: string; // model property
|
|
372
|
+
label: string; // header text
|
|
373
|
+
enums?: Record<any, string>; // value → display label map
|
|
374
|
+
displayFn?: (row: any) => string; // custom cell renderer
|
|
375
|
+
isActions?: boolean; // marks the edit/delete column
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## Skeleton Loading — `[oslSkeleton]`
|
|
382
|
+
|
|
383
|
+
Drop one attribute on any element.
|
|
384
|
+
|
|
385
|
+
```html
|
|
386
|
+
<!-- Auto-detects child structure -->
|
|
387
|
+
<div [oslSkeleton]="loading">...</div>
|
|
388
|
+
|
|
389
|
+
<!-- Explicit types -->
|
|
390
|
+
<table [oslSkeleton]="loading" oslSkeletonType="table"
|
|
391
|
+
[oslSkeletonTableRows]="8" [oslSkeletonTableCols]="4">
|
|
392
|
+
</table>
|
|
393
|
+
|
|
394
|
+
<ul [oslSkeleton]="loading" oslSkeletonType="list" [oslSkeletonListItems]="5"></ul>
|
|
395
|
+
|
|
396
|
+
<div [oslSkeleton]="loading" oslSkeletonType="card"></div>
|
|
397
|
+
|
|
398
|
+
<img [oslSkeleton]="loading" oslSkeletonType="circle" oslSkeletonCircleSize="48px">
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### Key inputs
|
|
402
|
+
|
|
403
|
+
| Input | Type | Default | Description |
|
|
404
|
+
|-------|------|---------|-------------|
|
|
405
|
+
| `oslSkeleton` | `boolean` | — | Toggle on/off |
|
|
406
|
+
| `oslSkeletonType` | `'auto'│'text'│'rect'│'circle'│'card'│'list'│'table'│'avatar-text'` | `'auto'` | Layout preset |
|
|
407
|
+
| `oslSkeletonAnimation` | `'shimmer'│'pulse'│'wave'│'none'` | `'shimmer'` | Animation style |
|
|
408
|
+
| `oslSkeletonTheme` | `'light'│'dark'` | `'light'` | Color theme |
|
|
409
|
+
| `oslSkeletonRows` | `number` | `3` | Text rows count |
|
|
410
|
+
| `oslSkeletonTableRows` | `number` | `5` | Table row count |
|
|
411
|
+
| `oslSkeletonTableCols` | `number` | `4` | Table column count |
|
|
412
|
+
| `oslSkeletonListItems` | `number` | `4` | List item count |
|
|
413
|
+
| `oslSkeletonDuration` | `number` | `1500` | Animation ms |
|
|
414
|
+
| `oslSkeletonDelay` | `number` | `0` | Delay before showing |
|
|
415
|
+
|
|
416
|
+
### Global theme
|
|
417
|
+
|
|
418
|
+
```typescript
|
|
419
|
+
// Set once in a root component
|
|
420
|
+
inject(OslSkeletonThemeService).setTheme('dark');
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
## `baseComponent` — UI Utilities
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
export class MyComponent {
|
|
429
|
+
private base = inject(baseComponent);
|
|
430
|
+
|
|
431
|
+
showFeedback(res: HttpResponse<any>) {
|
|
432
|
+
if (res.isSuccessful) this.base.showSuccess('Saved!');
|
|
433
|
+
else this.base.showError(res.error);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
openCustomDialog() {
|
|
437
|
+
this.base.openDialog(
|
|
438
|
+
'Edit User',
|
|
439
|
+
MyFormComponent, // formBody
|
|
440
|
+
MyFooterComponent, // formFooter
|
|
441
|
+
'40vw',
|
|
442
|
+
{ userId: 1 } // data passed to dialog
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
confirmDelete(item: any) {
|
|
447
|
+
this.base.openDeleteDialog(
|
|
448
|
+
`Delete "${item.name}"?`,
|
|
449
|
+
'Confirm Delete',
|
|
450
|
+
'Yes, Delete',
|
|
451
|
+
'Cancel',
|
|
452
|
+
item
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
---
|
|
459
|
+
|
|
460
|
+
## Utility Namespaces
|
|
461
|
+
|
|
462
|
+
Import by namespace — tree-shakable pure functions, no external dependencies.
|
|
463
|
+
|
|
464
|
+
```typescript
|
|
465
|
+
import { ArrayUtil, DateUtil, NumberUtil, ObjectUtil,
|
|
466
|
+
StringUtil, StorageUtil, ValidationUtil } from 'osl-base-extended';
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### ArrayUtil
|
|
470
|
+
|
|
471
|
+
```typescript
|
|
472
|
+
ArrayUtil.chunk([1,2,3,4,5], 2) // [[1,2],[3,4],[5]]
|
|
473
|
+
ArrayUtil.unique([1,1,2,3,2]) // [1,2,3]
|
|
474
|
+
ArrayUtil.uniqueBy(users, 'email')
|
|
475
|
+
ArrayUtil.groupBy(orders, 'status')
|
|
476
|
+
ArrayUtil.sortBy(items, 'price', 'asc')
|
|
477
|
+
ArrayUtil.filterBy(items, 'category', 'Electronics')
|
|
478
|
+
ArrayUtil.paginate(items, 2, 10) // page 2, 10 per page
|
|
479
|
+
ArrayUtil.flatten([[1,[2]],3])
|
|
480
|
+
ArrayUtil.sumBy(cart, 'total')
|
|
481
|
+
ArrayUtil.intersection([1,2,3],[2,3,4]) // [2,3]
|
|
482
|
+
ArrayUtil.difference([1,2,3],[2,3]) // [1]
|
|
483
|
+
ArrayUtil.toggle(selected, item) // add if missing, remove if present
|
|
484
|
+
ArrayUtil.shuffle(deck)
|
|
485
|
+
ArrayUtil.sample(pool, 3) // random 3
|
|
486
|
+
ArrayUtil.zip(['a','b'],[1,2]) // [['a',1],['b',2]]
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### DateUtil
|
|
490
|
+
|
|
491
|
+
```typescript
|
|
492
|
+
DateUtil.formatDate(new Date(), 'YYYY-MM-DD')
|
|
493
|
+
DateUtil.timeAgo(pastDate) // '3 hours ago'
|
|
494
|
+
DateUtil.getAge(birthDate) // 28
|
|
495
|
+
DateUtil.addDays(date, 7)
|
|
496
|
+
DateUtil.addMonths(date, 3)
|
|
497
|
+
DateUtil.diffInDays(dateA, dateB)
|
|
498
|
+
DateUtil.diffInMinutes(dateA, dateB)
|
|
499
|
+
DateUtil.isBefore(dateA, dateB)
|
|
500
|
+
DateUtil.isToday(date)
|
|
501
|
+
DateUtil.isWeekend(date)
|
|
502
|
+
DateUtil.startOfMonth(date)
|
|
503
|
+
DateUtil.endOfMonth(date)
|
|
504
|
+
DateUtil.nextWorkday(date)
|
|
505
|
+
DateUtil.getWeekNumber(date)
|
|
506
|
+
DateUtil.isLeapYear(2024) // true
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### NumberUtil
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
NumberUtil.formatCurrency(1500.5, 'USD') // '$1,500.50'
|
|
513
|
+
NumberUtil.abbreviate(1500000) // '1.5M'
|
|
514
|
+
NumberUtil.toOrdinal(3) // '3rd'
|
|
515
|
+
NumberUtil.clamp(value, 0, 100)
|
|
516
|
+
NumberUtil.percentage(part, total)
|
|
517
|
+
NumberUtil.round(3.14159, 2) // 3.14
|
|
518
|
+
NumberUtil.randomInt(1, 100)
|
|
519
|
+
NumberUtil.isPrime(17) // true
|
|
520
|
+
NumberUtil.fibonacci(10) // 55
|
|
521
|
+
NumberUtil.lerp(0, 100, 0.5) // 50
|
|
522
|
+
```
|
|
523
|
+
|
|
524
|
+
### ObjectUtil
|
|
525
|
+
|
|
526
|
+
```typescript
|
|
527
|
+
ObjectUtil.deepClone(obj)
|
|
528
|
+
ObjectUtil.deepMerge(defaults, overrides)
|
|
529
|
+
ObjectUtil.pick(user, ['name', 'email'])
|
|
530
|
+
ObjectUtil.omit(user, ['password'])
|
|
531
|
+
ObjectUtil.flattenObject({ a: { b: 1 } }) // { 'a.b': 1 }
|
|
532
|
+
ObjectUtil.unflattenObject({ 'a.b': 1 }) // { a: { b: 1 } }
|
|
533
|
+
ObjectUtil.getPath(obj, 'user.address.city')
|
|
534
|
+
ObjectUtil.setPath(obj, 'user.role', 'admin')
|
|
535
|
+
ObjectUtil.toQueryString({ page: 1, q: 'test' }) // 'page=1&q=test'
|
|
536
|
+
ObjectUtil.diff(objA, objB) // changed keys
|
|
537
|
+
ObjectUtil.isEqual(a, b) // deep equality
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### StringUtil
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
StringUtil.camelCase('hello world') // 'helloWorld'
|
|
544
|
+
StringUtil.pascalCase('hello world') // 'HelloWorld'
|
|
545
|
+
StringUtil.snakeCase('helloWorld') // 'hello_world'
|
|
546
|
+
StringUtil.kebabCase('Hello World') // 'hello-world'
|
|
547
|
+
StringUtil.titleCase('hello world') // 'Hello World'
|
|
548
|
+
StringUtil.toSlug('Hello World!') // 'hello-world'
|
|
549
|
+
StringUtil.truncate('Long text...', 20)
|
|
550
|
+
StringUtil.mask('4111111111111111', 4) // '************1111'
|
|
551
|
+
StringUtil.initials('Bilal Raza') // 'BR'
|
|
552
|
+
StringUtil.escapeHtml('<script>') // '<script>'
|
|
553
|
+
StringUtil.stripHtml('<p>Hello</p>') // 'Hello'
|
|
554
|
+
StringUtil.randomString(16)
|
|
555
|
+
StringUtil.isPalindrome('racecar') // true
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
### StorageUtil
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
// localStorage
|
|
562
|
+
StorageUtil.setLocal('user', { id: 1, name: 'Bilal' });
|
|
563
|
+
StorageUtil.getLocal<User>('user');
|
|
564
|
+
StorageUtil.removeLocal('user');
|
|
565
|
+
|
|
566
|
+
// sessionStorage
|
|
567
|
+
StorageUtil.setSession('draft', formData);
|
|
568
|
+
StorageUtil.getSession<Draft>('draft');
|
|
569
|
+
|
|
570
|
+
// Cookies
|
|
571
|
+
StorageUtil.setCookie('lang', 'en', 30); // expires in 30 days
|
|
572
|
+
StorageUtil.getCookie('lang');
|
|
573
|
+
StorageUtil.removeCookie('lang');
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
### ValidationUtil
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
ValidationUtil.isEmail('user@example.com') // true
|
|
580
|
+
ValidationUtil.isUrl('https://example.com') // true
|
|
581
|
+
ValidationUtil.isPhone('+1-800-555-0100')
|
|
582
|
+
ValidationUtil.isCreditCard('4111111111111111') // Luhn check
|
|
583
|
+
ValidationUtil.isIPv4('192.168.1.1')
|
|
584
|
+
ValidationUtil.isStrongPassword('P@ssw0rd!', {
|
|
585
|
+
minLength: 8,
|
|
586
|
+
requireUppercase: true,
|
|
587
|
+
requireSpecial: true,
|
|
588
|
+
})
|
|
589
|
+
ValidationUtil.passwordStrength('P@ssw0rd!') // 0–5 score
|
|
590
|
+
ValidationUtil.isJSON('{"key":"val"}')
|
|
591
|
+
ValidationUtil.isHexColor('#ff5500')
|
|
592
|
+
ValidationUtil.matchesPattern(value, /^\d{4}$/)
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## Module Import
|
|
598
|
+
|
|
599
|
+
```typescript
|
|
600
|
+
// app.module.ts (or standalone app)
|
|
601
|
+
import { FormStructureModule, OslSkeletonModule } from 'osl-base-extended';
|
|
602
|
+
|
|
603
|
+
@NgModule({
|
|
604
|
+
imports: [
|
|
605
|
+
FormStructureModule,
|
|
606
|
+
OslSkeletonModule,
|
|
607
|
+
]
|
|
608
|
+
})
|
|
609
|
+
export class AppModule {}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
---
|
|
613
|
+
|
|
614
|
+
## Interfaces Reference
|
|
615
|
+
|
|
616
|
+
```typescript
|
|
617
|
+
// HTTP
|
|
618
|
+
interface HttpResponse<T> {
|
|
619
|
+
isSuccessful: boolean;
|
|
620
|
+
statusCode: number;
|
|
621
|
+
error: string;
|
|
622
|
+
result: T;
|
|
623
|
+
headers?: HttpHeaders;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
interface myParams {
|
|
627
|
+
property: string;
|
|
628
|
+
value: any;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Grid
|
|
632
|
+
interface OslGridColumn {
|
|
633
|
+
key: string;
|
|
634
|
+
label: string;
|
|
635
|
+
enums?: Record<any, string>;
|
|
636
|
+
displayFn?: (row: any) => string;
|
|
637
|
+
isActions?: boolean;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
interface OslPageEvent { page: number; pageSize: number; }
|
|
641
|
+
interface OslSortEvent { key: string; direction: 'asc' | 'desc'; }
|
|
642
|
+
|
|
643
|
+
// Form
|
|
644
|
+
type InputType = 'text' | 'password' | 'email' | 'number' | 'tel' | 'url';
|
|
645
|
+
type DateInputType = 'date' | 'datetime-local' | 'time' | 'month' | 'week';
|
|
646
|
+
type TextareaResize = 'none' | 'both' | 'horizontal' | 'vertical';
|
|
647
|
+
type ButtonVariant = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info'
|
|
648
|
+
| 'outline-primary' | 'outline-secondary' | 'icon';
|
|
649
|
+
type ButtonSize = 'sm' | 'md' | 'lg';
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
---
|
|
653
|
+
|
|
654
|
+
## License
|
|
655
|
+
|
|
656
|
+
ISC — © Bilal Raza
|