如何在 Svelte 中动态定义 "value" 绑定?

How can I dynamically define a "value" bind in Svelte?

我是 Svelte 的新手,我正在尝试使用它来编写一个单页应用程序,该应用程序将显示一个表单,其中包含一些基于其他字段动态计算的字段值。理想情况下,我想使用静态 JSON 配置文件来驱动表单的呈现(以便使用其他输出 JSON 的工具轻松生成新表单)。

我希望能够动态定义表单字段之间的关系,以便当用户输入值时,计算字段会自动重新计算。

我想以类似的方式结束(但显然这行不通):

<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>

实时(非工作)REPL example here.

谁能指出我正确的方向?或者,如果这完全不可能,您能否建议一种不同的方法?

我的一个想法是将字符串键放入映射中,然后字符串名称引用调用函数来计算结果,但这感觉很笨拙

首先,您不能将字符串 f1 + f2ct1.fValue=='' 传递给 { expression }bind:class:use:, on: 并希望它能正常工作。

因为 Svelte 不是那样工作的。

苗条 is a compiler.

写的时候

<!-- expression -->
{ name + name }

<!-- or bind: -->
<input bind:value="{name}" />

<!-- or dynamic attribute -->
<input disabled="name === ''" />

<!-- or many more -->

REPL

如果查看编译输出的 JS,您将看不到字符串 name + namenamename === ''。那里使用的任何变量都会被分析和转换。

您可以阅读我的博客 "Compile Svelte in your Head" 以了解更多信息。


现在,关于如何使这项工作的任何建议,我首先建议修改为 JSON 配置文件(如果可能):

例如,如果您有:

{
  "formElements": [
    {
      "id": "f1",
      "label": "First value?"
    },
    {
      "id": "f2",
      "label": "Second value?"
    },
    {
      "id": "f2",
      "label": "Calculated automatically",
      "computed": {
        "type": "sum",
        "variables": ["f1", "f2"]
      }
    }
  ]
}

然后您可以通过以下方式实现派生字段:

<input type=number id={item.id} value={compute(item.computed)} readonly/>

你可以看看这个REPL

如果无法修改 formConfig,则您必须自己解析和计算表达式。

一个 over-simplified 解析 + 计算表达式的例子:REPL我不建议这样做。

我想我想出了一个方法来达到我想要的结果。关键见解是使用 Svelte 的 derived stores 功能来捕获字段之间的依赖关系。

您可以使用 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(),Svelte 将确保在输入更改时自动重新评估它。

从那里开始,定义 subscribe to the store value 使用 $ 的 Svelte 组件相对简单。

这是我的 stores.ts 文件最终的样子:

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);
};

sample.json 看起来像这样:

{
  "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"
      }
    }
  ]
}

请注意,像这样直接从 JSON 导入需要 TypeScript 中的 enabling the resolveJsonModule 功能。

你可以看到 a complete working example here, 但 Svelte REPL 似乎还不支持 TypeScript。我不得不删除所有类型注释并将 sample.json 重命名为 sample.json.js(这在某种程度上破坏了练习的重点)。