11ty / eleventy

A simpler site generator. Transforms a directory of templates (of varying types) into HTML.
https://www.11ty.dev/
MIT License
17.33k stars 495 forks source link

Eleventy reusable components shortcode #189

Closed tylersticka closed 6 years ago

tylersticka commented 6 years ago

I'm experimenting with using Eleventy to house some rapid prototypes.

As we continue to make prototypes, we plan to carve off certain shared elements that we can use as includes.

But it would also be nice to show those patterns on a page somewhere so we can visualize all those that are available to us.

In my head, I'd be able to have a file...

<button class="Button{% if class %} {{class}}{% endif %}">
  {% if label %}{{label}}{% else %}Label{% endif %}
</button>

That I could include in prototypes...

<h1>Behold! A button.</h1>

{% include patterns/button with label:'I am a button!' %}

But also iterate over for a simple "style guide" page...

<h1>All Our Patterns</h1>
{% for pattern in collection.patterns %}
  <article>
    <h2>{{ pattern.title }}</h2>
    {{ pattern.templateContent }}
  </article>
{% endfor %}

Possible? Crazy? Better suited for a plugin/shortcode/filter? Unintentional duplicate of #94?

Thanks in advance for your thoughts/assistance! 🙂

zachleat commented 6 years ago

A ha! Interesting. There’s a couple of different ways you could approach this but I think they may require some Eleventy changes to get it working like we’d want.

Use includes

Use content in the _includes directory. Stuff that lives there can’t have front matter (or template data files) and as such it would not be available in a tagged collection. The main benefit there is that it would work with the native include template library tags. This is a big benefit.

One thing you could try is to create your own collection in your eleventy config. You can make a collection out of anything—you don’t need to use eleventy’s tags feature for collections at all if you don’t want. See https://www.11ty.io/docs/collections/#advanced%3A-custom-filtering-and-sorting

Specifically something like this:

eleventyConfig.addCollection("posts", function(collection) {
    return collection.getFilteredByGlob("_includes/patterns/*.njk");
});

The data is still going to cause you problems there, though. In your collections loop you could just output {{ pattern.templateContent }} but the templateContent variable would likely not have the class or label data variables set. I’d be curious if the {% if label %}{{label}}{% else %}Label{% endif %} worked well enough for this purpose though.

Eleventy improvements

It sounds like it’d be good if Eleventy had some mechanism to set data in your config. This would allow you to loop over a glob of files and create an array of filenames from your patterns. Then you could loop over this array to use include tags the same as you would any other way. This overlaps with #155 but is probably closer to #184.

Harder to implement method, ymmv

The other thing you could do is use your own template environment and pass in your own includes directory. Example here:

https://www.11ty.io/docs/languages/nunjucks/#optional%3A-use-your-nunjucks-environment

This would allow you to change your FileSystemLoader to use include tags for things in a directory outside of the _includes directory (read: eleventy content templates that are in a collection). Read more about Nunjucks: https://mozilla.github.io/nunjucks/api.html#loader Sounds like you can use multiple loaders there, too, if you want to supplement _includes and not replace it.

zachleat commented 6 years ago

The other thing that might be worth investigating further is why relative includes aren’t working in Nunjucks (and maybe whatever other engine you’re using). https://github.com/mozilla/nunjucks/pull/349 suggests it should work.

{% include '../patterns/button.njk' %}

We have passing tests for this in Pug and EJS template types.

tylersticka commented 6 years ago

@zachleat Thank you so much for the suggestions! I'll give them a whirl and report back.

tylersticka commented 6 years ago

(I'll post new comments as I'm trying the different solutions.)

Use Includes

I tried this first because it seemed simplest.

Here is the pattern file, _includes/patterns/button/default.liquid:

---
title: Button
---
<button class="Button{% if class %} {{class}}{% endif %}"{% if disabled %} disabled{% endif %}>
  {% if label %}{{label}}{% else %}Button Label{% endif %}
</button>

So first, I tried including it like a normal include in prototypes/example1.liquid:

---
title: Example Prototype 1
---

{% include patterns/button/default %}

The include works just fine, but the frontmatter is not stripped from the output:

screen shot 2018-08-08 at 9 24 29 am

I went ahead and added your example to the Eleventy config (and re-started)...

module.exports = function (eleventyConfig) {
  eleventyConfig.addPassthroughCopy('src/content/fonts');
  eleventyConfig.addPassthroughCopy('src/content/images');

  eleventyConfig.addCollection('patterns', function(collection) {
    return collection.getFilteredByGlob('src/content/_includes/patterns/*.liquid');
  });

  return {
    dir: {
      input: 'src/content',
      output: 'dist'
    }
  }
}

And made this loop in index.liquid (feeling lazy):

<h2>Patterns</h2>
{% for pattern in collections.patterns %}
  <article>
    <h3>{{pattern.data.title}}</h3>
    Testing!
  </article>
{% endfor %}

But collections.patterns appears to be empty (does not output anything). If I'm reading the getFilteredByGlob method correctly, will this only work with items that have already been "seen" by Eleventy? It feels less like I'm filtering known items and more like I'm adding brand-new ones.

