Binding 'style' to a computed property - ember.js

I have a component which is inserted into the DOM as a '' tag (e.g., default behaviour). The component's job is to wrap a 3rd party jQuery tool and I'm trying to ensure it is responsive to "resize" events so I would like to explicitly set width and height style attributes.
In the component, it is easy enough to being to the style attribute:
attributeBindings: ['style'],
style: function() {
return "width: auto";
}.property('widthCalc'),
In this case, this works but doesn't do anything useful because style just returns a static string (width: auto).
Instead what I want to do is -- based on any change to the computed property widthCalc -- set the width based on the new value. So here's the next logical step:
style: function() {
var width = $('body')[0].offsetWidth;
return 'width: ' + width + 'px';
}.property('widthCalc'),
This too works, dynamically setting the DIV to the width of the body's width (note: this isn't really what I want but it does prove that this simple binding works). Now what I really want is to get the value of width from a computed property on the component but I don't even have to go that far to run into trouble; notice that instead of a global jQuery selector I switch to a localised component-scoped selector:
style: function() {
var width = this.$().offsetWidth;
return 'width: ' + width + 'px';
}.property('widthCalc'),
Unfortunately this causes the page NOT to load and gives the following error:
Uncaught Error: Something you did caused a view to re-render after it rendered but before it was inserted into the DOM.
I imagine this is Ember run-loop juju but I'm not sure how to proceed. Any help would be appreciated.

Since it is not possible to call this.$() in the component before it has been added to the dom, provide an initial value until the component is ready.
For example,
Setting a default value to the property style and on didInsertElement event reopen the class and define style as a calculated property using this.$()
http://emberjs.jsbin.com/delexoqize/1/edit?html,js,output
js
App.MyCompComponent = Em.Component.extend({
attributeBindings:["style"],
style:"visibility:hidden",
prop1:null,
initializeThisStyle:function(){
this.set("style","visibility:visible");
this.reopen({
style:function(){
// var thisOffsetWidth = this.$().get(0).offsetWidth;
return "visibility:visible;color:red;background-color:lightgrey;width:"+this.get("prop1")+"px";
}.property("prop1")
});
}.on("didInsertElement")
});
Alternatively handle the error raised by this.$() and provide a default value. Afterwards when the component will be added the property will be calculated as planned.
http://emberjs.jsbin.com/hilalapoce/1/edit?html,js,output
js
App.MyCompComponent = Em.Component.extend({
attributeBindings:["style"],
style:function(){
try{
this.$();//this will throw an erro initialy
return "visibility:visible;color:red;background-color:lightgrey;width:"+this.get("prop1")+"px";
}catch(e){
return "color:blue";
}
}.property("prop1"),
prop1:null
});

