osl-base-extended 1.0.26 → 1.0.28

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
- [![npm version](https://img.shields.io/npm/v/osl-base-extended)](https://www.npmjs.com/package/osl-base-extended)
6
- [![Angular](https://img.shields.io/badge/Angular-21%2B-red)](https://angular.dev)
7
- [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
8
- [![Author](https://img.shields.io/badge/author-Bilal%20Raza-informational)](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>') // '&lt;script&gt;'
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
+ [![npm version](https://img.shields.io/npm/v/osl-base-extended)](https://www.npmjs.com/package/osl-base-extended)
6
+ [![Angular](https://img.shields.io/badge/Angular-21%2B-red)](https://angular.dev)
7
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
8
+ [![Author](https://img.shields.io/badge/author-Bilal%20Raza-informational)](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>') // '&lt;script&gt;'
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