Skip to content

Commit

Permalink
Merge pull request #288 from danielo515/feat/dependent-fields
Browse files Browse the repository at this point in the history
Feat/dependent-fields
  • Loading branch information
danielo515 authored Jun 30, 2024
2 parents 388450c + f074617 commit f87f9a5
Show file tree
Hide file tree
Showing 29 changed files with 783 additions and 259 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ meta.json
site/
docs/index.md
docs/changelog.md
coverage
39 changes: 39 additions & 0 deletions docs/blog/posts/NextRelease.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
title: Release notes for 1.4x.0
date: 2024-06-29
tags: release-notes
---

This is a very exciting release for me, because it includes one of the features that I have been wanting to implement for a long time, and also one of the most requested features. But it also includes some minor improvements, so let's take those first:

- The placeholder of the label is set by default to the name of the field. This will make it easier for people to understand that the default label value is the name of the field.

Now the big feature:

## **Dependent fields**

As with every new feature, I like to start small, so this first version is very simple.
It just settles the basic foundation and works only with the most basic field types.
This will allow me to gather feedback and improve it in the next releases after making sure that the basic functionality is working as expected.

In this first approach there are not many safeguards either, so you can end up in forms that don't render anything, for example because of with fields that are excluding each other. I don't think this is going to be a big problem in practice, but I will be monitoring the feedback to see if it is necessary to add some kind of validation, or at least some kind of warning.
The reason I am not adding it any limitations in this first version is because flexibility: forms can be called with parameters to omit fields, default values, etc. and I don't want to limit that flexibility.

Here are some screenshots of the feature in action.

Form builder:

![boolean comparison](<conditional-boolean.png>)
![string comparison](<conditional-string.png>)

Form in preview mode with the condition met
![condition met](<condition-met.png>)
with the condition not met
![condition not met](<condition-not-met.png>)

This first iteration is purely visual: just because a field is hidden it does not mean that, if it has a value, is not going to be included in the result. If you fill a field, and then do something that makes it hidden, the value will still be included in the result. I think in practice most people just needs a way to start with several fields hidden, and then show them based on the value of other fields, so I think this is a good first approach.

The wording of the feature is not final, I'm not very satisfied with the current wording, so I'm open to suggestions.
I hope you like it, that it does not introduce too many inconveniences and that it is useful to you.

Please let me know your thoughts and suggestions.
Binary file added docs/blog/posts/condition-met.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/blog/posts/condition-not-met.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/blog/posts/conditional-boolean.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/blog/posts/conditional-string.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 11 additions & 10 deletions jest.config.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { type JestConfigWithTsJest } from 'ts-jest'
import { type JestConfigWithTsJest } from "ts-jest";

const jestConfig: JestConfigWithTsJest = {
// preset: 'ts-jest/presets/default-esm', // or other ESM presets
preset: 'ts-jest', // or other ESM presets
moduleDirectories: ['node_modules', '<rootDir>'],
roots: ['src'],
preset: "ts-jest", // or other ESM presets
moduleDirectories: ["node_modules", "<rootDir>"],
roots: ["src"],
moduleNameMapper: {
'^@std$': '<rootDir>/src/std/$1',
"^@std$": "<rootDir>/src/std/$1",
"^@core$": "<rootDir>/src/core/$1",
},
transformIgnorePatterns: ['node_modules/(?!(svelte)/)'],
transformIgnorePatterns: ["node_modules/(?!(svelte)/)"],
transform: {
'^.+\\.(t|j)sx?$': [
'ts-jest',
"^.+\\.(t|j)sx?$": [
"ts-jest",
{
useESM: true,
},
],
},
}
export default jestConfig
};
export default jestConfig;
53 changes: 2 additions & 51 deletions src/FormModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,7 @@
import { App } from "obsidian";
import { FormDefinition } from "src/core/formDefinition";
import { makeFormEngine } from "src/store/formStore";
import InputField from "src/views/components/Form/InputField.svelte";
import ObsidianInputWrapper from "src/views/components/Form/ObsidianInputWrapper.svelte";
import DocumentBlock from "./views/components/Form/DocumentBlock.svelte";
import InputDataview from "./views/components/Form/InputDataview.svelte";
import InputFolder from "./views/components/Form/InputFolder.svelte";
import InputNote from "./views/components/Form/InputNote.svelte";
import InputTag from "./views/components/Form/InputTag.svelte";
import InputTextArea from "./views/components/Form/InputTextArea.svelte";
import MultiSelectField from "./views/components/Form/MultiSelectField.svelte";
import ObsidianSelect from "./views/components/Form/ObsidianSelect.svelte";
import ObsidianToggle from "./views/components/Form/ObsidianToggle.svelte";
import InputSlider from "./views/components/Form/inputSlider.svelte";
import RenderField from "./views/components/Form/RenderField.svelte";
export let app: App;
export let reportFormErrors: (errors: string[]) => void;
export let formEngine: ReturnType<typeof makeFormEngine>;
Expand All @@ -23,43 +12,5 @@
</script>