With the component I was trying to solve for I ended coming up with an solution that seems effective to me which I will share below. For an understanding of the why I was getting the error and how one might more directly address that error please see the comment from #melc above.
My Solution
What I'm solving for is resizing a jQuery component wrapped in an Ember component. In many cases, resizing is handled gracefully by CSS alone but some jQuery components -- including the very nice knob component from aterrien -- has JS which gets directly involved and therefore needs the containers width and height properties to be set explicitly by the Ember component so that it reacts appropriately.
When solving for this I realised my use-case had two problems:
Solving for a page resize event
Adjusting to the fact that my knob component was -- at times -- in the DOM but in a part of the DOM which was not visible (more explicitly it was in Bootstrap tab which wasn't visible).
The Resize Listener
The first part of the solution is to listen for a page-level resize of the page. I do this with the following:
resizeListener: function() {
var self = this;
self.$(window).on('resize', Ember.run.bind(self, self.resizeDidHappen));
}.on('didInsertElement'),
Page Resize Handler
When a resize is done at the "page" level I now want my component to inspect what the resize impact has been on the component:
resizeDidHappen: function() {
Ember.run.debounce(this, function() {
// get dimensions
var newWidth = Number(this.$().parent().get(0).offsetWidth);
var newHeight = Number(this.$().parent().get(0).offsetHeight);
// set instance variables
this.set('width', newWidth);
this.set('height', newWidth);
// reconfigure knob
this.$('.knob').trigger(
'configure',
{
width: newWidth,
height: newWidth
}
);
}, 300);
}
This solves the page resize problem if it exists in isolation but to make the component it is probably a good idea to solve for the visibility use case as well (certainly in my case it was critical).
Visibility Handler
Why? Well, for two reasons that I can think of:
Many jQuery components refuse to load or perform badly if they aren't loaded
The ember component appears to not be able to establish a "resize" event when it is not visible in the DOM
The one problem is that there is no DOM-level event for visibility changes, so how do we react to a change in visibility without polling on an interval? Well in most cases there will be a UI element which is controlling the state of visibility. In my case it's Bootstrap's tab bar and in this case they have events that fire on the tabs when they become visible. Great. Here's a selector for Bootstrap's selector (assuming you're inside the content area of the newly visible tab):
visibilityEventEmitter: function(context) {
// since there is no specific DOM event for a change in visibility we must rely on
// whatever component is creating this change to notify us via a bespoke event
// this function is setup for a Bootstrap tab pane; for other event emmitters you will have to build your own
try {
var thisTabPane = context.$().closest('.tab-pane').attr('id');
var $emitter = context.$().closest('.tab-content').siblings('[role=tabpanel]').find('li a[aria-controls=' + thisTabPane + ']');
return $emitter;
} catch(e) {
console.log('Problem getting event emitter: %o', e);
}
return false;
},
visibilityEventName: 'shown.bs.tab',
then we just need to add the following code:
_init: function() {
var isVisible = this.$().get(0).offsetWidth > 0;
if (isVisible) {
this.visibilityDidHappen();
}
}.on('didInsertElement'),
visibilityListener: function() {
// Listen for visibility event and signal a resize when it happens
// note: this listener is placed on a DOM element which is assumed
// to always be visibile so no need to wait on placing this listener
var self = this;
Ember.run.schedule('afterRender', function() {
var $selector = self.get('visibilityEventEmitter')(self);
$selector.on(self.get('visibilityEventName'), Ember.run.bind(self, self.visibilityDidHappen ));
});
}.on('didInsertElement'),
visibilityDidHappen: function() {
// On the first visibility event, the component must be initialised
if(!this.get('isInitialised')) {
this.initiateKnob();
} else {
// force a resize assessment as window sizing may have changed
// since last time component was visible
this.resizeDidHappen();
}
},
Note that this also results in a tiny refactor of our resize listener, removing it's trigger from the didInsertElement event and instead being triggered by initiateKnob which will happen not when the Ember component loads but instead lazy load at the first point of visibility in the DOM.
initiateKnob: function() {
var self = this;
this.set('isInitialised', true);
var options = this.buildOptions();
this.$('.knob').knob(options);
this.syncValue();
this.resizeDidHappen(); // get dimensions initialised on load
console.log('setting resize listener for %s', self.elementId);
self.resizeListener(); // add a listener for future resize events
},
resizeListener: function() {
this.$(window).on('resize', Ember.run.bind(this, this.resizeDidHappen));
},
Does it work?
To a large degree but not completely. Here's what works:
the first 'tab' which is visible at load resizes on demand
all tabs resize when they are switched to (aka, when they gain visibility)
what doesn't work is:
tabs other than the first tab do not resize (aka, the onresize callback appears broken)
The error I get is:
vendor.js:13693 Uncaught TypeError: undefined is not a function
Backburner.run vendor.js:13716
Backburner.join vendor.js:34296
run.join vendor.js:34349
run.bind vendor.js:4759
jQuery.event.dispatch vendor.js:4427
jQuery.event.add.elemData.handle
Not sure what to make of this ... any help would be appreciated. Full code can be found here:
https://gist.github.com/295e7e05c3f2ec92fb45.git

Related

Ember view.renderer.willDestroyElement is not a function when transition to another route from component

In a component I setup a PIXI.js canvas element in didInsertElement.
Later when I transition to another route Ember throws this error:
Uncaught TypeError: view.renderer.willDestroyElement is not a function(…)
The new route appears to have loaded as the model and setupController hooks get called and the url changes to the new route. But the previous component is still loaded and the new routes DOM elements are not. Also of note the willDestroyElement does not get called from the old component.
I'm using "ember-cli": "2.9.1",
Updated with code snippet:
{{game-puzzle gameType='puzzle'}}
In the component:
didInsertElement() {
const $gameCanvas = $('#game-canvas').get(0);
this.set('$gameCanvas', $gameCanvas);
this.setupCanvas();
this.setupHud();
this.setupPiecesArray();
this.loadImages();
},
setupCanvas() {
console.log('#setupCanvas');
const width = this.get('screenPixelWidth');
const height = this.get('screenPixelHeight');
const $gameCanvas = this.get('$gameCanvas');
// Use autoDetectRenderer to choose the best available renderer
// either: WebGL or canvas
let renderer = PIXI.autoDetectRenderer(width, height, {
view: $gameCanvas,
backgroundColor: 0x000000
});
let stage = new PIXI.Container();
let container = new PIXI.Container();
this.set('renderer', renderer);
this.set('stage', stage);
this.set('container', container);
},
There is also the gameLoop method that uses requestAnimationFrame to update and render... but I dont think they will be that helpful here.
The issue seems to only happen if the canvas is initialized, with out the canvas setup the transition to route is fine. But obviously thats no good!
Which leads me to thinking its to do with the PIXI canvas setup.
Ok, hold head in shame time...
After some furious debugging, I realised the conflict in naming my Pixi.canvasRenderer to renderer.
This conflicts with the Ember Container renderer (renderer._dom).
By sheer convention in Pixi or even in Phaser (seeing as Phaser uses Pixi as its rendering engine) you store the canvasRenderer in a renderer variable.
By changing renderer to canvasRenderer the error of course goes away.
const canvasRenderer = PIXI.autoDetectRenderer(width, height, {
view: $gameCanvas,
backgroundColor: 0xffffff
});

Rendered component test fails after Ember 1.10 upgrade

I'm unit testing a component, in particular the rendered form. I'm approaching this pretty much as described in the Ember Guides.
In particular, the component has three computed properties which show different classes on the rendered elements depending on the backing model. I'm tweaking the properties in Ember.run() blocks and then looking at the rendered component again.
What's interesting here is that the computed properties seem not to be re-computing even through I'm touching the attribute they observe. Later tests which don't test rendering - just the return from the component - do pass.
Here's my test code:
moduleForComponent('wizard-tab', "Component - WizardTab", {
setup: function () {
this.tab = this.subject({ step: 2, stepCompleted: 1, tab: tabs.all()[1] });
}
});
test('#render', function () {
let tab = this.tab;
ok(this.$().find('span.wizard-tab-detail').length, "Active tab has a detail span"); // Passes
// Note that both of the additional states observe stepCompleted
// so I need to touch that to get them to recalculate
Ember.run( function () {
tab.set('stepCompleted', 2);
tab.set('tab', WizardTab.all()[4]);
});
ok(this.$().find('span.wizard-tab-icon-disabled').length, "Future tabs have a disabled class"); // Fails
Ember.run( function () {
tab.set('stepCompleted', 3);
tab.set('tab', WizardTab.all()[1]);
});
ok(this.$().find('span.wizard-tab-icon-done').length, "Inactive tabs have a done class"); // Fails
});
The first assertion passes, the next two fail. Using console.log statements I've validated that the set()s are working, but the property calculated from them is returning the wrong result.
Here's one of the computed property definitions:
disabled: function() {
return this.get('tab.stepNumber') > (this.get('stepCompleted') + 1);
}.property('stepCompleted')
(I literally get false for 5 > 2 when I put in console.log checks on that comparison.) Is there something I'm missing that would prevent that from updating when I check subsequent renders of the component?
This is ember CLI 0.2.0, Ember 1.10.0 and ember-cli-qunit 0.3.8.
ETA: Probably relevant: this test passes on Ember 1.8 and ember-cli-qunit 0.3.1. It's the update to Ember CLI 0.2.0 and accompanying Ember and ember-cli-qunit updates which cause the failure.
(ETA: note from kiwiupover's comment below that this section below isn't relevant to the problem; the guides may not show the best current way to do this.)
Note that the guides use a similar pattern:
test('changing colors', function() {
// this.subject() is available because we used moduleForComponent
var component = this.subject();
// we wrap this with Ember.run because it is an async function
Ember.run(function() {
component.set('name','red');
});
// first call to $() renders the component.
equal(this.$().attr('style'), 'color: red;');
// another async function, so we need to wrap it with Ember.run
Ember.run(function() {
component.set('name', 'green');
});
equal(this.$().attr('style'), 'color: green;');
});
I tried wrapping the second and third assertions in andThen() but that raised errors - andThen() was undefined.
I got this working by starting a new branch off development (our default branch) and re-running the update. Here are the differences between my original pass and what worked:
More component updates, I think just because some time has passed since my first attempt. ember-resolver, loader.js, ember-cli-app-version and ember-cli-dependency-checker had all moved up. I don't know if any of those mattered, but they did change.
The key part, I think, was isolating the three tests in separate test blocks and also updating the subject in an Ember.run() block for each test that used different attribute values from the setup component.
Here's what the three tests looked like when they passed:
moduleForComponent('wizard-tab', "Component - WizardTab", {
setup: function () {
this.tab = this.subject({ step: 2, stepCompleted: 1, tab: WizardTab.all()[1] });
}
});
test('Rendered active tabs have a detail span', function () {
let tab = this.tab;
ok(this.$().find('span.wizard-tab-detail').length, "Active tab has a detail span");
});
test('Rendered future tabs have a disabled class', function () {
let tab = this.tab;
Ember.run( function () {
tab.set('step', 2);
tab.set('stepCompleted', 2);
tab.set('tab', WizardTab.all()[4]);
});
ok(this.$().find('span.wizard-tab-icon-disabled').length, "Future tabs have a disabled class");
});
test('Rendered inactive tabs have a done class', function () {
let tab = this.tab;
Ember.run( function () {
tab.set('step', 2);
tab.set('stepCompleted', 3);
tab.set('tab', WizardTab.all()[1]);
});
ok(this.$().find('span.wizard-tab-icon-done').length, "Inactive tabs have a done class");
});
I believe that last change - moving from one test with some Ember.run() blocks to three - is what really did it. I used some {{log value}} lines in the template to look at which values were being sent to the template, and it was using the subject from the setup block all three times until I added the Ember.run() blocks.

Ember: Equivalent of document.getElementById('foo').addEventListener

I need to add a webkitTransitionEnd event listener to one of the DOM elements in my EmberView.
This javascript (DOM) equivalent would be:
function transitionEnded() {
console.log("transition ended");
}
document.getElementById('foo').addEventListener(
"webkitTransitionEnd",
this.transitionEnded,
true);
I've tried the following:
var MessageView = Ember.View.extend({
...
transitionEnded: function() {
console.log("Transition Alert!");
},
actions: {
closeMessageWithTransition: function(){
var elem = document.getElementById('transitionThis');
elem.addEventListener(
"webkitTransitionEnd", this.transitionEnded, true);
// Trigger the transition here
}
...
I've also tried using:
this.$("#transitionThis").get(0).addEventListener(...);
instead of using:
var elem = document.getElementById('transitionThis');
elem.addEventListener(...);
but to no avail. The transition happens, but I do not get any events nor do I see errors in the console.
I can confirm that the document.getElementById method selects the right element. So, where are my event handlers going?
EDIT 1: Didn't realize there was an emberjs.jsbin.com. So, here you go:
Emberjs.jsbin
PS: I do realize that the element I'm attaching a listener to ends up getting destroyed later when in transitionTo('messages') but I've commented out that bit and still no effect.
EDIT 2: I've added an alternate method of trying to catch the event using an EventManager as outlined in the Ember.View docs.
Alternate Method
It looks like it's using animation, not transition, webkitAnimationEnd is the appropriate hook.
http://emberjs.jsbin.com/awEWUfOd/4/edit

How to live update jqPlot graph with ember.js?

I know how to update and redraw a jqPlot object without using ember...
I created the following fiddle to show the "problem": http://jsfiddle.net/QNGWU/
Here, the function load() of App.graphStateController is called every second and updates the series data in the controller's content.
First problem: The updates of the series seem not to propagate to the view.
Second problem: Even if they would, where can i place a call to update the plot (i.e. plotObj.drawSeries())?
I already tried to register an observer in the view's didInsertElement function:
didInsertElement : function() {
var me = this;
me._super();
me.plotObj = $.jqplot('theegraph', this.series, this.options);
me.plotObj.draw();
me.addObserver('series', me.seriesChanged);
},
seriesChanged: function() {
var me = this;
if (me.plotObj != null) {
me.plotObj.drawSeries({});
}
}
But that didn't work...
Well, figured it out, see updated fiddle.
The secret sauce was to update the whole graphState object (not just it's properties) in App.graphStateController:
var newState = App.GraphState.create();
newState.set('series', series);
me.set('content', newState);
And then attach an observer to it in the App.graphStateView:
updateGraph : function() {
[...]
}.observes('graphState')
The updateGraph function then isn't pretty, since jqPlot's data series are stored as [x,y] pairs.
The whole problem, i guess, was that the properties series and options in the App.graphState object itself are not derived from Ember.object and therefore no events are fired for them. Another solution may be to change that to Ember.objects, too.

When is the template (.tpl) rendered for an Ext JS Component?

I am trying to inject another component into an element that is rendered by the template of another Coomponent..but in the afterrender event, the template is yet to be rendered so the call to Ext.get(el-id) returns null: TypeError el is null.
tpl:
new Ext.XTemplate(
'<tpl for=".">',
'<ul>',
'<li class="lang" id="cultureSelector-li"></li>',
'</ul>',
'</tpl>'
),
listeners: {
afterrender: {
fn: function (cmp) {
console.log(Ext.get('cultureSelector-li')); // < null :[
Ext.create('CultureSelector', {
renderTo: 'cultureSelector-li'
});
}
}
},
So when can I add this component so that the element is targeting has been created in the DOM?
I think it depends on the component that you are working with. For example, the Data Grid View has a "viewready" event that would suite your needs, and depending what you are attempting, the "boxready" function could work for combo box (only the first render though). Other than that, you can either go up through the element's parent classes searching for the XTemplate render function being called (might be in the layout manager) and extend it to fire an event there, or risk a race condition and just do it in a setTimeout() call with a reasonable delay.
I ended up having to do the work myself. So, I now have the template as a property called theTpl, and then rendered it in beforerender, and then i was able to get a handle on the element in afterrender. This seems wholly counter-intuitive, does anyone have any insight?
beforeRender: {
fn: function (me) {
me.update(me.theTpl.apply({}));
}
},
edit in fact I just extended Component thus:
Ext.define('Ext.ux.TemplatedComponent', {
extend: 'Ext.Component',
alias: 'widget.templatedComponent',
template: undefined,
beforeRender: function () {
var me = this;
var template = new Ext.XTemplate(me.template || '');
me.update(template.apply(me.data || {}));
me.callParent();
}
})
...template accepts an array of html fragments
Turns out I was using the wrong things - apparently we should be using the render* configs for this type of thing (so what are thetpl & data configs for?)
Here's a working fiddle provided for me from the sencha forums:
http://jsfiddle.net/qUudA/10/