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/
Related
I'm currently using the excellent ember-power-select add on as part of an ember-bootstrap form.
I have multiple drop down items on the form and I am trying to unify how they are handled into a single function that can be used as the onChange action in the power-select invocations:
{{#form.element
controlType="power-select"
label="Destination"
value=destinationSelection
options=destinationOptions
as |el|}}
{{#el.control
onChange=(action "setDropDown")
searchField="name"
as |item|}}
{{item.name}}
{{/el.control}}
{{/form.element}}
My handler function will simply set some values based on the selection of the drop down:
actions: {
setDropDown(selected, string) {
handleDropDown(selected, dropdown, this)
}
}
function handleDropDown(selected, dropdown, controller) {
let selection = `${dropdown}Selection`
let modelid = `model.${dropdown}_id`
set(controller, selection, selected)
set(controller, modelid, selected.id)
}
In order for this to work I really need to be able to pass a string to the setDropDown action from the onChange part of the component call, otherwise I have no way of telling the handler function which particular fields it should be setting without creating an action per dropdown.
However when I try passing in multiple arguments like
onChange=(action "setDropDown" "destination")
or
onChange=(action "setDropDown" selected "destination")
I lose the basic functionality of the onChange action taking the selected item as it's first argument.
I looked through the documentation and couldn't find any examples where the library author is passing multiple arguments into the onChange action and wondered if it was possible without breaking the functionality of the library.
You can use a specialized higher order helper function to create an action for ember-power-select that will ultimately invoke your action with extra arguments. Consider this helper handle-dropdown
import { helper } from '#ember/component/helper';
export function invokeFunction([prop, action]) {
return function(){
action(prop, ...arguments);
}
}
export default helper(invokeFunction);
So what we are doing here is creating the function that will be invoked by ember-power-select. In this function, we are invoking the original action with prop first, followed by every argument that ember-power-select invoked our onchange function with.
In your template, invoke this helper when passing your action to power-select
{{#power-select
onchange=(handle-dropdown 'foo' (action 'dropdownChanged'))
as |dropdown|}}
And then your action would be
actions: {
dropdownChanged(keyToSet, selectedValue){
this.set(keyToSet, selectedValue);
}
}
This would ultimately call dropdownChanged('foo', /* the selected value */)
Ember Bootstrap's Power Select integration gives you a nice API for use cases like this one. Let me give you an example.
Lets take a country selector as an example. We have a list of countries represented by a list of objects holding their two-letters country code as defined by ISO 3166-1 as id property and their name as name. The selected country should be represented on the model which is a POJO by there country code.
export default Component.extend({
// country code of country selected or null
selectedCountry: null,
// Using a computed property here to ensure that the array
// isn't shared among different instances of the compontent.
// This isn't needed anymore if using native classes and
// class fields.
countries: computed(() => {
return [
{ id: 'us', name: 'United States of America' },
{ id: 'ca', name: 'Canada' },
];
}),
// Using a computed property with getter and setter to map
// country code to an object in countries array.
selectedCountryObject: computed('selectedCountry', {
get() {
return this.countries.find((_) => _.id === this.selectedCountry);
},
set(key, value) {
this.set('selectedCountry', value.id);
return value;
}
}),
});
Now we could use Ember Bootstrap Power Select as expected:
{{#bs-form model=this as |form|}}
{{form.element controlType="power-select" property="selectedCountryObject" label="Country" options=this.countries}}
{{/bs-form}}
Disclaimer: Haven't tested that code myself, so there might be typos but I hope you get the idea.
I want to be able to set/get data of a template object when using template event handler functions. I have tried to set a variable at the point when the template is rendered and hoped it would be accessible later on, in this case when an element in the template is clicked by the users, but it isn't working:
<template name="fooBar">
<div class="some_element">CLICK ME</div>
</template>
Template.fooBar.rendered = function(){
this.templateVar = "Hello";
}
Template.fooBar.events({
'click .some_element': function(e,t){
alert(this.templateVar); // Should say 'Hello', but is 'undefined'.
}
});
Using reactive-dict package, you can do like this.
First add it.
meteor add reactive-dict
Second create the templates. (note im using meteor 1.1 version)
if (Meteor.isClient) {
Template.hello.onRendered(function(){
this.templateVar = new ReactiveDict(); //create the var templateVar
})
Template.hello.onRendered(function(){
this.templateVar.set("hi", "hello"); //give them a value
})
Template.hello.events({
'click .some_element': function(e,t){
console.log(t.templateVar.get('hi')) //and print the value using the template instance.
}
});
}
As per the suggestion given by Sindis, this is the quickest way to solve the issue:
Instead of...
alert(this.templateVar);
... just do...
alert(this.templateVar); or alert(Template.instance().templateVar);
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
I'm trying to display a simple template, but it seems like the template doesn't get added, as there is nothing added to the DOM. The code is called for sure and a container has the method setTpl(tpl). What am I doing wrong? The sample above is the most simple example I could imagine, but it doesn't work!
Ext.define('MyApp.view.sample', {
extend: 'Ext.Container'
config: {},
initialize: function() {
this.callParent();
var sampleText = '<div> why?? </div>';
var t = new Ext.Template(
sampleText,
{
compiled: true
}
);
t.compile();
this.setTpl(t);
},
});
HTML = template + data. So your next step is to call setData. Check the docs for tpl. If what you want is to plug in some raw HTML that doesn't depend on data, you've got the html config (and the corollary method setHTML). Last advice, if that's just for rendering some HTML, you don't need to use a container, a Component would be enough.
You have created a class, but you also need to instantiate it. Try something like this:
Ext.create('MyApp.view.sample', {
renderTo: 'some-div-id',
// any other necessary config options
// (see http://docs.sencha.com/extjs/3.4.0/#!/api/Ext.Container)
});
On the Ember MVC TodoApp there is an option "Clear all Completed".
I've been trying to do a simple "Clear All".
I've tried multiple things, none of them work as I expected (clearing the data, the local storage and refreshing the UI).
The ones that comes with the sample is this code below:
clearCompleted: function () {
this.filterProperty(
'completed', true
).forEach(this.removeObject, this);
},
My basic test, that I expected to work was this one:
clearAll: function () {
this.forEach(this.removeObject, this);
},
Though, it's leaving some items behind.
If I click the button that calls this function in the Entries controller a couple times the list ends up being empty. I have no clue what's going on! And don't want to do a 'workaround'.
The clearCompleted works perfectly by the way.
The answer depends on what you really want to know-- if you want to clear an ArrayProxy, as per the question title, you just call clear() on the ArrayProxy instance e.g.:
var stuff = ['apple', 'orange', 'banana'];
var ap = Ember.ArrayProxy.create({ content: Ember.A(stuff) });
ap.get('length'); // => 3
ap.clear();
ap.get('length'); // => 0
This way you're not touching the content property directly and any observers are notified (you'll notice on the TodoMVC example that the screen updates if you type Todos.router.entriesController.clear() in the console).
If you're specifically asking about the TodoMVC Ember example you're at the mercy of the quick and dirty "Store" implementation... if you did as above you'll see when you refresh the page the item's return since there is no binding or observing being done between the entry "controller" and the Store (kinda dumb since it's one of Ember's strengths but meh whatev)
Anywho... a "clearAll" method on the entriesController like you were looking for can be done like this:
clearAll: function() {
this.clear();
this.store.findAll().forEach(this.removeObject, this);
}
Well, this worked:
clearAll: function () {
for (var i = this.content.length - 1; i >= 0; i--) {
this.removeObject(this.content[i]);
}
},
If someone can confirm if it's the right way to do it that would be great!