@memberjunction/ng-forms 0.0.1 → 5.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/dynamic-form/dynamic-form.component.d.ts +116 -0
- package/dist/lib/dynamic-form/dynamic-form.component.d.ts.map +1 -0
- package/dist/lib/dynamic-form/dynamic-form.component.js +258 -0
- package/dist/lib/dynamic-form/dynamic-form.component.js.map +1 -0
- package/dist/lib/dynamic-form-field/dynamic-form-field.component.d.ts +111 -0
- package/dist/lib/dynamic-form-field/dynamic-form-field.component.d.ts.map +1 -0
- package/dist/lib/dynamic-form-field/dynamic-form-field.component.js +240 -0
- package/dist/lib/dynamic-form-field/dynamic-form-field.component.js.map +1 -0
- package/dist/lib/dynamic-form-response/dynamic-form-response.component.d.ts +43 -0
- package/dist/lib/dynamic-form-response/dynamic-form-response.component.d.ts.map +1 -0
- package/dist/lib/dynamic-form-response/dynamic-form-response.component.js +129 -0
- package/dist/lib/dynamic-form-response/dynamic-form-response.component.js.map +1 -0
- package/dist/lib/dynamic-form-response/form-response-utils.d.ts +74 -0
- package/dist/lib/dynamic-form-response/form-response-utils.d.ts.map +1 -0
- package/dist/lib/dynamic-form-response/form-response-utils.js +147 -0
- package/dist/lib/dynamic-form-response/form-response-utils.js.map +1 -0
- package/dist/lib/forms.module.d.ts +15 -0
- package/dist/lib/forms.module.d.ts.map +1 -0
- package/dist/lib/forms.module.js +97 -0
- package/dist/lib/forms.module.js.map +1 -0
- package/dist/public-api.d.ts +52 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +54 -0
- package/dist/public-api.js.map +1 -0
- package/package.json +43 -6
- package/README.md +0 -45
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Dynamic Form Field Component
|
|
3
|
+
*
|
|
4
|
+
* Renders a single form field based on a `FormQuestion` definition from
|
|
5
|
+
* `@memberjunction/ai-core-plus`. Supports 14+ input types including text,
|
|
6
|
+
* textarea, email, number, currency, date, datetime, time, daterange,
|
|
7
|
+
* buttongroup, radio, dropdown, checkbox, and slider.
|
|
8
|
+
*
|
|
9
|
+
* Implements `ControlValueAccessor` for seamless integration with Angular
|
|
10
|
+
* Reactive Forms. Width is automatically sized based on field type or an
|
|
11
|
+
* explicit `widthHint` on the question definition.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```html
|
|
15
|
+
* <mj-dynamic-form-field
|
|
16
|
+
* [Question]="question"
|
|
17
|
+
* [Control]="formGroup.get(question.id)"
|
|
18
|
+
* [formControlName]="question.id">
|
|
19
|
+
* </mj-dynamic-form-field>
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @module @memberjunction/ng-forms
|
|
23
|
+
*/
|
|
24
|
+
import { Component, Input, forwardRef } from '@angular/core';
|
|
25
|
+
import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
26
|
+
import * as i0 from "@angular/core";
|
|
27
|
+
import * as i1 from "@angular/common";
|
|
28
|
+
import * as i2 from "@angular/forms";
|
|
29
|
+
import * as i3 from "@progress/kendo-angular-buttons";
|
|
30
|
+
import * as i4 from "@progress/kendo-angular-inputs";
|
|
31
|
+
import * as i5 from "@progress/kendo-angular-dateinputs";
|
|
32
|
+
export class DynamicFormFieldComponent {
|
|
33
|
+
/**
|
|
34
|
+
* The question definition that drives this field's rendering.
|
|
35
|
+
* Determines the input type, label, validation, options, and layout.
|
|
36
|
+
*/
|
|
37
|
+
Question;
|
|
38
|
+
/**
|
|
39
|
+
* The reactive FormControl bound to this field.
|
|
40
|
+
* Used for validation state display and dropdown binding.
|
|
41
|
+
*/
|
|
42
|
+
Control;
|
|
43
|
+
/** Current field value (managed by ControlValueAccessor) */
|
|
44
|
+
Value = null;
|
|
45
|
+
/** Whether this field is disabled */
|
|
46
|
+
Disabled = false;
|
|
47
|
+
onChange = () => { };
|
|
48
|
+
onTouched = () => { };
|
|
49
|
+
ngOnInit() {
|
|
50
|
+
// No-op; field is configured via inputs
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Resolved question type string (handles both simple string and object types).
|
|
54
|
+
*/
|
|
55
|
+
get QuestionType() {
|
|
56
|
+
return typeof this.Question.type === 'string'
|
|
57
|
+
? this.Question.type
|
|
58
|
+
: this.Question.type.type;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Whether this is a choice-based question (buttongroup, radio, dropdown, checkbox).
|
|
62
|
+
*/
|
|
63
|
+
get IsChoiceQuestion() {
|
|
64
|
+
return ['buttongroup', 'radio', 'dropdown', 'checkbox'].includes(this.QuestionType);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Available options for choice-type questions.
|
|
68
|
+
*/
|
|
69
|
+
get Options() {
|
|
70
|
+
if (typeof this.Question.type === 'object' && 'options' in this.Question.type) {
|
|
71
|
+
return this.Question.type.options;
|
|
72
|
+
}
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether multiple selections are allowed (for checkbox type).
|
|
77
|
+
*/
|
|
78
|
+
get AllowMultiple() {
|
|
79
|
+
return this.QuestionType === 'checkbox' ||
|
|
80
|
+
(typeof this.Question.type === 'object' && 'multiple' in this.Question.type && !!this.Question.type.multiple);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* CSS class controlling the field width based on `widthHint` or intelligent defaults.
|
|
84
|
+
*
|
|
85
|
+
* Width mapping:
|
|
86
|
+
* - `'narrow'` (~200px): numbers, dates, times
|
|
87
|
+
* - `'medium'` (~450px): text inputs (default)
|
|
88
|
+
* - `'wide'` (100%): emails, buttongroup, radio, checkbox
|
|
89
|
+
* - `'full'` (100%): textarea
|
|
90
|
+
* - `'auto'` (150–350px): dropdowns
|
|
91
|
+
*/
|
|
92
|
+
get WidthClass() {
|
|
93
|
+
if (this.Question.widthHint) {
|
|
94
|
+
return `width-${this.Question.widthHint}`;
|
|
95
|
+
}
|
|
96
|
+
const type = this.QuestionType;
|
|
97
|
+
if (['number', 'currency', 'date', 'datetime', 'time'].includes(type))
|
|
98
|
+
return 'width-narrow';
|
|
99
|
+
if (['buttongroup', 'radio', 'checkbox'].includes(type))
|
|
100
|
+
return 'width-wide';
|
|
101
|
+
if (type === 'dropdown')
|
|
102
|
+
return 'width-auto';
|
|
103
|
+
if (type === 'textarea')
|
|
104
|
+
return 'width-full';
|
|
105
|
+
if (type === 'email')
|
|
106
|
+
return 'width-wide';
|
|
107
|
+
return 'width-medium';
|
|
108
|
+
}
|
|
109
|
+
/** Placeholder text for text-based inputs */
|
|
110
|
+
get Placeholder() {
|
|
111
|
+
if (typeof this.Question.type === 'object' && 'placeholder' in this.Question.type) {
|
|
112
|
+
return this.Question.type.placeholder;
|
|
113
|
+
}
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
/** Minimum value for numeric inputs */
|
|
117
|
+
get Min() {
|
|
118
|
+
if (typeof this.Question.type === 'object' && 'min' in this.Question.type) {
|
|
119
|
+
return this.Question.type.min;
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
/** Maximum value for numeric inputs */
|
|
124
|
+
get Max() {
|
|
125
|
+
if (typeof this.Question.type === 'object' && 'max' in this.Question.type) {
|
|
126
|
+
return this.Question.type.max;
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
/** Step increment for numeric inputs */
|
|
131
|
+
get Step() {
|
|
132
|
+
if (typeof this.Question.type === 'object' && 'step' in this.Question.type) {
|
|
133
|
+
return this.Question.type.step;
|
|
134
|
+
}
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
/** Prefix string for currency inputs (e.g. "$") */
|
|
138
|
+
get Prefix() {
|
|
139
|
+
if (typeof this.Question.type === 'object' && 'prefix' in this.Question.type) {
|
|
140
|
+
return this.Question.type.prefix;
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
/** Suffix string for currency/slider inputs (e.g. "USD", "%") */
|
|
145
|
+
get Suffix() {
|
|
146
|
+
if (typeof this.Question.type === 'object' && 'suffix' in this.Question.type) {
|
|
147
|
+
return this.Question.type.suffix;
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
/** Handle value changes from any input type */
|
|
152
|
+
OnValueChange(newValue) {
|
|
153
|
+
this.Value = newValue;
|
|
154
|
+
this.onChange(newValue);
|
|
155
|
+
this.onTouched();
|
|
156
|
+
}
|
|
157
|
+
/** Toggle a checkbox option (supports single and multiple selection) */
|
|
158
|
+
ToggleCheckbox(option) {
|
|
159
|
+
if (!this.AllowMultiple) {
|
|
160
|
+
this.OnValueChange(option.value);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const currentValues = Array.isArray(this.Value) ? this.Value : [];
|
|
164
|
+
const index = currentValues.indexOf(option.value);
|
|
165
|
+
const newValues = index > -1
|
|
166
|
+
? currentValues.filter((v) => v !== option.value)
|
|
167
|
+
: [...currentValues, option.value];
|
|
168
|
+
this.OnValueChange(newValues);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Check whether a checkbox option is currently selected */
|
|
172
|
+
IsChecked(option) {
|
|
173
|
+
if (!this.AllowMultiple) {
|
|
174
|
+
return this.Value === option.value;
|
|
175
|
+
}
|
|
176
|
+
return Array.isArray(this.Value) && this.Value.includes(option.value);
|
|
177
|
+
}
|
|
178
|
+
/** Get slider configuration with defaults */
|
|
179
|
+
GetSliderConfig() {
|
|
180
|
+
if (typeof this.Question.type === 'object' && this.Question.type.type === 'slider') {
|
|
181
|
+
return {
|
|
182
|
+
min: this.Question.type.min,
|
|
183
|
+
max: this.Question.type.max,
|
|
184
|
+
step: this.Question.type.step,
|
|
185
|
+
suffix: this.Question.type.suffix
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return { min: 0, max: 100, step: 1 };
|
|
189
|
+
}
|
|
190
|
+
/** Handle date range start date change */
|
|
191
|
+
OnDateRangeStartChange(value) {
|
|
192
|
+
const current = this.Value || {};
|
|
193
|
+
this.OnValueChange({ ...current, start: value });
|
|
194
|
+
}
|
|
195
|
+
/** Handle date range end date change */
|
|
196
|
+
OnDateRangeEndChange(value) {
|
|
197
|
+
const current = this.Value || {};
|
|
198
|
+
this.OnValueChange({ ...current, end: value });
|
|
199
|
+
}
|
|
200
|
+
/** Track-by function for options lists */
|
|
201
|
+
TrackByValue(_index, option) {
|
|
202
|
+
return option.value;
|
|
203
|
+
}
|
|
204
|
+
// ControlValueAccessor implementation
|
|
205
|
+
writeValue(value) {
|
|
206
|
+
this.Value = value;
|
|
207
|
+
}
|
|
208
|
+
registerOnChange(fn) {
|
|
209
|
+
this.onChange = fn;
|
|
210
|
+
}
|
|
211
|
+
registerOnTouched(fn) {
|
|
212
|
+
this.onTouched = fn;
|
|
213
|
+
}
|
|
214
|
+
setDisabledState(isDisabled) {
|
|
215
|
+
this.Disabled = isDisabled;
|
|
216
|
+
}
|
|
217
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DynamicFormFieldComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
218
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: DynamicFormFieldComponent, isStandalone: false, selector: "mj-dynamic-form-field", inputs: { Question: "Question", Control: "Control" }, providers: [
|
|
219
|
+
{
|
|
220
|
+
provide: NG_VALUE_ACCESSOR,
|
|
221
|
+
useExisting: forwardRef(() => DynamicFormFieldComponent),
|
|
222
|
+
multi: true
|
|
223
|
+
}
|
|
224
|
+
], ngImport: i0, template: "<div class=\"form-question\" [class.required]=\"Question.required\">\n <!-- Label -->\n @if (Question.label) {\n <label class=\"question-label\">\n {{ Question.label }}\n @if (Question.required) {\n <span class=\"required-indicator\">*</span>\n }\n </label>\n }\n\n <!-- Help Text -->\n @if (Question.helpText) {\n <div class=\"help-text\">\n {{ Question.helpText }}\n </div>\n }\n\n <!-- Input Container with width class -->\n <div class=\"input-container\" [ngClass]=\"WidthClass\">\n\n <!-- Text Input -->\n @if (QuestionType === 'text') {\n <input\n type=\"text\"\n kendoTextBox\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n class=\"question-input\"\n />\n }\n\n <!-- Email Input -->\n @if (QuestionType === 'email') {\n <input\n type=\"email\"\n kendoTextBox\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n class=\"question-input\"\n />\n }\n\n <!-- Textarea -->\n @if (QuestionType === 'textarea') {\n <textarea\n kendoTextArea\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n class=\"question-textarea\"\n rows=\"5\"\n ></textarea>\n }\n\n <!-- Number Input -->\n @if (QuestionType === 'number') {\n <kendo-numerictextbox\n [value]=\"$any(Value)\"\n [min]=\"Min || 0\"\n [max]=\"Max || 999999999\"\n [step]=\"Step || 1\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n class=\"question-input\"\n ></kendo-numerictextbox>\n }\n\n <!-- Currency Input -->\n @if (QuestionType === 'currency') {\n <kendo-numerictextbox\n [value]=\"$any(Value)\"\n [min]=\"Min || 0\"\n [max]=\"Max || 999999999\"\n [step]=\"Step || 0.01\"\n [format]=\"'c2'\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n class=\"question-input\"\n ></kendo-numerictextbox>\n }\n\n <!-- Date Picker -->\n @if (QuestionType === 'date') {\n <kendo-datepicker\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n class=\"question-input\"\n ></kendo-datepicker>\n }\n\n <!-- DateTime Picker -->\n @if (QuestionType === 'datetime') {\n <kendo-datetimepicker\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n class=\"question-input\"\n ></kendo-datetimepicker>\n }\n\n <!-- Button Group -->\n @if (QuestionType === 'buttongroup') {\n <kendo-buttongroup\n [selection]=\"'single'\"\n class=\"question-buttongroup\"\n >\n @for (option of Options; track TrackByValue($index, option)) {\n <button\n kendoButton\n [toggleable]=\"true\"\n [selected]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (click)=\"OnValueChange(option.value)\"\n class=\"buttongroup-option\"\n >\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </button>\n }\n </kendo-buttongroup>\n }\n\n <!-- Radio Buttons -->\n @if (QuestionType === 'radio') {\n <div class=\"question-radio-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"radio-option\">\n <input\n type=\"radio\"\n kendoRadioButton\n [name]=\"Question.id\"\n [value]=\"option.value\"\n [checked]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange(option.value)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"radio-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n\n <!-- Dropdown -->\n @if (QuestionType === 'dropdown') {\n <select\n [formControl]=\"Control\"\n [disabled]=\"Disabled\"\n class=\"question-dropdown\"\n >\n <option value=\"\">Select an option...</option>\n @for (option of Options; track TrackByValue($index, option)) {\n <option [value]=\"option.value\">\n {{ option.label }}\n </option>\n }\n </select>\n }\n\n <!-- Slider -->\n @if (QuestionType === 'slider') {\n <div class=\"question-slider-container\">\n <kendo-slider\n [min]=\"GetSliderConfig().min\"\n [max]=\"GetSliderConfig().max\"\n [smallStep]=\"GetSliderConfig().step || 1\"\n [value]=\"$any(Value) || GetSliderConfig().min\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n class=\"question-slider\"\n ></kendo-slider>\n <div class=\"slider-value\">\n {{ Value || GetSliderConfig().min }}{{ GetSliderConfig().suffix || '' }}\n </div>\n </div>\n }\n\n <!-- Date Range -->\n @if (QuestionType === 'daterange') {\n <div class=\"question-daterange\">\n <kendo-datepicker\n [value]=\"$any(Value)?.start\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnDateRangeStartChange($event)\"\n placeholder=\"Start Date\"\n class=\"daterange-start\"\n ></kendo-datepicker>\n <span class=\"daterange-separator\">to</span>\n <kendo-datepicker\n [value]=\"$any(Value)?.end\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnDateRangeEndChange($event)\"\n placeholder=\"End Date\"\n class=\"daterange-end\"\n ></kendo-datepicker>\n </div>\n }\n\n <!-- Time Picker -->\n @if (QuestionType === 'time') {\n <kendo-timepicker\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n [format]=\"'h:mm a'\"\n class=\"question-input\"\n ></kendo-timepicker>\n }\n\n <!-- Checkboxes -->\n @if (QuestionType === 'checkbox') {\n <div class=\"question-checkbox-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"checkbox-option\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n [checked]=\"IsChecked(option)\"\n [disabled]=\"Disabled\"\n (change)=\"ToggleCheckbox(option)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"checkbox-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n </div><!-- end input-container -->\n\n <!-- Validation Error -->\n @if (Control.invalid && Control.touched) {\n <div class=\"validation-error\">\n @if (Control.errors?.['required']) {\n <span>This field is required</span>\n }\n @if (Control.errors?.['email']) {\n <span>Please enter a valid email address</span>\n }\n @if (Control.errors?.['min']) {\n <span>Value must be at least {{ Min }}</span>\n }\n @if (Control.errors?.['max']) {\n <span>Value must be at most {{ Max }}</span>\n }\n </div>\n }\n</div>\n", styles: [".form-question {\n margin-bottom: 16px;\n}\n\n.question-label {\n display: block;\n font-weight: 500;\n margin-bottom: 6px;\n color: var(--mj-text-primary);\n font-size: 14px;\n}\n\n.required-indicator {\n color: var(--mj-status-error);\n margin-left: 2px;\n}\n\n.help-text {\n font-size: 12px;\n color: var(--mj-text-secondary);\n margin-bottom: 8px;\n font-style: italic;\n}\n\n/* Default widths - controlled by input-container width classes */\n.question-input,\n.question-textarea,\n.question-dropdown {\n width: 100%;\n max-width: 100%;\n}\n\n.question-dropdown {\n padding: 8px 12px;\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n font-size: 14px;\n font-family: inherit;\n background-color: var(--mj-bg-surface);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 9L1 4h10z'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 8px center;\n background-size: 12px;\n padding-right: 32px;\n cursor: pointer;\n transition: border-color 0.2s ease, box-shadow 0.2s ease;\n appearance: none;\n -webkit-appearance: none;\n -moz-appearance: none;\n color: var(--mj-text-primary);\n}\n\n.question-dropdown:hover {\n border-color: var(--mj-border-strong);\n}\n\n.question-dropdown:focus {\n outline: none;\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n.question-dropdown:disabled {\n background-color: var(--mj-bg-surface-sunken);\n cursor: not-allowed;\n opacity: 0.6;\n}\n\n.question-textarea {\n resize: vertical;\n min-height: 100px;\n}\n\n.question-buttongroup {\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n overflow-x: auto;\n overflow-y: hidden;\n -webkit-overflow-scrolling: touch;\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--mj-brand-primary) 30%, transparent) transparent;\n}\n\n.question-buttongroup::-webkit-scrollbar {\n height: 6px;\n}\n\n.question-buttongroup::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.question-buttongroup::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n border-radius: 3px;\n}\n\n.question-buttongroup::-webkit-scrollbar-thumb:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 50%, transparent);\n}\n\n.buttongroup-option {\n flex: 0 0 auto;\n white-space: nowrap;\n}\n\n.buttongroup-option i {\n margin-right: 6px;\n}\n\n.question-radio-group,\n.question-checkbox-group {\n display: flex;\n flex-wrap: wrap;\n gap: 12px 16px;\n}\n\n.radio-option,\n.checkbox-option {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 120px;\n flex: 0 0 auto;\n}\n\n.radio-label,\n.checkbox-label {\n cursor: pointer;\n user-select: none;\n margin: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.radio-label i,\n.checkbox-label i {\n color: var(--mj-text-secondary);\n}\n\n.validation-error {\n color: var(--mj-status-error);\n font-size: 12px;\n margin-top: 4px;\n}\n\n/* Required field styling */\n.form-question.required .question-label {\n font-weight: 600;\n}\n\n/* Slider styling */\n.question-slider-container {\n width: 100%;\n}\n\n.question-slider {\n width: 100%;\n margin-bottom: 8px;\n}\n\n.slider-value {\n text-align: center;\n font-size: 16px;\n font-weight: 600;\n color: var(--mj-brand-primary);\n padding: 4px 0;\n}\n\n/* Date range styling */\n.question-daterange {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.daterange-start,\n.daterange-end {\n flex: 1;\n min-width: 150px;\n}\n\n.daterange-separator {\n font-weight: 500;\n color: var(--mj-text-secondary);\n white-space: nowrap;\n}\n\n/* Mobile adjustments for date range */\n@media (max-width: 480px) {\n .question-daterange {\n flex-direction: column;\n align-items: stretch;\n }\n\n .daterange-separator {\n text-align: center;\n padding: 4px 0;\n }\n}\n\n/* ===== Semantic Width Classes ===== */\n/* Controls field width based on expected content length */\n\n.input-container {\n display: block;\n}\n\n/* Narrow fields - for numbers, dates, times, short codes */\n.input-container.width-narrow {\n max-width: 200px;\n}\n\n.input-container.width-narrow .question-input,\n.input-container.width-narrow .question-dropdown {\n width: 100%;\n}\n\n/* Medium fields - for names, cities, phone numbers (DEFAULT) */\n.input-container.width-medium {\n max-width: 450px;\n}\n\n.input-container.width-medium .question-input,\n.input-container.width-medium .question-dropdown {\n width: 100%;\n}\n\n/* Wide fields - for emails, URLs, long text */\n.input-container.width-wide {\n max-width: 100%;\n}\n\n.input-container.width-wide .question-input,\n.input-container.width-wide .question-dropdown {\n width: 100%;\n}\n\n/* Full width - for textarea, search fields */\n.input-container.width-full {\n max-width: 100%;\n}\n\n.input-container.width-full .question-input,\n.input-container.width-full .question-textarea,\n.input-container.width-full .question-dropdown {\n width: 100%;\n}\n\n/* Auto-width - for dropdowns, radio, checkbox (sizes to content) */\n.input-container.width-auto {\n max-width: 350px;\n min-width: 150px;\n width: auto;\n}\n\n.input-container.width-auto .question-dropdown {\n width: 100%;\n}\n\n/* Responsive adjustments for smaller screens */\n@media (max-width: 600px) {\n .input-container.width-narrow,\n .input-container.width-medium,\n .input-container.width-wide,\n .input-container.width-auto {\n max-width: 100%;\n }\n}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "component", type: i3.ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "component", type: i3.ButtonGroupComponent, selector: "kendo-buttongroup", inputs: ["disabled", "selection", "width", "tabIndex", "navigable"], outputs: ["navigate"], exportAs: ["kendoButtonGroup"] }, { kind: "directive", type: i4.TextBoxDirective, selector: "input[kendoTextBox]", inputs: ["value"] }, { kind: "component", type: i4.NumericTextBoxComponent, selector: "kendo-numerictextbox", inputs: ["focusableId", "disabled", "readonly", "title", "autoCorrect", "format", "max", "min", "decimals", "placeholder", "step", "spinners", "rangeValidation", "tabindex", "tabIndex", "changeValueOnScroll", "selectOnFocus", "value", "maxlength", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "inputFocus", "inputBlur"], exportAs: ["kendoNumericTextBox"] }, { kind: "directive", type: i4.TextAreaDirective, selector: "textarea[kendoTextArea]", inputs: ["autoSize", "value"], outputs: ["valueChange"] }, { kind: "directive", type: i4.CheckBoxDirective, selector: "input[kendoCheckBox]", inputs: ["size", "rounded"] }, { kind: "directive", type: i4.RadioButtonDirective, selector: "input[kendoRadioButton]", inputs: ["size"] }, { kind: "component", type: i4.SliderComponent, selector: "kendo-slider", inputs: ["focusableId", "dragHandleTitle", "incrementTitle", "animate", "decrementTitle", "showButtons", "value", "tabIndex"], exportAs: ["kendoSlider"] }, { kind: "component", type: i5.DatePickerComponent, selector: "kendo-datepicker", inputs: ["focusableId", "cellTemplate", "clearButton", "inputAttributes", "monthCellTemplate", "yearCellTemplate", "decadeCellTemplate", "centuryCellTemplate", "weekNumberTemplate", "headerTitleTemplate", "headerTemplate", "footerTemplate", "footer", "navigationItemTemplate", "weekDaysFormat", "showOtherMonthDays", "activeView", "bottomView", "topView", "calendarType", "animateCalendarNavigation", "disabled", "readonly", "readOnlyInput", "popupSettings", "navigation", "min", "max", "incompleteDateValidation", "autoCorrectParts", "autoSwitchParts", "autoSwitchKeys", "enableMouseWheel", "allowCaretMode", "autoFill", "focusedDate", "value", "format", "twoDigitYearMax", "formatPlaceholder", "placeholder", "tabindex", "tabIndex", "disabledDates", "adaptiveTitle", "adaptiveSubtitle", "rangeValidation", "disabledDatesValidation", "weekNumber", "size", "rounded", "fillMode", "adaptiveMode"], outputs: ["valueChange", "focus", "blur", "open", "close", "escape"], exportAs: ["kendo-datepicker"] }, { kind: "component", type: i5.DateTimePickerComponent, selector: "kendo-datetimepicker", inputs: ["focusableId", "weekDaysFormat", "showOtherMonthDays", "value", "format", "twoDigitYearMax", "tabindex", "disabledDates", "popupSettings", "adaptiveTitle", "adaptiveSubtitle", "disabled", "readonly", "readOnlyInput", "cancelButton", "formatPlaceholder", "placeholder", "steps", "focusedDate", "calendarType", "animateCalendarNavigation", "weekNumber", "min", "max", "rangeValidation", "disabledDatesValidation", "incompleteDateValidation", "autoCorrectParts", "autoSwitchParts", "autoSwitchKeys", "enableMouseWheel", "allowCaretMode", "clearButton", "autoFill", "adaptiveMode", "inputAttributes", "defaultTab", "size", "rounded", "fillMode", "headerTemplate", "footerTemplate", "footer"], outputs: ["valueChange", "open", "close", "focus", "blur", "escape"], exportAs: ["kendo-datetimepicker"] }, { kind: "component", type: i5.TimePickerComponent, selector: "kendo-timepicker", inputs: ["focusableId", "disabled", "readonly", "readOnlyInput", "clearButton", "format", "formatPlaceholder", "placeholder", "min", "max", "incompleteDateValidation", "autoSwitchParts", "autoSwitchKeys", "enableMouseWheel", "allowCaretMode", "cancelButton", "nowButton", "steps", "popupSettings", "tabindex", "tabIndex", "adaptiveTitle", "adaptiveSubtitle", "rangeValidation", "adaptiveMode", "value", "size", "rounded", "fillMode", "inputAttributes"], outputs: ["valueChange", "focus", "blur", "open", "close", "escape"], exportAs: ["kendo-timepicker"] }] });
|
|
225
|
+
}
|
|
226
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DynamicFormFieldComponent, decorators: [{
|
|
227
|
+
type: Component,
|
|
228
|
+
args: [{ standalone: false, selector: 'mj-dynamic-form-field', providers: [
|
|
229
|
+
{
|
|
230
|
+
provide: NG_VALUE_ACCESSOR,
|
|
231
|
+
useExisting: forwardRef(() => DynamicFormFieldComponent),
|
|
232
|
+
multi: true
|
|
233
|
+
}
|
|
234
|
+
], template: "<div class=\"form-question\" [class.required]=\"Question.required\">\n <!-- Label -->\n @if (Question.label) {\n <label class=\"question-label\">\n {{ Question.label }}\n @if (Question.required) {\n <span class=\"required-indicator\">*</span>\n }\n </label>\n }\n\n <!-- Help Text -->\n @if (Question.helpText) {\n <div class=\"help-text\">\n {{ Question.helpText }}\n </div>\n }\n\n <!-- Input Container with width class -->\n <div class=\"input-container\" [ngClass]=\"WidthClass\">\n\n <!-- Text Input -->\n @if (QuestionType === 'text') {\n <input\n type=\"text\"\n kendoTextBox\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n class=\"question-input\"\n />\n }\n\n <!-- Email Input -->\n @if (QuestionType === 'email') {\n <input\n type=\"email\"\n kendoTextBox\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n class=\"question-input\"\n />\n }\n\n <!-- Textarea -->\n @if (QuestionType === 'textarea') {\n <textarea\n kendoTextArea\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n class=\"question-textarea\"\n rows=\"5\"\n ></textarea>\n }\n\n <!-- Number Input -->\n @if (QuestionType === 'number') {\n <kendo-numerictextbox\n [value]=\"$any(Value)\"\n [min]=\"Min || 0\"\n [max]=\"Max || 999999999\"\n [step]=\"Step || 1\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n class=\"question-input\"\n ></kendo-numerictextbox>\n }\n\n <!-- Currency Input -->\n @if (QuestionType === 'currency') {\n <kendo-numerictextbox\n [value]=\"$any(Value)\"\n [min]=\"Min || 0\"\n [max]=\"Max || 999999999\"\n [step]=\"Step || 0.01\"\n [format]=\"'c2'\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n class=\"question-input\"\n ></kendo-numerictextbox>\n }\n\n <!-- Date Picker -->\n @if (QuestionType === 'date') {\n <kendo-datepicker\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n class=\"question-input\"\n ></kendo-datepicker>\n }\n\n <!-- DateTime Picker -->\n @if (QuestionType === 'datetime') {\n <kendo-datetimepicker\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n class=\"question-input\"\n ></kendo-datetimepicker>\n }\n\n <!-- Button Group -->\n @if (QuestionType === 'buttongroup') {\n <kendo-buttongroup\n [selection]=\"'single'\"\n class=\"question-buttongroup\"\n >\n @for (option of Options; track TrackByValue($index, option)) {\n <button\n kendoButton\n [toggleable]=\"true\"\n [selected]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (click)=\"OnValueChange(option.value)\"\n class=\"buttongroup-option\"\n >\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </button>\n }\n </kendo-buttongroup>\n }\n\n <!-- Radio Buttons -->\n @if (QuestionType === 'radio') {\n <div class=\"question-radio-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"radio-option\">\n <input\n type=\"radio\"\n kendoRadioButton\n [name]=\"Question.id\"\n [value]=\"option.value\"\n [checked]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange(option.value)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"radio-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n\n <!-- Dropdown -->\n @if (QuestionType === 'dropdown') {\n <select\n [formControl]=\"Control\"\n [disabled]=\"Disabled\"\n class=\"question-dropdown\"\n >\n <option value=\"\">Select an option...</option>\n @for (option of Options; track TrackByValue($index, option)) {\n <option [value]=\"option.value\">\n {{ option.label }}\n </option>\n }\n </select>\n }\n\n <!-- Slider -->\n @if (QuestionType === 'slider') {\n <div class=\"question-slider-container\">\n <kendo-slider\n [min]=\"GetSliderConfig().min\"\n [max]=\"GetSliderConfig().max\"\n [smallStep]=\"GetSliderConfig().step || 1\"\n [value]=\"$any(Value) || GetSliderConfig().min\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n class=\"question-slider\"\n ></kendo-slider>\n <div class=\"slider-value\">\n {{ Value || GetSliderConfig().min }}{{ GetSliderConfig().suffix || '' }}\n </div>\n </div>\n }\n\n <!-- Date Range -->\n @if (QuestionType === 'daterange') {\n <div class=\"question-daterange\">\n <kendo-datepicker\n [value]=\"$any(Value)?.start\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnDateRangeStartChange($event)\"\n placeholder=\"Start Date\"\n class=\"daterange-start\"\n ></kendo-datepicker>\n <span class=\"daterange-separator\">to</span>\n <kendo-datepicker\n [value]=\"$any(Value)?.end\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnDateRangeEndChange($event)\"\n placeholder=\"End Date\"\n class=\"daterange-end\"\n ></kendo-datepicker>\n </div>\n }\n\n <!-- Time Picker -->\n @if (QuestionType === 'time') {\n <kendo-timepicker\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n [format]=\"'h:mm a'\"\n class=\"question-input\"\n ></kendo-timepicker>\n }\n\n <!-- Checkboxes -->\n @if (QuestionType === 'checkbox') {\n <div class=\"question-checkbox-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"checkbox-option\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n [checked]=\"IsChecked(option)\"\n [disabled]=\"Disabled\"\n (change)=\"ToggleCheckbox(option)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"checkbox-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n </div><!-- end input-container -->\n\n <!-- Validation Error -->\n @if (Control.invalid && Control.touched) {\n <div class=\"validation-error\">\n @if (Control.errors?.['required']) {\n <span>This field is required</span>\n }\n @if (Control.errors?.['email']) {\n <span>Please enter a valid email address</span>\n }\n @if (Control.errors?.['min']) {\n <span>Value must be at least {{ Min }}</span>\n }\n @if (Control.errors?.['max']) {\n <span>Value must be at most {{ Max }}</span>\n }\n </div>\n }\n</div>\n", styles: [".form-question {\n margin-bottom: 16px;\n}\n\n.question-label {\n display: block;\n font-weight: 500;\n margin-bottom: 6px;\n color: var(--mj-text-primary);\n font-size: 14px;\n}\n\n.required-indicator {\n color: var(--mj-status-error);\n margin-left: 2px;\n}\n\n.help-text {\n font-size: 12px;\n color: var(--mj-text-secondary);\n margin-bottom: 8px;\n font-style: italic;\n}\n\n/* Default widths - controlled by input-container width classes */\n.question-input,\n.question-textarea,\n.question-dropdown {\n width: 100%;\n max-width: 100%;\n}\n\n.question-dropdown {\n padding: 8px 12px;\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n font-size: 14px;\n font-family: inherit;\n background-color: var(--mj-bg-surface);\n background-image: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 9L1 4h10z'/%3E%3C/svg%3E\");\n background-repeat: no-repeat;\n background-position: right 8px center;\n background-size: 12px;\n padding-right: 32px;\n cursor: pointer;\n transition: border-color 0.2s ease, box-shadow 0.2s ease;\n appearance: none;\n -webkit-appearance: none;\n -moz-appearance: none;\n color: var(--mj-text-primary);\n}\n\n.question-dropdown:hover {\n border-color: var(--mj-border-strong);\n}\n\n.question-dropdown:focus {\n outline: none;\n border-color: var(--mj-brand-primary);\n box-shadow: 0 0 0 3px color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n}\n\n.question-dropdown:disabled {\n background-color: var(--mj-bg-surface-sunken);\n cursor: not-allowed;\n opacity: 0.6;\n}\n\n.question-textarea {\n resize: vertical;\n min-height: 100px;\n}\n\n.question-buttongroup {\n display: flex;\n flex-wrap: nowrap;\n gap: 8px;\n overflow-x: auto;\n overflow-y: hidden;\n -webkit-overflow-scrolling: touch;\n scrollbar-width: thin;\n scrollbar-color: color-mix(in srgb, var(--mj-brand-primary) 30%, transparent) transparent;\n}\n\n.question-buttongroup::-webkit-scrollbar {\n height: 6px;\n}\n\n.question-buttongroup::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.question-buttongroup::-webkit-scrollbar-thumb {\n background: color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n border-radius: 3px;\n}\n\n.question-buttongroup::-webkit-scrollbar-thumb:hover {\n background: color-mix(in srgb, var(--mj-brand-primary) 50%, transparent);\n}\n\n.buttongroup-option {\n flex: 0 0 auto;\n white-space: nowrap;\n}\n\n.buttongroup-option i {\n margin-right: 6px;\n}\n\n.question-radio-group,\n.question-checkbox-group {\n display: flex;\n flex-wrap: wrap;\n gap: 12px 16px;\n}\n\n.radio-option,\n.checkbox-option {\n display: flex;\n align-items: center;\n gap: 8px;\n min-width: 120px;\n flex: 0 0 auto;\n}\n\n.radio-label,\n.checkbox-label {\n cursor: pointer;\n user-select: none;\n margin: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n}\n\n.radio-label i,\n.checkbox-label i {\n color: var(--mj-text-secondary);\n}\n\n.validation-error {\n color: var(--mj-status-error);\n font-size: 12px;\n margin-top: 4px;\n}\n\n/* Required field styling */\n.form-question.required .question-label {\n font-weight: 600;\n}\n\n/* Slider styling */\n.question-slider-container {\n width: 100%;\n}\n\n.question-slider {\n width: 100%;\n margin-bottom: 8px;\n}\n\n.slider-value {\n text-align: center;\n font-size: 16px;\n font-weight: 600;\n color: var(--mj-brand-primary);\n padding: 4px 0;\n}\n\n/* Date range styling */\n.question-daterange {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.daterange-start,\n.daterange-end {\n flex: 1;\n min-width: 150px;\n}\n\n.daterange-separator {\n font-weight: 500;\n color: var(--mj-text-secondary);\n white-space: nowrap;\n}\n\n/* Mobile adjustments for date range */\n@media (max-width: 480px) {\n .question-daterange {\n flex-direction: column;\n align-items: stretch;\n }\n\n .daterange-separator {\n text-align: center;\n padding: 4px 0;\n }\n}\n\n/* ===== Semantic Width Classes ===== */\n/* Controls field width based on expected content length */\n\n.input-container {\n display: block;\n}\n\n/* Narrow fields - for numbers, dates, times, short codes */\n.input-container.width-narrow {\n max-width: 200px;\n}\n\n.input-container.width-narrow .question-input,\n.input-container.width-narrow .question-dropdown {\n width: 100%;\n}\n\n/* Medium fields - for names, cities, phone numbers (DEFAULT) */\n.input-container.width-medium {\n max-width: 450px;\n}\n\n.input-container.width-medium .question-input,\n.input-container.width-medium .question-dropdown {\n width: 100%;\n}\n\n/* Wide fields - for emails, URLs, long text */\n.input-container.width-wide {\n max-width: 100%;\n}\n\n.input-container.width-wide .question-input,\n.input-container.width-wide .question-dropdown {\n width: 100%;\n}\n\n/* Full width - for textarea, search fields */\n.input-container.width-full {\n max-width: 100%;\n}\n\n.input-container.width-full .question-input,\n.input-container.width-full .question-textarea,\n.input-container.width-full .question-dropdown {\n width: 100%;\n}\n\n/* Auto-width - for dropdowns, radio, checkbox (sizes to content) */\n.input-container.width-auto {\n max-width: 350px;\n min-width: 150px;\n width: auto;\n}\n\n.input-container.width-auto .question-dropdown {\n width: 100%;\n}\n\n/* Responsive adjustments for smaller screens */\n@media (max-width: 600px) {\n .input-container.width-narrow,\n .input-container.width-medium,\n .input-container.width-wide,\n .input-container.width-auto {\n max-width: 100%;\n }\n}\n"] }]
|
|
235
|
+
}], propDecorators: { Question: [{
|
|
236
|
+
type: Input
|
|
237
|
+
}], Control: [{
|
|
238
|
+
type: Input
|
|
239
|
+
}] } });
|
|
240
|
+
//# sourceMappingURL=dynamic-form-field.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-form-field.component.js","sourceRoot":"","sources":["../../../src/lib/dynamic-form-field/dynamic-form-field.component.ts","../../../src/lib/dynamic-form-field/dynamic-form-field.component.html"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Dynamic Form Field Component\n *\n * Renders a single form field based on a `FormQuestion` definition from\n * `@memberjunction/ai-core-plus`. Supports 14+ input types including text,\n * textarea, email, number, currency, date, datetime, time, daterange,\n * buttongroup, radio, dropdown, checkbox, and slider.\n *\n * Implements `ControlValueAccessor` for seamless integration with Angular\n * Reactive Forms. Width is automatically sized based on field type or an\n * explicit `widthHint` on the question definition.\n *\n * @example\n * ```html\n * <mj-dynamic-form-field\n * [Question]=\"question\"\n * [Control]=\"formGroup.get(question.id)\"\n * [formControlName]=\"question.id\">\n * </mj-dynamic-form-field>\n * ```\n *\n * @module @memberjunction/ng-forms\n */\n\nimport { Component, Input, forwardRef, OnInit } from '@angular/core';\nimport { ControlValueAccessor, NG_VALUE_ACCESSOR, FormControl } from '@angular/forms';\nimport { FormQuestion, FormOption } from '@memberjunction/ai-core-plus';\n\n@Component({\n standalone: false,\n selector: 'mj-dynamic-form-field',\n templateUrl: './dynamic-form-field.component.html',\n styleUrls: ['./dynamic-form-field.component.css'],\n providers: [\n {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => DynamicFormFieldComponent),\n multi: true\n }\n ]\n})\nexport class DynamicFormFieldComponent implements ControlValueAccessor, OnInit {\n /**\n * The question definition that drives this field's rendering.\n * Determines the input type, label, validation, options, and layout.\n */\n @Input() Question!: FormQuestion;\n\n /**\n * The reactive FormControl bound to this field.\n * Used for validation state display and dropdown binding.\n */\n @Input() Control!: FormControl;\n\n /** Current field value (managed by ControlValueAccessor) */\n public Value: string | number | boolean | Date | Record<string, Date> | (string | number | boolean)[] | null = null;\n\n /** Whether this field is disabled */\n public Disabled = false;\n\n private onChange: (value: string | number | boolean | Date | Record<string, Date> | (string | number | boolean)[] | null) => void = () => {};\n private onTouched: () => void = () => {};\n\n ngOnInit(): void {\n // No-op; field is configured via inputs\n }\n\n /**\n * Resolved question type string (handles both simple string and object types).\n */\n public get QuestionType(): string {\n return typeof this.Question.type === 'string'\n ? this.Question.type\n : this.Question.type.type;\n }\n\n /**\n * Whether this is a choice-based question (buttongroup, radio, dropdown, checkbox).\n */\n public get IsChoiceQuestion(): boolean {\n return ['buttongroup', 'radio', 'dropdown', 'checkbox'].includes(this.QuestionType);\n }\n\n /**\n * Available options for choice-type questions.\n */\n public get Options(): FormOption[] {\n if (typeof this.Question.type === 'object' && 'options' in this.Question.type) {\n return this.Question.type.options;\n }\n return [];\n }\n\n /**\n * Whether multiple selections are allowed (for checkbox type).\n */\n public get AllowMultiple(): boolean {\n return this.QuestionType === 'checkbox' ||\n (typeof this.Question.type === 'object' && 'multiple' in this.Question.type && !!this.Question.type.multiple);\n }\n\n /**\n * CSS class controlling the field width based on `widthHint` or intelligent defaults.\n *\n * Width mapping:\n * - `'narrow'` (~200px): numbers, dates, times\n * - `'medium'` (~450px): text inputs (default)\n * - `'wide'` (100%): emails, buttongroup, radio, checkbox\n * - `'full'` (100%): textarea\n * - `'auto'` (150–350px): dropdowns\n */\n public get WidthClass(): string {\n if (this.Question.widthHint) {\n return `width-${this.Question.widthHint}`;\n }\n const type = this.QuestionType;\n if (['number', 'currency', 'date', 'datetime', 'time'].includes(type)) return 'width-narrow';\n if (['buttongroup', 'radio', 'checkbox'].includes(type)) return 'width-wide';\n if (type === 'dropdown') return 'width-auto';\n if (type === 'textarea') return 'width-full';\n if (type === 'email') return 'width-wide';\n return 'width-medium';\n }\n\n /** Placeholder text for text-based inputs */\n public get Placeholder(): string | undefined {\n if (typeof this.Question.type === 'object' && 'placeholder' in this.Question.type) {\n return this.Question.type.placeholder as string;\n }\n return undefined;\n }\n\n /** Minimum value for numeric inputs */\n public get Min(): number | undefined {\n if (typeof this.Question.type === 'object' && 'min' in this.Question.type) {\n return this.Question.type.min as number;\n }\n return undefined;\n }\n\n /** Maximum value for numeric inputs */\n public get Max(): number | undefined {\n if (typeof this.Question.type === 'object' && 'max' in this.Question.type) {\n return this.Question.type.max as number;\n }\n return undefined;\n }\n\n /** Step increment for numeric inputs */\n public get Step(): number | undefined {\n if (typeof this.Question.type === 'object' && 'step' in this.Question.type) {\n return this.Question.type.step as number;\n }\n return undefined;\n }\n\n /** Prefix string for currency inputs (e.g. \"$\") */\n public get Prefix(): string | undefined {\n if (typeof this.Question.type === 'object' && 'prefix' in this.Question.type) {\n return this.Question.type.prefix as string;\n }\n return undefined;\n }\n\n /** Suffix string for currency/slider inputs (e.g. \"USD\", \"%\") */\n public get Suffix(): string | undefined {\n if (typeof this.Question.type === 'object' && 'suffix' in this.Question.type) {\n return this.Question.type.suffix as string;\n }\n return undefined;\n }\n\n /** Handle value changes from any input type */\n public OnValueChange(newValue: string | number | boolean | Date | Record<string, Date> | (string | number | boolean)[] | null): void {\n this.Value = newValue;\n this.onChange(newValue);\n this.onTouched();\n }\n\n /** Toggle a checkbox option (supports single and multiple selection) */\n public ToggleCheckbox(option: FormOption): void {\n if (!this.AllowMultiple) {\n this.OnValueChange(option.value);\n } else {\n const currentValues = Array.isArray(this.Value) ? this.Value : [];\n const index = currentValues.indexOf(option.value);\n const newValues = index > -1\n ? currentValues.filter((v: string | number | boolean) => v !== option.value)\n : [...currentValues, option.value];\n this.OnValueChange(newValues);\n }\n }\n\n /** Check whether a checkbox option is currently selected */\n public IsChecked(option: FormOption): boolean {\n if (!this.AllowMultiple) {\n return this.Value === option.value;\n }\n return Array.isArray(this.Value) && this.Value.includes(option.value);\n }\n\n /** Get slider configuration with defaults */\n public GetSliderConfig(): { min: number; max: number; step?: number; suffix?: string } {\n if (typeof this.Question.type === 'object' && this.Question.type.type === 'slider') {\n return {\n min: this.Question.type.min,\n max: this.Question.type.max,\n step: this.Question.type.step,\n suffix: this.Question.type.suffix\n };\n }\n return { min: 0, max: 100, step: 1 };\n }\n\n /** Handle date range start date change */\n public OnDateRangeStartChange(value: Date): void {\n const current = (this.Value as Record<string, Date>) || {};\n this.OnValueChange({ ...current, start: value });\n }\n\n /** Handle date range end date change */\n public OnDateRangeEndChange(value: Date): void {\n const current = (this.Value as Record<string, Date>) || {};\n this.OnValueChange({ ...current, end: value });\n }\n\n /** Track-by function for options lists */\n public TrackByValue(_index: number, option: FormOption): string | number | boolean {\n return option.value;\n }\n\n // ControlValueAccessor implementation\n writeValue(value: string | number | boolean | Date | Record<string, Date> | (string | number | boolean)[] | null): void {\n this.Value = value;\n }\n\n registerOnChange(fn: (value: string | number | boolean | Date | Record<string, Date> | (string | number | boolean)[] | null) => void): void {\n this.onChange = fn;\n }\n\n registerOnTouched(fn: () => void): void {\n this.onTouched = fn;\n }\n\n setDisabledState(isDisabled: boolean): void {\n this.Disabled = isDisabled;\n }\n}\n","<div class=\"form-question\" [class.required]=\"Question.required\">\n <!-- Label -->\n @if (Question.label) {\n <label class=\"question-label\">\n {{ Question.label }}\n @if (Question.required) {\n <span class=\"required-indicator\">*</span>\n }\n </label>\n }\n\n <!-- Help Text -->\n @if (Question.helpText) {\n <div class=\"help-text\">\n {{ Question.helpText }}\n </div>\n }\n\n <!-- Input Container with width class -->\n <div class=\"input-container\" [ngClass]=\"WidthClass\">\n\n <!-- Text Input -->\n @if (QuestionType === 'text') {\n <input\n type=\"text\"\n kendoTextBox\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n class=\"question-input\"\n />\n }\n\n <!-- Email Input -->\n @if (QuestionType === 'email') {\n <input\n type=\"email\"\n kendoTextBox\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n class=\"question-input\"\n />\n }\n\n <!-- Textarea -->\n @if (QuestionType === 'textarea') {\n <textarea\n kendoTextArea\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n class=\"question-textarea\"\n rows=\"5\"\n ></textarea>\n }\n\n <!-- Number Input -->\n @if (QuestionType === 'number') {\n <kendo-numerictextbox\n [value]=\"$any(Value)\"\n [min]=\"Min || 0\"\n [max]=\"Max || 999999999\"\n [step]=\"Step || 1\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n class=\"question-input\"\n ></kendo-numerictextbox>\n }\n\n <!-- Currency Input -->\n @if (QuestionType === 'currency') {\n <kendo-numerictextbox\n [value]=\"$any(Value)\"\n [min]=\"Min || 0\"\n [max]=\"Max || 999999999\"\n [step]=\"Step || 0.01\"\n [format]=\"'c2'\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n class=\"question-input\"\n ></kendo-numerictextbox>\n }\n\n <!-- Date Picker -->\n @if (QuestionType === 'date') {\n <kendo-datepicker\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n class=\"question-input\"\n ></kendo-datepicker>\n }\n\n <!-- DateTime Picker -->\n @if (QuestionType === 'datetime') {\n <kendo-datetimepicker\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n class=\"question-input\"\n ></kendo-datetimepicker>\n }\n\n <!-- Button Group -->\n @if (QuestionType === 'buttongroup') {\n <kendo-buttongroup\n [selection]=\"'single'\"\n class=\"question-buttongroup\"\n >\n @for (option of Options; track TrackByValue($index, option)) {\n <button\n kendoButton\n [toggleable]=\"true\"\n [selected]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (click)=\"OnValueChange(option.value)\"\n class=\"buttongroup-option\"\n >\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </button>\n }\n </kendo-buttongroup>\n }\n\n <!-- Radio Buttons -->\n @if (QuestionType === 'radio') {\n <div class=\"question-radio-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"radio-option\">\n <input\n type=\"radio\"\n kendoRadioButton\n [name]=\"Question.id\"\n [value]=\"option.value\"\n [checked]=\"Value === option.value\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange(option.value)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"radio-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n\n <!-- Dropdown -->\n @if (QuestionType === 'dropdown') {\n <select\n [formControl]=\"Control\"\n [disabled]=\"Disabled\"\n class=\"question-dropdown\"\n >\n <option value=\"\">Select an option...</option>\n @for (option of Options; track TrackByValue($index, option)) {\n <option [value]=\"option.value\">\n {{ option.label }}\n </option>\n }\n </select>\n }\n\n <!-- Slider -->\n @if (QuestionType === 'slider') {\n <div class=\"question-slider-container\">\n <kendo-slider\n [min]=\"GetSliderConfig().min\"\n [max]=\"GetSliderConfig().max\"\n [smallStep]=\"GetSliderConfig().step || 1\"\n [value]=\"$any(Value) || GetSliderConfig().min\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n class=\"question-slider\"\n ></kendo-slider>\n <div class=\"slider-value\">\n {{ Value || GetSliderConfig().min }}{{ GetSliderConfig().suffix || '' }}\n </div>\n </div>\n }\n\n <!-- Date Range -->\n @if (QuestionType === 'daterange') {\n <div class=\"question-daterange\">\n <kendo-datepicker\n [value]=\"$any(Value)?.start\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnDateRangeStartChange($event)\"\n placeholder=\"Start Date\"\n class=\"daterange-start\"\n ></kendo-datepicker>\n <span class=\"daterange-separator\">to</span>\n <kendo-datepicker\n [value]=\"$any(Value)?.end\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnDateRangeEndChange($event)\"\n placeholder=\"End Date\"\n class=\"daterange-end\"\n ></kendo-datepicker>\n </div>\n }\n\n <!-- Time Picker -->\n @if (QuestionType === 'time') {\n <kendo-timepicker\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (valueChange)=\"OnValueChange($event)\"\n [format]=\"'h:mm a'\"\n class=\"question-input\"\n ></kendo-timepicker>\n }\n\n <!-- Checkboxes -->\n @if (QuestionType === 'checkbox') {\n <div class=\"question-checkbox-group\">\n @for (option of Options; track TrackByValue($index, option)) {\n <div class=\"checkbox-option\">\n <input\n type=\"checkbox\"\n kendoCheckBox\n [checked]=\"IsChecked(option)\"\n [disabled]=\"Disabled\"\n (change)=\"ToggleCheckbox(option)\"\n [id]=\"Question.id + '_' + option.value\"\n />\n <label [for]=\"Question.id + '_' + option.value\" class=\"checkbox-label\">\n @if (option.icon) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </label>\n </div>\n }\n </div>\n }\n </div><!-- end input-container -->\n\n <!-- Validation Error -->\n @if (Control.invalid && Control.touched) {\n <div class=\"validation-error\">\n @if (Control.errors?.['required']) {\n <span>This field is required</span>\n }\n @if (Control.errors?.['email']) {\n <span>Please enter a valid email address</span>\n }\n @if (Control.errors?.['min']) {\n <span>Value must be at least {{ Min }}</span>\n }\n @if (Control.errors?.['max']) {\n <span>Value must be at most {{ Max }}</span>\n }\n </div>\n }\n</div>\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { AgentResponseForm } from '@memberjunction/ai-core-plus';
|
|
2
|
+
import { FormResponseField } from './form-response-utils';
|
|
3
|
+
import * as i0 from "@angular/core";
|
|
4
|
+
export declare class DynamicFormResponseComponent {
|
|
5
|
+
/**
|
|
6
|
+
* Optional form schema for resolving labels, types, and display values.
|
|
7
|
+
* When provided, enables type-aware formatting and option label resolution.
|
|
8
|
+
*/
|
|
9
|
+
FormDefinition: AgentResponseForm | null;
|
|
10
|
+
/**
|
|
11
|
+
* The response data as a JSON string or pre-parsed object.
|
|
12
|
+
* Keys are question IDs, values are the user's responses.
|
|
13
|
+
*/
|
|
14
|
+
ResponseData: string | Record<string, unknown> | null;
|
|
15
|
+
/**
|
|
16
|
+
* Optional title override. Falls back to FormDefinition.title, then 'Form Response'.
|
|
17
|
+
*/
|
|
18
|
+
Title: string | null;
|
|
19
|
+
/**
|
|
20
|
+
* Display mode: 'pill' for inline badge style, 'card' for block card style,
|
|
21
|
+
* 'auto' to choose based on field count (default).
|
|
22
|
+
*/
|
|
23
|
+
DisplayMode: 'pill' | 'card' | 'auto';
|
|
24
|
+
/** Resolved fields ready for display */
|
|
25
|
+
get Fields(): FormResponseField[];
|
|
26
|
+
/** Whether this displays as a single-field inline pill */
|
|
27
|
+
get IsSingleField(): boolean;
|
|
28
|
+
/** Whether there is any data to display */
|
|
29
|
+
get HasData(): boolean;
|
|
30
|
+
/** Resolved title for multi-field display */
|
|
31
|
+
get ResolvedTitle(): string;
|
|
32
|
+
/** Parse the ResponseData input into a plain object */
|
|
33
|
+
private get parsedResponseData();
|
|
34
|
+
/** Resolve a single key/value pair into a display-ready field */
|
|
35
|
+
private resolveField;
|
|
36
|
+
/** Find the matching question in the schema */
|
|
37
|
+
private findQuestion;
|
|
38
|
+
/** Track-by function for fields */
|
|
39
|
+
TrackByKey(_index: number, field: FormResponseField): string;
|
|
40
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<DynamicFormResponseComponent, never>;
|
|
41
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<DynamicFormResponseComponent, "mj-dynamic-form-response", never, { "FormDefinition": { "alias": "FormDefinition"; "required": false; }; "ResponseData": { "alias": "ResponseData"; "required": false; }; "Title": { "alias": "Title"; "required": false; }; "DisplayMode": { "alias": "DisplayMode"; "required": false; }; }, {}, never, never, false, never>;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=dynamic-form-response.component.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-form-response.component.d.ts","sourceRoot":"","sources":["../../../src/lib/dynamic-form-response/dynamic-form-response.component.ts"],"names":[],"mappings":"AAgCA,OAAO,EAAE,iBAAiB,EAAgB,MAAM,8BAA8B,CAAC;AAC/E,OAAO,EAAE,iBAAiB,EAAqB,MAAM,uBAAuB,CAAC;;AAE7E,qBAMa,4BAA4B;IACrC;;;OAGG;IACM,cAAc,EAAE,iBAAiB,GAAG,IAAI,CAAQ;IAEzD;;;OAGG;IACM,YAAY,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAQ;IAEtE;;OAEG;IACM,KAAK,EAAE,MAAM,GAAG,IAAI,CAAQ;IAErC;;;OAGG;IACM,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,CAAU;IAExD,wCAAwC;IACxC,IAAW,MAAM,IAAI,iBAAiB,EAAE,CASvC;IAED,0DAA0D;IAC1D,IAAW,aAAa,IAAI,OAAO,CAIlC;IAED,2CAA2C;IAC3C,IAAW,OAAO,IAAI,OAAO,CAE5B;IAED,6CAA6C;IAC7C,IAAW,aAAa,IAAI,MAAM,CAEjC;IAED,uDAAuD;IACvD,OAAO,KAAK,kBAAkB,GAQ7B;IAED,iEAAiE;IACjE,OAAO,CAAC,YAAY;IAcpB,+CAA+C;IAC/C,OAAO,CAAC,YAAY;IAKpB,mCAAmC;IAC5B,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,MAAM;yCAtF1D,4BAA4B;2CAA5B,4BAA4B;CAyFxC"}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Dynamic Form Response Component
|
|
3
|
+
*
|
|
4
|
+
* Read-only companion to `DynamicFormComponent`. Renders a submitted form
|
|
5
|
+
* response as a styled pill (single field) or card (multiple fields) with
|
|
6
|
+
* type-aware value formatting.
|
|
7
|
+
*
|
|
8
|
+
* Accepts two input modes:
|
|
9
|
+
* - **Schema + ResponseData**: Pass the original `AgentResponseForm` schema
|
|
10
|
+
* plus the JSON response data to get full labels and type-aware formatting.
|
|
11
|
+
* - **ResponseData only**: Pass just the JSON response data for a simpler
|
|
12
|
+
* key/value display when the schema is unavailable.
|
|
13
|
+
*
|
|
14
|
+
* @example With schema
|
|
15
|
+
* ```html
|
|
16
|
+
* <mj-dynamic-form-response
|
|
17
|
+
* [FormDefinition]="responseForm"
|
|
18
|
+
* [ResponseData]="responseDataJson">
|
|
19
|
+
* </mj-dynamic-form-response>
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* @example Without schema
|
|
23
|
+
* ```html
|
|
24
|
+
* <mj-dynamic-form-response
|
|
25
|
+
* [ResponseData]="responseDataJson">
|
|
26
|
+
* </mj-dynamic-form-response>
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* @module @memberjunction/ng-forms
|
|
30
|
+
*/
|
|
31
|
+
import { Component, Input } from '@angular/core';
|
|
32
|
+
import { FormResponseUtils } from './form-response-utils';
|
|
33
|
+
import * as i0 from "@angular/core";
|
|
34
|
+
export class DynamicFormResponseComponent {
|
|
35
|
+
/**
|
|
36
|
+
* Optional form schema for resolving labels, types, and display values.
|
|
37
|
+
* When provided, enables type-aware formatting and option label resolution.
|
|
38
|
+
*/
|
|
39
|
+
FormDefinition = null;
|
|
40
|
+
/**
|
|
41
|
+
* The response data as a JSON string or pre-parsed object.
|
|
42
|
+
* Keys are question IDs, values are the user's responses.
|
|
43
|
+
*/
|
|
44
|
+
ResponseData = null;
|
|
45
|
+
/**
|
|
46
|
+
* Optional title override. Falls back to FormDefinition.title, then 'Form Response'.
|
|
47
|
+
*/
|
|
48
|
+
Title = null;
|
|
49
|
+
/**
|
|
50
|
+
* Display mode: 'pill' for inline badge style, 'card' for block card style,
|
|
51
|
+
* 'auto' to choose based on field count (default).
|
|
52
|
+
*/
|
|
53
|
+
DisplayMode = 'auto';
|
|
54
|
+
/** Resolved fields ready for display */
|
|
55
|
+
get Fields() {
|
|
56
|
+
const data = this.parsedResponseData;
|
|
57
|
+
if (!data)
|
|
58
|
+
return [];
|
|
59
|
+
const entries = Object.entries(data).filter(([, value]) => value != null && value !== '' && !(Array.isArray(value) && value.length === 0));
|
|
60
|
+
return entries.map(([key, value]) => this.resolveField(key, value));
|
|
61
|
+
}
|
|
62
|
+
/** Whether this displays as a single-field inline pill */
|
|
63
|
+
get IsSingleField() {
|
|
64
|
+
if (this.DisplayMode === 'card')
|
|
65
|
+
return false;
|
|
66
|
+
if (this.DisplayMode === 'pill')
|
|
67
|
+
return true;
|
|
68
|
+
return this.Fields.length === 1;
|
|
69
|
+
}
|
|
70
|
+
/** Whether there is any data to display */
|
|
71
|
+
get HasData() {
|
|
72
|
+
return this.Fields.length > 0;
|
|
73
|
+
}
|
|
74
|
+
/** Resolved title for multi-field display */
|
|
75
|
+
get ResolvedTitle() {
|
|
76
|
+
return this.Title || this.FormDefinition?.title || 'Form Response';
|
|
77
|
+
}
|
|
78
|
+
/** Parse the ResponseData input into a plain object */
|
|
79
|
+
get parsedResponseData() {
|
|
80
|
+
if (!this.ResponseData)
|
|
81
|
+
return null;
|
|
82
|
+
if (typeof this.ResponseData === 'object')
|
|
83
|
+
return this.ResponseData;
|
|
84
|
+
try {
|
|
85
|
+
return JSON.parse(this.ResponseData);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Resolve a single key/value pair into a display-ready field */
|
|
92
|
+
resolveField(key, value) {
|
|
93
|
+
const question = this.findQuestion(key);
|
|
94
|
+
const type = question ? FormResponseUtils.GetQuestionTypeString(question) : null;
|
|
95
|
+
const displayValue = FormResponseUtils.FormatValue(value, question, type);
|
|
96
|
+
return {
|
|
97
|
+
key,
|
|
98
|
+
label: question?.label || FormResponseUtils.HumanizeKey(key),
|
|
99
|
+
value,
|
|
100
|
+
displayValue,
|
|
101
|
+
type
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** Find the matching question in the schema */
|
|
105
|
+
findQuestion(id) {
|
|
106
|
+
if (!this.FormDefinition)
|
|
107
|
+
return null;
|
|
108
|
+
return this.FormDefinition.questions.find(q => q.id === id) || null;
|
|
109
|
+
}
|
|
110
|
+
/** Track-by function for fields */
|
|
111
|
+
TrackByKey(_index, field) {
|
|
112
|
+
return field.key;
|
|
113
|
+
}
|
|
114
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DynamicFormResponseComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
115
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: DynamicFormResponseComponent, isStandalone: false, selector: "mj-dynamic-form-response", inputs: { FormDefinition: "FormDefinition", ResponseData: "ResponseData", Title: "Title", DisplayMode: "DisplayMode" }, ngImport: i0, template: "@if (HasData) {\n <!-- Single field: inline pill -->\n @if (IsSingleField) {\n <span class=\"form-response-pill single-field\">\n <i class=\"fa fa-check\" aria-hidden=\"true\"></i>\n {{ Fields[0].displayValue }}\n </span>\n } @else {\n <!-- Multiple fields: card layout -->\n <div class=\"form-response-pill multi-field\">\n <div class=\"pill-header\">\n <i class=\"fa fa-check-square\" aria-hidden=\"true\"></i>\n {{ ResolvedTitle }}\n </div>\n <div class=\"pill-fields\">\n @for (field of Fields; track TrackByKey($index, field)) {\n <div class=\"pill-field\">\n <div class=\"field-question\">{{ field.label }}</div>\n <div class=\"field-answer\">{{ field.displayValue }}</div>\n </div>\n }\n </div>\n </div>\n }\n}\n", styles: ["/* ==================== Form Response Display ==================== */\n\n.form-response-pill {\n transition: all 0.2s ease;\n}\n\n/* Single field - inline pill */\n.form-response-pill.single-field {\n display: inline-flex;\n align-items: center;\n vertical-align: middle;\n gap: 6px;\n padding: 4px 12px;\n background: var(--mj-status-success);\n color: var(--mj-text-inverse);\n border: 2px solid color-mix(in srgb, var(--mj-status-success) 40%, transparent);\n border-radius: 16px;\n font-size: 13px;\n font-weight: 600;\n box-shadow: var(--mj-shadow-md);\n cursor: default;\n white-space: nowrap;\n}\n\n.form-response-pill.single-field i {\n font-size: 11px;\n opacity: 0.95;\n}\n\n/* Multi-field - card layout */\n.form-response-pill.multi-field {\n display: inline-block;\n max-width: min(800px, 100%);\n width: auto;\n min-width: 400px;\n margin: 8px 0;\n vertical-align: top;\n}\n\n.form-response-pill .pill-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background: var(--mj-status-success);\n color: var(--mj-text-inverse);\n font-weight: 600;\n font-size: 13px;\n border-radius: 12px 12px 0 0;\n border: 2px solid color-mix(in srgb, var(--mj-status-success) 40%, transparent);\n border-bottom: none;\n box-shadow: var(--mj-shadow-md);\n}\n\n.form-response-pill .pill-header i {\n font-size: 12px;\n opacity: 0.95;\n}\n\n.form-response-pill .pill-fields {\n padding: 10px 12px;\n background: color-mix(in srgb, var(--mj-status-success) 8%, transparent);\n border: 2px solid color-mix(in srgb, var(--mj-status-success) 40%, transparent);\n border-top: none;\n border-radius: 0 0 12px 12px;\n}\n\n.form-response-pill .pill-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n font-size: 13px;\n}\n\n.form-response-pill .pill-field:not(:last-child) {\n border-bottom: 1px solid color-mix(in srgb, var(--mj-status-success) 20%, transparent);\n padding-bottom: 12px;\n margin-bottom: 0;\n}\n\n.form-response-pill .field-question {\n font-weight: 600;\n color: var(--mj-status-success);\n font-size: 12px;\n line-height: 1.4;\n text-transform: uppercase;\n letter-spacing: 0.3px;\n opacity: 0.9;\n}\n\n.form-response-pill .field-answer {\n color: var(--mj-text-primary);\n font-weight: 400;\n font-size: 15px;\n line-height: 1.6;\n padding: 8px 0 8px 12px;\n background: transparent;\n border-left: 3px solid color-mix(in srgb, var(--mj-status-success) 40%, transparent);\n font-style: italic;\n}\n\n/* Hover effects */\n.form-response-pill.single-field:hover {\n transform: translateY(-1px);\n box-shadow: var(--mj-shadow-lg);\n}\n\n.form-response-pill.multi-field:hover {\n transform: translateY(-1px);\n}\n\n.form-response-pill.multi-field:hover .pill-header {\n box-shadow: var(--mj-shadow-lg);\n}\n\n/* Responsive adjustments */\n@media (max-width: 600px) {\n .form-response-pill.multi-field {\n min-width: 300px;\n }\n}\n\n@media (max-width: 400px) {\n .form-response-pill.multi-field {\n min-width: 100%;\n }\n\n .form-response-pill .field-question {\n font-size: 11px;\n }\n\n .form-response-pill .field-answer {\n font-size: 14px;\n padding: 6px 0 6px 10px;\n }\n}\n"] });
|
|
116
|
+
}
|
|
117
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DynamicFormResponseComponent, decorators: [{
|
|
118
|
+
type: Component,
|
|
119
|
+
args: [{ standalone: false, selector: 'mj-dynamic-form-response', template: "@if (HasData) {\n <!-- Single field: inline pill -->\n @if (IsSingleField) {\n <span class=\"form-response-pill single-field\">\n <i class=\"fa fa-check\" aria-hidden=\"true\"></i>\n {{ Fields[0].displayValue }}\n </span>\n } @else {\n <!-- Multiple fields: card layout -->\n <div class=\"form-response-pill multi-field\">\n <div class=\"pill-header\">\n <i class=\"fa fa-check-square\" aria-hidden=\"true\"></i>\n {{ ResolvedTitle }}\n </div>\n <div class=\"pill-fields\">\n @for (field of Fields; track TrackByKey($index, field)) {\n <div class=\"pill-field\">\n <div class=\"field-question\">{{ field.label }}</div>\n <div class=\"field-answer\">{{ field.displayValue }}</div>\n </div>\n }\n </div>\n </div>\n }\n}\n", styles: ["/* ==================== Form Response Display ==================== */\n\n.form-response-pill {\n transition: all 0.2s ease;\n}\n\n/* Single field - inline pill */\n.form-response-pill.single-field {\n display: inline-flex;\n align-items: center;\n vertical-align: middle;\n gap: 6px;\n padding: 4px 12px;\n background: var(--mj-status-success);\n color: var(--mj-text-inverse);\n border: 2px solid color-mix(in srgb, var(--mj-status-success) 40%, transparent);\n border-radius: 16px;\n font-size: 13px;\n font-weight: 600;\n box-shadow: var(--mj-shadow-md);\n cursor: default;\n white-space: nowrap;\n}\n\n.form-response-pill.single-field i {\n font-size: 11px;\n opacity: 0.95;\n}\n\n/* Multi-field - card layout */\n.form-response-pill.multi-field {\n display: inline-block;\n max-width: min(800px, 100%);\n width: auto;\n min-width: 400px;\n margin: 8px 0;\n vertical-align: top;\n}\n\n.form-response-pill .pill-header {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background: var(--mj-status-success);\n color: var(--mj-text-inverse);\n font-weight: 600;\n font-size: 13px;\n border-radius: 12px 12px 0 0;\n border: 2px solid color-mix(in srgb, var(--mj-status-success) 40%, transparent);\n border-bottom: none;\n box-shadow: var(--mj-shadow-md);\n}\n\n.form-response-pill .pill-header i {\n font-size: 12px;\n opacity: 0.95;\n}\n\n.form-response-pill .pill-fields {\n padding: 10px 12px;\n background: color-mix(in srgb, var(--mj-status-success) 8%, transparent);\n border: 2px solid color-mix(in srgb, var(--mj-status-success) 40%, transparent);\n border-top: none;\n border-radius: 0 0 12px 12px;\n}\n\n.form-response-pill .pill-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n font-size: 13px;\n}\n\n.form-response-pill .pill-field:not(:last-child) {\n border-bottom: 1px solid color-mix(in srgb, var(--mj-status-success) 20%, transparent);\n padding-bottom: 12px;\n margin-bottom: 0;\n}\n\n.form-response-pill .field-question {\n font-weight: 600;\n color: var(--mj-status-success);\n font-size: 12px;\n line-height: 1.4;\n text-transform: uppercase;\n letter-spacing: 0.3px;\n opacity: 0.9;\n}\n\n.form-response-pill .field-answer {\n color: var(--mj-text-primary);\n font-weight: 400;\n font-size: 15px;\n line-height: 1.6;\n padding: 8px 0 8px 12px;\n background: transparent;\n border-left: 3px solid color-mix(in srgb, var(--mj-status-success) 40%, transparent);\n font-style: italic;\n}\n\n/* Hover effects */\n.form-response-pill.single-field:hover {\n transform: translateY(-1px);\n box-shadow: var(--mj-shadow-lg);\n}\n\n.form-response-pill.multi-field:hover {\n transform: translateY(-1px);\n}\n\n.form-response-pill.multi-field:hover .pill-header {\n box-shadow: var(--mj-shadow-lg);\n}\n\n/* Responsive adjustments */\n@media (max-width: 600px) {\n .form-response-pill.multi-field {\n min-width: 300px;\n }\n}\n\n@media (max-width: 400px) {\n .form-response-pill.multi-field {\n min-width: 100%;\n }\n\n .form-response-pill .field-question {\n font-size: 11px;\n }\n\n .form-response-pill .field-answer {\n font-size: 14px;\n padding: 6px 0 6px 10px;\n }\n}\n"] }]
|
|
120
|
+
}], propDecorators: { FormDefinition: [{
|
|
121
|
+
type: Input
|
|
122
|
+
}], ResponseData: [{
|
|
123
|
+
type: Input
|
|
124
|
+
}], Title: [{
|
|
125
|
+
type: Input
|
|
126
|
+
}], DisplayMode: [{
|
|
127
|
+
type: Input
|
|
128
|
+
}] } });
|
|
129
|
+
//# sourceMappingURL=dynamic-form-response.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dynamic-form-response.component.js","sourceRoot":"","sources":["../../../src/lib/dynamic-form-response/dynamic-form-response.component.ts","../../../src/lib/dynamic-form-response/dynamic-form-response.component.html"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Dynamic Form Response Component\n *\n * Read-only companion to `DynamicFormComponent`. Renders a submitted form\n * response as a styled pill (single field) or card (multiple fields) with\n * type-aware value formatting.\n *\n * Accepts two input modes:\n * - **Schema + ResponseData**: Pass the original `AgentResponseForm` schema\n * plus the JSON response data to get full labels and type-aware formatting.\n * - **ResponseData only**: Pass just the JSON response data for a simpler\n * key/value display when the schema is unavailable.\n *\n * @example With schema\n * ```html\n * <mj-dynamic-form-response\n * [FormDefinition]=\"responseForm\"\n * [ResponseData]=\"responseDataJson\">\n * </mj-dynamic-form-response>\n * ```\n *\n * @example Without schema\n * ```html\n * <mj-dynamic-form-response\n * [ResponseData]=\"responseDataJson\">\n * </mj-dynamic-form-response>\n * ```\n *\n * @module @memberjunction/ng-forms\n */\n\nimport { Component, Input } from '@angular/core';\nimport { AgentResponseForm, FormQuestion } from '@memberjunction/ai-core-plus';\nimport { FormResponseField, FormResponseUtils } from './form-response-utils';\n\n@Component({\n standalone: false,\n selector: 'mj-dynamic-form-response',\n templateUrl: './dynamic-form-response.component.html',\n styleUrls: ['./dynamic-form-response.component.css']\n})\nexport class DynamicFormResponseComponent {\n /**\n * Optional form schema for resolving labels, types, and display values.\n * When provided, enables type-aware formatting and option label resolution.\n */\n @Input() FormDefinition: AgentResponseForm | null = null;\n\n /**\n * The response data as a JSON string or pre-parsed object.\n * Keys are question IDs, values are the user's responses.\n */\n @Input() ResponseData: string | Record<string, unknown> | null = null;\n\n /**\n * Optional title override. Falls back to FormDefinition.title, then 'Form Response'.\n */\n @Input() Title: string | null = null;\n\n /**\n * Display mode: 'pill' for inline badge style, 'card' for block card style,\n * 'auto' to choose based on field count (default).\n */\n @Input() DisplayMode: 'pill' | 'card' | 'auto' = 'auto';\n\n /** Resolved fields ready for display */\n public get Fields(): FormResponseField[] {\n const data = this.parsedResponseData;\n if (!data) return [];\n\n const entries = Object.entries(data).filter(\n ([, value]) => value != null && value !== '' && !(Array.isArray(value) && value.length === 0)\n );\n\n return entries.map(([key, value]) => this.resolveField(key, value));\n }\n\n /** Whether this displays as a single-field inline pill */\n public get IsSingleField(): boolean {\n if (this.DisplayMode === 'card') return false;\n if (this.DisplayMode === 'pill') return true;\n return this.Fields.length === 1;\n }\n\n /** Whether there is any data to display */\n public get HasData(): boolean {\n return this.Fields.length > 0;\n }\n\n /** Resolved title for multi-field display */\n public get ResolvedTitle(): string {\n return this.Title || this.FormDefinition?.title || 'Form Response';\n }\n\n /** Parse the ResponseData input into a plain object */\n private get parsedResponseData(): Record<string, unknown> | null {\n if (!this.ResponseData) return null;\n if (typeof this.ResponseData === 'object') return this.ResponseData;\n try {\n return JSON.parse(this.ResponseData) as Record<string, unknown>;\n } catch {\n return null;\n }\n }\n\n /** Resolve a single key/value pair into a display-ready field */\n private resolveField(key: string, value: unknown): FormResponseField {\n const question = this.findQuestion(key);\n const type = question ? FormResponseUtils.GetQuestionTypeString(question) : null;\n const displayValue = FormResponseUtils.FormatValue(value, question, type);\n\n return {\n key,\n label: question?.label || FormResponseUtils.HumanizeKey(key),\n value,\n displayValue,\n type\n };\n }\n\n /** Find the matching question in the schema */\n private findQuestion(id: string): FormQuestion | null {\n if (!this.FormDefinition) return null;\n return this.FormDefinition.questions.find(q => q.id === id) || null;\n }\n\n /** Track-by function for fields */\n public TrackByKey(_index: number, field: FormResponseField): string {\n return field.key;\n }\n}\n","@if (HasData) {\n <!-- Single field: inline pill -->\n @if (IsSingleField) {\n <span class=\"form-response-pill single-field\">\n <i class=\"fa fa-check\" aria-hidden=\"true\"></i>\n {{ Fields[0].displayValue }}\n </span>\n } @else {\n <!-- Multiple fields: card layout -->\n <div class=\"form-response-pill multi-field\">\n <div class=\"pill-header\">\n <i class=\"fa fa-check-square\" aria-hidden=\"true\"></i>\n {{ ResolvedTitle }}\n </div>\n <div class=\"pill-fields\">\n @for (field of Fields; track TrackByKey($index, field)) {\n <div class=\"pill-field\">\n <div class=\"field-question\">{{ field.label }}</div>\n <div class=\"field-answer\">{{ field.displayValue }}</div>\n </div>\n }\n </div>\n </div>\n }\n}\n"]}
|