Ember Component classNameBinding not binding - ember.js

I have a component that is a button which needs to change it's classNames via a property of it parent component/controller:
// components/my-button/component.js
import Ember from 'ember';
export default Ember.Component.extend({
tagName: 'button',
classNameBindings: ['classNames'],
// some other unrelated stuff following....
});
it's template:
// components/my-button/template.hbs
{{text}}
// nothing important here exept the test-output of:
{{classNames}}
and I insert it like this in another component:
// component/parent-component/template.hbs
{{my-button
classNames=variableClassNames
text='foo'
}}
// components/parent-component/component.js
import Ember from 'ember';
export default Ember.Component.extend({
isSortableDown: Ember.computed('track.sort', 'maxSort', function() {
return this.get('track.sort')<this.get('maxSort');
}),
variableClassNames: Ember.computed('isSortableDown',function() {
if(this.get('isSortableDown')) {
return 'btn btn-xs btn-default';
} else {
return 'btn btn-xs btn-default push-20-r';
}
}),
// other unrelated stuff...
});
Now here's my problem:
when isSortableDown is changing, variableClassNames in parent AND classNames in child (component/my-button) IS updated (also the test-output in my-button template).
BUT the classNameBindings are NOT updated, instead the classNames appear multiple times (when looking at the actual outputted DOM).
Well, that's not 100% right, they do get added, but never removed.
So if the className push-20-r once gets added, it'll stay there (in the DOM), but never removed, even if the property classNames doesn't include it anymore.
Finally my question is if I'm doing something wrong,
or if the classNameBindings should not be updated (but why the name 'bindings' then??),
or if this is eventually a bug?
I'm on
Ember 2.0.1
jQuery 1.11.3
The only maybe relevant issues I found are:
https://github.com/emberjs/ember.js/issues/11980
https://github.com/emberjs/ember.js/issues/11556
but they don't have an answer... or are only party related
Sidenote:
Yes, I want the component itself to be a button, not a div, because otherwise I'd have to change all the css.... I know I could do it by leaving the component a div and wrap that button and adjust it's classNames there.

you are using special property of ember component classNames as bound variable which was causing the problem instead you can take following approach
export default Ember.Component.extend({
tagName: 'button',
classNameBindings: ['isSortableDown::push-20-r'], // class added when isSortableDown is false
classNames: ['btn', 'btn-xs', 'btn-default'], // classes that don't change
isSortableDown: true
// some other unrelated stuff following....
});
in template
{{my-button
isSortableDown=isSortableDown
text='foo'
}}
in your parent component
export default Ember.Component.extend({
isSortableDown: Ember.computed('track.sort', 'maxSort', function() {
return this.get('track.sort')<this.get('maxSort');
}),
// other unrelated stuff...
});

classNames is a special property within components. You might try changing the name to something else and see if that helps?
http://emberjs.com/api/classes/Ember.Component.html#property_classNames

Related

Creating alias to a dynamic property now that Ember.Binding is deprecated

How can I add alias or observer to a property that's name I only know when the component is initialized?
I need this for a generic use component where the property name depends on what data was passed to the component. In previous Ember versions I could just create the binding on init:
binding = Ember.Binding.from("model.settings." + this.get('type')).to("setting");
binding.connect(this);
Then use the "setting" wherever it is needed and everything gets correctly updated when or if the property changes. The "type" property is passed to the component from the outside and is different for every instance of the component so I can't hard code the property name in the component itself.
Now Ember.Binding was deprecated in Ember 2.7 and will be removed in Ember 3.0.
I can't figure out how to achieve this without Ember.Binding. And no, there isn't a good way to pass the value from elsewhere or manage this without a binding as far as I can tell. The actual component is a bit more complicated than what I described above but the problem remains same.
You need to use defineProperty to create computed property for dynamic dependant key.. ember-twiddle
controllers/application.js
import Ember from 'ember';
export default Ember.Controller.extend({
appName: 'Ember Twiddle',
actions:{
changeType(){
this.set('model.settings.admin','changed-adminsettings');
}
}
});
templates/application.hbs
{{my-component model=model type='admin' }}
<button {{action 'changeType'}}> ChangeType</button>
my-component.hbs
{{setting}}
{{yield}}
my-component.js
import Ember from 'ember';
export default Ember.Component.extend({
init(){
this._super(...arguments);
var type= this.get('type');
Ember.defineProperty(this,'setting',Ember.computed('model.settings.' + type,function(){
return this.get('model.settings.'+type);
}));
}
});
Reference: https://github.com/emberjs/ember.js/issues/13912

