Ractive two-way databinding with expressions

658 views Asked by At

I'd like to have an input represent a list of primitive (string, number) values so you can enter them as comma-separated values but update the model as an array:

"Transforms": [
        {
            "Fn": "TheFunctionName",
            "Args": [ "arg1", 2, "arg3" ]
        },
        {
            "Fn": "AnotherMethod",
            "Args": [ 4.678 ]
        },
    ]

Would be administered with:

{{#each Transforms:i}}
   <li>
       <input value="{{Fn}}" placeholder="Function Name" />
       <input value="{{implode(Args)}}" placeholder="Function Arguments" />
   </li>
{{/each}}

and would render something like:

* [ TheFunctionName ] [ "arg1", 2, "arg3" ] * [ AnotherMethod ] [ 4.678 ]

Mainly so that I don't need to figure out a good UI to dynamically add/remove argument inputs (like bind on certain keypress, use buttons to add/remove fields, etc).

I could use a "placeholder" property for databinding, and probably observe that to update the actual property, but then I'd need to filter it out when 'serializing' the underlying model. It seems like I could use computed properties, which have a getter and a setter, but it's not clear from the docs how it works with nested properties in a list (i.e. there are many entries in data with the Transforms list).

1

There are 1 answers

5
Rich Harris On BEST ANSWER

The trick here is to not use two-way binding, but to use more traditional event handling techniques. This example listens for change events on the second input (not input events, which happen with each keystroke, since that would cause the cursor to jump about while the user is still typing) in each <li>, and tries to evaluate its contents. Meanwhile, a formatter takes the arguments and turns them back into a string:

var ractive = new Ractive({
    el: 'main',
    template: '#template',
    data: {
        transforms: [
            {
                fn: 'TheFunctionName',
                args: [ 'arg1', 2, 'arg3' ]
            },
            {
                fn: 'AnotherMethod',
                args: [ 4.678 ]
            }
        ],
        format: function ( args ) {
            return args.map( JSON.stringify ).join( ', ' );
        }    
    },
    updateArgs: function ( index, str ) {
        var keypath = 'transforms[' + index + '].args',
            args = this.get( keypath );
        
        try {
            // or use JSON.parse, if you don't want to eval
            this.set( keypath, eval( '([' + str + '])' ) );
        } catch ( err ) {
            // reset
            this.set( keypath, null );
            this.set( keypath, args );
        }
    }
});
<script src="http://cdn.ractivejs.org/edge/ractive.js"></script>

<main></main>

<script id='template' type='text/ractive'>
    <h2>input</h2>
    <ul>
        {{#each transforms:i}}
            <li>
                <input value='{{fn}}' placeholder='function name'/>
                <input
                    twoway='false'
                    value='{{format(args)}}'
                    placeholder='function arguments'
                    on-change='updateArgs(i,event.node.value)'
                />
            </li>
        {{/each}}
    </ul>
    
    <h2>output</h2>
    <ul>
        {{#each transforms}}
            <li>
                <p>function name: <strong>{{fn}}</strong></p>
                <p>args:</p>
                <ul>
                    {{#each args}}
                        <p>{{JSON.stringify(this)}} ({{typeof this}})</p>
                    {{/each}}
                </ul>
            </li>
        {{/each}}
    </ul>
</script>