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

Support for sub-expressions #48

Closed
breenyoung opened this issue Apr 17, 2015 · 19 comments
Closed

Support for sub-expressions #48

breenyoung opened this issue Apr 17, 2015 · 19 comments

Comments

@breenyoung
Copy link

I have a {{#each}} loop and within each iteration I have a helper being called I'd like to pass the @Index property to. Is it possible? Template compilation fails when I try to pass it into a helper (without the curly braces:

{{#each }}
{{myHelper @Index arg2}}
{{/each}}

@rexm
Copy link
Member

rexm commented Apr 17, 2015

This should be possible. Looks like a bug

@rexm rexm added the bug label Apr 17, 2015
@rexm
Copy link
Member

rexm commented Apr 18, 2015

Hm, I ran the below test case without any problems. Can you compare? One thing I noticed is that your code snippet doesn't include the name of the object you're each-ing.

string source = "{{#each people}}{{myHelper @index name}}{{/each}}";

Handlebars.RegisterHelper("myHelper", (output, context, arguments) =>
    {
        output.Write("{0}. {1} ", arguments[0], arguments[1]);
    });

var template = Handlebars.Compile(source);

var data = new {
    people = new [] {
        new {
            name = "Bill"
        },
        new {
            name = "Mary"
        }
    }
};

var result = template(data);
Assert.AreEqual("0. Bill 1. Mary ", result);

@rexm rexm removed the bug label Apr 18, 2015
@breenyoung
Copy link
Author

Sorry I omitted some of the code to be brief. It was actually in a sub expression I was trying to use it in.

{{#each InsuranceItems}}
{{#ifCond (mathHelper @Index "%" "2") "ne" "0"}}

{{InsuranceValue}} {{/ifCond}} {{/each}}

InsuranceItems is an IList of custom objects and mathHelper is just a custom helper that takes 3 arguments to execute a math function (in this case mod). IfCond is just an enhanced IF that can take a condition and test value.

@rexm
Copy link
Member

rexm commented Apr 20, 2015

The parens are causing the issue. The parser does not understand those characters, and the compiler doesn't support sub-expressions generally.

Sent from my iPhone

On Apr 20, 2015, at 8:07 AM, Breen notifications@github.com wrote:

Sorry I omitted some of the code to be brief. It was actually in a sub expression I was trying to use it in.

{{#each InsuranceItems}}
{{#ifCond (mathHelper @Index "%" "2") "ne" "0"}}

{{InsuranceValue}}

{{/ifCond}}

{{/each}}

InsuranceItems is an IList of custom objects and mathHelper is just a custom helper that takes 3 arguments to execute a math function (in this case mod). IfCond is just an enhanced IF that can take a condition and test value.


Reply to this email directly or view it on GitHub.

@breenyoung
Copy link
Author

Ah ok, is sub expressions something on the table to implement in the future?

@rexm
Copy link
Member

rexm commented Apr 20, 2015

My desire is to support those in v2, but there's no timeframe for that yet. If someone took on that feature it'd get us a lot closer sooner.

@rexm rexm changed the title passing {{@index}} into a helper? Support for sub-expressions Apr 30, 2015
@rexm
Copy link
Member

rexm commented Apr 30, 2015

Support for sub-expressions is desired for v2. See the Subexpressions section on the handlebarsjs documentation: http://handlebarsjs.com/expressions.html

@rexm
Copy link
Member

rexm commented May 17, 2015

This will require 2 main changes:

  • Enhancing the lexer & converter to understand parentheticals and convert them to a new expression type that behaves similarly to PathExpression... this part will be fairly straightforward
  • Allowing helpers to optionally return a value. Only having given this 5 minutes of thought, it doesn't seem so straightforward. Two possible solutions, neither of which I like:
    1. Break the HandlebarsHelper delegate contracts to return object instead of void. This is a huge breaking change and imposes an extra burden on writing every helper, even though most won't need it.
    2. Pass a Func<object> into the helpers as Return so they can optionally be called as Return(myValue);. This is also a breaking change, imposes less of a burden, but is pretty far off in left field in terms of normal program flow.

Open to suggestions in how to handle the return value!

@hkouns
Copy link

hkouns commented Dec 17, 2015

I wanted to add that I have had a very similar experience that I just referenced in a pull request for another project. (https://github.com/bvcms/bvcms/pull/81)

I have written several helpers including:
IfCond in the form of {{#IfCond lval oper rval}} {{/IfCond}}

Compare in the form of {{Compare lval oper rval}}

Math in form of {{Math lval oper rval "fmt?"}}
ex. {{Math Total "/" 12 "C"}}
fmt is optional. Uses standard/custom dotnet format strings

So when I added the following line to the template, I expected the Math sub-expression to be calculated first and the result returned (from writer.Write(...) statement) and used as an input in the Compare Helper.

{{Compare (Math Total "/" 12) ">" 100}}

returns an error...while

{{Compare 200 ">" 100}}

returns "True"

Similarly If I used the Compare helper for a logical operation (AND)

{{Compare "True" "&&" "True"}}

returns "True" ... while

{{Compare (Compare 200 ">" 100) "&&" "True"}}

returns an error.

Unfortunately, I don't have a solution... so just adding my name to those interested.

@rexm
Copy link
Member

rexm commented Dec 17, 2015

Kind of an interesting approach that doesn't break the existing contracts to try to do a type coercion on the value written to the stream out of a helper.

@rexm
Copy link
Member

rexm commented Dec 19, 2015

@hkouns @breenyoung basic sub-expressions are now supported in 1.5.0. I appreciate any help you can provide to give it a try in your projects and feedback here.

Usage is as @hkouns described:

the sub-expression [evaluated] first and the result returned from writer.Write(...) statement used as an input in the [outer] helper.

In other words, if a helper is put inside a sub-expression (parentheticals), the output written to the buffer will be captured independently and passed as an argument into the outer helper expression.

{{Compare (Compare 200 ">" 100) "&&" "True"}}

The inner Compare helper will be evaluated with the arguments 200, ">", and 100; the outer Compare helper will then be evaluated with the arguments {TextWriter output from inner Compare}, "&&", and "True"

@gretro
Copy link

gretro commented Jan 3, 2016

Test for SubExpressions are failing under Visual Studio 2015. Could it be an issue with .NET 4.6?

This is the StackTrace:


HandlebarsDotNet.HandlebarsCompilerException : An unhandled exception occurred while trying to compile the template
  ----> HandlebarsDotNet.HandlebarsCompilerException : An unhandled exception occurred while trying to compile the template
  ----> System.ArgumentException : Cannot bind to the target method because its signature or security transparency is not compatible with that of the delegate type.
   at HandlebarsDotNet.Compiler.FunctionBuilder.Compile(IEnumerable`1 expressions, String templatePath) in C:\Users\Gabriel\Desktop\rexm-Handlebars.Net\source\Handlebars\Compiler\FunctionBuilder.cs:line 66
   at HandlebarsDotNet.Compiler.HandlebarsCompiler.Compile(TextReader source) in C:\Users\Gabriel\Desktop\rexm-Handlebars.Net\source\Handlebars\Compiler\HandlebarsCompiler.cs:line 32
   at HandlebarsDotNet.Handlebars.HandlebarsEnvironment.Compile(TextReader template) in C:\Users\Gabriel\Desktop\rexm-Handlebars.Net\source\Handlebars\HandlebarsEnvironment.cs:line 51
   at HandlebarsDotNet.Handlebars.HandlebarsEnvironment.Compile(String template) in C:\Users\Gabriel\Desktop\rexm-Handlebars.Net\source\Handlebars\HandlebarsEnvironment.cs:line 58
   at HandlebarsDotNet.Handlebars.Compile(String template) in C:\Users\Gabriel\Desktop\rexm-Handlebars.Net\source\Handlebars\Handlebars.cs:line 29
   at HandlebarsDotNet.Test.SubExpressionTests.BasicSubExpression() in C:\Users\Gabriel\Desktop\rexm-Handlebars.Net\source\Handlebars.Test\SubExpressionTests.cs:line 25
--HandlebarsCompilerException
   at HandlebarsDotNet.Compiler.FunctionBuilder.Compile(IEnumerable`1 expressions, Expression parentContext, String templatePath) in C:\Users\Gabriel\Desktop\rexm-Handlebars.Net\source\Handlebars\Compiler\FunctionBuilder.cs:line 53
   at HandlebarsDotNet.Compiler.FunctionBuilder.Compile(IEnumerable`1 expressions, String templatePath) in C:\Users\Gabriel\Desktop\rexm-Handlebars.Net\source\Handlebars\Compiler\FunctionBuilder.cs:line 61
--ArgumentException
   at System.Delegate.CreateDelegate(Type type, MethodInfo method, Boolean throwOnBindFailure)
   at System.Delegate.CreateDelegate(Type type, MethodInfo method)
   at HandlebarsDotNet.Compiler.SubExpressionVisitor.VisitSubExpression(SubExpressionExpression subex) in C:\Users\Gabriel\Desktop\rexm-Handlebars.Net\source\Handlebars\Compiler\Translation\Expression\SubExpressionVisitor.cs:line 27
   at HandlebarsDotNet.Compiler.HandlebarsExpressionVisitor.Visit(Expression exp) in C:\Users\Gabriel\Desktop\rexm-Handlebars.Net\source\Handlebars\Compiler\Translation\Expression\HandlebarsExpressionVisitor.cs:line 49
   at System.Linq.Expressions.ExpressionVisitor.Visit(ReadOnlyCollection`1 nodes)
   at System.Linq.Expressions.ExpressionVisitor.VisitNewArray(NewArrayExpression node)
   at HandlebarsDotNet.Compiler.HandlebarsExpressionVisitor.Visit(Expression exp) in C:\Users\Gabriel\Desktop\rexm-Handlebars.Net\source\Handlebars\Compiler\Translation\Expression\HandlebarsExpressionVisitor.cs:line 51
   at System.Linq.Expressions.ExpressionVisitor.VisitArguments(IArgumentProvider nodes)
   at System.Linq.Expressions.ExpressionVisitor.VisitMethodCall(MethodCallExpression node)
   at HandlebarsDotNet.Compiler.HandlebarsExpressionVisitor.Visit(Expression exp) in C:\Users\Gabriel\Desktop\rexm-Handlebars.Net\source\Handlebars\Compiler\Translation\Expression\HandlebarsExpressionVisitor.cs:line 51
   at System.Linq.Expressions.ExpressionVisitor.VisitBlock(BlockExpression node)
   at HandlebarsDotNet.Compiler.HandlebarsExpressionVisitor.Visit(Expression exp) in C:\Users\Gabriel\Desktop\rexm-Handlebars.Net\source\Handlebars\Compiler\Translation\Expression\HandlebarsExpressionVisitor.cs:line 51
   at HandlebarsDotNet.Compiler.SubExpressionVisitor.Visit(Expression expr, CompilationContext context) in C:\Users\Gabriel\Desktop\rexm-Handlebars.Net\source\Handlebars\Compiler\Translation\Expression\SubExpressionVisitor.cs:line 12
   at HandlebarsDotNet.Compiler.FunctionBuilder.Compile(IEnumerable`1 expressions, Expression parentContext, String templatePath) in C:\Users\Gabriel\Desktop\rexm-Handlebars.Net\source\Handlebars\Compiler\FunctionBuilder.cs:line 47

@rexm
Copy link
Member

rexm commented Jan 5, 2016

Seems to be a difference between mono and Microsoft... fixed and pushed to Nuget 1.5.1

@rexm
Copy link
Member

rexm commented Jan 13, 2016

Haven't gotten any feedback... going to assume this is good. If you run into any problems, please open a new issue.

@rexm rexm closed this as completed Jan 13, 2016
@aweintraut
Copy link

I realize this is a closed issue, apologies if I should open a new one (will do if requested). I have a helper "ResourceLookup" used to look up a resource string. But I need to build the name of the resource string from my object tree.

What syntax would I use? In this example I need to get myObj.Type formatted as an argument to the TypeName_ resource string, but I have to have the quotes around TypeName or it doesn't pass it as a string to my helper.

 {{ResourceLookup "TypeName_(myObj.Type)"}}

What I get right now is a string that directly contains "TypeName_(myObj.Type)"
If I leave off the quotes, I get an error - Message: Token ')' could not be converted to an expression.
Appending one more option - if I do {{ResourceLookup TypeName_{{myObj.Type}} }}, I get "undefined" as the value of arg1 in my helper.

Any help is appreciated, thanks.

@rexm
Copy link
Member

rexm commented Mar 14, 2016

Please open a new issue so we can keep the discussion focused. When you do that, can you please include the model you are using in this example?

@davidcarroll
Copy link

Thank you @rexm for adding the Sub-Expression functionality. I just started using it and it works great!

@esskar
Copy link

esskar commented May 5, 2017

@rexm

Allowing helpers to optionally return a value. Only having given this 5 minutes of thought, it doesn't seem so straightforward. Two possible solutions, neither of which I like:
Break the HandlebarsHelper delegate contracts to return object instead of void. This is a huge breaking change and imposes an extra burden on writing every helper, even though most won't need it.

Well, you could have two HandlebarsHelper delegates:
the first returns void (as it is);
the second returns an object (HandlebarsHelperWithResult); then, when HandlebarsHelper are registered, you can convert them to an HandlebarsHelperWithResult like
private static readonly NoValue = new object();

public void RegisterHelper(string helperName, HandlebarsHelper helperFunction) 
{
    RegisterHelper(helperName, (TextWriter output, dynamic context, params object[] arguments) => {
        helperFunction(output, context, arguments);
        return NoValue;
    });
}

This would not introduce any breaking changes.

@rexm
Copy link
Member

rexm commented May 5, 2017

If you continue the rest of the thread, you'll see we came to a solution and implemented it in 1.5

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

No branches or pull requests

7 participants