@shivay_18/ng-crud 0.1.0
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.docx +0 -0
- package/README.md +142 -0
- package/dist/collection.json +10 -0
- package/dist/crud/files/module/components/__name@dasherize__-form/__name@dasherize__-form.component.html.template +48 -0
- package/dist/crud/files/module/components/__name@dasherize__-form/__name@dasherize__-form.component.scss.template +46 -0
- package/dist/crud/files/module/components/__name@dasherize__-form/__name@dasherize__-form.component.ts.template +121 -0
- package/dist/crud/files/module/components/__name@dasherize__-list/__name@dasherize__-delete-dialog.component.ts.template +27 -0
- package/dist/crud/files/module/components/__name@dasherize__-list/__name@dasherize__-list.component.html.template +75 -0
- package/dist/crud/files/module/components/__name@dasherize__-list/__name@dasherize__-list.component.scss.template +40 -0
- package/dist/crud/files/module/components/__name@dasherize__-list/__name@dasherize__-list.component.ts.template +95 -0
- package/dist/crud/files/module/components/__name@dasherize__-list/__name@dasherize__.module.ts.template +73 -0
- package/dist/crud/files/module/models/__name@dasherize__.model.ts.template +4 -0
- package/dist/crud/files/module/services/__name@dasherize__.service.ts.template +42 -0
- package/dist/crud/files/standalone/components/__name@dasherize__-form/__name@dasherize__-form.component.html.template +48 -0
- package/dist/crud/files/standalone/components/__name@dasherize__-form/__name@dasherize__-form.component.scss.template +46 -0
- package/dist/crud/files/standalone/components/__name@dasherize__-form/__name@dasherize__-form.component.ts.template +142 -0
- package/dist/crud/files/standalone/components/__name@dasherize__-list/__name@dasherize__-delete-dialog.component.ts.template +30 -0
- package/dist/crud/files/standalone/components/__name@dasherize__-list/__name@dasherize__-list.component.html.template +75 -0
- package/dist/crud/files/standalone/components/__name@dasherize__-list/__name@dasherize__-list.component.scss.template +40 -0
- package/dist/crud/files/standalone/components/__name@dasherize__-list/__name@dasherize__-list.component.ts.template +121 -0
- package/dist/crud/files/standalone/models/__name@dasherize__.model.ts.template +4 -0
- package/dist/crud/files/standalone/services/__name@dasherize__.service.ts.template +42 -0
- package/dist/crud/index.d.ts +3 -0
- package/dist/crud/index.js +141 -0
- package/dist/crud/index.js.map +1 -0
- package/dist/crud/schema.d.ts +7 -0
- package/dist/crud/schema.js +3 -0
- package/dist/crud/schema.js.map +1 -0
- package/dist/crud/schema.json +34 -0
- package/package.json +33 -0
package/README.docx
ADDED
|
Binary file
|
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# ng-crud
|
|
2
|
+
|
|
3
|
+
Angular CRUD generator using Angular Schematics and Angular Material.
|
|
4
|
+
|
|
5
|
+
Generates a **complete, production-ready CRUD** (List, Create, Edit, Delete) with:
|
|
6
|
+
|
|
7
|
+
- Angular Material UI (table, paginator, sort, dialogs, snackbar, forms)
|
|
8
|
+
- Reactive Forms with validation
|
|
9
|
+
- REST HTTP service
|
|
10
|
+
- Auto-detects Angular 14+ (Module-based) or Angular 17+ (Standalone)
|
|
11
|
+
- **Safe to uninstall** — generated code has zero dependency on this package
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- Angular 14+
|
|
18
|
+
- Angular Material installed (`ng add @angular/material`)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install --save-dev ng-crud
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
ng generate ng-crud:crud <name> --fields="field1:type,field2:type" --apiUrl="http://localhost:3000"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Options
|
|
37
|
+
|
|
38
|
+
| Option | Type | Default | Description |
|
|
39
|
+
| ------------ | ------- | ----------------------- | ----------------------------------------------- |
|
|
40
|
+
| `name` | string | _(required)_ | Resource name (e.g. `product`, `user`) |
|
|
41
|
+
| `fields` | string | `name:string` | Comma-separated fields. Append `*` for required |
|
|
42
|
+
| `apiUrl` | string | `http://localhost:3000` | Base REST API URL |
|
|
43
|
+
| `path` | string | _(empty)_ | Sub-path inside `src/app/` |
|
|
44
|
+
| `standalone` | boolean | auto-detected | Force standalone mode (Angular 17+) |
|
|
45
|
+
|
|
46
|
+
### Field Types
|
|
47
|
+
|
|
48
|
+
| Type | Form Control | Example |
|
|
49
|
+
| --------- | ------------ | ---------------- |
|
|
50
|
+
| `string` | text input | `name:string` |
|
|
51
|
+
| `number` | number input | `price:number` |
|
|
52
|
+
| `boolean` | checkbox | `active:boolean` |
|
|
53
|
+
|
|
54
|
+
Append `*` to mark a field as required: `name*:string`
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Examples
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Basic
|
|
62
|
+
ng generate ng-crud:crud product
|
|
63
|
+
|
|
64
|
+
# With fields
|
|
65
|
+
ng generate ng-crud:crud product --fields="name*:string,price*:number,active:boolean"
|
|
66
|
+
|
|
67
|
+
# With custom API and path
|
|
68
|
+
ng generate ng-crud:crud order --fields="orderId*:number,status*:string,total:number" --apiUrl="https://api.myapp.com" --path="features"
|
|
69
|
+
|
|
70
|
+
# Force standalone (Angular 17+)
|
|
71
|
+
ng generate ng-crud:crud user --fields="username*:string,email*:string,isAdmin:boolean" --standalone=true
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Generated File Structure
|
|
77
|
+
|
|
78
|
+
### Angular 14–16 (Module-based)
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
src/app/<name>/
|
|
82
|
+
├── <name>.module.ts ← NgModule with routing & all Material imports
|
|
83
|
+
├── models/
|
|
84
|
+
│ └── <name>.model.ts ← TypeScript interface
|
|
85
|
+
├── services/
|
|
86
|
+
│ └── <name>.service.ts ← HTTP CRUD service (GET/POST/PUT/DELETE)
|
|
87
|
+
├── <name>-list/
|
|
88
|
+
│ ├── <name>-list.component.ts
|
|
89
|
+
│ ├── <name>-list.component.html ← Material table, paginator, search
|
|
90
|
+
│ ├── <name>-list.component.scss
|
|
91
|
+
│ └── <name>-delete-dialog.component.ts
|
|
92
|
+
└── <name>-form/
|
|
93
|
+
├── <name>-form.component.ts ← Create & Edit (shared)
|
|
94
|
+
├── <name>-form.component.html ← Reactive form with validation
|
|
95
|
+
└── <name>-form.component.scss
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Angular 17+ (Standalone)
|
|
99
|
+
|
|
100
|
+
Same structure but without `<name>.module.ts`. Each component is standalone with its own imports.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## After Generation
|
|
105
|
+
|
|
106
|
+
### Module-based (Angular 14–16)
|
|
107
|
+
|
|
108
|
+
Add a lazy-loaded route to your `app-routing.module.ts`:
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
{
|
|
112
|
+
path: 'products',
|
|
113
|
+
loadChildren: () => import('./product/product.module').then(m => m.ProductModule)
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Standalone (Angular 17+)
|
|
118
|
+
|
|
119
|
+
The schematic auto-adds the list route to `app.routes.ts`. Add child routes manually if needed:
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
{
|
|
123
|
+
path: 'products',
|
|
124
|
+
loadComponent: () => import('./product/product-list/product-list.component').then(c => c.ProductListComponent)
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Uninstall
|
|
131
|
+
|
|
132
|
+
The generated code has **zero runtime dependency** on `ng-crud`. Uninstall safely anytime:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
npm uninstall ng-crud
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../node_modules/@angular-devkit/schematics/collection-schema.json",
|
|
3
|
+
"schematics": {
|
|
4
|
+
"crud": {
|
|
5
|
+
"description": "Generates a complete CRUD module with Material UI (list, form, service, model, routing).",
|
|
6
|
+
"factory": "./crud/index#crud",
|
|
7
|
+
"schema": "./crud/schema.json"
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<div class="page-container">
|
|
2
|
+
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<button mat-icon-button (click)="goBack()">
|
|
5
|
+
<mat-icon>arrow_back</mat-icon>
|
|
6
|
+
</button>
|
|
7
|
+
<h2>{{ isEditMode ? 'Edit' : 'Add' }} <%= classify(name) %></h2>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div *ngIf="isLoading" class="spinner-wrap">
|
|
11
|
+
<mat-spinner diameter="40"></mat-spinner>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<mat-card *ngIf="!isLoading">
|
|
15
|
+
<mat-card-content>
|
|
16
|
+
<form [formGroup]="form" (ngSubmit)="onSubmit()" novalidate>
|
|
17
|
+
|
|
18
|
+
<% fields.forEach(function(field) { %>
|
|
19
|
+
<% if (field.control === 'checkbox') { %>
|
|
20
|
+
<div class="field-row">
|
|
21
|
+
<mat-checkbox formControlName="<%= field.name %>"><%= field.label %></mat-checkbox>
|
|
22
|
+
</div>
|
|
23
|
+
<% } else { %>
|
|
24
|
+
<mat-form-field appearance="outline" class="full-width">
|
|
25
|
+
<mat-label><%= field.label %></mat-label>
|
|
26
|
+
<input matInput type="<%= field.control %>" formControlName="<%= field.name %>" />
|
|
27
|
+
<mat-error *ngIf="form.get('<%= field.name %>')?.invalid && form.get('<%= field.name %>')?.touched">
|
|
28
|
+
<% if (field.required) { %> <span *ngIf="form.get('<%= field.name %>')?.hasError('required')">
|
|
29
|
+
<%= field.label %> is required.
|
|
30
|
+
</span>
|
|
31
|
+
<% } %> </mat-error>
|
|
32
|
+
</mat-form-field>
|
|
33
|
+
<% } %>
|
|
34
|
+
<% }); %>
|
|
35
|
+
|
|
36
|
+
<div class="form-actions">
|
|
37
|
+
<button mat-button type="button" (click)="goBack()" [disabled]="isSaving">Cancel</button>
|
|
38
|
+
<button mat-raised-button color="primary" type="submit" [disabled]="isSaving || form.invalid">
|
|
39
|
+
<mat-spinner *ngIf="isSaving" diameter="18" class="inline-spinner"></mat-spinner>
|
|
40
|
+
{{ isSaving ? 'Saving...' : (isEditMode ? 'Update' : 'Save') }}
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
</form>
|
|
45
|
+
</mat-card-content>
|
|
46
|
+
</mat-card>
|
|
47
|
+
|
|
48
|
+
</div>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
.page-container {
|
|
2
|
+
padding: 24px;
|
|
3
|
+
max-width: 600px;
|
|
4
|
+
margin: 0 auto;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.page-header {
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: 8px;
|
|
11
|
+
margin-bottom: 20px;
|
|
12
|
+
|
|
13
|
+
h2 {
|
|
14
|
+
margin: 0;
|
|
15
|
+
font-size: 22px;
|
|
16
|
+
font-weight: 600;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.spinner-wrap {
|
|
21
|
+
display: flex;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
padding: 48px 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.full-width {
|
|
27
|
+
width: 100%;
|
|
28
|
+
margin-bottom: 4px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.field-row {
|
|
32
|
+
margin-bottom: 16px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.form-actions {
|
|
36
|
+
display: flex;
|
|
37
|
+
justify-content: flex-end;
|
|
38
|
+
gap: 8px;
|
|
39
|
+
margin-top: 8px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.inline-spinner {
|
|
43
|
+
display: inline-block;
|
|
44
|
+
margin-right: 6px;
|
|
45
|
+
vertical-align: middle;
|
|
46
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Component, OnInit } from '@angular/core';
|
|
2
|
+
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
|
3
|
+
import { ActivatedRoute, Router } from '@angular/router';
|
|
4
|
+
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
5
|
+
import { <%= classify(name) %> } from '../../models/<%= dasherize(name) %>.model';
|
|
6
|
+
import { <%= classify(name) %>Service } from '../../services/<%= dasherize(name) %>.service';
|
|
7
|
+
|
|
8
|
+
@Component({
|
|
9
|
+
selector: 'app-<%= dasherize(name) %>-form',
|
|
10
|
+
templateUrl: './<%= dasherize(name) %>-form.component.html',
|
|
11
|
+
styleUrls: ['./<%= dasherize(name) %>-form.component.scss'],
|
|
12
|
+
})
|
|
13
|
+
export class <%= classify(name) %>FormComponent implements OnInit {
|
|
14
|
+
form!: FormGroup;
|
|
15
|
+
isEditMode = false;
|
|
16
|
+
isLoading = false;
|
|
17
|
+
isSaving = false;
|
|
18
|
+
itemId: number | null = null;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
private fb: FormBuilder,
|
|
22
|
+
private service: <%= classify(name) %>Service,
|
|
23
|
+
private router: Router,
|
|
24
|
+
private route: ActivatedRoute,
|
|
25
|
+
private snackBar: MatSnackBar
|
|
26
|
+
) {}
|
|
27
|
+
|
|
28
|
+
ngOnInit(): void {
|
|
29
|
+
this.buildForm();
|
|
30
|
+
const id = this.route.snapshot.paramMap.get('id');
|
|
31
|
+
if (id) {
|
|
32
|
+
this.isEditMode = true;
|
|
33
|
+
this.itemId = +id;
|
|
34
|
+
this.loadItem(this.itemId);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
buildForm(): void {
|
|
39
|
+
this.form = this.fb.group({<% fields.forEach(function(field, i) { %>
|
|
40
|
+
<%= field.name %>: [<%= field.type === 'boolean' ? 'false' : field.type === 'number' ? 'null' : "''" %><% if (field.required) { %>, [Validators.required]<% } %>]<% if (i < fields.length - 1) { %>,<% } %><% }); %>
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
loadItem(id: number): void {
|
|
45
|
+
this.isLoading = true;
|
|
46
|
+
this.service.getById(id).subscribe({
|
|
47
|
+
next: (item) => {
|
|
48
|
+
this.form.patchValue(item);
|
|
49
|
+
this.isLoading = false;
|
|
50
|
+
},
|
|
51
|
+
error: (error) => {
|
|
52
|
+
let errorMessage = 'Failed to load item.';
|
|
53
|
+
if (error.status === 404) {
|
|
54
|
+
errorMessage = '<%= classify(name) %> not found.';
|
|
55
|
+
this.router.navigate(['/<%= dasherize(name) %>']);
|
|
56
|
+
} else if (error.status === 500) {
|
|
57
|
+
errorMessage = 'Server error. Please try again later.';
|
|
58
|
+
}
|
|
59
|
+
this.snackBar.open(errorMessage, 'Close', { duration: 4000 });
|
|
60
|
+
this.isLoading = false;
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
onSubmit(): void {
|
|
66
|
+
if (this.form.invalid) {
|
|
67
|
+
this.form.markAllAsTouched();
|
|
68
|
+
this.showValidationErrors();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.isSaving = true;
|
|
72
|
+
const value: <%= classify(name) %> = this.form.value;
|
|
73
|
+
const request = this.isEditMode
|
|
74
|
+
? this.service.update(this.itemId!, value)
|
|
75
|
+
: this.service.create(value);
|
|
76
|
+
|
|
77
|
+
request.subscribe({
|
|
78
|
+
next: () => {
|
|
79
|
+
this.snackBar.open(
|
|
80
|
+
`<%= classify(name) %> ${this.isEditMode ? 'updated' : 'created'} successfully.`,
|
|
81
|
+
'Close',
|
|
82
|
+
{ duration: 3000 }
|
|
83
|
+
);
|
|
84
|
+
this.isSaving = false;
|
|
85
|
+
this.goBack();
|
|
86
|
+
},
|
|
87
|
+
error: (error) => {
|
|
88
|
+
let errorMessage = 'Save failed. Please try again.';
|
|
89
|
+
if (error.status === 400 && error.error?.message) {
|
|
90
|
+
errorMessage = error.error.message;
|
|
91
|
+
} else if (error.status === 404) {
|
|
92
|
+
errorMessage = '<%= classify(name) %> not found.';
|
|
93
|
+
} else if (error.status === 500) {
|
|
94
|
+
errorMessage = 'Server error. Please try again later.';
|
|
95
|
+
}
|
|
96
|
+
this.snackBar.open(errorMessage, 'Close', { duration: 5000 });
|
|
97
|
+
this.isSaving = false;
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
private showValidationErrors(): void {
|
|
103
|
+
const errors: string[] = [];
|
|
104
|
+
Object.keys(this.form.controls).forEach(key => {
|
|
105
|
+
const control = this.form.get(key);
|
|
106
|
+
if (control?.invalid && control.errors) {
|
|
107
|
+
if (control.errors['required']) {
|
|
108
|
+
const fieldName = key.charAt(0).toUpperCase() + key.slice(1);
|
|
109
|
+
errors.push(`${fieldName} is required`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
if (errors.length > 0) {
|
|
114
|
+
this.snackBar.open(errors.join(', '), 'Close', { duration: 4000 });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
goBack(): void {
|
|
119
|
+
this.router.navigate(['..'], { relativeTo: this.route });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Component, Inject } from '@angular/core';
|
|
2
|
+
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
|
3
|
+
import { <%= classify(name) %> } from '../../models/<%= dasherize(name) %>.model';
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'app-<%= dasherize(name) %>-delete-dialog',
|
|
7
|
+
template: `
|
|
8
|
+
<h2 mat-dialog-title>Delete <%= classify(name) %></h2>
|
|
9
|
+
<mat-dialog-content>
|
|
10
|
+
Are you sure you want to delete this record? This cannot be undone.
|
|
11
|
+
</mat-dialog-content>
|
|
12
|
+
<mat-dialog-actions align="end">
|
|
13
|
+
<button mat-button (click)="close(false)">Cancel</button>
|
|
14
|
+
<button mat-raised-button color="warn" (click)="close(true)">Delete</button>
|
|
15
|
+
</mat-dialog-actions>
|
|
16
|
+
`,
|
|
17
|
+
})
|
|
18
|
+
export class <%= classify(name) %>DeleteDialogComponent {
|
|
19
|
+
constructor(
|
|
20
|
+
public dialogRef: MatDialogRef<<%= classify(name) %>DeleteDialogComponent>,
|
|
21
|
+
@Inject(MAT_DIALOG_DATA) public data: <%= classify(name) %>
|
|
22
|
+
) {}
|
|
23
|
+
|
|
24
|
+
close(result: boolean): void {
|
|
25
|
+
this.dialogRef.close(result);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<div class="page-container">
|
|
2
|
+
|
|
3
|
+
<div class="page-header">
|
|
4
|
+
<h2><%= classify(name) %> List</h2>
|
|
5
|
+
<button mat-raised-button color="primary" (click)="onCreate()">
|
|
6
|
+
<mat-icon>add</mat-icon> Add <%= classify(name) %>
|
|
7
|
+
</button>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<mat-card>
|
|
11
|
+
<mat-card-content>
|
|
12
|
+
|
|
13
|
+
<mat-form-field appearance="outline" class="search-box">
|
|
14
|
+
<mat-label>Search</mat-label>
|
|
15
|
+
<mat-icon matPrefix>search</mat-icon>
|
|
16
|
+
<input matInput (keyup)="applyFilter($event)" placeholder="Search..." />
|
|
17
|
+
</mat-form-field>
|
|
18
|
+
|
|
19
|
+
<div *ngIf="isLoading" class="spinner-wrap">
|
|
20
|
+
<mat-spinner diameter="40"></mat-spinner>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div [hidden]="isLoading">
|
|
24
|
+
<table mat-table [dataSource]="dataSource" matSort>
|
|
25
|
+
|
|
26
|
+
<ng-container matColumnDef="id">
|
|
27
|
+
<th mat-header-cell *matHeaderCellDef mat-sort-header>#</th>
|
|
28
|
+
<td mat-cell *matCellDef="let row">{{ row.id }}</td>
|
|
29
|
+
</ng-container>
|
|
30
|
+
|
|
31
|
+
<% fields.forEach(function(field) { %>
|
|
32
|
+
<ng-container matColumnDef="<%= field.name %>">
|
|
33
|
+
<th mat-header-cell *matHeaderCellDef mat-sort-header><%= field.label %></th>
|
|
34
|
+
<td mat-cell *matCellDef="let row">
|
|
35
|
+
<% if (field.type === 'boolean') { %>
|
|
36
|
+
<mat-chip [color]="row.<%= field.name %> ? 'primary' : ''" selected>
|
|
37
|
+
{{ row.<%= field.name %> ? 'Yes' : 'No' }}
|
|
38
|
+
</mat-chip>
|
|
39
|
+
<% } else { %>
|
|
40
|
+
{{ row.<%= field.name %> }}
|
|
41
|
+
<% } %>
|
|
42
|
+
</td>
|
|
43
|
+
</ng-container>
|
|
44
|
+
<% }); %>
|
|
45
|
+
|
|
46
|
+
<ng-container matColumnDef="actions">
|
|
47
|
+
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
|
48
|
+
<td mat-cell *matCellDef="let row">
|
|
49
|
+
<button mat-icon-button color="primary" (click)="onEdit(row)" matTooltip="Edit">
|
|
50
|
+
<mat-icon>edit</mat-icon>
|
|
51
|
+
</button>
|
|
52
|
+
<button mat-icon-button color="warn" (click)="onDelete(row)" matTooltip="Delete">
|
|
53
|
+
<mat-icon>delete</mat-icon>
|
|
54
|
+
</button>
|
|
55
|
+
</td>
|
|
56
|
+
</ng-container>
|
|
57
|
+
|
|
58
|
+
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
|
59
|
+
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
|
60
|
+
|
|
61
|
+
<tr class="mat-row" *matNoDataRow>
|
|
62
|
+
<td class="mat-cell no-data" [attr.colspan]="displayedColumns.length">
|
|
63
|
+
No records found.
|
|
64
|
+
</td>
|
|
65
|
+
</tr>
|
|
66
|
+
|
|
67
|
+
</table>
|
|
68
|
+
|
|
69
|
+
<mat-paginator [pageSizeOptions]="[5, 10, 25]" showFirstLastButtons></mat-paginator>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
</mat-card-content>
|
|
73
|
+
</mat-card>
|
|
74
|
+
|
|
75
|
+
</div>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
.page-container {
|
|
2
|
+
padding: 24px;
|
|
3
|
+
max-width: 1100px;
|
|
4
|
+
margin: 0 auto;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.page-header {
|
|
8
|
+
display: flex;
|
|
9
|
+
justify-content: space-between;
|
|
10
|
+
align-items: center;
|
|
11
|
+
margin-bottom: 16px;
|
|
12
|
+
|
|
13
|
+
h2 {
|
|
14
|
+
margin: 0;
|
|
15
|
+
font-size: 22px;
|
|
16
|
+
font-weight: 600;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.search-box {
|
|
21
|
+
width: 100%;
|
|
22
|
+
max-width: 360px;
|
|
23
|
+
margin-bottom: 16px;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.spinner-wrap {
|
|
27
|
+
display: flex;
|
|
28
|
+
justify-content: center;
|
|
29
|
+
padding: 48px 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
table {
|
|
33
|
+
width: 100%;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.no-data {
|
|
37
|
+
text-align: center;
|
|
38
|
+
padding: 32px;
|
|
39
|
+
color: #888;
|
|
40
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Component, OnInit, ViewChild } from '@angular/core';
|
|
2
|
+
import { MatPaginator } from '@angular/material/paginator';
|
|
3
|
+
import { MatSort } from '@angular/material/sort';
|
|
4
|
+
import { MatTableDataSource } from '@angular/material/table';
|
|
5
|
+
import { MatDialog } from '@angular/material/dialog';
|
|
6
|
+
import { MatSnackBar } from '@angular/material/snack-bar';
|
|
7
|
+
import { Router, ActivatedRoute } from '@angular/router';
|
|
8
|
+
import { <%= classify(name) %> } from '../../models/<%= dasherize(name) %>.model';
|
|
9
|
+
import { <%= classify(name) %>Service } from '../../services/<%= dasherize(name) %>.service';
|
|
10
|
+
import { <%= classify(name) %>DeleteDialogComponent } from './<%= dasherize(name) %>-delete-dialog.component';
|
|
11
|
+
|
|
12
|
+
@Component({
|
|
13
|
+
selector: 'app-<%= dasherize(name) %>-list',
|
|
14
|
+
templateUrl: './<%= dasherize(name) %>-list.component.html',
|
|
15
|
+
styleUrls: ['./<%= dasherize(name) %>-list.component.scss'],
|
|
16
|
+
})
|
|
17
|
+
export class <%= classify(name) %>ListComponent implements OnInit {
|
|
18
|
+
displayedColumns: string[] = ['id', <% fields.forEach(function(f, i) { %>'<%= f.name %>'<% if (i < fields.length - 1) { %>, <% } %><% }); %>, 'actions'];
|
|
19
|
+
dataSource = new MatTableDataSource<<%= classify(name) %>>();
|
|
20
|
+
isLoading = true;
|
|
21
|
+
|
|
22
|
+
@ViewChild(MatPaginator) paginator!: MatPaginator;
|
|
23
|
+
@ViewChild(MatSort) sort!: MatSort;
|
|
24
|
+
|
|
25
|
+
constructor(
|
|
26
|
+
private service: <%= classify(name) %>Service,
|
|
27
|
+
private router: Router,
|
|
28
|
+
private route: ActivatedRoute,
|
|
29
|
+
private dialog: MatDialog,
|
|
30
|
+
private snackBar: MatSnackBar
|
|
31
|
+
) {}
|
|
32
|
+
|
|
33
|
+
ngOnInit(): void {
|
|
34
|
+
this.loadData();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
loadData(): void {
|
|
38
|
+
this.isLoading = true;
|
|
39
|
+
this.service.getAll().subscribe({
|
|
40
|
+
next: (data) => {
|
|
41
|
+
this.dataSource.data = data;
|
|
42
|
+
this.dataSource.paginator = this.paginator;
|
|
43
|
+
this.dataSource.sort = this.sort;
|
|
44
|
+
this.isLoading = false;
|
|
45
|
+
},
|
|
46
|
+
error: (error) => {
|
|
47
|
+
let errorMessage = 'Failed to load data.';
|
|
48
|
+
if (error.status === 500) {
|
|
49
|
+
errorMessage = 'Server error. Please try again later.';
|
|
50
|
+
} else if (error.status === 0) {
|
|
51
|
+
errorMessage = 'Unable to connect to server. Please check your connection.';
|
|
52
|
+
}
|
|
53
|
+
this.snackBar.open(errorMessage, 'Close', { duration: 4000 });
|
|
54
|
+
this.isLoading = false;
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
applyFilter(event: Event): void {
|
|
60
|
+
const filterValue = (event.target as HTMLInputElement).value;
|
|
61
|
+
this.dataSource.filter = filterValue.trim().toLowerCase();
|
|
62
|
+
if (this.dataSource.paginator) this.dataSource.paginator.firstPage();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
onCreate(): void {
|
|
66
|
+
this.router.navigate(['new'], { relativeTo: this.route });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
onEdit(item: <%= classify(name) %>): void {
|
|
70
|
+
this.router.navigate([item.id, 'edit'], { relativeTo: this.route });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
onDelete(item: <%= classify(name) %>): void {
|
|
74
|
+
const ref = this.dialog.open(<%= classify(name) %>DeleteDialogComponent, { data: item, width: '400px' });
|
|
75
|
+
ref.afterClosed().subscribe((confirmed) => {
|
|
76
|
+
if (confirmed) {
|
|
77
|
+
this.service.delete(item.id!).subscribe({
|
|
78
|
+
next: () => {
|
|
79
|
+
this.snackBar.open('<%= classify(name) %> deleted successfully.', 'Close', { duration: 3000 });
|
|
80
|
+
this.loadData();
|
|
81
|
+
},
|
|
82
|
+
error: (error) => {
|
|
83
|
+
let errorMessage = 'Delete failed.';
|
|
84
|
+
if (error.status === 404) {
|
|
85
|
+
errorMessage = '<%= classify(name) %> not found or already deleted.';
|
|
86
|
+
} else if (error.status === 500) {
|
|
87
|
+
errorMessage = 'Server error. Please try again later.';
|
|
88
|
+
}
|
|
89
|
+
this.snackBar.open(errorMessage, 'Close', { duration: 4000 });
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { NgModule } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { ReactiveFormsModule } from '@angular/forms';
|
|
4
|
+
import { HttpClientModule } from '@angular/common/http';
|
|
5
|
+
import { RouterModule, Routes } from '@angular/router';
|
|
6
|
+
import { MatTableModule } from '@angular/material/table';
|
|
7
|
+
import { MatPaginatorModule } from '@angular/material/paginator';
|
|
8
|
+
import { MatSortModule } from '@angular/material/sort';
|
|
9
|
+
import { MatInputModule } from '@angular/material/input';
|
|
10
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
11
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
12
|
+
import { MatDialogModule } from '@angular/material/dialog';
|
|
13
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
14
|
+
import { MatCheckboxModule } from '@angular/material/checkbox';
|
|
15
|
+
import { MatSnackBarModule } from '@angular/material/snack-bar';
|
|
16
|
+
import { MatCardModule } from '@angular/material/card';
|
|
17
|
+
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
18
|
+
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
19
|
+
import { MatChipsModule } from '@angular/material/chips';
|
|
20
|
+
import { <%= classify(name) %>ListComponent } from './<%= dasherize(name) %>-list.component';
|
|
21
|
+
import { <%= classify(name) %>DeleteDialogComponent } from './<%= dasherize(name) %>-delete-dialog.component';
|
|
22
|
+
import { <%= classify(name) %>FormComponent } from '../<%= dasherize(name) %>-form/<%= dasherize(name) %>-form.component';
|
|
23
|
+
|
|
24
|
+
const routes: Routes = [
|
|
25
|
+
{
|
|
26
|
+
path: '',
|
|
27
|
+
component: <%= classify(name) %>ListComponent,
|
|
28
|
+
title: '<%= classify(name) %> List'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
path: 'new',
|
|
32
|
+
component: <%= classify(name) %>FormComponent,
|
|
33
|
+
title: 'New <%= classify(name) %>'
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
path: ':id/edit',
|
|
37
|
+
component: <%= classify(name) %>FormComponent,
|
|
38
|
+
title: 'Edit <%= classify(name) %>'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
path: '**',
|
|
42
|
+
redirectTo: ''
|
|
43
|
+
}
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
@NgModule({
|
|
47
|
+
declarations: [
|
|
48
|
+
<%= classify(name) %>ListComponent,
|
|
49
|
+
<%= classify(name) %>FormComponent,
|
|
50
|
+
<%= classify(name) %>DeleteDialogComponent,
|
|
51
|
+
],
|
|
52
|
+
imports: [
|
|
53
|
+
CommonModule,
|
|
54
|
+
ReactiveFormsModule,
|
|
55
|
+
HttpClientModule,
|
|
56
|
+
RouterModule.forChild(routes),
|
|
57
|
+
MatTableModule,
|
|
58
|
+
MatPaginatorModule,
|
|
59
|
+
MatSortModule,
|
|
60
|
+
MatInputModule,
|
|
61
|
+
MatButtonModule,
|
|
62
|
+
MatIconModule,
|
|
63
|
+
MatDialogModule,
|
|
64
|
+
MatFormFieldModule,
|
|
65
|
+
MatCheckboxModule,
|
|
66
|
+
MatSnackBarModule,
|
|
67
|
+
MatCardModule,
|
|
68
|
+
MatProgressSpinnerModule,
|
|
69
|
+
MatTooltipModule,
|
|
70
|
+
MatChipsModule,
|
|
71
|
+
],
|
|
72
|
+
})
|
|
73
|
+
export class <%= classify(name) %>Module {}
|