Ember.js - Using {{#if}} around {{bindAttr}} with shared condition? - ember.js

I'm on Ember-1.0.0-pre2 and I seem to be having trouble using an {{#if}} statement around an element which has a {{bindAttr class="..."}} and the binding conditions are the same. I.E. the if statment and class binding are to the same controller attribute. See code:
<button {{action "toggleShow" target="controller"}}>Toggle Visibility</button>
{{#if show}}
<div {{bindAttr class="show:red:green"}}>test</div>
{{/if}}
http://jsfiddle.net/y49ch/10/
If you click the "Toggle Visibility" button several times you'll notice you get a the common error that says: "Something you did caused a view to re-render after it rendered but before it was inserted into the DOM. Because this is avoidable and the cause of significant performance issues in applications, this behavior is deprecated ..."
At first look that seems stupid, but that's a very primitive example of my problem. In my case, there is a computed property on the end of both bindings (if and class attribute). In both cases the computed properties share a common dependent key. When that common dependent key changes it causes both helpers to be update and thus the error.
Is this a bug? I can guess what's happening here, but it seems like I should be able to do this safely.
EDIT: The above is a primitive example of the problem I'm having. It's meant to show it in a very simple way. Below is a more complex example.
Template:
<button {{action "toggleValue" target="controller"}}>Toggle Value</button><br>
{{#if greaterThanTen}}
<div {{bindAttr class="isOdd:red:green"}}>test</div>
{{/if}}
Javascript:
App.myController = Ember.Controller.create({
value: 10,
greaterThanTen: function() {
return this.get('value') > 10;
}.property('value'),
isOdd: function() {
return this.get('value') % 2 === 1;
}.property('value'),
toggleValue: function() {
this.set('value', (this.get('isOdd') ? 10 : 11));
}
});
http://jsfiddle.net/y49ch/16/

I see it now. Your original code had both points watching the same property which got me a little confused, but now it makes more sense. I can't really get what's going on, but I suspect it might have something to do with the runloop.
I've changed your code a little (see this jsfiddle) so that div is now a child view. Some of your properties were moved from the controller to the view (does your spec allow these guys to be at the view or does it have to be at the controller? unless I missed something only the view should be concerned about isOdd and toggleValue at this point) and the css is bound through classNameBindings watching for the value property that is bound to the parent view.
App.myController = Ember.Controller.create({
value: 10,
greaterThanTen: function() {
return this.get('value') > 10;
}.property('value')
});
App.MyView = Ember.View.extend({
templateName: 'my-view',
valueBinding: 'controller.value',
toggleValue: function() {
this.set('value', (this.get('isOdd') ? 10 : 11));
},
isOdd: function() {
return this.get('value') % 2 === 1;
}.property('value'),
ChildView: Em.View.extend({
classNameBindings: 'parentView.isOdd:red:green'
})
});
Now, the template looks like this:
<script type="text/x-handlebars" data-template-name="my-view">
<button {{action "toggleValue"}}>Toggle Value</button><br>
{{#if greaterThanTen}}
{{#view view.ChildView}}
test
{{/view}}
{{/if}}
</script>
Since the default tag for the View is div, it renders the same html, and it totally acts as a different view and prevents unecessary re-render.
Edit: Just as proof of concept, I've added a button to add to the value instead of just toggle so you can actually see the color changing after it gets visible. Here's the fiddle
Let me know if this is good for you

Related

In Ember, is there a way to update a component without a full re-render/route transition

I have a map application. When clicking on a pin, I want to update a "teaser" component in the template by supplying e.g. a query parameter previewId without triggering a full re-render of the route, because that would re-initialize the map and center it on the init position, take a lot of time, in short: is ugly and bad user experience.
So what I have is a map route:
export default Ember.Route.extend({
model: function () {
return this.store.findAll('map-object-proxy');
}
});
a map controller, where I handle the query params:
export default Ember.Controller.extend({
queryParams: ['previewId'],
previewId: null,
previewObject: Ember.computed('previewId', function () {
return this.store.findRecord('map-object', 1);
})
});
and a map-panel component which gets handed the previewObject from the map.hbs template:
<div id="map"></div>
<!-- ... -->
<div class="row" id="teaser-header">
<div class="col-xs-12">{{previewObject.someProperty}}</div>
</div>
map.hbs has this Handlebars markup:
{{map-panel elementId="map-panel" objectProxies=model previewObject=previewObject}}
Sorry, I've not yet quite come to terms with ember's component architecture, even more so as controllers will be deprecated soon and somehow seem to act like a fifth wheel.
Thanks!
You can "refresh" your component by wrapping it like this:
{{#if refresh}}
//component template
{{/if}}
and then an Action to hide and show the component, forcing it to render again.
const _this = this;
this.set('refresh', false);
Ember.run.next(function () {
_this.set('refresh', true);
});

How do I make a conditional helper with ember-cli and handlebars 2.0.0?

I am trying to achieve something similar to the form or function of:
{{#if has-permission "my_permission"}}
// do some stuff here
{{else}}
// fallback
{{/if}}
OR
{{#hasPermission session.user.permissions "my_permission"}}
I cannot figure this out. I've read this but there isn't much explaining on anything other than helpers that render content or alter it e.g. {{render "something"}}
When I try to make it a conditional I get this:
registerBoundHelper-generated helpers do not support use with Handlebars blocks.
Any help greatly appreciated, thanks!
EDIT:
My User object is serialized as such:
{"User": {
"id": 3,
"name": "john doe",
"permissions": [
"view_projects",
"edit_milestones",
"change_widgets"
]}
}
I want to be able to check a certain permission in the template, not bind a specific one to a computed property.
PS Im using ember 1.9.1, latest data and handlebars 2.0
According to the docs, this is not possible with a bound helper
Bound helpers do not support use with Handlebars blocks or the addition of child views of any kind.
What you can do is push that logic to the controller as follows:
App.IndexController = Ember.ArrayController.extend({
permission: "my_permission",
hasPermission: function(){
var permission = this.get('permission');
return permission === 'my_permission';
}.property('permission')
});
Then, in your template you can do:
<script type="text/x-handlebars" data-template-name="index">
{{#if hasPermission }}
<ul>
{{#each item in model}}
<li>{{item}}</li>
{{/each}}
</ul>
{{ else }}
// fallback
{{/if}}
</script>
Working solution here
You can make use of Ember's boundIf/unboundIf default helpers to create a nice and powerful helper to manage client side's user permissions and end up with something like this:
{{#can 'createPost'}}
<button {{action newBlogPost}}>New Post</button>
{{else}}
You don't have permission to post
{{/can}}
{{#each post in controller}}
<a {{action viewPost post href=true}}>{{post.title}}</a>
{{#can 'editPost' post}}
<button {{action editPost post}}>Edit</button>
{{/can}}
{{/each}}
If you take a look at the Ember's source code and see how if works:
Ember.Handlebars.registerHelper('if', function(context, options) {
Ember.assert("You must pass exactly one argument to the if helper", arguments.length === 2);
Ember.assert("You must pass a block to the if helper", options.fn && options.fn !== Handlebars.VM.noop);
return helpers.boundIf.call(options.contexts[0], context, options);
});
You can see that it only does some sanity checking and hands off to boundIf:
Ember.Handlebars.registerHelper('boundIf', function(property, fn) {
var context = (fn.contexts && fn.contexts[0]) || this;
var func = function(result) {
if (Ember.typeOf(result) === 'array') {
return get(result, 'length') !== 0;
} else {
return !!result;
}
};
return bind.call(context, property, fn, true, func, func);
});
This in turn calls bind which handles setting up all the observers and re-rendering when properties change. The result of the func it builds determines whether to display the content or not.
So if you create a helper which calls boundIf with some property to observe on an object, it will take care of the rest for us.
Handlebars.registerHelper('can', function(permissionName, property, options){
// do magic here
Ember.Handlebars.helpers.boundIf.call(someObject, "someProperty", options)
});
Lets fake out the magic and see what happens:
Handlebars.registerHelper('can', function(permissionName, property, options){
var permission = Ember.Object.create({
can: function(){
return true;
}.property()
});
Ember.Handlebars.helpers.boundIf.call(permission, "can", options)
});
Hmm, that leaves the content as hidden. It seems that it’s not calling the can on our permission.
If we look back at boundIf then we can see that it’s looking up the context on the options and only falls back to this if there’s not one set:
var context = (fn.contexts && fn.contexts[0]) || this;
We can get around this by nuking the contexts on the options we pass through to boundIf. (I’m not sure if this will cause issues, but it worked for me… YMMV and all that).
Handlebars.registerHelper('can', function(permissionName, property, options){
var permission = Ember.Object.create({
can: function(){
return true;
}.property()
});
// wipe out contexts so boundIf uses `this` (the permission) as the context
options.contexts = null;
Ember.Handlebars.helpers.boundIf.call(permission, "can", options)
});
If you twiddle the result of can from true to false then we see our content disappear and re-appear, success!
This example goes into more detail in this excellent post by Richard Livsey

Custom view helper in Ember.js, "You can't use appendChild outside of the rendering process"

I want to bind my custom view's class to a controller property.
[javascript]
App.IndexController = Ember.Controller.extend({
headerClass: "a"
});
App.TestHeaderView = Ember.View.extend({
classNames: ["test-header"],
classNameBindings: ["headerClass"],
headerClass: null,
templateName: "views/test-header"
});
[templates]
<script type="text/x-handlebars" data-template-name="index">
{{view App.TestHeaderView text="view helper" headerClass=controller.headerClass }}
<hr />
{{input value=headerClass}}
</script>
<script type="text/x-handlebars" data-template-name="views/test-header">
<small>{{view.text}}</small>
</script>
The result is predictable: everything works. I can enter the class name in the text box and see it reflected in the view.
So now I want to extend this and add my own helper that wraps the {{view}} call.
[javascript]
Ember.Handlebars.helper("test-header", function (options) {
return Ember.Handlebars.helpers.view.call(this, App.TestHeaderView, options);
});
[templates]
<script type="text/x-handlebars" data-template-name="index">
{{test-header text="custom helper" headerClass=controller.headerClass}}
</script>
Nothing special right? Except, I keep getting this:
Uncaught Error: You can't use appendChild outside of the rendering process
For full working jsbin, click here.
It seems this should work. I'm just wrapping the ember's view helper pretty much exactly. What am I missing?
I figured it out.
The trick is in the contexts array in the options hash.
When you call {{view App.MyView}} from handlebars, Ember's view helper gets in its options.contexts array the "context" in which it should search for "App.MyView" property - usually the current controller. In this case, "App.MyView" will be resolved regardless of the context, but I guess Ember keeps the context around and uses it to resolve bound properties.
When I called:
{{test-header text="custom helper" headerClass=controller.headerClass}}
there was no first argument from which to draw the context. Therefore, when I passed the call along to the view helper:
return Ember.Handlebars.helpers.view.call(this, App.TestHeaderView, options);
... there was no context passed along in the options.contexts array.
The way I fixed this is:
Ember.Handlebars.helper("test-header", function (options) {
options.contexts = [this].concat(options.contexts);
return Ember.Handlebars.helpers.view.call(this, App.TestHeaderView, options);
});
IMO Ember should do a better job here. They should either figure out a context from reference, or throw an error (a preferred option).

Access Controller in a View in a Render

I have a view like this:
App.AbilityFilter = Ember.TextField.extend({
classNames: ['span3'],
keyUp: function(evt) {
this.get('controller').send('filterAbilities','text');
},
placeholder:'Search abilities'
});
It's part of a render like this:
<script type="text/x-handlebars" data-template-name="abilities">
{{view App.AbilityFilter}}
<div class="accordion" id="abilities">
{{#each ability in model}}
<div class="accordion-group">
{{ability.name}}
</div>
{{/each}}
</div>
</script>
Which is being rendered in my application like this:
{{render 'abilities'}}
The problem I'm having is with the event or, rather, the action. The keyUp event fires perfectly well, but for some reason it won't go to a controller.
I've tried adding the filterAbilities to the actions hash on both the App.AbilitiesController and the App.IndexRoute according to this. According to this, the view should be part of the abilities controller since that's the context of it's parent, but it's not working.
I've done some testing and it almost seems like this.get('controller') isn't fetching a controller at all. I'm a bit lost as to what's causing the problem. This code worked a few RCs ago, but as soon as I upgraded to 1.0 it broke.
What I'm trying to do here is filter the list of abilities. If this isn't the way to this anymore, please let me know! Any help would be appreciated. Thanks!!
Ember.TextField and Ember.TextArea are no longer simple views but rather subclasses of Ember.Component which means that this.get('controller') does not refer anymore to the views controller.
But there is a different variable which indeed holds a reference to the surrounding controller and this is this.get('targetObject'). Therefore you should send your action to the targetObject:
App.AbilityFilter = Ember.TextField.extend({
classNames: ['span3'],
keyUp: function(evt) {
this.get('targetObject').send('filterAbilities','text');
},
placeholder:'Search abilities'
});
Hope it helps.

EmberJS: Property Scopes in an ArrayController?

this is probably a grossly simple question to answer, so I apologize if I am cluttering this forum in advance.
I am displaying a list of items that share the same model and controller.
I made these items editable via a <button {{ action 'edit' }}> next to each item which toggles a boolean value of a property "isEditable" in the controller.
However clicking this button causes all items in the list to become editable because they all share the controller property "isEditable". The desired effect is to make a single item editable at a time instead of all items at once.
A simplified version of my template looks like this:
{{#if isEditing}}
<p>{{input type="text" value=title}}</p>
<button {{action 'doneEditing'}}>Done</button>
{{else}}
<span class="title">{{title}}</span>
<button {{action 'edit'}}><span class="edit"</span></button>
{{/if}}
and the controller looks like this
App.ItemController = Ember.ArrayController.extend({
isEditing : false,
actions : {
edit : function(){
this.set('isEditing', true);
},
doneEditing : function(){
this.set('isEditing', false);
},
}
});
Anybody know how to accomplish this? Is it because each item shares the "isEditable" property? If so, how do I get around this? I don't want to put this into the model because it's purely a display thing, even though I know I can get it to work doing that.
Thanks :)
By default the controller lookup within an {{#each}} block will be the controller of the template where the {{#each}} was used. If each item needs to be presented by a custom controller (to hold it's own state for example) you can provide a itemController option which references a controller by lookup name. Each item in the loop will be then wrapped in an instance of this controller and the item itself will be set to the content property of that controller.
So, I assume you are displaying the list of items using the {{#each}} helper. Therefore you can specify an itemController in the {{#each}} helper to hold the isEditable state on a per item basis. This would look something like this:
{{#each item in controller itemController="item"}}
...
{{/each}}
Moreover you should define the defined itemController of type Ember.ObjectController like:
App.ItemController = Ember.ObjectController.extend({
isEditing : false,
actions : {
edit : function(){
this.set('isEditing', true);
},
doneEditing : function(){
this.set('isEditing', false);
},
}
});
And for the list you should then have an App.ItemsController of type Ember.ArrayController:
App.ItemsController = Ember.ArrayController.extend({});
See here for more info on the mentioned itemController support for the {{#each}} helper: http://emberjs.com/api/classes/Ember.Handlebars.helpers.html#method_each
Hope it helps.