@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.
Files changed (56) hide show
  1. package/README.md +370 -128
  2. package/bin/index.js +101 -3
  3. package/generators.json +19 -9
  4. package/package.json +2 -4
  5. package/src/generators/core/files/auth/auth.guard.ts.template +14 -0
  6. package/src/generators/core/files/auth/auth.service.ts.template +137 -0
  7. package/src/generators/core/files/auth/login.component.ts.template +165 -0
  8. package/src/generators/core/files/auth/signup.component.ts.template +171 -0
  9. package/src/generators/core/files/guard/__name__.guard.ts.template +6 -0
  10. package/src/generators/core/files/interceptor/__name__.interceptor.ts.template +6 -0
  11. package/src/generators/core/files/language-selector/language-selector.component.ts.template +82 -0
  12. package/src/generators/core/files/menu/__name__.component.html.template +14 -0
  13. package/src/generators/core/files/menu/__name__.component.scss.template +66 -0
  14. package/src/generators/core/files/menu/__name__.component.ts.template +24 -0
  15. package/src/generators/core/files/navbar/__name__.component.html.template +45 -0
  16. package/src/generators/core/files/navbar/__name__.component.scss.template +134 -0
  17. package/src/generators/core/files/navbar/__name__.component.ts.template +38 -0
  18. package/src/generators/core/files/service/__name__.service.ts.template +26 -0
  19. package/src/generators/core/files/theme-selector/theme-selector.component.ts.template +49 -0
  20. package/src/generators/core/files/translate/translate.pipe.ts.template +15 -0
  21. package/src/generators/core/files/translate/translate.service.ts.template +24 -0
  22. package/src/generators/core/generator.d.ts +7 -0
  23. package/src/generators/core/generator.js +49 -0
  24. package/src/generators/core/generator.js.map +1 -0
  25. package/src/generators/core/schema.json +34 -0
  26. package/src/generators/feature/blueprint.schema.json +34 -0
  27. package/src/generators/feature/files/application/store.ts.template +135 -0
  28. package/src/generators/feature/files/domain/model.ts.template +9 -0
  29. package/src/generators/{clean-feature → feature}/files/infrastructure/service.ts.template +5 -5
  30. package/src/generators/feature/files/ui/__singularName__.component.html.template +109 -0
  31. package/src/generators/feature/files/ui/__singularName__.component.scss.template +162 -0
  32. package/src/generators/feature/files/ui/__singularName__.component.ts.template +131 -0
  33. package/src/generators/feature/files/ui/_theme.scss.template +35 -0
  34. package/src/generators/feature/files/ui/form/form.component.ts.template +122 -0
  35. package/src/generators/feature/generator.d.ts +4 -0
  36. package/src/generators/feature/generator.js +209 -0
  37. package/src/generators/feature/generator.js.map +1 -0
  38. package/src/generators/feature/schema.d.ts +5 -0
  39. package/src/generators/{clean-feature → feature}/schema.json +25 -23
  40. package/src/generators/shared/files/ui/__name__.component.ts.template +57 -0
  41. package/src/generators/shared/files/util/__name__.ts.template +7 -0
  42. package/src/generators/shared/generator.d.ts +7 -0
  43. package/src/generators/shared/generator.js +31 -0
  44. package/src/generators/shared/generator.js.map +1 -0
  45. package/src/generators/shared/schema.json +23 -0
  46. package/src/index.js +1 -0
  47. package/src/utils/string-utils.d.ts +16 -0
  48. package/src/utils/string-utils.js +33 -0
  49. package/src/utils/string-utils.js.map +1 -0
  50. package/src/generators/clean-feature/files/application/store.ts.template +0 -6
  51. package/src/generators/clean-feature/files/domain/model.ts.template +0 -4
  52. package/src/generators/clean-feature/files/ui/component.ts.template +0 -44
  53. package/src/generators/clean-feature/generator.d.ts +0 -4
  54. package/src/generators/clean-feature/generator.js +0 -52
  55. package/src/generators/clean-feature/generator.js.map +0 -1
  56. 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
+ }
@@ -0,0 +1,4 @@
1
+ import { Tree } from '@nx/devkit';
2
+ import { FeatureGeneratorSchema } from './schema';
3
+ export declare function featureGenerator(tree: Tree, options: FeatureGeneratorSchema): Promise<void>;
4
+ export default featureGenerator;