@jigx/core-sdk 1.1.0 → 1.2.0-rc

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/action/ja.generate-pdf.d.ts +17 -1
  2. package/dist/action/ja.generate-pdf.d.ts.map +1 -1
  3. package/dist/action/ja.generate-pdf.js +4 -1
  4. package/dist/action/ja.in-background.d.ts +3 -2
  5. package/dist/action/ja.in-background.d.ts.map +1 -1
  6. package/dist/action/ja.in-background.js +1 -1
  7. package/dist/assets/example-extraction-cache.json +3 -3
  8. package/dist/assets/extracted-core-sdk-examples.yaml +18 -0
  9. package/dist/assets/extracted-core-sdk-types.yaml +58 -0
  10. package/dist/assets/type-extraction-cache.json +3 -3
  11. package/docs/array-fields.md +371 -0
  12. package/docs/conditional-logic.md +178 -0
  13. package/docs/convention-naming.md +102 -0
  14. package/docs/date-field.md +92 -0
  15. package/docs/dropdown-fields.md +879 -0
  16. package/docs/field-state.md +131 -0
  17. package/docs/field-types-overview.md +132 -0
  18. package/docs/formatting.md +421 -0
  19. package/docs/icons.md +142 -0
  20. package/docs/index.md +23 -0
  21. package/docs/jsonata-expressions.md +200 -0
  22. package/docs/media-fields.md +107 -0
  23. package/docs/overview.md +467 -0
  24. package/docs/pattern-build-deploy.md +91 -0
  25. package/docs/pattern-datasources.md +459 -0
  26. package/docs/pattern-forms.md +528 -0
  27. package/docs/pattern-global-actions.md +92 -0
  28. package/docs/pattern-javascript-functions.md +452 -0
  29. package/docs/pattern-navigation.md +304 -0
  30. package/docs/pattern-pdf-generation.md +391 -0
  31. package/docs/pattern-rest-acumatica.md +660 -0
  32. package/docs/pattern-sync-progress.md +96 -0
  33. package/docs/pattern-sync.md +653 -0
  34. package/docs/pattern-tabs-form.md +293 -0
  35. package/docs/recipe-index.md +64 -0
  36. package/docs/runtime-variables.md +127 -0
  37. package/docs/sections.md +81 -0
  38. package/docs/validation-patterns.md +150 -0
  39. package/package.json +3 -2
