Shopify / liquid

Liquid markup language. Safe, customer facing template language for flexible web apps.
https://shopify.github.io/liquid/
MIT License
11.11k stars 1.39k forks source link

Macro tag #580

Open emichael opened 9 years ago

emichael commented 9 years ago

Jinja2 has the incredibly useful macro tag which allows quick and efficient parameterized code reuse. I've looked and asked around on Stack Overflow, and it doesn't look like there is anything equivalent in Liquid. There really needs to be.

As it stands, I'm having to write horrendous 400 character lines with no whitespace and lots of repeated elements (just with different variables).

carsonreinke commented 9 years ago

This could be combination of the capture and raw tag to create the macro and a change to include to use the macro.

emichael commented 9 years ago

@carsonreinke Sorry, but just for my benefit, are you suggesting a way that I could effectively have a macro in Liquid as-is or a way that Liquid could be modified?

carsonreinke commented 9 years ago

Liquid out of the box, no. Customizations to the include tag could work, but I was just really identifying the concept behind how a macro tag could re-use some existing components.

mikebridge commented 9 years ago

FWIW, I've implemented this in a soon-to-be-released C# port of Liquid:

   {% macro mymacro arg1 arg2 %}
      You said '{{ arg1 }}'.  Also '{{arg2}}'.
    {% endmacro %}

then

    {% assign myvar = "there" %}
    {% mymacro "hello" myvar %}
-->You said 'hello'. Also 'there'.

My implementation is a simplified version of "include", but without 'for' or 'with', and with positional rather than named arguments.

emichael commented 9 years ago

@mikebridge Looks great to me!

Any status on getting this included in a release of the main version?

sdn90 commented 8 years ago

Is there interest from maintainers in adding this in if someone were to work on it?

IMO, the macro tag is important to add. My use case is for Shopify themes. Maintaining logic in themes is extremely hard. In many cases I can't use Javascript because of SEO concerns. This usually leads to 100s of lines of imperative mess.

Being able to "componentize" my UI would result major reduction in duplicated code. Creating variations of templates using snippets is painful because I can't pass children elements to {% include %}.

fw42 commented 8 years ago

@Thibaut can you comment on that?

Thibaut commented 8 years ago

I don't think Liquid should support global macros (shared between templates), because it would significantly increase complexity (allowing anyone to basically make their own language on top of Liquid, thereby increasing the learning curve for anyone editing templates they didn't create), and add more dependencies between templates (load order).

Would local macros be of any use? I'm not convinced they'd be a good tradeoff against complexity and performance.

I can't pass children elements to {% include %}.

@sdn90 Not sure what you mean by that. Snippets inherit their parent template's variables. Doesn't that solve your problem?

cshold commented 8 years ago

You can essentially do the same thing with named parameters now:

{% include 'module', arg1: 'Hey', arg2: 'Thibaut' %}

@emichael can you create a gist of your 400 character lines example and what the alternative would look like with a macro?

emichael commented 8 years ago

@cshold I think that it wouldn't look all that different? There might be limitations to include I'm not remembering. It's been a while. One benefit of macros in Jinja is that they can have kwargs with defaults; not sure if that's true of include.

The biggest difference is that with include, you're separating the definition from the use, which isn't always what you want. Think about the functions in your favorite programming language. If every function had to go in a different file, that would be a pain, right?

In Liquid, you should be able to define and use a subroutine in the same file, too. It lets template designers keep closely coupled bits of template all in the same place and doesn't force large projects to have huge numbers of snippet files lying around.

kainjow commented 8 years ago

Just chiming in to say I too think this would be very useful.

sdn90 commented 8 years ago

@Thibaut http://jinja.pocoo.org/docs/dev/templates/#call

Let's say I want to create a Bootstrap component library. Using {% includes %}, it's hard to nest/compose components

Bootstrap Panel http://getbootstrap.com/components/#panels-basic (note: never used Jinja before, don't know if this is valid)

Panel Macro

{% macro panel(color='default') %}
  <div class="panel panel-{{ color }}">
    <div class="panel-body">
      {{ caller() }}
    </div>
  </div>
{% endmacro %}

Usage

{% call panel(color='success') %}
  <h1>Successfully added to cart</h1>
  <a href="/cart" class="btn btn-success">View cart</a>
{% endcall %}

Expected output

<div class="panel panel-success">
  <div class="panel-body">
    <h1>Successfully added to cart</h1>
    <a href="/cart" class="btn btn-success">View cart</a>
   </div>