How do I make dynamic classNames in an ember 2.0 component?

For example:
Ember components allow you to add a classNames array and those classes will be added to the main div of the component.
say we have this component called new-div
export default Ember.Component.extend({
classNames: ['container']
});
then if you inspect this component when rendered you will see:
<div id="ember595" class="ember-view container">
...
<div>
this is fine but my issue is what if i want to use this component as a fluid container sometimes and sometimes I might want to make it a jumbotron etc.
Is there a way to do this in the html and have the component.js apply it correctly?
{{new-div extra-classes='class1,class2'}}
then in the component.js:
export default Ember.Component.extend({
classNames: [this.get('class-names')]
});
The #dmk'solution is the cleanest one, but if your scenario it is not working you can use classNameBindings:
export default Ember.Component.extend({
classNameBindings: ['getClassNames'],
getClassNames: Ember.computed('extra-classes', function(){
return this.get('extra-classes').replace(',', ' ');
})
})
You can add class names simply by specifying them inside the class attribute on your component:
{{new-div class="class1 class2"}}
If you're not adding too many classes, it's easy enough with class name bindings:
export default Ember.Component.extend({
classNameBindings: [
'foo:bar',
'foo:baz',
],
});
And set the value of foo:
{{new-div foo=true}}
This will toggle on all the above class names.
See: https://api.emberjs.com/ember/release/classes/Component/properties/classNameBindings?anchor=classNameBindings
Of course, you could get tricky with computed properties and mapping an array. Also: I like to avoid assigning dynamic class names to components explicitly. Things become messy rather quickly.
Just as an alternative one could use something like this
export default Ember.Component.extend({
attributeBindings: ['style'],
style: function(){
return new Ember.Handlebars.SafeString('class="foo bar"');
}.property(),
});
// NOT sure on this one untested
export default Ember.Component.extend({
attributeBindings: ['classNames'],
classNames: function(){
return 'foo bar';
}.property(),
});
If someone is using ember-component-css, you may want to try out the join-classes or the local-class attribute helper.
{{join-classes styles.myclass1 attributeValue}}
attributeValue can be a value from the component's controller (I mean component.js), or an item inside an each block.
If styles.myclass1 = .class1, and attributeValue = .dummy, then the selectors would be available as .class1.dummy in the styles.css.
local-class={{(concat "myclass-" myvalue)}}
If myvalue = 'part', then with this, the generated classname would include tree-to-component_myclass-part__sdfje2jbr2 (last part is generated id), and would be accessible in the stylesheet as .myclass-part.

Getting element by ID in Ember

