@memberjunction/ng-forms 5.21.0 → 5.23.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/__tests__/form-response-utils.test.d.ts +2 -0
- package/dist/__tests__/form-response-utils.test.d.ts.map +1 -0
- package/dist/__tests__/form-response-utils.test.js +249 -0
- package/dist/__tests__/form-response-utils.test.js.map +1 -0
- package/dist/__tests__/index.test.d.ts +2 -0
- package/dist/__tests__/index.test.d.ts.map +1 -0
- package/dist/__tests__/index.test.js +7 -0
- package/dist/__tests__/index.test.js.map +1 -0
- package/dist/lib/dynamic-form/dynamic-form.component.js +3 -3
- package/dist/lib/dynamic-form/dynamic-form.component.js.map +1 -1
- package/dist/lib/dynamic-form-field/dynamic-form-field.component.js +3 -5
- package/dist/lib/dynamic-form-field/dynamic-form-field.component.js.map +1 -1
- package/dist/lib/forms.module.d.ts +2 -4
- package/dist/lib/forms.module.d.ts.map +1 -1
- package/dist/lib/forms.module.js +10 -13
- package/dist/lib/forms.module.js.map +1 -1
- package/package.json +4 -6
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form-response-utils.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/form-response-utils.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { FormResponseUtils } from '../lib/dynamic-form-response/form-response-utils';
|
|
3
|
+
/** Helper to build a choice-type question (buttongroup / radio / dropdown / checkbox). */
|
|
4
|
+
function choiceQuestion(choiceType, options) {
|
|
5
|
+
return { id: 'q1', label: 'Question 1', type: { type: choiceType, options } };
|
|
6
|
+
}
|
|
7
|
+
/** Helper to build a simple-type question (text, date, etc.). */
|
|
8
|
+
function simpleQuestion(type) {
|
|
9
|
+
return { id: 'q1', label: 'Q', type };
|
|
10
|
+
}
|
|
11
|
+
// We cast through `unknown` so TypeScript accepts our minimal mock shapes.
|
|
12
|
+
const asQ = (q) => q;
|
|
13
|
+
describe('FormResponseUtils', () => {
|
|
14
|
+
// ------------------------------------------------------------------ FormatValue
|
|
15
|
+
describe('FormatValue', () => {
|
|
16
|
+
it('returns empty string for null value', () => {
|
|
17
|
+
expect(FormResponseUtils.FormatValue(null, null, 'text')).toBe('');
|
|
18
|
+
});
|
|
19
|
+
it('returns empty string for undefined value', () => {
|
|
20
|
+
expect(FormResponseUtils.FormatValue(undefined, null, 'text')).toBe('');
|
|
21
|
+
});
|
|
22
|
+
it('returns stringified value for plain text type', () => {
|
|
23
|
+
expect(FormResponseUtils.FormatValue('hello', null, 'text')).toBe('hello');
|
|
24
|
+
});
|
|
25
|
+
it('returns stringified number for unknown type', () => {
|
|
26
|
+
expect(FormResponseUtils.FormatValue(42, null, null)).toBe('42');
|
|
27
|
+
});
|
|
28
|
+
it('formats date type without time', () => {
|
|
29
|
+
// Use an explicit UTC datetime to avoid local-timezone date shifting
|
|
30
|
+
const result = FormResponseUtils.FormatValue('2025-06-15T12:00:00Z', null, 'date');
|
|
31
|
+
expect(result).toContain('Jun');
|
|
32
|
+
expect(result).toContain('2025');
|
|
33
|
+
});
|
|
34
|
+
it('formats datetime type with time', () => {
|
|
35
|
+
const result = FormResponseUtils.FormatValue('2025-06-15T14:30:00Z', null, 'datetime');
|
|
36
|
+
expect(result).toContain('Jun');
|
|
37
|
+
expect(result).toContain('15');
|
|
38
|
+
expect(result).toContain('2025');
|
|
39
|
+
// Should include time portion
|
|
40
|
+
expect(result).toMatch(/\d{1,2}:\d{2}/);
|
|
41
|
+
});
|
|
42
|
+
it('formats time type', () => {
|
|
43
|
+
const result = FormResponseUtils.FormatValue('14:30', null, 'time');
|
|
44
|
+
expect(result).toMatch(/2:30\s*PM/i);
|
|
45
|
+
});
|
|
46
|
+
it('formats daterange type', () => {
|
|
47
|
+
// Use mid-month dates to avoid timezone boundary shifts
|
|
48
|
+
const result = FormResponseUtils.FormatValue({ start: '2025-01-15', end: '2025-12-15' }, null, 'daterange');
|
|
49
|
+
expect(result).toContain('Jan');
|
|
50
|
+
expect(result).toContain('Dec');
|
|
51
|
+
expect(result).toContain('to');
|
|
52
|
+
});
|
|
53
|
+
it('resolves option label for buttongroup choice type', () => {
|
|
54
|
+
const q = choiceQuestion('buttongroup', [
|
|
55
|
+
{ value: 'a', label: 'Alpha' },
|
|
56
|
+
{ value: 'b', label: 'Beta' },
|
|
57
|
+
]);
|
|
58
|
+
expect(FormResponseUtils.FormatValue('b', asQ(q), 'buttongroup')).toBe('Beta');
|
|
59
|
+
});
|
|
60
|
+
it('resolves option label for dropdown choice type', () => {
|
|
61
|
+
const q = choiceQuestion('dropdown', [
|
|
62
|
+
{ value: 1, label: 'One' },
|
|
63
|
+
{ value: 2, label: 'Two' },
|
|
64
|
+
]);
|
|
65
|
+
// value comparison is done via String(), so numeric "1" matches
|
|
66
|
+
expect(FormResponseUtils.FormatValue(1, asQ(q), 'dropdown')).toBe('One');
|
|
67
|
+
});
|
|
68
|
+
it('falls back to stringified value when no matching option', () => {
|
|
69
|
+
const q = choiceQuestion('radio', [{ value: 'a', label: 'Alpha' }]);
|
|
70
|
+
expect(FormResponseUtils.FormatValue('z', asQ(q), 'radio')).toBe('z');
|
|
71
|
+
});
|
|
72
|
+
it('auto-detects ISO datetime strings for default type', () => {
|
|
73
|
+
const result = FormResponseUtils.FormatValue('2025-03-10T15:45:00Z', null, null);
|
|
74
|
+
expect(result).toContain('Mar');
|
|
75
|
+
expect(result).toContain('10');
|
|
76
|
+
expect(result).toContain('2025');
|
|
77
|
+
});
|
|
78
|
+
it('auto-detects ISO midnight as date-only', () => {
|
|
79
|
+
const result = FormResponseUtils.FormatValue('2025-03-15T00:00:00Z', null, null);
|
|
80
|
+
// Midnight UTC should be treated as date-only (no time component)
|
|
81
|
+
expect(result).toContain('Mar');
|
|
82
|
+
expect(result).toContain('2025');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
// -------------------------------------------------------------- ResolveOptionLabel
|
|
86
|
+
describe('ResolveOptionLabel', () => {
|
|
87
|
+
it('returns matching label', () => {
|
|
88
|
+
const q = choiceQuestion('radio', [
|
|
89
|
+
{ value: 'x', label: 'X Label' },
|
|
90
|
+
{ value: 'y', label: 'Y Label' },
|
|
91
|
+
]);
|
|
92
|
+
expect(FormResponseUtils.ResolveOptionLabel(asQ(q), 'y')).toBe('Y Label');
|
|
93
|
+
});
|
|
94
|
+
it('returns null when no match found', () => {
|
|
95
|
+
const q = choiceQuestion('radio', [{ value: 'x', label: 'X' }]);
|
|
96
|
+
expect(FormResponseUtils.ResolveOptionLabel(asQ(q), 'missing')).toBeNull();
|
|
97
|
+
});
|
|
98
|
+
it('returns null when question type is a simple string (no options)', () => {
|
|
99
|
+
const q = simpleQuestion('text');
|
|
100
|
+
expect(FormResponseUtils.ResolveOptionLabel(asQ(q), 'anything')).toBeNull();
|
|
101
|
+
});
|
|
102
|
+
it('matches numeric value via string coercion', () => {
|
|
103
|
+
const q = choiceQuestion('dropdown', [{ value: 42, label: 'Forty Two' }]);
|
|
104
|
+
expect(FormResponseUtils.ResolveOptionLabel(asQ(q), 42)).toBe('Forty Two');
|
|
105
|
+
});
|
|
106
|
+
it('matches boolean value via string coercion', () => {
|
|
107
|
+
const q = choiceQuestion('checkbox', [{ value: true, label: 'Yes' }]);
|
|
108
|
+
expect(FormResponseUtils.ResolveOptionLabel(asQ(q), true)).toBe('Yes');
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
// ------------------------------------------------------------------- FormatDate
|
|
112
|
+
describe('FormatDate', () => {
|
|
113
|
+
it('formats a valid date without time', () => {
|
|
114
|
+
const result = FormResponseUtils.FormatDate('2025-12-25', false);
|
|
115
|
+
expect(result).toContain('Dec');
|
|
116
|
+
expect(result).toContain('25');
|
|
117
|
+
expect(result).toContain('2025');
|
|
118
|
+
});
|
|
119
|
+
it('formats a valid date with time', () => {
|
|
120
|
+
const result = FormResponseUtils.FormatDate('2025-12-25T08:30:00Z', true);
|
|
121
|
+
expect(result).toContain('Dec');
|
|
122
|
+
expect(result).toContain('25');
|
|
123
|
+
expect(result).toMatch(/\d{1,2}:\d{2}/);
|
|
124
|
+
});
|
|
125
|
+
it('returns original string for invalid date', () => {
|
|
126
|
+
expect(FormResponseUtils.FormatDate('not-a-date', false)).toBe('not-a-date');
|
|
127
|
+
});
|
|
128
|
+
it('returns original string for empty string', () => {
|
|
129
|
+
expect(FormResponseUtils.FormatDate('', false)).toBe('');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
// ------------------------------------------------------------------- FormatTime
|
|
133
|
+
describe('FormatTime', () => {
|
|
134
|
+
it('formats 24h time to 12h', () => {
|
|
135
|
+
const result = FormResponseUtils.FormatTime('14:30');
|
|
136
|
+
expect(result).toMatch(/2:30\s*PM/i);
|
|
137
|
+
});
|
|
138
|
+
it('formats midnight', () => {
|
|
139
|
+
const result = FormResponseUtils.FormatTime('00:00');
|
|
140
|
+
expect(result).toMatch(/12:00\s*AM/i);
|
|
141
|
+
});
|
|
142
|
+
it('formats noon', () => {
|
|
143
|
+
const result = FormResponseUtils.FormatTime('12:00');
|
|
144
|
+
expect(result).toMatch(/12:00\s*PM/i);
|
|
145
|
+
});
|
|
146
|
+
it('returns locale string for malformed time (NaN hours produces Invalid Date)', () => {
|
|
147
|
+
// 'bad'.split(':').map(Number) yields [NaN, NaN], which makes an invalid Date.
|
|
148
|
+
// toLocaleTimeString on an invalid Date returns 'Invalid Date' in Node.
|
|
149
|
+
const result = FormResponseUtils.FormatTime('bad');
|
|
150
|
+
expect(result).toBe('Invalid Date');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
// --------------------------------------------------------------- FormatDateRange
|
|
154
|
+
describe('FormatDateRange', () => {
|
|
155
|
+
it('formats start and end dates', () => {
|
|
156
|
+
// Use mid-month dates to avoid timezone boundary shifts
|
|
157
|
+
const result = FormResponseUtils.FormatDateRange({ start: '2025-01-15', end: '2025-06-15' });
|
|
158
|
+
expect(result).toContain('Jan');
|
|
159
|
+
expect(result).toContain('Jun');
|
|
160
|
+
expect(result).toContain('to');
|
|
161
|
+
});
|
|
162
|
+
it('returns JSON string for non-range values', () => {
|
|
163
|
+
expect(FormResponseUtils.FormatDateRange('some string')).toBe('"some string"');
|
|
164
|
+
});
|
|
165
|
+
it('returns JSON string for null', () => {
|
|
166
|
+
expect(FormResponseUtils.FormatDateRange(null)).toBe('null');
|
|
167
|
+
});
|
|
168
|
+
it('returns JSON string for object without start/end', () => {
|
|
169
|
+
const result = FormResponseUtils.FormatDateRange({ foo: 'bar' });
|
|
170
|
+
expect(result).toContain('foo');
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
// ----------------------------------------------------------------- HumanizeKey
|
|
174
|
+
describe('HumanizeKey', () => {
|
|
175
|
+
it('converts camelCase to title case', () => {
|
|
176
|
+
expect(FormResponseUtils.HumanizeKey('firstName')).toBe('First Name');
|
|
177
|
+
});
|
|
178
|
+
it('converts snake_case to title case', () => {
|
|
179
|
+
expect(FormResponseUtils.HumanizeKey('first_name')).toBe('First Name');
|
|
180
|
+
});
|
|
181
|
+
it('converts PascalCase to title case', () => {
|
|
182
|
+
expect(FormResponseUtils.HumanizeKey('FirstName')).toBe('First Name');
|
|
183
|
+
});
|
|
184
|
+
it('handles kebab-case', () => {
|
|
185
|
+
expect(FormResponseUtils.HumanizeKey('first-name')).toBe('First Name');
|
|
186
|
+
});
|
|
187
|
+
it('handles single word', () => {
|
|
188
|
+
expect(FormResponseUtils.HumanizeKey('name')).toBe('Name');
|
|
189
|
+
});
|
|
190
|
+
it('handles all-uppercase abbreviation (treated as individual letters)', () => {
|
|
191
|
+
// Each uppercase letter gets a space before it
|
|
192
|
+
const result = FormResponseUtils.HumanizeKey('XMLParser');
|
|
193
|
+
// "XMLParser" -> " X M L Parser" -> trimmed -> "X M L Parser" -> title-cased
|
|
194
|
+
expect(result).toBe('X M L Parser');
|
|
195
|
+
});
|
|
196
|
+
it('handles empty string', () => {
|
|
197
|
+
expect(FormResponseUtils.HumanizeKey('')).toBe('');
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
// ------------------------------------------------------------------ EscapeHtml
|
|
201
|
+
describe('EscapeHtml', () => {
|
|
202
|
+
it('escapes ampersands', () => {
|
|
203
|
+
expect(FormResponseUtils.EscapeHtml('a&b')).toBe('a&b');
|
|
204
|
+
});
|
|
205
|
+
it('escapes less-than', () => {
|
|
206
|
+
expect(FormResponseUtils.EscapeHtml('<script>')).toBe('<script>');
|
|
207
|
+
});
|
|
208
|
+
it('escapes greater-than', () => {
|
|
209
|
+
expect(FormResponseUtils.EscapeHtml('a>b')).toBe('a>b');
|
|
210
|
+
});
|
|
211
|
+
it('escapes double quotes', () => {
|
|
212
|
+
expect(FormResponseUtils.EscapeHtml('"hello"')).toBe('"hello"');
|
|
213
|
+
});
|
|
214
|
+
it('escapes single quotes', () => {
|
|
215
|
+
expect(FormResponseUtils.EscapeHtml("it's")).toBe("it's");
|
|
216
|
+
});
|
|
217
|
+
it('handles all special chars together', () => {
|
|
218
|
+
expect(FormResponseUtils.EscapeHtml('<a href="x">&\'</a>')).toBe('<a href="x">&'</a>');
|
|
219
|
+
});
|
|
220
|
+
it('leaves safe strings unchanged', () => {
|
|
221
|
+
expect(FormResponseUtils.EscapeHtml('Hello World 123')).toBe('Hello World 123');
|
|
222
|
+
});
|
|
223
|
+
it('handles empty string', () => {
|
|
224
|
+
expect(FormResponseUtils.EscapeHtml('')).toBe('');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
// --------------------------------------------------------------- FormatValueHtml
|
|
228
|
+
describe('FormatValueHtml', () => {
|
|
229
|
+
it('formats and escapes HTML', () => {
|
|
230
|
+
const result = FormResponseUtils.FormatValueHtml('<b>bold</b>', null, 'text');
|
|
231
|
+
expect(result).toBe('<b>bold</b>');
|
|
232
|
+
});
|
|
233
|
+
it('returns empty string for null', () => {
|
|
234
|
+
expect(FormResponseUtils.FormatValueHtml(null, null, 'text')).toBe('');
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
// --------------------------------------------------------- GetQuestionTypeString
|
|
238
|
+
describe('GetQuestionTypeString', () => {
|
|
239
|
+
it('returns string type directly', () => {
|
|
240
|
+
const q = simpleQuestion('text');
|
|
241
|
+
expect(FormResponseUtils.GetQuestionTypeString(asQ(q))).toBe('text');
|
|
242
|
+
});
|
|
243
|
+
it('returns type property from object type', () => {
|
|
244
|
+
const q = choiceQuestion('dropdown', []);
|
|
245
|
+
expect(FormResponseUtils.GetQuestionTypeString(asQ(q))).toBe('dropdown');
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
//# sourceMappingURL=form-response-utils.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"form-response-utils.test.js","sourceRoot":"","sources":["../../src/__tests__/form-response-utils.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect } from 'vitest';\nimport { FormResponseUtils } from '../lib/dynamic-form-response/form-response-utils';\n\n// Minimal types matching the real FormQuestion / FormOption interfaces from @memberjunction/ai-core-plus.\n// We only need the subset that FormResponseUtils actually reads.\ninterface MockFormOption {\n value: string | number | boolean;\n label: string;\n}\n\ninterface MockFormQuestion {\n id: string;\n label: string;\n type: string | { type: string; options: MockFormOption[] };\n required?: boolean;\n}\n\n/** Helper to build a choice-type question (buttongroup / radio / dropdown / checkbox). */\nfunction choiceQuestion(\n choiceType: 'buttongroup' | 'radio' | 'dropdown' | 'checkbox',\n options: MockFormOption[],\n): MockFormQuestion {\n return { id: 'q1', label: 'Question 1', type: { type: choiceType, options } };\n}\n\n/** Helper to build a simple-type question (text, date, etc.). */\nfunction simpleQuestion(type: string): MockFormQuestion {\n return { id: 'q1', label: 'Q', type };\n}\n\n// We cast through `unknown` so TypeScript accepts our minimal mock shapes.\nconst asQ = (q: MockFormQuestion) => q as unknown as Parameters<typeof FormResponseUtils.FormatValue>[1];\n\ndescribe('FormResponseUtils', () => {\n // ------------------------------------------------------------------ FormatValue\n describe('FormatValue', () => {\n it('returns empty string for null value', () => {\n expect(FormResponseUtils.FormatValue(null, null, 'text')).toBe('');\n });\n\n it('returns empty string for undefined value', () => {\n expect(FormResponseUtils.FormatValue(undefined, null, 'text')).toBe('');\n });\n\n it('returns stringified value for plain text type', () => {\n expect(FormResponseUtils.FormatValue('hello', null, 'text')).toBe('hello');\n });\n\n it('returns stringified number for unknown type', () => {\n expect(FormResponseUtils.FormatValue(42, null, null)).toBe('42');\n });\n\n it('formats date type without time', () => {\n // Use an explicit UTC datetime to avoid local-timezone date shifting\n const result = FormResponseUtils.FormatValue('2025-06-15T12:00:00Z', null, 'date');\n expect(result).toContain('Jun');\n expect(result).toContain('2025');\n });\n\n it('formats datetime type with time', () => {\n const result = FormResponseUtils.FormatValue('2025-06-15T14:30:00Z', null, 'datetime');\n expect(result).toContain('Jun');\n expect(result).toContain('15');\n expect(result).toContain('2025');\n // Should include time portion\n expect(result).toMatch(/\\d{1,2}:\\d{2}/);\n });\n\n it('formats time type', () => {\n const result = FormResponseUtils.FormatValue('14:30', null, 'time');\n expect(result).toMatch(/2:30\\s*PM/i);\n });\n\n it('formats daterange type', () => {\n // Use mid-month dates to avoid timezone boundary shifts\n const result = FormResponseUtils.FormatValue(\n { start: '2025-01-15', end: '2025-12-15' },\n null,\n 'daterange',\n );\n expect(result).toContain('Jan');\n expect(result).toContain('Dec');\n expect(result).toContain('to');\n });\n\n it('resolves option label for buttongroup choice type', () => {\n const q = choiceQuestion('buttongroup', [\n { value: 'a', label: 'Alpha' },\n { value: 'b', label: 'Beta' },\n ]);\n expect(FormResponseUtils.FormatValue('b', asQ(q), 'buttongroup')).toBe('Beta');\n });\n\n it('resolves option label for dropdown choice type', () => {\n const q = choiceQuestion('dropdown', [\n { value: 1, label: 'One' },\n { value: 2, label: 'Two' },\n ]);\n // value comparison is done via String(), so numeric \"1\" matches\n expect(FormResponseUtils.FormatValue(1, asQ(q), 'dropdown')).toBe('One');\n });\n\n it('falls back to stringified value when no matching option', () => {\n const q = choiceQuestion('radio', [{ value: 'a', label: 'Alpha' }]);\n expect(FormResponseUtils.FormatValue('z', asQ(q), 'radio')).toBe('z');\n });\n\n it('auto-detects ISO datetime strings for default type', () => {\n const result = FormResponseUtils.FormatValue('2025-03-10T15:45:00Z', null, null);\n expect(result).toContain('Mar');\n expect(result).toContain('10');\n expect(result).toContain('2025');\n });\n\n it('auto-detects ISO midnight as date-only', () => {\n const result = FormResponseUtils.FormatValue('2025-03-15T00:00:00Z', null, null);\n // Midnight UTC should be treated as date-only (no time component)\n expect(result).toContain('Mar');\n expect(result).toContain('2025');\n });\n });\n\n // -------------------------------------------------------------- ResolveOptionLabel\n describe('ResolveOptionLabel', () => {\n it('returns matching label', () => {\n const q = choiceQuestion('radio', [\n { value: 'x', label: 'X Label' },\n { value: 'y', label: 'Y Label' },\n ]);\n expect(FormResponseUtils.ResolveOptionLabel(asQ(q)!, 'y')).toBe('Y Label');\n });\n\n it('returns null when no match found', () => {\n const q = choiceQuestion('radio', [{ value: 'x', label: 'X' }]);\n expect(FormResponseUtils.ResolveOptionLabel(asQ(q)!, 'missing')).toBeNull();\n });\n\n it('returns null when question type is a simple string (no options)', () => {\n const q = simpleQuestion('text');\n expect(FormResponseUtils.ResolveOptionLabel(asQ(q)!, 'anything')).toBeNull();\n });\n\n it('matches numeric value via string coercion', () => {\n const q = choiceQuestion('dropdown', [{ value: 42, label: 'Forty Two' }]);\n expect(FormResponseUtils.ResolveOptionLabel(asQ(q)!, 42)).toBe('Forty Two');\n });\n\n it('matches boolean value via string coercion', () => {\n const q = choiceQuestion('checkbox', [{ value: true, label: 'Yes' }]);\n expect(FormResponseUtils.ResolveOptionLabel(asQ(q)!, true)).toBe('Yes');\n });\n });\n\n // ------------------------------------------------------------------- FormatDate\n describe('FormatDate', () => {\n it('formats a valid date without time', () => {\n const result = FormResponseUtils.FormatDate('2025-12-25', false);\n expect(result).toContain('Dec');\n expect(result).toContain('25');\n expect(result).toContain('2025');\n });\n\n it('formats a valid date with time', () => {\n const result = FormResponseUtils.FormatDate('2025-12-25T08:30:00Z', true);\n expect(result).toContain('Dec');\n expect(result).toContain('25');\n expect(result).toMatch(/\\d{1,2}:\\d{2}/);\n });\n\n it('returns original string for invalid date', () => {\n expect(FormResponseUtils.FormatDate('not-a-date', false)).toBe('not-a-date');\n });\n\n it('returns original string for empty string', () => {\n expect(FormResponseUtils.FormatDate('', false)).toBe('');\n });\n });\n\n // ------------------------------------------------------------------- FormatTime\n describe('FormatTime', () => {\n it('formats 24h time to 12h', () => {\n const result = FormResponseUtils.FormatTime('14:30');\n expect(result).toMatch(/2:30\\s*PM/i);\n });\n\n it('formats midnight', () => {\n const result = FormResponseUtils.FormatTime('00:00');\n expect(result).toMatch(/12:00\\s*AM/i);\n });\n\n it('formats noon', () => {\n const result = FormResponseUtils.FormatTime('12:00');\n expect(result).toMatch(/12:00\\s*PM/i);\n });\n\n it('returns locale string for malformed time (NaN hours produces Invalid Date)', () => {\n // 'bad'.split(':').map(Number) yields [NaN, NaN], which makes an invalid Date.\n // toLocaleTimeString on an invalid Date returns 'Invalid Date' in Node.\n const result = FormResponseUtils.FormatTime('bad');\n expect(result).toBe('Invalid Date');\n });\n });\n\n // --------------------------------------------------------------- FormatDateRange\n describe('FormatDateRange', () => {\n it('formats start and end dates', () => {\n // Use mid-month dates to avoid timezone boundary shifts\n const result = FormResponseUtils.FormatDateRange({ start: '2025-01-15', end: '2025-06-15' });\n expect(result).toContain('Jan');\n expect(result).toContain('Jun');\n expect(result).toContain('to');\n });\n\n it('returns JSON string for non-range values', () => {\n expect(FormResponseUtils.FormatDateRange('some string')).toBe('\"some string\"');\n });\n\n it('returns JSON string for null', () => {\n expect(FormResponseUtils.FormatDateRange(null)).toBe('null');\n });\n\n it('returns JSON string for object without start/end', () => {\n const result = FormResponseUtils.FormatDateRange({ foo: 'bar' });\n expect(result).toContain('foo');\n });\n });\n\n // ----------------------------------------------------------------- HumanizeKey\n describe('HumanizeKey', () => {\n it('converts camelCase to title case', () => {\n expect(FormResponseUtils.HumanizeKey('firstName')).toBe('First Name');\n });\n\n it('converts snake_case to title case', () => {\n expect(FormResponseUtils.HumanizeKey('first_name')).toBe('First Name');\n });\n\n it('converts PascalCase to title case', () => {\n expect(FormResponseUtils.HumanizeKey('FirstName')).toBe('First Name');\n });\n\n it('handles kebab-case', () => {\n expect(FormResponseUtils.HumanizeKey('first-name')).toBe('First Name');\n });\n\n it('handles single word', () => {\n expect(FormResponseUtils.HumanizeKey('name')).toBe('Name');\n });\n\n it('handles all-uppercase abbreviation (treated as individual letters)', () => {\n // Each uppercase letter gets a space before it\n const result = FormResponseUtils.HumanizeKey('XMLParser');\n // \"XMLParser\" -> \" X M L Parser\" -> trimmed -> \"X M L Parser\" -> title-cased\n expect(result).toBe('X M L Parser');\n });\n\n it('handles empty string', () => {\n expect(FormResponseUtils.HumanizeKey('')).toBe('');\n });\n });\n\n // ------------------------------------------------------------------ EscapeHtml\n describe('EscapeHtml', () => {\n it('escapes ampersands', () => {\n expect(FormResponseUtils.EscapeHtml('a&b')).toBe('a&b');\n });\n\n it('escapes less-than', () => {\n expect(FormResponseUtils.EscapeHtml('<script>')).toBe('<script>');\n });\n\n it('escapes greater-than', () => {\n expect(FormResponseUtils.EscapeHtml('a>b')).toBe('a>b');\n });\n\n it('escapes double quotes', () => {\n expect(FormResponseUtils.EscapeHtml('\"hello\"')).toBe('"hello"');\n });\n\n it('escapes single quotes', () => {\n expect(FormResponseUtils.EscapeHtml(\"it's\")).toBe(\"it's\");\n });\n\n it('handles all special chars together', () => {\n expect(FormResponseUtils.EscapeHtml('<a href=\"x\">&\\'</a>')).toBe(\n '<a href="x">&'</a>',\n );\n });\n\n it('leaves safe strings unchanged', () => {\n expect(FormResponseUtils.EscapeHtml('Hello World 123')).toBe('Hello World 123');\n });\n\n it('handles empty string', () => {\n expect(FormResponseUtils.EscapeHtml('')).toBe('');\n });\n });\n\n // --------------------------------------------------------------- FormatValueHtml\n describe('FormatValueHtml', () => {\n it('formats and escapes HTML', () => {\n const result = FormResponseUtils.FormatValueHtml('<b>bold</b>', null, 'text');\n expect(result).toBe('<b>bold</b>');\n });\n\n it('returns empty string for null', () => {\n expect(FormResponseUtils.FormatValueHtml(null, null, 'text')).toBe('');\n });\n });\n\n // --------------------------------------------------------- GetQuestionTypeString\n describe('GetQuestionTypeString', () => {\n it('returns string type directly', () => {\n const q = simpleQuestion('text');\n expect(FormResponseUtils.GetQuestionTypeString(asQ(q)!)).toBe('text');\n });\n\n it('returns type property from object type', () => {\n const q = choiceQuestion('dropdown', []);\n expect(FormResponseUtils.GetQuestionTypeString(asQ(q)!)).toBe('dropdown');\n });\n });\n});\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.js","sourceRoot":"","sources":["../../src/__tests__/index.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAE9C,QAAQ,CAAC,0BAA0B,EAAE,GAAG,EAAE;IACxC,EAAE,CAAC,4BAA4B,EAAE,GAAG,EAAE;QACpC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC","sourcesContent":["import { describe, it, expect } from 'vitest';\n\ndescribe('@memberjunction/ng-forms', () => {\n it('should have a passing test', () => {\n expect(true).toBe(true);\n });\n});\n"]}
|
|
@@ -35,7 +35,7 @@ import { Component, Input, Output, EventEmitter, ChangeDetectorRef, inject } fro
|
|
|
35
35
|
import { FormGroup, FormControl, Validators } from '@angular/forms';
|
|
36
36
|
import * as i0 from "@angular/core";
|
|
37
37
|
import * as i1 from "@angular/forms";
|
|
38
|
-
import * as i2 from "@
|
|
38
|
+
import * as i2 from "@memberjunction/ng-ui-components";
|
|
39
39
|
import * as i3 from "../dynamic-form-field/dynamic-form-field.component";
|
|
40
40
|
export class DynamicFormComponent {
|
|
41
41
|
/**
|
|
@@ -241,11 +241,11 @@ export class DynamicFormComponent {
|
|
|
241
241
|
this.FormGroup = new FormGroup(controls);
|
|
242
242
|
}
|
|
243
243
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DynamicFormComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
244
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: DynamicFormComponent, isStandalone: false, selector: "mj-dynamic-form", inputs: { FormDefinition: "FormDefinition", Disabled: "Disabled", Visible: "Visible" }, outputs: { FormSubmitted: "FormSubmitted" }, ngImport: i0, template: "@if (IsVisible) {\n <div class=\"dynamic-form\">\n <!-- Simple Choice Mode (single question, no title, buttongroup/radio) -->\n @if (IsSimpleChoice) {\n <div class=\"simple-choice-buttons\">\n @for (option of GetOptions(FormDefinition.questions[0]); track TrackByValue($index, option)) {\n <button\n
|
|
244
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.3", type: DynamicFormComponent, isStandalone: false, selector: "mj-dynamic-form", inputs: { FormDefinition: "FormDefinition", Disabled: "Disabled", Visible: "Visible" }, outputs: { FormSubmitted: "FormSubmitted" }, ngImport: i0, template: "@if (IsVisible) {\n <div class=\"dynamic-form\">\n <!-- Simple Choice Mode (single question, no title, buttongroup/radio) -->\n @if (IsSimpleChoice) {\n <div class=\"simple-choice-buttons\">\n @for (option of GetOptions(FormDefinition.questions[0]); track TrackByValue($index, option)) {\n <button\n mjButton\n [variant]=\"IsSimpleChoiceSelected(option.value) ? 'primary' : 'secondary'\"\n [disabled]=\"Disabled\"\n (click)=\"OnSimpleChoiceClick(option.value)\"\n class=\"choice-button\"\n [class.selected]=\"IsSimpleChoiceSelected(option.value)\"\n >\n <span>\n @if (IsSimpleChoiceSelected(option.value)) {\n <i class=\"fa-solid fa-check\" aria-hidden=\"true\"></i>\n } @else if (HasIcon(option)) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </span>\n </button>\n }\n </div>\n }\n <!-- Full Form Mode (complex forms) -->\n @if (!IsSimpleChoice) {\n <form [formGroup]=\"FormGroup\" (ngSubmit)=\"OnSubmit()\" class=\"response-form\">\n <!-- Form Title -->\n @if (FormDefinition.title) {\n <h3 class=\"form-title\">{{ FormDefinition.title }}</h3>\n }\n <!-- Form Description -->\n @if (FormDefinition.description) {\n <p class=\"form-description\">{{ FormDefinition.description }}</p>\n }\n <!-- Questions (excludes footer choice question) -->\n @if (MainQuestions.length > 0) {\n <div class=\"form-questions\">\n @for (question of MainQuestions; track TrackByQuestionId($index, question)) {\n <mj-dynamic-form-field\n [Question]=\"question\"\n [Control]=\"GetControl(question.id)\"\n [formControlName]=\"question.id\"\n ></mj-dynamic-form-field>\n }\n </div>\n }\n <!-- Footer Actions -->\n <div class=\"form-actions\">\n <!-- Footer Choice Buttons (replaces Submit when present) -->\n @if (FooterChoiceQuestion) {\n @for (option of GetOptions(FooterChoiceQuestion); track TrackByValue($index, option)) {\n <button\n mjButton\n type=\"button\"\n [variant]=\"IsFooterChoiceSelected(option.value) ? 'primary' : 'secondary'\"\n [disabled]=\"Disabled\"\n (click)=\"OnFooterChoiceClick(option.value)\"\n class=\"footer-choice-button\"\n [class.selected]=\"IsFooterChoiceSelected(option.value)\"\n >\n <span>\n @if (IsFooterChoiceSelected(option.value)) {\n <i class=\"fa-solid fa-check\" aria-hidden=\"true\"></i>\n } @else if (HasIcon(option)) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </span>\n </button>\n }\n }\n <!-- Standard Submit Button (when no footer choice) -->\n @if (ShowSubmitButton) {\n <button\n mjButton\n type=\"submit\"\n variant=\"primary\"\n [disabled]=\"Disabled || IsSubmitting || FormGroup.invalid\"\n class=\"submit-button\"\n >\n @if (!IsSubmitting) {\n <span>\n <i class=\"fa fa-check\" aria-hidden=\"true\"></i>\n {{ SubmitLabel }}\n </span>\n }\n @if (IsSubmitting) {\n <span>\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n Submitting...\n </span>\n }\n </button>\n }\n </div>\n </form>\n }\n </div>\n}\n", styles: [".dynamic-form {\n margin-top: 16px;\n display: block;\n width: fit-content;\n max-width: 600px;\n}\n\n/* Simple Choice Mode */\n.simple-choice-buttons {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 12px;\n}\n\n.choice-button {\n padding: 8px 16px;\n font-size: 14px;\n}\n\n.choice-button i,\n.choice-button i.fa-solid {\n margin-right: 6px;\n}\n\n.choice-button.selected {\n box-shadow: 0 0 0 2px var(--mj-brand-primary);\n}\n\n/* Full Form Mode */\n.response-form {\n background: color-mix(in srgb, var(--mj-brand-primary) 3%, transparent);\n border: 2px solid color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n border-radius: 12px;\n padding: 0;\n margin-top: 12px;\n box-shadow: 0 2px 8px color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n overflow: hidden;\n}\n\n.form-title {\n margin: 0;\n padding: 12px 16px;\n font-size: 14px;\n font-weight: 600;\n color: var(--mj-text-inverse);\n background: var(--mj-brand-primary);\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.form-title::before {\n content: \"\\f044\";\n font-family: \"Font Awesome 6 Free\";\n font-weight: 900;\n font-size: 13px;\n opacity: 0.95;\n}\n\n.form-description {\n margin: 0;\n padding: 12px 16px;\n font-size: 13px;\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n border-bottom: 1px solid color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n font-style: italic;\n}\n\n.form-questions {\n padding: 16px;\n background: var(--mj-bg-surface-card);\n}\n\n.form-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n justify-content: flex-start;\n padding: 12px 16px;\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, transparent);\n border-top: 1px solid color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n.submit-button {\n min-width: 120px;\n}\n\n/* Footer choice buttons - horizontal on desktop with text wrapping */\n.footer-choice-button {\n min-width: 120px;\n max-width: 100%;\n white-space: normal;\n text-align: left;\n line-height: 1.3;\n padding: 10px 16px;\n}\n\n.footer-choice-button i.fa,\n.footer-choice-button i.fa-solid {\n margin-right: 6px;\n flex-shrink: 0;\n}\n\n.footer-choice-button.selected {\n box-shadow: 0 0 0 2px var(--mj-brand-primary);\n}\n\n.submit-button i.fa {\n margin-right: 6px;\n}\n\n/* Responsive Design */\n@media (max-width: 768px) {\n .dynamic-form {\n margin-top: 12px;\n }\n\n .response-form {\n border-radius: 10px;\n }\n\n .form-questions {\n padding: 12px;\n }\n\n .form-actions {\n padding: 10px 12px;\n }\n\n .simple-choice-buttons {\n gap: 8px;\n }\n\n .choice-button {\n padding: 10px 14px;\n font-size: 13px;\n }\n}\n\n@media (max-width: 480px) {\n .dynamic-form {\n margin-top: 10px;\n width: 100%;\n max-width: 100%;\n }\n\n .response-form {\n border-radius: 8px;\n border-width: 1.5px;\n }\n\n .form-title {\n font-size: 13px;\n padding: 10px 12px;\n }\n\n .form-description {\n font-size: 12px;\n padding: 10px 12px;\n }\n\n .form-questions {\n padding: 10px;\n }\n\n .form-actions {\n padding: 8px 10px;\n flex-direction: column;\n }\n\n .choice-button {\n width: 100%;\n min-height: 44px;\n padding: 12px 16px;\n font-size: 14px;\n }\n\n .footer-choice-button {\n width: 100%;\n min-height: 44px;\n font-size: 14px;\n }\n\n .submit-button {\n width: 100%;\n min-height: 44px;\n font-size: 13px;\n }\n}\n"], dependencies: [{ kind: "directive", type: i1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: i2.MJButtonDirective, selector: "button[mjButton], a[mjButton]", inputs: ["variant", "size", "toggleable", "selected"], outputs: ["selectedChange"] }, { kind: "component", type: i3.DynamicFormFieldComponent, selector: "mj-dynamic-form-field", inputs: ["Question", "Control"] }] });
|
|
245
245
|
}
|
|
246
246
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DynamicFormComponent, decorators: [{
|
|
247
247
|
type: Component,
|
|
248
|
-
args: [{ standalone: false, selector: 'mj-dynamic-form', template: "@if (IsVisible) {\n <div class=\"dynamic-form\">\n <!-- Simple Choice Mode (single question, no title, buttongroup/radio) -->\n @if (IsSimpleChoice) {\n <div class=\"simple-choice-buttons\">\n @for (option of GetOptions(FormDefinition.questions[0]); track TrackByValue($index, option)) {\n <button\n
|
|
248
|
+
args: [{ standalone: false, selector: 'mj-dynamic-form', template: "@if (IsVisible) {\n <div class=\"dynamic-form\">\n <!-- Simple Choice Mode (single question, no title, buttongroup/radio) -->\n @if (IsSimpleChoice) {\n <div class=\"simple-choice-buttons\">\n @for (option of GetOptions(FormDefinition.questions[0]); track TrackByValue($index, option)) {\n <button\n mjButton\n [variant]=\"IsSimpleChoiceSelected(option.value) ? 'primary' : 'secondary'\"\n [disabled]=\"Disabled\"\n (click)=\"OnSimpleChoiceClick(option.value)\"\n class=\"choice-button\"\n [class.selected]=\"IsSimpleChoiceSelected(option.value)\"\n >\n <span>\n @if (IsSimpleChoiceSelected(option.value)) {\n <i class=\"fa-solid fa-check\" aria-hidden=\"true\"></i>\n } @else if (HasIcon(option)) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </span>\n </button>\n }\n </div>\n }\n <!-- Full Form Mode (complex forms) -->\n @if (!IsSimpleChoice) {\n <form [formGroup]=\"FormGroup\" (ngSubmit)=\"OnSubmit()\" class=\"response-form\">\n <!-- Form Title -->\n @if (FormDefinition.title) {\n <h3 class=\"form-title\">{{ FormDefinition.title }}</h3>\n }\n <!-- Form Description -->\n @if (FormDefinition.description) {\n <p class=\"form-description\">{{ FormDefinition.description }}</p>\n }\n <!-- Questions (excludes footer choice question) -->\n @if (MainQuestions.length > 0) {\n <div class=\"form-questions\">\n @for (question of MainQuestions; track TrackByQuestionId($index, question)) {\n <mj-dynamic-form-field\n [Question]=\"question\"\n [Control]=\"GetControl(question.id)\"\n [formControlName]=\"question.id\"\n ></mj-dynamic-form-field>\n }\n </div>\n }\n <!-- Footer Actions -->\n <div class=\"form-actions\">\n <!-- Footer Choice Buttons (replaces Submit when present) -->\n @if (FooterChoiceQuestion) {\n @for (option of GetOptions(FooterChoiceQuestion); track TrackByValue($index, option)) {\n <button\n mjButton\n type=\"button\"\n [variant]=\"IsFooterChoiceSelected(option.value) ? 'primary' : 'secondary'\"\n [disabled]=\"Disabled\"\n (click)=\"OnFooterChoiceClick(option.value)\"\n class=\"footer-choice-button\"\n [class.selected]=\"IsFooterChoiceSelected(option.value)\"\n >\n <span>\n @if (IsFooterChoiceSelected(option.value)) {\n <i class=\"fa-solid fa-check\" aria-hidden=\"true\"></i>\n } @else if (HasIcon(option)) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </span>\n </button>\n }\n }\n <!-- Standard Submit Button (when no footer choice) -->\n @if (ShowSubmitButton) {\n <button\n mjButton\n type=\"submit\"\n variant=\"primary\"\n [disabled]=\"Disabled || IsSubmitting || FormGroup.invalid\"\n class=\"submit-button\"\n >\n @if (!IsSubmitting) {\n <span>\n <i class=\"fa fa-check\" aria-hidden=\"true\"></i>\n {{ SubmitLabel }}\n </span>\n }\n @if (IsSubmitting) {\n <span>\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n Submitting...\n </span>\n }\n </button>\n }\n </div>\n </form>\n }\n </div>\n}\n", styles: [".dynamic-form {\n margin-top: 16px;\n display: block;\n width: fit-content;\n max-width: 600px;\n}\n\n/* Simple Choice Mode */\n.simple-choice-buttons {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 12px;\n}\n\n.choice-button {\n padding: 8px 16px;\n font-size: 14px;\n}\n\n.choice-button i,\n.choice-button i.fa-solid {\n margin-right: 6px;\n}\n\n.choice-button.selected {\n box-shadow: 0 0 0 2px var(--mj-brand-primary);\n}\n\n/* Full Form Mode */\n.response-form {\n background: color-mix(in srgb, var(--mj-brand-primary) 3%, transparent);\n border: 2px solid color-mix(in srgb, var(--mj-brand-primary) 30%, transparent);\n border-radius: 12px;\n padding: 0;\n margin-top: 12px;\n box-shadow: 0 2px 8px color-mix(in srgb, var(--mj-brand-primary) 10%, transparent);\n overflow: hidden;\n}\n\n.form-title {\n margin: 0;\n padding: 12px 16px;\n font-size: 14px;\n font-weight: 600;\n color: var(--mj-text-inverse);\n background: var(--mj-brand-primary);\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.form-title::before {\n content: \"\\f044\";\n font-family: \"Font Awesome 6 Free\";\n font-weight: 900;\n font-size: 13px;\n opacity: 0.95;\n}\n\n.form-description {\n margin: 0;\n padding: 12px 16px;\n font-size: 13px;\n color: var(--mj-brand-primary);\n background: color-mix(in srgb, var(--mj-brand-primary) 8%, transparent);\n border-bottom: 1px solid color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n font-style: italic;\n}\n\n.form-questions {\n padding: 16px;\n background: var(--mj-bg-surface-card);\n}\n\n.form-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n justify-content: flex-start;\n padding: 12px 16px;\n background: color-mix(in srgb, var(--mj-brand-primary) 5%, transparent);\n border-top: 1px solid color-mix(in srgb, var(--mj-brand-primary) 15%, transparent);\n}\n\n.submit-button {\n min-width: 120px;\n}\n\n/* Footer choice buttons - horizontal on desktop with text wrapping */\n.footer-choice-button {\n min-width: 120px;\n max-width: 100%;\n white-space: normal;\n text-align: left;\n line-height: 1.3;\n padding: 10px 16px;\n}\n\n.footer-choice-button i.fa,\n.footer-choice-button i.fa-solid {\n margin-right: 6px;\n flex-shrink: 0;\n}\n\n.footer-choice-button.selected {\n box-shadow: 0 0 0 2px var(--mj-brand-primary);\n}\n\n.submit-button i.fa {\n margin-right: 6px;\n}\n\n/* Responsive Design */\n@media (max-width: 768px) {\n .dynamic-form {\n margin-top: 12px;\n }\n\n .response-form {\n border-radius: 10px;\n }\n\n .form-questions {\n padding: 12px;\n }\n\n .form-actions {\n padding: 10px 12px;\n }\n\n .simple-choice-buttons {\n gap: 8px;\n }\n\n .choice-button {\n padding: 10px 14px;\n font-size: 13px;\n }\n}\n\n@media (max-width: 480px) {\n .dynamic-form {\n margin-top: 10px;\n width: 100%;\n max-width: 100%;\n }\n\n .response-form {\n border-radius: 8px;\n border-width: 1.5px;\n }\n\n .form-title {\n font-size: 13px;\n padding: 10px 12px;\n }\n\n .form-description {\n font-size: 12px;\n padding: 10px 12px;\n }\n\n .form-questions {\n padding: 10px;\n }\n\n .form-actions {\n padding: 8px 10px;\n flex-direction: column;\n }\n\n .choice-button {\n width: 100%;\n min-height: 44px;\n padding: 12px 16px;\n font-size: 14px;\n }\n\n .footer-choice-button {\n width: 100%;\n min-height: 44px;\n font-size: 14px;\n }\n\n .submit-button {\n width: 100%;\n min-height: 44px;\n font-size: 13px;\n }\n}\n"] }]
|
|
249
249
|
}], propDecorators: { FormDefinition: [{
|
|
250
250
|
type: Input
|
|
251
251
|
}], Disabled: [{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dynamic-form.component.js","sourceRoot":"","sources":["../../../src/lib/dynamic-form/dynamic-form.component.ts","../../../src/lib/dynamic-form/dynamic-form.component.html"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Dynamic Form Component\n *\n * Top-level container that renders a complete form from an `AgentResponseForm`\n * schema definition. Automatically builds a reactive `FormGroup`, handles\n * validation, and emits structured response data on submission.\n *\n * Supports two rendering modes:\n * - **Simple choice mode**: Single buttongroup/radio question with no title —\n * renders as standalone buttons for quick one-click responses.\n * - **Full form mode**: Title bar, description, multiple fields, and an action\n * footer with either a Submit button or choice buttons.\n *\n * @example Basic usage\n * ```html\n * <mj-dynamic-form\n * [FormDefinition]=\"responseForm\"\n * [Disabled]=\"isSaving\"\n * (FormSubmitted)=\"onFormSubmitted($event)\">\n * </mj-dynamic-form>\n * ```\n *\n * @example With visibility control\n * ```html\n * <mj-dynamic-form\n * [FormDefinition]=\"responseForm\"\n * [Visible]=\"isLastMessage && isOwner\"\n * (FormSubmitted)=\"onFormSubmitted($event)\">\n * </mj-dynamic-form>\n * ```\n *\n * @module @memberjunction/ng-forms\n */\n\nimport { Component, Input, Output, EventEmitter, OnInit, ChangeDetectorRef, inject } from '@angular/core';\nimport { FormGroup, FormControl, Validators } from '@angular/forms';\nimport { AgentResponseForm, FormQuestion, FormOption } from '@memberjunction/ai-core-plus';\n\n@Component({\n standalone: false,\n selector: 'mj-dynamic-form',\n templateUrl: './dynamic-form.component.html',\n styleUrls: ['./dynamic-form.component.css']\n})\nexport class DynamicFormComponent implements OnInit {\n /**\n * The form schema that drives rendering.\n * Contains title, description, questions array, and optional submit label.\n */\n @Input() FormDefinition!: AgentResponseForm;\n\n /**\n * Whether the form is disabled (e.g. while saving).\n * Disables all inputs and buttons.\n */\n @Input() Disabled = false;\n\n /**\n * Controls overall visibility. When `false`, the component renders nothing.\n * Defaults to `true` so the form is always shown unless explicitly hidden.\n */\n @Input() Visible = true;\n\n /**\n * Emitted when the user submits the form. The payload is a key-value map\n * where keys are question IDs and values are the user's responses.\n */\n @Output() FormSubmitted = new EventEmitter<Record<string, string | number | boolean | Date>>();\n\n /** The reactive form group built from the form definition */\n public FormGroup!: FormGroup;\n\n /** Whether a submission is in progress */\n public IsSubmitting = false;\n\n private cdr = inject(ChangeDetectorRef);\n\n ngOnInit(): void {\n this.buildFormGroup();\n }\n\n /**\n * Whether this form renders as a simple choice (single buttongroup/radio, no title).\n */\n public get IsSimpleChoice(): boolean {\n if (!this.FormDefinition) return false;\n return (\n this.FormDefinition.questions.length === 1 &&\n !this.FormDefinition.title &&\n this.isChoiceQuestion(this.FormDefinition.questions[0])\n );\n }\n\n /**\n * The choice question that should render in the footer (if present).\n * When a form has a buttongroup/radio question, those buttons replace\n * the standard Submit button in the footer.\n */\n public get FooterChoiceQuestion(): FormQuestion | null {\n if (!this.FormDefinition || this.IsSimpleChoice) return null;\n const choiceQuestions = this.FormDefinition.questions.filter(q => this.isChoiceQuestion(q));\n return choiceQuestions.length > 0 ? choiceQuestions[choiceQuestions.length - 1] : null;\n }\n\n /**\n * Questions rendered in the main form area (excludes footer choice question).\n */\n public get MainQuestions(): FormQuestion[] {\n if (!this.FormDefinition) return [];\n const footerChoice = this.FooterChoiceQuestion;\n if (!footerChoice) return this.FormDefinition.questions;\n return this.FormDefinition.questions.filter(q => q.id !== footerChoice.id);\n }\n\n /**\n * Whether to show the standard Submit button (true when no footer choice question).\n */\n public get ShowSubmitButton(): boolean {\n return !this.FooterChoiceQuestion;\n }\n\n /**\n * Whether the form should be visible (has questions and Visible is true).\n */\n public get IsVisible(): boolean {\n return (\n this.Visible &&\n !!this.FormDefinition &&\n this.FormDefinition.questions.length > 0\n );\n }\n\n /**\n * Text for the submit button. Falls back to \"Submit\" if not specified.\n */\n public get SubmitLabel(): string {\n return this.FormDefinition?.submitLabel || 'Submit';\n }\n\n /** Get the FormControl for a specific question by ID */\n public GetControl(questionId: string): FormControl {\n return this.FormGroup.get(questionId) as FormControl;\n }\n\n /** Handle simple choice button click (single-question forms) */\n public OnSimpleChoiceClick(value: string | number | boolean): void {\n if (!this.Disabled) {\n const question = this.FormDefinition.questions[0];\n // Update the FormControl so external readers (e.g. collectFormData) can access the value\n this.FormGroup.get(question.id)?.setValue(value);\n this.FormSubmitted.emit({ [question.id]: value });\n }\n }\n\n /** Whether a given option value is the currently selected simple choice */\n public IsSimpleChoiceSelected(value: string | number | boolean): boolean {\n if (!this.IsSimpleChoice) return false;\n const question = this.FormDefinition.questions[0];\n const control = this.FormGroup.get(question.id);\n return control?.value === value;\n }\n\n /** Handle footer choice button click — selects the option and emits the form data */\n public OnFooterChoiceClick(value: string | number | boolean): void {\n if (!this.Disabled && this.FooterChoiceQuestion) {\n const mainControlKeys = this.MainQuestions.map(q => q.id);\n let hasInvalidFields = false;\n\n for (const key of mainControlKeys) {\n const control = this.FormGroup.get(key);\n if (control?.invalid) {\n control.markAsTouched();\n hasInvalidFields = true;\n }\n }\n\n if (hasInvalidFields) return;\n\n // Update the FormControl so external readers (e.g. collectFormData) can access the value\n this.FormGroup.get(this.FooterChoiceQuestion.id)?.setValue(value);\n const formData = { ...this.FormGroup.value };\n this.FormSubmitted.emit(formData);\n }\n }\n\n /** Whether a given option value is the currently selected footer choice */\n public IsFooterChoiceSelected(value: string | number | boolean): boolean {\n if (!this.FooterChoiceQuestion) return false;\n const control = this.FormGroup.get(this.FooterChoiceQuestion.id);\n return control?.value === value;\n }\n\n /** Handle full form submission */\n public OnSubmit(): void {\n if (this.FormGroup.invalid) {\n Object.keys(this.FormGroup.controls).forEach(key => {\n this.FormGroup.controls[key].markAsTouched();\n });\n return;\n }\n\n if (!this.Disabled && !this.IsSubmitting) {\n this.IsSubmitting = true;\n this.FormSubmitted.emit(this.FormGroup.value);\n\n // Reset submitting state after a short delay — the parent container\n // is responsible for hiding the form or handling the response\n setTimeout(() => {\n this.IsSubmitting = false;\n this.cdr.detectChanges();\n }, 1000);\n }\n }\n\n /** Get options for a choice question */\n public GetOptions(question: FormQuestion): FormOption[] {\n if (typeof question.type === 'object' && 'options' in question.type) {\n return question.type.options;\n }\n return [];\n }\n\n /** Check if an option has an icon */\n public HasIcon(option: FormOption): boolean {\n return !!option?.icon;\n }\n\n /** Track-by function for questions */\n public TrackByQuestionId(_index: number, question: FormQuestion): string {\n return question.id;\n }\n\n /** Track-by function for options */\n public TrackByValue(_index: number, option: FormOption): string | number | boolean {\n return option.value;\n }\n\n private isChoiceQuestion(question: FormQuestion): boolean {\n const type = typeof question.type === 'string' ? question.type : question.type.type;\n return ['buttongroup', 'radio'].includes(type);\n }\n\n private buildFormGroup(): void {\n const controls: Record<string, FormControl> = {};\n\n for (const question of this.FormDefinition.questions) {\n const validators = [];\n if (question.required) {\n validators.push(Validators.required);\n }\n\n const questionType = typeof question.type === 'string' ? question.type : question.type.type;\n if (questionType === 'email') {\n validators.push(Validators.email);\n }\n\n if (['number', 'currency'].includes(questionType) && typeof question.type === 'object') {\n if ('min' in question.type && question.type.min != null) {\n validators.push(Validators.min(question.type.min as number));\n }\n if ('max' in question.type && question.type.max != null) {\n validators.push(Validators.max(question.type.max as number));\n }\n }\n\n let initialValue = question.defaultValue;\n if (!initialValue) {\n if (questionType === 'checkbox' && typeof question.type === 'object' && 'multiple' in question.type && question.type.multiple) {\n initialValue = [];\n } else {\n initialValue = null;\n }\n }\n\n controls[question.id] = new FormControl(initialValue, validators);\n }\n\n this.FormGroup = new FormGroup(controls);\n }\n}\n","@if (IsVisible) {\n <div class=\"dynamic-form\">\n <!-- Simple Choice Mode (single question, no title, buttongroup/radio) -->\n @if (IsSimpleChoice) {\n <div class=\"simple-choice-buttons\">\n @for (option of GetOptions(FormDefinition.questions[0]); track TrackByValue($index, option)) {\n <button\n kendoButton\n [themeColor]=\"IsSimpleChoiceSelected(option.value) ? 'primary' : 'base'\"\n [disabled]=\"Disabled\"\n (click)=\"OnSimpleChoiceClick(option.value)\"\n class=\"choice-button\"\n [class.selected]=\"IsSimpleChoiceSelected(option.value)\"\n >\n <span>\n @if (IsSimpleChoiceSelected(option.value)) {\n <i class=\"fa-solid fa-check\" aria-hidden=\"true\"></i>\n } @else if (HasIcon(option)) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </span>\n </button>\n }\n </div>\n }\n <!-- Full Form Mode (complex forms) -->\n @if (!IsSimpleChoice) {\n <form [formGroup]=\"FormGroup\" (ngSubmit)=\"OnSubmit()\" class=\"response-form\">\n <!-- Form Title -->\n @if (FormDefinition.title) {\n <h3 class=\"form-title\">{{ FormDefinition.title }}</h3>\n }\n <!-- Form Description -->\n @if (FormDefinition.description) {\n <p class=\"form-description\">{{ FormDefinition.description }}</p>\n }\n <!-- Questions (excludes footer choice question) -->\n @if (MainQuestions.length > 0) {\n <div class=\"form-questions\">\n @for (question of MainQuestions; track TrackByQuestionId($index, question)) {\n <mj-dynamic-form-field\n [Question]=\"question\"\n [Control]=\"GetControl(question.id)\"\n [formControlName]=\"question.id\"\n ></mj-dynamic-form-field>\n }\n </div>\n }\n <!-- Footer Actions -->\n <div class=\"form-actions\">\n <!-- Footer Choice Buttons (replaces Submit when present) -->\n @if (FooterChoiceQuestion) {\n @for (option of GetOptions(FooterChoiceQuestion); track TrackByValue($index, option)) {\n <button\n kendoButton\n type=\"button\"\n [themeColor]=\"IsFooterChoiceSelected(option.value) ? 'primary' : 'base'\"\n [disabled]=\"Disabled\"\n (click)=\"OnFooterChoiceClick(option.value)\"\n class=\"footer-choice-button\"\n [class.selected]=\"IsFooterChoiceSelected(option.value)\"\n >\n <span>\n @if (IsFooterChoiceSelected(option.value)) {\n <i class=\"fa-solid fa-check\" aria-hidden=\"true\"></i>\n } @else if (HasIcon(option)) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </span>\n </button>\n }\n }\n <!-- Standard Submit Button (when no footer choice) -->\n @if (ShowSubmitButton) {\n <button\n kendoButton\n type=\"submit\"\n [themeColor]=\"'primary'\"\n [disabled]=\"Disabled || IsSubmitting || FormGroup.invalid\"\n class=\"submit-button\"\n >\n @if (!IsSubmitting) {\n <span>\n <i class=\"fa fa-check\" aria-hidden=\"true\"></i>\n {{ SubmitLabel }}\n </span>\n }\n @if (IsSubmitting) {\n <span>\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n Submitting...\n </span>\n }\n </button>\n }\n </div>\n </form>\n }\n </div>\n}\n"]}
|
|
1
|
+
{"version":3,"file":"dynamic-form.component.js","sourceRoot":"","sources":["../../../src/lib/dynamic-form/dynamic-form.component.ts","../../../src/lib/dynamic-form/dynamic-form.component.html"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Dynamic Form Component\n *\n * Top-level container that renders a complete form from an `AgentResponseForm`\n * schema definition. Automatically builds a reactive `FormGroup`, handles\n * validation, and emits structured response data on submission.\n *\n * Supports two rendering modes:\n * - **Simple choice mode**: Single buttongroup/radio question with no title —\n * renders as standalone buttons for quick one-click responses.\n * - **Full form mode**: Title bar, description, multiple fields, and an action\n * footer with either a Submit button or choice buttons.\n *\n * @example Basic usage\n * ```html\n * <mj-dynamic-form\n * [FormDefinition]=\"responseForm\"\n * [Disabled]=\"isSaving\"\n * (FormSubmitted)=\"onFormSubmitted($event)\">\n * </mj-dynamic-form>\n * ```\n *\n * @example With visibility control\n * ```html\n * <mj-dynamic-form\n * [FormDefinition]=\"responseForm\"\n * [Visible]=\"isLastMessage && isOwner\"\n * (FormSubmitted)=\"onFormSubmitted($event)\">\n * </mj-dynamic-form>\n * ```\n *\n * @module @memberjunction/ng-forms\n */\n\nimport { Component, Input, Output, EventEmitter, OnInit, ChangeDetectorRef, inject } from '@angular/core';\nimport { FormGroup, FormControl, Validators } from '@angular/forms';\nimport { AgentResponseForm, FormQuestion, FormOption } from '@memberjunction/ai-core-plus';\n\n@Component({\n standalone: false,\n selector: 'mj-dynamic-form',\n templateUrl: './dynamic-form.component.html',\n styleUrls: ['./dynamic-form.component.css']\n})\nexport class DynamicFormComponent implements OnInit {\n /**\n * The form schema that drives rendering.\n * Contains title, description, questions array, and optional submit label.\n */\n @Input() FormDefinition!: AgentResponseForm;\n\n /**\n * Whether the form is disabled (e.g. while saving).\n * Disables all inputs and buttons.\n */\n @Input() Disabled = false;\n\n /**\n * Controls overall visibility. When `false`, the component renders nothing.\n * Defaults to `true` so the form is always shown unless explicitly hidden.\n */\n @Input() Visible = true;\n\n /**\n * Emitted when the user submits the form. The payload is a key-value map\n * where keys are question IDs and values are the user's responses.\n */\n @Output() FormSubmitted = new EventEmitter<Record<string, string | number | boolean | Date>>();\n\n /** The reactive form group built from the form definition */\n public FormGroup!: FormGroup;\n\n /** Whether a submission is in progress */\n public IsSubmitting = false;\n\n private cdr = inject(ChangeDetectorRef);\n\n ngOnInit(): void {\n this.buildFormGroup();\n }\n\n /**\n * Whether this form renders as a simple choice (single buttongroup/radio, no title).\n */\n public get IsSimpleChoice(): boolean {\n if (!this.FormDefinition) return false;\n return (\n this.FormDefinition.questions.length === 1 &&\n !this.FormDefinition.title &&\n this.isChoiceQuestion(this.FormDefinition.questions[0])\n );\n }\n\n /**\n * The choice question that should render in the footer (if present).\n * When a form has a buttongroup/radio question, those buttons replace\n * the standard Submit button in the footer.\n */\n public get FooterChoiceQuestion(): FormQuestion | null {\n if (!this.FormDefinition || this.IsSimpleChoice) return null;\n const choiceQuestions = this.FormDefinition.questions.filter(q => this.isChoiceQuestion(q));\n return choiceQuestions.length > 0 ? choiceQuestions[choiceQuestions.length - 1] : null;\n }\n\n /**\n * Questions rendered in the main form area (excludes footer choice question).\n */\n public get MainQuestions(): FormQuestion[] {\n if (!this.FormDefinition) return [];\n const footerChoice = this.FooterChoiceQuestion;\n if (!footerChoice) return this.FormDefinition.questions;\n return this.FormDefinition.questions.filter(q => q.id !== footerChoice.id);\n }\n\n /**\n * Whether to show the standard Submit button (true when no footer choice question).\n */\n public get ShowSubmitButton(): boolean {\n return !this.FooterChoiceQuestion;\n }\n\n /**\n * Whether the form should be visible (has questions and Visible is true).\n */\n public get IsVisible(): boolean {\n return (\n this.Visible &&\n !!this.FormDefinition &&\n this.FormDefinition.questions.length > 0\n );\n }\n\n /**\n * Text for the submit button. Falls back to \"Submit\" if not specified.\n */\n public get SubmitLabel(): string {\n return this.FormDefinition?.submitLabel || 'Submit';\n }\n\n /** Get the FormControl for a specific question by ID */\n public GetControl(questionId: string): FormControl {\n return this.FormGroup.get(questionId) as FormControl;\n }\n\n /** Handle simple choice button click (single-question forms) */\n public OnSimpleChoiceClick(value: string | number | boolean): void {\n if (!this.Disabled) {\n const question = this.FormDefinition.questions[0];\n // Update the FormControl so external readers (e.g. collectFormData) can access the value\n this.FormGroup.get(question.id)?.setValue(value);\n this.FormSubmitted.emit({ [question.id]: value });\n }\n }\n\n /** Whether a given option value is the currently selected simple choice */\n public IsSimpleChoiceSelected(value: string | number | boolean): boolean {\n if (!this.IsSimpleChoice) return false;\n const question = this.FormDefinition.questions[0];\n const control = this.FormGroup.get(question.id);\n return control?.value === value;\n }\n\n /** Handle footer choice button click — selects the option and emits the form data */\n public OnFooterChoiceClick(value: string | number | boolean): void {\n if (!this.Disabled && this.FooterChoiceQuestion) {\n const mainControlKeys = this.MainQuestions.map(q => q.id);\n let hasInvalidFields = false;\n\n for (const key of mainControlKeys) {\n const control = this.FormGroup.get(key);\n if (control?.invalid) {\n control.markAsTouched();\n hasInvalidFields = true;\n }\n }\n\n if (hasInvalidFields) return;\n\n // Update the FormControl so external readers (e.g. collectFormData) can access the value\n this.FormGroup.get(this.FooterChoiceQuestion.id)?.setValue(value);\n const formData = { ...this.FormGroup.value };\n this.FormSubmitted.emit(formData);\n }\n }\n\n /** Whether a given option value is the currently selected footer choice */\n public IsFooterChoiceSelected(value: string | number | boolean): boolean {\n if (!this.FooterChoiceQuestion) return false;\n const control = this.FormGroup.get(this.FooterChoiceQuestion.id);\n return control?.value === value;\n }\n\n /** Handle full form submission */\n public OnSubmit(): void {\n if (this.FormGroup.invalid) {\n Object.keys(this.FormGroup.controls).forEach(key => {\n this.FormGroup.controls[key].markAsTouched();\n });\n return;\n }\n\n if (!this.Disabled && !this.IsSubmitting) {\n this.IsSubmitting = true;\n this.FormSubmitted.emit(this.FormGroup.value);\n\n // Reset submitting state after a short delay — the parent container\n // is responsible for hiding the form or handling the response\n setTimeout(() => {\n this.IsSubmitting = false;\n this.cdr.detectChanges();\n }, 1000);\n }\n }\n\n /** Get options for a choice question */\n public GetOptions(question: FormQuestion): FormOption[] {\n if (typeof question.type === 'object' && 'options' in question.type) {\n return question.type.options;\n }\n return [];\n }\n\n /** Check if an option has an icon */\n public HasIcon(option: FormOption): boolean {\n return !!option?.icon;\n }\n\n /** Track-by function for questions */\n public TrackByQuestionId(_index: number, question: FormQuestion): string {\n return question.id;\n }\n\n /** Track-by function for options */\n public TrackByValue(_index: number, option: FormOption): string | number | boolean {\n return option.value;\n }\n\n private isChoiceQuestion(question: FormQuestion): boolean {\n const type = typeof question.type === 'string' ? question.type : question.type.type;\n return ['buttongroup', 'radio'].includes(type);\n }\n\n private buildFormGroup(): void {\n const controls: Record<string, FormControl> = {};\n\n for (const question of this.FormDefinition.questions) {\n const validators = [];\n if (question.required) {\n validators.push(Validators.required);\n }\n\n const questionType = typeof question.type === 'string' ? question.type : question.type.type;\n if (questionType === 'email') {\n validators.push(Validators.email);\n }\n\n if (['number', 'currency'].includes(questionType) && typeof question.type === 'object') {\n if ('min' in question.type && question.type.min != null) {\n validators.push(Validators.min(question.type.min as number));\n }\n if ('max' in question.type && question.type.max != null) {\n validators.push(Validators.max(question.type.max as number));\n }\n }\n\n let initialValue = question.defaultValue;\n if (!initialValue) {\n if (questionType === 'checkbox' && typeof question.type === 'object' && 'multiple' in question.type && question.type.multiple) {\n initialValue = [];\n } else {\n initialValue = null;\n }\n }\n\n controls[question.id] = new FormControl(initialValue, validators);\n }\n\n this.FormGroup = new FormGroup(controls);\n }\n}\n","@if (IsVisible) {\n <div class=\"dynamic-form\">\n <!-- Simple Choice Mode (single question, no title, buttongroup/radio) -->\n @if (IsSimpleChoice) {\n <div class=\"simple-choice-buttons\">\n @for (option of GetOptions(FormDefinition.questions[0]); track TrackByValue($index, option)) {\n <button\n mjButton\n [variant]=\"IsSimpleChoiceSelected(option.value) ? 'primary' : 'secondary'\"\n [disabled]=\"Disabled\"\n (click)=\"OnSimpleChoiceClick(option.value)\"\n class=\"choice-button\"\n [class.selected]=\"IsSimpleChoiceSelected(option.value)\"\n >\n <span>\n @if (IsSimpleChoiceSelected(option.value)) {\n <i class=\"fa-solid fa-check\" aria-hidden=\"true\"></i>\n } @else if (HasIcon(option)) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </span>\n </button>\n }\n </div>\n }\n <!-- Full Form Mode (complex forms) -->\n @if (!IsSimpleChoice) {\n <form [formGroup]=\"FormGroup\" (ngSubmit)=\"OnSubmit()\" class=\"response-form\">\n <!-- Form Title -->\n @if (FormDefinition.title) {\n <h3 class=\"form-title\">{{ FormDefinition.title }}</h3>\n }\n <!-- Form Description -->\n @if (FormDefinition.description) {\n <p class=\"form-description\">{{ FormDefinition.description }}</p>\n }\n <!-- Questions (excludes footer choice question) -->\n @if (MainQuestions.length > 0) {\n <div class=\"form-questions\">\n @for (question of MainQuestions; track TrackByQuestionId($index, question)) {\n <mj-dynamic-form-field\n [Question]=\"question\"\n [Control]=\"GetControl(question.id)\"\n [formControlName]=\"question.id\"\n ></mj-dynamic-form-field>\n }\n </div>\n }\n <!-- Footer Actions -->\n <div class=\"form-actions\">\n <!-- Footer Choice Buttons (replaces Submit when present) -->\n @if (FooterChoiceQuestion) {\n @for (option of GetOptions(FooterChoiceQuestion); track TrackByValue($index, option)) {\n <button\n mjButton\n type=\"button\"\n [variant]=\"IsFooterChoiceSelected(option.value) ? 'primary' : 'secondary'\"\n [disabled]=\"Disabled\"\n (click)=\"OnFooterChoiceClick(option.value)\"\n class=\"footer-choice-button\"\n [class.selected]=\"IsFooterChoiceSelected(option.value)\"\n >\n <span>\n @if (IsFooterChoiceSelected(option.value)) {\n <i class=\"fa-solid fa-check\" aria-hidden=\"true\"></i>\n } @else if (HasIcon(option)) {\n <i class=\"fa {{ option.icon }}\" aria-hidden=\"true\"></i>\n }\n {{ option.label }}\n </span>\n </button>\n }\n }\n <!-- Standard Submit Button (when no footer choice) -->\n @if (ShowSubmitButton) {\n <button\n mjButton\n type=\"submit\"\n variant=\"primary\"\n [disabled]=\"Disabled || IsSubmitting || FormGroup.invalid\"\n class=\"submit-button\"\n >\n @if (!IsSubmitting) {\n <span>\n <i class=\"fa fa-check\" aria-hidden=\"true\"></i>\n {{ SubmitLabel }}\n </span>\n }\n @if (IsSubmitting) {\n <span>\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n Submitting...\n </span>\n }\n </button>\n }\n </div>\n </form>\n }\n </div>\n}\n"]}
|
|
@@ -26,9 +26,7 @@ import { NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
|
26
26
|
import * as i0 from "@angular/core";
|
|
27
27
|
import * as i1 from "@angular/common";
|
|
28
28
|
import * as i2 from "@angular/forms";
|
|
29
|
-
import * as i3 from "@
|
|
30
|
-
import * as i4 from "@progress/kendo-angular-inputs";
|
|
31
|
-
import * as i5 from "@progress/kendo-angular-dateinputs";
|
|
29
|
+
import * as i3 from "@memberjunction/ng-ui-components";
|
|
32
30
|
export class DynamicFormFieldComponent {
|
|
33
31
|
/**
|
|
34
32
|
* The question definition that drives this field's rendering.
|
|
@@ -221,7 +219,7 @@ export class DynamicFormFieldComponent {
|
|
|
221
219
|
useExisting: forwardRef(() => DynamicFormFieldComponent),
|
|
222
220
|
multi: true
|
|
223
221
|
}
|
|
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"] }] });
|
|
222
|
+
], 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 class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Email Input -->\n @if (QuestionType === 'email') {\n <input\n type=\"email\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Textarea -->\n @if (QuestionType === 'textarea') {\n <textarea\n class=\"mj-textarea question-textarea\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n rows=\"5\"\n ></textarea>\n }\n\n <!-- Number Input -->\n @if (QuestionType === 'number') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 1\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Currency Input -->\n @if (QuestionType === 'currency') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 0.01\"\n [Decimals]=\"2\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Date Picker -->\n @if (QuestionType === 'date') {\n <mj-datepicker\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-datepicker>\n }\n\n <!-- DateTime Picker -->\n @if (QuestionType === 'datetime') {\n <input\n type=\"datetime-local\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Button Group -->\n @if (QuestionType === 'buttongroup') {\n <div class=\"mj-buttongroup question-buttongroup\">\n @for (option of Options; track TrackByValue($index, option)) {\n <button\n mjButton\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 </div>\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 class=\"mj-radio\"\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 <input\n type=\"range\"\n class=\"mj-slider question-slider\"\n [min]=\"GetSliderConfig().min\"\n [max]=\"GetSliderConfig().max\"\n [step]=\"GetSliderConfig().step || 1\"\n [value]=\"$any(Value) || GetSliderConfig().min\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange(+$any($event.target).value)\"\n />\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 <mj-datepicker\n [ngModel]=\"$any(Value)?.start\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeStartChange($any($event))\"\n Placeholder=\"Start Date\"\n class=\"daterange-start\"\n ></mj-datepicker>\n <span class=\"daterange-separator\">to</span>\n <mj-datepicker\n [ngModel]=\"$any(Value)?.end\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeEndChange($any($event))\"\n Placeholder=\"End Date\"\n class=\"daterange-end\"\n ></mj-datepicker>\n </div>\n }\n\n <!-- Time Picker -->\n @if (QuestionType === 'time') {\n <input\n type=\"time\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\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 class=\"mj-checkbox\"\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.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i3.MJButtonDirective, selector: "button[mjButton], a[mjButton]", inputs: ["variant", "size", "toggleable", "selected"], outputs: ["selectedChange"] }, { kind: "component", type: i3.MJNumericInputComponent, selector: "mj-numeric-input", inputs: ["Min", "Max", "Step", "Format", "Decimals", "Disabled", "Placeholder"] }, { kind: "component", type: i3.MJDatepickerComponent, selector: "mj-datepicker", inputs: ["Min", "Max", "Format", "Placeholder", "Disabled"], outputs: ["ValueChange"] }] });
|
|
225
223
|
}
|
|
226
224
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DynamicFormFieldComponent, decorators: [{
|
|
227
225
|
type: Component,
|
|
@@ -231,7 +229,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
231
229
|
useExisting: forwardRef(() => DynamicFormFieldComponent),
|
|
232
230
|
multi: true
|
|
233
231
|
}
|
|
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"] }]
|
|
232
|
+
], 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 class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Email Input -->\n @if (QuestionType === 'email') {\n <input\n type=\"email\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Textarea -->\n @if (QuestionType === 'textarea') {\n <textarea\n class=\"mj-textarea question-textarea\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n rows=\"5\"\n ></textarea>\n }\n\n <!-- Number Input -->\n @if (QuestionType === 'number') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 1\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Currency Input -->\n @if (QuestionType === 'currency') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 0.01\"\n [Decimals]=\"2\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Date Picker -->\n @if (QuestionType === 'date') {\n <mj-datepicker\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-datepicker>\n }\n\n <!-- DateTime Picker -->\n @if (QuestionType === 'datetime') {\n <input\n type=\"datetime-local\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Button Group -->\n @if (QuestionType === 'buttongroup') {\n <div class=\"mj-buttongroup question-buttongroup\">\n @for (option of Options; track TrackByValue($index, option)) {\n <button\n mjButton\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 </div>\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 class=\"mj-radio\"\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 <input\n type=\"range\"\n class=\"mj-slider question-slider\"\n [min]=\"GetSliderConfig().min\"\n [max]=\"GetSliderConfig().max\"\n [step]=\"GetSliderConfig().step || 1\"\n [value]=\"$any(Value) || GetSliderConfig().min\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange(+$any($event.target).value)\"\n />\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 <mj-datepicker\n [ngModel]=\"$any(Value)?.start\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeStartChange($any($event))\"\n Placeholder=\"Start Date\"\n class=\"daterange-start\"\n ></mj-datepicker>\n <span class=\"daterange-separator\">to</span>\n <mj-datepicker\n [ngModel]=\"$any(Value)?.end\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeEndChange($any($event))\"\n Placeholder=\"End Date\"\n class=\"daterange-end\"\n ></mj-datepicker>\n </div>\n }\n\n <!-- Time Picker -->\n @if (QuestionType === 'time') {\n <input\n type=\"time\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\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 class=\"mj-checkbox\"\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
233
|
}], propDecorators: { Question: [{
|
|
236
234
|
type: Input
|
|
237
235
|
}], Control: [{
|
|
@@ -1 +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"]}
|
|
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 class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Email Input -->\n @if (QuestionType === 'email') {\n <input\n type=\"email\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Textarea -->\n @if (QuestionType === 'textarea') {\n <textarea\n class=\"mj-textarea question-textarea\"\n [value]=\"$any(Value) || ''\"\n [placeholder]=\"Placeholder\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange($any($event.target).value)\"\n rows=\"5\"\n ></textarea>\n }\n\n <!-- Number Input -->\n @if (QuestionType === 'number') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 1\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Currency Input -->\n @if (QuestionType === 'currency') {\n <mj-numeric-input\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Min]=\"Min || 0\"\n [Max]=\"Max || 999999999\"\n [Step]=\"Step || 0.01\"\n [Decimals]=\"2\"\n [Disabled]=\"Disabled\"\n (ngModelChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-numeric-input>\n }\n\n <!-- Date Picker -->\n @if (QuestionType === 'date') {\n <mj-datepicker\n [ngModel]=\"$any(Value)\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnValueChange($any($event))\"\n class=\"question-input\"\n ></mj-datepicker>\n }\n\n <!-- DateTime Picker -->\n @if (QuestionType === 'datetime') {\n <input\n type=\"datetime-local\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\n }\n\n <!-- Button Group -->\n @if (QuestionType === 'buttongroup') {\n <div class=\"mj-buttongroup question-buttongroup\">\n @for (option of Options; track TrackByValue($index, option)) {\n <button\n mjButton\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 </div>\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 class=\"mj-radio\"\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 <input\n type=\"range\"\n class=\"mj-slider question-slider\"\n [min]=\"GetSliderConfig().min\"\n [max]=\"GetSliderConfig().max\"\n [step]=\"GetSliderConfig().step || 1\"\n [value]=\"$any(Value) || GetSliderConfig().min\"\n [disabled]=\"Disabled\"\n (input)=\"OnValueChange(+$any($event.target).value)\"\n />\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 <mj-datepicker\n [ngModel]=\"$any(Value)?.start\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeStartChange($any($event))\"\n Placeholder=\"Start Date\"\n class=\"daterange-start\"\n ></mj-datepicker>\n <span class=\"daterange-separator\">to</span>\n <mj-datepicker\n [ngModel]=\"$any(Value)?.end\"\n [ngModelOptions]=\"{standalone: true}\"\n [Disabled]=\"Disabled\"\n (ValueChange)=\"OnDateRangeEndChange($any($event))\"\n Placeholder=\"End Date\"\n class=\"daterange-end\"\n ></mj-datepicker>\n </div>\n }\n\n <!-- Time Picker -->\n @if (QuestionType === 'time') {\n <input\n type=\"time\"\n class=\"mj-input question-input\"\n [value]=\"$any(Value)\"\n [disabled]=\"Disabled\"\n (change)=\"OnValueChange($any($event.target).value)\"\n />\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 class=\"mj-checkbox\"\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"]}
|
|
@@ -4,12 +4,10 @@ import * as i2 from "./dynamic-form-field/dynamic-form-field.component";
|
|
|
4
4
|
import * as i3 from "./dynamic-form-response/dynamic-form-response.component";
|
|
5
5
|
import * as i4 from "@angular/common";
|
|
6
6
|
import * as i5 from "@angular/forms";
|
|
7
|
-
import * as i6 from "@
|
|
8
|
-
import * as i7 from "@progress/kendo-angular-inputs";
|
|
9
|
-
import * as i8 from "@progress/kendo-angular-dateinputs";
|
|
7
|
+
import * as i6 from "@memberjunction/ng-ui-components";
|
|
10
8
|
export declare class DynamicFormsModule {
|
|
11
9
|
static ɵfac: i0.ɵɵFactoryDeclaration<DynamicFormsModule, never>;
|
|
12
|
-
static ɵmod: i0.ɵɵNgModuleDeclaration<DynamicFormsModule, [typeof i1.DynamicFormComponent, typeof i2.DynamicFormFieldComponent, typeof i3.DynamicFormResponseComponent], [typeof i4.CommonModule, typeof i5.FormsModule, typeof i5.ReactiveFormsModule, typeof i6.
|
|
10
|
+
static ɵmod: i0.ɵɵNgModuleDeclaration<DynamicFormsModule, [typeof i1.DynamicFormComponent, typeof i2.DynamicFormFieldComponent, typeof i3.DynamicFormResponseComponent], [typeof i4.CommonModule, typeof i5.FormsModule, typeof i5.ReactiveFormsModule, typeof i6.MJButtonDirective, typeof i6.MJNumericInputComponent, typeof i6.MJDatepickerComponent], [typeof i1.DynamicFormComponent, typeof i2.DynamicFormFieldComponent, typeof i3.DynamicFormResponseComponent]>;
|
|
13
11
|
static ɵinj: i0.ɵɵInjectorDeclaration<DynamicFormsModule>;
|
|
14
12
|
}
|
|
15
13
|
//# sourceMappingURL=forms.module.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forms.module.d.ts","sourceRoot":"","sources":["../../src/lib/forms.module.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"forms.module.d.ts","sourceRoot":"","sources":["../../src/lib/forms.module.ts"],"names":[],"mappings":";;;;;;;AAuDA,qBAoBa,kBAAkB;yCAAlB,kBAAkB;0CAAlB,kBAAkB;0CAAlB,kBAAkB;CAAI"}
|
package/dist/lib/forms.module.js
CHANGED
|
@@ -43,10 +43,8 @@
|
|
|
43
43
|
import { NgModule } from '@angular/core';
|
|
44
44
|
import { CommonModule } from '@angular/common';
|
|
45
45
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
46
|
-
//
|
|
47
|
-
import {
|
|
48
|
-
import { InputsModule } from '@progress/kendo-angular-inputs';
|
|
49
|
-
import { DateInputsModule } from '@progress/kendo-angular-dateinputs';
|
|
46
|
+
// MJ UI components
|
|
47
|
+
import { MJButtonDirective, MJNumericInputComponent, MJDatepickerComponent } from '@memberjunction/ng-ui-components';
|
|
50
48
|
// Components
|
|
51
49
|
import { DynamicFormComponent } from './dynamic-form/dynamic-form.component';
|
|
52
50
|
import { DynamicFormFieldComponent } from './dynamic-form-field/dynamic-form-field.component';
|
|
@@ -59,17 +57,16 @@ export class DynamicFormsModule {
|
|
|
59
57
|
DynamicFormResponseComponent], imports: [CommonModule,
|
|
60
58
|
FormsModule,
|
|
61
59
|
ReactiveFormsModule,
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
MJButtonDirective,
|
|
61
|
+
MJNumericInputComponent,
|
|
62
|
+
MJDatepickerComponent], exports: [DynamicFormComponent,
|
|
65
63
|
DynamicFormFieldComponent,
|
|
66
64
|
DynamicFormResponseComponent] });
|
|
67
65
|
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DynamicFormsModule, imports: [CommonModule,
|
|
68
66
|
FormsModule,
|
|
69
67
|
ReactiveFormsModule,
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
DateInputsModule] });
|
|
68
|
+
MJNumericInputComponent,
|
|
69
|
+
MJDatepickerComponent] });
|
|
73
70
|
}
|
|
74
71
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImport: i0, type: DynamicFormsModule, decorators: [{
|
|
75
72
|
type: NgModule,
|
|
@@ -83,9 +80,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.3", ngImpor
|
|
|
83
80
|
CommonModule,
|
|
84
81
|
FormsModule,
|
|
85
82
|
ReactiveFormsModule,
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
MJButtonDirective,
|
|
84
|
+
MJNumericInputComponent,
|
|
85
|
+
MJDatepickerComponent
|
|
89
86
|
],
|
|
90
87
|
exports: [
|
|
91
88
|
DynamicFormComponent,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"forms.module.js","sourceRoot":"","sources":["../../src/lib/forms.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAElE,mBAAmB;AACnB,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"forms.module.js","sourceRoot":"","sources":["../../src/lib/forms.module.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAElE,mBAAmB;AACnB,OAAO,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;AAErH,aAAa;AACb,OAAO,EAAE,oBAAoB,EAAE,MAAM,uCAAuC,CAAC;AAC7E,OAAO,EAAE,yBAAyB,EAAE,MAAM,mDAAmD,CAAC;AAC9F,OAAO,EAAE,4BAA4B,EAAE,MAAM,yDAAyD,CAAC;;AAsBvG,MAAM,OAAO,kBAAkB;uGAAlB,kBAAkB;wGAAlB,kBAAkB,iBAlBvB,oBAAoB;YACpB,yBAAyB;YACzB,4BAA4B,aAG5B,YAAY;YACZ,WAAW;YACX,mBAAmB;YACnB,iBAAiB;YACjB,uBAAuB;YACvB,qBAAqB,aAGrB,oBAAoB;YACpB,yBAAyB;YACzB,4BAA4B;wGAGvB,kBAAkB,YAbvB,YAAY;YACZ,WAAW;YACX,mBAAmB;YAEnB,uBAAuB;YACvB,qBAAqB;;2FAQhB,kBAAkB;kBApB9B,QAAQ;mBAAC;oBACN,YAAY,EAAE;wBACV,oBAAoB;wBACpB,yBAAyB;wBACzB,4BAA4B;qBAC/B;oBACD,OAAO,EAAE;wBACL,YAAY;wBACZ,WAAW;wBACX,mBAAmB;wBACnB,iBAAiB;wBACjB,uBAAuB;wBACvB,qBAAqB;qBACxB;oBACD,OAAO,EAAE;wBACL,oBAAoB;wBACpB,yBAAyB;wBACzB,4BAA4B;qBAC/B;iBACJ","sourcesContent":["/**\n * @fileoverview MJ Dynamic Forms Module\n *\n * Provides reusable dynamic form components that render structured forms from\n * `AgentResponseForm` JSON schemas. Supports 14+ input types with built-in\n * validation, responsive layout, and intelligent field sizing.\n *\n * ## Usage\n *\n * Import `DynamicFormsModule` into your feature module:\n *\n * ```typescript\n * import { DynamicFormsModule } from '@memberjunction/ng-forms';\n *\n * @NgModule({\n * imports: [DynamicFormsModule]\n * })\n * export class MyFeatureModule { }\n * ```\n *\n * Then use in templates:\n *\n * ```html\n * <mj-dynamic-form\n * [FormDefinition]=\"myFormSchema\"\n * [Disabled]=\"isSaving\"\n * (FormSubmitted)=\"onSubmit($event)\">\n * </mj-dynamic-form>\n * ```\n *\n * Or use the field component individually within your own reactive forms:\n *\n * ```html\n * <mj-dynamic-form-field\n * [Question]=\"question\"\n * [Control]=\"myFormGroup.get(question.id)\"\n * [formControlName]=\"question.id\">\n * </mj-dynamic-form-field>\n * ```\n *\n * @module @memberjunction/ng-forms\n */\n\nimport { NgModule } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule, ReactiveFormsModule } from '@angular/forms';\n\n// MJ UI components\nimport { MJButtonDirective, MJNumericInputComponent, MJDatepickerComponent } from '@memberjunction/ng-ui-components';\n\n// Components\nimport { DynamicFormComponent } from './dynamic-form/dynamic-form.component';\nimport { DynamicFormFieldComponent } from './dynamic-form-field/dynamic-form-field.component';\nimport { DynamicFormResponseComponent } from './dynamic-form-response/dynamic-form-response.component';\n\n@NgModule({\n declarations: [\n DynamicFormComponent,\n DynamicFormFieldComponent,\n DynamicFormResponseComponent\n ],\n imports: [\n CommonModule,\n FormsModule,\n ReactiveFormsModule,\n MJButtonDirective,\n MJNumericInputComponent,\n MJDatepickerComponent\n ],\n exports: [\n DynamicFormComponent,\n DynamicFormFieldComponent,\n DynamicFormResponseComponent\n ]\n})\nexport class DynamicFormsModule { }\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ng-forms",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.23.0",
|
|
4
4
|
"description": "MemberJunction: Generic dynamic form rendering from JSON schemas. Renders structured forms with 14+ field types (text, textarea, number, currency, date, daterange, time, checkbox, radio, dropdown, buttongroup, slider, email) with built-in validation, responsive layout, and intelligent field sizing.",
|
|
5
5
|
"main": "./dist/public-api.js",
|
|
6
6
|
"typings": "./dist/public-api.d.ts",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"/dist"
|
|
9
9
|
],
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "
|
|
11
|
+
"test": "vitest run",
|
|
12
12
|
"build": "ngc",
|
|
13
13
|
"test:watch": "vitest"
|
|
14
14
|
},
|
|
@@ -33,10 +33,8 @@
|
|
|
33
33
|
"@angular/forms": "21.1.3"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@memberjunction/ai-core-plus": "5.
|
|
37
|
-
"@
|
|
38
|
-
"@progress/kendo-angular-dateinputs": "22.0.1",
|
|
39
|
-
"@progress/kendo-angular-inputs": "22.0.1",
|
|
36
|
+
"@memberjunction/ai-core-plus": "5.23.0",
|
|
37
|
+
"@memberjunction/ng-ui-components": "5.23.0",
|
|
40
38
|
"tslib": "^2.8.1"
|
|
41
39
|
},
|
|
42
40
|
"sideEffects": false,
|