RactiveJS - when is it safe to call find(...) - ractivejs

When is it safe to call the find(...) method on a ractive instance and be guaranteed that the template has been rendered and the DOM elements are available?
Background:
I'm new to RactiveJS, building my first application with it. I've been instantiating Ractive instances and then calling methods like find() on those instances to access elements rendered from my templates. Something like this:
var ractive = new Ractive({ el: ..., template: ..., data: ..., etc });
var element = ractive.find('.some-template-element');
This has been working fine so far, but I'm wondering whether I might have a race condition here because of the fact that ractive seems to render templates asynchronously. Is it safe to write code like the above or do I need to instead move everything into callbacks like this?
ractive.on('complete', function() {
var element = ractive.find('.some-template-element');
});
Too Much Information:
In practice, of course, what I'm doing is more complicated than this simple pseudo-code. I'm creating 'widgets' that use Ractive as an internal implementation detail. Right now, I create those widgets and then start calling methods on them. But if the Ractive methods aren't ready to call right away, I'll need to restructure my widgets to expose callbacks/promises that get called once I know my ractives have been created and are ready for use.
I haven't been able to find details in the RactiveJS documentation that explain when it's safe to call the various functions on Ractive, but I'm hoping I've just missed something.

Assuming you've provided an el option, rendering happens synchronously with the new Ractive instantiation.
While you can subscribe via ractive.on( 'event', ... ), it is often handy and cognitively easier to use the onevent options:
var log = [];
var r = new Ractive({
el: document.body,
template: '#template',
data: {
log: log
},
components: {
'child-component': Ractive.extend({
template: '<span>child</span>',
oninit: function() {
log.push('child component init');
},
onrender: function() {
var span = this.find('span');
log.push('child component render find span:' + !!span);
},
oncomplete: function() {
log.push('child component complete');
}
})
},
oninit: function() {
log.push('parent view init');
},
onrender: function() {
var div = this.find('div'),
span = this.find('span');
log.push('parent component render, find div: ' + !!div + ' find span: ' + !!span);
},
oncomplete: function() {
log.push('parent component complete');
}
});
// these won't fire becasue they already happened!
r.on('init', function() {
log.push('view on("init"...)');
});
r.on('render', function() {
log.push('view on("render"...)');
});
// this will fire because complete is always async.
r.on('complete', function() {
log.push('view on("complete"...)');
});
<script src='//cdn.jsdelivr.net/ractive/0.7.3/ractive-legacy.min.js'></script>
<script src='//cdn.jsdelivr.net/ractive.transitions-fade/0.2.1/ractive-transitions-fade.min.js'></script>
<script id='template' type='text/ractive'>
<div intro='fade'>main view</div>
<child-component/>{{#log}}
<li>{{.}} {{/}}
</script>

Related

Why is data set with Meteor Iron Router not available in the template rendered callback?

This is a bit puzzling to me. I set data in the router (which I'm using very simply intentionally at this stage of my project), as follows :
Router.route('/groups/:_id',function() {
this.render('groupPage', {
data : function() {
return Groups.findOne({_id : this.params._id});
}
}, { sort : {time: -1} } );
});
The data you would expect, is now available in the template helpers, but if I have a look at 'this' in the rendered function its null
Template.groupPage.rendered = function() {
console.log(this);
};
I'd love to understand why (presuming its an expected result), or If its something I'm doing / not doing that causes this?
From my experience, this isn't uncommon. Below is how I handle it in my routes.
From what I understand, the template gets rendered client-side while the client is subscribing, so the null is actually what data is available.
Once the client recieves data from the subscription (server), it is added to the collection which causes the template to re-render.
Below is the pattern I use for routes. Notice the if(!this.ready()) return;
which handles the no data situation.
Router.route('landing', {
path: '/b/:b/:brandId/:template',
onAfterAction: function() {
if (this.title) document.title = this.title;
},
data: function() {
if(!this.ready()) return;
var brand = Brands.findOne(this.params.brandId);
if (!brand) return false;
this.title = brand.title;
return brand;
},
waitOn: function() {
return [
Meteor.subscribe('landingPageByBrandId', this.params.brandId),
Meteor.subscribe('myProfile'), // For verification
];
},
});
Issue
I was experiencing this myself today. I believe that there is a race condition between the Template.rendered callback and the iron router data function. I have since raised a question as an IronRouter issue on github to deal with the core issue.
In the meantime, workarounds:
Option 1: Wrap your code in a window.setTimeout()
Template.groupPage.rendered = function() {
var data_context = this.data;
window.setTimeout(function() {
console.log(data_context);
}, 100);
};
Option 2: Wrap your code in a this.autorun()
Template.groupPage.rendered = function() {
var data_context = this.data;
this.autorun(function() {
console.log(data_context);
});
};
Note: in this option, the function will run every time that the template's data context changes! The autorun will be destroyed along with the template though, unlike Tracker.autorun calls.

Can twitter flightjs do component within component

In Facebook react.js, you can compose component within component, or maybe mix and match.
I'm wondering if twitter flight can do the same thing. if so, can anyone gives me an example?
this is what I have so far:
define(function (require) {
var defineComponent = require('flight/lib/component'),
infoInput = require('component/info_input');
return defineComponent(inputSection, infoInput);
function inputSection () {
this.after('initialize', function() {
infoInput.doSomehting();
});
};
});
and my info_input.js is defined below:
define(function (require) {
var defineComponent = require('flight/lib/component');
return defineComponent(infoInput);
function infoInput() {
this.after('initialize', function() {
});
this.doSomething = function() {
alert('I will do something');
};
};
});
This is what mixins are for.
Flight Components are enriched mixins.
From doc/component_api.md
It comes with a set of basic functionality such as event handling and Component registration. Each Component definition mixes in a set of custom properties which describe its behavior.
Read more about Components.
So the answer to your question is Yes.
I guess that what you are doing is legit, although I've never done it before.
I'd rather move the shared logic to a Mixin or attach the two components to the same element and let them talk via events:
component/input_section.js
this.after('initialize', function () {
this.trigger('uiSomethingRequired');
});
component/info_input.js
this.after('initialize', function () {
this.on('uiSomethingRequired', this.doSomething);
});
Solution mentioned by G.G above works!
We may go a step ahead to trigger events on restricted scope instead of document:
component/input_section.js
this.after('initialize', function () {
this.$node.closest(this.attr.parentClass).trigger('uiSomethingRequired');
});
component/info_input.js
this.after('initialize', function () {
this.on(this.$node.closest(this.attr.parentClass), 'uiSomethingRequired', this.doSomething);
});

data binding in react + signalR

I have a strategy question.
I want to change data in my website using signalR and display changed data using react. My question would be: How to perform data binding between signalR and react?
My first clue is the following:
signalR:
chat.client.addMessage = function (name, message) {
chatHistory.push({ Author: name, Text: message }); //here I change global variable chatHistory
};
react:
var CommentList = React.createClass({some class here});
var CommentBox = React.createClass({
componentRefresh: function () {
this.setState({ data: chatHistory });
},
getInitialState: function () {
return { data: chatHistory };
},
componentDidMount: function () {
this.componentRefresh();
setInterval(this.componentRefresh, this.props.interval);
},
render: function () {
return (
React.DOM.div(null,
CommentList({ data: this.state.data })
)
);
}
});
React.renderComponent(
CommentBox({ interval: 2000 }),
document.getElementById('content')
);
in react commentBox component I feed global chatHistory and ask for a new value every 2 seconds.
Is there more elegant way of doing it?
and how to avoid redrawing of CommentBox if chatHistory variable wasn't changed?
Your approach of maintaining state in CommentBox is fine. As your component base grows, it might become complicated to maintain self-updating components though. I recommend investigating the Flux architecture the React team designed and their Todo MVC Flux example in particular.
You could implement shouldComponentUpdate to prevent React from re-rendering the CommentBox if you know state hasn't changed. Also, you should keep a reference to the interval so you can clear it when the CommentBox is unmounted otherwise it will go on polling after the component is removed.
var CommentBox = React.createClass({
...
componentDidMount: function() {
this.componentRefresh();
this._interval = setInterval(this.componentRefresh, this.props.interval);
},
componentWillUnmount: function() {
clearInterval(this._interval);
this._interval = null;
},
shouldComponentUpdate: function(nextProps, nextState) {
// Do a deep comparison of `chatHistory`. For example, use
// Underscore's `isEqual` function.
return !_.isEqual(this.state.chatHistory, nextState.chatHistory);
},
...
});

Deep link to a position in a page, using Meteor JS

I have a meteor app with multiple pages. I want to be able to deeplink to an anchor somewhere halfway the page.
Traditionally, in normal html, you'd make an somewhere in your page, and link to it via /mypage.html#chapter5.
If I do this, my meteor app won't scroll down to that spot.
What is the best approach around this?
#Akshat 's answer works for on the same page, but what if you want to be able to pass around a url w/ a "#" in it? I did it how the meteor docs did.
Template.myTemplate.rendered = function() {
var hash = document.location.hash.substr(1);
if (hash && !Template.myTemplate.scrolled) {
var scroller = function() {
return $("html, body").stop();
};
Meteor.setTimeout(function() {
var elem = $('#'+hash);
if (elem.length) {
scroller().scrollTop(elem.offset().top);
// Guard against scrolling again w/ reactive changes
Template.myTemplate.scrolled = true;
}
},
0);
}
};
Template.myTemplate.destroyed = function() {
delete Template.myTemplate.scrolled;
};
Stolen from the source to the meteor docs.
Are you using some kind of javascript router? Meteor Router?
You could use something like a javascript based scrolling method. One such example is with JQuery: (You can place this in your link/buttons click handler)
Template.hello.events({
'click #theitemtoclick':function(e,tmpl) {
e.preventDefault();
$('html, body').animate({
scrollTop: $("#item_id").offset().top
}, 600);
}
});
Then tag your html item where you would put your anchor with the id:
<h1 id="item_id">Section X</h1>
Currently, there's an issue in IronRouter where the hash is removed from the url. This is discussed here and here. Fortunately there is a fix even though it doesn't appear to be in the stable version.
My Iron Router solution with traditional anchor tags:
1) Apply the IronRouter fix above
2)
Router.configure({
...
after: function () {
Session.set('hash', this.params.hash);
},
...
});
3)
function anchorScroll () {
Deps.autorun(function (){
var hash = Session.get('hash');
if (hash) {
var offset = $('a[name="'+hash+'"]').offset();
if (offset){
$('html, body').animate({scrollTop: offset.top},400);
}
}
Session.set('hash', '');
});
}
Template.MYTEMPLATE.rendered = function (){
anchorScroll();
};
Unfortunately this has to be set in each template's .rendered() otherwise the anchor tag is not guaranteed to be in the DOM.
For better or worse this will scroll again with a code push.
Mike's Answer didn't quite work for me. The hash was returning empty in the onRendered callback. I nested the code in an additional Meteor.setTimeout
fyi I'm using Blaze.
Below worked like a charm :)
Template.myTemplate.onRendered(function() {
Meteor.setTimeout(function(){
var hash = document.location.hash.substr(1);
if (hash && !Template.myTemplate.scrolled) {
var scroller = function() {
return $("html, body").stop();
};
Meteor.setTimeout(function() {
var elem = $("a[name='" + hash + "']");
if (elem.length) {
scroller().scrollTop(elem.offset().top);
// Guard against scrolling again w/ reactive changes
Template.myTemplate.scrolled = true;
}
},
0);
}
},0);
});
Template.myTemplate.destroyed = function() {
delete Template.myTemplate.scrolled;
};

