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

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

Related

Ember: Update ObjectController property from ArrayController action?

Disclaimer: I'm quite new to Ember. Very open to any advice anyone may have.
I have a action in a ArrayController that should set an ObjectController property. How I can access the right context to set that property when creating a new Object?
Here is abbreviated app code show my most recent attempt:
ChatApp.ConversationsController = Ember.ArrayController.extend({
itemController: 'conversation',
actions: {
openChat: function(user_id, profile_id){
if(this.existingChat(profile_id)){
new_chat = this.findBy('profile_id', profile_id).get('firstObject');
}else{
new_chat = this.store.createRecord('conversation', {
profile_id: profile_id,
});
new_chat.save();
}
var flashTargets = this.filterBy('profile_id', profile_id);
flashTargets.setEach('isFlashed', true);
}
},
existingChat: function(profile_id){
return this.filterBy('profile_id', profile_id).get('length') > 0;
}
});
ChatApp.ConversationController = Ember.ObjectController.extend({
isFlashed: false
});
The relevant template code:
{{#each conversation in controller itemController="conversation"}}
<li {{bind-attr class="conversation.isFlashed:flashed "}}>
<h3>Profile: {{conversation.profile}} Conversation: {{conversation.id}}</h3>
other stuff
</li>
{{/each}}
I don't see why you need an object that handles setting a property for all the elements in your list. Have each item take care of itself, this means components time.
Controllers and Views will be deprecated anyway, so you would do something like:
App.IndexRoute = Ember.Route.extend({
model: function() {
return [...];
}
});
App.ConversationComponent = Ember.Component.extend({
isFlashed: false,
actions: {
// handle my own events and properties
}
});
and in your template
{{#each item in model}}
{{conversation content=item}}
{{/each}}
So, whenever you add an item to the model a new component is created and you avoid having to perform the existingChat logic.
ArrayController and ItemController are going to be depreciated. As you are new to Ember I think that it would be better for you not to use them and focus on applying to coming changes.
What I can advice you is to create some kind of proxy object that will handle your additional properties (as isFlashed, but also like isChecked or isActive, etc.). This proxy object (actually an array of proxy objects) can look like this (and be a computed property):
proxiedCollection: Ember.computed.map("yourModelArray", function(item) {
return Object.create({
content: item,
isFlashed: false
});
});
And now, your template can look like:
{{#each conversation in yourModelArray}}
<li {{bind-attr class="conversation.isFlashed:flashed "}}>
<h3>Profile: {{conversation.content.profile}} Conversation: {{conversation.content.id}}</h3>
other stuff
</li>
{{/each}}
Last, but not least you get rid of ArrayController. However, you would not use filterBy method (as it allows only one-level deep, and you would have the array of proxy objects, that each of them handles some properties you filtered by - e.g. id). You can still use explicit forEach and provide a function that handles setting:
this.get("proxiedCollection").forEach((function(_this) {
return function(proxiedItem) {
if (proxiedItem.get("content.profile_id") === profile_id) {
return proxiedItem.set("isFlashed", true);
}
};
})(this));

How can I render a block only if a specific route is active?

I wanna render a block in Ember Handlebars only, if a specific route is active.
So, how can I create a 'ifRoute' helper, with the same conditons then the 'active' class on the 'linkTo' helper?
I want this, because I've a two layer navigation. So, I want to show the sub-navigation only, if the head navigation point is active. I dont wanna use the 'active' class, because I use lazy loading and I only want to load the sub navigation when the head navigation point is active.
So, what I want to do is:
<ul>
{{#each assortmentGroups}}
<li>
{{#linkTo "assortmentGroup" this}} {{description}} {{/linkTo}}
{{#ifRoute "assortmentGroup" this}}
<ul>
{{#each itemCategories}}
<li>{{#linkTo "itemCategory" this}} {{description}} {{/linkTo}}</li>
{{/each}}
</ul>
{{/ifRoute}}
</li>
{{/each}}
<ul>
How can I do this or is there a better solution?
Thanks
Just add to the controller:
needs: ['application'],
isCorrectRouteActive: Ember.computed.equal('controllers.application.currentRouteName', 'correctRoute')
Similarly:
isCorrectPathActive: Ember.computed.equal('controllers.application.currentPath', 'correct.path')
isCorrectURLActive: Ember.computed.equal('controllers.application.currentURL', 'correctURL')
I am quite sure latest Ember does the rest
Here are two possible options, although for both you first have to save the currentPath in your ApplicationController to have access to it whenever you need it:
var App = Ember.Application.create({
currentPath: ''
});
App.ApplicationController = Ember.ObjectController.extend({
updateCurrentPath: function() {
App.set('currentPath', this.get('currentPath'));
}.observes('currentPath')
});
Using a computed property
Then in the controller backing up the template, let's say you have a NavigationController you create the computed property and define also the dependency to the ApplicationController with the needs API to gather access, then in the CP you check if the currentPath is the one you want:
App.NavigationController = Ember.Controller.extend({
needs: 'application',
showSubMenu: function(){
var currentPath = this.get('controllers.application.currentPath');
return (currentPath === "assortmentGroup");
}.property('controllers.application.currentPath')
});
So you can use a simple {{#if}} helper in your template:
...
{{#linkTo "assortmentGroup" this}} {{description}} {{/linkTo}}
{{#if showSubMenu}}
<ul>
{{#each itemCategories}}
<li>{{#linkTo "itemCategory" this}} {{description}} {{/linkTo}}</li>
{{/each}}
</ul>
{{/if}}
</li>
...
Using a custom '{{#ifRoute}}' helper
But if your really want a custom helper to deal with your condition then this is how you could do it, note that the currentPath stored on your application is still needed since we need a way to get the value of the current route:
Ember.Handlebars.registerHelper('ifRoute', function(value, options) {
if (value === App.get('currentPath')) {
return options.fn(this);
}
else {
return options.inverse(this);
}
});
And then you could use it like this:
...
{{#linkTo "assortmentGroup" this}} {{description}} {{/linkTo}}
{{#ifRoute "assortmentGroup"}}
<ul>
{{#each itemCategories}}
<li>{{#linkTo "itemCategory" this}} {{description}} {{/linkTo}}</li>
{{/each}}
</ul>
{{/ifRoute}}
</li>
...
See here also a simple Demo of the "custom helper" solution: http://jsbin.com/izurix/7/edit
Note: with the second solution there is a catch! Since bound helpers do not support blocks (in embers handlebars customization) I used a simple helper that does not reevaluate the condition depending on bindings which is may not what you want.
Hope it helps.
After investigating the ember code for the linkTo and if helpers, the answer from intuitivepixel and a blog post about writing my own bound block helper, I've found a solution:
var resolveParams = Ember.Router.resolveParams;
var resolvedPaths = function(options) {
var types = options.options.types.slice(1),
data = options.options.data;
return resolveParams(options.context, options.params, { types: types, data: data });
};
Ember.Handlebars.registerHelper('ifRoute', function(name) {
var options = [].slice.call(arguments, -1)[0];
var params = [].slice.call(arguments, 1, -1);
var theResolvedPaths = resolvedPaths({ context: this, options: options, params: params });
var router = options.data.keywords.controller.container.lookup('router:main');
var self = this;
var evaluateIsCurrentRoute = function() {
self.set('current_route_is_active_bool_for_ifroute', (function() {
return router.isActive.apply(router, [name].concat(theResolvedPaths)) ||
router.isActive.apply(router, [(name + '.index')].concat(theResolvedPaths));
})());
};
evaluateIsCurrentRoute();
router.addObserver('url', evaluateIsCurrentRoute);
options.contexts = null;
return Ember.Handlebars.helpers.boundIf.call(this, 'current_route_is_active_bool_for_ifroute', options);
});
I found an easy way to check if a route is active, but to get this into a computed property may not be so easy.
// Test if you are currently in a route by it's lowercase name
App.isInRoute = function(name) {
return App.Router.router.currentHandlerInfos.mapProperty('name').contains(name);
}
To use:
App.isInRoute('posts.show'); // true if in the route

Calling controller action from view in Ember

I have a submit button with a onClick view event. This event checks a flag and depending upon the condition it will allow form submission. I'd like the submit action on the controller to be called. What is the best way to do this?
Here another solution based on the example by albertjan for the case you have to perform some logic in your View and afterwards delegate to your controller. This is the way i understood your question:
HBS:
<script type="text/x-handlebars" data-template-name="index">
<button {{action submit target="view"}} >Sumbit</button>
</script>
View:
App.ThingView = Ember.View.extend({
submit : function(){
//do the view part of your logic
var object = //do whatever you may need
this.get("controller").send("submitInController", object); //you do not have to send object, if you do not need to
}
});
Controller:
App.ThingController = Em.ObjectController.extend({
submitInController: function(model) {
// do the controller part of your logic
}
});
Note: The call from your view will also bubble up to your current route. So this is basically the same code, that ember is executing when using the action helper.
I would handle the whole event on the controller:
HBS:
<script type="text/x-handlebars" data-template-name="index">
<button {{action "submit"}}>Sumbit</button>
</script>
Controller:
App.ThingController = Em.ObjectController.extend({
submit: function() {
//handle things here!
//change the state of your object here to reflect the changes that
//the submit made so that the view shows these.
}
});
In Ember version 1.0.0, I've been having success adding actions to their own object literal in the controller.
IndexTemplate.html
<script type="text/x-handlebars" data-template-name="index">
<button {{action "submit"}}>Submit</button>
</script>
ThingController.js
App.ThingController = Ember.ObjectController.extend({
actions: {
submit: function() {
//handle things here!
//change the state of your object here to reflect the changes that
//the submit made so that the view shows these.
}
}
});
For more information, check out the {{action}} helper documentation from Ember Guides.
You can trigger an action from a view if the view uses the ViewTargetActionSupport mixin. The following example demonstrates its usage:
App.SomeController = Ember.Controller.extend({
actions: {
doSomething: function() {
alert('Doing something!');
}
}
});
App.SomeView = Ember.View.extend(Ember.ViewTargetActionSupport, {
someMethod: function() {
this.triggerAction({action: 'doSomething'});
}
});

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

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

What's the best idiom for creating an EmberJS view that can show all its child-views, or just one?

Say I have a model App.Page, an ArrayController of App.Page, and an App.PageView to render each App.Page.
I'm trying to figure out how to best implement App.MyPagesView so it works like so:
if App.showAllPages is true: I want MyPagesView to contain an App.PageView(s) for displaying each of the App.Page in App.pages
Else: I want MyPagesView only show one App.PageView, bound to App.pages.currentPage.
The most straightforward implementation that occurs to me is using a template like so:
// MyPagesViewApproach1
{{#unless App.showAllPages}}
{{view App.PageView pageBinding="pages.currentPage"}}
{{else}}
{{#each pages}}
{{view App.PageView pageBinding="this"}}
{{/each}}
{{/unless}}
But won't this create new views for the existing models every time the user toggles showAllPages on and off? Also, I get emberJS warnings about performance issues when I try to use this template.
The PageView(s) could be quite complex and expensive to render. I'd really like to create a PageView once for each Page, and just remove/hide the irrelevant PageViews from the DOM when they're not in use.
App = Ember.Application.create({
showAllPages: false,
pages: Ember.ArrayController.create({
content: []
currentPage: null
}),
ready: function () {
this.pages.pushObject(App.Page.create({title: 'Page One'});
this.pages.pushObject(App.Page.create({title: 'Some Other Page'});
this.pages.pushObject(App.Page.create({title: 'Grrreatest Page Evar'});
this.pagesController.set('currentPage',
this.pagesController.get('firstObject'));
}
});
App.Page = Ember.Object.extend({
title: null
// etc, etc...
});
App.PageView = Ember.View.extend({
templateName: 'page',
page: null // should be bound to an App.Page
});
App.MyPagesView_Approach1 = Ember.View.extend({
pagesBinding: 'Elicitation.pages'
// ???
});
App.MyPagesView_Approach2 = Ember.ContainerView.extend({
// ???
});
And my HTML:
<script type="text/x-handlebars" data-template-name="page">
The title of this page is {{ page.title }}
</script>
{{view App.MyPagesView }}
To recap, what's the proper EmberJS-y way to implement MyPagesView so it responds to App.showAllPages without re-creating all the views each time its toggled?
Should it be some sort of ContainerView? Or should I use the unless/else template shown at the top of the question? Or something entirely different? I feel like a really simple solution exists in EmberJS, but its elluding me.
Here's the best I've come up with, encapsulated as a re-usable View class called "CurrentCollectionView". I'm using CollectionView, and using view.set('isVisible') to hide/show appropriate child views. Basically use it like a CollectionView, but you can set currentContent to hide all but one element of content, or use showAllContent to override currentContent.
App.CurrentCollectionView = Ember.CollectionView.extend({
showAllContent: false,
currentContent: null,
currentContentChanged: function () {
console.log("Elicitation.PagesView.currentContentChanged()");
var showAllContent = this.get('showAllContent');
if (Ember.none(showAllContent) || !showAllContent) {
var contents = this.get('content');
var currentContent = this.get('currentContent');
this.get('childViews').forEach(function (view, i) {
var isVisible = contents.objectAt(i) == currentContent;
view.set('isVisible', isVisible);
});
} else {
this.get('childViews').forEach(function (view) {
view.set('isVisible', true);
});
}
}.observes('currentContent', 'showAllContent', 'childViews')
});
An example of using CurrentCollectionView to implement MyPagesView:
App.MyPagesView = App.CurrentCollectionView.extend({
itemViewClass: App.PageView,
contentBinding: 'App.pages',
currentContentBinding: 'App.pages.currentPage',
showAllContentBinding: 'App.showAllPages',
});
or as using it inline as a template:
{{view App.CurrentCollectionView itemViewClass="App.PageView" contentBinding="App.pages" currentContentBinding="App.pages.currentPage" showAllContentBinding="App.showAllPages"}}
Hope somebody else finds this useful and/or can improve on it (please!)