squirrellyjs / squirrelly

Semi-embedded JS template engine that supports helpers, filters, partials, and template inheritance. 4KB minzipped, written in TypeScript ⛺
https://squirrelly.js.org
MIT License
592 stars 82 forks source link

Explicit Helper References #120

Closed clitetailor closed 4 years ago

clitetailor commented 4 years ago

Currently, helper references are referred implicitly in template:

{{each(a)}}
  This is a helper ref from helper1: {{@this}}
{{/each}}

This is quite bad practice when people always have to remember the names of refs. Dealing with nesting refs is quite really odd, also:

{{each(a)}}
  {{each(b)}}
    {{each(c)}}
      {{@../../this}}
    {{/each}}
  {{/each}}
{{/each}}

So the idea of this proposal is to explicitly assign ids to refs and reference them by ids:

{{each(items) item, index}}
  The #{{@index}} item is: {{@item}}
{{/each}}

Later in defineHelper, we can define our helper like this:

Sqrl.defineHelper("each", function(args, content, blocks) {
  let items = args[0]
  let templates = []  

  for (let i = 0; i < items.length; ++i, item = item[i]) {
    templates.push(content(item, i))
  }

  return templates.join('')
})

This way we can also remove the @ if we want. Reference a nested field is also much more easier and standard:

{{each(items) item, id}}
  The #{{id + 1}} item is: {{item.name}}
{{/each}}
nebrelbug commented 4 years ago

Hi @clitetailor - sorry for the late reply, and thanks for the suggestion!

I really like your idea of the user naming helper references! This would allow us to get rid of IDs and scoping (../), which I agree have rather clumsy syntax.

My initial thought is that I'd prefer still using the @ sign, because I feel that it makes it easier to distinguish helper references from global references.

One possible problem I can foresee is users forgetting which helper reference name comes first and putting something like:

{{each(items) id, item}}
  The #{{id + 1}} item is: {{item.name}}
{{/each}}

In which case their template would error. Do you think this would be a problem?

nebrelbug commented 4 years ago

Also, what do you think of a few ideas I have to make the syntax easier to read at a glance?

  1. {{each(items)->item, id}}
    The #{{id + 1}} item is: {{item.name}}
    {{/each}}
  2. {{each(items) [item, id]}}
    The #{{id + 1}} item is: {{item.name}}
    {{/each}}
clitetailor commented 4 years ago

Hi @clitetailor - sorry for the late reply

You reply so fast actually 😄

My initial thought is that I'd prefer still using the @ sign, because I feel that it makes it easier to distinguish helper references from global references.

This is not a problem. We'd better let scope do its job. Actually, @ can be used in better way:

{{@import helper1, helper2}}
{{@global scripts, styles, tags}}

This way, we can explicitly tell which variables or helpers the template use. This also make code splitting much less painful. Scoped macro may also be helpful

One possible problem I can foresee is users forgetting which helper reference name comes first.

This is also not a problem, user easier to remember variable position than its name. Many template engines use this pattern (Svelte 3 template for e.g.). JS callbacks also works the same way:

items.forEach((item, i) => { /* ... */ })

Also, what do you think of a few ideas I have to make the syntax easier to read at a glance?

Originally i got the same ideal with you. But using a lot of symbols can be an ache. It distract user typing from center of the keyboard and remember such symbols is not efficient also. In Svelte they do:

{{#each items as item, index (id)}}

In AngularJS:

ng-for="let item in items; id = $index track by id"

So my ideal is something like this:

1.

{{~each(items) as item, index with name = item.name}}

{{~each(items) with name = $0.name, index = $1 }}

{{~load(file) as value}}
  {{value}}
{{#catch as error}}
  {{error}}
{{/load}}

{{~load(file) #catch as error}}
  {{error}}
{{/load}}

2.

{{~each(items): item, index}}

{{~each(items): { name }, id}}

{{~load(file): value}}
  {{value}}
{{#catch: error}}
  {{error}}
{{/load}}

{{~load(file) #catch: error}}
  {{error}}
{{/load}}

{{~load(file) #catch}}
  Error!
{{/load}}

The first way is much more readable and extensible while the second way is shorter and easier to look and type. What do you think?

nebrelbug commented 4 years ago

Thanks for all of the great ideas and code examples!

What about something like this:

{{~each(items) => item, index}}
OR 
{{~each(items) => { name }, id}}

And for the load example, something like this:

{{~load(file) => value}}
  {{value}}
{{#catch => error}}
  {{error}}
{{/load}}

I like using a :, but I think I might like => even better because it's similar to JS arrow function syntax

nebrelbug commented 4 years ago

As far as using @, right now Squirrelly takes all references that don't start with @ (so global references) and compiles them into options.[reference]. Ex. {{stuff}} is compiled into options.stuff (options contains the data that was passed into the template). Similarly, helper references are compiled into code that references a helper object passed down as a parameter.

I just had an idea, though. What if we have data references and scoped references. Scoped references, beginning with @, would just reference variables in the current scope, so this would include helper references and global variables. Data references would begin with an alphanumeric character (or maybe .) and they would compile to options.[datareference].

With this compiling strategy, {{@index}} would be compiled to index, and {{stuff}} would be compiled to options.stuff. This would let us have cleaner code and still be able to tell at a glance whether a reference is referencing the data or comes from a helper.

As far as your idea:

{{@import helper1, helper2}}
{{@global scripts, styles, tags}}

I really like it! Maybe we could change @ to something else though, or perhaps change the preceding character for scoped refs to something different.

clitetailor commented 4 years ago

Hi, @nebrelbug sorry for the long delay. Coming back to the issue after a long time, here is my few thoughts:

And for the load example, something like this:

{{~load(file) => value}}
  {{value}}
{{#catch => error}}
  {{error}}
{{/load}}

I like using a :, but I think I might like => even better because it's similar to JS arrow function syntax

After thinking for a while, i think this is the most suitable syntax to be used.

For the scope of this issue, there're two changes that are possible:

One thing i'm not very clear is: What does options do? Is there any thing options contains other than global variables? What is datareference is about?

What do you think?

nebrelbug commented 4 years ago

Hi @clitetailor, it's good to hear from you! I've actually changed my mind from before. I still think the syntax you quoted is best, but I think it's probably best that we don't use @ at all or distinguish between data references, scoped references etc.

options just holds data the user puts into the template.

On the bright side, I just finished a parser for this new syntax! I'll post it on GitHub within the next week or so, so keep posted. Now that that's complete, it should be pretty easy to write the rest of Squirrelly v8!

nebrelbug commented 4 years ago

Hi @clitetailor! Just wanted to let you know that the Squirrelly v8 betas are published with your idea for template syntax! Would you be ok if I added you to the contributors in the README?

clitetailor commented 4 years ago

Oh, that's great! I'd love to! 😄

nebrelbug commented 4 years ago

Great! @all-contributors please add @clitetailor for ideas and code!

allcontributors[bot] commented 4 years ago

@nebrelbug

I've put up a pull request to add @clitetailor! :tada:

nebrelbug commented 4 years ago

Alrighty @clitetailor, just added you! I'm going to close this issue now because it's implemented.

It's been a pleasure working with you and I hope you continue to contribute!