Search code examples
ractivejs

Computed property, filters and updates to DOM


I have a computed property with a lot of elements. Based on user input, I need to filter out some of the elements on which it's based.

[
    {name:'hal',    display: false},
    {name:'john',   display: true },
    {name:'jack',   display: false},
    {name:'george', display: true },
    {name:'tom',    display: false},
    {name:'rick',   display: false},
    {name:'paul',   display: true },
    {name:'ringo',  display: true },
]

In my example, I want to be able to filter out elements that have display:false at will.

What happens is that, due to the efficency of how Ractive reacts to data changes, only specific changes get performed on the DOM, triggering unwanted transitions.

A fiddle is worth a thousand words: http://jsfiddle.net/7kubqbba/4/ (edited, see old version)

If you click the toggle button, you'll notice that a transition gets performed on the computed elements on the left.

I believe this happens because Ractive, rather than updating the whole <li>....</li> tag, updates just the specific parts that should be affected. In this specific case: name and class.

<li class="animateme animateme-{{display}}">{{name}}</li>

What I'd like to do is:

  • keep using computed properties
  • rebuild the whole <li> on data change (basically, giving up some of Ractive's efficency)

Any tip on how I can do that?

(it's entirely possibile that I'm approaching this issue in the wrong way, if so, please let me know)


edit:

tl;dr. visual transition should trigger when any of the 'display' property gets modified (i.e. ), not when 'display:false' elements are being filtered out.

Updated fiddle: http://jsfiddle.net/7kubqbba/4/


Solution

  • See http://jsfiddle.net/7kubqbba/5/ for examples below.

    I actually think the inline filtered example is the better approach, you can move the logic into a function if it becomes complicated:

    {{#if show(this) }} ... {{/if}}
    
    ...
    
    data: {
        show ( item ) {
            const active = this.get( 'active' );
            return active !== true || item.display;
        }
    }
    

    Otherwise, you need to use an ractive.observe and a ractive.merge to achieve the same effect as a computed property, but with identity aware DOM node semantics:

    this.observe('active', active => {
        const entries = this.get('entries');
        const merge = active ? entries.filter( entry => entry.display ) : entries;
        this.merge( 'filtered', merge );
    });
    

    Note that this highlights the fact that ractive does not track changes between identical object references in different arrays (unless using magic mode), a fact "hidden" by the recomputation of filteredElements that was occurring on each data change in your original example.

    If you need this behavior you can set up observers to map between the arrays:

        this.observe('filtered.*.display', ( display, o, k, index ) => {
            this.update('entries.'+index+'.display' );
        });
        this.observe('entries.*.display', ( display, o, k, index ) => {
            this.update('filtered.'+index+'.display' );
        });
    

    Or in next version of ractive (0.8) you could possibly use the link method.

    EDIT: One more option I thought of is to disable transitions when the filter changes. See http://jsfiddle.net/7kubqbba/6/:

        <li on-click="changeDisplay:{{i}},{{display}}" class="animateme animateme-{{display}} {{#if noTransition}}noTransition{{/if}}">{{name}}</li>
    
        // in component:
        self.observe('filteredElements', (n, o) => {
            if ( n!==o ) {
                this.set( 'noTransition', true );
            }
        })
    
        self.observe('filteredElements', (n, o) => {
            setTimeout( () => this.set( 'noTransition', false ) );
        }, { defer: true });