Svelte 中使用中间转换进行双向绑定的惯用方式

Hut*_*tzz 11 svelte

双向绑定在 Svelte 中非常出色且优雅,但我经常遇到的情况是需要双向绑定以及转换类型或进行某种清理的中间转换。例如:

  • value绑定到具有以下形式的选择组件的 prop{value, label},但其父组件仅处理一个值
  • 类型转换,其中 a<input type=text>也是某种其他类型(数字、日期、自定义类型)的输入,或者将对象编辑为 JSON 的输入,该对象也可以从外部更改。

我的问题是:在 Svelte 中解决此模式的最佳、惯用且简单的方法是什么?

到目前为止,我发现的最好的可重用解决方案是创建一个用于一对一转换的商店工厂a,该工厂返回两个商店,然后b您可以使用它们并将其绑定到其他组件:

示例:在这里使用 REPL

// App.svelte
<script>
    import oneToOne from './oneToOne.js'
    
    const f = x => JSON.stringify({x});
    const fInv = x => {try{return JSON.parse(x).x} catch(err){return NaN}};
    
    let [a, b] = oneToOne(13, f, fInv); 
</script>

A: <input bind:value={$a}/>
B: <input bind:value={$b}/>
Run Code Online (Sandbox Code Playgroud)
// oneToOne.js
import { writable } from 'svelte/store';

const identity = x => x;

export default function oneToOne(val, f = identity, fInv = identity) {
    let fInvB = val;
    let fA = f(val);
    
    const A = writable(fInvB);
    const B = writable(fA);
    
    B.subscribe((b) => {    
        if(fA !== b && !(Number.isNaN(fA) && Number.isNaN(b))) {            
            fInvB = fInv(b);
            fA = b;         
            A.set(fInvB)
        }
    }); 
    
    A.subscribe((a) => {        
        if(fInvB !== a && !(Number.isNaN(fInvB) && Number.isNaN(a))) {          
             fA = f(a); 
             fInvB = a;         
             B.set(fA)
        }
    }); 

    return [A, B];
}
Run Code Online (Sandbox Code Playgroud)

这有道理吗?我是否缺少一种更简单的方法来执行此操作或完全避免这种复杂性?

Hut*_*tzz 7

感谢@HB,我意识到我错过了对这个 github 问题的评论,我认为这是迄今为止我所见过的最简单/小/惯用的编码方式。

\n

我错过的想法是:为了避免输入/输出转换的循环依赖问题,只需声明in/ out(或f(x), fInv(x)) 并在其中进行赋值,然后响应式调用它们。最初的例子最终会是这样的:

\n
<script>\n    let a = 13, b;\n    const f = x => b = JSON.stringify({x});\n    const fInv = x => {try{a = JSON.parse(x).x} catch(err){a = NaN}};\n    \n    $: f(a);\n    $: fInv(b); \n</script>\n\nA: <input bind:value={a}/>\nB: <input bind:value={b}/>\n
Run Code Online (Sandbox Code Playgroud)\n

完整的 REPL 在这里

\n

就是这样!不需要商店或助手,同样或更少的冗长。我最初认为可能存在重入问题/无限循环 if f(fInv(x)) !== x,但如果我理解正确的话它是有效的,因为 Svelte 永远不会在同一个tick中多次执行反应语句。

\n

编辑:没有无限循环,但有一个丑陋的一面:根据反应语句的顺序,两个方向之一的赋值将触发变量的可重入更新,从而触发更新(已知问题) 。在某些情况下,当转换不对称时,这可能会很烦人。请参阅下面的更多内容

\n

警告/有趣的极端情况

\n

在我原来的示例和建议的解决方案中,当 JSON 输入被编辑为无效 JSON 时,fInv返回NaNfor a,但它不会传播回bJSON 输入(如果是这样,则为{x: null}),因此您可以继续键入,直到有效已到达 json。这很好,因为它允许“更平滑”的 json 文本编辑,但它会在两个输入之间创建不一致的状态。

\n

无论这是否可取,这个答案并不总是以相同的方式表现:原始答案oneToOne.js明确阻止存储更新“重入”,因此如果在“二进制奖励轨道”中键入非数字,则NaN不会传播回来。但在这个answer\xc2\xb4s实现中,根据反应性语句的顺序,相同的操作可以触发重入,传播NaN到原始二进制输入。

\n


H.B*_*.B. 4

同时定义两个存储似乎有点不必要,具体取决于预期的语义。也可以将其视为存在一个源和一个派生存储。

<script>
    import { transformed } from './transform-store.js';
    import { writable } from 'svelte/store';

    const number = writable(13);
    const json = transformed(number, {
        in: value => JSON.stringify({ x: value }),
        out: value => { try { return JSON.parse(value).x; } catch(err) { return NaN; } },
    });
</script>

Number to JSON:
<input bind:value={$number} type="number" />
<input bind:value={$json} />
Run Code Online (Sandbox Code Playgroud)

transform-store.js

import { derived } from 'svelte/store';

export function transformed(store, options) {
    const identity = x => x;
    const transformIn = options.in ?? identity;
    const transformOut = options.out ?? identity;
    
    const { subscribe } = derived(store, $store => transformIn($store));
    const set = value => store.set(transformOut(value));
    
    return { subscribe, set };
}
Run Code Online (Sandbox Code Playgroud)

REPL

set转换后的存储本质上是一个派生存储,通过定义其自己的修改源存储的函数来增强可写性。


这也可以通过使用反应式语句和属性描述符在没有存储的情况下完成:

<script>
    function transformed(get, set) {
        const o = {};
        Object.defineProperty(o, 'value', { get, set });
        
        return o;
    }

    let number = 13;
    $: json = transformed(
        () => JSON.stringify({ x: number }),
        value => { try { number = JSON.parse(value).x; } catch(err) { number = NaN; } },    
    )
</script>

Number to JSON:
<input bind:value={number} type="number" />
<input bind:value={json.value} />
Run Code Online (Sandbox Code Playgroud)

REPL

不过,理想情况下只有内置语言支持。