Skip to content

Instantly share code, notes, and snippets.

@jamesreggio
Created December 12, 2013 20:20
Show Gist options
  • Save jamesreggio/7934736 to your computer and use it in GitHub Desktop.
Save jamesreggio/7934736 to your computer and use it in GitHub Desktop.
This Gist illustrates a bug in the latest version of Ractive.js (0.3.7).
<!--
This Gist illustrates a bug in the latest version of Ractive.js (0.3.7).
Download and open this file on your local machine to view the repro.
When an array is shared between the root Ractive view model and a
component (Child), the array's `_ractive.roots` cache will originally contain
`[Ractive, Child]`. However, when the array is modified from within the child,
the cache is reconstructed in the opposite order: `[Child, Ractive]`. It is
unclear whether this difference in ordering is itself a bug, or whether it
just happens to surface a bug in other code.
When the cache is reconstructed in the opposite (Child first) order, 'smart'
array operations are performed twice. The explanation for this (if the
difference in ordering isn't itself the bug) pertains to a difference in
`update` semantics between standard `update` observers and special
array-specific operations.
I assume that the `render` method on `Dom*` classes is meant to be idempotent.
That is to say, instances can be rendered any number of times with the same
model, and the same output is expected. In this repro, `DomSection.render`
calls `updateSection`, which then calls `updateListSection`, which will ensure
that the proper number of fragments exist in the list. This function is
idempotent.
On the other hand, the 'smart' array operations implemented on `DomSection` are
not idempotent. Every time `DomSection.push` is called, it will add a fragment
to the section. In this repro, both `DomSection.render` and `DomSection.push`
are called once; the bug pertains to the order of the calls. If `push` is
called before `render`, `render` will perceive that the list is already the
same length as the array and will do nothing; however, if `render` is called
first (as is the case when the root Ractive's observers are executed first),
the new fragment will be added once by `render/updateListSection` and a second
time by `push`.
I can think of a number of potential bugs here, but my lack of understanding of
the implied constraints/invariants makes it difficult to say which of these are
bugs, versus which are by design. Here's my best attempt at a list of potential
problems:
1. The `_ractive.roots` cache on the array should be the same every time, but
changes when it is reconstructed by a component instance's `set` method.
2. 'Smart' array updates should only occur on the root that owns the array;
updates to an array passed to a child component should be performed using
'dumb' updates, i.e., `DomSection.update`.
3. The implementation of 'smart' array updates are broken because they're not
idempotent; i.e., they don't check to ensure that the `DomSection` hasn't
already been updated.
-->
<!DOCTYPE html>
<html>
<head>
<title>Ractive.js Components Bug</title>
<script src="https://rawgithub.com/RactiveJS/Ractive/master/Ractive.js">
</script>
</head>
<body>
<h2>Ractive.js Components Bug</h2>
<h3>Repro Steps</h3>
<ol>
<li>Set items from component.</li>
<li>Push item from Ractive root.</li>
</ol>
<h3>Excepted Behavior</h3>
<p>"3: test 3" is added to the list.</p>
<h3>Actual Behavior</h3>
<p>"3: test 3" is added to the list, along with a malformed fourth item.</p>
<br><br>
<h2>Repro App</h2>
<div id="app"></div>
<script id="component-template" type="text/ractive">
<ul>
{{#items:i}}
<li>{{i}}: {{.}}</li>
{{/items}}
</ul>
<p><button on-click="set">Set items from component</button></p>
</script>
<script id="app-template" type="text/ractive">
<rv-component items="{{items}}"></rv-component>
<p><button on-click="push">Push item from Ractive root</button></p>
<p>Current order: {{JSON.stringify(items)}}</p>
</script>
<script>
var Component, app;
Component = Ractive.extend({
template: '#component-template',
init: function(){
this.on({
set: function(){
// Calling `set` on component instance with a clean copy of the
// array will result in its `_ractive.roots` cache being rebuilt
// in the opposite order.
this.set({ items: this.data.items.slice() });
}
});
}
});
app = new Ractive({
el: '#app',
template: '#app-template',
data: {
items: ['test 0', 'test 1', 'test 2'],
counter: 3
},
components: {
component: Component
}
});
app.on({
push: function(){
this.data.items.push('test ' + this.data.counter++);
}
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment