I am currently using iron-router and this is my very first attempt to try out the Meteor platform. I has been running into issues where most of the jquery libraries failed to initialized properly because the of the way Meteor renders html, $(document).ready() fires before any templates are rendered. I am wondering is there any callbacks from Meteor/iron-router that allows me to replace the jQuery's dom ready?
Also, how should I (easily and properly) handle the live update of the dom elements if some of them are customized by jQuery/javascript?
This is what i am currently doing, i feel like it is very hackish and probably would run into issues if the elements got updated after the initialization.
var jsInitalized = false;
Router.map(function () {
this.route('', {
path: '/',
layoutTemplate: 'default',
after: function(){
if(!jsInitalized){
setTimeout(function(){
$(document).ready( function() { $$$(); });
}, 0);
jsInitalized = true;
}
}
});
}
With Meteor you generally want to think about when a template is ready, not when the dom is ready.
For example, let's say you want to use the jQuery DataTables plugin to add sorting to a table element that's created by a template. You would listen to the template's rendered event and bind the plugin to the dom:
HTML:
<template name="data_table">
<table class="table table-striped" id="tblData">
</table>
</template>
JavaScript:
Template.data_table.rendered = function () {
$('#tblData').dataTable();
};
Now anytime the template is re-rendered (for example, if the data changes), your handler will be called and you can bind the jQuery plugin to the dom again.
This is the general approach. For a complete example (that includes populating the table with rows) see this answer.
Try making a separate .js file, call it rendered.js if you'd like. and then;
Template.layout.rendered = function ()
{
$(document).ready(function(){console.log('ready')});
}
I use template layout, but you can do Template.default.rendered. I hope that helps.
Also take a look at this part of documentation, especially the Template.events; http://docs.meteor.com/#templates_api
I use Meteor v0.8.0 with Iron Router (under Windows 7) and here is how I handle 'DOM ready':
When I want to modify the DOM after a specific template has been rendered:
I use Template.myTemplateName.rendered on the client side :
Template.blog.rendered = function()
{
$('#addPost').click(function()
{
...
});
}
When I want to modify the DOM after any new path has been rendered:
I use Router.onAfterAction, but there seems to be a trick:
Router.onAfterAction(function()
{
setTimeout(function()
{
$('.clickable').click(function()
{
...
});
}, 0);
});
Notice the setTimeout(..., 0), it doesn't work for me otherwise (DOM empty).
Notice that you can use onAfterAction on specific path, but most of the time I think it is redundant with the Template.myTemplateName.rendered method above.
What seems to be missing:
A way to modify the DOM after any template has been rendered.
Related
I've got a non-angular page made with fairly basic JS, and thought it'd be a splendid idea to try and add learn some Angular2 and use it for some new functionality.
My plan was that I'd bind an Angular2 component to an object that is being updated by the old code, and I'd use Angular2 magic to update a chunk of UI.
The problem is I cant convince Angular2 to react to any changes made in the outside JS. What the trick to doing that? Attempts at googling the problem lead to in depth explanations of Angular2's change detection process, which hasn't been helpful so far. Is this just an awful idea?
I found a random Angular2 jsfiddle and hacked it up to show the problem. Strings are added to 'window.names', but you dont see them until one is added from the angular side: https://jsfiddle.net/byfo3jg3/ . The code follows:
var names = ['Joe'];
setTimeout(function() {
names.push("Frank");
}, 1000);
setTimeout(function() {
names.push("Sterve");
}, 2000);
setTimeout(function() {
names.push("Garfield");
}, 3000);
(function() {
var HelloApp,
ListThing;
ListThing = ng
.Component({
selector: 'list-thing',
template: '<ul><li *ng-for="#name of names">{{name}}</li></ul>',
directives: [ng.NgFor]
})
.Class({
constructor: function() {
this.names = window.names;
setTimeout(function() {
this.names.push("Oh hai");
}.bind(this), 10000);
}
});
HelloApp = ng
.Component({
selector: 'hello-app',
template: '<list-thing></list-thing>',
directives: [ListThing]
})
.Class({
constructor: function() {}
});
document.addEventListener('DOMContentLoaded', function() {
ng.bootstrap(HelloApp);
});
}());
You will need to set the NgZone to window object and then call run function of the zone.
Please refer to Angular 2 How to get Angular to detect changes made outside Angular? SO Question
names should be component property to work inside of template:
constructor(){this.names = window.names}
Changes to window.names will not be detected by angular, so you have few options: poll names using setInterval(()=>{this.names = window.names}, 1000) or expose global callback:
constructor(zone:NgZone)
{
window.notify = ()=> {
zone.run(()=> {
this.names = window.names;
});
}
}
and call it from plain js window.notify() or use other methods to invoke change detection.
Is this just an awful idea?
Yes.
Angular's automatic change detection system assumes that changes to data (that you want your components to display) are happening inside an event handler that is monkey-patched by Zone.js. Because then Angular's change detection will execute when such an event handler fires (well, technically, it will execute after the event handler finishes).
If you want a component view to automatically update, you have to change the bound data inside Angular – inside the Angular zone. As #Jigar answered, you can modify your code to call angularZone.run(_ => // make changes here), but if you have to do that, you might as well move the code that manages and manipulates the data into a service (or a component, if the logic is minimal).
See also Günter's alternative approach: set up an event listener inside Angular (hence inside the Angular zone). Then fire that event whenever you make changes outside the Angular zone.
The default Accounts-UI widget takes a while to load. I want to check on the client when it is ready, so that I can perform some DOM manipulations on it afterwards. I am currently using a timer like so:
Template.sign_in_modal.onRendered(function (){
Tracker.afterFlush(function () {
Meteor.setTimeout(function () {
$('a#login-sign-in-link').click();
$('a#login-name-link').click();
$('a.login-close-text').remove();
}, 100);
});
});
The above hack works locally (probably because it loads faster) but not when I push to saturnapi.com. I just want it to be expanded by default as shown below. Is there a way to ensure the UI widget is fully loaded via a template helper or otherwise make it expanded by default?
I would suggest checking when the <a id="login-sigin-in-link"></a> is added to the DOM. This could be verified by checking $('a#login-sign-in-link').length. If the element is on the DOM do your manipulation.
However if it is not just check again in a few milliseconds. I would suggest using setInterval().
See below for the complete solution:
Template.sign_in_modal.onRendered(function (){
var setIntervalId = Meteor.setInterval(function() {
if($('a#login-sign-in-link').length) {
$('a#login-sign-in-link').click();
$('a.login-close-text').remove();
Meteor.clearInterval(setIntervalId);
}
}, 100);
});
Template.sign_in_modal.onDestroyed(function() {
$('.modal-backdrop.fade.in').remove();
});
Some may think that using loginButtons.onRendered(function(){}); is a good way to verify if the element has been added to the DOM, but it is not. If you try to do the same DOM manipulation in onRendered, it will throw an afterFlush error. The onRendered function has been extremely misleading.
I'm aware of Template.onRendered, however I have the need to destroy and setup some plugins that act on the dom when the actual context is updated.
So saying I have content template, I'd need something similar to the following:
Template.content.onBeforeChange(function () {
$(".editor").editable("destroy");
});
Template.content.onAfterChange(function () {
$(".editor").editable();
});
Is there any current way I can achieve this with the existing Template api?
You should be able to detect a context change within a template autorun by looking at currentData like this:
Template.content.onRendered(function() {
this.autorun(function() {
if (Template.currentData()) {
// the context just changed - insert code here
}
});
});
I'm unclear if that works for your particular case because this technique only gets you the equivalent of onAfterChange.
I am forced to assign rendered callbacks to all my templates.
Until 0.9.0 I used to do it like this:
_.each( Template, function( template, name ) {
//...
template.rendered = function() {
//...
};
});
But now, Template is a constructor and not an object, so this method won't work here. Is there any way to pass callback function to all templates or fire function when all templates were rendered using Blaze?
Here is a quick workaround I came up with, iterating over every Template property to find out if it corresponds to a template definition, and if it does, assign the onRendered callback.
// make sure this code is executed after all your templates have been defined
Meteor.startup(function(){
for(var property in Template){
// check if the property is actually a blaze template
if(Blaze.isTemplate(Template[property])){
var template=Template[property];
// assign the template an onRendered callback who simply prints the view name
template.onRendered(function(){
console.log(this.view.name);
});
}
}
});
I don't know what's your use case so there may be better solutions depending on it.
With Meteor 1.2.1 the Template object has an onRendered(hook) function to accomplish an 'all template' onRendered behaviour.
Template.onRendered(function(){
var template = this;
Deps.afterFlush(function() {
console.log("triggering Jquery mobile component creation for "+template.view.name);
$(template.firstNode.parentElement).trigger("create");
});
});
The postponed update via Deps.afterFlush(callback) is optional and subject to your application needs.
I have a template that looks something like this:
<template name="foo">
<textarea name="text">{{contents}}</textarea>
</template>
I render it with:
Template.foo = function() {
return Foos.find();
}
And I have some event handlers:
Template.foo.events = {
'blur textarea': blurHandler
}
What I want to do is set the rows attribute of the textarea depending on the size of its contents. I realize that I could write a Handlebars helper, but it wouldn't have access to the DOM element being rendered, which would force me to do some unnecessary duplication. What I want, ideally, is for meteor to trigger an event after an element is rendered. Something like:
Template.foo.events = {
'render textarea': sizeTextarea
}
Is this possible?
As of Meteor 0.4.0 it is possible to check if a template has finished rendering, see http://docs.meteor.com/#template_rendered
If I understand your question correctly, you should wrap your textarea resize code inside a Template.foo.onRendered function:
Template.foo.onRendered(function () {
this.attach_textarea();
})
I think the current 'best' way to do this (it's a bit of a hack) is to use Meteor.defer ala Callback after the DOM was updated in Meteor.js.
Geoff is one of the meteor devs, so his word is gospel :)
So in your case, you could do something like:
<textarea id="{{attach_textarea}}">....</textarea>
and
Template.foo.attach_textarea = function() {
if (!this.uuid) this.uuid = Meteor.uuid();
Meteor.defer(function() {
$('#' + this.uuid).whatever();
});
return this.uuid;
}
EDIT
Note, that as 0.4.0, you can do this in a much ad-hoc way (as pointed out by Sander):
Template.foo.rendered = function() {
$(this.find('textarea')).whatever();
}
Since about June 2014, the correct way to do this has been to set a callback using Template.myTemplate.onRendered() .
Yeah I think so - not sure if it's "the right way", but this works for me:
In your app JS, the client section will run whatever javascript there on the client. For example:
if (Meteor.is_client) {
$(function() {
$('textarea').attr('rows' , 12) // or whatever you need to do
})
...
Note the example here uses JQuery, in which case you need to provide this to the client (I think :-). In my case:
I created a /client dir, and added jquery.js file under this.
Hope this helps.