I'll try and explain what's going on under the hood before presenting a possible solution:
Wrapping objects in magic mode
In magic mode, when Ractive encounters an unwrapped data descriptor of an object, it wraps it by replacing it with an accessor descriptor - the get()/set()
functions. (More info on MDN, for those interested.) So when you do self.imgIndex = 1
, you're actually triggering the set()
function, which knows how to notify all the dependants of the imgIndex
property.
The key word here is 'encounters'. The only way Ractive knows that it needs to wrap imgIndex
is if we do ractive.get('imgIndex')
. This happens internally because we have an {{imgIndex}}
mustache.
So that's why the index property updates.
Dependency tracking with computed values
Within an ordinary template, you can have what basically amount to computed values, using the get()
method:
<p>{{ curImageCaption() }}</p>
ractive = new Ractive({
el: 'body',
template: template,
data: {
images: images,
imgIndex: 0,
curImageCaption: function () {
var imgIndex = this.get( 'imgIndex' );
return this.get( 'images' )[ imgIndex ].caption;
}
}
});
Here, because we're calling ractive.get()
inside the curImageCaption
function, Ractive knows that it needs to rerun the function each time either images
or imgIndex
changes.
What you're in effect asking is a reasonable question: why doesn't retrieving the value of self.imgIndex
in magic mode work the same as doing ractive.get('imgIndex')
?
The answer comes in two parts: Firstly, I hadn't thought of adding that feature, and secondly, it turns out it doesn't work! Or rather, it's extremely fragile. I changed magic mode so that the get()
accessor captured the dependency the same way ractive.get()
does - but self.imgIndex
is only an accessor descriptor (as opposed to a data descriptor) if Ractive has already encountered it. So it worked when we had <p>Image index: {{ imgIndex }} </p>
at the top of the template, but not when it's at the bottom!
Normally the prescription would be fairly simple: use ractive.get()
to make the dependency on self.imgIndex
explicit inside curImageSrc()
and curImageCaption()
. But because you're using a custom viewmodel object, that's not ideal because it effectively means hard-coding keypaths.
A solution - creating a custom adaptor
Here's what I'd recommend - making an adaptor that works with the custom viewmodel object:
Ractive.adaptors.slides = {
filter: function ( object ) {
return object instanceof SlideshowViewModel;
},
wrap: function ( ractive, slides, keypath, prefix ) {
var originalNext, originalPrev;
// intercept next() and prev()
originalNext = slides.next;
slides.next = function () {
originalNext.call( slides );
ractive.update( keypath );
};
originalPrev = slides.prev;
slides.prev = function () {
originalPrev.call( slides );
ractive.update( keypath );
};
return {
get: function () {
return {
current: slides.curImage(),
index: slides.imgIndex
};
},
teardown: function () {
slides.next = originalNext;
slides.prev = originalPrev;
}
};
}
};
var ractive = new Ractive({
el: '#output',
template: '#template',
data: mySlideshow,
adaptors: [ 'slides' ]
});
This is a very simple adaptor, and it could probably be improved, but you get the gist - we're intercepting calls to next()
and prev()
, and letting Ractive know (via ractive.update()
) that it needs to do some dirty checking. Note that we're presenting a facade (via the get()
method of the wrapper), so the template looks slightly different - see this pen.
Hope this helps.