emilioforrer / haml_coffee_assets

Haml Coffee templates in the Rails asset pipeline or as Sprockets engine.
https://rubygems.org/gems/haml_coffee_assets
MIT License
438 stars 145 forks source link

null values show up as text #9

Closed JeanMertz closed 12 years ago

JeanMertz commented 12 years ago

I just switched from using .jst.eco for my Backbone templates to using hamlc. I love it so far, soo much cleaner. While updating my templates I am bumping into one issue though, I can't seem to get a null value to parse nothing.

In my Backbone Model I do:

defaults:
  email: null
  first_name: null
  last_name: null
  company: null

This line in eco shows the company when there is one, and shows nothing when there isn't:

<div class="field">
  <label for="company"> company:</label>
  <input type="text" name="company" id="company" value=<%= @company %> >
</div>

While the new hamlc shows the actual text null inside the input field when no company is provided:

.field
  %label{ for: 'company' } company:
  %input{ type: 'text', name: 'company', id: 'company', value: @company }

Any ideas?

netzpirat commented 12 years ago

The reason is that haml-coffee doesn't have some specific logic at render time to handle some kind of attribute value checking, so normal JavaScript rules take place: "" + null = "null"

There are now two options to handle this case:

  1. Rewrite the escaping function to return '' (empty string) in case the text is null or undefined. Pro: Simple fix, Con: Works only for escaped text
  2. Introduce a specific attribute conversion function. Pro: Works for all attributes and enables more logic like boolean attribute conversion, Con: More complex template

I think I'm going to implement the second option. In the meanwhile you could add

window.HAML.escape = function(text) {
  if (text === null || text === undefined) {
    return "";
  }  
  return ("" + text)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;");
});

as escaping function as a workaround.

JeanMertz commented 12 years ago

Thank you for the help, I am still new to this whole Backbone/templating stuff. Another quick question on this topic:

Given this file:

index.hamlc

%td= @email
%td= @first_name
%td= @last_name
%td= @company

%td
  %a{ href: "#/#{@id}" } Show
  %a{ href: "#/#{@id}/edit" } Edit
  %a{ href: "#/#{@id}/destroy" class: 'destroy' } Destroy

I wanted to hide the Destroy link for the current_user. I tried several things, but I couldn't find out how to solve this. I also tried renaming the file to index.hamlc.erb and using:

  <% if current_user.id == @user.id %>
  %a{ href: "#/#{@id}/destroy" class: 'destroy' } Destroy
  <% end %>

But that resulted in undefined local variable or method current_user. Am I thinking about this the wrong way? Are these templates not processed on the server before they are sent to the client (I asume they are), or should I actually retrieve current_user in the Backbone model before using it in the views/templates?

netzpirat commented 12 years ago

I use CanCan for the authorization and every backend resource that my Backbone app gets through the REST API will be decorated with its abilities. So for example when a user requests the all users, the response looks like this:

{
  success: true
  count: 2
  users: [
    { id: 1, name: "User 1", abilities: ['read', 'update', 'destroy'] }
    { id: 2, name: "User 2", abilities: ['read'] }
  ]
}

Since a real REST API is stateless, the frontend sends the authentication token of the current logged in user, and thus all abilities are for the current user. You can easily extract this into a Rails responder, so you don't have to manually add the abilities for each resource. The advantage of this approach is, you have a single Ability class to control access to the resource for the backend and also the frontend.

Now you can ask the resource if the current user is allowed to destroy it:

- if @user.can('destroy')
  %a{ href: "#/#{@id}/destroy" class: 'destroy' } Destroy

Where the can method is simply:

can: (ability) -> _.include @get('abilities'), ability
JeanMertz commented 12 years ago

Hmm, I see how you do this, that seems like a nice solution. So what you say is that with the abilities added to user 1 and user 2, that means that the current user is able to update and destroy the user with id 1 but can only view the user with id 2, is that correct?

However, the question that remains, is how I can access the current_user resource, for example, to check the role of the user and render specific parts of the UI based on the role? Do I simply send the current_user resource to the javascript?

Sorry for the totally off-topic discussion here, I really appreciate the help!

netzpirat commented 12 years ago

You're right with the abilities, that's exactly the way it's meant to be.

If you like to access the current user in your template it's a perfect match for the global context. There's a section in the README about it, but basically the global context is an object that gets automatically merged into the local context (the data you're passing to the render() method). This would be the ideal place to set the current user, so that @currentUser is always present in every template without the need to explicit pass it with the local context.

JeanMertz commented 12 years ago

Thank you, that seems perfect to me. Re-reading your two posts, I understand how it works, but your javascript implementation of cancan seems to imply the exact opposite of what it actually does.

Reading your

if @user.can('destroy')

It reads as if you want to ask "Can the user destroy", but what it really does is "Can I destroy this user". I guess I just have to get used to reading it different, but maybe something like can('destroy', @user) or something would be easier to understand?

Anyway, thanks for helping out. I'm off to implementing this functionality.

netzpirat commented 12 years ago

I was doing to much things in parallel, and the example is just pseudo code without thinking that hard :P Of course it would make sense to choose a name carefully so that it reflects what it does.