{#each fields as definition}
{@const { value, errors } = formEngine.addField(definition)}
{#if definition.input.type === "select"}
<ObsidianSelect input={definition.input} field={definition} {value} {errors} />
{:else if definition.input.type === "toggle"}
<ObsidianToggle field={definition} {value} />
{:else if definition.input.type === "folder"}
<InputFolder field={definition} {value} {app} />
{:else if definition.input.type === "dataview"}
<InputDataview field={definition} input={definition.input} {value} {errors} {app} />
{:else if definition.input.type === "note"}
<InputNote field={definition} input={definition.input} {value} {errors} {app} />
{:else if definition.input.type === "textarea"}
<InputTextArea field={definition} {value} {errors} />
{:else if definition.input.type === "document_block"}
<!-- I need to put this separated to be able to target the correct slot, it does not work inside #if -->
<ObsidianInputWrapper
label={definition.label || definition.name}
description={definition.description}
>
<DocumentBlock field={definition.input} form={formEngine} slot="info" />
</ObsidianInputWrapper>
{:else}
<ObsidianInputWrapper
{errors}
label={definition.label || definition.name}
description={definition.description}
required={definition.isRequired}
>
{#if definition.input.type === "multiselect"}
<MultiSelectField input={definition.input} {value} {errors} {app} />
{:else if definition.input.type === "slider"}
<InputSlider input={definition.input} {value} />
{:else if definition.input.type === "tag"}
<InputTag input={definition.input} {value} {errors} {app} />
{:else}
<InputField {value} inputType={definition.input.type} />
{/if}
</ObsidianInputWrapper>
{/if}
<RenderField {definition} model={formEngine.addField(definition)} {formEngine} {app} />
{/each}
6 changes: 3 additions & 3 deletions src/core/findInputDefinitionSchema.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { A, NonEmptyArray, ParsingFn, parse, pipe } from "@std";
import * as E from "fp-ts/Either";
import { ValiError, BaseSchema } from "valibot";
import { FieldMinimal, FieldMinimalSchema } from "./formDefinitionSchema";
import { BaseSchema, ValiError } from "valibot";
import { AllFieldTypes } from "./formDefinition";
import { InputTypeToParserMap } from "./InputDefinitionSchema";
import { FieldMinimal, FieldMinimalSchema } from "./formDefinitionSchema";
import { InputTypeToParserMap } from "./input/InputDefinitionSchema";

export function stringifyIssues(error: ValiError): NonEmptyArray<string> {
return error.issues.map(
Expand Down
4 changes: 2 additions & 2 deletions src/core/formDefinition.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { MultiselectSchema } from "./InputDefinitionSchema";
import { parse } from "valibot";
import {
isDataViewSource,
isInputNoteFromFolder,
isInputSelectFixed,
isInputSlider,
isSelectFromNotes,
} from "./formDefinition";
import { parse } from "valibot";
import { MultiselectSchema } from "./input/InputDefinitionSchema";

describe("isDataViewSource", () => {
it("should return true for valid inputDataviewSource objects", () => {
Expand Down
30 changes: 17 additions & 13 deletions src/core/formDefinition.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { type Output, is, safeParse } from "valibot";
import { input } from "@core";
import { A, O, pipe } from "@std";
import { Simplify } from "type-fest";
import { is, safeParse, type Output } from "valibot";
import {
FieldDefinitionSchema,
FormDefinitionLatestSchema,
FieldListSchema,
FormDefinitionBasicSchema,
FormDefinitionLatestSchema,
MigrationError,
} from "./formDefinitionSchema";
import { A, O, pipe } from "@std";
import { Simplify } from "type-fest";
import {
InputBasicSchema,
InputDataviewSourceSchema,
Expand All @@ -25,7 +26,7 @@ import {
inputType,
multiselect,
selectFromNotes,
} from "./InputDefinitionSchema";
} from "./input/InputDefinitionSchema";

export const InputTypeReadable: Record<AllFieldTypes, string> = {
text: "Text",
Expand Down Expand Up @@ -104,17 +105,20 @@ export type EditableInput = {
allowUnknownValues?: boolean;
};

export type EditableField = {
name: string;
label?: string;
description: string;
input: EditableInput;
folder?: string;
options?: { value: string; label: string }[];
condition?: input.Condition;
};

export type EditableFormDefinition = FormDefinition & {
title: string;
name: string;
fields: {
name: string;
label?: string;
description: string;
input: EditableInput;
folder?: string;
options?: { value: string; label: string }[];
}[];
fields: EditableField[];
};

export function isValidBasicInput(input: unknown): input is basicInput {
Expand Down
26 changes: 14 additions & 12 deletions src/core/formDefinitionSchema.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,24 @@
import { input } from "@core";
import { parse, pipe } from "@std";
import * as E from "fp-ts/Either";
import { pipe, parse } from "@std";
import {
object,
literal,
type Output,
is,
ValiError,
array,
string,
optional,
boolean,
is,
literal,
merge,
unknown,
ValiError,
object,
optional,
passthrough,
boolean,
string,
unknown,
type Output,
} from "valibot";
import { FormDefinition } from "./formDefinition";
import { findFieldErrors, stringifyIssues } from "./findInputDefinitionSchema";
import { FormDefinition } from "./formDefinition";
import { InputTypeSchema, nonEmptyString } from "./input/InputDefinitionSchema";
import { ParsedTemplateSchema } from "./template/templateSchema";
import { InputTypeSchema, nonEmptyString } from "./InputDefinitionSchema";

/**
* Here are the core logic around the main domain of the plugin,
Expand All @@ -30,6 +31,7 @@ export const FieldDefinitionSchema = object({
label: optional(string()),
description: string(),
isRequired: optional(boolean()),
condition: optional(input.ConditionSchema),
input: InputTypeSchema,
});
/**
Expand Down
2 changes: 1 addition & 1 deletion src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * as input from "./InputDefinitionSchema";
export * as input from "./input";
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
toTrimmed,
union,
} from "valibot";
import { AllFieldTypes, AllSources } from "./formDefinition";
import { AllFieldTypes, AllSources } from "../formDefinition";

/**
* Here are the definition for the input types.
Expand Down Expand Up @@ -142,6 +142,8 @@ export const InputTypeSchema = union([
DocumentBlock,
]);

export type Input = Output<typeof InputTypeSchema>;

export const InputTypeToParserMap: Record<AllFieldTypes, ParsingFn<BaseSchema>> = {
number: parseC(InputBasicSchema),
text: parseC(InputBasicSchema),
Expand Down
Loading

0 comments on commit f87f9a5

Please sign in to comment.