@@ -0,0 +1,131 @@
1
+ ## Basic Usage
2
+
3
+ ```typescript
4
+ form.addStep({ instanceId: 'details', icon: 'pencil-edit-info' }, (step) => {
5
+ const emailField = step.addEmail({ name: 'email', label: 'Email' })
6
+ // Phone required only if email is empty
7
+ step.addText({
8
+ name: 'phone',
9
+ label: 'Phone',
10
+ isRequired: new JsonataBuilder('$email = null or $email = ""', {
11
+ email: emailField.state.value,
12
+ }),
13
+ })
14
+ })
15
+ ```
16
+
17
+ ## How It Works
18
+
19
+ `field.state.value` is a template variable replaced with actual state reference at runtime:
20
+
21
+ ```typescript
22
+ step.addDropdown({
23
+ name: 'status',
24
+ label: 'Status',
25
+ data: [
26
+ { label: 'Active', value: 'active' },
27
+ { label: 'Inactive', value: 'inactive' },
28
+ ],
29
+ })
30
+ // When user selects "active":
31
+ // status.state.value → "active" (immediately)
32
+ ```
33
+
34
+ ## Characteristics
35
+
36
+ | Property | Behavior |
37
+ | --- | --- |
38
+ | Updates | Immediate on user input |
39
+ | Scope | Current step only |
40
+ | Persistence | Not persisted until step saved |
41
+ | Use case | Local validation, conditional fields |
42
+
43
+ ## Important Limitations
44
+
45
+ **Self-reference in `value` creates a cycle (not allowed):**
46
+
47
+ A field's `value` cannot reference its own `.state.value`
48
+
49
+
50
+ **ArrayField does not expose `.state`:**
51
+
52
+ ```typescript
53
+ const step1 = form.addStep({ instanceId: 'collect-items' }, (step) => {
54
+ // Wrong - arrayField has no .state
55
+ const items = step.addArrayField({ name: 'items', label: 'Items', title: 'Item' })
56
+ items.addText({ name: 'name', label: 'Name' })
57
+ // items.state.value // Error - doesn't exist
58
+ })
59
+ step1.with({ icon: 'list', title: 'Items' })
60
+ // Correct - use step.data.getArrayFieldData()
61
+ form.addStep({ instanceId: 'select-item', icon: 'cursor-select-1' }, (otherStep) => {
62
+ otherStep.addDropdown({
63
+ name: 'selected',
64
+ label: 'Select Item',
65
+ data: step1.data.getArrayFieldData('items'),
66
+ })
67
+ })
68
+ ```
69
+
70
+ **Cross-step access not available:**
71
+
72
+ ```typescript
73
+ const step1 = form.addStep({ instanceId: 'type' }, (step) => {
74
+ step.addDropdown({
75
+ name: 'status',
76
+ label: 'Status',
77
+ data: [
78
+ { label: 'Active', value: 'active' },
79
+ { label: 'Inactive', value: 'inactive' },
80
+ ],
81
+ })
82
+ })
83
+ step1.with({ icon: 'settings', title: 'Type' })
84
+ // Wrong - field.state.value only works within same step
85
+ // step1Field.state.value // Won't work cross-step
86
+ // Correct - use step.data.getFieldData() for cross-step
87
+ form.addStep({ instanceId: 'details', icon: 'pencil-edit-info' }, (step2) => {
88
+ step2.addText({
89
+ name: 'field',
90
+ label: 'Field',
91
+ isVisible: new JsonataBuilder('$status = "active"', {
92
+ status: step1.data.getFieldData('status'),
93
+ }),
94
+ })
95
+ })
96
+ ```
97
+
98
+ ## Dropdown state.selected
99
+
100
+ Dropdowns expose an additional `state.selected` property for accessing the full selected object in the same step:
101
+
102
+ ```typescript
103
+ const productField = step.addDropdown({
104
+ name: 'product',
105
+ label: 'Product',
106
+ data: [
107
+ { label: 'Widget A', value: 'widget-a', icon: 'cube-shape' },
108
+ { label: 'Widget B', value: 'widget-b', icon: 'cube-shape' },
109
+ ],
110
+ })
111
+ // state.value → "widget-a" (just the value)
112
+ // state.selected → { label: "Widget A", value: "widget-a", icon: "cube" }
113
+ // Access full object to show selected label in read-only field
114
+ step.addText({
115
+ name: 'selectedLabel',
116
+ label: 'Selected Product Name',
117
+ value: new JsonataBuilder('$product.label', {
118
+ product: productField.state.selected,
119
+ }),
120
+ isDisabled: true,
121
+ })
122
+ ```
123
+
124
+ | Property | Returns | Use Case |
125
+ | --- | --- | --- |
126
+ | `state.value` | `string \| number` | Selected value for conditions |
127
+ | `state.selected` | Full object | Access label, icon, or nested properties |
128
+
129
+ **Ref:** `./dropdown-fields.md` for more on dropdown state
130
+
131
+ **Ref:** `./cross-step-data.md` for cross-step data access
@@ -0,0 +1,132 @@
1
+ ## Adding Fields
2
+
3
+ Fields are added to steps or sections:
4
+
5
+ ```typescript
6
+ // Add to step
7
+ step.addText({ name: 'username', label: 'Username' })
8
+ // Add to section
9
+ const section = step.addSection({ title: 'Contact Info' })
10
+ section.addEmail({ name: 'email', label: 'Email' })
11
+ ```
12
+
13
+ **Important:** Call field methods on step or section only - NOT on field references.
14
+
15
+ ## Common Properties
16
+
17
+ All fields support these via constructor or `.with()`:
18
+
19
+ ```typescript
20
+ const otherField = step.addCheckbox({ name: 'showNotes', label: 'Show Notes' })
21
+ // Pass all properties in constructor
22
+ step.addText({
23
+ name: 'notes',
24
+ label: 'Notes',
25
+ icon: 'common-file-text',
26
+ helperText: 'Enter additional details',
27
+ isRequired: false, // Make optional
28
+ isDisabled: true, // Read-only
29
+ isVisible: new JsonataBuilder('$showNotes = true', { showNotes: otherField.state.value }),
30
+ })
31
+ ```
32
+
33
+ Or use `.with()` to update after construction:
34
+
35
+ ```typescript
36
+ const field = step.addText({ name: 'notes', label: 'Notes' })
37
+ field.with({
38
+ icon: 'common-file-text',
39
+ helperText: 'Enter additional details',
40
+ isRequired: false,
41
+ })
42
+ ```
43
+
44
+ ## Required vs Optional
45
+
46
+ - **Most fields required by default** - no need to set `isRequired: true`
47
+ - **Checkbox is optional by default** - set `isRequired: true` if needed
48
+ - To make any field optional: `isRequired: false`
49
+
50
+ ## Text Fields
51
+
52
+ ```typescript
53
+ // Single-line
54
+ step.addText({ name: 'title', label: 'Title' })
55
+ // Multi-line textarea
56
+ step.addText({
57
+ name: 'notes',
58
+ label: 'Notes',
59
+ isMultiline: true,
60
+ })
61
+ ```
62
+
63
+ ## Validated Input Fields
64
+
65
+ ```typescript
66
+ // Email - built-in format validation
67
+ step.addEmail({ name: 'email', label: 'Email' })
68
+ // Phone - phone-pad keyboard
69
+ step.addPhone({ name: 'phone', label: 'Phone' })
70
+ // Number - numeric input
71
+ step.addNumber({ name: 'quantity', label: 'Quantity' })
72
+ ```
73
+
74
+ ## Date/Time Fields
75
+
76
+ ```typescript
77
+ step.addDate({
78
+ mode: 'datetime',
79
+ name: 'eventDateTime',
80
+ label: 'Event Date & Time',
81
+ })
82
+ ```
83
+
84
+ ## Checkbox Fields
85
+
86
+ ```typescript
87
+ step.addCheckbox({
88
+ name: 'consent',
89
+ label: 'I agree to terms',
90
+ isRequired: true, // Checkbox optional by default, set true to require
91
+ })
92
+ ```
93
+
94
+ **Choice vs Checkbox for yes/no:**
95
+
96
+ - Required yes/no question: Use choice field (checkbox fails validation when unchecked)
97
+ - Optional toggle: Use checkbox
98
+
99
+ ## Rating Fields
100
+
101
+ ```typescript
102
+ step.addRating({ name: 'satisfaction', label: 'Satisfaction Rating' })
103
+ ```
104
+
105
+ **Rating vs Number:**
106
+
107
+ - Rating: Subjective feedback on fixed scale (1-5 stars, quality)
108
+ - Number: Objective/precise values (counts, measurements)
109
+
110
+ ## Slider Fields
111
+
112
+ ```typescript
113
+ step.addSlider({ name: 'price', label: 'Price', minimum: 0, maximum: 1000, step: 50 })
114
+ ```
115
+
116
+ **Slider vs Number:**
117
+
118
+ - Slider: Bounded range with known min/max, coarse precision
119
+ - Number: High precision, large ranges, exact values
120
+
121
+ ## Helper Text Guidelines
122
+
123
+ - Use only when providing NEW information (formatting hints, constraints, examples)
124
+ - Never repeat the label
125
+ - Omit entirely if label is self-explanatory
126
+
127
+ ```typescript
128
+ // Good - adds information
129
+ step.addEmail({ name: 'email', label: 'Email', helperText: 'We will never share your email' })
130
+ // Bad - repeats label
131
+ step.addEmail({ name: 'email2', label: 'Email', helperText: 'Enter your email address' }) // Don't do this
132
+ ```
@@ -0,0 +1,421 @@
1
+ Field `format` and `FormatBuilder` use [ECMA-402](https://tc39.es/ecma402/) `Intl.NumberFormat` for numbers (currency, percent, units) and [Moment.js](https://momentjs.com/docs/#/displaying/) `dateFormat` presets/patterns for dates (`LL`, `fromNow`, etc.). Both are locale-aware — numbers adapt decimal separators, thousands grouping, and currency symbols; dates adapt month names, orderings, and relative-time wording automatically.
2
+
3
+ **Important:** Field `format` only applies when field is disabled. When editable, raw value displays.
4
+
5
+
6
+ ### Where FormatBuilder applies
7
+
8
+ Use `FormatBuilder` to format values in these specific properties:
9
+
10
+ - `submissionItemTitle` — form-level submission title
11
+ - `submissionItemSubtitle` — form-level submission subtitle
12
+ - ArrayField `title` — item title in the array list
13
+ - ArrayField `subtitle` — item subtitle in the array list
14
+ - ArrayField `description` — item description in the array list
15
+ - Step `title` — step header text
16
+
17
+ Wrap `FormatBuilder` around a `JsonataBuilder` expression, then pass to `I18nBuilder` if combining with text.
18
+
19
+ ## ECMA-402 Standard
20
+
21
+ Format options follow two systems:
22
+
23
+ - **Number formatting**: [ECMA-402](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl) `Intl.NumberFormat` — currency, percent, units, scientific notation. Locale-aware, adapts to user's device locale automatically.
24
+ - **Date formatting**: [Moment.js](https://momentjs.com/docs/#/displaying/) `dateFormat` presets (`LL`, `fromNow`, etc.). See [Date Format Codes](#date-format-codes) below.
25
+
26
+ No manual locale configuration needed - the SDK handles it.
27
+
28
+ ## Field Format Property
29
+
30
+ ### When Format Applies
31
+
32
+ - **Disabled/read-only fields**: Format is applied, formatted value displays
33
+ - **Editable fields**: Raw value from `value` property displays, format ignored
34
+
35
+ ```typescript
36
+ // Format applied - field is disabled
37
+ step.addNumber({
38
+ name: 'total',
39
+ label: 'Total',
40
+ value: 1234.56,
41
+ isDisabled: true,
42
+ format: { numberStyle: 'currency', currency: 'USD' },
43
+ })
44
+ // Displays: $1,234.56
45
+
46
+ // Format NOT applied - field is editable
47
+ step.addNumber({
48
+ name: 'amount',
49
+ label: 'Amount',
50
+ value: 1234.56,
51
+ format: { numberStyle: 'currency', currency: 'USD' },
52
+ })
53
+ // Displays: 1234.56 (raw value)
54
+ ```
55
+
56
+ ## Currency Formatting
57
+
58
+ ```typescript
59
+ step.addNumber({
60
+ name: 'price',
61
+ label: 'Price',
62
+ format: { currency: 'USD', numberStyle: 'currency' },
63
+ })
64
+ ```
65
+
66
+ ### Currency Options
67
+
68
+ ```typescript
69
+ step.addNumber({
70
+ name: 'price',
71
+ label: 'Price',
72
+ format: {
73
+ numberStyle: 'currency',
74
+ currency: 'EUR',
75
+ currencyDisplay: 'symbol', // €, $, £ (options: 'symbol', 'code', 'name', 'narrowSymbol')
76
+ currencySign: 'accounting', // (100) for negative (options: 'standard', 'accounting')
77
+ },
78
+ })
79
+ ```
80
+
81
+ ## Percentage Formatting
82
+
83
+ ```typescript
84
+ // Format as percentage
85
+ step.addNumber({
86
+ name: 'completion-rate',
87
+ label: 'Completion Rate',
88
+ format: { numberStyle: 'percent' },
89
+ })
90
+ ```
91
+
92
+ ## Decimal Formatting
93
+
94
+ ```typescript
95
+ // Fixed decimal places (123.45, 0.50)
96
+ step.addNumber({
97
+ name: 'average-score',
98
+ label: 'Average Score',
99
+ format: { numberStyle: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 },
100
+ })
101
+ // Significant digits (123.4, 0.5, 0.001234)
102
+ step.addNumber({
103
+ name: 'precision-value',
104
+ label: 'Precision Value',
105
+ format: { minimumSignificantDigits: 3, maximumSignificantDigits: 4 },
106
+ })
107
+ // Combining both: min/max fraction digits
108
+ step.addNumber({
109
+ name: 'detailed-score',
110
+ label: 'Detailed Score',
111
+ format: { minimumFractionDigits: 1, maximumFractionDigits: 3 },
112
+ })
113
+ ```
114
+
115
+ ### Decimal Options
116
+
117
+ ```typescript
118
+ format: {
119
+ minimumFractionDigits: 2, // Min decimal places
120
+ maximumFractionDigits: 4, // Max decimal places
121
+ minimumSignificantDigits: 3, // Min significant digits
122
+ maximumSignificantDigits: 5 // Max significant digits
123
+ }
124
+ ```
125
+
126
+ ## Unit Formatting
127
+
128
+ ```typescript
129
+ // Distance in kilometers with 2 decimal precision
130
+ step.addNumber({
131
+ name: 'distance-km',
132
+ label: 'Distance (km)',
133
+ format: {
134
+ numberStyle: 'unit',
135
+ unit: 'kilometer',
136
+ unitDisplay: 'long',
137
+ minimumFractionDigits: 2,
138
+ maximumFractionDigits: 2,
139
+ },
140
+ })
141
+ // Height in meters with 1 decimal place
142
+ step.addNumber({
143
+ name: 'height-m',
144
+ label: 'Height (m)',
145
+ format: {
146
+ numberStyle: 'unit',
147
+ unit: 'meter',
148
+ unitDisplay: 'short',
149
+ minimumFractionDigits: 1,
150
+ maximumFractionDigits: 1,
151
+ },
152
+ })
153
+ // Width in millimeters (no decimals for precision)
154
+ step.addNumber({
155
+ name: 'width-mm',
156
+ label: 'Width (mm)',
157
+ format: {
158
+ numberStyle: 'unit',
159
+ unit: 'millimeter',
160
+ unitDisplay: 'narrow',
161
+ minimumFractionDigits: 0,
162
+ maximumFractionDigits: 0,
163
+ },
164
+ })
165
+ // Weight in kilograms with sign display
166
+ step.addNumber({
167
+ name: 'weight-kg',
168
+ label: 'Weight Change (kg)',
169
+ format: {
170
+ numberStyle: 'unit',
171
+ unit: 'kilogram',
172
+ unitDisplay: 'short',
173
+ signDisplay: 'always',
174
+ minimumFractionDigits: 1,
175
+ },
176
+ })
177
+ // Temperature in celsius with minimum integer digits
178
+ step.addNumber({
179
+ name: 'temperature',
180
+ label: 'Temperature',
181
+ format: {
182
+ numberStyle: 'unit',
183
+ unit: 'celsius',
184
+ minimumIntegerDigits: 2,
185
+ },
186
+ })
187
+ ```
188
+
189
+ ### Unit Display Options
190
+
191
+ | Display | Example |
192
+ | --- | --- |
193
+ | `'short'` | 150 lb |
194
+ | `'long'` | 150 pounds |
195
+ | `'narrow'` | 150# |
196
+
197
+ ### Common Units
198
+
199
+ - Length: `meter`, `kilometer`, `mile`, `foot`, `inch`
200
+ - Weight: `kilogram`, `pound`, `ounce`
201
+ - Temperature: `celsius`, `fahrenheit`
202
+ - Volume: `liter`, `gallon`
203
+ - Time: `hour`, `minute`, `second`
204
+
205
+ ## Scientific Notation
206
+
207
+ ```typescript
208
+ // Set multiple format properties
209
+ step.addNumber({
210
+ name: 'result',
211
+ label: 'Result',
212
+ format: {
213
+ notation: 'scientific',
214
+ minimumSignificantDigits: 3,
215
+ maximumSignificantDigits: 5,
216
+ },
217
+ })
218
+ ```
219
+
220
+ ## Dynamic Formatting
221
+
222
+ Format properties support `JsonataBuilder` for dynamic values:
223
+
224
+ ```typescript
225
+ // Let user select currency
226
+ const currencyField = step.addDropdown({
227
+ name: 'currency',
228
+ label: 'Currency',
229
+ data: [
230
+ { label: 'USD', value: 'USD' },
231
+ { label: 'EUR', value: 'EUR' },
232
+ { label: 'GBP', value: 'GBP' },
233
+ ],
234
+ })
235
+ // Set currency dynamically based on selection
236
+ step.addNumber({
237
+ name: 'amount',
238
+ label: 'Amount',
239
+ format: {
240
+ currency: new JsonataBuilder('$selectedCurrency', {
241
+ selectedCurrency: currencyField.state.value,
242
+ }),
243
+ numberStyle: 'currency',
244
+ },
245
+ })
246
+ ```
247
+
248
+ ## Updating Format with .with()
249
+
250
+ ```typescript
251
+ const price = step.addNumber({ name: 'price', label: 'Price' })
252
+ // Update format after construction
253
+ price.with({
254
+ format: { numberStyle: 'currency', currency: 'USD', minimumFractionDigits: 2 },
255
+ })
256
+ ```
257
+
258
+ ## FormatBuilder Class
259
+
260
+ Use `FormatBuilder` to format values for text display in `submissionItemTitle`, `submissionItemSubtitle`, arrayField item `title`/`subtitle`/`description`, and step `title`. Supports same number options as field format, plus date formatting.
261
+
262
+ ### Number Formatting with FormatBuilder
263
+
264
+ ```typescript
265
+ // Step 1: Collect balance
266
+ const step1 = form.addStep(
267
+ { instanceId: 'input', title: 'Enter Balance', icon: 'accounting-calculator' },
268
+ (step) => {
269
+ step.addNumber({ name: 'balance', label: 'Balance' })
270
+ },
271
+ )
272
+ // Step 2: Display formatted balance in title
273
+ form.addStep(
274
+ {
275
+ instanceId: 'summary',
276
+ title: new I18nBuilder('balance.display', 'Balance: {amount}', {
277
+ amount: new FormatBuilder(
278
+ new JsonataBuilder('$balance', { balance: step1.data.getFieldData('balance') }),
279
+ { numberStyle: 'currency', currency: 'USD' },
280
+ ),
281
+ }),
282
+ icon: 'cash-payment-wallet',
283
+ },
284
+ (step) => {
285
+ step.addText({ name: 'note', label: 'Note' })
286
+ },
287
+ )
288
+ ```
289
+
290
+ ### Date Formatting with FormatBuilder
291
+
292
+ ```typescript
293
+ // Step 1: Collect date
294
+ const step1 = form.addStep(
295
+ { instanceId: 'input', title: 'Enter Date', icon: 'calendar-date' },
296
+ (step) => {
297
+ step.addDate({ name: 'updatedAt', label: 'Last Updated' })
298
+ },
299
+ )
300
+ // Step 2: Display formatted date in title
301
+ form.addStep(
302
+ {
303
+ instanceId: 'activity',
304
+ title: new I18nBuilder('activity.updated', 'Updated {when}', {
305
+ when: new FormatBuilder(
306
+ new JsonataBuilder('$date', { date: step1.data.getFieldData('updatedAt') }),
307
+ { dateFormat: 'fromNow' },
308
+ ),
309
+ }),
310
+ icon: 'stopwatch',
311
+ },
312
+ (step) => {
313
+ step.addText({ name: 'note', label: 'Note' })
314
+ },
315
+ )
316
+ ```
317
+
318
+ ## Date Format Codes
319
+
320
+ Use these format codes with `FormatBuilder` for date display:
321
+
322
+ | Format | Example Output | Description |
323
+ | --- | --- | --- |
324
+ | `'fromNow'` | "2 hours ago", "3 days ago" | Relative time (past) |
325
+ | `'toNow'` | "in 23 hours" | Relative time (future) |
326
+ | `'LT'` | "3:28 PM" | Time only |
327
+ | `'LTS'` | "3:28:57 PM" | Time with seconds |
328
+ | `'L'` | "03/03/2022" | Short date |
329
+ | `'l'` | "3/3/2022" | Short date (no padding) |
330
+ | `'LL'` | "March 3, 2022" | Long date (default) |
331
+ | `'ll'` | "Mar 3, 2022" | Abbreviated date |
332
+ | `'LLL'` | "March 3, 2022 3:28 PM" | Long date + time |
333
+ | `'lll'` | "Mar 3, 2022 3:28 PM" | Abbreviated date + time |
334
+ | `'LLLL'` | "Thursday, March 3, 2022 3:28 PM" | Full date + time |
335
+ | `'llll'` | "Thu, Mar 3, 2022 3:28 PM" | Abbreviated full |
336
+ | `'HH:mm'` | "15:28" | 24-hour format |
337
+
338
+ ## Common Mistakes
339
+
340
+ ### Don't use date fields without FormatBuilder
341
+
342
+ Date fields store ISO 8601 strings. Using a raw date reference displays unformatted text like `"2024-01-15T14:30:00Z"`. This applies to `submissionItemTitle`, `submissionItemSubtitle`, arrayField `title`/`subtitle`/`description`, and step `title`. Always wrap in `FormatBuilder` with a `dateFormat` option.
343
+
344
+ **In submission title/subtitle:**
345
+
346
+ ```typescript
347
+ const step1 = form.addStep({ instanceId: 'booking', icon: 'calendar' }, (step) => {
348
+ step.addDate({ name: 'appointment', label: 'Appointment Date' })
349
+ })
350
+ // WRONG: using JsonataBuilder alone displays raw ISO string like "2024-01-15T14:30:00Z"
351
+ // submissionItemTitle: new JsonataBuilder('$date', { date: step1.data.getFieldData('appointment') })
352
+ // CORRECT: always wrap date fields in FormatBuilder
353
+ // Displays: "January 15, 2024"
354
+ form.with({
355
+ submissionItemTitle: new FormatBuilder(
356
+ new JsonataBuilder('$date', { date: step1.data.getFieldData('appointment') }),
357
+ { dateFormat: 'LL' },
358
+ ),
359
+ submissionItemSubtitle: 'Booking',
360
+ })
361
+ ```
362
+
363
+ **In arrayField item title/subtitle:**
364
+
365
+ ```typescript
366
+ const events = step.addArrayField({ name: 'events', label: 'Events' })
367
+ // WRONG: raw ISO string in title
368
+ // events.with({
369
+ // title: new JsonataBuilder('$date', { date: events.getCurrentItemRef('eventDate') }),
370
+ // })
371
+ // CORRECT: wrap in FormatBuilder
372
+ events.with({
373
+ title: new FormatBuilder(
374
+ new JsonataBuilder('$date', { date: events.getCurrentItemRef('eventDate') }),
375
+ { dateFormat: 'LL' },
376
+ ),
377
+ })
378
+ events.addText({ name: 'eventName', label: 'Event Name' })
379
+ events.addDate({ name: 'eventDate', label: 'Event Date' })
380
+ ```
381
+
382
+ ### Don't use JSONata formatting functions
383
+
384
+ **Wrong:**
385
+
386
+ ```typescript
387
+ // DON'T DO THIS - use format property instead of JSONata functions
388
+ value: new JsonataBuilder('$formatNumber($price, "#,##0.00")', {
389
+ price: step1.data.getFieldData('price')
390
+ }) // JSONata formatting is not locale-aware
391
+ ```
392
+
393
+ **Correct:**
394
+
395
+ ```typescript
396
+ step.addNumber({
397
+ name: 'price',
398
+ label: 'Price',
399
+ format: { numberStyle: 'currency', currency: 'USD' },
400
+ })
401
+ ```
402
+
403
+ **Benefits of format property / FormatBuilder:**
404
+
405
+ - Validates options at build time
406
+ - Type-safe configuration
407
+ - Integrates with field rendering
408
+ - Handles locale automatically
409
+
410
+ ## Best Practices
411
+
412
+ | Scenario | Approach |
413
+ | --- | --- |
414
+ | Disabled number field | Field `format` property |
415
+ | `submissionItemTitle` / `submissionItemSubtitle` with formatting | `I18nBuilder` + `FormatBuilder` |
416
+ | ArrayField `title` / `subtitle` / `description` with formatting | `I18nBuilder` + `FormatBuilder` |
417
+ | Step `title` with formatting | `I18nBuilder` + `FormatBuilder` |
418
+ | Date values in any display property | Always `FormatBuilder` (even without `I18nBuilder`) |
419
+ | Never | JSONata `$formatNumber()` / `$formatBase()` functions |
420
+
421
+ **Ref:** `./i18n.md` for internationalization patterns