diff --git a/trmnl-form-builder.js b/trmnl-form-builder.js
index 99e8f4c..91d3bc8 100644
--- a/trmnl-form-builder.js
+++ b/trmnl-form-builder.js
@@ -102,7 +102,10 @@ class TRMLYamlForm extends HTMLElement {
i18n_descriptions: { label: 'Description Translations', type: 'i18n_map', help: 'Add translated descriptions for specific locales.' },
// Conditional Logic
- conditional_validation: { label: 'Conditional Logic', type: 'conditional_builder', help: 'Hide other fields based on this field\'s value' }
+ conditional_validation: { label: 'Conditional Logic', type: 'conditional_builder', help: 'Hide other fields based on this field\'s value' },
+
+ // Dependencies
+ depends_on: { label: 'Depends On', type: 'depends_on_select', help: 'Load options only after another field has a value' }
};
// --- Field Types (Added 'properties' array to each) ---
@@ -167,9 +170,9 @@ class TRMLYamlForm extends HTMLElement {
category: 'SELECTION', name: 'Select', description: 'Dropdown',
properties: ['options', 'default', 'multiple', 'optional', 'conditional_validation']
},
- xhrSelect: {
- category: 'SELECTION', name: 'XhrSelect', description: 'Dynamic dropdown',
- properties: ['endpoint', 'http_verb', 'multiple', 'optional']
+ xhrSelect: {
+ category: 'SELECTION', name: 'XhrSelect', description: 'Dynamic dropdown',
+ properties: ['endpoint', 'http_verb', 'depends_on', 'multiple', 'optional']
},
xhrSelectSearch: {
category: 'SELECTION', name: 'XhrSelectSearch', description: 'Searchable dynamic',
@@ -1039,7 +1042,10 @@ class TRMLYamlForm extends HTMLElement {
// Perform the move
const [draggedField] = this.fields.splice(this.draggedFromIndex, 1);
this.fields.splice(toIndex, 0, draggedField);
-
+
+ // Clean up any invalid depends_on references after reorder
+ this.cleanupDependsOn();
+
// Re-render
this.updateCanvasView();
this.updateYamlOutput();
@@ -1066,6 +1072,38 @@ class TRMLYamlForm extends HTMLElement {
this.updateFieldCount();
}
+ // Clean up invalid depends_on references after reorder/delete
+ cleanupDependsOn() {
+ const cleared = [];
+
+ this.fields.forEach((field, index) => {
+ if (!field.depends_on) return;
+
+ const parentKeyname = field.depends_on;
+
+ // Check if parent exists and is an xhrSelect
+ const parent = this.fields.find(f => f.keyname === parentKeyname);
+ if (!parent || parent.field_type !== 'xhrSelect') {
+ cleared.push({ field: field.keyname, parent: parentKeyname, reason: 'parent was deleted' });
+ delete field.depends_on;
+ return;
+ }
+
+ // Check if parent appears before this field
+ const parentIndex = this.fields.findIndex(f => f.keyname === parentKeyname);
+ if (parentIndex >= index) {
+ cleared.push({ field: field.keyname, parent: parentKeyname, reason: 'parent must appear first' });
+ delete field.depends_on;
+ }
+ });
+
+ // Show toast for each cleared dependency
+ if (cleared.length > 0) {
+ const messages = cleared.map(c => `'${c.field}' dependency on '${c.parent}' cleared (${c.reason})`);
+ this.showToast(messages.join('\n'));
+ }
+ }
+
updateAuthorBioState() {
const hasAuthorBio = this.fields.some(f => f.field_type === 'author_bio');
const btn = this.shadowRoot.querySelector('button[data-field-type="author_bio"]');
@@ -1254,7 +1292,66 @@ class TRMLYamlForm extends HTMLElement {
${def.help ? `
${def.help}
` : ''}
`;
}
-
+
+ // --- Depends On Select Renderer ---
+ if (def.type === 'depends_on_select') {
+ const currentIndex = this.fields.findIndex(f => f.id === field.id);
+
+ // Find fields that would create a circular dependency if selected
+ // (any field that directly or transitively depends on the current field)
+ const wouldCreateCycle = (candidateKeyname) => {
+ const visited = new Set([field.keyname]);
+ let current = candidateKeyname;
+
+ while (current) {
+ if (visited.has(current)) return true;
+ visited.add(current);
+ const parent = this.fields.find(f => f.keyname === current);
+ current = parent?.depends_on;
+ }
+ return false;
+ };
+
+ // Get xhrSelect fields that:
+ // 1. Are not this field
+ // 2. Appear BEFORE this field in the list (parent must be declared first)
+ // 3. Would not create a circular dependency
+ const validParentFields = this.fields.filter((f, idx) =>
+ f.id !== field.id &&
+ f.field_type === 'xhrSelect' &&
+ idx < currentIndex &&
+ !wouldCreateCycle(f.keyname)
+ );
+
+ const currentValue = field.depends_on || '';
+
+ let optionsHtml = '';
+ validParentFields.forEach(f => {
+ const selected = currentValue === f.keyname ? 'selected' : '';
+ optionsHtml += ``;
+ });
+
+ // Check if there are xhrSelect fields but none are valid parents
+ const allXhrSelectFields = this.fields.filter(f => f.id !== field.id && f.field_type === 'xhrSelect');
+ let helpMsg = '';
+ if (allXhrSelectFields.length === 0) {
+ helpMsg = 'Add another xhrSelect field first to enable dependencies.
';
+ } else if (validParentFields.length === 0) {
+ helpMsg = 'No valid parents available. Parent fields must appear before this field in the list.
';
+ } else if (def.help) {
+ helpMsg = `${def.help}
`;
+ }
+
+ return `
+
+
+
+ ${helpMsg}
+
`;
+ }
+
// --- Handle generic Select inputs (including http_verb) ---
if (def.options && Array.isArray(def.options)) {
// Determine the current value, falling back to default if not set
@@ -1573,6 +1670,12 @@ class TRMLYamlForm extends HTMLElement {
this.updateYamlOutput();
return;
}
+ // Special handling for 'depends_on' - delete property if set to empty/None
+ if (prop === 'depends_on' && value === '') {
+ delete field.depends_on;
+ this.updateYamlOutput();
+ return;
+ }
}
// 2. Save Value
@@ -1685,11 +1788,15 @@ class TRMLYamlForm extends HTMLElement {
// Always print the http_verb for XHR fields, even if it's the default 'GET'.
lines.push(` http_verb: ${verb}`);
} else if (field.http_verb && field.http_verb !== 'GET') {
- // Keep this secondary check for non-XHR fields that might use a non-default verb
+ // Keep this secondary check for non-XHR fields that might use a non-default verb
// (though unlikely with current definitions)
lines.push(` http_verb: ${field.http_verb}`);
}
-
+
+ if (field.depends_on) {
+ lines.push(` depends_on: ${field.depends_on}`);
+ }
+
if (field.value) {
lines.push(` value: ${this.escapeYaml(field.value)}`);
}
@@ -1762,10 +1869,11 @@ class TRMLYamlForm extends HTMLElement {
reorderFields(draggedId, targetId) {
const draggedIndex = this.fields.findIndex(f => f.id === draggedId);
const targetIndex = this.fields.findIndex(f => f.id === targetId);
-
+
if (draggedIndex !== -1 && targetIndex !== -1) {
const [draggedField] = this.fields.splice(draggedIndex, 1);
this.fields.splice(targetIndex, 0, draggedField);
+ this.cleanupDependsOn();
this.updateCanvasView();
this.updateYamlOutput();
}
@@ -1809,10 +1917,12 @@ class TRMLYamlForm extends HTMLElement {
if (this.selectedFieldId === fieldId) {
this.selectedFieldId = this.fields[0]?.id || null;
}
+ // Clean up any depends_on references to the deleted field
+ this.cleanupDependsOn();
this.updateCanvasView();
this.updateConfigPanel();
this.updateYamlOutput();
- this.updateAuthorBioState();
+ this.updateAuthorBioState();
}
updateFieldCount() {