Backbone Collection.fetch() returns first item null

I'm using the following code in my view to fetch my collection from the server:
initialize: function () {
_this = this;
this.collection.fetch({
success : function(collection, response) {
_.each(response, function(i){
var todo = new TodosModel({
id: i.id,
content: i.content,
completed: i.completed
});
// Add to collection
_this.collection.add(todo);
// Render
_this.render(todo);
});
},
error : function(collection, response) {
console.log('ERROR GETTING COLLECTION!');
}
});
},
Which seems to work - here's the output from my server:
{
"0": {
"id": 1,
"content": "one",
"completed": false
},
"3": {
"id": 4,
"content": "two",
"completed": true
},
"4": {
"id": 5,
"content": "tester",
"completed": false
}
}
Except for the fact that if I log out my collection there is a null entry in the first position:
Which then causes issues as if I add an item it takes the ID of the last element. I'm new to backbone and am hoping I'm just missing something simple.
Here's my crack at a quick run through of your code. I haven't tested anything so there might be typos. I'm still not sure where the stray empty model is coming from but if you restructure your application as outlined below, I suspect the problem will go away.
The model and collection look okay so let us have a look at your view.
el: $('#todos'),
listBlock: $('#todos-list'),
newTodoField: $('#add input'),
//...
template: $('#todo-template').html(),
//...
events: { /* ... */ },
These should be okay but you need to ensure that all those elements are in the DOM when your view "class" is loaded. Usually you'd compile the template once:
template: _.template($('#todo-template').html()),
and then just use this.template as a function to get your HTML. I'll assume that template is a compiled template function below.
initialize: function () {
_this = this;
You have an accidental global variable here, this can cause interesting bugs. You want to say var _this = this;.
this.el = $(this.el);
Backbone already gives you a jQuery'd version of el in $el so you don't need to do this, just use this.$el.
this.collection.fetch({
success : function(collection, response) {
_.each(response, function(i) {
var todo = new TodosModel({ /* ... */ });
// Add to collection
_this.collection.add(todo);
// Render
_this.render(todo);
});
},
//...
The collection's fetch will add the models to the collection before the success handler is called so you don't have to create new models or add anything to the collection. Generally the render method renders the whole thing rather than rendering just one piece and you bind the view's render to the collection's "reset" event; the fetch call will trigger a "reset" event when it has fetched so the usual pattern looks like this:
initialize: function() {
// So we don't have to worry about the context. Do this before you
// use `render` or you'll have reference problems.
_.bindAll(this, 'render');
// Trigger a call to render when the collection has some stuff.
this.collection.on('reset', this.render);
// And go get the stuff we want. You can put your `error` callback in
// here if you want it, wanting it is a good idea.
this.collection.fetch();
}
Now for render:
render: function (todo) {
var templ = _.template(this.template);
this.listBlock.append(templ({
id: todo.get('id'),
content: todo.get('content'),
completed: todo.get('completed')
}));
// Mark completed
if(todo.get('completed')) {
this.listBlock.children('li[data-id="'+todo.get('id')+'"]')
.addClass('todo-completed');
}
}
Normally this would be split into two pieces:
render to render the whole collection.
Another method, say renderOne, to render a single model. This also allows you to bind renderOne to the collection's "add" event.
So something like this would be typical:
render: function() {
// Clear it out so that we can start with a clean slate. This may or
// may not be what you want depending on the structure of your HTML.
// You might want `this.listBlock.empty()` instead.
this.$el.empty();
// Punt to `renderOne` for each item. You can use the second argument
// to get the right `this` or add `renderOne` to the `_.bindAll` list
// up in `initialize`.
this.collection.each(this.renderOne, this);
},
renderOne: function(todo) {
this.listBlock.append(
this.template({
todo: todo.toJSON()
})
)
// Mark completed
if(todo.get('completed')) {
this.listBlock.find('li[data-id="' + todo.id + '"]')
.addClass('todo-completed');
}
}
Notice the use of toJSON to supply data to the template. Backbone models and collections have a toJSON method to give you a simplified version of the data so you might as well use it. The model's id is available as an attribute so you don't have to use get to get it. You could (and probably should) push the todo-completed logic into the template, just a little
<% if(completed) { %>class="completed"<% } %>
in the right place should do the trick.
addTodo: function (e) {
//...
var todo = new TodosModel({
id: todoID,
content: todoContent,
completed: todoCompleted
});
this.render(todo);
todo.save();
_this.collection.add(todo);
You could bind renderOne to the collection's "add" event to take care of rendering the new model. Then use the save callbacks to finish it off:
var _this = this;
var todo = new TodosModel({ /* ... */ });
todo.save({}, {
wait: true,
success: function(model, response) {
// Let the events deal with rendering...
_this.collection.add(model);
}
});
Again, an error callback on the save might be nice.
completeTodo: function (e) {
//...
todo.save({
completed: todoCompleted
});
}
The save call here will trigger a 'change:completed' event so you could bind to that to adjust the HTML.
removeTodo: function (e) {
//...
}
The destroy call will trigger a "destroy" event on the model and on the collection:
Any event that is triggered on a model in a collection will also
be triggered on the collection directly, for convenience. This
allows you to listen for changes to specific attributes in any model
in a collection, [...]
So you could listen for "destroy" events on the collection and use those to remove the TODO from the display. And destroying the model should remove it from the collection without your intervention.
printColl: function () {
this.collection.each(function (todo) {
console.log('ID: '+todo.get('id')+' | CONTENT: '+todo.get('content')+' | COMPLETED: '+todo.get('completed'));
});
}
You could just console.log(this.collection.toJSON()) instead,
you'd have to click around a little to open up the stuff in the
console but you wouldn't miss anything that way.
All the event binding for the collection would take place in your
view's initialize method. If you're going to remove the view then
you'd want to override the remove to unbind from the collection
to prevent memory leaks:
remove: function() {
// Call this.collection.off(...) to undo all the bindings from
// `initialize`.
//...
// Then do what the default `remove` does.
this.$el.remove()
}
You could also use a separate view for each TODO item but that might be overkill for something simple.

Resources