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

Closed
ssgstarter opened this issue May 8, 2019 · 23 comments

Comments

@ssgstarter
Copy link

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

Where does the tags variable come from?

@ssgstarter
Copy link
Author

As part of the Front Matter.

@Ryuno-Ki
Copy link
Contributor

Ryuno-Ki commented May 8, 2019

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

@ssgstarter
Copy link
Author

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

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

Ryuno-Ki commented May 9, 2019

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

@ssgstarter
Copy link
Author

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

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

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

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

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

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

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

@jevets
Copy link

jevets commented May 17, 2019

@danfascia Is that a private repo, maybe?

@danfascia
Copy link

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

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

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

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

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

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") %}

@zachleat
Copy link
Member

Couple of things here:

Most importantly, Eleventy transforms tags to always be an array. You should never encounter a string tags property. This may be a bit confusing in your tests above because JavaScript includes both a String and Array includes method.

Secondly, based on some of the discussion in this issue I added all of these tests and they all pass fine:

test("Nunjucks Test if statements on arrays (Issue #524)", async t => {
  let tr = new TemplateRender("njk", "./test/stubs/");

  t.is(
    await tr.render("{% if 'first' in tags %}Success.{% endif %}", {
      tags: ["first", "second"]
    }),
    "Success."
  );

  t.is(
    await tr.render("{% if 'sdfsdfs' in tags %}{% else %}Success.{% endif %}", {
      tags: ["first", "second"]
    }),
    "Success."
  );

  t.is(
    await tr.render("{% if false %}{% elseif 'first' in tags %}Success.{% endif %}", {
      tags: ["first", "second"]
    }),
    "Success."
  );

  t.is(
    await tr.render("{% if tags.includes('first') %}Success.{% endif %}", {
      tags: ["first", "second"]
    }),
    "Success."
  );

  t.is(
    await tr.render("{% if tags.includes('dsds') %}{% else %}Success.{% endif %}", {
      tags: ["first", "second"]
    }),
    "Success."
  );

  t.is(
    await tr.render("{% if false %}{% elseif tags.includes('first') %}Success.{% endif %}", {
      tags: ["first", "second"]
    }),
    "Success."
  );
});

So you shouldn’t need a filter for this but if you want to use a filter that’s okay too.

Thanks everyone for your help here!

@zachleat
Copy link
Member

This is an automated message to let you know that a helpful response was posted to your issue and for the health of the repository issue tracker the issue will be closed. This is to help alleviate issues hanging open waiting for a response from the original poster.

If the response works to solve your problem—great! But if you’re still having problems, do not let the issue’s closing deter you if you have additional questions! Post another comment and I will reopen the issue. Thanks!

zachleat added a commit that referenced this issue Jun 18, 2019
@zachleat
Copy link
Member

zachleat commented Jun 18, 2019

Hmm, re-reading the original comment I think it might just be a baseline JS syntax misunderstanding. You’re likely trying to run includes on a template that doesn’t include a tags property (or is null like tags: ).

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

short circuits when title === "Home" (never running the elseif)

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

does not short circuit.

Maybe also related to #556 which @edwardhorsford fixed for the next version.

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

6 participants