plush icon indicating copy to clipboard operation
plush copied to clipboard

string concatenation in for loop

Open random-char opened this issue 5 years ago • 10 comments

Writing menu renderer with recursive calls. The problem is that I cannot append a string to menuString inside a for loop. Or I don't know some catch :\

package main

import (
	"fmt"
	"github.com/gobuffalo/plush"
	"log"
)

func main()  {
	html := `<html>
<%
let renderMenu = fn(menuItems, inner) {
  let menuString = "<ul class='nav nav-pills nav-sidebar flex-column' data-widget='treeview' role='menu' data-accordion='false'>"
  if (inner) {
    menuString = "<ul class='nav nav-treeview'>"
  }

  for (item) in menuItems {
    if (len(item["items"]) > 0) {
      menuString = menuString + "<li class='nav-item has-treeview menu-open'><a href='#' class='nav-link'><p>" + item["name"] + "<i class='right fas fa-angle-left right'></i></p></a>" + renderMenu(item["items"], true) + "</li>"
    } else {
      menuString = menuString + "<li class='nav-item'><a href='" + item["link"] + "' class='nav-link'><p>" + item["name"] + "</p></a></li>"
    }
  }

  return raw(menuString + "</ul>")
}

let renderMenuOuter = fn(menuItems) {
  return renderMenu(menuItems, false)
}
%>

<%= renderMenuOuter(menuItems) %>
</html>`

	ctx := plush.NewContext()

	menuItems := []interface{} {
		map[string]interface{} {
			"name": "o1",
			"link": "#",
			"items": []map[string]interface{} {
				{
					"name": "i1",
					"link": "/i1",
				},
				{
					"name": "i2",
					"link": "/i2",
				},
			},
		},
		map[string]interface{} {
			"name": "o2",
			"link": "/o2",
		},
		map[string]interface{} {
			"name": "o3",
			"link": "/o3",
		},
	}
	ctx.Set("menuItems", menuItems)

	s, err := plush.Render(html, ctx)
	if err != nil {
		log.Fatal(err)
	}

	fmt.Print(s)
}

Current render result is:

<html>

<ul class='nav nav-pills nav-sidebar flex-column' data-widget='treeview' role='menu' data-accordion='false'></ul>
</html>a

Expected: to have some <li> elements

random-char avatar Dec 19 '19 20:12 random-char

@paganotoni ,

	r := require.New(t)
	input := `<%let b = "1" %> <% for (i,v) in ["a", "b", "c"] {b = b + 1} %><%= b %>`
	s, err := Render(input, NewContext())
	r.NoError(err)
	r.Equal("1abc", s)

I took a look at this issue and it seems the culprit is this line of code. The global context which b gets copied to a For scope context. So during the for loop, b gets appened with v correctly, however, on function return, the old context gets copied back which has the old value of b. So it overwrites it.

Possible solutions: A) Easiest -> remove that line of code. All the tests ran fine, but I don't know if buffalo will be affected by it or not. B) Keep track of what the global values are and copy them back to the global context.

Mido-sys avatar Apr 15 '21 14:04 Mido-sys

Another solution will be introducing a new keyword global for identifiers. I think that will be the cleanest way. No reason to keep track of the identifiers if they're not needed in the local scope.

@paganotoni , please let me know and I can send a pull request.

so instead of let x = "Hello world" it will be global x = "Hello world" or global x = func() {}

Mido-sys avatar Apr 15 '21 21:04 Mido-sys

@paganotoni @sio4,

Any updates on this? I think a global identifier will be the best route to go. Right now, this code doesn't work:

let isBackOrder = false
for (variant) in product.ProductVariants {				
    if (variant.VariantAllowBackorder) {
        isBackorder = true
        break
    }								
}

We re-wrote the above code to this:

let isBackOrder = for (variant) in product.ProductVariants {				
    if (variant.VariantAllowBackorder) {
      return true
    }								
}

It doesn't feel natural, and I'm sure there will be times that we will need to modify the identifier in other parts of the code. So I guess a global identifier will help solve this, and changes will be backward compatible

