feat: add reactive conditional visibility to form modal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luca Oelfke 2026-02-13 20:37:12 +01:00
parent c494a75242
commit bfe4dc6d5e

View file

@ -2,6 +2,7 @@ import { App, Modal, TFile, Notice, FuzzySuggestModal } from 'obsidian';
import { FormDefinition } from '../types';
import { renderField, RenderedField, FieldValue } from './field-renderers';
import { validateForm, ValidationError } from '../utils/validators';
import { computeVisibility } from '../utils/condition-engine';
import { FormProcessor } from '../core/form-processor';
// ---------------------------------------------------------------------------
@ -37,6 +38,7 @@ export class FilePickerModal extends FuzzySuggestModal<TFile> {
export class FormModal extends Modal {
private form: FormDefinition;
private renderedFields: Map<string, RenderedField> = new Map();
private fieldContainers: Map<string, HTMLElement> = new Map();
private targetFile: TFile | undefined;
constructor(app: App, form: FormDefinition, targetFile?: TFile) {
@ -109,17 +111,25 @@ export class FormModal extends Modal {
// Render each field
for (const field of this.form.fields) {
const fieldWrapper = fieldsEl.createDiv({ cls: 'ff-field-wrapper' });
const initial = existingFrontmatter[field.id] as FieldValue | undefined;
const defaultVal = field.defaultValue as FieldValue | undefined;
const rendered = renderField(
this.app,
fieldsEl,
fieldWrapper,
field,
initial ?? defaultVal,
);
this.renderedFields.set(field.id, rendered);
this.fieldContainers.set(field.id, fieldWrapper);
}
// Attach change listeners for reactivity
this.attachChangeListeners(fieldsEl);
// Initial visibility evaluation
this.reevaluateVisibility();
// Footer
const footerEl = contentEl.createDiv({ cls: 'ff-form-footer' });
const submitText =
@ -157,8 +167,20 @@ export class FormModal extends Modal {
});
}
private async handleSubmit(): Promise<void> {
// Collect values
private reevaluateVisibility(): void {
const values = this.collectValues();
const visibility = computeVisibility(this.form.fields, values);
for (const field of this.form.fields) {
const container = this.fieldContainers.get(field.id);
if (!container) continue;
const isVisible = visibility.get(field.id) !== false;
container.style.display = isVisible ? '' : 'none';
}
}
private collectValues(): Record<string, unknown> {
const values: Record<string, unknown> = {};
for (const field of this.form.fields) {
const rendered = this.renderedFields.get(field.id);
@ -166,6 +188,29 @@ export class FormModal extends Modal {
values[field.id] = rendered.getValue();
}
}
return values;
}
private attachChangeListeners(container: HTMLElement): void {
container.addEventListener('input', () => this.reevaluateVisibility());
container.addEventListener('change', () => this.reevaluateVisibility());
container.addEventListener('click', () => {
// Defer to let toggle/rating state update first
requestAnimationFrame(() => this.reevaluateVisibility());
});
}
private async handleSubmit(): Promise<void> {
// Collect values (only from visible fields)
const allValues = this.collectValues();
const visibility = computeVisibility(this.form.fields, allValues);
const values: Record<string, unknown> = {};
for (const field of this.form.fields) {
if (visibility.get(field.id) !== false) {
values[field.id] = allValues[field.id];
}
}
// Clear previous errors
for (const rendered of this.renderedFields.values()) {
@ -173,7 +218,10 @@ export class FormModal extends Modal {
}
// Validate
const errors: ValidationError[] = validateForm(this.form.fields, values);
const visibleFields = this.form.fields.filter(
(f) => visibility.get(f.id) !== false,
);
const errors: ValidationError[] = validateForm(visibleFields, values);
if (errors.length > 0) {
for (const err of errors) {
const rendered = this.renderedFields.get(err.fieldId);
@ -207,5 +255,6 @@ export class FormModal extends Modal {
onClose(): void {
this.contentEl.empty();
this.renderedFields.clear();
this.fieldContainers.clear();
}
}