feat: add reactive conditional visibility to form modal
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c494a75242
commit
bfe4dc6d5e
1 changed files with 53 additions and 4 deletions
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue