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

How to address tags within template or rather how to check with Nunjucks if certain tag exists? #524

Open
ssgstarter opened this issue May 8, 2019 · 20 comments

Comments

Projects
None yet
5 participants
@ssgstarter
Copy link

commented May 8, 2019

My solution for this (third line) is:

{% if title === "Home" %}
    <div class="home">
{% elseif tags.includes("tag1") %}
    <div class="tag1">
{% else %}
{% endif %}

I am wondering why this does not work:

{% if title === "Home" %}
    <div class="home">
{% endif %}
{% if tags.includes("tag1") %}
    <div class="tag1">
{% endif %}

In general, is the above solution correct?
I am asking this because I get the following error when I extend the above code like this:

{% if title === "Home" %}
    <div class="home">
{% elseif tags.includes("tag1") %}
    <div class="tag1">
{% elseif page.outputPath === "_output/subfolder/index.html" %}
    {% include 'layouts/include.njk' %}
{% else %}
{% endif %}
Error: Unable to call `tags["includes"]`, which is undefined or falsey (Template render error):
@kleinfreund

This comment has been minimized.

Copy link
Contributor

commented May 8, 2019

Where does the tags variable come from?

@ssgstarter

This comment has been minimized.

Copy link
Author

commented May 8, 2019

As part of the Front Matter.

@Ryuno-Ki

This comment has been minimized.

Copy link

commented May 8, 2019

Could you share that part?
E.g. is it a list [] or a dict {} (in Python parlance).

@ssgstarter

This comment has been minimized.

Copy link
Author

commented May 9, 2019

On some pages I use single tags:

---
tags: tag1
---

on some others multiple tags on multiple lines;

---
tags: 
    - tag1
    - tag2
    - tag3
---

What I search for is a safe 11ty and Nunjucks solution to check if "tags" contains "tag1" (in Liquid parlance).

@ssgstarter ssgstarter changed the title Nunjucks conditional operator if tag exists? How to address tags within template or rather how to check with Nunjucks if certain tag exists? May 9, 2019

@Ryuno-Ki

This comment has been minimized.

Copy link

commented May 9, 2019

Can't reproduce.

Using a directory structure like this:

.
├── _includes
│   └── tag.njk
├── index.md
├── package.json
├── package-lock.json
├── _site
│   ├── index.html
│   └── test_with_tags
│       ├── foreign
│       │   └── index.html
│       ├── multiple
│       │   └── index.html
│       └── single
│           └── index.html
└── test_with_tags
    ├── foreign.md
    ├── multiple.md
    └── single.md

with _includes/tag.njk defined as

---
---
{% if title === "Home" %}
    <div class="home">Home</div>
{% endif %}
{% if tags.includes("tag1") %}
    <div class="tag1">
{% else %}
    <div>
{% endif %}
Tags: {{ tags }}
{{ content | safe }}
    </div>

index.md being a plain Hello World, single.md being

---
layout: tag
tags: tag1
---
I'm single

and multiple.md as

---
layout: tag
tags:
  - tag1
  - tag2
---
I'm multiple.

resp. foreign.md as

---
layout: tag
tags: tag2
---
I don't have tag1

yields the following HTML:

    <div class="tag1">

Tags: tag1
<p>I'm single</p>

    </div>
    <div class="tag1">

Tags: tag1,tag2
<p>I'm multiple.</p>

    </div>
    <div>

Tags: tag2
<p>I don't have tag1</p>

    </div>
@Ryuno-Ki

This comment has been minimized.

Copy link

commented May 9, 2019

On a side note: {% elif "tag1" in tags %} might already do the trick.

@ssgstarter

This comment has been minimized.

Copy link
Author

commented May 13, 2019

Thank you very much, @Ryuno-Ki for notes. Back in the house now.

Indeed, the deeper I get the stranger things become ...

To sum up:

{% elseif "tag1" in tags %}                {# works #} 
{% if "tag1" in tags %}                    {# crashes #} 
{% elseif tags.includes("tag1") %}         {# works #} 
{% if tags.includes("tag1") %}             {# crashes #} 

But, with both working examples, it seems that any further conditional have to be in the same syntax; for the first example:

{% elseif "tag1" in tags %}                {# works #} 
   do something
{% elseif "tag9" in tags %}                {# works #} 
   do something else
{% elseif "tag1" in tags %}                {# works #} 
   do something
{% elseif page.outputPath === "output/path/to/file/with/tag9" %}                {# crashes #} 
   do something else
@Ryuno-Ki

This comment has been minimized.

Copy link

commented May 13, 2019

Hm, I recall that some variables behave differently on the home page vs. all other pages.

That is title is only set on the homepage and available as page.data.title (from frontmatter) on all others.

Maybe check for existence of tags first and wrap the inclusion checks inside?

@ssgstarter

This comment has been minimized.

Copy link
Author

commented May 14, 2019

Very interesting aspects, @Ryuno-Ki.

Given the documentation, I have thought so far that only title does exist, but not page.data.title resp. let us call it item.data.title exists:

                                                    {# works #}

{% if title === "AnyPageTitle" %}                   {# to check any page title #}
   do something
{% elseif "tag1" in tags %}  
   do something
{% else %}
    do something
{% endif %}
                                                    {# works not for tags, but works for title also #}
                                                    
{% for item in collections.all %}                   {# e.g. looping through all collections #}
    {% if item.data.title === "AnyPageTitle" %}     {# to check any page title #}
        do something
    {% elseif item.data.tags === "tag1" %}
        do something
    {% else %}
        do something
    {% endif %}
{% endfor %}
                                                    {# crashes #}

{% for item in collections.all %}                   {# e.g. looping through all collections #}
    {% if item.data.title === "AnyPageTitle" %}     {# to check any page title #}  
        do something
    {% elseif "tag1" in tags %}  
        do something
    {% else %}
        do something
    {% endif %}
{% endfor %}

Even though it is expected, the third example does not work for some reasons. these could be:

  1. As for me, it is a little bit confusing when to use which title variable.
  2. Especially tags are so tricky because "they" could be a string or an array.
@jevets

This comment has been minimized.

Copy link

commented May 14, 2019

I'm pretty sure Eleventy casts tags as arrays, even if written as strings in front matter yaml. (Can't find that in the docs atm, but quite sure I read it somewhere in there.)

Nunjucks isn't straight javascript, so I'd be surprised if {% tags.includes('tag') %} works at all. (edit: Hence the error about tags["includes"])

  • Use item.data.title when item is a Collection item.
  • Use title directly when in the file that defines title in its front matter
  • Use title in an included file when it's included from a template file that defines front matter. (The included file acts as if it were written in the original template file in the first place; it inherits the current variable scope of its caller.)

You could set up a nunjucks or universal filter to check if tag exists on the collection item.

Or just loop through the items tags and set a flag if the tag in question exists.

{% set found = false %}

{% for item in collections.all %}
  {% for tag in item.data.tags %}
    {% if tag === 'tag1' %}{% set found = true %}{% endif %}
  {% endfor %}
  {% if found %} here we are tag1 {% endif %}
{% endfor %}
@ssgstarter

This comment has been minimized.

Copy link
Author

commented May 15, 2019

Very useful hints @jevets. Thank you.

The conclusion I draw from this excursion on the whole is that I am tending to get back to where I was starting from, i e. to separate conditions into several layout templates when it comes to produce something concrete.
The experiment was to find a way to do all this with one layout template (and several conditions).

The puzzling thing is that some situations work unexpectedly and some do not work with or without errors. To give some insights from a beginner`s perspective, some reasons for giving up are:

  • tags seem to be contradictory in syntax and use (string vs. array, conditional vs. loop)
  • title seems to be contradictory in syntax and use (layout vs. include, conditional vs. loop like collection.all)
  • dimmish with layouts when to use and/or mix conditionals and/or loops

And in addition:

  • {% tags.includes('tag') %} works without errors and fails with and without errors in various situations even it is javascript within Nunjucks

I like Eleventy very much. It is such an elegant and powerful tool.

As a beginner, you can achieve success quickly, but you can also fail quickly against the background of the documentation's status quo.

Much more transparency is needed here to let the diamond shine.

@danfascia

This comment has been minimized.

Copy link

commented May 16, 2019

I don't think there is a good native way to do this.

I use this filter:
https://github.com/danfascia/radiologymasters/blob/master/11ty/filters/includes.js

@ssgstarter

This comment has been minimized.

Copy link
Author

commented May 17, 2019

@danfascia:
This is what I was talking about in my last posting.
Your file does not exist.

@jevets

This comment has been minimized.

Copy link

commented May 17, 2019

@danfascia Is that a private repo, maybe?

@danfascia

This comment has been minimized.

Copy link

commented May 19, 2019

Sorry, it is a private repo, here is the filter code to be placed in includes.js and imported in via the eleventy config as a universal filter

/**
 * Select objects in array whose key includes a value
 *
 * @param {Array} arr Array to test
 * @param {String} key Key to inspect
 * @param {String} value Value key needs to include
 * @return {String} Filtered array
 *
 */
module.exports = function (arr, key, value) {
  return arr.filter(item => {
    const keys = key.split('.');
    const reduce = keys.reduce((object, key) => {
      return object[key];
    }, item);
    const str = String(reduce);

    return (str.includes(value) ? item : false);
  });
};

Here is an example of how to use it, since I always find that the missing piece of the 11ty docs.

{% set postslist = collections.cases | includes("data.modalities", tag ) %}
          {%- if postslist.length > 0 -%}
            {% for case in postslist %}
            {% include "case-list-item.njk" %}
            {% endfor %}
         {%- endif -%}
@ssgstarter

This comment has been minimized.

Copy link
Author

commented May 20, 2019

Looks interesting, @danfascia.

In this constellation, how would the important Universal filter part look like? I guess:

eleventyConfig.addFilter( "INCLUDE_FILTER", function( arr, key, value ) {
    return arr.filter( item => {
        const keys = key.split( '.' );
        const reduce = keys.reduce( ( object, key ) => {
            return object[ key ];
        }, item );
        const str = String( reduce );

        return ( str.includes( value ) ? item : false );
    } );
} );

If so, include.js would become redundant, would it not?

May I ask you to explain the {% set %} line (in regard to the Universal filter and the general question of this thread). I have never seen such a notion before. Is there no need to close it by {% endset %}?

@jevets

This comment has been minimized.

Copy link

commented May 21, 2019

@ssgstarter {% set %} sets a variable into the current context.

Commonly used in two ways. See the nunjucks docs for more.

{% set foo = "some string" %}

{% set bar %}
  some string and the contents of an `include`
  {% include "file.njk" %}
{% endset %}

To import the filter, you'd need to do something like this:

// .eleventy.js

const includesFilter = require("./path/to/your/copy/of/includes.js")

module.exports = function (eleventyConfig) {
  eleventyConfig.addFilter('includes', includesFilter)
  // you could name it whatever you want, i.e.
  // eleventyConfig.addFilter('has_tag', includesFilter)
}
@ssgstarter

This comment has been minimized.

Copy link
Author

commented May 21, 2019

Thank you, @jevets for elucidations. Especially the second part of your hint.

{% set %} in general is clear, I mean rather the whole line:

{% set postslist = collections.cases | includes("data.modalities", tag ) %}
@jevets

This comment has been minimized.

Copy link

commented May 21, 2019

@ssgstarter This is an example of how you could use the filter. See nunjucks filter docs too.

If you'd named the filter something else when registering it with Eleventy, you'd call it differently.

If you did this:

eleventyConfig.addFilter('has_tag', includesFilter)

Then you'd use it like this:

{% set postslist = collections.cases | has_tag("data.tags", "tag1" ) %}

In @danfascia's example usage:

  • First you're creating a variable called postslist and assigning it to Eleventy's filtered collection of items (collections.cases in @danfascia's case, probably collections.posts in many others' cases). The first half of the line, before the | character.

{% set postslist = collections.cases %}

  • But then you're sending that collection through the new includes filter (or has_tag filter if you named it that), which filters the collection to remove any items that don't have the tag ("tag1" in the above example; the tag variable in @danfascia's usage example). The second half of the line, after the | character.

{% set postslist = collections.posts | includes("data.tags", "tag1") %}

  • Then you're simply looping over the resulting filtered collection of items, using a variable called post to represent the current iteration, and including a file that expects a post variable.
{% for post in postslist %}
  {% include "post.njk" %}
{% endfor %}

I think you just need to spend some time getting to know nunjucks filters. Have a look through nunjuck's built-in filters to get the idea. The includes filter is a custom filter.


A potential example for your use case:

{% if title === "Home" %}
    <div class="home">
{% elseif tags | includes(tags, "tag1") %}
    <div class="tag1">
{% endif %}
@ssgstarter

This comment has been minimized.

Copy link
Author

commented May 22, 2019

Awesome, @jevets! 🥇 Thank you so much for these deep explanations.

Now I can read and understand the different meaning of includes in @danfascia's 🥇 great example (who I also want to say thank you for sharing these excellent snippets).

@zachleat I can imagine that other people (especially beginners like me) would be glad to find their both revealing aspects in the official documentation. By the way, I am a big fan of your work!


What remains is somehow Faustian: to use or not to use? Maybe a question of further use cases ...

related to @Ryuno-Ki's great hint: somehow pure and legible without any custom filter etc.

{% elseif "tag1" in tags %}

or related @danfascia's little bit extensive code and much harder to read

{% elseif tags | includes(tags, "tag1") %}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.