Reputation: 3272
I'm new to Svelte, and I'm trying to use it to write a single-page app that will display a form with some field values dynamically calculated based on other fields. Ideally, I'd like to drive the rendering of the form using static JSON configuration files (to make it easy to generate new forms with other tools that output JSON).
I want to be able to dynamically define the relationships between form fields, so that when the user enters values, the computed fields recalculate automatically.
I'd like to end up with something similar to this (but obviously this doesn't work):
<script>
let objFormConfig = JSON.parse(`{
"formElements": [
{
"id": "f1",
"label": "First value?"
},
{
"id": "f2",
"label": "Second value?"
},
{
"id": "f2",
"label": "Calculated automatically",
"computed": "f1 + f2"
}
]
}`);
</script>
<form>
{#each objFormConfig.formElements as item}
<div>
<label for="{item.id}">{item.label}
{#if item.computed}
<input type=number id={item.id} value={item.computed} readonly/>
{:else}
<input type=number id={item.id} bind:value={item.id}/>
{/if}
</label>
</div>
{/each}
</form>
Live (non-working) REPL example here.
Can anyone point me in the right direction? Or if it's completely impossible, could you suggest a different approach?
One idea I had is to have string keys into a map, and then string names referencing functions that get called to calculate the result, but that feels clunky
Upvotes: 0
Views: 5876
Reputation: 3272
I think I've come up with a way to achieve the result I wanted. The key insight is using Svelte's derived stores feature to capture the dependency relationships between the fields.
You can use the Function constructor to create a function from a JSON string (there may be security issues with this approach—though less serious than eval()
; in my case the form config is trusted input). Then, you pass that function as a callback to derived() and Svelte will make sure it gets automatically reevaluated when the inputs change.
From there, it's relatively straightforward to define Svelte components that subscribe to the store value using $
.
Here's what my stores.ts
file winds up looking like:
import { Writable, writable, derived } from 'svelte/store';
import { fields } from "./sample.json";
export const fieldMap: Map<string, Writable<number>> = new Map();
const computedFields = fields.filter((f) => f.computed);
const userFields = fields.filter((f) => !f.computed);
userFields.forEach((field) => {fieldMap[field.id] = writable(0)})
type JsonFunction = (values: number[]) => number;
for (const cf of computedFields) {
const fromStatic = new Function(...cf.computed.args, "return " + cf.computed.body);
const derivedFunction: JsonFunction = (values: number[]) => fromStatic(...values);
const arg0: Writable<number> = fieldMap[cf.computed.args[0]];
const moreArgs: Array<Writable<number>> = [];
for (const arg of cf.computed.args.slice(1)) {
moreArgs.push(fieldMap[arg]);
}
fieldMap[cf.id] = derived([arg0, ...moreArgs], derivedFunction);
};
With a sample.json
that looks like this:
{
"fields": [
{
"label": "First value?",
"id": "f1"
},
{
"label": "Second value?",
"id": "f2"
},
{
"label": "Summed",
"id": "f3",
"computed": {
"args": [
"f1",
"f2"
],
"body": "f1 + f2"
}
},
{
"label": "Doubled",
"id": "f4",
"computed": {
"args": [
"f3"
],
"body": "f3 * 2"
}
}
]
}
Note that importing directly from JSON like this requires enabling the resolveJsonModule feature in TypeScript.
You can see a complete working example here, but the Svelte REPL doesn't appear to support TypeScript yet. I've had to remove all the type annotations and rename sample.json
to sample.json.js
(which does somewhat defeat the point of the exercise).
Upvotes: 1
Reputation: 2334
First and foremost, you can't have a string f1 + f2
or ct1.fValue==''
, pass it to { expression }
, bind:
, class:
, use:
, on:
and expect it to work.
Because Svelte doesn't work that way.
Svelte is a compiler.
When you write
<!-- expression -->
{ name + name }
<!-- or bind: -->
<input bind:value="{name}" />
<!-- or dynamic attribute -->
<input disabled="name === ''" />
<!-- or many more -->
if you look at the compiled output JS, you will not see the string name + name
, name
or name === ''
. Whatever variable used in there is analysed and transformed.
You can read my blog "Compile Svelte in your Head" to understand more on this.
Now, as to any suggestion on how to make this work, I would first suggest modifying to JSON configuration files (if possible):
for example, if you have:
{
"formElements": [
{
"id": "f1",
"label": "First value?"
},
{
"id": "f2",
"label": "Second value?"
},
{
"id": "f2",
"label": "Calculated automatically",
"computed": {
"type": "sum",
"variables": ["f1", "f2"]
}
}
]
}
Then you can implement derived fields via:
<input type=number id={item.id} value={compute(item.computed)} readonly/>
You can check out this REPL
If it is impossible to modify the formConfig, then you would have to parse and evaluate the expression yourself.
A over-simplified example of parse + evaluate the expression: REPL. I wouldn't recommend doing it this way.
Upvotes: 1