Reacting to changes made outside of Angular2 - data-binding

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.

Related

How to check if Accounts UI widget loaded on client?

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.

Force rerendering a component

What's the best way to completely teardown and reinstantiate a component in the old one's place, preferably from a template?
Our use case is we have a bunch of Backbone models/collections that are used in our views. In init we might listen to some of those model or collection events (that are sometimes deep), or we may do some sort of setup work relative to that model. It seems we have two options: listen for if the entire model property changes on the view and then unbind any events and bind them to the new model and redo any setup work, or force the view to teardown and put a new one in its place with the new model, since the template may change significantly or even completely. We chose the latter route due to the significance of the change and to ensure we start with a clean slate in the view.
Up to this point we've been wrapping the component in a conditional and changing a boolean to force the old component to teardown and a new one to rerender:
HTML
<p>Some stuff that isn't bound to the model: {{prop1}}, {{prop2}}</p>
{{#if isRenderable}}
<myComponent model="{{model}}" />
{{/if}}
JS
component.set('isRenderable', false); // force `myComponent` to teardown
component.set('model', aDifferentModel); // this often happens in/via template
component.set('isRenderable', true); // force a new `myComponent` to render
Is this a decent approach or are we looking at this all wrong? It seems like there has to be a better option, especially since is necessary in a few places in our app.
One way to do this would be to use the reset() method of the component to change the data, and include a dynamic template function to choose the appropriate template. One of the advantages is that it will not need to re-render the template unless data.type changes. (btw - the design behind the default for components to not re-render is that if the data is updating is more efficient to update the DOM values than to re-render everything. The falsey-block trick works to force a refresh - but that may not always be needed).
There are a lot of details that are specific to your implementation, but this example will give you some ideas:
var Page = Ractive.extend({
template: function(data, t){
return data.type ? t.fromId(data.type) : 'loading...'
}
})
var r = new Ractive({
el: '#container',
template: '#template',
data: { model: datas.person1 },
components: {
page: Page
},
oninit: function(){
var page = this.findComponent('page')
this.observe('model', function(n){
page.reset(n)
})
},
load: function(load){
this.set('model', datas[load])
}
})
This works if there is shared-logic, or no logic, in the component that is rendering the various models.
Often though, you want to use a different component for each model type because there are observers and event handlers specific to that view for that particular model. In that case, this example up-levels the dynamism to the parent and uses an option function for the component:
var r = new Ractive({
el: '#container',
template: '#template',
data: datas.person1,
components: {
page: function(data){
return components[data.type]
}
},
load: function(load){
this.reset( datas[load] )
}
})

how to properly handle dom ready for Meteor

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.

how to properly bind jquery ui behaviors in meteor?

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

extjs 4 designer 1.2 grid renderer

i m working with extjs designer 1.2. I have a button on panel that opens window on click. The window has grid for which i have applied renderer as following in js file . The problem is renderer works well when the window opens up for first time, but when i close window & reopen it, the effect goes off.
Ext.define('MyApp.view.TestWindow', {
extend: 'MyApp.view.ui.TestWindow',
initComponent: function() {
var me = this;
me.callParent(arguments);
}
});
==========================================================================
Ext.define('MyApp.view.TestPanel', {
extend: 'MyApp.view.ui.TestPanel',
initComponent: function() {
var me = this;
me.callParent(arguments);
Ext.data.StoreManager.lookup('Test').load();
me.down('button[id=testbutton]').on('click',me.onTestBtnClick,me);
},
onTestBtnClick: function(){
var win = new Ext.create('MyApp.view.TestWindow');
win.show();
win.down('#testgrid').columns[0].renderer=function(val){
return '<span style="color:red;">' + val + '</span>';
}
}
});
Observation : When i use renderer in ui.js i.e. the file generated by exporting project from designer, i dont face above stated problem. What can be solution for this problem?
I've resolved similar issues caused by the closeAction config option of my Ext.Window (MyApp.view.TestWindow in your case) being set to hide, instead of destroy (Ext JS 4 default). Your illustrated button click event handler instantiates a new Ext.Window (MyApp.view.TestWindow in your case) every time it is fired. If these instances are not created and destroyed properly you may experience DOM ID contention and undesirable results.
If your goal is to persist such instances a better approach, regardless of the state of your current config options, would be for you to relocate your instantiation logic to a global scope and only manage the showing and hideing of this component in your button click event handler.
Because you have not provided the underlying MyApp.view.TestWindow logic, I am only left to assume that the root cause of your issue does pertain to a combination of either misconfigured config options and/or component instance management, ultimately resulting in components contending for the same DOM ID.
Another thing to be mindful of is the use of statically defined id config options. If you are statically defining an id config option on any component you must ensure that those components are either singletons, or their instances assigned in a global scope for reuse. Again, this all boils down to proper component management.
Lastly, it is also a possibility that the use of my suggestion does not reveal any glaring issues specific to your MyApp.view.TestWindow. If this is the case, inspect and ensure that none of the underlying MyApp.view.TestWindow child components (grid, column model, column, etc.) are culprit.
EDIT
Below is an example:
Ext.define('MyApp.view.TestPanel', {
extend: 'MyApp.view.ui.TestPanel',
initComponent: function() {
var me = this;
me.callParent(arguments);
Ext.data.StoreManager.lookup('Test').load();
me.down('button[id=testbutton]').on('click',me.onTestBtnClick,me);
me.testWindow = new Ext.create('MyApp.view.TestWindow');
me.testWindow.down('#testgrid').columns[0].renderer=function(val){
return '<span style="color:red;">' + val + '</span>';
}
},
onTestBtnClick: function(){
var me = this;
me.testWindow.show();
}
});

Resources