Skip to content

Commit 7f82334

Browse files
nicobytesspbolton
authored andcommitted
33971 task support field visibility and state management in bridge api (#35003)
This pull request adds robust show/hide field support to both Angular and Dojo form bridges, enabling dynamic field visibility management in forms. It introduces a standardized `show()` and `hide()` API for fields, propagates visibility changes to the Angular state store, and includes comprehensive tests to ensure correct behavior and error handling. Additionally, it updates the UI to reflect field visibility and improves documentation and type safety. **Field Visibility API and Integration:** * Added `show()` and `hide()` methods to the `FormFieldAPI` interface, and implemented them in both `AngularFormBridge` and `DojoFormBridge` to allow programmatic control of field visibility. [[1]](diffhunk://#diff-9d724fb56dff8e4f9a22bc4a0963020c10c8964e1a0db7dc66de8cf1da373d92R39-R48) [[2]](diffhunk://#diff-08b7f2dbb6ec1f4033d7a5801c7f63b080a8f9565b11c3f9b7c40ea512578e6fR249-R260) [[3]](diffhunk://#diff-fe3a388c08be205717a8a85cc52f63bf2b3aacac655960871dc86e0276987e09R252-R275) * In `AngularFormBridge`, introduced an optional `onFieldVisibilityChange` callback, injected via the factory and used to decouple field visibility changes from the store, enabling state updates when fields are shown or hidden. [[1]](diffhunk://#diff-08b7f2dbb6ec1f4033d7a5801c7f63b080a8f9565b11c3f9b7c40ea512578e6fR33-R46) [[2]](diffhunk://#diff-08b7f2dbb6ec1f4033d7a5801c7f63b080a8f9565b11c3f9b7c40ea512578e6fR55-R70) [[3]](diffhunk://#diff-7e7427e48350646c481f2a6d1653c32ba7e345af017115e8c16ae748689e1bfeL11-R26) [[4]](diffhunk://#diff-7e7427e48350646c481f2a6d1653c32ba7e345af017115e8c16ae748689e1bfeL42-R55) [[5]](diffhunk://#diff-cc87f031e0082ccc972a895afc5c910e3c87b060d93355eb7d757c25759d44a4L127-R137) * Updated the Angular form bridge factory and the `NativeFieldComponent` to pass and handle the `onFieldVisibilityChange` callback, ensuring visibility changes are reflected in the Angular store. [[1]](diffhunk://#diff-7e7427e48350646c481f2a6d1653c32ba7e345af017115e8c16ae748689e1bfeL11-R26) [[2]](diffhunk://#diff-7e7427e48350646c481f2a6d1653c32ba7e345af017115e8c16ae748689e1bfeL42-R55) [[3]](diffhunk://#diff-cc87f031e0082ccc972a895afc5c910e3c87b060d93355eb7d757c25759d44a4L127-R137) **UI and Store Synchronization:** * Modified form and field component templates to apply the `hidden` class based on the field's visibility state from the store, ensuring the UI reflects the correct visibility. * Injected and mocked `DotEditContentStore` in relevant components and tests to support and verify visibility state propagation. [[1]](diffhunk://#diff-cc87f031e0082ccc972a895afc5c910e3c87b060d93355eb7d757c25759d44a4R63-R67) [[2]](diffhunk://#diff-007f8368e1918a0e20d13e5200f1bd7b8959b0724e5f4835b2d976b317bd7b72R10-R11) [[3]](diffhunk://#diff-007f8368e1918a0e20d13e5200f1bd7b8959b0724e5f4835b2d976b317bd7b72R25-R30) [[4]](diffhunk://#diff-04465fbcea9c39bc66d73064e129700c837b5f08317a4fcab62e054b42d15665R13) [[5]](diffhunk://#diff-04465fbcea9c39bc66d73064e129700c837b5f08317a4fcab62e054b42d15665R47-R52) **Testing Enhancements:** * Added comprehensive tests for the new `show()` and `hide()` methods in both Angular and Dojo bridges, including error handling and NgZone integration. [[1]](diffhunk://#diff-56d79fca411f7de0eccc5e1f0b399892ecebc2c9a5148d75ebba52fa1f272639L371-R380) [[2]](diffhunk://#diff-56d79fca411f7de0eccc5e1f0b399892ecebc2c9a5148d75ebba52fa1f272639R453-R530) [[3]](diffhunk://#diff-d0fd14ebe4c48f46fc11d776a7a09ec7b511a7004d8aa03fc7731089e27c0334R250-R332) **Documentation and Minor Fixes:** * Improved documentation for the form bridge configuration and APIs, clarifying the new callback and its usage. * Fixed a minor typo in an event binding in the binary field wrapper. * Updated CSS grid class for better layout consistency. This PR fixes: #33971
1 parent 9ef6b36 commit 7f82334

19 files changed

Lines changed: 965 additions & 63 deletions

File tree

.claude/skills/vtl-migration/SKILL.md

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ For the full migration rules, all code examples, the DaisyUI styling section, an
1717
| `DotCustomFieldApi.get('id')` | `DotCustomFieldApi.getField('id').getValue()` |
1818
| `DotCustomFieldApi.set('id', val)` | `DotCustomFieldApi.getField('id').setValue(val)` |
1919
| `DotCustomFieldApi.onChangeField('id', cb)` | `DotCustomFieldApi.getField('id').onChange(cb)` |
20+
| Manual DOM show/hide of a field | `DotCustomFieldApi.getField('id').show()` / `.hide()` |
21+
| Manual DOM enable/disable of a field | `DotCustomFieldApi.getField('id').enable()` / `.disable()` |
2022
| `dojo.ready(fn)` | `DotCustomFieldApi.ready(fn)` |
2123
| `dojo.byId('el')` | `document.getElementById('el')` |
2224
| `dijit.byId('id')` | `DotCustomFieldApi.getField('id')` |
@@ -106,6 +108,34 @@ DotCustomFieldApi.ready(() => {
106108
});
107109
```
108110

111+
**Field visibility and state control:**
112+
```js
113+
DotCustomFieldApi.ready(() => {
114+
const mediaField = DotCustomFieldApi.getField('media');
115+
const mediaFileField = DotCustomFieldApi.getField('mediafile');
116+
117+
// Show/hide based on current value
118+
if (mediaField.getValue() === 'upload') {
119+
mediaFileField.show();
120+
} else {
121+
mediaFileField.hide();
122+
}
123+
124+
// React to changes
125+
mediaField.onChange((value) => {
126+
if (value === 'upload') {
127+
mediaFileField.show();
128+
} else {
129+
mediaFileField.hide();
130+
}
131+
});
132+
133+
// Enable/disable a field
134+
mediaFileField.disable(); // blocks editing, applies disabled styling
135+
mediaFileField.enable(); // restores interactivity
136+
});
137+
```
138+
109139
**Multiple onChange for the same field** → combine into one handler:
110140
```js
111141
// Old: two separate onChangeField calls for 'title'
@@ -158,6 +188,8 @@ Verify the migration passes this checklist (details in `references/migration-gui
158188
- [ ] Styling uses DaisyUI components where applicable (buttons, inputs, selects, modals, links) and Tailwind for layout; no inline styles unless necessary
159189
- [ ] All field access inside `DotCustomFieldApi.ready()`
160190
- [ ] All `getField()` calls stored in variables and reused
191+
- [ ] Field visibility uses `field.show()` / `field.hide()` instead of manual DOM manipulation
192+
- [ ] Field state uses `field.enable()` / `field.disable()` instead of manual DOM attribute changes
161193
- [ ] VTL variables unchanged
162194
- [ ] Business logic unchanged
163195

.claude/skills/vtl-migration/references/migration-guide.md

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,29 @@ DotCustomFieldApi.ready(() => {
3030
field.onChange((value) => {
3131
console.log(value);
3232
});
33+
34+
// Visibility control
35+
field.hide(); // hides the field (input + label) from view
36+
field.show(); // restores the field's visibility
37+
38+
// State control
39+
field.disable(); // prevents user interaction, applies disabled styling
40+
field.enable(); // restores interactivity
3341
});
3442
```
3543

44+
### Field Object Methods
45+
46+
| Method | Description |
47+
|--------|-------------|
48+
| `getValue()` | Returns the current value of the field |
49+
| `setValue(value)` | Sets the field's value |
50+
| `onChange(callback)` | Subscribes to value changes |
51+
| `show()` | Makes the field visible (input and associated label) |
52+
| `hide()` | Hides the field from view (input and associated label) |
53+
| `enable()` | Restores interactivity to the field |
54+
| `disable()` | Prevents user interaction and applies disabled styling |
55+
3656
Always wrap all field access code inside `DotCustomFieldApi.ready()` to ensure the API is initialized.
3757

3858
---
@@ -283,6 +303,59 @@ For `dojoType="dijit.Dialog"` elements, use the native HTML `<dialog>` element w
283303
</script>
284304
```
285305
306+
### Rule 12: Use `field.show()` / `field.hide()` and `field.enable()` / `field.disable()` for field visibility and state
307+
308+
When toggling a field's visibility or enabled/disabled state, use the built-in API methods instead of manipulating the DOM directly. These methods handle both the input element and its associated label.
309+
310+
**Old (manual DOM manipulation):**
311+
```js
312+
// Hiding a field by manipulating DOM
313+
const fieldEl = document.getElementById("mediafile");
314+
fieldEl.style.display = "none";
315+
// or
316+
fieldEl.closest(".field-wrapper").classList.add("hidden");
317+
318+
// Disabling a field by manipulating DOM
319+
fieldEl.setAttribute("disabled", "true");
320+
```
321+
322+
**New (API methods):**
323+
```js
324+
DotCustomFieldApi.ready(() => {
325+
const mediaFileField = DotCustomFieldApi.getField("mediafile");
326+
327+
// Visibility
328+
mediaFileField.hide(); // hides the field + label
329+
mediaFileField.show(); // restores visibility
330+
331+
// State
332+
mediaFileField.disable(); // blocks editing + disabled styling
333+
mediaFileField.enable(); // restores interactivity
334+
});
335+
```
336+
337+
**Conditional visibility based on another field's value:**
338+
```js
339+
DotCustomFieldApi.ready(() => {
340+
const mediaField = DotCustomFieldApi.getField("media");
341+
const mediaFileField = DotCustomFieldApi.getField("mediafile");
342+
343+
const toggleVisibility = (value) => {
344+
if (value === "upload") {
345+
mediaFileField.show();
346+
} else {
347+
mediaFileField.hide();
348+
}
349+
};
350+
351+
// Set initial visibility
352+
toggleVisibility(mediaField.getValue() || "external");
353+
354+
// React to changes
355+
mediaField.onChange(toggleVisibility);
356+
});
357+
```
358+
286359
### Native Components and Platform Styles
287360
288361
Custom fields run inside the admin Angular app, which uses **DaisyUI 5** and **Tailwind CSS**. Prefer **DaisyUI component classes** for semantic styling so the field matches the platform and respects the theme.
@@ -464,6 +537,8 @@ Be consistent when writing `<script>` tags in migrated VTL files:
464537
465538
**DO change:**
466539
- API method calls: get/set/onChangeField → getField/getValue/setValue/onChange
540+
- Manual DOM show/hide of fields → `field.show()` / `field.hide()`
541+
- Manual DOM enable/disable of fields → `field.enable()` / `field.disable()`
467542
- `dojo.ready``DotCustomFieldApi.ready`
468543
- `dojo.byId``document.getElementById`
469544
- `dijit.byId``DotCustomFieldApi.getField`
@@ -495,6 +570,8 @@ Be consistent when writing `<script>` tags in migrated VTL files:
495570
- [ ] All `DotCustomFieldApi.get('name')``getField('name').getValue()`
496571
- [ ] All `DotCustomFieldApi.set('name', value)``getField('name').setValue(value)`
497572
- [ ] All `DotCustomFieldApi.onChangeField('name', cb)``getField('name').onChange(cb)`
573+
- [ ] Manual DOM show/hide of fields → `getField('name').show()` / `.hide()`
574+
- [ ] Manual DOM enable/disable of fields → `getField('name').enable()` / `.disable()`
498575
499576
### 4. Remove Dojo/Dijit
500577
- [ ] Remove all `dojo.require()` statements
@@ -516,6 +593,8 @@ Be consistent when writing `<script>` tags in migrated VTL files:
516593
- [ ] All VTL variables (`${fieldId}`, `$variable`) remain unchanged
517594
- [ ] Business logic unchanged
518595
- [ ] Functionality remains identical
596+
- [ ] Field visibility uses `field.show()` / `field.hide()` instead of manual DOM manipulation
597+
- [ ] Field state uses `field.enable()` / `field.disable()` instead of manual DOM attribute changes
519598
- [ ] Styling uses DaisyUI components where applicable; custom CSS only when DaisyUI/Tailwind are insufficient
520599
521600
---
@@ -565,6 +644,20 @@ const value = field.getValue() || "";
565644
const length = value.length;
566645
```
567646
647+
### Don't manually manipulate DOM for field visibility or state
648+
649+
```js
650+
// BAD — manual DOM manipulation
651+
const el = document.querySelector('[data-field="mediafile"]');
652+
el.style.display = "none";
653+
el.setAttribute("disabled", "true");
654+
655+
// GOOD — use the API
656+
const mediaFileField = DotCustomFieldApi.getField("mediafile");
657+
mediaFileField.hide();
658+
mediaFileField.disable();
659+
```
660+
568661
### Don't mix old and new APIs
569662
570663
```js
@@ -978,3 +1071,97 @@ DotCustomFieldApi.ready(() => {
9781071
<input type="text" id="slugInput" class="input input-bordered w-full" />
9791072
<div id="slugSuggestion" class="hidden mt-2 text-primary" role="region" aria-live="polite"></div>
9801073
```
1074+
1075+
---
1076+
1077+
### Example 4: Conditional Field Visibility (show/hide)
1078+
1079+
This example shows a "Media" form where the "Media File" field is shown or hidden based on the "Media Location" radio selection. The custom field stores the selection in one field and toggles visibility of another.
1080+
1081+
**New (show_hide.vtl):**
1082+
```html
1083+
<script>
1084+
DotCustomFieldApi.ready(() => {
1085+
const mediaField = DotCustomFieldApi.getField("media");
1086+
const mediaFileField = DotCustomFieldApi.getField("mediafile");
1087+
1088+
const currentValue = mediaField.getValue() || "external";
1089+
const radios = document.querySelectorAll('input[name="mediaLocation"]');
1090+
1091+
const initialRadio = Array.from(radios).find((radio) => radio.value === currentValue);
1092+
if (initialRadio) {
1093+
initialRadio.checked = true;
1094+
}
1095+
1096+
if (currentValue === "upload") {
1097+
mediaFileField.show();
1098+
} else {
1099+
mediaFileField.hide();
1100+
}
1101+
1102+
radios.forEach((radio) => {
1103+
radio.addEventListener("change", (e) => {
1104+
const value = e.target.value;
1105+
mediaField.setValue(value);
1106+
1107+
if (value === "upload") {
1108+
mediaFileField.show();
1109+
} else {
1110+
mediaFileField.hide();
1111+
}
1112+
});
1113+
});
1114+
});
1115+
</script>
1116+
1117+
<fieldset class="fieldset">
1118+
<legend class="fieldset-legend">Media Location</legend>
1119+
<div class="flex gap-4">
1120+
<label class="flex items-center gap-2 cursor-pointer">
1121+
<input type="radio" name="mediaLocation" value="upload" class="radio radio-primary" />
1122+
<span>Upload File</span>
1123+
</label>
1124+
<label class="flex items-center gap-2 cursor-pointer">
1125+
<input type="radio" name="mediaLocation" value="external" class="radio radio-primary" checked />
1126+
<span>Link to External File</span>
1127+
</label>
1128+
</div>
1129+
</fieldset>
1130+
```
1131+
1132+
Key points:
1133+
- `mediaFileField.show()` and `mediaFileField.hide()` control the visibility of the entire "mediafile" field (input + label) — no manual DOM manipulation needed.
1134+
- The radio buttons use DaisyUI classes (`radio radio-primary`) and are wrapped in a `fieldset` with `fieldset-legend`.
1135+
- The initial visibility is set based on `mediaField.getValue()` before attaching the change listener.
1136+
1137+
---
1138+
1139+
### Example 5: Conditional Field Enable/Disable
1140+
1141+
This example disables an "Expiration Date" field until the user checks an "Enable Expiration" checkbox.
1142+
1143+
**New (expiration_toggle.vtl):**
1144+
```html
1145+
<script>
1146+
DotCustomFieldApi.ready(() => {
1147+
const enableExpirationField = DotCustomFieldApi.getField("enableExpiration");
1148+
const expirationDateField = DotCustomFieldApi.getField("expirationDate");
1149+
1150+
const toggle = (value) => {
1151+
if (value === "true" || value === true) {
1152+
expirationDateField.enable();
1153+
} else {
1154+
expirationDateField.disable();
1155+
}
1156+
};
1157+
1158+
toggle(enableExpirationField.getValue());
1159+
enableExpirationField.onChange(toggle);
1160+
});
1161+
</script>
1162+
```
1163+
1164+
Key points:
1165+
- `expirationDateField.disable()` prevents editing and applies disabled styling; `enable()` restores interactivity.
1166+
- No manual DOM attribute manipulation (`setAttribute("disabled", ...)`) is needed.
1167+
- The pattern is the same as show/hide: set initial state, then react to changes via `onChange`.

0 commit comments

Comments
 (0)