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

Macro tag #580

Open
emichael opened this issue May 29, 2015 · 22 comments
Open

Macro tag #580

emichael opened this issue May 29, 2015 · 22 comments

Comments

@emichael
Copy link

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
Copy link
Contributor

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

@emichael
Copy link
Author

@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
Copy link
Contributor

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
Copy link

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
Copy link
Author

@mikebridge Looks great to me!

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

@sdn90
Copy link

sdn90 commented Sep 4, 2016

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
Copy link
Contributor

fw42 commented Sep 12, 2016

@Thibaut can you comment on that?

@Thibaut
Copy link
Contributor

Thibaut commented Sep 12, 2016

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
Copy link

cshold commented Sep 12, 2016

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
Copy link
Author

emichael commented Sep 12, 2016

@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
Copy link
Contributor

kainjow commented Sep 12, 2016

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

@sdn90
Copy link

sdn90 commented Sep 12, 2016

@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
Copy link
Contributor

Thibaut commented Sep 13, 2016

@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
Copy link
Author

@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
Copy link
Contributor

Thibaut commented Sep 13, 2016

@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
Copy link

dyspop commented Sep 13, 2016

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
Copy link
Author

@dyspop Liquid <- Jekyll <- Github Pages

@dyspop
Copy link

dyspop commented Sep 13, 2016

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
Copy link
Author

emichael commented Sep 13, 2016

@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
Copy link

dyspop commented Sep 13, 2016

not directly, no... fair enough.

@numito
Copy link

numito commented Jan 23, 2018

@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
Copy link

kj commented Apr 3, 2018

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.

warpfork added a commit to ipld/ipld that referenced this issue Apr 22, 2021
This is... a bit of a journey.

First of all, it's caused me to switch to njk templates instead of liquid.
In large part, this is because liquid doesn't seem to want to have recursion features,
and that makes a nonstarter for handling things like a menu that can have variable depth.
(See Shopify/liquid#580 ; from this, I gather that it's
not just that liquid doesn't have a feature for this, it's that they don't *want* one.
I understand the desire to avoid recursion (and thus turing-completeness), but it's unhelpful here.)
In smaller part, it's just going with the flow of eleventy docs, which seem to prefer njk.
Ultimately, the strength of my preference in *any* direction would be weak,
but liquid crossed a hard-disqualifying line, so, let's try njk now.

Okay.  Moving on.  All that was just to describe why one file is renamed.

About this nav plugin:

- It works!

- I like having fairly explicit control over what ends up in nav,
  and how its labeled.  (I expect we'll have quite a few pages, so,
  being able to be explicit about what's listed and how will be
  import to be able to control the signal-noise of the display.)

- Ordering in the list is controlled by numbers in the per-page frontmatter.
  I'm not really a fan of this at all.  I'd rather nudge around an overall list.
  Oh well; this isn't critical enough to spend a lot of time on.

- I'm super unthrilled by having to repeat the parent key a lot.
  I think this navigation plugin is trying to support the ability to
  have navigational hierarchy that's totally divorced from directory structure...
  but _I don't really want that_, and it makes basic use more redundant.

- I feel like I might be able to improve the above by computed properties
  based on the page URL.  But I haven't discerned an incantation for that yet.

- One can also use the `foo/foo.11tydata.json` file to set the nav parent
  property for the whole directory... and maybe I'll take that angle...
  but I'd still find this relatively high friction and a bit disappointing.

- I'm a tad worried this nav thing is gonna get me in trouble when it collides
  with the intention to sometimes have the same page names in parallel in
  the docs and the specs directory trees (e.g. for those things that need
  both a formal spec as well as a human-friendly explainer doc).
  The likely result will be I'll stop using human-readable keys for the
  nav config, and use separate title properties...?  Mmfh.

- I think I'm going to be using CSS on the nav menu *heavily* to control
  the legibility and noise level of things.  Probably including setting
  all elements beyond a certain depth to display-none.  (And then adding
  back other css selectors that can toggle them back into visibility.)
  Future work.

So it feels like I'm going to be reinventing a good bit of kludge to
make this navigation plugin work in the simple way that I want.
But maybe that's okay.  Let's see how it goes.

Worst case scenario?  I might make a totally static nav menu, by hand
(with just a little template power sprinkled on for the you-are-here
highlighting based on the current page url).
I'd like to have data that can be used to generate group listings, too
(for example, as you can see being formed for the "known codecs" page),
but maybe that can be a separate problem anyway (using "categories"
instead of a nav tree for that might be appropriate)).

Anyway.  Let's see how it goes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests