Magic mode ignoring updates to custom object - ractivejs

Please see this pen for a demo of the issue (based on the slideshow from the tutorial). When clicking on "next" and "prev" arrows, you'll notice that the imgIndex mustache updates correctly, but the expression mustaches such as <p>{{ curImageCaption() }}</p> do not recognize when their values are changing.
That is, the object is mutated such that the mustache value would change if the expressions were re-evaluated, but ractive doesn't seem to realize that. Is there any way to get this to work, barring writing adaptors? Am I misunderstanding how magic mode works? The interesting thing is that even if I explicitly call ractive.update() inside the event handlers, ractive still doesn't respond.
UPDATE WITH NEW INFO
After more fiddling, I came up with this hack that gets it working. The hack is to change, eg, <p>{{ curImageCaption() }}</p> to <p>{{ curImageCaption(imgIndex) }}</p> -- adding a simple primitive to the mustache expression which ractive understands how to watch correctly.
I think I see what's going on now, but having to explicitly add arguments to the mustache expression containing changing primitives defeats much of the purpose of having the separate domain object -- that is, now you are coding your domain object with ractive in mind, using changing primitives a sort of basic pub/sub mechanism for notifying ractive of changes.
Having to create a real pub/sub mechanism on my custom objects, which ractive then explicitly subscribes to, would be fine. The problem is, as I noted in the OP, even when ractive is notified of a change via ractive.update(), it still doesn't know it should recompute the mustaches unless I use the fake argument hack. So it's not clear what callback ractive should be registering to make everything work.
I don't understand the inner-working of ractive well enough to do this, but I suspect what's needed is the ability to directly work with the _deps stuff, and manually trigger recomputes for expressions. If this sounds right, an example of how to accomplish it would be appreciated.
UPDATE 2 -- A decent solution
Here is a proof of concept for a not-too-hacky workaround.
The idea is to use ECMA5 properties to decorate your custom domain object, providing properties that delegate to the existing methods you want to use but which don't work inside ractive templates. The properties, otoh, work just fine.
So instead of <p>{{ curImageCaption() }}</p> we simply write <p>{{ imageCaption }}</p>, and then we decorate our custom domain object like so:
Object.defineProperty(mySlideshow, "imageCaption", {
configurable: true,
get: function() { return this.curImageCaption() },
set: function() { }
});
This decoration, a bit verbose in my demo, can easily be slimmed down by creating a helper method which accepts an object mapping your new ractive-friendly property names to names of existing methods on your object, and takes care of the above boilerplate for you.
NOTE: One drawback of this method is that you do have to call ractive.update() manually in your event handlers. I'd like to know if there's a way of getting around that. And if there is not, how big of a performance hit does this cause? Does it defeat the whole purpose of ractive's surgical updates?
Update 3 -- A better decent solution?
This pen takes yet another approach, in which link our custom domain model with ractive via a generic dispatcher object (an object that implements notify()). I think this is my favorite of the approaches so far....
It's similar to the official ractive adaptors, but we are using DI to pass our unofficial ractive adapter to our domain object, rather than wrapping our object. At first glance it might seem we are "coding to ractive," but in fact this is only partially true. Even if we were using another framework, we'd need to use some notification mechanism to broadcast changes to our view model so that views could react to it. This DI approach seems to require less boilerplate than official ractive adaptors, though I don't understand them well enough to know this for sure. It is not as completely general a solution as the official adaptors either.
Code from pen for posterity
HTML
<div id='output'></div>
<script id='template' type='text/ractive'>
<div class='slideshow'>
<div class='main'>
<a class='prev' on-tap='prev'><span>«</span></a>
<div class='main-image' style='background-image: url({{ curImageSrc() }});'></div>
<a class='next' on-tap='next'><span>»</span></a>
</div>
<div class='caption'>
<p>{{ curImageCaption() }}</p>
<p>Image index: {{ imgIndex }} </p>
</div>
</div>
</script>
JS
// Fix JS modular arithmetic to always return positive numbers
function mod(m, n) { return ((m%n)+n)%n; }
function SlideshowViewModel(imageData) {
var self = this;
self.imgIndex = 0;
self.next = function() { self.setLegalIndex(self.imgIndex+1); }
self.prev = function() { self.setLegalIndex(self.imgIndex-1); }
self.curImage = function() { return imageData[self.imgIndex]; }
self.curImageSrc = function() { return self.curImage().src; }
self.curImageCaption = function() { return self.curImage().caption; }
self.setLegalIndex = function(newIndex) { self.imgIndex = mod(newIndex, imageData.length); }
}
var mySlideshow = new SlideshowViewModel(
[
{ src: imgPath('problem.gif'), caption: 'Trying to work out a problem after the 5th hour' },
{ src: imgPath('css.gif'), caption: 'Trying to fix someone else\'s CSS' },
{ src: imgPath('ie.gif'), caption: 'Testing interface on Internet Explorer' },
{ src: imgPath('w3c.gif'), caption: 'Trying to code to W3C standards' },
{ src: imgPath('build.gif'), caption: 'Visiting the guy that wrote the build scripts' },
{ src: imgPath('test.gif'), caption: 'I don\'t need to test that. What can possibly go wrong?' }
]
);
var ractive = new Ractive({
el: '#output',
template: '#template',
data: mySlideshow,
magic: true
});
ractive.on( 'next', function(event) {
ractive.data.next();
});
ractive.on( 'prev', function(event) {
ractive.data.prev();
});
function imgPath(name) { return 'http://learn.ractivejs.org/files/gifs/' + name; }

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.

Maybe this is an academic exercise, and I'm new to Ractive, but it seems the problem lies in the template not having a context to the current image.
EDITED: Use current Image as a context block instead of looping through collection.
<div class='slideshow'>
{{#curImage}}
<div class='main'>
<a class='prev' on-tap='prev'><span>«</span></a>
<div class='main-image' style='background-image: url({{ src }});'></div>
<a class='next' on-tap='next'><span>»</span></a>
</div>
<div class='caption'>
<p>{{ caption }}</p>
<p>Image index: {{ imgIndex }} </p>
</div>
</div>
...
function SlideshowViewModel(imageData) {
...
self.curImage = imageData[self.imgIndex]
...
self.setLegalIndex = function(newIndex) {
self.imgIndex = mod(newIndex,imageData.length);
self.curImage = imageData[self.imgIndex]
}
}
This is using your original pen with just the key modifications. Here is new pen.
I would still move the buttons into an outer part of the template so the display in the middle could be made into a partial:
<div class='main'>
<a class='prev' on-tap='prev'><span>«</span></a>
{{#current}}
{{>partial}}
{{/}}
{{/current}}
<a class='next' on-tap='next'><span>»</span></a>
</div>
and encapsulate in Ractive.extend, but if ViewModel works for you...

Related

Can Ractive events work in markup rendered with the triple-stash syntax?

If I have the following in my Ractive template:
<span on-click='handleClick'>click me</span>
Then I can listen for the click with this:
app.on({
handleClick:function() {
alert("clicked!") ;
}
})
But lets say I have that same markup stored in a string variable called clicklyspan:
app.set("clicklyspan", "<span on-click='handleClick'>click me</span>")
and I render it in the template using the triple-stash syntax:
{{{clicklyspan}}}
The handleClick listener no longer gets fired. Is there anything I can do to force some kind of update to the rendered template so that the listener works? Say, after I do that app.set() call?
Here's a fiddle demonstrating the problem.
Thanks,
Dave
I have never used Ractive, but I did some research and it seems you have to use partials, like this:
var app = new Ractive({
el: 'container',
template: '#template',
data: {
myFunction: function() {
var template = '<a on-click="handleClick">I can now be clicked as well!</a>';
if (!this.partials.myFunction) {
this.partials.myFunction = template;
}
else {
this.resetPartial('myFunction', template);
}
return 'myFunction';
}
}
});
You will also need to use this instead of the triple mustache:
{{> myFunction() }}
Here's the corresponding jsfiddle.
Of course, replace myFunction with whatever name you like.
Related question I found useful:
RactiveJS events on tripple mustache

vue.js reference div id on v-on:click

Using v-on:click I'd like to set a variable with the id of the div in Vue.JS - how do I reference this?
<div id="foo" v-on:click="select">...</div>
<script>
new Vue({
el: '#app',
data: {
},
methods: {
select: function(){
divID = this.id // ??
alert(divID)
}
}
})
</script>
You can extend your event handler with the event object $event. That should fit your needs:
<div id="foo" v-on:click="select($event)">...</div>
The event is passed on in javascript:
export default {
methods: {
select: function(event) {
targetId = event.currentTarget.id;
console.log(targetId); // returns 'foo'
}
}
}
As mentioned in the comments, `$event` is not strictly necessary, when using it as the only parameter. It's a nice reminder that this property is passed on, when writing it explicitly.
However, nobody will stop you from writing the short notation:
<div id="foo" #click="select">...</div>
Beware that the method will not receive the `$event` object when you add another parameter. You need to explicitly add it at the position you will handle it in the listener. Any parameter order will work:
<div id="foo" #click="select(bar, $event)">...</div>
To find more options of the v-on directive, you can look through the corresponding entry in the vue documentation:
Vue API Documentation - v-on
Inspired by #nirazul's answer, to retrieve data attributes:
HTML:
<ul>
<li
v-for="style in styles"
:key="style.id"
#click="doFilter"
data-filter-section="data_1"
:data-filter-size="style.size"
>
{{style.name}}
</li>
</ul>
JS:
export default {
methods: {
doFilter(e) {
let curTarget = e.currentTarget;
let curTargetData = curTarget.dataset;
if (curTargetData) {
console.log("Section: " + curTargetData.filterSection);
console.log("Size: " + curTargetData.filterSize);
}
}
}
}
Just to highlight another option than the selected answer for the same question, I have a delete button on a record and want to perform an action with the record's unique id (a number). I could do the selected answer as before:
<button :id="record.id" #click="del">×</button>
This leaves the unfortunate reality that my del function needs to pull the id attribute out of the javascript event, which is more about the API (the DOM) than my domain (my app). Also using a number as an element id isn't ideal and could cause a conflict if I do it more than once in a view. So here's something that's just as clear and avoids any future confusion:
<button #click="()=>del(record.id)">×</button>
methods: {
del(id) {
fetch(`/api/item/${id}`, {method:"DELETE"})
}
}
You see, now my del function takes the record id instead of an event, simplifying things.
Note that if you do this wrong, you will invoke your delete function immediately, which is not what you want. Don't do this:~~
<button #click="del(record.id)">×</button>
If you end up doing that, Vue will call the del function every time this html fragment is rendered. Using the anonymous function ()=>del(record.id) will return a function that's ready to be executed when the click event happens.
Actually #nirazul proved this is fine. Not sure what my issue was.

Meteor.js: template.<html>.events vs Template.<template>.events 'this' binding seems inconsistent

I'm looking through the Meteor simple tutorial and the way that 'this' binding in the different Template objects works seems inconsistent to me in my unknowledgeable state.
Template.body.events({
"submit .new-task": function(event) {
console.log(this); // Logs an empty object
}
})
Template.task.events({
"click .toggle-checked": function() {
console.log(this); // logs a task
}
});
I can see that task is an xml template defined in the view, which is a visual representation of the items returned by a function in the Template.body.helpers object.
I guess that the task objects are bound the html representation of each object (though I can't see how as there doesn't seem to be any identifying property within the li elements??)
Anyhow. When I click the task, this is the task. But when I submit the form, I was expecting this to be the body. Why is it not?
I was expecting Meteor to handle Template.body and Template.task in a similar way
In Meteor this referes to the data context. You define it with helpers or with the route controller ( IronRouter or FlowRouter)
Example:
{{#with myData}}
<h1>{{title}}</h1>
{{/with}}
js
Template.yourTemplate.helpers({
myData : function(){
return {
title : "My title"
}
}
})
You need to use the "event" argument
Template.task.events({
"click .toggle-checked": function( event , instance ) {
console.log( event );
}
});
The instance argument is also very useful. You have access to a jQuery selector like: instance.$() and it will only search for elements on your template and also child templates.
Personally I use the instance a lot. My Favorite pattern is:
Template.task.onCreated(function(){
this.vars = new ReactiveDict();
this.data = "some data";
});
Later if you want to access vars or data:
Events - You get this on the arguments
Helpers - var instance = Template.instance();
With instance you avoid storing states in the global namespace, like Session, and your code is a lot easier to maintain and understand. I hope this helps you to understand how template works in Blaze.

Translating number into currency without jQuery or manipulating the DOM

It is said that,
A good rule of thumb is that if you're using jQuery to manipulate any
DOM elements, you're probably doing it wrong.
And:
Needing access to an element from a helper function indicates that you
are trying to use a procedural coding style rather than a
template-driven style.
I have a simple number input that I wish to translate into a more comprehensible currency outside it so that the user understands what he is doing. I wanted to do this:
<input type="number" class="raw-price">
<p>Price in USD: {{priceInUsd}}</p>
And then define a helper:
Template.myTemplate.helpers({
priceInUsd: function() {
var rawPrice = $('.raw-price').val()
//perform calculation
return calculationResult
}
})
First of all, this isn't working (I don't really know why). Second, this goes against the "rules" I posted above. How am I supposed to do it? I probably could do this the same way using an event listener instead, but that would still be the wrong approach, I assume.
Flash update!
Actually! Here's a better solution: reactive variables! If you wish to keep your rawPrice in this one template, just install the standard reactive-var package:
meteor add reactive-var
And go at it this way:
Template:
<input type="number" class="raw-price">
<p>Price in USD: {{priceInUsd}}</p>
onCreated:
Template.myTemplate.onCreated(function() {
this.rawPrice = new ReactiveVar;
this.rawPrice.set(''); // not sure what you want to preset your value to
});
Helper:
Template.myTemplate.helpers({
priceInUsd: function() {
var rawPrice = Template.instance().rawPrice.get()
//perform calculation
return calculationResult
}
})
Event:
Template.myTemplate.events({
"change .raw_price": function (evt, template) {
template.rawPrice.set($(evt.currentTarget).val());
}
});
Previous (accepted) answer
According to most examples of two-way data binding in meteor I have seen, best case would probably be to use a helper, an event and a Session variable, like so.
Template:
<input type="number" class="raw-price">
<p>Price in USD: {{priceInUsd}}</p>
Helper:
Template.myTemplate.helpers({
priceInUsd: function() {
var rawPrice = Session.get('rawPrice');
//perform calculation
return calculationResult
}
})
Event:
Template.myTemplate.events({
"change .raw_price": function (evt) {
Session.set("rawPrice", $(evt.currentTarget).val());
}
});
Sadly, you're using a session variable, but it is still better than using a collection for such a local thing, like I saw in other examples...

Access an original TemplateInstace from the helper in Meteor

Could anybody point me how to access an original TemplateInstance from the meteor helper. I'm aware of the Template.instance() but it appears to return the template instance where the helper was called, not the template instance for which the helper was defined.
Imagine we have two tiny templates:
<template name='demo'>
<h1>{{helper}}</h1>
{{# border}}
<h2>{{helper}}</h2>
{{/border}}
</template>
<template name='border'>
<div style="border:1px solid red">
{{> UI.contentBlock}}
</div>
</template>
With the following behavior:
Template.demo.created = function() {
this.result = "OK";
}
Template.demo.helpers({
helper: function() {
var tmpl = Template.instance();
return tmpl.result || "FAILED";
}
});
I've expected to obtain two "OK" for the demo template: the second one should be in the red border. But since Template.instance() returns original TemplateInstance only when helper is called at the top level of its owner template the result is "FAILED" (of course in the red border).
Question: Is there any public api to get the original TemplateInstance (without need to traverse view/parentView/_templateInstace)?
I think the best way to do this might be to either just set a Session variable, or use a Reactive Variable (using the reactive-var package - here is the documentation).
I've made a meteor pad to show how this more - here.
Basically:
Template.demo.created = function() {
result = new ReactiveVar('OK');
}
Template.demo.helpers({
helper: function() {
return result.get() || "FAILED";
}
});
I think your main problem is that you not setting a template instance variable correctly. Try the below code...
Set an instance variable:
Template.instance().result.set("OK");
Get an instance variable:
Template.instance().get("result");
So your updated code would be:
Template.demo.created = function() {
Template.instance().result.set("OK");
}
Template.demo.helpers({
helper: function() {
return Template.instance().get("result") || "FAILED";
}
});
It seems that it's known and already fixed (?) Meteor bug. More here: https://github.com/meteor/meteor/issues/3745
Comment from rclai on GitHub:
This was already addressed and fixed for the next release.
Run meteor like this, not sure if it still works:
meteor --release TEMPLATE-CURRENT-DATA#0.0.1
Another alternative is to use aldeed:template-extensions, which has super nice features, especially with dealing with template instances and I believe their way of fetching the template instance is a workaround this issue.

Resources