Lets say we’re writing a blog which allows users to login, but only certain users can write and edit articles.
We want to display add/edit buttons based on permissions, so how do we do that?
For simple permissions, this is quite trivial. For example, to check if the current logged in user is an
administrator we can just do something like:
This only works if we have a single property and we can’t pass any arguments, which means the following won’t work:
blog/index.handlebars
123
{{#if App.currentUser.canEditPost post }}
<button{{actioneditPostpost}}>edit</button> {{/if}}
Research
What we want is a version of if which knows about permissions and will let us pass in arguments so that we can end up with something like this:
blog/index.handlebars
123456789101112
{{#can createPost}}
<button{{actionnewBlogPost}}>New Post</button> {{else}}
You don't have permission to post
{{/can}}
{{#each post in controller}}
<a{{actionviewPostposthref=true}}>{{post.title}}</a> {{#can editPost post}}
<button{{actioneditPostpost}}>Edit</button> {{/can}}
{{/each}}
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);returnhelpers.boundIf.call(options.contexts[0],context,options);});
This just does some sanity checking and hands off to boundIf:
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.
It looks like if we create a helper which calls boundIf with some property to observe on an object, it will take care of the rest for us.
can-helper.js
123456
Handlebars.registerHelper('can',function(permissionName,property,options){// do magic hereEmber.Handlebars.helpers.boundIf.call(someObject,"someProperty",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:
ember-handlebars/lib/helpers/binding.js
1
varcontext=(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).
can-helper.js
12345678910111213
Handlebars.registerHelper('can',function(permissionName,property,options){varpermission=Ember.Object.create({can:function(){returntrue;}.property()});// wipe out contexts so boundIf uses `this` (the permission) as the contextoptions.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!
Implementation
Lets define a class to represent our actual permission:
We want to refer to this with a more friendly name in our templates, we could figure out that createPost maps to App.CanCreatePost by
capitalizing and prepending with ‘Can’, but instead lets make a simple registry:
We now have a couple of permissions which have a can property we can bind to and friendly names to lookup from the templates.
All our helper needs to do is take the passed in name, create an appropriate permission with any attributes and pass that off
to the boundIf helper.
After bit of trial and error, I ended up with the following:
varget=Ember.get,isGlobalPath=Ember.isGlobalPath,normalizePath=Ember.Handlebars.normalizePath;vargetProp=function(context,property,options){if(isGlobalPath(property)){returnget(property);}else{varpath=normalizePath(context,property,options.data);returnget(path.root,path.path);}};Handlebars.registerHelper('can',function(permissionName,property,options){varattrs,context,key,path,permission;// property is optional, if we've only got 2 arguments then the property contains our optionsif(!options){options=property;property=null;}context=(options.contexts&&options.contexts[0])||this;attrs={};// if we've got a property name, get its value and set it to the permission's content// this will set the passed in `post` to the content eg:// {{#can editPost post}} ... {{/can}}if(property){attrs.content=getProp(context,property,options);}// if we've got any options, find their values eg:// {{#can createPost project:Project user:App.currentUser}} ... {{/can}}for(keyinoptions.hash){path=options.hash[key];attrs[key]=getProp(context,path,options);}// find & create the permission with the supplied attributespermission=App.Permissions.get(permissionName,attrs);// ensure boundIf uses permission as context and not the view/controller// otherwise it looks for 'can' in the wrong placeoptions.contexts=null;// bind it all together and kickoff the observersreturnEmber.Handlebars.helpers.boundIf.call(permission,"can",options);});
That’s it, now we can show/hide content based on user permissions and have them automatically update when a user
logs in or their permissions change.