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 { FormDefinition } from '../types';
import { renderField, RenderedField, FieldValue } from './field-renderers'; import { renderField, RenderedField, FieldValue } from './field-renderers';
import { validateForm, ValidationError } from '../utils/validators'; import { validateForm, ValidationError } from '../utils/validators';
import { computeVisibility } from '../utils/condition-engine';
import { FormProcessor } from '../core/form-processor'; import { FormProcessor } from '../core/form-processor';
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -37,6 +38,7 @@ export class FilePickerModal extends FuzzySuggestModal<TFile> {
export class FormModal extends Modal { export class FormModal extends Modal {
private form: FormDefinition; private form: FormDefinition;
private renderedFields: Map<string, RenderedField> = new Map(); private renderedFields: Map<string, RenderedField> = new Map();
private fieldContainers: Map<string, HTMLElement> = new Map();
private targetFile: TFile | undefined; private targetFile: TFile | undefined;
constructor(app: App, form: FormDefinition, targetFile?: TFile) { constructor(app: App, form: FormDefinition, targetFile?: TFile) {
@ -109,17 +111,25 @@ export class FormModal extends Modal {
// Render each field // Render each field
for (const field of this.form.fields) { for (const field of this.form.fields) {
const fieldWrapper = fieldsEl.createDiv({ cls: 'ff-field-wrapper' });
const initial = existingFrontmatter[field.id] as FieldValue | undefined; const initial = existingFrontmatter[field.id] as FieldValue | undefined;
const defaultVal = field.defaultValue as FieldValue | undefined; const defaultVal = field.defaultValue as FieldValue | undefined;
const rendered = renderField( const rendered = renderField(
this.app, this.app,
fieldsEl, fieldWrapper,
field, field,
initial ?? defaultVal, initial ?? defaultVal,
); );
this.renderedFields.set(field.id, rendered); 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 // Footer
const footerEl = contentEl.createDiv({ cls: 'ff-form-footer' }); const footerEl = contentEl.createDiv({ cls: 'ff-form-footer' });
const submitText = const submitText =
@ -157,8 +167,20 @@ export class FormModal extends Modal {
}); });
} }
private async handleSubmit(): Promise<void> { private reevaluateVisibility(): void {
// Collect values 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> = {}; const values: Record<string, unknown> = {};
for (const field of this.form.fields) { for (const field of this.form.fields) {
const rendered = this.renderedFields.get(field.id); const rendered = this.renderedFields.get(field.id);
@ -166,6 +188,29 @@ export class FormModal extends Modal {
values[field.id] = rendered.getValue(); 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 // Clear previous errors
for (const rendered of this.renderedFields.values()) { for (const rendered of this.renderedFields.values()) {
@ -173,7 +218,10 @@ export class FormModal extends Modal {
} }
// Validate // 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) { if (errors.length > 0) {
for (const err of errors) { for (const err of errors) {
const rendered = this.renderedFields.get(err.fieldId); const rendered = this.renderedFields.get(err.fieldId);
@ -207,5 +255,6 @@ export class FormModal extends Modal {
onClose(): void { onClose(): void {
this.contentEl.empty(); this.contentEl.empty();
this.renderedFields.clear(); this.renderedFields.clear();
this.fieldContainers.clear();
} }
} }