I am running two ember applications. One has the following component:
import Ember from 'ember';
export default Ember.Component.extend({
tagName: 'a',
click: function() {
Ember.$('#wrapper').toggleClass('toggled');
}
});
and the other one, has this one:
import Ember from 'ember';
export default Ember.Component.extend({
tagName: 'a',
click: function() {
this.$('#wrapper').toggleClass('toggled');
}
});
What I can't understand here is why in one application I select an element by ID using Ember.$('#wrapper') and in the other using this.$('#wrapper').
What is this about? Ember version?
UPDATE
I'm very puzzled, since both components are the same:
{{#show-menu}}
<i class="fa fa-bars"></i>`
{{/show-menu}}`
They are both hamburger menus used to hide a sidebar div, and the #wrapper is an external element.
Since in both cases the #wrapper are external elements, shouldn't just the first case work #Gaurav and #Kevin Jhangiani?
The difference is in the context of the jquery selector.
Ember.$()
is scoped to the entire document, ie, you can access any element on the page.
In contrast,
this.$()
is scoped to the current component or view, and thus you can only access dom elements that are children.
Generally, you should be using this.$ as it will be more performant (since the search space is only child elements). Ember.$ should be reserved for times when you absolutely need to access an element outside of the current context.
Ember.$('#wrapper') will find an element in the page with the id of wrapper.
this.$('#wrapper') will find an elment within the component with the id of wrapper.
If there is any chance that the component you are defining will ever occur more than once in the page, then you should use neither. Edit the appropriate template so that wrapper is a class, not an id. Then use:
this.$('.wrapper')
Since you are essentially just toggling a class, the more "Ember" way of doing this is having a conditional class on your wrapper and toggle a property on your component:
import Ember from 'ember';
export default Ember.Component.extend({
tagName: 'a',
classToggle: false,
click: function() {
this.toggleProperty('classToggle');
}
});
Then, on your DOM element you can have a conditional class:
<div id="wrapper" class="{{if toggleClass "toggled"}}">...</div>
or, if you are using an older version of Ember:
<div id="wrapper" {{bind-attr class="toggleClass:toggled"}}>...</div>
This is a bit more reusable as your component doesn't rely on a DOM element (which can get messy if you want to reuse this component ever).

Where should I set defaults on components with attributeBindings?

I have a component {{upload-image src=foo.bar}}. When foo.bar is falsy I want to give it a different value. I setting the value in init like so:
import Ember from 'ember';
export default Ember.Component.extend({
attributeBindings: ['src'],
tagName: 'img',
init: function(){
this.set('src', 'http://www.placecage.com/40/40');
this._super();
this.set('src', 'http://www.placecage.com/40/40');
},
click: function(){
console.log(this.get('src'));
}
});
However it doesn't work. The image gets rendered with the value I pass in, not the new one. When I click the image it does log the placecage image. Instead if I try to override it later on say didInsertElement it works as expected:
import Ember from 'ember';
export default Ember.Component.extend({
attributeBindings: ['src'],
tagName: 'img',
didInsertElement: function(){
this.set('src', 'http://www.placecage.com/40/40');
},
click: function(){
console.log(this.get('src'));
}
});
It seems a bit silly to have it render out and only then be able to change the value causing (I presume) a re-render. What's the appropriate place to check if src is falsy and set it to a default and get the expected results?
In Ember 1.13 you can do that in
attributeBindings: ['src:src'],
didInitAttrs() {
this.set('src', <new value>);
}
Or you could do the falsy logic outside of the component and pass it in.
I would create another property defined as follows:
effectiveSrc: function() {
return this.get('src') || 'http://www.placecage.com/40/40';
}.property('src')
then use effectiveSrc in your logic and templates.
Another alternative is to give src a default:
src: 'http://www.placecage.com/40/40'
Then when calling your template, omit src if its value is falsy, something like
{{#if foo.bar}}
{{upload-image src=foo.bar}}
{{else}}
{{upload-image}}
{{/if}}
The following might also work, need to try it:
{{upload-image src=(if foo.bar foo.bar)}}
With ember 2.x (I've tested with 2.4.2, 2.4.3 and 2.5.0), I've achieved this functionality with the mentioned default by torazaburro :
export default Ember.Component.extend({
attributeBindings: ['src'],
tagName: 'img',
src: 'http://www.placecage.com/40/40',
click() {
console.log(this.get('src'));
}
}
And the templates:
{{upload-image src="http://cdn.sstatic.net/Sites/stackoverflow/img/apple-touch-icon.png"}}
{{upload-image}}
The Twiddle to test it
There's an open issue over on Github https://github.com/emberjs/ember.js/issues/11637 where they seem to be saying that I shouldn't set the value using the two-way data binding this way.
I suppose I should make it read from a computed property instead and set my default from that.

Ember JS: using a component to wrap accounting.js library

I am trying to wrap accounting.js into a component but i am missing something fundamental and getting an error.
Here is the each loop in my template:
{{#each item in model}}
{{#with item}}
{{#link-to 'debtor' debtor_id}}
<div>{{debtor_id}}</div>
<div>{{debtor_legacy_account_number}}</div>
<div>{{debtor_full_name}}</div>
<!-- below is the component call -->
<div>{{currency-widget value=debtor_balance}}</div>
<div>{{debtor_debt_number}}</div>
{{/link-to}}
{{/with}}
{{/each}}
and here is the component:
// app/components/currency-widget.js
import Ember from 'ember';
export default Ember.Component.extend({
tagName: 'span',
classNames: ['currency-widget'],
didInsertElement: function(value) {
return accounting.formatMoney(value);
}
});
as you can see, i want to have this component loop through and return a formatted value for each debtor_balance property passed to it.
Update: I don't have any errors now.. but the component is not returning the new value.
I added the dependency in my brocfile
app.import('vendor/accounting.js/accounting.min.js');
And I also included accounting and formatMoney to global in my .jshintrc file
It must be the the logic inside the component? maybe its something simple
I don't believe you can pass a value to the didInsertElement function, but you can still access properties that you set in the template when you define the component. When you say:
{{currency-widget value=debtor_balance}}
You're effectively setting a value property on the component that is equal to the debtor_balance. So update your component code to access the components "value" attribute, as opposed to trying to pass the value to the didInsertElement function.
export default Ember.Component.extend({
tagName: 'span',
value: 0,
classNames: ['currency-widget'],
didInsertElement: function() {
return accounting.formatMoney(this.get('value'));
}
});