raymond icon indicating copy to clipboard operation
raymond copied to clipboard

Helper within partial doesn't have access to the partial context

Open kabukky opened this issue 8 years ago • 3 comments

Hi,

thanks for this great library!

Here is the code to reproduce the issue mentioned in the title:

package main

import (
	"fmt"

	"github.com/aymerick/raymond"
)

func main() {
	//template
	templateString := `
<div class="post">
	{{> userMessage tagName="h1" }}
	<h1>Comments</h1>
	{{#each comments}}
		{{> userMessage tagName="h2" }}
	{{/each}}
</div>
`
	tpl := raymond.MustParse(templateString)

	// partial
	partialString := `
<{{tagName}}>
	By {{author.firstName}} {{author.lastName}}
</{{tagName}}>
<div class="body">
	{{content}}
	{{body}}
</div>
`
	tpl.RegisterPartial("userMessage", partialString)

	// content helper
	raymond.RegisterHelper("content", func(options *raymond.Options) string {
		fmt.Println("body: ", options.ValueStr("body"))
		return ""
	})

	// context
	ctx := map[string]interface{}{
		"author": map[string]string{
			"firstName": "Alan",
			"lastName":  "Johnson",
		},
		"body": "I Love Handlebars",
		"comments": []map[string]interface{}{
			map[string]interface{}{
				"author": map[string]string{
					"firstName": "Yehuda",
					"lastName":  "Katz",
				},
				"body": "Me too!",
			},
		},
	}
	result := tpl.MustExec(ctx)
	fmt.Println(result)
}

And here's the output:

body:
body:

<div class="post">

	<h1>
		By Alan Johnson
	</h1>
	<div class="body">

		I Love Handlebars
	</div>
	<h1>Comments</h1>

		<h2>
			By Yehuda Katz
		</h2>
		<div class="body">

			Me too!
		</div>

</div>

As you can see, options.ValueStr("body") inside the content helper is an empty string, while {{body}} in the partial renders just fine. I would expect the content helper to have access to the context it is used under. Is this intended behavior?

Best, Kai

kabukky avatar Apr 19 '17 19:04 kabukky

Update: If I remove the hash parameters for the partial it works just fine:

<div class="post">
	{{> userMessage }}
	<h1>Comments</h1>
	{{#each comments}}
		{{> userMessage tagName="h2" }}
	{{/each}}
</div>

Results in:

body:  I Love Handlebars
body:

So the helper in {{> userMessage }} gets the parent context, the helper in {{> userMessage tagName="h2" }} does not.

kabukky avatar Apr 20 '17 05:04 kabukky

I think I got to the root of it:

  • evalPartial pushes the hash parameters of the partial on the context stack.
  • Value on the Options usescurCtx(), which returns the context at the top of the stack. Inside a partial with hash parameters, this means that Value only has access to those hash parameters.
  • Evaluating {{body}} works because evalPathExpression walks through all contexts using evalDepthPath

How could we fix this?

  • Value could have access to all parent contexts. It seems reasonable at first glance since expressions within partials have access to all parent contexts too. Is that how handlebars.js does it?
  • partialContext, when it detects hash parameters, could create a new context that also includes the parent context like so:
	if node.Hash != nil {
		hash, _ := node.Hash.Accept(v).(map[string]interface{})
		curCtx, _ := v.curCtx().Interface().(map[string]interface{})
		newCtx := make(map[string]interface{})
		for k, v := range curCtx {
			newCtx[k] = v
		}
		for k, v := range hash {
			newCtx[k] = v
		}
		return reflect.ValueOf(newCtx)
	}

The second option seems preferable to me. That way the separation of contexts still holds when using Value inside a helper. What do you think? Just to repeat: I'm not sure if this is intended behavior - so please excuse me if this is in line with handlebars.js

kabukky avatar Apr 20 '17 09:04 kabukky

I went another way in my fork and added a options.ValueFromAllCtx function (see #19). That way, there is no tinkering with existing functions.

kabukky avatar Apr 26 '17 14:04 kabukky