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

Template inheritance #17

Open
thekid opened this issue Oct 22, 2023 · 2 comments
Open

Template inheritance #17

thekid opened this issue Oct 22, 2023 · 2 comments
Labels
documentation Improvements or additions to documentation

Comments

@thekid
Copy link
Member

thekid commented Oct 22, 2023

Solution

The typical layout makes use of inline partials as follows:

layout.handlebars:

<!DOCTYPE html>
<html>
<head>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{{> title}}</title>
</head>
<body>
  <nav>
    {{> nav}}
  </nav>
  <main>
    {{> content}}
  </main>
  {{> scripts}}
</body>

home.handlebars:

{{#> layout}}
  {{#*inline "title"}}My title{{/inline}}
  {{#*inline "nav"}}
    My navigation
  {{/inline}}
  {{#*inline "content"}}
    My content
  {{/inline}}
  {{#*inline "scripts"}}
    <script>...</script>
  {{/inline}}
{{/layout}}

Templates using the layout must specify all inline templates, or else rendering will throw an exception. To make specifying a certain inline template optional, we can use partial blocks as follows:

   </main>
-  {{> scripts}}
+  {{#> scripts}}{{/scripts}}
 </body>

Another option is to use partial parameters, which works best for plain strings:

   <meta name="viewport" content="width=device-width, initial-scale=1">
-  <title>{{> title}}</title>
+  <title>{{#with title}}{{.}}{{else}}Default title{{/with}}</title>
 </head>

The home template would then pass this parameter via {{#> layout title="My title"}}. *On a side note, we could create a helper to shorten the with-else syntax to something along the lines of {{coalesce title "Default title"}}.

So basically, we can mimic the following:

abstract class Layout {
  public $title= 'Default title';
  public abstract function nav();
  public abstract function content();
  public function scripts() { return ''; }
}

class Home extends Layout {
  public $title= 'My title';
  public function nav() { return 'My navigation'; }
  public function content() { return 'My content'; }
  public function scripts() { return '<script>...</script>'; }
}

Limitations

However, this does not work for more than one level. For example, we cannot have some common navigation which is inherited for all pages, but then replaced by a specific one inside a page template: The common navigation would always be used due to how inline templates' scoping works.

One solution to this is to call optional templates in the common layer:

{{#*inline "nav"}}
  {{#> site-nav}}Default navigation{{/site-nav}}
{{/inline}}

This would render the Default navigation if the template does not specify a site-nav inline partial, its contents otherwise.

See also

@thekid thekid added the documentation Improvements or additions to documentation label Oct 22, 2023
@thekid
Copy link
Member Author

thekid commented Oct 22, 2023

To avoid naming clashes, one idea could be to use variables prefixed by an @ for passing

home.handlebars:

{{#> layout @title="My app"}}
  ...
{{/layout}}

layout.handlebars:

<title>{{#with @title}}{{.}}{{else}}Default title{{/with}}</title>

This notation has no special meaning here but would mimic that of https://handlebarsjs.com/api-reference/data-variables.html#data-variables - and decrease the risk of clashing with context variables or helpers.

@thekid
Copy link
Member Author

thekid commented Oct 22, 2023

To shorten the with-else construct, we could make use of the ^ form and simply use sections instead of the with helper as follows:

- {{#with title}}{{.}}{{else}}Default title{{/with}}
+ {{#title}}{{.}}{{^}}Default title{{/title}}

Following this, a short syntax that could be introduced might be:

{{title ^ "Default title"}}

(much like Laravel's {{ $name or 'Guest' }}, see also handlebars-lang/handlebars.js#1146)

This currently parses into a VariableNode(name: title, options: [Lookup(^), Quoted("Default Title")]) - we could add this without changing the parser itself - we would only overwrite the write method and have it react to the ^ operator (*and possibly others).

However, this would deviate from the official Handlebars language (where the above is a parse error!) and would increase the learning curve.


Another option without the need for modifying the parser would be a coalesce helper which:

- {{#with title}}{{.}}{{else}}Default title{{/with}}
+ {{coalesce title 'Default title'}}

This helper is trivial to implement and comes in handy in a number of situations.

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

No branches or pull requests

1 participant