</div>
Thibaut commented 8 years ago

@sdn90 This isn't something we're interested in supporting in Shopify, because it would significantly increase the complexity of templates.

When reading a template you'd see something completely different from the output. If you wanted to make a change to a theme you didn't create, you'd have to search for macros, figure out what they do, etc. Every theme would have a learning curve, and what you'd learn in one theme wouldn't apply to other themes. You also couldn't easily copy/paste code from one theme to another anymore.

Global macros would also introduce dependencies between templates that would be hard to resolve, as well as potential conflicts. {% include %} tells Liquid exactly where to find the piece of code you want to include. A macro's name doesn't tell Liquid anything about where that macro is defined. So then you end up adding {% include 'macros' %} to every template, complexity goes up another level, and performance takes a hit.

emichael commented 8 years ago

@Thibaut Sorry, but that's complete nonsense.

First of all, providing named functions (a feature of every modern programming language I can think of) doesn't mean the user has to use them. Secondly, you already have named functions. They just have the strange property that they have to go in a file of the same name. It is already the case in Liquid that the output of a template doesn't correspond 1-1 with the template itself (you also have variable assignment and usage). Thirdly, complexity isn't a function of the programming language being used; it's a function of the problem being solved. And as it stands, when users of Liquid need to manage some inherent complexity in certain problems, problems that require nested function calls etc., the only thing they can do is create a slurry of tiny snippet files instead of packaging the related functionality into one file (a much better way to manage complexity).

Also, about performance..... include is a disk read. call is an in-memory operation. Why would turning every call into a disk operation be faster?

You seem to have a certain idea about how this would be implemented, and I think it's a bit of a strawman. There are lots of ways to manage namespacing, conflicts, etc. But even something basic like the C preprocessor's include would be better than what you have.

Thibaut commented 8 years ago

@emichael Liquid is not a modern programming language, nor does it want to be. Liquid is designed to be approachable to (and usable by) a wide range of users, from designers to front-end developers to even non-technical people. Templates need to be easy to understand. Repeated code is perfectly ok in that context.

Adding global macros would be akin to encouraging every developer to make their templates more complex. We do not want this in Shopify, sorry. Keeping the majority of templates as simple as possible in the eyes of the many is more important to us than making the few templates that are doing complex things a bit simpler in the eyes of a technical audience.

To be clear, I'm not arguing that macros are not a good solution to manage complexity — only that they would significantly increase the average complexity of templates, and that is what matters to us.

dyspop commented 8 years ago

If you're really interested in macros, why not simply extend your instance of liquid?

https://github.com/Shopify/liquid/wiki/liquid-for-programmers#create-your-own-tags

emichael commented 8 years ago

@dyspop Liquid <- Jekyll <- Github Pages

dyspop commented 8 years ago

if you can stand to store your macros in the same file you can do:

{% capture panel %}
    <div class="panel panel-|color|">
      <div class="panel-body">
        |caller|
      </div>
    </div>
{% endcapture %}

{% capture caller %}
  <h1>Successfully added to cart</h1>
    <a href="/cart" class="btn btn-success">View cart</a>
{% endcapture %}

{{ panel | replace: '|color|', 'success' | replace: '|caller|', caller }}

I didn't run this code, but I use this technique (as a concept) extensively, with a lot of enhancements that make it more manageable than calling like this, but this is the core concept.

emichael commented 8 years ago

@dyspop That's not what I'm suggesting. You can only do simple string replacement with that technique. You can't feed it more complex data structures, and you can't do any sort of control flow based on the "inputs."

dyspop commented 8 years ago

not directly, no... fair enough.

numito commented 6 years ago

@Thibaut I can see you are not creating complex templates and you never run in the issue of really missing macros, not having macros is a pain when you have a complex layout, you end up having several snippets with never ending if and capture or assign. The templates becomes unreadable. Cut and paste is not your friend. It's a pity because having simple macros would improve readability and have no performance impact compared to cascading if/capture/assign etc.. No need to figure out namespace or anything, you just include your macros in your base template, and then you can call them in your pages.

kj commented 6 years ago

In my experience, the lack of macros in fact creates far more complexity in the readability of templates, as theme authors frequently have to resort to hacky, complex, and incomprehensible workarounds to get the job done where a short, simple macro could have solved the problem perfectly. I don't buy this argument that readability would suffer. I think the opposite would be the case. There are some truly horrible hacks (out of necessity) to be found in some third party themes that probably even the original author can barely follow. That hurts all of us who have to understand that code to make customizations for clients.