(I also tried _includes/patterns/*liquid as the glob but still no dice.)

tylersticka commented 6 years ago

Relative Include Paths

Moving the patterns directory out of _includes and referencing it with a relative path ({% include ../patterns/button/default %}) works for looping over the patterns:

screen shot 2018-08-08 at 9 51 26 am

But when included, the frontmatter remains in the output:

screen shot 2018-08-08 at 9 24 29 am
kleinfreund commented 6 years ago

The include works just fine, but the frontmatter is not stripped from the output:

It looks like it’s being processed as Markdown (note the triple dashes --- becoming em dashes ). Are includes supposed to have front matter in them? I don’t believe so.

Ah, right, @zachleat wrote above:

Use content in the _includes directory. Stuff that lives there can’t have front matter (or template data files) and as such it would not be available in a tagged collection.

tylersticka commented 6 years ago

It looks like it’s being processed as Markdown (note the triple dashes --- becoming em dashes —). Are includes supposed to have front matter in them? I don’t believe so.

I thought the same thing at first, but I think it only looks this way because of ligatures in the font we're using. The output still retains triple slashes, and here's what it looks like when I remove the font:

screen shot 2018-08-08 at 11 03 08 am
tylersticka commented 6 years ago

Okay, after playing around with shortcodes, filters, etc., I've come to the conclusion that what I want will actually give me trouble down the line (not because of Eleventy).

This project will eventually output some assets that will integrate with a tool that happens to use Liquid for templates. So the whole goal of some of this stuff is to have certain includes be DRY when consumed by that tool.

The issue with the Frontmatter being retained isn't in Eleventy, it's in Liquid. And the only way to get around it would be to write a replacement tag (or filter or shortcode) that would need to also be integrated with that eventual tool (since it won't be expecting frontmatter in partials either).

So in conclusion (in case others find this issue and are looking for advice), my summary is this:

Thank you @zachleat for your help. Sometimes you need to bang your head against a wall a few times before realizing the door's just a few feet over. 😅

zachleat commented 6 years ago

Thanks for thinking out loud here, this is super helpful feedback to see what you’re trying to do.

Maybe I can offer a bit of additional clarification.

Note that Front matter is a feature that is in Eleventy and is not provided by any templating engine. Front matter is processed by Eleventy separately using the gray-matter plugin before any Liquid/Nunjucks/etc processing takes place. Includes are a separate beast in that they reside solely in the template engine’s world. Eleventy does not do any pre-processing to includes and as such is not able to strip or process front matter.

Using collections to fake components/patterns isn’t really something that is going to work out of the box here, sounds like.

You can't use files in _includes as posts.

I’m not sure how you’re defining posts here but _includes files are not first-class templates (as described above).

You can include posts in other templates as long as the templating language supports relative paths. But any Frontmatter in included files will be retained in the output, since includes are part of the templating language (not Eleventy).

Yes, correct {% include %} will not process front matter.

To me it sounds like Eleventy needs a mechanism for reusable components in _includes. The pieces are pretty much all there already. Eleventy would expose a shortcode to include the component template file (and parse front matter out) and allow you to pass in data to override the front matter stuff.

Reusable component Bikeshed

_includes/components/patterns/button.liquid:

---
title: Button
---
<button class="Button{% if class %} {{class}}{% endif %}"{% if disabled %} disabled{% endif %}>
  {% if label %}{{label}}{% else %}Button Label{% endif %}
</button>
---

and then in a content template, use the shortcode to include:

---
title: Example Prototype 1
---

{% eleventy-component "patterns/button.liquid" "Title Override" %}
zachleat commented 6 years ago

This repository is now using lodash style issue management for enhancements. This means enhancement issues will now be closed instead of leaving them open.

The enhancement backlog can be found here: https://github.com/11ty/eleventy/issues?utf8=%E2%9C%93&q=label%3Aneeds-votes+sort%3Areactions-%2B1-desc+

Don’t forget to upvote the top comment with 👍!

zachleat commented 6 years ago

Also marginally related: #148

tylersticka commented 6 years ago

The "Reusable component Bikeshed" example would be amazing!

zachleat commented 6 years ago

@tylersticka haha how did this get so many upvotes in 5 minutes

tylersticka commented 6 years ago

@zachleat It's not like I shared this issue in the Cloud Four Slack or anything, I don't know why you'd make such accusations... 🙄

zachleat commented 2 years ago

Worth plugging the new Eleventy WebC plugin here that will perform (most of?) the requirements in this issue! https://github.com/11ty/eleventy-plugin-webc

tettoffensive commented 1 month ago

@tylersticka Curious how you would do this today. I'm just getting started with 11ty 3.0 and just want some basic components. I'm currently using liquid and markdown. But want a header, a button, etc.

tylersticka commented 1 month ago

@tettoffensive My earlier comment is still a pretty good summary of where my head is on this topic.

I haven't bugged @zachleat about this in a while because I'm no longer convinced that this is Eleventy's problem to solve.

If I'm working on a project that's self-contained in Eleventy and I want components, I use WebC.

But if I'm wanting to organize a set of template includes in Eleventy that I can build with Eleventy or in some other project using a template engine like Liquid, then any solution I implement has to work in Eleventy and whatever other platform I hope to use those same templates in.

So it's probably still a good idea to keep your documentation of a component separate from the template, and to just treat templates as you would any other include.

That said, I'd be curious if Virtual Templates might at least offer a means of easily associating meta data with those includes and making documentation generation a bit less manual. But I haven't had a good project for testing that out yet.