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

Add a Component template tag #165

Open
treagod opened this issue Feb 18, 2024 · 9 comments
Open

Add a Component template tag #165

treagod opened this issue Feb 18, 2024 · 9 comments

Comments

@treagod
Copy link
Contributor

treagod commented Feb 18, 2024

Description

Let's enhance Marten's templating system by introducing a new component tag. Unlike the existing include tag, the component tag provides a more structured and robust approach for building reusable components within templates.

Background

While the include tag serves its purpose for certain scenarios, it falls short when it comes to building complex applications with reusable components. There is a need for a more sophisticated mechanism that ensures better encapsulation, parameterization, and error handling within components.

Proposed Solution

Introduce a component tag to Marten's templating system, which offers the following key features:

  • Components cannot access variables in the global template scope; they only have access to variables provided by context producers.
  • Components must specify which parameters they require, indicating whether a parameter is required or optional with a default value.
  • Instantiating a component without a required parameter would result in an error, providing better error handling and preventing runtime issues.
  • Components can define slots, allowing flexibility in content insertion and customization within the component. Slots shouldn't have access to the components scope, but to the outer scope.

Functionality

  • The component tag will be used to define reusable components within Marten templates.
  • Components will declare their required parameters and optional parameters with default values, ensuring clear communication of dependencies.
  • When instantiating a component, the template engine will enforce parameter requirements and handle errors gracefully if any required parameters are missing.
  • Components can define slots within their markup, allowing dynamic content insertion and customization.

Example Usage:

{# Define component  #}
{% define_component "button" with type: String %}
<button class="{{ type }}%>
  {% slot %}Default slot text{% endslot %}
</button>
{% enddefine_component %}

{# Use component  #}
{% component "button" with type: "primary" %}
  Slot Text
{% endcomponent %}

Discussion

Where are components stored? How are they loaded when they are referenced?

@ellmetha
Copy link
Member

Is this mechanism similar to Jinja macros?

Considering this example, I'm a bit uncertain about the value of this mechanism. As far as I can see, this example could be implemented very easily using a snippet and the include template tag.

For example, the snippet could look something like this:

<button class="{{ type }}">
  {{ text | default: "Default slot text" }}
</button>

And then you could use this snippet as follows:

{% include "my_snippet" with type="primary" %}
{% include "my_snippet" with type="primary", text="Slot text" %}

Why couldn't the include template tag be sufficient for this or similar use cases? Are there other use cases where include is clearly not sufficient?

The other aspect mentioned here is related to limiting access to variables inside included templates. This is something that is not supported by the present implementation of the include template tag, but we could add support for an only modifier (similarly to how Django does this). When only is set on an include template tag, the included template would only have access to the specified variables if any and eventually those coming from context producers:

{% include "my_snippet" with type="primary", text="Slot text" only %}

It is true that include template tags do not validate incoming variables. But if we were to address this need, I would argue that this is probably not something that should be defined in templates directly. Indeed, making it possible to define validation rules inside templates will always be super limited due to the nature of the Marten template parser and the constraints of defining validation rules in a templating language.

From my understanding of the issue, what we want to do here is essentially make it possible to include templates whose input variables are validated properly. What about making it easy to define Crystal abstractions that allow doing just that by generating template tags that render these templates (something that is called custom inclusion tag in Django) while performing the intended validation? For example, defining such "inclusion tags" for Marten could look something like this:

class MyButtonTag < Marten::Templates::InclusionTag
  template_name "tags/button.html"

  field :type, :string
  field :text, :string, default: "My button name"
end

And in a template, you would use the corresponding automatically generated template tag:

{% my_button type: "primary", text: "Custom text" %}

The advantage of doing this in Crystal is that it becomes possible to leverage Marten::Core::Validation and that way end users can easily define any custom validation rules they want (something that wouldn't be possible to achieve with validation rules defined in the Marten template language!).

But again, this (having validated inclusion tags) seems like a niche use case given that the include tag can already be used to define small "components" very easily. I think this could be implemented down the line though! But I would see this being defined in Crystal to allow for maximal customization of validation (since it seems this is what we want to achieve here).

@treagod
Copy link
Contributor Author

treagod commented Feb 19, 2024

Indeed {{ text | default: "Default slot text" }} seems to be sufficient for text slots. But what is the case for more complex HTML? Is there a mechansim? Happy to know

For the slots I was thinking that marten could use named slots in the future.

@ellmetha
Copy link
Member

ellmetha commented Feb 19, 2024

Do you have an example where you would need to include complex HTML in included templates? I guess a card with headers and bodies that can be overridden would be such an example.

<div class="card-component">
    <div class="header">
        {% slot "header" %}Card header{% endslot %}
    </div>
    <div class="body">
        {% slot "body" %}This is a <b>card</b> body{% endslot %}
    </div>
</div>

I have been using templating languages similar to Marten's for quite a long time and never found myself wishing for something like that so I am curious to understand more about this use case. At first sight, it looks like something inherited from concepts coming mainly from Vue.js?

But even trying to implement the example above with include tags would probably be doable if we were to add some helper tags and syntactic sugar like a capture tag for capturing a template block's output and assigning it to a variable.

For example:

<div class="card-component">
    <div class="header">
        {% if header %}{{ header }}{% else %}Card header{% endif %}
    </div>
    <div class="body">
        {% if body %}{{ body }}{% else %}This is a <b>card</b> body{% endif %}
    </div>
</div>
{% capture card_body %}
This is a <b>card</b> body with lots of custom HTML
{% endcapture %}

{% include "snippets/card.html" with body=card_body %}

@notramo
Copy link
Contributor

notramo commented Feb 20, 2024

The upcoming Svelte 5 snippets are more flexible than Vue (and Svelte 4) slots, and the capture example posted by @ellmetha seems very similar to it. I think snippets are a well thought-out feature, so it would be worth copying over to Marten. It would also make it easier to learn for Svelte developers.

@notramo
Copy link
Contributor

notramo commented Feb 20, 2024

@ellmetha The with param="value" for the include tag is hard to find in the documentation. It should be mentioned in template introduction, and maybe demonstrated in the tutorial.

Using include and with is sufficient for some of the use case (when I opened the discussion, I did not know about with - I only noticed it in your comments). If snippets were added, that would be sufficient for nearly all of the use cases.

However, for complex component logic, defining InclusionTag Crystal classes is a good idea. Blade components follow a similar approach by having a PHP class for component logic and a Blade template for rendering.

@ellmetha
Copy link
Member

Improvements were shipped for the documentation. I think the capture template tag may be interesting to add regardless of this discussion as it would provide a way to easily assign the content of a block to a variable (something that is not possible presently).

Otherwise, I am going to look into these svelte snippets and think a bit more about this idea of inclusion tags. Let's continue this discussion!

@notramo
Copy link
Contributor

notramo commented Feb 23, 2024

Snippets could replace layouts (extend and block) too, effectively turning layouts into components.

Here are some examples of how the concept of Svelte 5 snippets would translate to Marten templates.
It is similar to Jinja macros, so the macro keyword could be used instead of snippet, and call instead of render. However, since Jinja includes Python which is a full featured language, but Marten templates are more primitive, I would like to avoid the call name, as it's not a function call, but similar behavior to the Marten::Handler#render method, so calling it render would point out the similarities.

This differs from the capture tag (specified in #170) in that while capture assigns the rendered content of the template, snippet assigns a renderable template which can be re-rendered multiple times with different input.

Using current, extend-based layouts:

{% extend "base.html" %}

{% block title %}Hello{% endblock %}

{% block content %}
  <p>Hello world!</p>
{% endblock %}

Using new, snippet-based layouts (note that main content is named children instead of content)

{% snippet title %}Hello{% endsnippet %}

{% snippet children %}
  <p>Hello world!</p>
{% endsnippet %}

{% include "base.html" with children=children title=title %}

Even better, snippets defined in descendant content are automatically assigned to the context as variables:

{% include "base.html" %}
  {% snippet title %}Hello{% endsnippet %}

  {# direct content is automatically assigned as a snippet to the 'children' variable #}
  <p>Hello world!</p>
{% endinclude %}

base.html when using snippets (note that main content is named children instead of content):

<html>
  <head>
    <title>{% render title %}</title>
  </head>
  <body>
    {% render children %}
  </body>
</html>

Rendering default when snippet is not provided:

{% if title %}
  {% render title %}
{% else %}
  <h1>No title provided!</h1>
{% endif %}

Providing extra context variables to the rendered snippet (similarly to include ... with ...)

{% render table_row with row_data=record %}

This is useful for e.g. rendering tables, and any other situation where a single snippet can be rendered multiple times inside the same component, with different data (e.g. using for statement).

@ellmetha
Copy link
Member

Snippets could replace layouts (extend and block) too, effectively turning layouts into components.

Template inheritance and the use of extend and block will remain as is. These tags provide a simple and convenient mechanism for defining templates that inherit from each other and I don't think that these should change at all. What you are proposing is a complete change of the way template inheritance work and I'm afraid I have to disagree with it: Marten is opinionated on the fact that child templates should always explicitly define the parent template they extend while parent templates should only define blocks that can be overridden by child templates. This approach has been carefully crafted and reflects the design philosophy of the framework. It would take considerable persuasion for me to reconsider it.

Let's concentrate our discussion specifically on the matter of included templates.

@notramo
Copy link
Contributor

notramo commented Feb 23, 2024

The above code examples could be applied to any components, not only layout components, but other use cases too. Obviously, the extend and block tags don't have to be removed, because it has it's own use case, however, components would provide a more flexible alternative to template inheritance. One could even mix the two approach in the same project, using extend based layouts for some pages, while using component based for others.
The purpose for the above code examples is to demonstrate the flexible capabilities of snippets, not to propose removing layout inheritance.

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

No branches or pull requests

3 participants