I am trying to create a group of draggable DOM objects using jQuery UI's .draggable() that are populated through Meteor subscriptions. The code I came up with looks like
Meteor.subscribe('those_absent', function() {
$( "li.ui-draggable" ).draggable( { revert: "invalid" } );
});
Meteor.subscribe('those_present', function() {
$( "li.ui-draggable" ).draggable( { revert: "invalid" } );
});
These correspond with some Meteor.publish() calls, so that any time the collection changes, the .draggable() behaviour will be attached. At least, that was my intention.
However, it only works once - once one of these <li>'s has been dragged and dropped, then they are no longer draggable at all.
When the objects are dropped, I'm firing a custom event that is attached to the Template for the item like so
$( "#c_absent .inner-drop" ).droppable({
drop: function( event, ui ) {
ui.draggable.trigger('inout.leave');
}
});
Template.loftie_detail.events = {
'inout.leave': function (e) {
Lofties.update({_id:this._id}, {$set: {present: 'N' }});
}
};
So, my thinking is that this change to the collection on drop should propagate through the pub/sub process and re-run the .draggable() line above. But it doesn't seem to.
The complete code for this can be seen here https://github.com/sbeam/in-out/blob/master/client/inout.js and the app is live at http://inout.meteor.com/ (there are some other probably unrelated issues with items randomly losing values or disappearing from the UI altogether)
So if my understanding of how pub/sub works in Meteor is off, it would be good to know. Or is there a more efficient way to achieve this UI behavior binding that works without it?
The way I have implemented this in my apps is with the method shown by #lashleigh.
I have a template event that listens using code like this :
Template.myDraggableItem.events({
'mouseover .workItem' : function() {
$(this._id).draggable();
}
});
Then I listen for the dragstop like this.
$('body').on('dragstop', '.myDraggableItem', function (e) {
// Update the collection with the new position
};
You can see the app that's using this code at aduno.meteor.com
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 working on a Meteor project that has come custom Pagination using Sessions. The template rendering the contents of said items is using ellipsis.js and highlight.js to do some DOM formatting. The code looks something like thus:
if (Meteor.isClient) {
Meteor.startup(function () {
Session.setDefault("homePageSize", 10);
Session.setDefault("homePageStart", 0);
});
}
Template.home.articlesPaginated = function() {
return Articles.find({published: true}, {sort: {post_date: -1}, skip: Session.get("homePageStart"), limit: Session.get("homePageSize")});
}
Template.home.rendered = function() {
// Setup ellipsis
$('.ellipsis').dotdotdot({
ellipsis: '...',
wrap: 'word',
fallbackToLetter: true,
after: $('a.blog_continue')
});
// Setup highlight.js
$('pre code').each(function(i, block) {
hljs.highlightBlock(block);
});
}
Template.home.events({
'click .next': function(event) {
var offset = Session.get("homePageStart") + Session.get("homePageSize");
if (offset < 0) {
offset = 0;
}
Session.set("homePageStart", offset);
},
'click .prev': function(event) {
var offset = Session.get("homePageStart") - Session.get("homePageSize");
if (offset < 0) {
offset = 0;
}
Session.set("homePageStart", offset);
}
});
Pagination is working just fine, but as soon as the Template re-renders I loose all the ellipsis.js and highlight.js formatting. I know the obvious reason is that the DOM has changed, and since the Template.render only runs once up-front and doesn't happen when the Template re-renders the DOM updates are not being applied. So, what is the best way to trigger ellipsis.js and highlight.js after the Template is done such that it gets re-called everytime the Template re-renders?
Basically you need to listen for changes in your Articles collection, which is a client-side subset of the server database clipped to contain only the currently visible paginated articles.
When you detect a change in the articles subset, you'll need to retrigger initialization of ellipsis.js and highlight.js.
You could reorganize your code as follow :
First, we define the cursor declaration as a separate function on his own because we need to use it twice :
function articlesPaginated(){
return Articles.find({
published: true
}, {
sort: {
post_date: -1
},
skip: Session.get("homePageStart"),
limit: Session.get("homePageSize")
});
}
Template.home.helpers({
articlesPaginated:articlesPaginated
});
Then in the rendered callback, we need to setup a reactive computation that will depend on this cursor, so whenever the articles subset is updated to a new page, our computation will rerun.
But we need to be aware that the helper we defined on the home template returns the same cursor so it's going to be invalidated and trigger DOM refresh AT THE SAME TIME... JavaScript is single-threaded and the Tracker.Computation manual states that the order of execution of concurrently invalidated computations is unpredictable.
So we cannot just trigger the ellipsis/highlight initialization code in the computation because this setup code assumes that the DOM is ready, however at this precise moment we don't know if DOM manipulation has just happened before or is going to happen immediately after.
Fortunately there is a Tracker.afterFlush method which allows us to execute code after concurrent computations are done so we are sure that by that time DOM state is OK.
Having understand all these implications, we can write the following rendered callback :
Template.home.rendered=function(){
// declare a template managed Deps.Computation
this.autorun(function(){
// have this reactive computation depend on the SAME cursor
// that triggers DOM rerendering
var articles=articlesPaginated();
// forEach is actually the method that triggers a dependency on the cursor in this computation
articles.forEach(function(article){
// you can manipulate the model here if needed
});
// setup a callback to execute your DOM alteration code after
// it is actually rerendered by Blaze
Tracker.afterFlush(function(){
// your ellipsis/highlight initialization code goes here
});
});
};
If you can put your Articles into another template, then you could apply formatting individually as they are inserted.
Template.article.rendered = function () {
// Setup ellipsis
this.$('.ellipsis').dotdotdot({
ellipsis: '...',
wrap: 'word',
fallbackToLetter: true,
after: $('a.blog_continue')
});
// Setup highlight.js
this.$('pre code').each(function(i, block) {
hljs.highlightBlock(block);
});
};
Assuming your template looks something like this.
<template name="home">
...
{{#each articlesPaginated}}
{{> article}}
{{/each}}
{{> paginationControls}}
...
</template>
This has the added benefit of scoping the formatting to just the articles, rather than the entire DOM.
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.