@devmed555/angular-clean-architecture-cli 0.0.1 → 0.0.3
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 +370 -128
- package/bin/index.js +101 -3
- package/generators.json +19 -9
- package/package.json +2 -4
- package/src/generators/core/files/auth/auth.guard.ts.template +14 -0
- package/src/generators/core/files/auth/auth.service.ts.template +137 -0
- package/src/generators/core/files/auth/login.component.ts.template +165 -0
- package/src/generators/core/files/auth/signup.component.ts.template +171 -0
- package/src/generators/core/files/guard/__name__.guard.ts.template +6 -0
- package/src/generators/core/files/interceptor/__name__.interceptor.ts.template +6 -0
- package/src/generators/core/files/language-selector/language-selector.component.ts.template +82 -0
- package/src/generators/core/files/menu/__name__.component.html.template +14 -0
- package/src/generators/core/files/menu/__name__.component.scss.template +66 -0
- package/src/generators/core/files/menu/__name__.component.ts.template +24 -0
- package/src/generators/core/files/navbar/__name__.component.html.template +45 -0
- package/src/generators/core/files/navbar/__name__.component.scss.template +134 -0
- package/src/generators/core/files/navbar/__name__.component.ts.template +38 -0
- package/src/generators/core/files/service/__name__.service.ts.template +26 -0
- package/src/generators/core/files/theme-selector/theme-selector.component.ts.template +49 -0
- package/src/generators/core/files/translate/translate.pipe.ts.template +15 -0
- package/src/generators/core/files/translate/translate.service.ts.template +24 -0
- package/src/generators/core/generator.d.ts +7 -0
- package/src/generators/core/generator.js +49 -0
- package/src/generators/core/generator.js.map +1 -0
- package/src/generators/core/schema.json +34 -0
- package/src/generators/feature/blueprint.schema.json +34 -0
- package/src/generators/feature/files/application/store.ts.template +135 -0
- package/src/generators/feature/files/domain/model.ts.template +9 -0
- package/src/generators/{clean-feature → feature}/files/infrastructure/service.ts.template +5 -5
- package/src/generators/feature/files/ui/__singularName__.component.html.template +109 -0
- package/src/generators/feature/files/ui/__singularName__.component.scss.template +162 -0
- package/src/generators/feature/files/ui/__singularName__.component.ts.template +131 -0
- package/src/generators/feature/files/ui/_theme.scss.template +35 -0
- package/src/generators/feature/files/ui/form/form.component.ts.template +122 -0
- package/src/generators/feature/generator.d.ts +4 -0
- package/src/generators/feature/generator.js +209 -0
- package/src/generators/feature/generator.js.map +1 -0
- package/src/generators/feature/schema.d.ts +5 -0
- package/src/generators/{clean-feature → feature}/schema.json +25 -23
- package/src/generators/shared/files/ui/__name__.component.ts.template +57 -0
- package/src/generators/shared/files/util/__name__.ts.template +7 -0
- package/src/generators/shared/generator.d.ts +7 -0
- package/src/generators/shared/generator.js +31 -0
- package/src/generators/shared/generator.js.map +1 -0
- package/src/generators/shared/schema.json +23 -0
- package/src/index.js +1 -0
- package/src/utils/string-utils.d.ts +16 -0
- package/src/utils/string-utils.js +33 -0
- package/src/utils/string-utils.js.map +1 -0
- package/src/generators/clean-feature/files/application/store.ts.template +0 -6
- package/src/generators/clean-feature/files/domain/model.ts.template +0 -4
- package/src/generators/clean-feature/files/ui/component.ts.template +0 -44
- package/src/generators/clean-feature/generator.d.ts +0 -4
- package/src/generators/clean-feature/generator.js +0 -52
- package/src/generators/clean-feature/generator.js.map +0 -1
- package/src/generators/clean-feature/schema.d.ts +0 -4
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<div class="<%= singularName %>-container">
|
|
2
|
+
<!-- Use @let for computed template values -->
|
|
3
|
+
@let isLoading = store.loading();
|
|
4
|
+
@let hasError = store.error();
|
|
5
|
+
@let items = store.filteredAndSortedItems();
|
|
6
|
+
@let isEmpty = items.length === 0;
|
|
7
|
+
|
|
8
|
+
<!-- Header -->
|
|
9
|
+
<div class="header">
|
|
10
|
+
<h1><%= pascalName %> Management</h1>
|
|
11
|
+
<button
|
|
12
|
+
mat-raised-button
|
|
13
|
+
color="primary"
|
|
14
|
+
(click)="openCreateDialog()"
|
|
15
|
+
[disabled]="isLoading">
|
|
16
|
+
<mat-icon>add</mat-icon>
|
|
17
|
+
Create <%= pascalName %>
|
|
18
|
+
</button>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<!-- Filter -->
|
|
22
|
+
<mat-form-field class="filter-field">
|
|
23
|
+
<mat-label>Filter</mat-label>
|
|
24
|
+
<input
|
|
25
|
+
matInput
|
|
26
|
+
[ngModel]="store.filter()"
|
|
27
|
+
(ngModelChange)="onFilterChange($event)"
|
|
28
|
+
placeholder="Search..." />
|
|
29
|
+
<mat-icon matSuffix>search</mat-icon>
|
|
30
|
+
</mat-form-field>
|
|
31
|
+
|
|
32
|
+
<!-- Loading Spinner -->
|
|
33
|
+
@if (isLoading) {
|
|
34
|
+
<div class="loading-container">
|
|
35
|
+
<mat-spinner diameter="50"></mat-spinner>
|
|
36
|
+
</div>
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
<!-- Error Message -->
|
|
40
|
+
@if (hasError) {
|
|
41
|
+
<div class="error-message">
|
|
42
|
+
<mat-icon>error</mat-icon>
|
|
43
|
+
<span>{{ hasError }}</span>
|
|
44
|
+
</div>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
<!-- Data Table -->
|
|
48
|
+
@if (!isLoading && !hasError) {
|
|
49
|
+
<div class="table-container">
|
|
50
|
+
<table mat-table [dataSource]="items" class="mat-elevation-z2">
|
|
51
|
+
<% attributes.forEach(function(attr) { %>
|
|
52
|
+
<!-- <%= attr.name.charAt(0).toUpperCase() + attr.name.slice(1) %> Column -->
|
|
53
|
+
<ng-container matColumnDef="<%= attr.name %>">
|
|
54
|
+
<th mat-header-cell *matHeaderCellDef><%= attr.name.charAt(0).toUpperCase() + attr.name.slice(1) %></th>
|
|
55
|
+
<td mat-cell *matCellDef="let item">
|
|
56
|
+
<% if (attr.type === 'boolean') { %>
|
|
57
|
+
<mat-icon [class.active]="item.<%= attr.name %>">
|
|
58
|
+
{{ item.<%= attr.name %> ? 'check_circle' : 'cancel' }}
|
|
59
|
+
</mat-icon>
|
|
60
|
+
<% } else if (attr.type === 'number') { %>
|
|
61
|
+
{{ item.<%= attr.name %> | number }}
|
|
62
|
+
<% } else if (attr.type === 'Date') { %>
|
|
63
|
+
{{ item.<%= attr.name %> | date:'short' }}
|
|
64
|
+
<% } else { %>
|
|
65
|
+
{{ item.<%= attr.name %> }}
|
|
66
|
+
<% } %>
|
|
67
|
+
</td>
|
|
68
|
+
</ng-container>
|
|
69
|
+
<% }); %>
|
|
70
|
+
|
|
71
|
+
<!-- Actions Column -->
|
|
72
|
+
<ng-container matColumnDef="actions">
|
|
73
|
+
<th mat-header-cell *matHeaderCellDef>Actions</th>
|
|
74
|
+
<td mat-cell *matCellDef="let item">
|
|
75
|
+
<button
|
|
76
|
+
mat-icon-button
|
|
77
|
+
color="primary"
|
|
78
|
+
(click)="openEditDialog(item)"
|
|
79
|
+
matTooltip="Edit">
|
|
80
|
+
<mat-icon>edit</mat-icon>
|
|
81
|
+
</button>
|
|
82
|
+
<button
|
|
83
|
+
mat-icon-button
|
|
84
|
+
color="warn"
|
|
85
|
+
(click)="confirmDelete(item)"
|
|
86
|
+
matTooltip="Delete">
|
|
87
|
+
<mat-icon>delete</mat-icon>
|
|
88
|
+
</button>
|
|
89
|
+
</td>
|
|
90
|
+
</ng-container>
|
|
91
|
+
|
|
92
|
+
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
|
93
|
+
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
|
94
|
+
|
|
95
|
+
<!-- No Data Row -->
|
|
96
|
+
@if (isEmpty) {
|
|
97
|
+
<tr class="mat-row no-data-row">
|
|
98
|
+
<td class="mat-cell" [attr.colspan]="displayedColumns.length">
|
|
99
|
+
<div class="no-data">
|
|
100
|
+
<mat-icon>inbox</mat-icon>
|
|
101
|
+
<p>No <%= singularName %>s found</p>
|
|
102
|
+
</div>
|
|
103
|
+
</td>
|
|
104
|
+
</tr>
|
|
105
|
+
}
|
|
106
|
+
</table>
|
|
107
|
+
</div>
|
|
108
|
+
}
|
|
109
|
+
</div>
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
@use './_theme' as theme;
|
|
2
|
+
|
|
3
|
+
.<%= singularName %>-container {
|
|
4
|
+
padding: theme.$spacing-lg;
|
|
5
|
+
max-width: 1200px;
|
|
6
|
+
margin: 0 auto;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.header {
|
|
10
|
+
display: flex;
|
|
11
|
+
justify-content: space-between;
|
|
12
|
+
align-items: center;
|
|
13
|
+
margin-bottom: theme.$spacing-xl;
|
|
14
|
+
padding-bottom: theme.$spacing-sm;
|
|
15
|
+
border-bottom: 1px solid var(--color-border);
|
|
16
|
+
|
|
17
|
+
h1 {
|
|
18
|
+
margin: 0;
|
|
19
|
+
font-size: 2.5rem;
|
|
20
|
+
font-weight: 700;
|
|
21
|
+
letter-spacing: -0.5px;
|
|
22
|
+
|
|
23
|
+
// Gradient text effect
|
|
24
|
+
background: linear-gradient(135deg, var(--color-primary), var(--color-accent));
|
|
25
|
+
-webkit-background-clip: text;
|
|
26
|
+
-webkit-text-fill-color: transparent;
|
|
27
|
+
background-clip: text;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
button {
|
|
31
|
+
height: 48px;
|
|
32
|
+
padding: 0 24px;
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
border-radius: 24px; // Pill shape
|
|
35
|
+
box-shadow: var(--shadow-md);
|
|
36
|
+
transition: all 0.3s ease;
|
|
37
|
+
|
|
38
|
+
&:hover {
|
|
39
|
+
transform: translateY(-2px);
|
|
40
|
+
box-shadow: var(--shadow-lg);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
mat-icon {
|
|
44
|
+
margin-right: theme.$spacing-sm;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.filter-field {
|
|
50
|
+
width: 100%;
|
|
51
|
+
margin-bottom: theme.$spacing-md;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
.loading-container {
|
|
55
|
+
display: flex;
|
|
56
|
+
justify-content: center;
|
|
57
|
+
align-items: center;
|
|
58
|
+
padding: theme.$spacing-xl;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.error-message {
|
|
62
|
+
display: flex;
|
|
63
|
+
align-items: center;
|
|
64
|
+
gap: theme.$spacing-sm;
|
|
65
|
+
padding: theme.$spacing-md;
|
|
66
|
+
background-color: theme.$error-background;
|
|
67
|
+
color: theme.$error-color;
|
|
68
|
+
border-radius: theme.$border-radius;
|
|
69
|
+
margin-bottom: theme.$spacing-md;
|
|
70
|
+
|
|
71
|
+
mat-icon {
|
|
72
|
+
color: theme.$error-color;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.table-container {
|
|
77
|
+
overflow-x: auto;
|
|
78
|
+
border-radius: theme.$border-radius;
|
|
79
|
+
|
|
80
|
+
table {
|
|
81
|
+
width: 100%;
|
|
82
|
+
background-color: theme.$card-background;
|
|
83
|
+
|
|
84
|
+
th {
|
|
85
|
+
background-color: theme.$table-header-background;
|
|
86
|
+
color: theme.$text-primary;
|
|
87
|
+
font-weight: 600;
|
|
88
|
+
padding: theme.$spacing-md;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
td {
|
|
92
|
+
padding: theme.$spacing-md;
|
|
93
|
+
color: theme.$text-secondary;
|
|
94
|
+
|
|
95
|
+
mat-icon {
|
|
96
|
+
&.active {
|
|
97
|
+
color: theme.$success-color;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
&:not(.active) {
|
|
101
|
+
color: theme.$text-disabled;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
tr {
|
|
107
|
+
&:hover {
|
|
108
|
+
background-color: theme.$table-row-hover;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.no-data-row {
|
|
115
|
+
height: 200px;
|
|
116
|
+
|
|
117
|
+
.no-data {
|
|
118
|
+
display: flex;
|
|
119
|
+
flex-direction: column;
|
|
120
|
+
align-items: center;
|
|
121
|
+
justify-content: center;
|
|
122
|
+
gap: theme.$spacing-sm;
|
|
123
|
+
color: theme.$text-disabled;
|
|
124
|
+
|
|
125
|
+
mat-icon {
|
|
126
|
+
font-size: 48px;
|
|
127
|
+
width: 48px;
|
|
128
|
+
height: 48px;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
p {
|
|
132
|
+
margin: 0;
|
|
133
|
+
font-size: 1rem;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Responsive
|
|
139
|
+
@media (max-width: 768px) {
|
|
140
|
+
.<%= singularName %>-container {
|
|
141
|
+
padding: theme.$spacing-md;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.header {
|
|
145
|
+
flex-direction: column;
|
|
146
|
+
align-items: flex-start;
|
|
147
|
+
gap: theme.$spacing-md;
|
|
148
|
+
|
|
149
|
+
button {
|
|
150
|
+
width: 100%;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.table-container {
|
|
155
|
+
table {
|
|
156
|
+
th, td {
|
|
157
|
+
padding: theme.$spacing-sm;
|
|
158
|
+
font-size: 0.875rem;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
|
|
2
|
+
import { FormsModule } from '@angular/forms';
|
|
3
|
+
import { DatePipe, DecimalPipe } from '@angular/common';
|
|
4
|
+
import { MatTableModule } from '@angular/material/table';
|
|
5
|
+
import { MatSortModule } from '@angular/material/sort';
|
|
6
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
7
|
+
import { MatIconModule } from '@angular/material/icon';
|
|
8
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
9
|
+
import { MatInputModule } from '@angular/material/input';
|
|
10
|
+
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
|
11
|
+
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
12
|
+
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
|
13
|
+
import { MatTooltipModule } from '@angular/material/tooltip';
|
|
14
|
+
import { <%= pascalName %>Store } from '../application/store';
|
|
15
|
+
import { <%= pascalName %>FormComponent } from './form/form.component';
|
|
16
|
+
import { <%= pascalName %> } from '../domain/model';
|
|
17
|
+
import { ConfirmDialogComponent } from '../../../shared/ui/confirm-dialog/confirm-dialog.component';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* <%= pascalName %> feature component - CRUD operations with Angular Material
|
|
21
|
+
*
|
|
22
|
+
* Angular 21 Features Used:
|
|
23
|
+
* - @let template syntax for local variables
|
|
24
|
+
* - Signals via NgRx Signal Store
|
|
25
|
+
* - ChangeDetectionStrategy.OnPush for zoneless compatibility
|
|
26
|
+
* - inject() function for dependency injection
|
|
27
|
+
*/
|
|
28
|
+
@Component({
|
|
29
|
+
selector: 'app-<%= singularName %>-feature',
|
|
30
|
+
imports: [
|
|
31
|
+
FormsModule,
|
|
32
|
+
DatePipe,
|
|
33
|
+
DecimalPipe,
|
|
34
|
+
MatTableModule,
|
|
35
|
+
MatSortModule,
|
|
36
|
+
MatButtonModule,
|
|
37
|
+
MatIconModule,
|
|
38
|
+
MatFormFieldModule,
|
|
39
|
+
MatInputModule,
|
|
40
|
+
MatDialogModule,
|
|
41
|
+
MatProgressSpinnerModule,
|
|
42
|
+
MatSnackBarModule,
|
|
43
|
+
MatTooltipModule
|
|
44
|
+
],
|
|
45
|
+
templateUrl: './<%= singularName %>.component.html',
|
|
46
|
+
styleUrls: ['./<%= singularName %>.component.scss'],
|
|
47
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
48
|
+
})
|
|
49
|
+
export class <%= pascalName %>Component {
|
|
50
|
+
// Dependency injection using inject() - Angular 14+ pattern
|
|
51
|
+
protected readonly store = inject(<%= pascalName %>Store);
|
|
52
|
+
private readonly dialog = inject(MatDialog);
|
|
53
|
+
private readonly snackBar = inject(MatSnackBar);
|
|
54
|
+
|
|
55
|
+
// Table columns configuration
|
|
56
|
+
protected readonly displayedColumns: string[] = [<% attributes.forEach(function(attr, index) { %>'<%= attr.name %>', <% }); %>'actions'];
|
|
57
|
+
|
|
58
|
+
// Load data on component init using constructor for zoneless compatibility
|
|
59
|
+
constructor() {
|
|
60
|
+
// Defer loading to allow store initialization
|
|
61
|
+
queueMicrotask(() => this.store.loadAll());
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onFilterChange(value: string): void {
|
|
65
|
+
this.store.setFilter(value);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
onSortChange(field: keyof <%= pascalName %>, direction: 'asc' | 'desc'): void {
|
|
69
|
+
this.store.setSort(field, direction);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
openCreateDialog(): void {
|
|
73
|
+
const dialogRef = this.dialog.open(<%= pascalName %>FormComponent, {
|
|
74
|
+
width: '500px',
|
|
75
|
+
maxWidth: '90vw',
|
|
76
|
+
data: null,
|
|
77
|
+
panelClass: 'themed-dialog'
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
dialogRef.afterClosed().subscribe((result) => {
|
|
81
|
+
if (result) {
|
|
82
|
+
this.store.create(result);
|
|
83
|
+
this.showNotification('<%= pascalName %> created successfully');
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
openEditDialog(item: <%= pascalName %>): void {
|
|
89
|
+
const dialogRef = this.dialog.open(<%= pascalName %>FormComponent, {
|
|
90
|
+
width: '500px',
|
|
91
|
+
maxWidth: '90vw',
|
|
92
|
+
data: item,
|
|
93
|
+
panelClass: 'themed-dialog'
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
dialogRef.afterClosed().subscribe((result) => {
|
|
97
|
+
if (result) {
|
|
98
|
+
this.store.update({ id: item.id, data: result });
|
|
99
|
+
this.showNotification('<%= pascalName %> updated successfully');
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
confirmDelete(item: <%= pascalName %>): void {
|
|
105
|
+
const dialogRef = this.dialog.open(ConfirmDialogComponent, {
|
|
106
|
+
width: '400px',
|
|
107
|
+
maxWidth: '90vw',
|
|
108
|
+
data: {
|
|
109
|
+
title: 'Delete <%= singularName %>',
|
|
110
|
+
message: `Are you sure you want to delete this <%= singularName %>?`
|
|
111
|
+
},
|
|
112
|
+
panelClass: 'themed-dialog'
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
dialogRef.afterClosed().subscribe((confirmed) => {
|
|
116
|
+
if (confirmed) {
|
|
117
|
+
this.store.delete(item.id);
|
|
118
|
+
this.showNotification('<%= pascalName %> deleted successfully');
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Helper method for snackbar notifications
|
|
124
|
+
private showNotification(message: string): void {
|
|
125
|
+
this.snackBar.open(message, 'Close', {
|
|
126
|
+
duration: 3000,
|
|
127
|
+
horizontalPosition: 'end',
|
|
128
|
+
verticalPosition: 'bottom'
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Theme Variables - Customize these to brand your application
|
|
2
|
+
// Colors
|
|
3
|
+
$primary-color: var(--color-primary) !default;
|
|
4
|
+
$accent-color: var(--color-accent) !default;
|
|
5
|
+
$warn-color: var(--color-error) !default;
|
|
6
|
+
$success-color: var(--color-success) !default;
|
|
7
|
+
$error-color: var(--color-error) !default;
|
|
8
|
+
|
|
9
|
+
// Backgrounds
|
|
10
|
+
$background-color: var(--color-bg) !default;
|
|
11
|
+
$card-background: var(--color-bg-card) !default;
|
|
12
|
+
$table-header-background: var(--color-bg-elevated) !default;
|
|
13
|
+
$table-row-hover: rgba(0, 0, 0, 0.04) !default;
|
|
14
|
+
$error-background: rgba(244, 67, 54, 0.1) !default;
|
|
15
|
+
|
|
16
|
+
// Text
|
|
17
|
+
$text-primary: var(--color-text) !default;
|
|
18
|
+
$text-secondary: var(--color-text-secondary) !default;
|
|
19
|
+
$text-disabled: var(--color-text-disabled) !default;
|
|
20
|
+
|
|
21
|
+
// Spacing
|
|
22
|
+
$spacing-xs: 0.25rem !default;
|
|
23
|
+
$spacing-sm: 0.5rem !default;
|
|
24
|
+
$spacing-md: 1rem !default;
|
|
25
|
+
$spacing-lg: 1.5rem !default;
|
|
26
|
+
$spacing-xl: 2rem !default;
|
|
27
|
+
|
|
28
|
+
// Border
|
|
29
|
+
$border-radius: 4px !default;
|
|
30
|
+
$border-color: var(--color-border) !default;
|
|
31
|
+
|
|
32
|
+
// Shadows
|
|
33
|
+
$shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1) !default;
|
|
34
|
+
$shadow-md: 0 4px 8px rgba(0, 0, 0, 0.15) !default;
|
|
35
|
+
$shadow-lg: 0 8px 16px rgba(0, 0, 0, 0.2) !default;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Component, inject, ChangeDetectionStrategy, signal, computed } from '@angular/core';
|
|
2
|
+
import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';
|
|
3
|
+
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
|
4
|
+
import { MatFormFieldModule } from '@angular/material/form-field';
|
|
5
|
+
import { MatInputModule } from '@angular/material/input';
|
|
6
|
+
import { MatButtonModule } from '@angular/material/button';
|
|
7
|
+
import { MatCheckboxModule } from '@angular/material/checkbox';
|
|
8
|
+
import { <%= pascalName %> } from '../../domain/model';
|
|
9
|
+
|
|
10
|
+
@Component({
|
|
11
|
+
selector: 'app-<%= singularName %>-form',
|
|
12
|
+
imports: [
|
|
13
|
+
ReactiveFormsModule,
|
|
14
|
+
MatDialogModule,
|
|
15
|
+
MatFormFieldModule,
|
|
16
|
+
MatInputModule,
|
|
17
|
+
MatButtonModule,
|
|
18
|
+
MatCheckboxModule
|
|
19
|
+
],
|
|
20
|
+
template: `
|
|
21
|
+
@let isEditMode = !!data;
|
|
22
|
+
@let title = isEditMode ? 'Edit' : 'Create';
|
|
23
|
+
|
|
24
|
+
<h2 mat-dialog-title>{{ title }} <%= pascalName %></h2>
|
|
25
|
+
|
|
26
|
+
<mat-dialog-content>
|
|
27
|
+
<form [formGroup]="form" class="form-container">
|
|
28
|
+
<% attributes.forEach(function(attr) { %>
|
|
29
|
+
<% if (attr.type === 'boolean') { %>
|
|
30
|
+
<mat-checkbox formControlName="<%= attr.name %>" class="form-field">
|
|
31
|
+
<%= attr.name.charAt(0).toUpperCase() + attr.name.slice(1) %>
|
|
32
|
+
</mat-checkbox>
|
|
33
|
+
<% } else if (attr.type === 'number') { %>
|
|
34
|
+
<mat-form-field class="form-field" appearance="outline">
|
|
35
|
+
<mat-label><%= attr.name.charAt(0).toUpperCase() + attr.name.slice(1) %></mat-label>
|
|
36
|
+
<input matInput type="number" formControlName="<%= attr.name %>" />
|
|
37
|
+
@if (form.get('<%= attr.name %>')?.hasError('required')) {
|
|
38
|
+
<mat-error>This field is required</mat-error>
|
|
39
|
+
}
|
|
40
|
+
</mat-form-field>
|
|
41
|
+
<% } else { %>
|
|
42
|
+
<mat-form-field class="form-field" appearance="outline">
|
|
43
|
+
<mat-label><%= attr.name.charAt(0).toUpperCase() + attr.name.slice(1) %></mat-label>
|
|
44
|
+
<input matInput formControlName="<%= attr.name %>" />
|
|
45
|
+
@if (form.get('<%= attr.name %>')?.hasError('required')) {
|
|
46
|
+
<mat-error>This field is required</mat-error>
|
|
47
|
+
}
|
|
48
|
+
</mat-form-field>
|
|
49
|
+
<% } %>
|
|
50
|
+
<% }); %>
|
|
51
|
+
</form>
|
|
52
|
+
</mat-dialog-content>
|
|
53
|
+
|
|
54
|
+
<mat-dialog-actions align="end">
|
|
55
|
+
<button mat-button (click)="onCancel()">Cancel</button>
|
|
56
|
+
<button
|
|
57
|
+
mat-raised-button
|
|
58
|
+
color="primary"
|
|
59
|
+
(click)="onSave()"
|
|
60
|
+
[disabled]="isFormInvalid()">
|
|
61
|
+
{{ title }}
|
|
62
|
+
</button>
|
|
63
|
+
</mat-dialog-actions>
|
|
64
|
+
`,
|
|
65
|
+
styles: `
|
|
66
|
+
.form-container {
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
gap: 1rem;
|
|
70
|
+
min-width: 400px;
|
|
71
|
+
padding: 1rem 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.form-field {
|
|
75
|
+
width: 100%;
|
|
76
|
+
}
|
|
77
|
+
`,
|
|
78
|
+
changeDetection: ChangeDetectionStrategy.OnPush
|
|
79
|
+
})
|
|
80
|
+
export class <%= pascalName %>FormComponent {
|
|
81
|
+
private readonly dialogRef = inject(MatDialogRef<<%= pascalName %>FormComponent>);
|
|
82
|
+
protected readonly data = inject<Partial<<%= pascalName %>> | null>(MAT_DIALOG_DATA, { optional: true });
|
|
83
|
+
private readonly fb = inject(FormBuilder);
|
|
84
|
+
|
|
85
|
+
// Signal-based form state tracking
|
|
86
|
+
private readonly formDirty = signal(false);
|
|
87
|
+
private readonly formValid = signal(false);
|
|
88
|
+
|
|
89
|
+
readonly form = this.fb.group({
|
|
90
|
+
<% attributes.forEach(function(attr, index) { %>
|
|
91
|
+
<%= attr.name %>: [
|
|
92
|
+
this.data?.<%= attr.name %> ?? <% if (attr.type === 'boolean') { %>false<% } else if (attr.type === 'number') { %>0<% } else { %>''<% } %>,
|
|
93
|
+
<% if (attr.type === 'number') { %>[Validators.required, Validators.min(0)]<% } else { %>Validators.required<% } %>
|
|
94
|
+
]<% if (index < attributes.length - 1) { %>,<% } %>
|
|
95
|
+
<% }); %>
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Computed signal for form validation state
|
|
99
|
+
readonly isFormInvalid = computed(() => !this.formValid() || !this.formDirty());
|
|
100
|
+
|
|
101
|
+
constructor() {
|
|
102
|
+
// Track form state changes with signals for zoneless compatibility
|
|
103
|
+
this.form.statusChanges.subscribe(() => {
|
|
104
|
+
this.formValid.set(this.form.valid);
|
|
105
|
+
});
|
|
106
|
+
this.form.valueChanges.subscribe(() => {
|
|
107
|
+
this.formDirty.set(this.form.dirty);
|
|
108
|
+
});
|
|
109
|
+
// Initial state
|
|
110
|
+
this.formValid.set(this.form.valid);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
onCancel(): void {
|
|
114
|
+
this.dialogRef.close();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
onSave(): void {
|
|
118
|
+
if (this.form.valid) {
|
|
119
|
+
this.dialogRef.close(this.form.value);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|