Question

I have an object of message streams that looks like this:

ractive.data.messages:

{
    stream_id1: {
        some_stream_metadata: "foo",
        stream: [  
            {id: "someid1", message: "message1"}, 
            {id: "someid2", message: "message2"}
        ]
    },
    stream_id2: {
        some_stream_metadata: "bar",
        stream: [
            {id: "someid3", message: "message3"},
            {id: "someid4", message: "message4"}
        ]
    }
}

main_template:

{{#messages[ current_stream_id ]}}
    {{>render_message_stream}}
{{/messages[ current_stream_id ]}}

render_message_stream:

{{#stream}}
    <div class="stream">
    ...someotherstuff...
        {{>render_message}}
    </div>
{{/stream}}

render_message:

<div class="message">
...someotherstuff...
    {{message}}
</div>

I change "current_stream_id" to change the rendered stream of messages.

On updates, i change the contents of the message streams like this:

ractive.merge(
    "messages.stream_id1.stream",
    new_message_stream,
    {
        compare: function ( item ) { return item.id; }
    });

I also tried the compare: true option instead of the function, with the same results:

Ractive always thinks that these two messages belong effectively to the same DOM element, even though they live in a completely different message stream:

ractive.data.messages[ "stream_id1" ].stream[1].message
ractive.data.messages[ "stream_id2" ].stream[1].message

Problems:

  • When there are intro/outro animations ractive animates always just the end of the messages stream, even when a message in the middle of the stream was deleted, i need help to make ractive understand which messages are identical.

  • When i change the current_stream_id, ractive does not rerender the complete {{>render_message_stream}} partial, but goes inside the existing dom and changes the {{message}} field in all existing messages, though this might be good for dom element reuse, this triggers a lot of animations that are wrong. (Eg. it triggers intro/outro animations for the last message in the stream if stream1 has one message more than stream2).

Was it helpful?

Solution

One of these issues has a straightforward answer; unfortunately the other one doesn't.

I'll start with the easy one - the fact that

ractive.data.messages[ "stream_id1" ].stream[1].message
ractive.data.messages[ "stream_id2" ].stream[1].message

belong to the same DOM element. You're correct in that Ractive updates the existing elements rather than removing them and creating new ones - this is a core part of its design. In this case that's undesirable behaviour, but you can work around it like so:

// instead of immediately switching to a new stream ID like this...
ractive.set( 'current_stream_id', 'stream_id2' );

// you can set it to a non-existent ID. That will cause the existing DOM
// to be removed. When you set it to an ID that *does* exist, new DOM
// will be created:
ractive.set( 'current_stream_id', null );
ractive.set( 'current_stream_id', 'stream_id2' );

// or, if you'd like the initial transitions to complete first...
ractive.set( 'current_stream_id', null ).then(function () {
  ractive.set( 'current_stream_id', 'stream_id2' );
});

The other issue - that merge() isn't merging, but is instead behaving as though you were doing ractive.set('messages.stream_id1.stream', new_message_stream) - is tougher. The problem is that while you and I know that {{#messages[ current_stream_id ]}} equates to messages.stream_id1 when current_stream_id === 'stream_id1, Ractive doesn't.

What it does know is that we have an expression whose value is determined by messages and current_stream_id. When the value of either of those references changes, the expression is re-evaluated, and if that value changes, the DOM gets updated - but using a standard set(). When you do ractive.merge('messages.stream_id1.stream', ...), Ractive updates all the things that depend on keypaths that are 'upstream' or 'downstream' of messages.stream_id1.stream - which includes messages. So that's how the expression knows that it needs to re-evaluate.

It's possible that a future version of Ractive will be able to handle this case in a smarter fashion. Perhaps it could make a note of arrays that are subject to merge operations, and check evaluator results to see if they're identical to one of those arrays, and if so use merge() rather than set(). Perhaps it could analyse the function in some way to see if the {{#messages[ current_stream_id ]}} section should register itself as a dependant of messages.stream_id1 for as long as current_stream_id === 'stream_id1', rather than the internally-generated ${messages-current_stream_id-} keypath.

None of that helps you in the meantime though. The only way to use merge() in your current situation is to have a separate reference that doesn't use an expression, and a bit of magic with pattern observers:

main_template:

{{#current_messages}} <!-- rather than `messages[ current_stream_id ]` -->
    {{>render_message_stream}}
{{/current_messages}}

render_message_stream:

{{#current_message_stream}} <!-- rather than `stream` -->
    <div class="stream">
        {{>render_message}}
    </div>
{{/current_message_stream}}

code:

ractive.observe( 'current_stream_id', function ( id ) {
    var current_messages = this.get( 'messages.' + id );

    this.set( 'current_messages', current_messages );

    // hide existing stream, then show new stream
    this.set( 'current_message_stream', null ).then(function () {
        this.set( 'current_message_stream', current_messages.stream );
    });
});

// when ANY message stream changes, we see if it's the current one - if so, we
// perform a merge on the top-level `current_message_stream` array
ractive.observe( 'messages.*.stream', function ( new_stream, old_stream, keypath, id ) {
    // the value of any * characters are passed in as extra arguments, hence `id`

    if ( id === this.get( 'current_stream_id' ) ) {
        this.merge( 'current_message_stream', new_stream, {
            compare: function ( item ) {
                return item.id;
            }
        });
    }
});

I've set up a JSFiddle demonstrating this. I hope it makes sense, let me know if not - and sorry I didn't get round to answering this question much sooner.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top