Mido-sys avatar Apr 16 '22 10:04 Mido-sys

Hi @Mido-sys, I didn't aware of this issue, I will take a look at this soon.

Currently, I am not sure if I understood the issue. Will try, and will ask you for more information or a sample if it is needed.

sio4 avatar Apr 16 '22 13:04 sio4

@sio4,

Here's a quick sample:

<%
let isBackOrder = false
for (i,v) in [1] {
  if (v == 1) {
     isBackOrder = true 	
  } 
}
%>
<%=  isBackOrder %>

isBackOrder should return true, but it doesn't due to this

I could have a PR ready for a new keyword global soon if we're ok adding to plush.

Mido-sys avatar Apr 16 '22 13:04 Mido-sys

Oh, I see. Sorry for the late response. I just tried to understand what is the @random-char's issue and the followings are my explanation:

  1. Plush is a templating engine rather than an inline programming environment.
  2. I feel like what OP tried to do is somewhat more than a templating. I think writing a recursive function within the template is not a templating.
  3. However, I agree that sometimes we need a more programmatic way to handle the template.

So the possible solution is that:

  1. Keep the template as simple as possible.
  2. If there is a recursive thing, consider using partial.
  3. If you still need more programmatic handling, consider writing a helper rather than trying to make the template complex.

So the OP's template could be changed as something similar to:

<html>
<%=
if (inner) { %><ul class='nav nav-treeview'><% } else { %><ul class='nav'><% } %><%=
for (item) in menuItems { %><%=
	if (len(item["items"]) > 0) { %>
	<li class='nav-item has-treeview menu-open'><a href='#' class='nav-link'><p><%= item["name"] %><i class='right fas fa-angle-left right'></i></p></a>
	INCLUDE_AS_PARTIAL
	</li><%
	} %>
	<li class='nav-item'><a href='<%= item["link"] %>' class='nav-link'><p><%= item["name"] %></p></a></li><%
} %>
</ul>
</html>

I just stop implementing the full example here, but I think it could indicate the direction of templating with plush.

It will print out the following:

<html>
<ul class='nav'>
	<li class='nav-item has-treeview menu-open'><a href='#' class='nav-link'><p>o1<i class='right fas fa-angle-left right'></i></p></a>
	INCLUDE_AS_PARTIAL
	</li>
	<li class='nav-item'><a href='#' class='nav-link'><p>o1</p></a></li>
	<li class='nav-item'><a href='/o2' class='nav-link'><p>o2</p></a></li>
	<li class='nav-item'><a href='/o3' class='nav-link'><p>o3</p></a></li>
</ul>
</html>

This is the way of templating with plush.

For the @Mido-sys's example, I feel like that kind of thing could be easily implemented as a helper function. If you need a helper function rather than a partial, please refer to the following section: https://github.com/gobuffalo/plush#helpers

sio4 avatar Apr 27 '22 14:04 sio4

In fact, I don't work with plush recently, and my memory and ideas could be rusty. Please feel free to let me know if there was some change or if there is something I missed.

sio4 avatar Apr 27 '22 14:04 sio4

@sio4 , Partials will be an overkill for a simple task to create a flag in a loop. Our FED developers don't code in Go and they will require a backend developer to add a partial every time they want to use a flag in a loop.

I think creating a new global keyword flag will be the best option.

Mido-sys avatar Apr 27 '22 14:04 Mido-sys

@sio4 , no worries, I enjoy working on Plush and as I mentioned I could have a PR for a global keyword ready. We faced the above issue many times and creating partials just takes a long time.

Mido-sys avatar Apr 27 '22 14:04 Mido-sys

Actually, I don't have enough context but IMO, your last few example such as isBackOrdet like things could be implemented as helper function which is easily used on the plush templates.

Could you please give me more context if that is not an option? I also will take a look at the code you mentioned earlier but it could not be a high priority.

sio4 avatar Apr 27 '22 15:04 sio4

I am about to close it since it has no activity. please feel free to reopen it.

sio4 avatar Sep 04 '22 14:09 sio4