Created
December 12, 2013 20:20
-
-
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 file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!-- | |
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