@decaf-ts/for-angular 0.0.4 → 0.0.5
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/LICENSE.md +646 -144
- package/README.md +37 -242
- package/dist/lib/README.md +92 -0
- package/dist/lib/assets/i18n/en.json +131 -0
- package/dist/lib/assets/images/angular-logo.svg +45 -0
- package/dist/lib/assets/images/decaf-logo-black.svg +22 -0
- package/dist/lib/assets/images/decaf-logo-lw.svg +50 -0
- package/dist/lib/assets/images/decaf-logo-white.svg +22 -0
- package/dist/lib/assets/images/decaf-logo.svg +54 -0
- package/dist/lib/components/component-renderer/component-renderer.component.d.ts +267 -0
- package/dist/lib/components/crud-field/crud-field.component.d.ts +447 -0
- package/dist/{for-angular/components/decaf-crud-form → lib/components/crud-form}/constants.d.ts +0 -0
- package/dist/lib/components/crud-form/crud-form.component.d.ts +102 -0
- package/dist/{for-angular/components/decaf-crud-form → lib/components/crud-form}/types.d.ts +0 -0
- package/dist/lib/components/model-renderer/model-renderer.component.d.ts +97 -0
- package/dist/lib/engine/DynamicModule.d.ts +17 -0
- package/dist/{for-angular → lib}/engine/NgxCrudFormField.d.ts +22 -14
- package/dist/lib/engine/NgxFormService.d.ts +167 -0
- package/dist/lib/engine/NgxRenderingEngine.d.ts +128 -0
- package/dist/lib/engine/NgxRenderingEngine2.d.ts +251 -0
- package/dist/lib/engine/ValidatorFactory.d.ts +15 -0
- package/dist/lib/engine/constants.d.ts +151 -0
- package/dist/lib/engine/decorators.d.ts +25 -0
- package/dist/lib/engine/index.d.ts +15 -0
- package/dist/lib/engine/types.d.ts +293 -0
- package/dist/lib/esm2022/components/component-renderer/component-renderer.component.mjs +309 -0
- package/dist/lib/esm2022/components/crud-field/crud-field.component.mjs +288 -0
- package/dist/lib/esm2022/components/crud-form/constants.mjs +14 -0
- package/dist/lib/esm2022/components/crud-form/crud-form.component.mjs +140 -0
- package/dist/lib/esm2022/components/crud-form/types.mjs +2 -0
- package/dist/lib/esm2022/components/model-renderer/model-renderer.component.mjs +137 -0
- package/dist/{for-angular → lib}/esm2022/decaf-ts-for-angular.mjs +0 -0
- package/dist/lib/esm2022/engine/DynamicModule.mjs +18 -0
- package/dist/lib/esm2022/engine/NgxCrudFormField.mjs +117 -0
- package/dist/lib/esm2022/engine/NgxFormService.mjs +315 -0
- package/dist/lib/esm2022/engine/NgxRenderingEngine.mjs +194 -0
- package/dist/lib/esm2022/engine/NgxRenderingEngine2.mjs +333 -0
- package/dist/lib/esm2022/engine/ValidatorFactory.mjs +102 -0
- package/dist/lib/esm2022/engine/constants.mjs +160 -0
- package/dist/lib/esm2022/engine/decorators.mjs +38 -0
- package/dist/lib/esm2022/engine/index.mjs +16 -0
- package/dist/lib/esm2022/engine/types.mjs +2 -0
- package/dist/lib/esm2022/for-angular.module.mjs +118 -0
- package/dist/lib/esm2022/interfaces.mjs +2 -0
- package/dist/lib/esm2022/public-apis.mjs +13 -0
- package/dist/lib/fesm2022/decaf-ts-for-angular.mjs +2138 -0
- package/dist/lib/fesm2022/decaf-ts-for-angular.mjs.map +1 -0
- package/dist/lib/for-angular.module.d.ts +44 -0
- package/dist/{for-angular → lib}/index.d.ts +0 -0
- package/dist/lib/interfaces.d.ts +28 -0
- package/dist/lib/public-apis.d.ts +12 -0
- package/package.json +73 -26
- package/dist/for-angular/README.md +0 -297
- package/dist/for-angular/assets/i18n/en.json +0 -21
- package/dist/for-angular/components/decaf-crud-field/decaf-crud-field.component.d.ts +0 -49
- package/dist/for-angular/components/decaf-crud-form/decaf-crud-form.component.d.ts +0 -28
- package/dist/for-angular/components/decaf-model-renderer/decaf-model-renderer.component.d.ts +0 -20
- package/dist/for-angular/engine/DynamicModule.d.ts +0 -2
- package/dist/for-angular/engine/NgxFormService.d.ts +0 -119
- package/dist/for-angular/engine/NgxRenderingEngine.d.ts +0 -18
- package/dist/for-angular/engine/ValidatorFactory.d.ts +0 -4
- package/dist/for-angular/engine/constants.d.ts +0 -13
- package/dist/for-angular/engine/decorators.d.ts +0 -1
- package/dist/for-angular/engine/index.d.ts +0 -5
- package/dist/for-angular/engine/types.d.ts +0 -44
- package/dist/for-angular/esm2022/components/decaf-crud-field/decaf-crud-field.component.mjs +0 -129
- package/dist/for-angular/esm2022/components/decaf-crud-form/constants.mjs +0 -14
- package/dist/for-angular/esm2022/components/decaf-crud-form/decaf-crud-form.component.mjs +0 -80
- package/dist/for-angular/esm2022/components/decaf-crud-form/types.mjs +0 -2
- package/dist/for-angular/esm2022/components/decaf-model-renderer/decaf-model-renderer.component.mjs +0 -47
- package/dist/for-angular/esm2022/engine/DynamicModule.mjs +0 -3
- package/dist/for-angular/esm2022/engine/NgxCrudFormField.mjs +0 -115
- package/dist/for-angular/esm2022/engine/NgxFormService.mjs +0 -235
- package/dist/for-angular/esm2022/engine/NgxRenderingEngine.mjs +0 -84
- package/dist/for-angular/esm2022/engine/ValidatorFactory.mjs +0 -48
- package/dist/for-angular/esm2022/engine/constants.mjs +0 -15
- package/dist/for-angular/esm2022/engine/decorators.mjs +0 -14
- package/dist/for-angular/esm2022/engine/index.mjs +0 -6
- package/dist/for-angular/esm2022/engine/types.mjs +0 -2
- package/dist/for-angular/esm2022/interfaces.mjs +0 -2
- package/dist/for-angular/esm2022/public-apis.mjs +0 -6
- package/dist/for-angular/fesm2022/decaf-ts-for-angular.mjs +0 -759
- package/dist/for-angular/fesm2022/decaf-ts-for-angular.mjs.map +0 -1
- package/dist/for-angular/interfaces.d.ts +0 -8
- package/dist/for-angular/public-apis.d.ts +0 -5
|
@@ -0,0 +1,2138 @@
|
|
|
1
|
+
import { UIKeys, parseValueByType, HTML5InputTypes, HTML5CheckTypes, escapeHtml, parseToNumber, RenderingEngine, RenderingError } from '@decaf-ts/ui-decorators';
|
|
2
|
+
import * as i0 from '@angular/core';
|
|
3
|
+
import { reflectComponentType, inject, EnvironmentInjector, EventEmitter, ViewContainerRef, TemplateRef, ViewChild, Input, Output, Component, ElementRef, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
4
|
+
import { VALIDATION_PARENT_KEY, ValidationKeys, DEFAULT_PATTERNS, Validation, ComparisonValidationKeys, PathProxyEngine, isValidDate, parseDate, Model, sf } from '@decaf-ts/decorator-validation';
|
|
5
|
+
import * as i1 from '@angular/common';
|
|
6
|
+
import { NgComponentOutlet, Location } from '@angular/common';
|
|
7
|
+
import { apply, metadata } from '@decaf-ts/reflection';
|
|
8
|
+
import { InternalError, OperationKeys } from '@decaf-ts/db-decorators';
|
|
9
|
+
import * as i2 from '@angular/forms';
|
|
10
|
+
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
|
11
|
+
import { getLogger, ForAngularModule } from 'src/lib/for-angular.module';
|
|
12
|
+
import { NgxRenderingEngine2 as NgxRenderingEngine2$1 } from 'src/lib/engine/NgxRenderingEngine2';
|
|
13
|
+
import { __decorate, __metadata } from 'tslib';
|
|
14
|
+
import * as i1$1 from '@ionic/angular/standalone';
|
|
15
|
+
import { IonInput, IonItem, IonCheckbox, IonRadioGroup, IonRadio, IonSelect, IonSelectOption, IonLabel, IonTextarea, IonText, IonIcon } from '@ionic/angular/standalone';
|
|
16
|
+
import * as i3 from '@ngx-translate/core';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @description Angular engine key constants
|
|
20
|
+
* @summary Contains key strings used by the Angular rendering engine for reflection,
|
|
21
|
+
* dynamic component creation, and other engine operations.
|
|
22
|
+
* @typedef {Object} AngularEngineKeys
|
|
23
|
+
* @property {string} REFLECT - Prefix for reflection metadata keys
|
|
24
|
+
* @property {string} DYNAMIC - Key for dynamic component identification
|
|
25
|
+
* @property {string} ANNOTATIONS - Key for component annotations
|
|
26
|
+
* @property {string} ECMP - Key for embedded components
|
|
27
|
+
* @property {string} NG_REFLECT - Prefix for Angular reflection attributes
|
|
28
|
+
* @property {string} RENDERED - Prefix for rendered component markers
|
|
29
|
+
* @property {string} MAPPER - Key for property mappers
|
|
30
|
+
* @property {string} CHILDREN - Key for child components
|
|
31
|
+
* @property {string} LISTABLE - Key for listable components
|
|
32
|
+
* @property {string} RENDER - Key for renderable components
|
|
33
|
+
* @property {string} RENDERED_ID - Template for rendered component IDs
|
|
34
|
+
* @property {string} PARENT - Key for comparison decorators and validators
|
|
35
|
+
* @const AngularEngineKeys
|
|
36
|
+
* @memberOf module:engine
|
|
37
|
+
*/
|
|
38
|
+
const AngularEngineKeys = {
|
|
39
|
+
REFLECT: `${UIKeys.REFLECT}.angular.`,
|
|
40
|
+
DYNAMIC: 'dynamic-component',
|
|
41
|
+
ANNOTATIONS: '__annotations__',
|
|
42
|
+
ECMP: 'ecmp',
|
|
43
|
+
NG_REFLECT: 'ng-reflect-',
|
|
44
|
+
RENDERED: 'rendered-as-',
|
|
45
|
+
MAPPER: 'mapper',
|
|
46
|
+
CHILDREN: 'children',
|
|
47
|
+
LISTABLE: 'listable',
|
|
48
|
+
RENDER: 'render',
|
|
49
|
+
RENDERED_ID: 'rendered-as-{0}',
|
|
50
|
+
PARENT: '_parent',
|
|
51
|
+
VALIDATION_PARENT_KEY: VALIDATION_PARENT_KEY
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* @description Form validation state constants
|
|
55
|
+
* @summary Contains constants representing the possible validation states of a form.
|
|
56
|
+
* These are used to check and handle form validation throughout the application.
|
|
57
|
+
* @typedef {Object} FormConstants
|
|
58
|
+
* @property {string} VALID - Constant representing a valid form state
|
|
59
|
+
* @property {string} INVALID - Constant representing an invalid form state
|
|
60
|
+
* @const FormConstants
|
|
61
|
+
* @memberOf module:engine
|
|
62
|
+
*/
|
|
63
|
+
const FormConstants = {
|
|
64
|
+
VALID: 'VALID',
|
|
65
|
+
INVALID: 'INVALID',
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* @description Event name constants
|
|
69
|
+
* @summary Enum containing constants for event names used throughout the application.
|
|
70
|
+
* These are used to standardize event naming and handling.
|
|
71
|
+
* @enum {string}
|
|
72
|
+
* @readonly
|
|
73
|
+
* @property {string} BACK_BUTTON_NAVIGATION - Event fired when back button navigation ends
|
|
74
|
+
* @property {string} REFRESH_EVENT - Event fired when a refresh action occurs
|
|
75
|
+
* @property {string} CLICK_EVENT - Event fired when a click action occurs
|
|
76
|
+
* @property {string} SUBMIT_EVENT - Event fired when a form submission occurs
|
|
77
|
+
* @memberOf module:engine
|
|
78
|
+
*/
|
|
79
|
+
var EventConstants;
|
|
80
|
+
(function (EventConstants) {
|
|
81
|
+
EventConstants["BACK_BUTTON_NAVIGATION"] = "backButtonNavigationEndEvent";
|
|
82
|
+
EventConstants["REFRESH_EVENT"] = "RefreshEvent";
|
|
83
|
+
EventConstants["CLICK_EVENT"] = "ClickEvent";
|
|
84
|
+
EventConstants["SUBMIT_EVENT"] = "SubmitEvent";
|
|
85
|
+
})(EventConstants || (EventConstants = {}));
|
|
86
|
+
/**
|
|
87
|
+
* @description Logger level constants
|
|
88
|
+
* @summary Enum defining the logging levels used in the application's logging system.
|
|
89
|
+
* Lower values represent more verbose logging, while higher values represent more critical logs.
|
|
90
|
+
* @enum {number}
|
|
91
|
+
* @readonly
|
|
92
|
+
* @property {number} ALL - Log everything (most verbose)
|
|
93
|
+
* @property {number} DEBUG - Log debug information
|
|
94
|
+
* @property {number} INFO - Log informational messages
|
|
95
|
+
* @property {number} WARN - Log warnings
|
|
96
|
+
* @property {number} ERROR - Log errors
|
|
97
|
+
* @property {number} CRITICAL - Log critical errors (least verbose)
|
|
98
|
+
* @memberOf module:engine
|
|
99
|
+
*/
|
|
100
|
+
var LoggerLevels;
|
|
101
|
+
(function (LoggerLevels) {
|
|
102
|
+
LoggerLevels[LoggerLevels["ALL"] = 0] = "ALL";
|
|
103
|
+
LoggerLevels[LoggerLevels["DEBUG"] = 1] = "DEBUG";
|
|
104
|
+
LoggerLevels[LoggerLevels["INFO"] = 2] = "INFO";
|
|
105
|
+
LoggerLevels[LoggerLevels["WARN"] = 3] = "WARN";
|
|
106
|
+
LoggerLevels[LoggerLevels["ERROR"] = 4] = "ERROR";
|
|
107
|
+
LoggerLevels[LoggerLevels["CRITICAL"] = 5] = "CRITICAL";
|
|
108
|
+
})(LoggerLevels || (LoggerLevels = {}));
|
|
109
|
+
;
|
|
110
|
+
/**
|
|
111
|
+
* @description Route direction constants
|
|
112
|
+
* @summary Enum defining the possible navigation directions in the application.
|
|
113
|
+
* Used for controlling navigation flow and animation directions.
|
|
114
|
+
* @enum {string}
|
|
115
|
+
* @readonly
|
|
116
|
+
* @property {string} BACK - Navigate back to the previous page
|
|
117
|
+
* @property {string} FORWARD - Navigate forward to the next page
|
|
118
|
+
* @property {string} ROOT - Navigate to the root/home page
|
|
119
|
+
* @memberOf module:engine
|
|
120
|
+
*/
|
|
121
|
+
var RouteDirections;
|
|
122
|
+
(function (RouteDirections) {
|
|
123
|
+
RouteDirections["BACK"] = "back";
|
|
124
|
+
RouteDirections["FORWARD"] = "forward";
|
|
125
|
+
RouteDirections["ROOT"] = "root";
|
|
126
|
+
})(RouteDirections || (RouteDirections = {}));
|
|
127
|
+
/**
|
|
128
|
+
* @description Component tag name constants
|
|
129
|
+
* @summary Enum defining the tag names for custom components used in the application.
|
|
130
|
+
* These tag names are used for component registration and rendering.
|
|
131
|
+
* @enum {string}
|
|
132
|
+
* @readonly
|
|
133
|
+
* @property {string} LIST_ITEM - Tag name for list item component
|
|
134
|
+
* @property {string} LIST_INFINITE - Tag name for infinite scrolling list component
|
|
135
|
+
* @property {string} LIST_PAGINATED - Tag name for paginated list component
|
|
136
|
+
* @memberOf module:engine
|
|
137
|
+
*/
|
|
138
|
+
var ComponentsTagNames;
|
|
139
|
+
(function (ComponentsTagNames) {
|
|
140
|
+
ComponentsTagNames["LIST_ITEM"] = "ngx-decaf-list-item";
|
|
141
|
+
ComponentsTagNames["LIST_INFINITE"] = "ngx-decaf-list-infinite";
|
|
142
|
+
ComponentsTagNames["LIST_PAGINATED"] = "ngx-decaf-list-paginated";
|
|
143
|
+
})(ComponentsTagNames || (ComponentsTagNames = {}));
|
|
144
|
+
/**
|
|
145
|
+
* @description Base component property name constants
|
|
146
|
+
* @summary Enum defining the standard property names used by base components in the application.
|
|
147
|
+
* These property names are used for consistent property access across components.
|
|
148
|
+
* @enum {string}
|
|
149
|
+
* @readonly
|
|
150
|
+
* @property {string} MODEL - Property name for the component's data model
|
|
151
|
+
* @property {string} LOCALE - Property name for localization settings
|
|
152
|
+
* @property {string} PK - Property name for primary key
|
|
153
|
+
* @property {string} ITEMS - Property name for collection items
|
|
154
|
+
* @property {string} ROUTE - Property name for routing information
|
|
155
|
+
* @property {string} OPERATIONS - Property name for available operations
|
|
156
|
+
* @property {string} UID - Property name for unique identifier
|
|
157
|
+
* @property {string} TRANSLATABLE - Property name for translation flag
|
|
158
|
+
* @property {string} MAPPER - Property name for property mapper
|
|
159
|
+
* @property {string} INITIALIZED - Property name for initialization state
|
|
160
|
+
* @memberOf module:engine
|
|
161
|
+
*/
|
|
162
|
+
var BaseComponentProps;
|
|
163
|
+
(function (BaseComponentProps) {
|
|
164
|
+
BaseComponentProps["MODEL"] = "model";
|
|
165
|
+
BaseComponentProps["LOCALE"] = "locale";
|
|
166
|
+
BaseComponentProps["PK"] = "pk";
|
|
167
|
+
BaseComponentProps["ITEMS"] = "items";
|
|
168
|
+
BaseComponentProps["ROUTE"] = "route";
|
|
169
|
+
BaseComponentProps["OPERATIONS"] = "operations";
|
|
170
|
+
BaseComponentProps["UID"] = "uid";
|
|
171
|
+
BaseComponentProps["TRANSLATABLE"] = "translatable";
|
|
172
|
+
BaseComponentProps["MAPPER"] = "mapper";
|
|
173
|
+
BaseComponentProps["INITIALIZED"] = "initialized";
|
|
174
|
+
})(BaseComponentProps || (BaseComponentProps = {}));
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
*
|
|
178
|
+
* Resolves the correct validator key and its associated properties based on the input key and type.
|
|
179
|
+
*
|
|
180
|
+
* When the validation key is TYPE, it's necessary to resolve the actual validator based on the
|
|
181
|
+
* field's type (e.g., 'password', 'email', 'url') instead of using the generic getValidator("type") logic.
|
|
182
|
+
* This allows directly invoking specific validators like getValidator('password'), ensuring the correct
|
|
183
|
+
* behavior for type-based validation.
|
|
184
|
+
*
|
|
185
|
+
* @param key - The validation key (e.g., 'type', 'required', etc.).
|
|
186
|
+
* @param value - The value that needs be provided to the validator.
|
|
187
|
+
* @param type - The field's declared type.
|
|
188
|
+
* @returns An object containing the resolved validator key and its corresponding props.
|
|
189
|
+
*/
|
|
190
|
+
const resolveValidatorKeyProps = (key, value, type) => {
|
|
191
|
+
const patternValidators = {
|
|
192
|
+
[ValidationKeys.PASSWORD]: DEFAULT_PATTERNS.PASSWORD.CHAR8_ONE_OF_EACH,
|
|
193
|
+
[ValidationKeys.EMAIL]: DEFAULT_PATTERNS.EMAIL,
|
|
194
|
+
[ValidationKeys.URL]: DEFAULT_PATTERNS.URL,
|
|
195
|
+
};
|
|
196
|
+
const isTypeBased = key === ValidationKeys.TYPE && Object.keys(patternValidators).includes(type);
|
|
197
|
+
const validatorKey = isTypeBased ? type : key;
|
|
198
|
+
const props = {
|
|
199
|
+
[validatorKey]: value,
|
|
200
|
+
// Email, Password, and URL are validated using the "pattern" key
|
|
201
|
+
...(isTypeBased && { [ValidationKeys.PATTERN]: patternValidators[type] }),
|
|
202
|
+
};
|
|
203
|
+
return { validatorKey, props };
|
|
204
|
+
};
|
|
205
|
+
class ValidatorFactory {
|
|
206
|
+
static spawn(fieldProps, key) {
|
|
207
|
+
if (!Validation.keys().includes(key))
|
|
208
|
+
throw new Error('Unsupported custom validation');
|
|
209
|
+
const validatorFn = (control) => {
|
|
210
|
+
const { name, type } = fieldProps;
|
|
211
|
+
const { validatorKey, props } = resolveValidatorKeyProps(key, fieldProps[key], type);
|
|
212
|
+
const validator = Validation.get(validatorKey);
|
|
213
|
+
// parseValueByType does not support undefined values
|
|
214
|
+
const value = typeof control.value !== 'undefined'
|
|
215
|
+
? parseValueByType(type, type === HTML5InputTypes.CHECKBOX ? name : control.value, fieldProps)
|
|
216
|
+
: undefined;
|
|
217
|
+
// Create a proxy to enable access to parent and child values
|
|
218
|
+
let proxy = ValidatorFactory.createProxy({});
|
|
219
|
+
if (Object.values(ComparisonValidationKeys).includes(key)) {
|
|
220
|
+
const parent = control instanceof FormGroup ? control : control[AngularEngineKeys.PARENT];
|
|
221
|
+
proxy = ValidatorFactory.createProxy(parent);
|
|
222
|
+
}
|
|
223
|
+
let errs;
|
|
224
|
+
try {
|
|
225
|
+
errs = validator.hasErrors(value, props, proxy);
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
errs = `${key} validator failed to validate: ${e}`;
|
|
229
|
+
console.warn(errs);
|
|
230
|
+
}
|
|
231
|
+
return errs ? { [validatorKey]: true } : null;
|
|
232
|
+
};
|
|
233
|
+
Object.defineProperty(validatorFn, 'name', {
|
|
234
|
+
value: `${key}Validator`,
|
|
235
|
+
});
|
|
236
|
+
return validatorFn;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* @summary Creates a proxy wrapper for an Angular AbstractControl to assist with custom validation logic.
|
|
240
|
+
* @description Returns a structured proxy object that simulates a hierarchical tree of form values.
|
|
241
|
+
* Enables Validators handling method to access parent and child properties using consistent dot-notation in Angular forms.
|
|
242
|
+
*
|
|
243
|
+
* @param {AbstractControl} control - The control to wrap in a proxy.
|
|
244
|
+
* @returns {PathProxy<any>} A proxy object exposing form values and enabling recursive parent access.
|
|
245
|
+
*/
|
|
246
|
+
static createProxy(control) {
|
|
247
|
+
return PathProxyEngine.create(control, {
|
|
248
|
+
getValue(target, prop) {
|
|
249
|
+
if (target instanceof FormControl)
|
|
250
|
+
return target.value;
|
|
251
|
+
if (target instanceof FormGroup) {
|
|
252
|
+
const control = target.controls[prop];
|
|
253
|
+
return control instanceof FormControl ? control.value : control;
|
|
254
|
+
}
|
|
255
|
+
// const value = target[prop];
|
|
256
|
+
// if (value instanceof FormControl)
|
|
257
|
+
// return value.value;
|
|
258
|
+
//
|
|
259
|
+
// if (value instanceof FormGroup) {
|
|
260
|
+
// const control = value.controls[prop];
|
|
261
|
+
// return control instanceof FormControl ? control.value : control;
|
|
262
|
+
// }
|
|
263
|
+
return target[prop];
|
|
264
|
+
},
|
|
265
|
+
getParent: function (target) {
|
|
266
|
+
return target._parent;
|
|
267
|
+
},
|
|
268
|
+
ignoreUndefined: true,
|
|
269
|
+
ignoreNull: true,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* @description Service for managing Angular forms and form controls.
|
|
276
|
+
* @summary The NgxFormService provides utility methods for creating, managing, and validating Angular forms and form controls. It includes functionality for registering forms, adding controls, validating fields, and handling form data.
|
|
277
|
+
*
|
|
278
|
+
* @class
|
|
279
|
+
* @param {WeakMap<AbstractControl, FieldProperties>} controls - A WeakMap to store control properties.
|
|
280
|
+
* @param {Map<string, FormGroup>} formRegistry - A Map to store registered forms.
|
|
281
|
+
*
|
|
282
|
+
* @example
|
|
283
|
+
* // Creating a form from components
|
|
284
|
+
* const components = [
|
|
285
|
+
* { inputs: { name: 'username', type: 'text', required: true } },
|
|
286
|
+
* { inputs: { name: 'password', type: 'password', minLength: 8 } }
|
|
287
|
+
* ];
|
|
288
|
+
* const form = NgxFormService.createFormFromComponents('loginForm', components, true);
|
|
289
|
+
*
|
|
290
|
+
* // Validating fields
|
|
291
|
+
* NgxFormService.validateFields(form);
|
|
292
|
+
*
|
|
293
|
+
* // Getting form data
|
|
294
|
+
* const formData = NgxFormService.getFormData(form);
|
|
295
|
+
*
|
|
296
|
+
* @mermaid
|
|
297
|
+
* sequenceDiagram
|
|
298
|
+
* participant C as Component
|
|
299
|
+
* participant NFS as NgxFormService
|
|
300
|
+
* participant AF as Angular Forms
|
|
301
|
+
* C->>NFS: createFormFromComponents()
|
|
302
|
+
* NFS->>AF: new FormGroup()
|
|
303
|
+
* NFS->>NFS: addFormControl()
|
|
304
|
+
* NFS->>AF: addControl()
|
|
305
|
+
* NFS-->>C: Return FormGroup
|
|
306
|
+
* C->>NFS: validateFields()
|
|
307
|
+
* NFS->>AF: markAsTouched(), markAsDirty(), updateValueAndValidity()
|
|
308
|
+
* C->>NFS: getFormData()
|
|
309
|
+
* NFS->>AF: Get control values
|
|
310
|
+
* NFS-->>C: Return form data
|
|
311
|
+
*/
|
|
312
|
+
class NgxFormService {
|
|
313
|
+
static { this.controls = new WeakMap(); }
|
|
314
|
+
static { this.formRegistry = new Map(); }
|
|
315
|
+
/**
|
|
316
|
+
* @description Adds a form to the registry.
|
|
317
|
+
* @summary Registers a FormGroup with a unique identifier. Throws an error if the identifier is already in use.
|
|
318
|
+
* @param {string} formId - The unique identifier for the form.
|
|
319
|
+
* @param {FormGroup} formGroup - The FormGroup to be registered.
|
|
320
|
+
* @throws {Error} If a FormGroup with the given id is already registered.
|
|
321
|
+
*/
|
|
322
|
+
static addRegistry(formId, formGroup) {
|
|
323
|
+
if (this.formRegistry.has(formId))
|
|
324
|
+
throw new Error(`A FormGroup with id '${formId}' is already registered.`);
|
|
325
|
+
this.formRegistry.set(formId, formGroup);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* @description Removes a form from the registry.
|
|
329
|
+
* @summary Deletes a FormGroup from the registry using its unique identifier.
|
|
330
|
+
* @param {string} formId - The unique identifier of the form to be removed.
|
|
331
|
+
*/
|
|
332
|
+
static removeRegistry(formId) {
|
|
333
|
+
this.formRegistry.delete(formId);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* @description Resolves the parent group and control name from a path.
|
|
337
|
+
* @summary Traverses the form group structure to find the parent group and control name for a given path.
|
|
338
|
+
* @param {FormGroup} formGroup - The root FormGroup.
|
|
339
|
+
* @param {string} path - The path to the control.
|
|
340
|
+
* @return {FormParentGroup} A tuple containing the parent FormGroup and the control name.
|
|
341
|
+
*/
|
|
342
|
+
static resolveParentGroup(formGroup, path) {
|
|
343
|
+
const parts = path.split('.');
|
|
344
|
+
const controlName = parts.pop();
|
|
345
|
+
let currentGroup = formGroup;
|
|
346
|
+
for (const part of parts) {
|
|
347
|
+
if (!currentGroup.get(part)) {
|
|
348
|
+
currentGroup.addControl(part, new FormGroup({}));
|
|
349
|
+
}
|
|
350
|
+
currentGroup = currentGroup.get(part);
|
|
351
|
+
}
|
|
352
|
+
return [currentGroup, controlName];
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* @description Adds a form control to a form group.
|
|
356
|
+
* @summary Creates and adds a form control to the specified form group based on the provided component properties.
|
|
357
|
+
* @param {FormGroup} formGroup - The form group to add the control to.
|
|
358
|
+
* @param {ComponentInput} componentProps - The properties of the component to create the control from.
|
|
359
|
+
*/
|
|
360
|
+
static addFormControl(formGroup, componentProps) {
|
|
361
|
+
const { name, childOf } = componentProps;
|
|
362
|
+
const fullPath = childOf ? `${childOf}.${name}` : name;
|
|
363
|
+
const [parentGroup, controlName] = this.resolveParentGroup(formGroup, fullPath);
|
|
364
|
+
if (!parentGroup.get(controlName)) {
|
|
365
|
+
const control = NgxFormService.fromProps(componentProps, componentProps.updateMode || 'change');
|
|
366
|
+
NgxFormService.register(control, componentProps);
|
|
367
|
+
parentGroup.addControl(controlName, control);
|
|
368
|
+
}
|
|
369
|
+
componentProps['formGroup'] = parentGroup;
|
|
370
|
+
componentProps['formControl'] = parentGroup.get(controlName);
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* @description Retrieves a control from a registered form.
|
|
374
|
+
* @summary Finds and returns an AbstractControl from a registered form using the form id and optional path.
|
|
375
|
+
* @param {string} formId - The unique identifier of the form.
|
|
376
|
+
* @param {string} [path] - The path to the control within the form.
|
|
377
|
+
* @return {AbstractControl} The requested AbstractControl.
|
|
378
|
+
* @throws {Error} If the form is not found in the registry or the control is not found in the form.
|
|
379
|
+
*/
|
|
380
|
+
static getControlFromForm(formId, path) {
|
|
381
|
+
const form = this.formRegistry.get(formId);
|
|
382
|
+
if (!form)
|
|
383
|
+
throw new Error(`Form with id '${formId}' not found in the registry.`);
|
|
384
|
+
if (!path)
|
|
385
|
+
return form;
|
|
386
|
+
const control = form.get(path);
|
|
387
|
+
if (!control)
|
|
388
|
+
throw new Error(`Control with path '${path}' not found in form '${formId}'.`);
|
|
389
|
+
return control;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* @description Creates a form from component configurations.
|
|
393
|
+
* @summary Generates a FormGroup based on an array of component configurations and optionally registers it.
|
|
394
|
+
* @param {string} id - The unique identifier for the form.
|
|
395
|
+
* @param {ComponentConfig[]} components - An array of component configurations.
|
|
396
|
+
* @param {boolean} [registry=false] - Whether to register the created form.
|
|
397
|
+
* @return {FormGroup} The created FormGroup.
|
|
398
|
+
*/
|
|
399
|
+
static createFormFromComponents(id, components, registry = false) {
|
|
400
|
+
const form = new FormGroup({});
|
|
401
|
+
components.forEach(component => {
|
|
402
|
+
this.addFormControl(form, component.inputs);
|
|
403
|
+
});
|
|
404
|
+
if (registry)
|
|
405
|
+
this.addRegistry(id, form);
|
|
406
|
+
return form;
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* @description Adds a control to a form based on component properties.
|
|
410
|
+
* @summary Creates and adds a form control to a form (existing or new) based on the provided component properties.
|
|
411
|
+
* @param {string} id - The unique identifier of the form.
|
|
412
|
+
* @param {FieldProperties} componentProperties - The properties of the component to create the control from.
|
|
413
|
+
* @return {AbstractControl} The form or created control.
|
|
414
|
+
*/
|
|
415
|
+
static addControlFromProps(id, componentProperties) {
|
|
416
|
+
const form = this.formRegistry.get(id) ?? new FormGroup({});
|
|
417
|
+
if (!this.formRegistry.has(id))
|
|
418
|
+
this.addRegistry(id, form);
|
|
419
|
+
if (componentProperties.path)
|
|
420
|
+
this.addFormControl(form, componentProperties);
|
|
421
|
+
return form;
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* @description Retrieves form data from a FormGroup.
|
|
425
|
+
* @summary Extracts and processes the data from a FormGroup, handling different input types and nested form groups.
|
|
426
|
+
* @param {FormGroup} formGroup - The FormGroup to extract data from.
|
|
427
|
+
* @return {Record<string, unknown>} An object containing the form data.
|
|
428
|
+
*/
|
|
429
|
+
static getFormData(formGroup) {
|
|
430
|
+
const data = {};
|
|
431
|
+
for (const key in formGroup.controls) {
|
|
432
|
+
const control = formGroup.controls[key];
|
|
433
|
+
if (!(control instanceof FormControl)) {
|
|
434
|
+
data[key] = NgxFormService.getFormData(control);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const props = NgxFormService.getPropsFromControl(control);
|
|
438
|
+
let value = control.value;
|
|
439
|
+
if (!HTML5CheckTypes.includes(props['type'])) {
|
|
440
|
+
switch (props['type']) {
|
|
441
|
+
case HTML5InputTypes.NUMBER:
|
|
442
|
+
value = parseToNumber(value);
|
|
443
|
+
break;
|
|
444
|
+
case HTML5InputTypes.DATE:
|
|
445
|
+
case HTML5InputTypes.DATETIME_LOCAL:
|
|
446
|
+
value = new Date(value);
|
|
447
|
+
break;
|
|
448
|
+
default:
|
|
449
|
+
value = escapeHtml(value);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
data[key] = value;
|
|
453
|
+
}
|
|
454
|
+
return data;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* @description Validates fields in a form control or form group.
|
|
458
|
+
* @summary Recursively validates all fields in a form control or form group, marking them as touched and dirty.
|
|
459
|
+
* @param {AbstractControl} control - The control or form group to validate.
|
|
460
|
+
* @param {string} [path] - The path to the control within the form.
|
|
461
|
+
* @return {boolean} True if all fields are valid, false otherwise.
|
|
462
|
+
* @throws {Error} If no control is found at the specified path or if the control type is unknown.
|
|
463
|
+
*/
|
|
464
|
+
static validateFields(control, path) {
|
|
465
|
+
control = path ? control.get(path) : control;
|
|
466
|
+
if (!control)
|
|
467
|
+
throw new Error(`No control found at path: ${path || 'root'}.`);
|
|
468
|
+
const isAllowed = [FormGroup, FormControl].some(type => control instanceof type);
|
|
469
|
+
if (!isAllowed)
|
|
470
|
+
throw new Error(`Unknown control type at: ${path || 'root'}`);
|
|
471
|
+
control.markAsTouched();
|
|
472
|
+
control.markAsDirty();
|
|
473
|
+
control.updateValueAndValidity({ emitEvent: true });
|
|
474
|
+
if (control instanceof FormGroup) {
|
|
475
|
+
Object.values(control.controls).forEach((childControl) => {
|
|
476
|
+
this.validateFields(childControl);
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return control.valid;
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* @description Generates validators from component properties.
|
|
483
|
+
* @summary Creates an array of ValidatorFn based on the supported validation keys in the component properties.
|
|
484
|
+
* @param {FieldProperties} props - The component properties.
|
|
485
|
+
* @return {ValidatorFn[]} An array of validator functions.
|
|
486
|
+
*/
|
|
487
|
+
static validatorsFromProps(props) {
|
|
488
|
+
const supportedValidationKeys = Validation.keys();
|
|
489
|
+
return Object.keys(props)
|
|
490
|
+
.filter((k) => supportedValidationKeys.includes(k))
|
|
491
|
+
.map((k) => {
|
|
492
|
+
return ValidatorFactory.spawn(props, k);
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* @description Creates a FormControl from component properties.
|
|
497
|
+
* @summary Generates a FormControl with validators based on the provided component properties.
|
|
498
|
+
* @param {FieldProperties} props - The component properties.
|
|
499
|
+
* @param {FieldUpdateMode} [updateMode='change'] - The update mode for the control.
|
|
500
|
+
* @return {FormControl} The created FormControl.
|
|
501
|
+
*/
|
|
502
|
+
static fromProps(props, updateMode = 'change') {
|
|
503
|
+
const validators = this.validatorsFromProps(props);
|
|
504
|
+
const composed = validators.length ? Validators.compose(validators) : null;
|
|
505
|
+
return new FormControl({
|
|
506
|
+
value: props.value && props.type !== HTML5InputTypes.CHECKBOX
|
|
507
|
+
? props.type === HTML5InputTypes.DATE
|
|
508
|
+
? !isValidDate(parseDate(props.format, props.value))
|
|
509
|
+
? undefined : props.value :
|
|
510
|
+
props.value : undefined,
|
|
511
|
+
disabled: props.disabled,
|
|
512
|
+
}, {
|
|
513
|
+
validators: composed,
|
|
514
|
+
updateOn: updateMode,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* @description Retrieves properties from a FormControl.
|
|
519
|
+
* @summary Gets the FieldProperties associated with a FormControl from the internal WeakMap.
|
|
520
|
+
* @param {FormControl} control - The FormControl to get properties for.
|
|
521
|
+
* @return {FieldProperties} The properties associated with the control.
|
|
522
|
+
*/
|
|
523
|
+
static getPropsFromControl(control) {
|
|
524
|
+
return this.controls.get(control) || {};
|
|
525
|
+
}
|
|
526
|
+
/**
|
|
527
|
+
* @description Finds a parent element with a specific tag.
|
|
528
|
+
* @summary Traverses up the DOM tree to find the nearest parent element with the specified tag.
|
|
529
|
+
* @param {HTMLElement} el - The starting element.
|
|
530
|
+
* @param {string} tag - The tag name to search for.
|
|
531
|
+
* @return {HTMLElement} The found parent element.
|
|
532
|
+
* @throws {Error} If no parent with the specified tag is found.
|
|
533
|
+
*/
|
|
534
|
+
static getParentEl(el, tag) {
|
|
535
|
+
let parent;
|
|
536
|
+
while ((parent = el.parentElement) !== null) {
|
|
537
|
+
if (parent.tagName.toLowerCase() === tag.toLowerCase()) {
|
|
538
|
+
return parent;
|
|
539
|
+
}
|
|
540
|
+
el = parent;
|
|
541
|
+
}
|
|
542
|
+
throw new Error(`No parent with the tag ${tag} was found for provided element`);
|
|
543
|
+
}
|
|
544
|
+
/**
|
|
545
|
+
* @description Registers a control with its properties.
|
|
546
|
+
* @summary Associates a control with its properties in the internal WeakMap.
|
|
547
|
+
* @param {AbstractControl} control - The control to register.
|
|
548
|
+
* @param {FieldProperties} props - The properties to associate with the control.
|
|
549
|
+
*/
|
|
550
|
+
static register(control, props) {
|
|
551
|
+
this.controls.set(control, props);
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* @description Unregisters a control.
|
|
555
|
+
* @summary Removes a control and its associated properties from the internal WeakMap.
|
|
556
|
+
* @param {AbstractControl} control - The control to unregister.
|
|
557
|
+
* @return {boolean} True if the control was successfully unregistered, false otherwise.
|
|
558
|
+
*/
|
|
559
|
+
static unregister(control) {
|
|
560
|
+
return this.controls.delete(control);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* @description Resets a form group.
|
|
564
|
+
* @summary Recursively resets all controls in a form group, clearing values, errors, and marking them as pristine and untouched.
|
|
565
|
+
* @param {FormGroup} formGroup - The form group to reset.
|
|
566
|
+
*/
|
|
567
|
+
static reset(formGroup) {
|
|
568
|
+
for (const key in formGroup.controls) {
|
|
569
|
+
const control = formGroup.controls[key];
|
|
570
|
+
if (!(control instanceof FormControl)) {
|
|
571
|
+
NgxFormService.reset(control);
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
const { type } = NgxFormService.getPropsFromControl(control);
|
|
575
|
+
if (!HTML5CheckTypes.includes(type))
|
|
576
|
+
control.setValue(undefined);
|
|
577
|
+
control.markAsPristine();
|
|
578
|
+
control.markAsUntouched();
|
|
579
|
+
control.setErrors(null);
|
|
580
|
+
control.updateValueAndValidity();
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* @description Angular implementation of the RenderingEngine with enhanced features
|
|
587
|
+
* @summary This class extends the base RenderingEngine to provide Angular-specific rendering capabilities
|
|
588
|
+
* with additional features compared to NgxRenderingEngine. It handles the conversion of field definitions
|
|
589
|
+
* to Angular components, manages component registration, and provides utilities for component creation
|
|
590
|
+
* and input handling. This implementation uses Angular's newer component APIs.
|
|
591
|
+
*
|
|
592
|
+
* @template AngularFieldDefinition - Type for Angular-specific field definitions
|
|
593
|
+
* @template AngularDynamicOutput - Type for Angular-specific component output
|
|
594
|
+
*
|
|
595
|
+
* @class NgxRenderingEngine2
|
|
596
|
+
* @example
|
|
597
|
+
* ```typescript
|
|
598
|
+
* const engine = NgxRenderingEngine2.get();
|
|
599
|
+
* engine.initialize();
|
|
600
|
+
* const output = engine.render(myModel, {}, viewContainerRef, injector, templateRef);
|
|
601
|
+
* ```
|
|
602
|
+
*
|
|
603
|
+
* @mermaid
|
|
604
|
+
* sequenceDiagram
|
|
605
|
+
* participant Client
|
|
606
|
+
* participant Engine as NgxRenderingEngine2
|
|
607
|
+
* participant Components as RegisteredComponents
|
|
608
|
+
*
|
|
609
|
+
* Client->>Engine: get()
|
|
610
|
+
* Client->>Engine: initialize()
|
|
611
|
+
* Client->>Engine: render(model, props, vcr, injector, tpl)
|
|
612
|
+
* Engine->>Engine: toFieldDefinition(model, props)
|
|
613
|
+
* Engine->>Engine: fromFieldDefinition(fieldDef, vcr, injector, tpl)
|
|
614
|
+
* Engine->>Components: components(fieldDef.tag)
|
|
615
|
+
* Components-->>Engine: component constructor
|
|
616
|
+
* Engine->>Engine: createComponent(component, inputs, metadata, vcr, injector, template)
|
|
617
|
+
* Engine-->>Client: return AngularDynamicOutput
|
|
618
|
+
*/
|
|
619
|
+
class NgxRenderingEngine2 extends RenderingEngine {
|
|
620
|
+
/**
|
|
621
|
+
* @description Creates a new instance of NgxRenderingEngine2
|
|
622
|
+
* @summary Initializes the rendering engine with the 'angular' engine type.
|
|
623
|
+
* This constructor sets up the base configuration needed for Angular-specific rendering.
|
|
624
|
+
*/
|
|
625
|
+
constructor() {
|
|
626
|
+
super('angular');
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* @description Converts a field definition to an Angular component output
|
|
630
|
+
* @summary This private method takes a field definition and creates the corresponding Angular component.
|
|
631
|
+
* It handles component instantiation, input property mapping, and child component rendering.
|
|
632
|
+
* The method validates input properties against the component's metadata and processes
|
|
633
|
+
* child components recursively.
|
|
634
|
+
*
|
|
635
|
+
* @param {FieldDefinition<AngularFieldDefinition>} fieldDef - The field definition to convert
|
|
636
|
+
* @param {ViewContainerRef} vcr - The view container reference for component creation
|
|
637
|
+
* @param {Injector} injector - The Angular injector for dependency injection
|
|
638
|
+
* @param {TemplateRef<any>} tpl - The template reference for content projection
|
|
639
|
+
* @param {string} registryFormId - Form identifier for the component renderer
|
|
640
|
+
* @return {AngularDynamicOutput} The Angular component output with component reference and inputs
|
|
641
|
+
*
|
|
642
|
+
* @mermaid
|
|
643
|
+
* sequenceDiagram
|
|
644
|
+
* participant Method as fromFieldDefinition
|
|
645
|
+
* participant Components as NgxRenderingEngine2.components
|
|
646
|
+
* participant Angular as Angular Core
|
|
647
|
+
* participant Process as processChild
|
|
648
|
+
*
|
|
649
|
+
* Method->>Components: components(fieldDef.tag)
|
|
650
|
+
* Components-->>Method: component constructor
|
|
651
|
+
* Method->>Angular: reflectComponentType(component)
|
|
652
|
+
* Angular-->>Method: componentMetadata
|
|
653
|
+
* Method->>Method: Validate input properties
|
|
654
|
+
* Method->>Method: Create result object
|
|
655
|
+
* alt Has children
|
|
656
|
+
* Method->>Process: Process children recursively
|
|
657
|
+
* Process->>Method: Return processed children
|
|
658
|
+
* Method->>Angular: Create embedded view
|
|
659
|
+
* Method->>Method: Create component instance
|
|
660
|
+
* end
|
|
661
|
+
* Method-->>Caller: return AngularDynamicOutput
|
|
662
|
+
*/
|
|
663
|
+
fromFieldDefinition(fieldDef, vcr, injector, tpl, registryFormId = Date.now().toString(36).toUpperCase()) {
|
|
664
|
+
const cmp = fieldDef?.component || NgxRenderingEngine2.components(fieldDef.tag);
|
|
665
|
+
const component = (cmp.constructor);
|
|
666
|
+
const componentMetadata = reflectComponentType(component);
|
|
667
|
+
if (!componentMetadata) {
|
|
668
|
+
throw new InternalError(`Metadata for component ${fieldDef.tag} not found.`);
|
|
669
|
+
}
|
|
670
|
+
const { inputs: possibleInputs } = componentMetadata;
|
|
671
|
+
const inputs = { ...fieldDef.props };
|
|
672
|
+
const unmappedKeys = Object.keys(inputs).filter(input => {
|
|
673
|
+
const isMapped = possibleInputs.find(({ propName }) => propName === input);
|
|
674
|
+
if (!isMapped)
|
|
675
|
+
delete inputs[input];
|
|
676
|
+
return !isMapped;
|
|
677
|
+
});
|
|
678
|
+
if (unmappedKeys.length > 0)
|
|
679
|
+
console.warn(`Unmapped input properties for component ${fieldDef.tag}: ${unmappedKeys.join(', ')}`);
|
|
680
|
+
const result = {
|
|
681
|
+
component,
|
|
682
|
+
inputs,
|
|
683
|
+
injector,
|
|
684
|
+
};
|
|
685
|
+
if (fieldDef.rendererId)
|
|
686
|
+
result.inputs['rendererId'] = fieldDef.rendererId;
|
|
687
|
+
// process children
|
|
688
|
+
if (fieldDef.children?.length) {
|
|
689
|
+
result.children = fieldDef.children.map((child) => {
|
|
690
|
+
// create a child form and add its controls as properties of child.props
|
|
691
|
+
NgxFormService.addControlFromProps(registryFormId, child.props);
|
|
692
|
+
return this.fromFieldDefinition(child, vcr, injector, tpl, registryFormId);
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
// generating DOM
|
|
696
|
+
vcr.clear();
|
|
697
|
+
const template = vcr.createEmbeddedView(tpl, injector).rootNodes;
|
|
698
|
+
const componentInstance = NgxRenderingEngine2.createComponent(component, { ...inputs, model: this._model }, componentMetadata, vcr, injector, template);
|
|
699
|
+
result.instance = NgxRenderingEngine2._instance = componentInstance.instance;
|
|
700
|
+
return result;
|
|
701
|
+
}
|
|
702
|
+
/**
|
|
703
|
+
* @description Creates an Angular component instance
|
|
704
|
+
* @summary This static utility method creates an Angular component instance with the specified
|
|
705
|
+
* inputs and template. It uses Angular's component creation API to instantiate the component
|
|
706
|
+
* and then sets the input properties using the provided metadata.
|
|
707
|
+
*
|
|
708
|
+
* @param {Type<unknown>} component - The component type to create
|
|
709
|
+
* @param {KeyValue} [inputs={}] - The input properties to set on the component
|
|
710
|
+
* @param {ComponentMirror<unknown>} metadata - The component metadata for input validation
|
|
711
|
+
* @param {ViewContainerRef} vcr - The view container reference for component creation
|
|
712
|
+
* @param {Injector} injector - The Angular injector for dependency injection
|
|
713
|
+
* @param {any} [template=[]] - The template nodes to project into the component
|
|
714
|
+
* @return {ComponentRef<unknown>} The created component reference
|
|
715
|
+
*/
|
|
716
|
+
static createComponent(component, inputs = {}, metadata, vcr, injector, template = []) {
|
|
717
|
+
const componentInstance = vcr.createComponent(component, {
|
|
718
|
+
environmentInjector: injector,
|
|
719
|
+
projectableNodes: [template],
|
|
720
|
+
});
|
|
721
|
+
this.setInputs(componentInstance, inputs, metadata);
|
|
722
|
+
return componentInstance;
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* @description Extracts decorator metadata from a model
|
|
726
|
+
* @summary This method provides access to the field definition generated from a model's
|
|
727
|
+
* decorators. It's a convenience wrapper around the toFieldDefinition method that
|
|
728
|
+
* converts a model to a field definition based on its decorators and the provided
|
|
729
|
+
* global properties.
|
|
730
|
+
*
|
|
731
|
+
* @param {Model} model - The model to extract decorators from
|
|
732
|
+
* @param {Record<string, unknown>} globalProps - Global properties to include in the field definition
|
|
733
|
+
* @return {FieldDefinition<AngularFieldDefinition>} The field definition generated from the model
|
|
734
|
+
*/
|
|
735
|
+
getDecorators(model, globalProps) {
|
|
736
|
+
return this.toFieldDefinition(model, globalProps);
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* @description Destroys the current engine instance
|
|
740
|
+
* @summary This static method clears the current instance reference, effectively
|
|
741
|
+
* destroying the singleton instance of the rendering engine. This can be used
|
|
742
|
+
* to reset the engine state or to prepare for a new instance creation.
|
|
743
|
+
*
|
|
744
|
+
* @return {Promise<void>} A promise that resolves when the instance is destroyed
|
|
745
|
+
*/
|
|
746
|
+
static async destroy() {
|
|
747
|
+
NgxRenderingEngine2._instance = undefined;
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* @description Renders a model into an Angular component output
|
|
751
|
+
* @summary This method takes a model and converts it to an Angular component output.
|
|
752
|
+
* It first stores a reference to the model, then converts it to a field definition
|
|
753
|
+
* using the base RenderingEngine's toFieldDefinition method, and finally converts
|
|
754
|
+
* that field definition to an Angular component output using fromFieldDefinition.
|
|
755
|
+
*
|
|
756
|
+
* @template M - Type extending Model
|
|
757
|
+
* @param {M} model - The model to render
|
|
758
|
+
* @param {Record<string, unknown>} globalProps - Global properties to pass to the component
|
|
759
|
+
* @param {ViewContainerRef} vcr - The view container reference for component creation
|
|
760
|
+
* @param {Injector} injector - The Angular injector for dependency injection
|
|
761
|
+
* @param {TemplateRef<any>} tpl - The template reference for content projection
|
|
762
|
+
* @return {AngularDynamicOutput} The Angular component output with component reference and inputs
|
|
763
|
+
*
|
|
764
|
+
* @mermaid
|
|
765
|
+
* sequenceDiagram
|
|
766
|
+
* participant Client as Client Code
|
|
767
|
+
* participant Render as render method
|
|
768
|
+
* participant ToField as toFieldDefinition
|
|
769
|
+
* participant FromField as fromFieldDefinition
|
|
770
|
+
*
|
|
771
|
+
* Client->>Render: render(model, globalProps, vcr, injector, tpl)
|
|
772
|
+
* Render->>Render: Store model reference
|
|
773
|
+
* Render->>ToField: toFieldDefinition(model, globalProps)
|
|
774
|
+
* ToField-->>Render: fieldDef
|
|
775
|
+
* Render->>FromField: fromFieldDefinition(fieldDef, vcr, injector, tpl)
|
|
776
|
+
* FromField-->>Render: AngularDynamicOutput
|
|
777
|
+
* Render-->>Client: return AngularDynamicOutput
|
|
778
|
+
*/
|
|
779
|
+
render(model, globalProps, vcr, injector, tpl) {
|
|
780
|
+
let result;
|
|
781
|
+
try {
|
|
782
|
+
this._model = model;
|
|
783
|
+
const formId = Date.now().toString(36).toUpperCase();
|
|
784
|
+
const fieldDef = this.toFieldDefinition(model, globalProps);
|
|
785
|
+
result = this.fromFieldDefinition(fieldDef, vcr, injector, tpl, formId);
|
|
786
|
+
result.instance['formGroup'] = NgxFormService.getControlFromForm(formId);
|
|
787
|
+
NgxFormService.removeRegistry(formId);
|
|
788
|
+
}
|
|
789
|
+
catch (e) {
|
|
790
|
+
throw new InternalError(`Failed to render Model ${model.constructor.name}: ${e}`);
|
|
791
|
+
}
|
|
792
|
+
return result;
|
|
793
|
+
}
|
|
794
|
+
/**
|
|
795
|
+
* @description Initializes the rendering engine
|
|
796
|
+
* @summary This method initializes the rendering engine. It checks if the engine is already initialized
|
|
797
|
+
* and sets the initialized flag to true. This method is called before the engine is used
|
|
798
|
+
* to ensure it's properly set up for rendering operations.
|
|
799
|
+
*
|
|
800
|
+
* @param {...any[]} args - Initialization arguments
|
|
801
|
+
* @return {Promise<void>} A promise that resolves when initialization is complete
|
|
802
|
+
*/
|
|
803
|
+
async initialize(...args) {
|
|
804
|
+
if (this.initialized)
|
|
805
|
+
return;
|
|
806
|
+
// ValidatableByType[]
|
|
807
|
+
this.initialized = true;
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* @description Registers a component with the rendering engine
|
|
811
|
+
* @summary This static method registers a component constructor with the rendering engine
|
|
812
|
+
* under a specific name. It initializes the components registry if needed and throws
|
|
813
|
+
* an error if a component is already registered under the same name to prevent
|
|
814
|
+
* accidental overrides.
|
|
815
|
+
*
|
|
816
|
+
* @param {string} name - The name to register the component under
|
|
817
|
+
* @param {Constructor<unknown>} constructor - The component constructor
|
|
818
|
+
* @return {void}
|
|
819
|
+
*/
|
|
820
|
+
static registerComponent(name, constructor) {
|
|
821
|
+
if (!this._components)
|
|
822
|
+
this._components = {};
|
|
823
|
+
if (name in this._components)
|
|
824
|
+
throw new InternalError(`Component already registered under ${name}`);
|
|
825
|
+
this._components[name] = {
|
|
826
|
+
constructor: constructor,
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
/**
|
|
830
|
+
* @description Retrieves registered components from the rendering engine
|
|
831
|
+
* @summary This static method retrieves either all registered components or a specific component
|
|
832
|
+
* by its selector. When called without a selector, it returns an array of all registered
|
|
833
|
+
* components. When called with a selector, it returns the specific component if found,
|
|
834
|
+
* or throws an error if the component is not registered.
|
|
835
|
+
*
|
|
836
|
+
* @param {string} [selector] - Optional selector to retrieve a specific component
|
|
837
|
+
* @return {Object|Array} Either a specific component or an array of all components
|
|
838
|
+
*/
|
|
839
|
+
static components(selector) {
|
|
840
|
+
if (!selector)
|
|
841
|
+
return Object.values(this._components);
|
|
842
|
+
if (!(selector in this._components))
|
|
843
|
+
throw new InternalError(`No Component registered under ${selector}`);
|
|
844
|
+
return this._components[selector];
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* @description Generates a key for reflection metadata
|
|
848
|
+
* @summary This static method generates a key for reflection metadata by prefixing the input key
|
|
849
|
+
* with the Angular engine's reflection prefix. This is used for storing and retrieving
|
|
850
|
+
* metadata in a namespaced way to avoid conflicts with other metadata.
|
|
851
|
+
*
|
|
852
|
+
* @param {string} key - The base key to prefix
|
|
853
|
+
* @return {string} The prefixed key for reflection metadata
|
|
854
|
+
*/
|
|
855
|
+
static key(key) {
|
|
856
|
+
return `${AngularEngineKeys.REFLECT}${key}`;
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* @description Sets input properties on a component instance
|
|
860
|
+
* @summary This static utility method sets input properties on a component instance
|
|
861
|
+
* based on the provided inputs object and component metadata. It handles both simple
|
|
862
|
+
* values and nested objects, recursively processing object properties. The method
|
|
863
|
+
* validates each input against the component's metadata to ensure only valid inputs
|
|
864
|
+
* are set.
|
|
865
|
+
*
|
|
866
|
+
* @param {ComponentRef<unknown>} component - The component reference to set inputs on
|
|
867
|
+
* @param {KeyValue} inputs - The input properties to set
|
|
868
|
+
* @param {ComponentMirror<unknown>} metadata - The component metadata for input validation
|
|
869
|
+
* @return {void}
|
|
870
|
+
*
|
|
871
|
+
* @mermaid
|
|
872
|
+
* sequenceDiagram
|
|
873
|
+
* participant Caller
|
|
874
|
+
* participant SetInputs as setInputs
|
|
875
|
+
* participant Parse as parseInputValue
|
|
876
|
+
* participant Component as ComponentRef
|
|
877
|
+
*
|
|
878
|
+
* Caller->>SetInputs: setInputs(component, inputs, metadata)
|
|
879
|
+
* SetInputs->>SetInputs: Iterate through inputs
|
|
880
|
+
* loop For each input
|
|
881
|
+
* SetInputs->>SetInputs: Check if input exists in metadata
|
|
882
|
+
* alt Input is 'props'
|
|
883
|
+
* SetInputs->>Parse: parseInputValue(component, value)
|
|
884
|
+
* Parse->>Parse: Recursively process nested objects
|
|
885
|
+
* Parse->>Component: setInput(key, value)
|
|
886
|
+
* else Input is valid
|
|
887
|
+
* SetInputs->>Component: setInput(key, value)
|
|
888
|
+
* end
|
|
889
|
+
* end
|
|
890
|
+
*/
|
|
891
|
+
static setInputs(component, inputs, metadata) {
|
|
892
|
+
function parseInputValue(component, input) {
|
|
893
|
+
Object.keys(input).forEach(key => {
|
|
894
|
+
const value = input[key];
|
|
895
|
+
if (typeof value === 'object' && !!value)
|
|
896
|
+
return parseInputValue(component, value);
|
|
897
|
+
component.setInput(key, value);
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
Object.entries(inputs).forEach(([key, value]) => {
|
|
901
|
+
const prop = metadata.inputs.find((item) => item.propName === key);
|
|
902
|
+
if (prop) {
|
|
903
|
+
if (key === 'props')
|
|
904
|
+
parseInputValue(component, value);
|
|
905
|
+
// if(key === 'locale' && !value)
|
|
906
|
+
// value = getLocaleFromClassName(this._componentName);
|
|
907
|
+
component.setInput(key, value);
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* @description Marks an Angular component as dynamically loadable
|
|
915
|
+
* @summary Decorator that registers an Angular component with the NgxRenderingEngine2 for dynamic loading.
|
|
916
|
+
* This decorator must be applied before the @Component decorator to properly extract component metadata.
|
|
917
|
+
* It adds metadata to the component class and registers it with the rendering engine using its selector.
|
|
918
|
+
* @function Dynamic
|
|
919
|
+
* @return {Function} A decorator function that can be applied to Angular component classes
|
|
920
|
+
* @mermaid
|
|
921
|
+
* sequenceDiagram
|
|
922
|
+
* participant C as Component Class
|
|
923
|
+
* participant D as Dynamic Decorator
|
|
924
|
+
* participant R as NgxRenderingEngine2
|
|
925
|
+
* participant M as Angular Metadata
|
|
926
|
+
* C->>D: Apply decorator
|
|
927
|
+
* D->>M: reflectComponentType()
|
|
928
|
+
* M-->>D: Return component metadata
|
|
929
|
+
* alt No metadata found
|
|
930
|
+
* D->>D: Throw InternalError
|
|
931
|
+
* else Metadata found
|
|
932
|
+
* D->>R: registerComponent(selector, constructor)
|
|
933
|
+
* D->>C: Apply metadata
|
|
934
|
+
* end
|
|
935
|
+
* @category Decorators
|
|
936
|
+
*/
|
|
937
|
+
function Dynamic() {
|
|
938
|
+
return apply((original) => {
|
|
939
|
+
const metadata = reflectComponentType(original);
|
|
940
|
+
if (!metadata)
|
|
941
|
+
throw new InternalError(`Could not find Component metadata. @Dynamic decorator must come above @Component`);
|
|
942
|
+
NgxRenderingEngine2.registerComponent(metadata.selector, original);
|
|
943
|
+
}, metadata(NgxRenderingEngine2.key(AngularEngineKeys.DYNAMIC), true));
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* @description Abstract base class for dynamic Angular modules
|
|
948
|
+
* @summary The DynamicModule serves as a base class for Angular modules that need to be
|
|
949
|
+
* dynamically loaded or configured at runtime. It provides a common type for the rendering
|
|
950
|
+
* engine to identify and work with dynamic modules.
|
|
951
|
+
* @class DynamicModule
|
|
952
|
+
* @example
|
|
953
|
+
* ```typescript
|
|
954
|
+
* @NgModule({
|
|
955
|
+
* declarations: [MyComponent],
|
|
956
|
+
* imports: [CommonModule]
|
|
957
|
+
* })
|
|
958
|
+
* export class MyDynamicModule extends DynamicModule {}
|
|
959
|
+
* ```
|
|
960
|
+
*/
|
|
961
|
+
class DynamicModule {
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/**
|
|
965
|
+
* @description Angular implementation of the RenderingEngine
|
|
966
|
+
* @summary This class extends the base RenderingEngine to provide Angular-specific rendering capabilities.
|
|
967
|
+
* It handles the conversion of field definitions to Angular components and manages component registration.
|
|
968
|
+
* @template AngularFieldDefinition - Type for Angular-specific field definitions
|
|
969
|
+
* @template AngularDynamicOutput - Type for Angular-specific component output
|
|
970
|
+
* @param {Injector} injector - Angular injector for dependency injection
|
|
971
|
+
* @param {ViewContainerRef} vcr - View container reference for component creation
|
|
972
|
+
* @param {TemplateRef<any>} tpl - Template reference for content projection
|
|
973
|
+
* @class NgxRenderingEngine
|
|
974
|
+
* @example
|
|
975
|
+
* ```typescript
|
|
976
|
+
* const engine = new NgxRenderingEngine();
|
|
977
|
+
* engine.initialize();
|
|
978
|
+
* const output = engine.render(myModel, {}, viewContainerRef, injector, templateRef);
|
|
979
|
+
* ```
|
|
980
|
+
* @mermaid
|
|
981
|
+
* sequenceDiagram
|
|
982
|
+
* participant Client
|
|
983
|
+
* participant Engine as NgxRenderingEngine
|
|
984
|
+
* participant Components as RegisteredComponents
|
|
985
|
+
*
|
|
986
|
+
* Client->>Engine: new NgxRenderingEngine()
|
|
987
|
+
* Client->>Engine: initialize()
|
|
988
|
+
* Client->>Engine: render(model, props, vcr, injector, tpl)
|
|
989
|
+
* Engine->>Engine: toFieldDefinition(model, props)
|
|
990
|
+
* Engine->>Engine: fromFieldDefinition(fieldDef, vcr, injector, tpl)
|
|
991
|
+
* Engine->>Components: components(fieldDef.tag)
|
|
992
|
+
* Components-->>Engine: component constructor
|
|
993
|
+
* Engine->>Client: return AngularDynamicOutput
|
|
994
|
+
*/
|
|
995
|
+
class NgxRenderingEngine extends RenderingEngine {
|
|
996
|
+
constructor() {
|
|
997
|
+
super('angular');
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* @description Converts a field definition to an Angular component output
|
|
1001
|
+
* @summary This private method takes a field definition and creates the corresponding Angular component.
|
|
1002
|
+
* It handles component instantiation, input property mapping, and child component rendering.
|
|
1003
|
+
* @param {FieldDefinition<AngularFieldDefinition>} fieldDef - The field definition to convert
|
|
1004
|
+
* @param {ViewContainerRef} vcr - The view container reference for component creation
|
|
1005
|
+
* @param {Injector} injector - The Angular injector for dependency injection
|
|
1006
|
+
* @param {TemplateRef<any>} tpl - The template reference for content projection
|
|
1007
|
+
* @return {AngularDynamicOutput} The Angular component output with component reference and inputs
|
|
1008
|
+
* @mermaid
|
|
1009
|
+
* sequenceDiagram
|
|
1010
|
+
* participant Method as fromFieldDefinition
|
|
1011
|
+
* participant Components as NgxRenderingEngine.components
|
|
1012
|
+
* participant Angular as Angular Core
|
|
1013
|
+
*
|
|
1014
|
+
* Method->>Components: components(fieldDef.tag)
|
|
1015
|
+
* Components-->>Method: component constructor
|
|
1016
|
+
* Method->>Angular: reflectComponentType(component)
|
|
1017
|
+
* Angular-->>Method: componentMetadata
|
|
1018
|
+
* Method->>Method: Check input properties
|
|
1019
|
+
* Method->>Method: Create result object
|
|
1020
|
+
* Method->>Method: Process children if any
|
|
1021
|
+
* Method-->>Caller: return AngularDynamicOutput
|
|
1022
|
+
*/
|
|
1023
|
+
fromFieldDefinition(fieldDef, vcr, injector, tpl) {
|
|
1024
|
+
const component = NgxRenderingEngine.components(fieldDef.tag)
|
|
1025
|
+
.constructor;
|
|
1026
|
+
const componentMetadata = reflectComponentType(component);
|
|
1027
|
+
if (!componentMetadata) {
|
|
1028
|
+
throw new InternalError(`Metadata for component ${fieldDef.tag} not found.`);
|
|
1029
|
+
}
|
|
1030
|
+
const inputs = fieldDef.props;
|
|
1031
|
+
const possibleInputs = componentMetadata.inputs;
|
|
1032
|
+
const inputKeys = Object.keys(inputs);
|
|
1033
|
+
for (const input of possibleInputs) {
|
|
1034
|
+
const index = inputKeys.indexOf(input.propName);
|
|
1035
|
+
if (index !== -1) {
|
|
1036
|
+
inputKeys.splice(index, 1);
|
|
1037
|
+
}
|
|
1038
|
+
if (!inputKeys.length)
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
if (inputKeys.length)
|
|
1042
|
+
console.warn(`Unmapped input properties for component ${fieldDef.tag}: ${inputKeys.join(', ')}`);
|
|
1043
|
+
const result = {
|
|
1044
|
+
component: component,
|
|
1045
|
+
inputs: inputs || {},
|
|
1046
|
+
injector: injector,
|
|
1047
|
+
};
|
|
1048
|
+
if (fieldDef.rendererId) {
|
|
1049
|
+
result.inputs['rendererId'] =
|
|
1050
|
+
fieldDef.rendererId;
|
|
1051
|
+
}
|
|
1052
|
+
if (fieldDef.children && fieldDef.children.length) {
|
|
1053
|
+
result.children = fieldDef.children.map((child) => {
|
|
1054
|
+
return this.fromFieldDefinition(child, vcr, injector, tpl);
|
|
1055
|
+
});
|
|
1056
|
+
const template = vcr.createEmbeddedView(tpl, injector).rootNodes;
|
|
1057
|
+
result.content = [template];
|
|
1058
|
+
}
|
|
1059
|
+
return result;
|
|
1060
|
+
}
|
|
1061
|
+
/**
|
|
1062
|
+
* @description Renders a model into an Angular component output
|
|
1063
|
+
* @summary This method takes a model and converts it to an Angular component output.
|
|
1064
|
+
* It first converts the model to a field definition using the base RenderingEngine's
|
|
1065
|
+
* toFieldDefinition method, then converts that field definition to an Angular component output.
|
|
1066
|
+
* @template M - Type extending Model
|
|
1067
|
+
* @param {M} model - The model to render
|
|
1068
|
+
* @param {Record<string, unknown>} globalProps - Global properties to pass to the component
|
|
1069
|
+
* @param {ViewContainerRef} vcr - The view container reference for component creation
|
|
1070
|
+
* @param {Injector} injector - The Angular injector for dependency injection
|
|
1071
|
+
* @param {TemplateRef<any>} tpl - The template reference for content projection
|
|
1072
|
+
* @return {AngularDynamicOutput} The Angular component output with component reference and inputs
|
|
1073
|
+
* @mermaid
|
|
1074
|
+
* sequenceDiagram
|
|
1075
|
+
* participant Client as Client Code
|
|
1076
|
+
* participant Render as render method
|
|
1077
|
+
* participant ToField as toFieldDefinition
|
|
1078
|
+
* participant FromField as fromFieldDefinition
|
|
1079
|
+
*
|
|
1080
|
+
* Client->>Render: render(model, globalProps, vcr, injector, tpl)
|
|
1081
|
+
* Render->>ToField: toFieldDefinition(model, globalProps)
|
|
1082
|
+
* ToField-->>Render: fieldDef
|
|
1083
|
+
* Render->>FromField: fromFieldDefinition(fieldDef, vcr, injector, tpl)
|
|
1084
|
+
* FromField-->>Render: AngularDynamicOutput
|
|
1085
|
+
* Render-->>Client: return AngularDynamicOutput
|
|
1086
|
+
*/
|
|
1087
|
+
render(model, globalProps, vcr, injector, tpl) {
|
|
1088
|
+
let result;
|
|
1089
|
+
try {
|
|
1090
|
+
const fieldDef = this.toFieldDefinition(model, globalProps);
|
|
1091
|
+
result = this.fromFieldDefinition(fieldDef, vcr, injector, tpl);
|
|
1092
|
+
}
|
|
1093
|
+
catch (e) {
|
|
1094
|
+
throw new InternalError(`Failed to render Model ${model.constructor.name}: ${e}`);
|
|
1095
|
+
}
|
|
1096
|
+
return result;
|
|
1097
|
+
}
|
|
1098
|
+
/**
|
|
1099
|
+
* @description Initializes the rendering engine
|
|
1100
|
+
* @summary This method initializes the rendering engine. It checks if the engine is already initialized
|
|
1101
|
+
* and sets the initialized flag to true. This method is called before the engine is used.
|
|
1102
|
+
* @param {...any[]} args - Initialization arguments
|
|
1103
|
+
* @return {Promise<void>} A promise that resolves when initialization is complete
|
|
1104
|
+
*/
|
|
1105
|
+
async initialize(...args) {
|
|
1106
|
+
if (this.initialized)
|
|
1107
|
+
return;
|
|
1108
|
+
// ValidatableByType[]
|
|
1109
|
+
this.initialized = true;
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* @description Registers a component with the rendering engine
|
|
1113
|
+
* @summary This static method registers a component constructor with the rendering engine
|
|
1114
|
+
* under a specific name. It throws an error if a component is already registered under the same name.
|
|
1115
|
+
* @param {string} name - The name to register the component under
|
|
1116
|
+
* @param {Constructor<unknown>} constructor - The component constructor
|
|
1117
|
+
* @return {void}
|
|
1118
|
+
*/
|
|
1119
|
+
static registerComponent(name, constructor) {
|
|
1120
|
+
if (!this._components)
|
|
1121
|
+
this._components = {};
|
|
1122
|
+
if (name in this._components)
|
|
1123
|
+
throw new InternalError(`Component already registered under ${name}`);
|
|
1124
|
+
this._components[name] = {
|
|
1125
|
+
constructor: constructor,
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
/**
|
|
1129
|
+
* @description Retrieves registered components from the rendering engine
|
|
1130
|
+
* @summary This static method retrieves either all registered components or a specific component
|
|
1131
|
+
* by its selector. It throws an error if the requested component is not registered.
|
|
1132
|
+
* @param {string} [selector] - Optional selector to retrieve a specific component
|
|
1133
|
+
* @return {Object|Array} Either a specific component or an array of all components
|
|
1134
|
+
*/
|
|
1135
|
+
static components(selector) {
|
|
1136
|
+
if (!selector)
|
|
1137
|
+
return Object.values(this._components);
|
|
1138
|
+
if (!(selector in this._components))
|
|
1139
|
+
throw new InternalError(`No Component registered under ${selector}`);
|
|
1140
|
+
return this._components[selector];
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* @description Generates a key for reflection metadata
|
|
1144
|
+
* @summary This static method generates a key for reflection metadata by prefixing the input key
|
|
1145
|
+
* with the Angular engine's reflection prefix. This is used for storing and retrieving metadata.
|
|
1146
|
+
* @param {string} key - The base key to prefix
|
|
1147
|
+
* @return {string} The prefixed key for reflection metadata
|
|
1148
|
+
*/
|
|
1149
|
+
static key(key) {
|
|
1150
|
+
return `${AngularEngineKeys.REFLECT}${key}`;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
/**
|
|
1155
|
+
* @module engine
|
|
1156
|
+
* @description Angular rendering engine for Decaf applications
|
|
1157
|
+
* @summary The engine module provides core functionality for rendering Angular components
|
|
1158
|
+
* in Decaf applications. It includes constants, decorators, rendering engines, and utility types
|
|
1159
|
+
* that enable dynamic component creation, property mapping, and component lifecycle management.
|
|
1160
|
+
* Key exports include {@link NgxRenderingEngine}, {@link DynamicModule}, and various decorators
|
|
1161
|
+
* for component configuration.
|
|
1162
|
+
*/
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* @description Dynamic component renderer for Decaf Angular applications.
|
|
1166
|
+
* @summary This component provides a flexible way to dynamically render Angular components
|
|
1167
|
+
* at runtime based on a tag name. It handles the creation, property binding, and event
|
|
1168
|
+
* subscription for dynamically loaded components. This is particularly useful for
|
|
1169
|
+
* building configurable UIs where components need to be determined at runtime.
|
|
1170
|
+
*
|
|
1171
|
+
* @component {ComponentRendererComponent}
|
|
1172
|
+
* @example
|
|
1173
|
+
* <ngx-decaf-component-renderer
|
|
1174
|
+
* [tag]="tag"
|
|
1175
|
+
* [globals]="globals"
|
|
1176
|
+
* (listenEvent)="listenEvent($event)">
|
|
1177
|
+
* </ngx-decaf-component-renderer>
|
|
1178
|
+
*
|
|
1179
|
+
* @mermaid
|
|
1180
|
+
* classDiagram
|
|
1181
|
+
* class ComponentRendererComponent {
|
|
1182
|
+
* +ViewContainerRef vcr
|
|
1183
|
+
* +string tag
|
|
1184
|
+
* +Record~string, unknown~ globals
|
|
1185
|
+
* +EnvironmentInjector injector
|
|
1186
|
+
* +ComponentRef~unknown~ component
|
|
1187
|
+
* +EventEmitter~ModelRenderCustomEvent~ listenEvent
|
|
1188
|
+
* +ngOnInit()
|
|
1189
|
+
* +ngOnDestroy()
|
|
1190
|
+
* +ngOnChanges(changes)
|
|
1191
|
+
* -createComponent(tag, globals)
|
|
1192
|
+
* -subscribeEvents()
|
|
1193
|
+
* -unsubscribeEvents()
|
|
1194
|
+
* }
|
|
1195
|
+
* ComponentRendererComponent --|> OnInit
|
|
1196
|
+
* ComponentRendererComponent --|> OnChanges
|
|
1197
|
+
* ComponentRendererComponent --|> OnDestroy
|
|
1198
|
+
*
|
|
1199
|
+
* @implements {OnInit}
|
|
1200
|
+
* @implements {OnChanges}
|
|
1201
|
+
* @implements {OnDestroy}
|
|
1202
|
+
*/
|
|
1203
|
+
class ComponentRendererComponent {
|
|
1204
|
+
/**
|
|
1205
|
+
* @description Creates an instance of ComponentRendererComponent.
|
|
1206
|
+
* @summary Initializes a new ComponentRendererComponent. This component doesn't require
|
|
1207
|
+
* any dependencies to be injected in its constructor as it uses the inject function to
|
|
1208
|
+
* obtain the EnvironmentInjector.
|
|
1209
|
+
*
|
|
1210
|
+
* @memberOf ComponentRendererComponent
|
|
1211
|
+
*/
|
|
1212
|
+
constructor() {
|
|
1213
|
+
/**
|
|
1214
|
+
* @description Global properties to pass to the rendered component.
|
|
1215
|
+
* @summary This input property allows passing a set of properties to the dynamically
|
|
1216
|
+
* rendered component. These properties will be mapped to the component's inputs if they
|
|
1217
|
+
* match. Properties that don't match any input on the target component will be filtered out
|
|
1218
|
+
* with a warning.
|
|
1219
|
+
*
|
|
1220
|
+
* @type {Record<string, unknown>}
|
|
1221
|
+
* @default {}
|
|
1222
|
+
* @memberOf ComponentRendererComponent
|
|
1223
|
+
*/
|
|
1224
|
+
this.globals = {};
|
|
1225
|
+
/**
|
|
1226
|
+
* @description Injector used for dependency injection in the dynamic component.
|
|
1227
|
+
* @summary This injector is used when creating the dynamic component to provide it with
|
|
1228
|
+
* access to the application's dependency injection system. It ensures that the dynamically
|
|
1229
|
+
* created component can access the same services and dependencies as statically created
|
|
1230
|
+
* components.
|
|
1231
|
+
*
|
|
1232
|
+
* @type {EnvironmentInjector}
|
|
1233
|
+
* @memberOf ComponentRendererComponent
|
|
1234
|
+
*/
|
|
1235
|
+
this.injector = inject(EnvironmentInjector);
|
|
1236
|
+
/**
|
|
1237
|
+
* @description Event emitter for events from the rendered component.
|
|
1238
|
+
* @summary This output property emits events that originate from the dynamically rendered
|
|
1239
|
+
* component. It allows the parent component to listen for and respond to events from the
|
|
1240
|
+
* dynamic component, creating a communication channel between the parent and the dynamically
|
|
1241
|
+
* rendered child.
|
|
1242
|
+
*
|
|
1243
|
+
* @type {EventEmitter<ModelRenderCustomEvent>}
|
|
1244
|
+
* @memberOf ComponentRendererComponent
|
|
1245
|
+
*/
|
|
1246
|
+
this.listenEvent = new EventEmitter();
|
|
1247
|
+
this.parent = undefined;
|
|
1248
|
+
this.logger = getLogger(this);
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* @description Initializes the component after Angular first displays the data-bound properties.
|
|
1252
|
+
* @summary Sets up the component by creating the dynamic component specified by the tag input.
|
|
1253
|
+
* This method is called once when the component is initialized and triggers the dynamic
|
|
1254
|
+
* component creation process with the provided tag name and global properties.
|
|
1255
|
+
*
|
|
1256
|
+
* @mermaid
|
|
1257
|
+
* sequenceDiagram
|
|
1258
|
+
* participant A as Angular Lifecycle
|
|
1259
|
+
* participant C as ComponentRendererComponent
|
|
1260
|
+
* participant R as NgxRenderingEngine2
|
|
1261
|
+
*
|
|
1262
|
+
* A->>C: ngOnInit()
|
|
1263
|
+
* C->>C: createComponent(tag, globals)
|
|
1264
|
+
* C->>R: components(tag)
|
|
1265
|
+
* R-->>C: Return component constructor
|
|
1266
|
+
* C->>C: Process component inputs
|
|
1267
|
+
* C->>C: Create component instance
|
|
1268
|
+
* C->>C: subscribeEvents()
|
|
1269
|
+
*
|
|
1270
|
+
* @return {void}
|
|
1271
|
+
* @memberOf ComponentRendererComponent
|
|
1272
|
+
*/
|
|
1273
|
+
ngOnInit() {
|
|
1274
|
+
if (!this.parent)
|
|
1275
|
+
this.createComponent(this.tag, this.globals);
|
|
1276
|
+
this.createParentComponent();
|
|
1277
|
+
}
|
|
1278
|
+
/**
|
|
1279
|
+
* @description Cleans up resources when the component is destroyed.
|
|
1280
|
+
* @summary Performs cleanup operations when the component is being destroyed by Angular.
|
|
1281
|
+
* This includes unsubscribing from all event emitters of the dynamic component and
|
|
1282
|
+
* destroying the rendering engine instance to prevent memory leaks.
|
|
1283
|
+
*
|
|
1284
|
+
* @mermaid
|
|
1285
|
+
* sequenceDiagram
|
|
1286
|
+
* participant A as Angular Lifecycle
|
|
1287
|
+
* participant C as ComponentRendererComponent
|
|
1288
|
+
* participant R as NgxRenderingEngine2
|
|
1289
|
+
*
|
|
1290
|
+
* A->>C: ngOnDestroy()
|
|
1291
|
+
* alt component exists
|
|
1292
|
+
* C->>C: unsubscribeEvents()
|
|
1293
|
+
* C->>R: destroy()
|
|
1294
|
+
* end
|
|
1295
|
+
*
|
|
1296
|
+
* @return {Promise<void>} A promise that resolves when cleanup is complete
|
|
1297
|
+
* @memberOf ComponentRendererComponent
|
|
1298
|
+
*/
|
|
1299
|
+
async ngOnDestroy() {
|
|
1300
|
+
if (this.component) {
|
|
1301
|
+
this.unsubscribeEvents();
|
|
1302
|
+
NgxRenderingEngine2$1.destroy();
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
/**
|
|
1306
|
+
* @description Creates and renders a dynamic component.
|
|
1307
|
+
* @summary This method handles the creation of a dynamic component based on the provided tag.
|
|
1308
|
+
* It retrieves the component constructor from the rendering engine, processes its inputs,
|
|
1309
|
+
* filters out unmapped properties, creates the component instance, and sets up event subscriptions.
|
|
1310
|
+
*
|
|
1311
|
+
* @param {string} tag - The tag name of the component to create
|
|
1312
|
+
* @param {KeyValue} globals - Global properties to pass to the component
|
|
1313
|
+
* @return {void}
|
|
1314
|
+
*
|
|
1315
|
+
* @mermaid
|
|
1316
|
+
* sequenceDiagram
|
|
1317
|
+
* participant C as ComponentRendererComponent
|
|
1318
|
+
* participant R as NgxRenderingEngine2
|
|
1319
|
+
* participant V as ViewContainerRef
|
|
1320
|
+
*
|
|
1321
|
+
* C->>R: components(tag)
|
|
1322
|
+
* R-->>C: Return component constructor
|
|
1323
|
+
* C->>C: reflectComponentType(component)
|
|
1324
|
+
* C->>C: Process input properties
|
|
1325
|
+
* C->>C: Filter unmapped properties
|
|
1326
|
+
* C->>V: clear()
|
|
1327
|
+
* C->>R: createComponent(component, props, metadata, vcr, injector, [])
|
|
1328
|
+
* R-->>C: Return component reference
|
|
1329
|
+
* C->>C: subscribeEvents()
|
|
1330
|
+
*
|
|
1331
|
+
* @private
|
|
1332
|
+
* @memberOf ComponentRendererComponent
|
|
1333
|
+
*/
|
|
1334
|
+
createComponent(tag, globals = {}) {
|
|
1335
|
+
const component = NgxRenderingEngine2$1.components(tag)
|
|
1336
|
+
?.constructor;
|
|
1337
|
+
const metadata = reflectComponentType(component);
|
|
1338
|
+
const componentInputs = metadata.inputs;
|
|
1339
|
+
const props = globals?.['item'];
|
|
1340
|
+
delete props['tag'];
|
|
1341
|
+
const inputKeys = Object.keys(props);
|
|
1342
|
+
const unmappedKeys = [];
|
|
1343
|
+
for (const input of inputKeys) {
|
|
1344
|
+
if (!inputKeys.length)
|
|
1345
|
+
break;
|
|
1346
|
+
const prop = componentInputs.find((item) => item.propName === input);
|
|
1347
|
+
if (!prop) {
|
|
1348
|
+
delete props[input];
|
|
1349
|
+
unmappedKeys.push(input);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
if (unmappedKeys.length)
|
|
1353
|
+
this.logger.info(`Unmapped input properties for component ${tag}: ${unmappedKeys.join(', ')}`);
|
|
1354
|
+
this.vcr.clear();
|
|
1355
|
+
this.component = NgxRenderingEngine2$1.createComponent(component, props, metadata, this.vcr, this.injector, []);
|
|
1356
|
+
this.subscribeEvents();
|
|
1357
|
+
}
|
|
1358
|
+
createParentComponent() {
|
|
1359
|
+
const { component, inputs } = this.parent;
|
|
1360
|
+
const metadata = reflectComponentType(component);
|
|
1361
|
+
const template = this.vcr.createEmbeddedView(this.inner, this.injector).rootNodes;
|
|
1362
|
+
this.component = NgxRenderingEngine2$1.createComponent(component, inputs, metadata, this.vcr, this.injector, template);
|
|
1363
|
+
this.subscribeEvents();
|
|
1364
|
+
}
|
|
1365
|
+
/**
|
|
1366
|
+
* @description Subscribes to events emitted by the dynamic component.
|
|
1367
|
+
* @summary This method sets up subscriptions to all EventEmitter properties of the
|
|
1368
|
+
* dynamically created component. When an event is emitted by the dynamic component,
|
|
1369
|
+
* it is captured and re-emitted through the listenEvent output property with additional
|
|
1370
|
+
* metadata about the event source.
|
|
1371
|
+
*
|
|
1372
|
+
* @mermaid
|
|
1373
|
+
* sequenceDiagram
|
|
1374
|
+
* participant C as ComponentRendererComponent
|
|
1375
|
+
* participant D as Dynamic Component
|
|
1376
|
+
* participant P as Parent Component
|
|
1377
|
+
*
|
|
1378
|
+
* C->>C: subscribeEvents()
|
|
1379
|
+
* C->>D: Get instance properties
|
|
1380
|
+
* loop For each property
|
|
1381
|
+
* C->>C: Check if property is EventEmitter
|
|
1382
|
+
* alt is EventEmitter
|
|
1383
|
+
* C->>D: Subscribe to event
|
|
1384
|
+
* D-->>C: Event emitted
|
|
1385
|
+
* C->>P: Re-emit event with metadata
|
|
1386
|
+
* end
|
|
1387
|
+
* end
|
|
1388
|
+
*
|
|
1389
|
+
* @private
|
|
1390
|
+
* @return {void}
|
|
1391
|
+
* @memberOf ComponentRendererComponent
|
|
1392
|
+
*/
|
|
1393
|
+
subscribeEvents() {
|
|
1394
|
+
if (this.component) {
|
|
1395
|
+
const instance = this.component?.instance;
|
|
1396
|
+
const componentKeys = Object.keys(instance);
|
|
1397
|
+
for (const key of componentKeys) {
|
|
1398
|
+
const value = instance[key];
|
|
1399
|
+
if (value instanceof EventEmitter)
|
|
1400
|
+
instance[key].subscribe((event) => {
|
|
1401
|
+
this.listenEvent.emit({
|
|
1402
|
+
name: key,
|
|
1403
|
+
...event,
|
|
1404
|
+
});
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
/**
|
|
1410
|
+
* @description Unsubscribes from all events of the dynamic component.
|
|
1411
|
+
* @summary This method cleans up event subscriptions when the component is being destroyed.
|
|
1412
|
+
* It iterates through all properties of the dynamic component instance and unsubscribes
|
|
1413
|
+
* from any EventEmitter properties to prevent memory leaks and unexpected behavior after
|
|
1414
|
+
* the component is destroyed.
|
|
1415
|
+
*
|
|
1416
|
+
* @mermaid
|
|
1417
|
+
* sequenceDiagram
|
|
1418
|
+
* participant C as ComponentRendererComponent
|
|
1419
|
+
* participant D as Dynamic Component
|
|
1420
|
+
*
|
|
1421
|
+
* C->>C: unsubscribeEvents()
|
|
1422
|
+
* C->>D: Get instance properties
|
|
1423
|
+
* loop For each property
|
|
1424
|
+
* C->>C: Check if property is EventEmitter
|
|
1425
|
+
* alt is EventEmitter
|
|
1426
|
+
* C->>D: Unsubscribe from event
|
|
1427
|
+
* end
|
|
1428
|
+
* end
|
|
1429
|
+
*
|
|
1430
|
+
* @private
|
|
1431
|
+
* @return {void}
|
|
1432
|
+
* @memberOf ComponentRendererComponent
|
|
1433
|
+
*/
|
|
1434
|
+
unsubscribeEvents() {
|
|
1435
|
+
if (this.component) {
|
|
1436
|
+
const instance = this.component?.instance;
|
|
1437
|
+
const componentKeys = Object.keys(instance);
|
|
1438
|
+
for (const key of componentKeys) {
|
|
1439
|
+
const value = instance[key];
|
|
1440
|
+
if (value instanceof EventEmitter)
|
|
1441
|
+
instance[key].unsubscribe();
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ComponentRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1446
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: ComponentRendererComponent, isStandalone: true, selector: "ngx-decaf-component-renderer", inputs: { tag: "tag", globals: "globals", parent: "parent" }, outputs: { listenEvent: "listenEvent" }, viewQueries: [{ propertyName: "vcr", first: true, predicate: ["componentViewContainer"], descendants: true, read: ViewContainerRef, static: true }, { propertyName: "inner", first: true, predicate: ["inner"], descendants: true, read: TemplateRef, static: true }], ngImport: i0, template: "<ng-template #componentViewContainer></ng-template>\n\n<ng-template #inner>\n @if(parent?.children?.length) {\n @for(child of parent.children; track child) {\n @if(!child.children?.length) {\n <ng-container\n *ngComponentOutlet=\"\n child.component;\n injector: child.injector;\n inputs: child.inputs;\n content:child.content;\n \"\n />\n } @else {\n <ngx-decaf-component-renderer [parent]=\"child\"> </ngx-decaf-component-renderer>\n }\n }\n }\n</ng-template>\n\n\n", styles: [""], dependencies: [{ kind: "component", type: ComponentRendererComponent, selector: "ngx-decaf-component-renderer", inputs: ["tag", "globals", "parent"], outputs: ["listenEvent"] }, { kind: "ngmodule", type: ForAngularModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }] }); }
|
|
1447
|
+
}
|
|
1448
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ComponentRendererComponent, decorators: [{
|
|
1449
|
+
type: Component,
|
|
1450
|
+
args: [{ selector: 'ngx-decaf-component-renderer', imports: [ForAngularModule], standalone: true, template: "<ng-template #componentViewContainer></ng-template>\n\n<ng-template #inner>\n @if(parent?.children?.length) {\n @for(child of parent.children; track child) {\n @if(!child.children?.length) {\n <ng-container\n *ngComponentOutlet=\"\n child.component;\n injector: child.injector;\n inputs: child.inputs;\n content:child.content;\n \"\n />\n } @else {\n <ngx-decaf-component-renderer [parent]=\"child\"> </ngx-decaf-component-renderer>\n }\n }\n }\n</ng-template>\n\n\n" }]
|
|
1451
|
+
}], ctorParameters: () => [], propDecorators: { vcr: [{
|
|
1452
|
+
type: ViewChild,
|
|
1453
|
+
args: ['componentViewContainer', { static: true, read: ViewContainerRef }]
|
|
1454
|
+
}], tag: [{
|
|
1455
|
+
type: Input,
|
|
1456
|
+
args: [{ required: true }]
|
|
1457
|
+
}], globals: [{
|
|
1458
|
+
type: Input
|
|
1459
|
+
}], listenEvent: [{
|
|
1460
|
+
type: Output
|
|
1461
|
+
}], parent: [{
|
|
1462
|
+
type: Input
|
|
1463
|
+
}], inner: [{
|
|
1464
|
+
type: ViewChild,
|
|
1465
|
+
args: ['inner', { read: TemplateRef, static: true }]
|
|
1466
|
+
}] } });
|
|
1467
|
+
|
|
1468
|
+
/**
|
|
1469
|
+
* @description Component for rendering dynamic models
|
|
1470
|
+
* @summary This component is responsible for dynamically rendering models,
|
|
1471
|
+
* handling model changes, and managing event subscriptions for the rendered components.
|
|
1472
|
+
* It uses the NgxRenderingEngine2 to render the models and supports both string and Model inputs.
|
|
1473
|
+
* @class
|
|
1474
|
+
* @template M - Type extending Model
|
|
1475
|
+
* @param {Injector} injector - Angular Injector for dependency injection
|
|
1476
|
+
* @example
|
|
1477
|
+
* <ngx-decaf-model-renderer
|
|
1478
|
+
* [model]="myModel"
|
|
1479
|
+
* [globals]="globalVariables"
|
|
1480
|
+
* (listenEvent)="handleEvent($event)">
|
|
1481
|
+
* </ngx-decaf-model-renderer>
|
|
1482
|
+
* @mermaid
|
|
1483
|
+
* sequenceDiagram
|
|
1484
|
+
* participant App
|
|
1485
|
+
* participant ModelRenderer
|
|
1486
|
+
* participant RenderingEngine
|
|
1487
|
+
* participant Model
|
|
1488
|
+
* App->>ModelRenderer: Input model
|
|
1489
|
+
* ModelRenderer->>Model: Parse if string
|
|
1490
|
+
* Model-->>ModelRenderer: Parsed model
|
|
1491
|
+
* ModelRenderer->>RenderingEngine: Render model
|
|
1492
|
+
* RenderingEngine-->>ModelRenderer: Rendered output
|
|
1493
|
+
* ModelRenderer->>ModelRenderer: Subscribe to events
|
|
1494
|
+
* ModelRenderer-->>App: Emit events
|
|
1495
|
+
*/
|
|
1496
|
+
class ModelRendererComponent {
|
|
1497
|
+
constructor(injector) {
|
|
1498
|
+
this.injector = injector;
|
|
1499
|
+
/**
|
|
1500
|
+
* @description Global variables to be passed to the rendered component
|
|
1501
|
+
*/
|
|
1502
|
+
this.globals = {};
|
|
1503
|
+
/**
|
|
1504
|
+
* @description Event emitter for custom events from the rendered component
|
|
1505
|
+
*/
|
|
1506
|
+
this.listenEvent = new EventEmitter();
|
|
1507
|
+
this.JSON = JSON;
|
|
1508
|
+
}
|
|
1509
|
+
/**
|
|
1510
|
+
* @description Refreshes the rendered model
|
|
1511
|
+
* @param {string | M} model - The model to be rendered
|
|
1512
|
+
*/
|
|
1513
|
+
refresh(model) {
|
|
1514
|
+
model =
|
|
1515
|
+
typeof model === 'string'
|
|
1516
|
+
? Model.build({}, JSON.parse(model))
|
|
1517
|
+
: model;
|
|
1518
|
+
this.output = model.render(this.globals || {}, this.vcr, this.injector, this.inner);
|
|
1519
|
+
if (this.output?.inputs)
|
|
1520
|
+
this.rendererId = sf(AngularEngineKeys.RENDERED_ID, this.output.inputs['rendererId']);
|
|
1521
|
+
this.instance = this.output?.instance;
|
|
1522
|
+
this.subscribeEvents();
|
|
1523
|
+
}
|
|
1524
|
+
/**
|
|
1525
|
+
* @description Lifecycle hook that is called when data-bound properties of a directive change
|
|
1526
|
+
* @param {SimpleChanges} changes - Object containing changes
|
|
1527
|
+
*/
|
|
1528
|
+
ngOnChanges(changes) {
|
|
1529
|
+
if (changes[BaseComponentProps.MODEL]) {
|
|
1530
|
+
const { currentValue, previousValue, firstChange } = changes[BaseComponentProps.MODEL];
|
|
1531
|
+
this.refresh(currentValue);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
/**
|
|
1535
|
+
* @description Lifecycle hook that is called when a directive, pipe, or service is destroyed
|
|
1536
|
+
* @return {Promise<void>}
|
|
1537
|
+
*/
|
|
1538
|
+
async ngOnDestroy() {
|
|
1539
|
+
if (this.instance) {
|
|
1540
|
+
this.unsubscribeEvents();
|
|
1541
|
+
await NgxRenderingEngine2.destroy();
|
|
1542
|
+
}
|
|
1543
|
+
this.output = undefined;
|
|
1544
|
+
}
|
|
1545
|
+
subscribeEvents() {
|
|
1546
|
+
if (this.instance) {
|
|
1547
|
+
const componentKeys = Object.keys(this.instance);
|
|
1548
|
+
for (const key of componentKeys) {
|
|
1549
|
+
const value = this.instance[key];
|
|
1550
|
+
if (value instanceof EventEmitter)
|
|
1551
|
+
this.instance[key].subscribe((event) => {
|
|
1552
|
+
this.listenEvent.emit({
|
|
1553
|
+
component: this.output?.component.name || '',
|
|
1554
|
+
name: key,
|
|
1555
|
+
...event,
|
|
1556
|
+
});
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
/**
|
|
1562
|
+
* @description Unsubscribes from events emitted by the rendered component
|
|
1563
|
+
*/
|
|
1564
|
+
unsubscribeEvents() {
|
|
1565
|
+
if (this.instance) {
|
|
1566
|
+
const componentKeys = Object.keys(this.instance);
|
|
1567
|
+
for (const key of componentKeys) {
|
|
1568
|
+
const value = this.instance[key];
|
|
1569
|
+
if (value instanceof EventEmitter)
|
|
1570
|
+
this.instance[key].unsubscribe();
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ModelRendererComponent, deps: [{ token: i0.Injector }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1575
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: ModelRendererComponent, isStandalone: true, selector: "ngx-decaf-model-renderer", inputs: { model: "model", globals: "globals", rendererId: "rendererId" }, outputs: { listenEvent: "listenEvent" }, viewQueries: [{ propertyName: "inner", first: true, predicate: ["inner"], descendants: true, read: TemplateRef, static: true }, { propertyName: "vcr", first: true, predicate: ["componentOuter"], descendants: true, read: ViewContainerRef, static: true }], usesOnChanges: true, ngImport: i0, template: " <ng-template #componentOuter></ng-template>\n <ng-template #inner>\n <div [id]=\"rendererId || null\">\n @for (child of output?.children; track child) {\n @if(child?.children?.length) {\n <ngx-decaf-component-renderer [parent]=\"child\" />\n } @else {\n <ng-container\n #childComponents\n *ngComponentOutlet=\"\n child.component;\n injector: child.injector;\n inputs: child.inputs;\n content:child.content;\n \"\n />\n }\n }\n </div>\n </ng-template>\n", styles: [""], dependencies: [{ kind: "ngmodule", type: ForAngularModule }, { kind: "directive", type: i1.NgComponentOutlet, selector: "[ngComponentOutlet]", inputs: ["ngComponentOutlet", "ngComponentOutletInputs", "ngComponentOutletInjector", "ngComponentOutletContent", "ngComponentOutletNgModule", "ngComponentOutletNgModuleFactory"] }, { kind: "component", type: ComponentRendererComponent, selector: "ngx-decaf-component-renderer", inputs: ["tag", "globals", "parent"], outputs: ["listenEvent"] }] }); }
|
|
1576
|
+
}
|
|
1577
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: ModelRendererComponent, decorators: [{
|
|
1578
|
+
type: Component,
|
|
1579
|
+
args: [{ standalone: true, imports: [ForAngularModule, NgComponentOutlet, ComponentRendererComponent], selector: 'ngx-decaf-model-renderer', template: " <ng-template #componentOuter></ng-template>\n <ng-template #inner>\n <div [id]=\"rendererId || null\">\n @for (child of output?.children; track child) {\n @if(child?.children?.length) {\n <ngx-decaf-component-renderer [parent]=\"child\" />\n } @else {\n <ng-container\n #childComponents\n *ngComponentOutlet=\"\n child.component;\n injector: child.injector;\n inputs: child.inputs;\n content:child.content;\n \"\n />\n }\n }\n </div>\n </ng-template>\n" }]
|
|
1580
|
+
}], ctorParameters: () => [{ type: i0.Injector }], propDecorators: { model: [{
|
|
1581
|
+
type: Input,
|
|
1582
|
+
args: [{ required: true }]
|
|
1583
|
+
}], globals: [{
|
|
1584
|
+
type: Input
|
|
1585
|
+
}], inner: [{
|
|
1586
|
+
type: ViewChild,
|
|
1587
|
+
args: ['inner', { read: TemplateRef, static: true }]
|
|
1588
|
+
}], rendererId: [{
|
|
1589
|
+
type: Input
|
|
1590
|
+
}], vcr: [{
|
|
1591
|
+
type: ViewChild,
|
|
1592
|
+
args: ['componentOuter', { static: true, read: ViewContainerRef }]
|
|
1593
|
+
}], listenEvent: [{
|
|
1594
|
+
type: Output
|
|
1595
|
+
}] } });
|
|
1596
|
+
|
|
1597
|
+
/**
|
|
1598
|
+
* @class NgxCrudFormField
|
|
1599
|
+
* @implements {FieldProperties}
|
|
1600
|
+
* @implements {ControlValueAccessor}
|
|
1601
|
+
* @summary Abstract class representing a CRUD form field for Angular applications
|
|
1602
|
+
* @description This class provides the base implementation for CRUD form fields in Angular,
|
|
1603
|
+
* implementing both CrudFormField and ControlValueAccessor interfaces.
|
|
1604
|
+
*/
|
|
1605
|
+
class NgxCrudFormField {
|
|
1606
|
+
constructor(elementRef) {
|
|
1607
|
+
this.elementRef = elementRef;
|
|
1608
|
+
/**
|
|
1609
|
+
* @summary String formatting function
|
|
1610
|
+
* @description Provides access to the sf function for error message formatting
|
|
1611
|
+
* @prop {function(string, ...string): string} sf - String formatting function
|
|
1612
|
+
*/
|
|
1613
|
+
this.sf = sf;
|
|
1614
|
+
/**
|
|
1615
|
+
* @summary Change callback function
|
|
1616
|
+
* @description Function called when the field value changes
|
|
1617
|
+
* @property {function(): unknown} onChange - onChange event handler
|
|
1618
|
+
*/
|
|
1619
|
+
this.onChange = () => {
|
|
1620
|
+
};
|
|
1621
|
+
/**
|
|
1622
|
+
* @summary Touch callback function
|
|
1623
|
+
* @description Function called when the field is touched
|
|
1624
|
+
* @property {function(): unknown} onTouch - onTouch event handler
|
|
1625
|
+
*/
|
|
1626
|
+
this.onTouch = () => {
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
/**
|
|
1630
|
+
* @summary Write value to the field
|
|
1631
|
+
* @description Sets the value of the field
|
|
1632
|
+
* @param {string} obj - The value to be set
|
|
1633
|
+
*/
|
|
1634
|
+
writeValue(obj) {
|
|
1635
|
+
this.value = obj;
|
|
1636
|
+
}
|
|
1637
|
+
/**
|
|
1638
|
+
* @summary Register change callback
|
|
1639
|
+
* @description Registers a function to be called when the field value changes
|
|
1640
|
+
* @param {function(): unknown} fn - The function to be called on change
|
|
1641
|
+
*/
|
|
1642
|
+
registerOnChange(fn) {
|
|
1643
|
+
this.onChange = fn;
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* @summary Register touch callback
|
|
1647
|
+
* @description Registers a function to be called when the field is touched
|
|
1648
|
+
* @param {function(): unknown} fn - The function to be called on touch
|
|
1649
|
+
*/
|
|
1650
|
+
registerOnTouched(fn) {
|
|
1651
|
+
this.onTouch = fn;
|
|
1652
|
+
}
|
|
1653
|
+
/**
|
|
1654
|
+
* @summary Set disabled state
|
|
1655
|
+
* @description Sets the disabled state of the field
|
|
1656
|
+
* @param {boolean} isDisabled - Whether the field should be disabled
|
|
1657
|
+
*/
|
|
1658
|
+
setDisabledState(isDisabled) {
|
|
1659
|
+
this.disabled = isDisabled;
|
|
1660
|
+
}
|
|
1661
|
+
/**
|
|
1662
|
+
* @summary After view initialization logic
|
|
1663
|
+
* @description Performs necessary setup after the view has been initialized
|
|
1664
|
+
* @returns {HTMLElement} The parent element of the field
|
|
1665
|
+
*/
|
|
1666
|
+
afterViewInit() {
|
|
1667
|
+
let parent;
|
|
1668
|
+
switch (this.operation) {
|
|
1669
|
+
case OperationKeys.READ:
|
|
1670
|
+
case OperationKeys.DELETE:
|
|
1671
|
+
return this.component.nativeElement.parentElement;
|
|
1672
|
+
case OperationKeys.CREATE:
|
|
1673
|
+
case OperationKeys.UPDATE:
|
|
1674
|
+
try {
|
|
1675
|
+
parent = NgxFormService.getParentEl(this.component.nativeElement, 'div');
|
|
1676
|
+
}
|
|
1677
|
+
catch (e) {
|
|
1678
|
+
throw new RenderingError(`Unable to retrieve parent form element for the ${this.operation}: ${e instanceof Error ? e.message : e}`);
|
|
1679
|
+
}
|
|
1680
|
+
// NgxFormService.register(parent.id, this.formGroup, this as AngularFieldDefinition);
|
|
1681
|
+
return parent;
|
|
1682
|
+
default:
|
|
1683
|
+
throw new InternalError(`Invalid operation: ${this.operation}`);
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* @summary Cleanup on component destruction
|
|
1688
|
+
* @description Unregisters the field when the component is destroyed
|
|
1689
|
+
*/
|
|
1690
|
+
onDestroy() {
|
|
1691
|
+
if (this.formGroup)
|
|
1692
|
+
NgxFormService.unregister(this.formGroup);
|
|
1693
|
+
}
|
|
1694
|
+
/**
|
|
1695
|
+
* @summary Get field errors
|
|
1696
|
+
* @description Retrieves all errors associated with the field
|
|
1697
|
+
* @returns {Array<{key: string, message: string}>} An array of error objects
|
|
1698
|
+
*/
|
|
1699
|
+
getErrors(parent) {
|
|
1700
|
+
const collapsableContainer = parent.closest('ion-accordion-group');
|
|
1701
|
+
if (collapsableContainer)
|
|
1702
|
+
collapsableContainer.setAttribute('value', 'open');
|
|
1703
|
+
return Object.keys(this.formControl.errors ?? {}).map(key => ({
|
|
1704
|
+
key: key,
|
|
1705
|
+
message: key,
|
|
1706
|
+
}));
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
/**
|
|
1711
|
+
* @description A dynamic form field component for CRUD operations.
|
|
1712
|
+
* @summary The CrudFieldComponent is a versatile form field component that adapts to different
|
|
1713
|
+
* input types and CRUD operations. It extends NgxCrudFormField to inherit form handling capabilities
|
|
1714
|
+
* and implements lifecycle hooks to properly initialize, render, and clean up. This component
|
|
1715
|
+
* supports various input types (text, number, date, select, etc.), validation rules, and styling
|
|
1716
|
+
* options, making it suitable for building dynamic forms for create, read, update, and delete
|
|
1717
|
+
* operations.
|
|
1718
|
+
*
|
|
1719
|
+
* @param {CrudOperations} operation - The CRUD operation being performed (create, read, update, delete)
|
|
1720
|
+
* @param {string} name - The field name, used as form control identifier
|
|
1721
|
+
* @param {PossibleInputTypes} type - The input type (text, number, date, select, etc.)
|
|
1722
|
+
* @param {string|number|Date} value - The initial value of the field
|
|
1723
|
+
* @param {boolean} disabled - Whether the field is disabled
|
|
1724
|
+
* @param {string} label - The display label for the field
|
|
1725
|
+
* @param {string} placeholder - Placeholder text when field is empty
|
|
1726
|
+
* @param {string} format - Format pattern for the field value
|
|
1727
|
+
* @param {boolean} hidden - Whether the field should be hidden
|
|
1728
|
+
* @param {number|Date} max - Maximum allowed value
|
|
1729
|
+
* @param {number} maxlength - Maximum allowed length
|
|
1730
|
+
* @param {number|Date} min - Minimum allowed value
|
|
1731
|
+
* @param {number} minlength - Minimum allowed length
|
|
1732
|
+
* @param {string} pattern - Validation pattern
|
|
1733
|
+
* @param {boolean} readonly - Whether the field is read-only
|
|
1734
|
+
* @param {boolean} required - Whether the field is required
|
|
1735
|
+
* @param {number} step - Step value for number inputs
|
|
1736
|
+
* @param {FormGroup} formGroup - The parent form group
|
|
1737
|
+
* @param {StringOrBoolean} translatable - Whether field labels should be translated
|
|
1738
|
+
*
|
|
1739
|
+
* @component CrudFieldComponent
|
|
1740
|
+
* @example
|
|
1741
|
+
* <ngx-decaf-crud-field
|
|
1742
|
+
* operation="create"
|
|
1743
|
+
* name="firstName"
|
|
1744
|
+
* type="text"
|
|
1745
|
+
* label="<NAME>"
|
|
1746
|
+
* placeholder="<NAME>"
|
|
1747
|
+
* [value]="model.firstName"
|
|
1748
|
+
* [disabled]="model.readOnly">
|
|
1749
|
+
*
|
|
1750
|
+
*
|
|
1751
|
+
* @memberOf module:for-angular
|
|
1752
|
+
*/
|
|
1753
|
+
let CrudFieldComponent = class CrudFieldComponent extends NgxCrudFormField {
|
|
1754
|
+
constructor(elementRef) {
|
|
1755
|
+
super(elementRef);
|
|
1756
|
+
this.elementRef = elementRef;
|
|
1757
|
+
/**
|
|
1758
|
+
* @description The parent field path, if this field is nested.
|
|
1759
|
+
* @summary Specifies the full dot-delimited path of the parent field. This is only set when the field is nested.
|
|
1760
|
+
*
|
|
1761
|
+
* @type {string}
|
|
1762
|
+
* @memberOf CrudFieldComponent
|
|
1763
|
+
*/
|
|
1764
|
+
this.childOf = '';
|
|
1765
|
+
/**
|
|
1766
|
+
* @description The initial value of the field.
|
|
1767
|
+
* @summary Sets the initial value of the form field. This can be a string, number, or Date
|
|
1768
|
+
* depending on the field type. For select fields, this should match one of the option values.
|
|
1769
|
+
*
|
|
1770
|
+
* @type {string | number | Date}
|
|
1771
|
+
* @default ''
|
|
1772
|
+
* @memberOf CrudFieldComponent
|
|
1773
|
+
*/
|
|
1774
|
+
this.value = '';
|
|
1775
|
+
/**
|
|
1776
|
+
* @description Spellcheck attribute for text inputs.
|
|
1777
|
+
* @summary Enables or disables spellchecking for text inputs.
|
|
1778
|
+
* When true, the browser will check the spelling of the input text.
|
|
1779
|
+
*
|
|
1780
|
+
* @type {boolean}
|
|
1781
|
+
* @default false
|
|
1782
|
+
* @memberOf CrudFieldComponent
|
|
1783
|
+
*/
|
|
1784
|
+
this.spellcheck = false;
|
|
1785
|
+
/**
|
|
1786
|
+
* @description Input mode for text inputs.
|
|
1787
|
+
* @summary Hints at the type of data that might be entered by the user while editing the element.
|
|
1788
|
+
* This can affect the virtual keyboard layout on mobile devices.
|
|
1789
|
+
*
|
|
1790
|
+
* @type {'none' | 'text' | 'tel' | 'url' | 'email' | 'numeric' | 'decimal' | 'search'}
|
|
1791
|
+
* @default 'none'
|
|
1792
|
+
* @memberOf CrudFieldComponent
|
|
1793
|
+
*/
|
|
1794
|
+
this.inputmode = 'none';
|
|
1795
|
+
/**
|
|
1796
|
+
* @description Autocomplete behavior for the field.
|
|
1797
|
+
* @summary Specifies whether and how the browser should automatically complete the input.
|
|
1798
|
+
* This can improve user experience by suggesting previously entered values.
|
|
1799
|
+
*
|
|
1800
|
+
* @type {AutocompleteTypes}
|
|
1801
|
+
* @default 'off'
|
|
1802
|
+
* @memberOf CrudFieldComponent
|
|
1803
|
+
*/
|
|
1804
|
+
this.autocomplete = 'off';
|
|
1805
|
+
/**
|
|
1806
|
+
* @description Fill style for the field.
|
|
1807
|
+
* @summary Determines the fill style of the field, such as 'outline' or 'solid'.
|
|
1808
|
+
* This affects the border and background of the field.
|
|
1809
|
+
*
|
|
1810
|
+
* @type {'outline' | 'solid'}
|
|
1811
|
+
* @default 'outline'
|
|
1812
|
+
* @memberOf CrudFieldComponent
|
|
1813
|
+
*/
|
|
1814
|
+
this.fill = 'outline';
|
|
1815
|
+
/**
|
|
1816
|
+
* @description Placement of the label relative to the field.
|
|
1817
|
+
* @summary Specifies where the label should be placed relative to the field.
|
|
1818
|
+
* Options include 'start', 'end', 'floating', 'stacked', and 'fixed'.
|
|
1819
|
+
*
|
|
1820
|
+
* @type {'start' | 'end' | 'floating' | 'stacked' | 'fixed'}
|
|
1821
|
+
* @default 'floating'
|
|
1822
|
+
* @memberOf CrudFieldComponent
|
|
1823
|
+
*/
|
|
1824
|
+
this.labelPlacement = 'floating';
|
|
1825
|
+
/**
|
|
1826
|
+
* @description Update mode for the field.
|
|
1827
|
+
* @summary Determines when the field value should be updated in the form model.
|
|
1828
|
+
* Options include 'change', 'blur', and 'submit'.
|
|
1829
|
+
*
|
|
1830
|
+
* @type {FieldUpdateMode}
|
|
1831
|
+
* @default 'change'
|
|
1832
|
+
* @memberOf CrudFieldComponent
|
|
1833
|
+
*/
|
|
1834
|
+
this.updateOn = 'change';
|
|
1835
|
+
/**
|
|
1836
|
+
* @description Translatability of field labels.
|
|
1837
|
+
* @summary Indicates whether the field labels should be translated based on the current language settings.
|
|
1838
|
+
* This is useful for applications supporting multiple languages.
|
|
1839
|
+
*
|
|
1840
|
+
* @type {StringOrBoolean}
|
|
1841
|
+
* @default true
|
|
1842
|
+
* @memberOf CrudFieldComponent
|
|
1843
|
+
*/
|
|
1844
|
+
this.translatable = true;
|
|
1845
|
+
}
|
|
1846
|
+
ngOnInit() {
|
|
1847
|
+
// super.onInit(this.updateOn);
|
|
1848
|
+
if ([OperationKeys.READ, OperationKeys.DELETE].includes(this.operation)) {
|
|
1849
|
+
this.formGroup = undefined;
|
|
1850
|
+
}
|
|
1851
|
+
else {
|
|
1852
|
+
if (this.type === HTML5InputTypes.RADIO && !this.value)
|
|
1853
|
+
this.formGroup?.get(this.name)?.setValue(this.options[0].value); // TODO: migrate to RenderingEngine
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
ngAfterViewInit() {
|
|
1857
|
+
if ([OperationKeys.READ, OperationKeys.DELETE].includes(this.operation))
|
|
1858
|
+
super.afterViewInit();
|
|
1859
|
+
}
|
|
1860
|
+
ngOnDestroy() {
|
|
1861
|
+
if ([OperationKeys.READ, OperationKeys.DELETE].includes(this.operation))
|
|
1862
|
+
this.onDestroy();
|
|
1863
|
+
}
|
|
1864
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CrudFieldComponent, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1865
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: CrudFieldComponent, isStandalone: true, selector: "ngx-decaf-crud-field", inputs: { operation: "operation", name: "name", path: "path", childOf: "childOf", type: "type", value: "value", disabled: "disabled", label: "label", placeholder: "placeholder", format: "format", hidden: "hidden", max: "max", maxlength: "maxlength", min: "min", minlength: "minlength", pattern: "pattern", readonly: "readonly", required: "required", step: "step", equals: "equals", different: "different", lessThan: "lessThan", lessThanOrEqual: "lessThanOrEqual", greaterThan: "greaterThan", greaterThanOrEqual: "greaterThanOrEqual", cols: "cols", rows: "rows", alignment: "alignment", checked: "checked", justify: "justify", cancelText: "cancelText", interface: "interface", options: "options", mode: "mode", spellcheck: "spellcheck", inputmode: "inputmode", autocomplete: "autocomplete", fill: "fill", labelPlacement: "labelPlacement", updateOn: "updateOn", formGroup: "formGroup", formControl: "formControl", translatable: "translatable", uid: "uid" }, viewQueries: [{ propertyName: "component", first: true, predicate: ["component"], descendants: true, read: ElementRef }], usesInheritance: true, ngImport: i0, template: "@if(operation === 'read' || operation === 'delete') {\n <ng-container #component>\n <div [class]=\"'dcf-input-item ' + operation\">\n <ion-item>\n <ion-label>\n {{ label | translate }}<br />\n @if(value) {\n <ion-text class=\"dcf-display-block\" [innerHTML]=\"value\"></ion-text>\n } @else {\n <br />\n }\n </ion-label>\n </ion-item>\n </div>\n </ng-container>\n} @else {\n <ng-container #component [formGroup]=\"formGroup\">\n <div #container [class]=\"'dcf-input-item ' + (operation || 'create')\">\n @if(type === 'textarea') {\n <ion-textarea\n [mode]=\"mode\"\n [hidden]=\"hidden\"\n [required]=\"required !== undefined ? required : null\"\n [minlength]=\"minlength !== undefined ? minlength : null\"\n [maxlength]=\"maxlength !== undefined ? maxlength : null\"\n [readonly]=\"readonly !== undefined ? readonly : null\"\n [inputmode]=\"inputmode\"\n [spellcheck]=\"spellcheck\"\n [rows]=\"rows\"\n [labelPlacement]=\"labelPlacement\"\n [value]=\"value\"\n [fill]=\"fill\"\n [placeholder]=\"translatable ? (placeholder | translate) : placeholder\"\n [formControlName]=\"name\"\n [label]=\"translatable ? (label | translate) : label\"\n >\n </ion-textarea>\n }\n @else if(type === 'checkbox') {\n <ion-item>\n <ion-checkbox\n #checkboxElement\n [name]=\"path\"\n [mode]=\"mode\"\n [hidden]=\"hidden\"\n [labelPlacement]=\"labelPlacement\"\n [justify]=\"justify\"\n [value]=\"value\"\n [checked]=\"checked\"\n [readonly]=\"readonly\"\n (ionChange)=\"checked = !checked\"\n [formControlName]=\"name\"\n >\n <span [innerHTML]=\"label | translate\"></span>\n </ion-checkbox>\n </ion-item>\n }\n @else if(type === 'radio') {\n <ion-radio-group [formControlName]=\"name\" [name]=\"path\" [value]=\"value\" #component>\n <label class=\"dcf-radio-group-label\" [for]=\"path\">{{label | translate}}</label>\n @for(option of options; track option) {\n <ion-item>\n <ion-radio\n [mode]=\"mode\"\n [hidden]=\"hidden\"\n [labelPlacement]=\"labelPlacement\"\n [alignment]=\"alignment\"\n [justify]=\"justify\"\n [readonly]=\"readonly\"\n [value]=\"option.value\"\n >{{ translatable ? (option?.text | translate) : option?.text }}</ion-radio>\n </ion-item>\n }\n </ion-radio-group>\n }\n @else if(type === 'select') {\n <ion-item>\n <ion-select\n [name]=\"path\"\n [mode]=\"mode\"\n [hidden]=\"hidden\"\n [labelPlacement]=\"labelPlacement\"\n [label]=\"translatable ? (label | translate : label) : label\"\n [value]=\"value\"\n [fill]=\"fill\"\n [placeholder]=\"translatable ? (placeholder | translate) : placeholder\"\n [formControlName]=\"name\"\n [interface]=\"interface\">\n @for(option of options; track option.value) {\n <ion-select-option [value]=\"option.value\">\n {{ translatable ? (option.text | translate) : options.text }}\n </ion-select-option>\n }\n </ion-select>\n </ion-item>\n }\n @else {\n <ion-input\n [name]=\"path\"\n [type]=\"type\"\n [mode]=\"mode\"\n [hidden]=\"hidden\"\n [inputmode]=\"inputmode\"\n [labelPlacement]=\"labelPlacement\"\n [required]=\"required !== undefined ? required : false\"\n [minlength]=\"minlength !== undefined ? minlength : null\"\n [maxlength]=\"maxlength !== undefined ? maxlength : null\"\n [readonly]=\"readonly !== undefined ? readonly : null\"\n [max]=\"max !== undefined ? max : null\"\n [min]=\"min !== undefined ? min : null\"\n [pattern]=\"pattern !== undefined ? pattern : null\"\n [step]=\"step !== undefined ? step : null\"\n [fill]=\"fill\"\n [placeholder]=\"translatable ? (placeholder | translate) : placeholder\"\n [formControlName]=\"name\"\n [label]=\"translatable ? (label | translate) : label\">\n </ion-input>\n }\n @if((!formControl.pristine || formControl.touched) && !formControl.valid) {\n <div class=\"error dcf-error dcf-flex dcf-flex-top\">\n @for(error of getErrors(container); track error.key) {\n * {{ sf((\"errors.\" + error.message) | translate, this[error.key]) }}\n }\n </div>\n }\n </div>\n </ng-container>\n}\n\n", styles: [".dcf-input-item.create,.dcf-input-item.update{margin-bottom:1.8rem;margin-top:0!important}.dcf-input-item.create.checkbox+.checkbox,.dcf-input-item.update.checkbox+.checkbox{margin-top:-.25rem!important}.dcf-input-item.create ion-item,.dcf-input-item.update ion-item{--border-color: transparent}.dcf-input-item.create ion-item.dcf-text-wrap ion-label>*,.dcf-input-item.update ion-item.dcf-text-wrap ion-label>*{white-space:wrap!important;word-break:break-all!important}.dcf-input-item.read ion-label,.dcf-input-item.delete ion-label{font-weight:600}.dcf-input-item ion-item{--padding-end: 0rem;--padding-start: 0px !important;--padding-top: 0px !important;--background: transparent;--background-hover-opacity: .1;--background-hover: var(--ion-color-primary);--background-activated-opacity: .15;--background-focused: var(--ion-color-primary);--background-focused-opacity: .15}.dcf-input-item ion-item span,.dcf-input-item ion-item ion-text{font-weight:400!important;font-size:.925rem;min-height:.5rem!important}.dcf-input-item ion-item span:not(.dcf-display-block),.dcf-input-item ion-item ion-text:not(.dcf-display-block){display:inline-block}.dcf-input-item ion-item span.dcf-display-block,.dcf-input-item ion-item ion-text.dcf-display-block{display:block!important}ion-textarea textarea{scrollbar-width:thin!important;margin-bottom:.5rem!important}ion-select.dcf-select-label-placement-floating::part(label){line-height:1.2rem!important}.dcf-proccessing,.dcf-proccessing *{pointer-events:none;touch-action:none;cursor:text}ion-checkbox{--size: 1.5rem;--checkbox-background-checked: var(--ion-color-primary);--checkmark-width: 2px}ion-item{--inner-padding-start: .75rem}ion-checkbox::part(container){border-radius:50%;border:2px solid var(--ion-color-primary);padding:3px}ion-item .dcf-radio-group-label,ion-radio-group .dcf-radio-group-label{font-weight:600}ion-item .dcf-radio-group-label~ion-item,ion-radio-group .dcf-radio-group-label~ion-item{margin-top:.5rem;--inner-padding-start: .75rem}ion-item+.dcf-helper,ion-radio-group+.dcf-helper{padding-left:.75rem;position:relative}.dcf-error{position:absolute;color:var(--ion-color-danger)!important;font-size:.8rem!important;font-weight:600!important;line-height:1.1rem;box-sizing:border-box;z-index:9999;margin-top:0;animation-duration:.1s;animation-timing-function:ease-out;animation-fill-mode:both;animation-name:fadeTopSmallAnimation;display:flex;align-items:flex-start;gap:.25rem}.dcf-error .ti,.dcf-error ion-icon{position:relative;top:2px!important;min-width:20px;font-size:1rem!important;text-align:left}.dcf-helper{font-size:.875rem!important;font-weight:500;margin-top:.25rem;margin-bottom:-.75rem}.dcf-helper.dcf-has-action{cursor:pointer;color:var(--ion-color-gray-8)!important;text-decoration:underline}.dcf-error+.dcf-helper{padding-top:1rem}@keyframes fadeTopSmallAnimation{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeBottomSmallAnimation{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeTopMediumAnimation{0%{opacity:0;transform:translateY(-50px)}to{opacity:1;transform:translateY(0)}}\n"], dependencies: [{ kind: "ngmodule", type: ForAngularModule }, { kind: "component", type: i1$1.IonText, selector: "ion-text", inputs: ["color", "mode"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MinLengthValidator, selector: "[minlength][formControlName],[minlength][formControl],[minlength][ngModel]", inputs: ["minlength"] }, { kind: "directive", type: i2.MaxLengthValidator, selector: "[maxlength][formControlName],[maxlength][formControl],[maxlength][ngModel]", inputs: ["maxlength"] }, { kind: "directive", type: i2.PatternValidator, selector: "[pattern][formControlName],[pattern][formControl],[pattern][ngModel]", inputs: ["pattern"] }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "pipe", type: i3.TranslatePipe, name: "translate" }, { kind: "component", type: IonInput, selector: "ion-input", inputs: ["accept", "autocapitalize", "autocomplete", "autocorrect", "autofocus", "clearInput", "clearOnEdit", "color", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "max", "maxlength", "min", "minlength", "mode", "multiple", "name", "pattern", "placeholder", "readonly", "required", "shape", "size", "spellcheck", "step", "type", "value"] }, { kind: "component", type: IonItem, selector: "ion-item", inputs: ["button", "color", "detail", "detailIcon", "disabled", "download", "href", "lines", "mode", "rel", "routerAnimation", "routerDirection", "target", "type"] }, { kind: "component", type: IonCheckbox, selector: "ion-checkbox", inputs: ["checked", "color", "disabled", "errorText", "helperText", "indeterminate", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonRadioGroup, selector: "ion-radio-group", inputs: ["allowEmptySelection", "compareWith", "errorText", "helperText", "name", "value"] }, { kind: "component", type: IonRadio, selector: "ion-radio", inputs: ["alignment", "color", "disabled", "justify", "labelPlacement", "mode", "name", "value"] }, { kind: "component", type: IonSelect, selector: "ion-select", inputs: ["cancelText", "color", "compareWith", "disabled", "errorText", "expandedIcon", "fill", "helperText", "interface", "interfaceOptions", "justify", "label", "labelPlacement", "mode", "multiple", "name", "okText", "placeholder", "selectedText", "shape", "toggleIcon", "value"] }, { kind: "component", type: IonSelectOption, selector: "ion-select-option", inputs: ["disabled", "value"] }, { kind: "component", type: IonLabel, selector: "ion-label", inputs: ["color", "mode", "position"] }, { kind: "component", type: IonTextarea, selector: "ion-textarea", inputs: ["autoGrow", "autocapitalize", "autofocus", "clearOnEdit", "color", "cols", "counter", "counterFormatter", "debounce", "disabled", "enterkeyhint", "errorText", "fill", "helperText", "inputmode", "label", "labelPlacement", "maxlength", "minlength", "mode", "name", "placeholder", "readonly", "required", "rows", "shape", "spellcheck", "value", "wrap"] }] }); }
|
|
1866
|
+
};
|
|
1867
|
+
CrudFieldComponent = __decorate([
|
|
1868
|
+
Dynamic(),
|
|
1869
|
+
__metadata("design:paramtypes", [ElementRef])
|
|
1870
|
+
], CrudFieldComponent);
|
|
1871
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CrudFieldComponent, decorators: [{
|
|
1872
|
+
type: Component,
|
|
1873
|
+
args: [{ standalone: true, imports: [
|
|
1874
|
+
ForAngularModule,
|
|
1875
|
+
IonInput,
|
|
1876
|
+
IonItem,
|
|
1877
|
+
IonCheckbox,
|
|
1878
|
+
IonRadioGroup,
|
|
1879
|
+
IonRadio,
|
|
1880
|
+
IonSelect,
|
|
1881
|
+
IonSelectOption,
|
|
1882
|
+
IonLabel,
|
|
1883
|
+
IonText,
|
|
1884
|
+
IonTextarea,
|
|
1885
|
+
], selector: 'ngx-decaf-crud-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: "@if(operation === 'read' || operation === 'delete') {\n <ng-container #component>\n <div [class]=\"'dcf-input-item ' + operation\">\n <ion-item>\n <ion-label>\n {{ label | translate }}<br />\n @if(value) {\n <ion-text class=\"dcf-display-block\" [innerHTML]=\"value\"></ion-text>\n } @else {\n <br />\n }\n </ion-label>\n </ion-item>\n </div>\n </ng-container>\n} @else {\n <ng-container #component [formGroup]=\"formGroup\">\n <div #container [class]=\"'dcf-input-item ' + (operation || 'create')\">\n @if(type === 'textarea') {\n <ion-textarea\n [mode]=\"mode\"\n [hidden]=\"hidden\"\n [required]=\"required !== undefined ? required : null\"\n [minlength]=\"minlength !== undefined ? minlength : null\"\n [maxlength]=\"maxlength !== undefined ? maxlength : null\"\n [readonly]=\"readonly !== undefined ? readonly : null\"\n [inputmode]=\"inputmode\"\n [spellcheck]=\"spellcheck\"\n [rows]=\"rows\"\n [labelPlacement]=\"labelPlacement\"\n [value]=\"value\"\n [fill]=\"fill\"\n [placeholder]=\"translatable ? (placeholder | translate) : placeholder\"\n [formControlName]=\"name\"\n [label]=\"translatable ? (label | translate) : label\"\n >\n </ion-textarea>\n }\n @else if(type === 'checkbox') {\n <ion-item>\n <ion-checkbox\n #checkboxElement\n [name]=\"path\"\n [mode]=\"mode\"\n [hidden]=\"hidden\"\n [labelPlacement]=\"labelPlacement\"\n [justify]=\"justify\"\n [value]=\"value\"\n [checked]=\"checked\"\n [readonly]=\"readonly\"\n (ionChange)=\"checked = !checked\"\n [formControlName]=\"name\"\n >\n <span [innerHTML]=\"label | translate\"></span>\n </ion-checkbox>\n </ion-item>\n }\n @else if(type === 'radio') {\n <ion-radio-group [formControlName]=\"name\" [name]=\"path\" [value]=\"value\" #component>\n <label class=\"dcf-radio-group-label\" [for]=\"path\">{{label | translate}}</label>\n @for(option of options; track option) {\n <ion-item>\n <ion-radio\n [mode]=\"mode\"\n [hidden]=\"hidden\"\n [labelPlacement]=\"labelPlacement\"\n [alignment]=\"alignment\"\n [justify]=\"justify\"\n [readonly]=\"readonly\"\n [value]=\"option.value\"\n >{{ translatable ? (option?.text | translate) : option?.text }}</ion-radio>\n </ion-item>\n }\n </ion-radio-group>\n }\n @else if(type === 'select') {\n <ion-item>\n <ion-select\n [name]=\"path\"\n [mode]=\"mode\"\n [hidden]=\"hidden\"\n [labelPlacement]=\"labelPlacement\"\n [label]=\"translatable ? (label | translate : label) : label\"\n [value]=\"value\"\n [fill]=\"fill\"\n [placeholder]=\"translatable ? (placeholder | translate) : placeholder\"\n [formControlName]=\"name\"\n [interface]=\"interface\">\n @for(option of options; track option.value) {\n <ion-select-option [value]=\"option.value\">\n {{ translatable ? (option.text | translate) : options.text }}\n </ion-select-option>\n }\n </ion-select>\n </ion-item>\n }\n @else {\n <ion-input\n [name]=\"path\"\n [type]=\"type\"\n [mode]=\"mode\"\n [hidden]=\"hidden\"\n [inputmode]=\"inputmode\"\n [labelPlacement]=\"labelPlacement\"\n [required]=\"required !== undefined ? required : false\"\n [minlength]=\"minlength !== undefined ? minlength : null\"\n [maxlength]=\"maxlength !== undefined ? maxlength : null\"\n [readonly]=\"readonly !== undefined ? readonly : null\"\n [max]=\"max !== undefined ? max : null\"\n [min]=\"min !== undefined ? min : null\"\n [pattern]=\"pattern !== undefined ? pattern : null\"\n [step]=\"step !== undefined ? step : null\"\n [fill]=\"fill\"\n [placeholder]=\"translatable ? (placeholder | translate) : placeholder\"\n [formControlName]=\"name\"\n [label]=\"translatable ? (label | translate) : label\">\n </ion-input>\n }\n @if((!formControl.pristine || formControl.touched) && !formControl.valid) {\n <div class=\"error dcf-error dcf-flex dcf-flex-top\">\n @for(error of getErrors(container); track error.key) {\n * {{ sf((\"errors.\" + error.message) | translate, this[error.key]) }}\n }\n </div>\n }\n </div>\n </ng-container>\n}\n\n", styles: [".dcf-input-item.create,.dcf-input-item.update{margin-bottom:1.8rem;margin-top:0!important}.dcf-input-item.create.checkbox+.checkbox,.dcf-input-item.update.checkbox+.checkbox{margin-top:-.25rem!important}.dcf-input-item.create ion-item,.dcf-input-item.update ion-item{--border-color: transparent}.dcf-input-item.create ion-item.dcf-text-wrap ion-label>*,.dcf-input-item.update ion-item.dcf-text-wrap ion-label>*{white-space:wrap!important;word-break:break-all!important}.dcf-input-item.read ion-label,.dcf-input-item.delete ion-label{font-weight:600}.dcf-input-item ion-item{--padding-end: 0rem;--padding-start: 0px !important;--padding-top: 0px !important;--background: transparent;--background-hover-opacity: .1;--background-hover: var(--ion-color-primary);--background-activated-opacity: .15;--background-focused: var(--ion-color-primary);--background-focused-opacity: .15}.dcf-input-item ion-item span,.dcf-input-item ion-item ion-text{font-weight:400!important;font-size:.925rem;min-height:.5rem!important}.dcf-input-item ion-item span:not(.dcf-display-block),.dcf-input-item ion-item ion-text:not(.dcf-display-block){display:inline-block}.dcf-input-item ion-item span.dcf-display-block,.dcf-input-item ion-item ion-text.dcf-display-block{display:block!important}ion-textarea textarea{scrollbar-width:thin!important;margin-bottom:.5rem!important}ion-select.dcf-select-label-placement-floating::part(label){line-height:1.2rem!important}.dcf-proccessing,.dcf-proccessing *{pointer-events:none;touch-action:none;cursor:text}ion-checkbox{--size: 1.5rem;--checkbox-background-checked: var(--ion-color-primary);--checkmark-width: 2px}ion-item{--inner-padding-start: .75rem}ion-checkbox::part(container){border-radius:50%;border:2px solid var(--ion-color-primary);padding:3px}ion-item .dcf-radio-group-label,ion-radio-group .dcf-radio-group-label{font-weight:600}ion-item .dcf-radio-group-label~ion-item,ion-radio-group .dcf-radio-group-label~ion-item{margin-top:.5rem;--inner-padding-start: .75rem}ion-item+.dcf-helper,ion-radio-group+.dcf-helper{padding-left:.75rem;position:relative}.dcf-error{position:absolute;color:var(--ion-color-danger)!important;font-size:.8rem!important;font-weight:600!important;line-height:1.1rem;box-sizing:border-box;z-index:9999;margin-top:0;animation-duration:.1s;animation-timing-function:ease-out;animation-fill-mode:both;animation-name:fadeTopSmallAnimation;display:flex;align-items:flex-start;gap:.25rem}.dcf-error .ti,.dcf-error ion-icon{position:relative;top:2px!important;min-width:20px;font-size:1rem!important;text-align:left}.dcf-helper{font-size:.875rem!important;font-weight:500;margin-top:.25rem;margin-bottom:-.75rem}.dcf-helper.dcf-has-action{cursor:pointer;color:var(--ion-color-gray-8)!important;text-decoration:underline}.dcf-error+.dcf-helper{padding-top:1rem}@keyframes fadeTopSmallAnimation{0%{opacity:0;transform:translateY(-10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeBottomSmallAnimation{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:translateY(0)}}@keyframes fadeTopMediumAnimation{0%{opacity:0;transform:translateY(-50px)}to{opacity:1;transform:translateY(0)}}\n"] }]
|
|
1886
|
+
}], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { operation: [{
|
|
1887
|
+
type: Input,
|
|
1888
|
+
args: [{ required: true }]
|
|
1889
|
+
}], name: [{
|
|
1890
|
+
type: Input,
|
|
1891
|
+
args: [{ required: true }]
|
|
1892
|
+
}], path: [{
|
|
1893
|
+
type: Input,
|
|
1894
|
+
args: [{ required: true }]
|
|
1895
|
+
}], childOf: [{
|
|
1896
|
+
type: Input
|
|
1897
|
+
}], type: [{
|
|
1898
|
+
type: Input,
|
|
1899
|
+
args: [{ required: true }]
|
|
1900
|
+
}], value: [{
|
|
1901
|
+
type: Input
|
|
1902
|
+
}], disabled: [{
|
|
1903
|
+
type: Input
|
|
1904
|
+
}], label: [{
|
|
1905
|
+
type: Input,
|
|
1906
|
+
args: [{ required: true }]
|
|
1907
|
+
}], placeholder: [{
|
|
1908
|
+
type: Input
|
|
1909
|
+
}], format: [{
|
|
1910
|
+
type: Input
|
|
1911
|
+
}], hidden: [{
|
|
1912
|
+
type: Input
|
|
1913
|
+
}], max: [{
|
|
1914
|
+
type: Input
|
|
1915
|
+
}], maxlength: [{
|
|
1916
|
+
type: Input
|
|
1917
|
+
}], min: [{
|
|
1918
|
+
type: Input
|
|
1919
|
+
}], minlength: [{
|
|
1920
|
+
type: Input
|
|
1921
|
+
}], pattern: [{
|
|
1922
|
+
type: Input
|
|
1923
|
+
}], readonly: [{
|
|
1924
|
+
type: Input
|
|
1925
|
+
}], required: [{
|
|
1926
|
+
type: Input
|
|
1927
|
+
}], step: [{
|
|
1928
|
+
type: Input
|
|
1929
|
+
}], equals: [{
|
|
1930
|
+
type: Input
|
|
1931
|
+
}], different: [{
|
|
1932
|
+
type: Input
|
|
1933
|
+
}], lessThan: [{
|
|
1934
|
+
type: Input
|
|
1935
|
+
}], lessThanOrEqual: [{
|
|
1936
|
+
type: Input
|
|
1937
|
+
}], greaterThan: [{
|
|
1938
|
+
type: Input
|
|
1939
|
+
}], greaterThanOrEqual: [{
|
|
1940
|
+
type: Input
|
|
1941
|
+
}], cols: [{
|
|
1942
|
+
type: Input
|
|
1943
|
+
}], rows: [{
|
|
1944
|
+
type: Input
|
|
1945
|
+
}], alignment: [{
|
|
1946
|
+
type: Input
|
|
1947
|
+
}], checked: [{
|
|
1948
|
+
type: Input
|
|
1949
|
+
}], justify: [{
|
|
1950
|
+
type: Input
|
|
1951
|
+
}], cancelText: [{
|
|
1952
|
+
type: Input
|
|
1953
|
+
}], interface: [{
|
|
1954
|
+
type: Input
|
|
1955
|
+
}], options: [{
|
|
1956
|
+
type: Input
|
|
1957
|
+
}], mode: [{
|
|
1958
|
+
type: Input
|
|
1959
|
+
}], spellcheck: [{
|
|
1960
|
+
type: Input
|
|
1961
|
+
}], inputmode: [{
|
|
1962
|
+
type: Input
|
|
1963
|
+
}], autocomplete: [{
|
|
1964
|
+
type: Input
|
|
1965
|
+
}], fill: [{
|
|
1966
|
+
type: Input
|
|
1967
|
+
}], labelPlacement: [{
|
|
1968
|
+
type: Input
|
|
1969
|
+
}], updateOn: [{
|
|
1970
|
+
type: Input
|
|
1971
|
+
}], component: [{
|
|
1972
|
+
type: ViewChild,
|
|
1973
|
+
args: ['component', { read: ElementRef }]
|
|
1974
|
+
}], formGroup: [{
|
|
1975
|
+
type: Input
|
|
1976
|
+
}], formControl: [{
|
|
1977
|
+
type: Input
|
|
1978
|
+
}], translatable: [{
|
|
1979
|
+
type: Input
|
|
1980
|
+
}], uid: [{
|
|
1981
|
+
type: Input
|
|
1982
|
+
}] } });
|
|
1983
|
+
|
|
1984
|
+
const CssClasses = {
|
|
1985
|
+
BUTTONS_CONTAINER: 'buttons-container',
|
|
1986
|
+
};
|
|
1987
|
+
const DefaultFormReactiveOptions = {
|
|
1988
|
+
buttons: {
|
|
1989
|
+
submit: {
|
|
1990
|
+
text: 'Submit',
|
|
1991
|
+
},
|
|
1992
|
+
clear: {
|
|
1993
|
+
text: 'Clear',
|
|
1994
|
+
},
|
|
1995
|
+
},
|
|
1996
|
+
};
|
|
1997
|
+
|
|
1998
|
+
/**
|
|
1999
|
+
* @component CrudFormComponent
|
|
2000
|
+
* @example <ngx-decaf-crud-form
|
|
2001
|
+
* action="create"
|
|
2002
|
+
* operation="create"
|
|
2003
|
+
* formGroup="formGroup"
|
|
2004
|
+
* rendererId="rendererId"
|
|
2005
|
+
* submitEvent="submitEvent"
|
|
2006
|
+
* target="_self"
|
|
2007
|
+
* method="event">
|
|
2008
|
+
* </ngx-decaf-crud-form>
|
|
2009
|
+
*
|
|
2010
|
+
* @param {string} action - The action to be performed (create, read, update, delete)
|
|
2011
|
+
* @param {CrudOperations} operation - The CRUD operation being performed (create, read, update, delete)
|
|
2012
|
+
* @param {FormGroup} formGroup - The form group
|
|
2013
|
+
* @param {string} rendererId - The renderer id
|
|
2014
|
+
* @param {SubmitEvent} submitEvent - The submit event
|
|
2015
|
+
* @param {string} target - The target
|
|
2016
|
+
* @param {string} method - The method
|
|
2017
|
+
*/
|
|
2018
|
+
let CrudFormComponent = class CrudFormComponent {
|
|
2019
|
+
constructor() {
|
|
2020
|
+
this.updateOn = 'change';
|
|
2021
|
+
this.target = '_self';
|
|
2022
|
+
this.method = 'event';
|
|
2023
|
+
this.submitEvent = new EventEmitter();
|
|
2024
|
+
/**
|
|
2025
|
+
* @description Angular Location service.
|
|
2026
|
+
* @summary Injected service that provides access to the browser's URL and history.
|
|
2027
|
+
* This service is used for interacting with the browser's history API, allowing
|
|
2028
|
+
* for back navigation and URL manipulation outside of Angular's router.
|
|
2029
|
+
*
|
|
2030
|
+
* @private
|
|
2031
|
+
* @type {Location}
|
|
2032
|
+
* @memberOf CrudFormComponent
|
|
2033
|
+
*/
|
|
2034
|
+
this.location = inject(Location);
|
|
2035
|
+
this.OperationKeys = OperationKeys;
|
|
2036
|
+
}
|
|
2037
|
+
// ngAfterViewInit() {
|
|
2038
|
+
// if (![OperationKeys.READ, OperationKeys.DELETE].includes(this.operation))
|
|
2039
|
+
// NgxFormService.formAfterViewInit(this, this.rendererId);
|
|
2040
|
+
// }
|
|
2041
|
+
async ngOnInit() {
|
|
2042
|
+
if (!this.logger)
|
|
2043
|
+
this.logger = getLogger(this);
|
|
2044
|
+
if (this.operation === OperationKeys.READ || this.operation === OperationKeys.DELETE)
|
|
2045
|
+
this.formGroup = undefined;
|
|
2046
|
+
this.options = Object.assign({}, DefaultFormReactiveOptions, this.options || {});
|
|
2047
|
+
}
|
|
2048
|
+
ngOnDestroy() {
|
|
2049
|
+
if (this.formGroup)
|
|
2050
|
+
NgxFormService.unregister(this.formGroup);
|
|
2051
|
+
}
|
|
2052
|
+
/**
|
|
2053
|
+
* @param {SubmitEvent} event
|
|
2054
|
+
*/
|
|
2055
|
+
async submit(event) {
|
|
2056
|
+
event.preventDefault();
|
|
2057
|
+
event.stopImmediatePropagation();
|
|
2058
|
+
event.stopPropagation();
|
|
2059
|
+
if (!NgxFormService.validateFields(this.formGroup))
|
|
2060
|
+
return false;
|
|
2061
|
+
const data = NgxFormService.getFormData(this.formGroup);
|
|
2062
|
+
console.log('Submit=', data);
|
|
2063
|
+
this.submitEvent.emit({
|
|
2064
|
+
data,
|
|
2065
|
+
component: 'FormReactiveComponent',
|
|
2066
|
+
name: this.action || EventConstants.SUBMIT_EVENT,
|
|
2067
|
+
handlers: this.handlers,
|
|
2068
|
+
});
|
|
2069
|
+
}
|
|
2070
|
+
handleReset() {
|
|
2071
|
+
this.location.back();
|
|
2072
|
+
// if(OperationKeys.DELETE !== this.operation)
|
|
2073
|
+
// NgxFormService.reset(this.formGroup);
|
|
2074
|
+
// else
|
|
2075
|
+
// this.location.back();
|
|
2076
|
+
}
|
|
2077
|
+
handleDelete() {
|
|
2078
|
+
this.submitEvent.emit({
|
|
2079
|
+
data: this.uid,
|
|
2080
|
+
component: 'FormReactiveComponent',
|
|
2081
|
+
name: EventConstants.SUBMIT_EVENT,
|
|
2082
|
+
});
|
|
2083
|
+
}
|
|
2084
|
+
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CrudFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2085
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "18.2.13", type: CrudFormComponent, isStandalone: true, selector: "ngx-decaf-crud-form", inputs: { model: "model", updateOn: "updateOn", target: "target", method: "method", options: "options", action: "action", operation: "operation", handlers: "handlers", formGroup: "formGroup", childOf: "childOf", rendererId: "rendererId", uid: "uid" }, outputs: { submitEvent: "submitEvent" }, viewQueries: [{ propertyName: "component", first: true, predicate: ["reactiveForm"], descendants: true, read: ElementRef }], ngImport: i0, template: "@if(operation !== 'read' && operation !== 'delete') {\n <form #reactiveForm [id]=\"rendererId\" [formGroup]=\"formGroup\" (submit)=\"submit($event)\" novalidate [target]=\"target\">\n <ng-content #formContent></ng-content>\n <div class=\"dcf-flex dcf-flex-right\">\n <div>\n @if(options.buttons.clear) {\n <ion-button fill=\"clear\" (click)=\"handleReset()\">\n @if(options.buttons.clear?.icon) {\n <ion-icon aria-hidden=\"true\" [slot]=\"options.buttons.clear?.iconSlot\" [name]=\"options.buttons.clear?.icon\"></ion-icon>\n }\n {{operation === 'update' ? 'Back' : options.buttons.clear?.text}}\n </ion-button>\n }\n\n <ion-button\n type=\"submit\">\n @if(options.buttons.submit.icon) {\n <ion-icon aria-hidden=\"true\" [slot]=\"options.buttons.submit.iconSlot\" [name]=\"options.buttons.submit.icon\"></ion-icon>\n }\n {{ action ? action : options.buttons.submit.text}}\n </ion-button>\n </div>\n </div>\n </form>\n} @else {\n <div [class]=\"'dcf-flex dcf-flex-right ' + operation\" id=\"dcf-buttons-container\">\n <div>\n @if(options.buttons.clear) {\n <ion-button fill=\"clear\" (click)=\"handleReset()\">\n @if(options.buttons.clear?.icon) {\n <ion-icon aria-hidden=\"true\" [slot]=\"options.buttons.clear?.iconSlot\" [name]=\"options.buttons.clear?.icon\"></ion-icon>\n }\n {{ ['delete', 'read', 'update'].includes(operation) ? 'Back' : options.buttons.clear?.text}}\n </ion-button>\n }\n @if(operation === 'delete' && uid) {\n <ion-button\n (click)=\"handleDelete()\"\n color=\"danger\"\n type=\"button\">\n @if(options.buttons.submit.icon) {\n <ion-icon aria-hidden=\"true\" [slot]=\"options.buttons.submit.iconSlot\" [name]=\"options.buttons.submit.icon\"></ion-icon>\n }\n Delete\n </ion-button>\n }\n @if(operation === OperationKeys.CREATE || operation === OperationKeys.UPDATE) {\n <ion-button\n type=\"submit\">\n @if(options.buttons.submit.icon) {\n <ion-icon aria-hidden=\"true\" [slot]=\"options.buttons.submit.iconSlot\" [name]=\"options.buttons.submit.icon\"></ion-icon>\n }\n {{options.buttons.submit.text}}\n </ion-button>\n }\n\n </div>\n </div>\n}\n\n", styles: ["#dcf-buttons-container{margin-top:1.8rem;margin-bottom:0}\n"], dependencies: [{ kind: "ngmodule", type: ForAngularModule }, { kind: "component", type: i1$1.IonButton, selector: "ion-button", inputs: ["buttonType", "color", "disabled", "download", "expand", "fill", "form", "href", "mode", "rel", "routerAnimation", "routerDirection", "shape", "size", "strong", "target", "type"] }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "component", type: IonIcon, selector: "ion-icon", inputs: ["color", "flipRtl", "icon", "ios", "lazy", "md", "mode", "name", "sanitize", "size", "src"] }] }); }
|
|
2086
|
+
};
|
|
2087
|
+
CrudFormComponent = __decorate([
|
|
2088
|
+
Dynamic()
|
|
2089
|
+
], CrudFormComponent);
|
|
2090
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.13", ngImport: i0, type: CrudFormComponent, decorators: [{
|
|
2091
|
+
type: Component,
|
|
2092
|
+
args: [{ standalone: true, selector: 'ngx-decaf-crud-form', imports: [ForAngularModule, IonIcon], template: "@if(operation !== 'read' && operation !== 'delete') {\n <form #reactiveForm [id]=\"rendererId\" [formGroup]=\"formGroup\" (submit)=\"submit($event)\" novalidate [target]=\"target\">\n <ng-content #formContent></ng-content>\n <div class=\"dcf-flex dcf-flex-right\">\n <div>\n @if(options.buttons.clear) {\n <ion-button fill=\"clear\" (click)=\"handleReset()\">\n @if(options.buttons.clear?.icon) {\n <ion-icon aria-hidden=\"true\" [slot]=\"options.buttons.clear?.iconSlot\" [name]=\"options.buttons.clear?.icon\"></ion-icon>\n }\n {{operation === 'update' ? 'Back' : options.buttons.clear?.text}}\n </ion-button>\n }\n\n <ion-button\n type=\"submit\">\n @if(options.buttons.submit.icon) {\n <ion-icon aria-hidden=\"true\" [slot]=\"options.buttons.submit.iconSlot\" [name]=\"options.buttons.submit.icon\"></ion-icon>\n }\n {{ action ? action : options.buttons.submit.text}}\n </ion-button>\n </div>\n </div>\n </form>\n} @else {\n <div [class]=\"'dcf-flex dcf-flex-right ' + operation\" id=\"dcf-buttons-container\">\n <div>\n @if(options.buttons.clear) {\n <ion-button fill=\"clear\" (click)=\"handleReset()\">\n @if(options.buttons.clear?.icon) {\n <ion-icon aria-hidden=\"true\" [slot]=\"options.buttons.clear?.iconSlot\" [name]=\"options.buttons.clear?.icon\"></ion-icon>\n }\n {{ ['delete', 'read', 'update'].includes(operation) ? 'Back' : options.buttons.clear?.text}}\n </ion-button>\n }\n @if(operation === 'delete' && uid) {\n <ion-button\n (click)=\"handleDelete()\"\n color=\"danger\"\n type=\"button\">\n @if(options.buttons.submit.icon) {\n <ion-icon aria-hidden=\"true\" [slot]=\"options.buttons.submit.iconSlot\" [name]=\"options.buttons.submit.icon\"></ion-icon>\n }\n Delete\n </ion-button>\n }\n @if(operation === OperationKeys.CREATE || operation === OperationKeys.UPDATE) {\n <ion-button\n type=\"submit\">\n @if(options.buttons.submit.icon) {\n <ion-icon aria-hidden=\"true\" [slot]=\"options.buttons.submit.iconSlot\" [name]=\"options.buttons.submit.icon\"></ion-icon>\n }\n {{options.buttons.submit.text}}\n </ion-button>\n }\n\n </div>\n </div>\n}\n\n", styles: ["#dcf-buttons-container{margin-top:1.8rem;margin-bottom:0}\n"] }]
|
|
2093
|
+
}], propDecorators: { model: [{
|
|
2094
|
+
type: Input
|
|
2095
|
+
}], updateOn: [{
|
|
2096
|
+
type: Input
|
|
2097
|
+
}], component: [{
|
|
2098
|
+
type: ViewChild,
|
|
2099
|
+
args: ['reactiveForm', { static: false, read: ElementRef }]
|
|
2100
|
+
}], target: [{
|
|
2101
|
+
type: Input
|
|
2102
|
+
}], method: [{
|
|
2103
|
+
type: Input
|
|
2104
|
+
}], options: [{
|
|
2105
|
+
type: Input
|
|
2106
|
+
}], action: [{
|
|
2107
|
+
type: Input
|
|
2108
|
+
}], operation: [{
|
|
2109
|
+
type: Input,
|
|
2110
|
+
args: [{ required: true }]
|
|
2111
|
+
}], handlers: [{
|
|
2112
|
+
type: Input
|
|
2113
|
+
}], formGroup: [{
|
|
2114
|
+
type: Input
|
|
2115
|
+
}], childOf: [{
|
|
2116
|
+
type: Input
|
|
2117
|
+
}], rendererId: [{
|
|
2118
|
+
type: Input
|
|
2119
|
+
}], uid: [{
|
|
2120
|
+
type: Input
|
|
2121
|
+
}], submitEvent: [{
|
|
2122
|
+
type: Output
|
|
2123
|
+
}] } });
|
|
2124
|
+
|
|
2125
|
+
/**
|
|
2126
|
+
* @description Angular integration for the Decaf framework
|
|
2127
|
+
* @summary This module provides Angular components and services for integrating with the Decaf framework.
|
|
2128
|
+
* It includes components for rendering models, CRUD operations, and form handling, as well as
|
|
2129
|
+
* rendering engines and utility functions to facilitate Angular application development with Decaf.
|
|
2130
|
+
* @module for-angular
|
|
2131
|
+
*/
|
|
2132
|
+
|
|
2133
|
+
/**
|
|
2134
|
+
* Generated bundle index. Do not edit.
|
|
2135
|
+
*/
|
|
2136
|
+
|
|
2137
|
+
export { AngularEngineKeys, BaseComponentProps, ComponentsTagNames, CrudFieldComponent, CrudFormComponent, Dynamic, DynamicModule, EventConstants, FormConstants, LoggerLevels, ModelRendererComponent, NgxRenderingEngine, NgxRenderingEngine2, RouteDirections };
|
|
2138
|
+
//# sourceMappingURL=decaf-ts-for-angular.mjs.map
|