Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Functions support #168

Closed
evil-shrike opened this issue Jan 30, 2012 · 13 comments
Closed

Functions support #168

evil-shrike opened this issue Jan 30, 2012 · 13 comments

Comments

@evil-shrike
Copy link

Let's consider the example:

var source = "<ul><li>Title: {{title}}</li></ul>";
var tmpl = Handlebars.compile(source);
var vm = { title: "xyz"};
var markup = tmpl(vm);

This works. But instead of passing json-object I need to supply an object which has functions instead of fields:

var vm = {
    title: function() {
        if (arguments.length > 0) {
            this._title = arguments[0];
        } else {
            return this._title;
        }
    }
}

So in template I'm trying to use a function call:

var source = "<ul><li>Title: {{title()}}</li></ul>";

But this doesn't work. It throws:

Parse error on line 1:
<ul><li>Title: {{title()}}</li><li>Te
-----------------^
Expecting 'ID', got 'undefined'

What are my options?

@jblotus
Copy link
Contributor

jblotus commented Feb 3, 2012

you should use helpers instead of passing in functions.

@evil-shrike
Copy link
Author

It's very inconvenient. Actualy these "functions" are "properties". I.e. technically they are functions but only because of the lack of full properties support in all browsers. So it's logicaly to expect {{propName()}} syntax. So does paths: {{prop1().prop2()}}.
Is it hard to implement?

@jblotus
Copy link
Contributor

jblotus commented Feb 3, 2012

I think this is by design, so I don't think it's going to happen. The goal for templates is to keep logic to a minimum, and helper functionality is available to assist with that. It seems like you are trying to set or read a property of a template context. You should be massaging your data before it goes into the template.

@evil-shrike
Copy link
Author

anyway I'd like to hear the author. Honestly I don't understand your point about "keep logic to a minimum". I'm not trying to add any logic. I've just had my data in a form of objects with properties-functions. Take a look at Knockout (it's not my case but a similar approach).

@wagenet
Copy link
Collaborator

wagenet commented Feb 9, 2012

I've discussed this with @wycats and this is specifically something he wants to avoid. He's very adamant that Handlebars draw the line at string/number properties.

If this is something you want support for, you'll have to override the compiler or write your own {{call property}} method. If you want some ideas on how this might be done, you can look at Ember's extension of Handlebars.

@wagenet wagenet closed this as completed Feb 9, 2012
@evil-shrike
Copy link
Author

I understand you point.
Could you take me a tip where can I change HB's code to add support of functions (Amber actualy doesn't do this, it uses properties created with defineProperty so they look like fields for HB).

