136 lines
5.4 KiB
Markdown
136 lines
5.4 KiB
Markdown
# Conditional Logic Design Document
|
||
|
||
**Date**: 2026-02-13
|
||
**Status**: Approved
|
||
**Plugin**: obsidian-formfire
|
||
**Phase**: 3 — Conditional Field Visibility
|
||
|
||
## Problem
|
||
|
||
Forms with many fields become overwhelming when not all fields are relevant in every context. A "Meeting Notes" form shouldn't show "Book Author" fields. Currently all fields are always visible — users must skip irrelevant ones manually.
|
||
|
||
## Solution
|
||
|
||
Add per-field conditional visibility rules with AND/OR logic. Fields can reference other fields' values to determine whether they should be shown or hidden. Hidden fields are excluded from submission.
|
||
|
||
## Approach
|
||
|
||
**Rule-per-Field** — each `FormField` gets an optional `conditions` object containing rules and a logic combinator. This keeps conditions co-located with the field they control and avoids a separate rules data structure.
|
||
|
||
## Data Model
|
||
|
||
### New Types
|
||
|
||
```typescript
|
||
type ConditionOperator =
|
||
| 'equals' | 'not_equals' // all field types
|
||
| 'contains' | 'not_contains' // text, textarea, tags
|
||
| 'is_empty' | 'is_not_empty' // all field types
|
||
| 'greater_than' | 'less_than'; // number, rating, slider
|
||
|
||
interface ConditionRule {
|
||
fieldId: string; // reference to another field
|
||
operator: ConditionOperator;
|
||
value?: unknown; // not needed for is_empty/is_not_empty
|
||
}
|
||
|
||
interface FieldConditions {
|
||
logic: 'and' | 'or'; // how rules are combined
|
||
rules: ConditionRule[]; // at least 1 rule
|
||
}
|
||
```
|
||
|
||
### FormField Extension
|
||
|
||
```typescript
|
||
interface FormField {
|
||
// ... existing fields ...
|
||
conditions?: FieldConditions; // optional, undefined = always visible
|
||
}
|
||
```
|
||
|
||
### Operator Availability by Field Type
|
||
|
||
| Operator | text/textarea | number | toggle | date/time | dropdown | tags | note-link/folder | rating | slider | color |
|
||
|----------|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||
| equals / not_equals | yes | yes | yes | yes | yes | — | yes | yes | yes | yes |
|
||
| contains / not_contains | yes | — | — | — | — | yes | — | — | — | — |
|
||
| is_empty / is_not_empty | yes | yes | — | yes | yes | yes | yes | yes | yes | yes |
|
||
| greater_than / less_than | — | yes | — | — | — | — | — | yes | yes | — |
|
||
|
||
Note: `toggle` doesn't support `is_empty` since it always has a value (true/false).
|
||
|
||
## Evaluation Engine
|
||
|
||
New module: `src/utils/condition-engine.ts`
|
||
|
||
```typescript
|
||
evaluateConditions(
|
||
conditions: FieldConditions,
|
||
values: Record<string, unknown>
|
||
): boolean
|
||
```
|
||
|
||
### Logic
|
||
|
||
1. Each `ConditionRule` is evaluated against the current value of the referenced field
|
||
2. Results are combined: `and` = all must be true, `or` = at least one must be true
|
||
3. Return: `true` = show field, `false` = hide field
|
||
|
||
### Operator Semantics
|
||
|
||
- `equals` / `not_equals` — string coercion for comparison, strict for booleans
|
||
- `contains` / `not_contains` — substring check for strings, element check for arrays (tags)
|
||
- `is_empty` / `is_not_empty` — `null`, `undefined`, `""`, `[]` count as empty
|
||
- `greater_than` / `less_than` — numeric comparison (parseFloat)
|
||
|
||
### Cycle Prevention
|
||
|
||
Fields can only reference fields that appear **before** them in the field list. This prevents circular dependencies by design and is enforced in the builder UI (field dropdown only shows preceding fields).
|
||
|
||
## Reactivity in Form Modal
|
||
|
||
1. **Change listeners** on every field — value changes trigger `reevaluateVisibility()`
|
||
2. `reevaluateVisibility()` iterates all fields with `conditions`, evaluates them, toggles `display: none`
|
||
3. **Hidden fields excluded from submit** — values not written to frontmatter
|
||
4. **Validation skips hidden fields** — a required field that is hidden does not block submission
|
||
|
||
## Builder UI
|
||
|
||
### Per-Field Conditions Editor
|
||
|
||
Located below existing field settings, collapsible:
|
||
|
||
**Collapsed**: Shows summary like `"Show if: type = Meeting"` or `"Always visible"`
|
||
|
||
**Expanded**:
|
||
1. **Logic dropdown** at top: `ALL conditions match` (AND) / `ANY condition matches` (OR)
|
||
2. **Rule rows**, each containing:
|
||
- Field dropdown (only fields before current field)
|
||
- Operator dropdown (filtered by selected field's type)
|
||
- Value input (type-dependent: text input, dropdown with options, number input, toggle; hidden for is_empty/is_not_empty)
|
||
- Delete button (×)
|
||
3. **"+ Add condition"** button
|
||
|
||
### Live Preview Behavior
|
||
|
||
Fields with unmet conditions are shown **grayed out with a "conditional" badge** in the builder preview (not fully hidden, so the builder user can see all fields exist).
|
||
|
||
## Files Changed
|
||
|
||
| File | Change |
|
||
|------|--------|
|
||
| `src/types.ts` | New types: `ConditionOperator`, `ConditionRule`, `FieldConditions`; add `conditions?` to `FormField` |
|
||
| `src/utils/condition-engine.ts` | **New** — `evaluateConditions()`, operator logic, helpers |
|
||
| `src/utils/validators.ts` | Accept visibility map, skip hidden fields |
|
||
| `src/ui/form-modal.ts` | Change listeners, `reevaluateVisibility()`, exclude hidden fields from submit |
|
||
| `src/ui/form-builder.ts` | Conditions editor UI per field (collapsed/expanded, rule editor) |
|
||
| `src/ui/field-renderers.ts` | No changes — conditions operate one level above |
|
||
| `styles.css` | Condition editor styles, conditional badge in preview, show/hide transitions |
|
||
|
||
## Out of Scope
|
||
|
||
- Nested condition groups (AND inside OR) — one level is sufficient
|
||
- Form-level conditions (hide entire form)
|
||
- Actions other than show/hide (e.g. set value, change required)
|
||
- Migration — `conditions` is optional, existing forms work unchanged
|