I'd allow writing templates with simple fields:
FirstName: {{firstName}}
and convert them into "firstName()" function call (I'd add condition check for "typeof fieldName === 'function'" indeed).

p.s. I'd like also add that I can't customize HB with helpers for my case as HB evaluate helpers arguments before executing. For an example:

var obj = { };
{{myhelper firstName}}
Handlebars.registerHelper('domain', function(arg) {
    // arg is underfined. Here I need to have "firstName" value (not value of "firstName" field/property), but I don't have it
});

@evil-shrike
Copy link
Author

Excute my insistence but I've discovered that HB actually does support function calls. The only problem is HB passes some arguments with it (object like this: "{hash:{}}")
Please take a look into populateCall function (line #1312):

this.source.push("if(" + condition + "typeof " + id + " === functionType) { " + nextStack + " = " + id + ".call(" + paramString + "); }");

it transforms into something like this:

if(typeof stack1 === functionType) { stack1 = stack1.call(depth0, {hash:{}}); }

If I remove the "{hash:{}}" argument from function call then it starts working as I expect: put field name into template but have function "fieldName()" invocation at runtime.

Could comment what "hash:{}" argument is and why is it needed. Maybe you'll find appropriate to remove it ?

@wagenet
Copy link
Collaborator

wagenet commented Feb 9, 2012

@evil-shrike I was thinking of this code in Ember: https://github.com/emberjs/ember.js/blob/master/packages/ember-handlebars/lib/ext.js#L78-101 which overwrites the default handler in Handlebars. It then calls this helper instead of the default one: https://github.com/emberjs/ember.js/blob/master/packages/ember-handlebars/lib/helpers/binding.js#L64-86 which either calls a helper if there is one of that name or sets up an Ember binding.

As for the hash, I suspect that Handlebars treats any function it finds as a helper and the hash is passed as part of that. I don't see this behavior being changed.

@evil-shrike
Copy link
Author

I've come up with the following solution:

    Handlebars.JavaScriptCompiler.prototype.nameLookup = function(parent, name, type) {
        if (parent === "helpers") {
            if (Handlebars.JavaScriptCompiler.isValidJavaScriptVariableName(name))
                return parent + "." + name;
            else
                return parent + "['" + name + "']";
        }

        if (/^[0-9]+$/.test(name)) {
            return parent + "[" + name + "]";
        } else if (Handlebars.JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
            // ( typeof parent.name === "function" ? parent.name() : parent.name)
            return "(typeof " + parent + "." + name + " === 'function' ? " + parent + "." + name + "() : " + parent + "." + name + ")";
        } else {
            return parent + "['" + name + "']";
        }
    };

It has pretty ugly part - "if (parent === 'helpers')", that's because of the fact that nameLookup is called for every name (even for helpers).

@bkuberek
Copy link

I came across this as I am in a similar situation trying to use Handlebars with Backbone js. My models have lots properties I would like to execute at runtime and it is just very inconvenient to have a helper for each.

@evil-shrike is your solution working as expected?

@bkuberek
Copy link

@evil-shrike the code you provided above worked for me except for functions that required arguments. In backbone, a model's attributes are accessed via the get method: user.get('email'); So I tried this:

{{user.get "email"}}

But it did not work. I didn't want to override toJSON so I created a new function to return the serialize object.

@asselin
Copy link

asselin commented Dec 6, 2012

Here's a better solution for Handlebars + Backbone integration. This checks for the existence of a get() method on the object and uses that for access if it exists.

     Handlebars.JavaScriptCompiler.prototype.nameLookup = function(parent, name, type) {
        if (/^[0-9]+$/.test(name)) {
           return parent + "[" + name + "]";
        } else if (Handlebars.JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
           // (typeof parent.get == "function" ? parent.get('name') : parent.name)
           return "(typeof " + parent + ".get == 'function' ? " + parent + ".get('" + name + "') : " + parent + "." + name + ")";
        } else {
           return parent + "['" + name + "']";
        }
     };

@markomanninen
Copy link

Small change to the last one to add support for any function call on template, without argument(s) however:


Handlebars.JavaScriptCompiler.prototype.nameLookup = function(parent, name, type) {
    if (/^[0-9]+$/.test(name)) {
        return parent + "[" + name + "]";
    } else if (Handlebars.JavaScriptCompiler.isValidJavaScriptVariableName(name)) {
        return "(typeof " + parent + "." + name + " == 'function' ? " + parent + "." + name + "() : " + parent + "." + name + ")";
    } else {
        return parent + "['" + name + "']";
    }
};

Works with a models like this:


form.feedback = {
    type: "warning"
    warning: function() {return this.type == "warning"},
    error: function() {return this.type == "error"},
    success: function() {return this.type == "success"},
    has: function() {return ["error", "warning", "success"].contains(this.type)},
}

Note contains is not a core javascript list prototype function. Anyway in the template now you can use simple functions:


{{#if form.feedback.has}}
    {{form.feedback.type}}: 
    {{#if form.feedback.success}}ok{{/if}}
    {{#if form.feedback.warning}}stay calm{{/if}}
    {{#if form.feedback.error}}remove{{/if}}
{{/if}}

Keeps template logic simple.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants