11ty / eleventy

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

If expression for classes #1580

Closed larzknoke closed 3 months ago

larzknoke commented 3 years ago

Hi,

I want to set a class in my navigation if a url is visited.

This is working: {{ 'active' if '/foo' in page.url }}

but this is not working: {{ 'active' if '/foo' or '/bar or '/foobar' in page.url }}

for the second expression the active class is always set.

What I'm doing wrong here?

pdehaan commented 3 years ago

Are you using Nunjucks or Liquid or something else?

larzknoke commented 3 years ago

Nunjucks

pdehaan commented 3 years ago

Ah, looks like Nunjucks based on the docs.

This worked for me, although it's a bit verbose:

{{ 'active' if '/foo' in page.url or '/bar' in page.url or '/foobar' in page.url }}

Wondering if your original if '/foo' or '/bar' in page.url is treating it as:

if ('/foo') {...}
else if ('/bar' in page.url) {...}

Which might explain the behavior since if ('/foo') {} would be truthy and it's not considering those to all be in the context of in page.url.

pdehaan commented 3 years ago

If you don't like the non-scaleable verbosity, you might be able to create a custom filter (since I didn't see an obvious Nunjucks one for testing if a value is in an array):

// .eleventy.js
module.exports = eleventyConfig => {
  eleventyConfig.addFilter("contains", (value, needle="", haystack=[]) => {
    return haystack.some(hay => needle.includes(hay)) ? value : "";
  });

  return {};
};

And you can use the custom filter like a so:

{{ 'active' | contains(page.url, ['/foo', '/bar', '/foobar']) }}

Since our goofy contains filter will check to see if our needle (url) is in the haystack (array of slugs), it will either return the original value ("active") if found, or an empty string if not. We could probably also tweak it slightly to give you better control of the else case by letting you pass an optional value if not found instead of hardcoding the empty string. Which would let you easily negate logic if you'd rather have it say "active" if found or fall back to "inactive" if not found.

pdehaan commented 3 years ago

In fact, here's what that would look like:

eleventyConfig.addFilter("contains", (value, needle="", haystack=[], def="") => {
  return haystack.some(hay => needle.includes(hay)) ? value : def;
});
1. {{ 'active' | contains(page.url, ['/foo', '/bar', '/foobar']) }}
2. {{ 'active' | contains(page.url, ['/foo', '/bar', '/foobar'], "inactive") }}
3. {{ 'active' | contains(page.url, ['/foo', '/home', '/foobar'], "inactive") }}

OUTPUT

Where my test page is called /home.njk so my page.url is /home/.

1. 
2. inactive
3. active
  1. Since my current page.url doesn't match any of the slugs, the default empty string is returned.
  2. Since my current page.url doesn't match any of the slugs, my custom default value, "inactive", is returned.
  3. Since my current page.url DOES match one of the slugs (replaced '/bar' with '/home'), the original value passed to the filter ("active") is returned.
tannerdolby commented 3 years ago

@pdehaan I think your right, that its treating the if expression with or usage as a if-else-if ladder. Therefore, if the first branch is truthy, then there wouldn't be a reason to execute the second or third branch and statement. The basic syntax for a if expression in Nunjucks is this:

{{ expression-to-echo if expression-to-match operator value [ else else-expression-here ] }}

I remember using an if expression to apply an 'active' class for nav menu items based on the page.url string and encountered this same issue using multiple or's.

What I did was just used logical & instead with the negation operator. There should be a way to use multiple or in an if expression but this is what I remember doing. This didn't feel right but eh,

<li class="{{ 'active' if '/about/' in page.url and not '/contact/' in page.url and not '/writing' in page.url  }}">About</div>

Bryan Robinson wrote a good article about creating 'active' state for nav menu items using if expressions here: https://bryanlrobinson.com/blog/using-nunjucks-if-expressions-to-create-an-active-navigation-state-in-11ty/

edwardhorsford commented 3 years ago

For legibility, I'd chain your filter off of the array rather than active, and have the filter return true or false. This makes it a bit more general purpose.

Suggestion: {{ 'active' if (['a','b','c'] | contains(page.url) )}}

pdehaan commented 3 years ago

https://mozilla.github.io/nunjucks/templating.html#if-expression

The one thing that was confusing me, was that I wasn't able to find any obvious documentation for the "in page.url" syntax in the Nunjucks docs.


@edwardhorsford That's a MUCH better approach! I didn't consider wrapping a filter like that.

  eleventyConfig.addFilter("contains", (haystack=[], needle="") => {
    return haystack.some(hay => needle.includes(hay));
  });
1. {{ 'active' if (['/foo','/bar','/foobar'] | contains(page.url)) }}
2. {{ 'active' if (['/foo','/home','/foobar'] | contains(page.url)) }}
3. {{ 'active' if (['/foo','/bar','/foobar'] | contains(page.url)) else "inactive" }}

OUTPUT

1. 
2. active
3. inactive
larzknoke commented 3 years ago

Thanks a lot for the quick and detailed solutions/explanations. The filters are working perfectly for me!

pdehaan commented 3 years ago

Ah, before I close this tab, I thought of [at least] one potential bug/gotcha in the above snippet:

return haystack.some(hay => needle.includes(hay));

That snippet is using String#includes(), which means the slug portion can exist anywhere so all of these might be valid:

  1. /foo-bar/
  2. /some/foo/things
  3. /football

Not sure if you'd want to tighten up your patterns and logic a bit to maybe do something like including trailing slashes:

{{ 'active' if (['/foo/', '/bar/', '/foobar/'] | contains(page.url)) }}

This will ensure that full paths match, and not stuff like /foo-buz-stuff/ which would currently match since it starts with /foo.


As well as changing it from needle.includes(hay) (which is a weird way to say that, I guess), to maybe use String#startsWith() instead:

return haystack.some(hay => needle.startsWith(hay));

This way, you're only checking the top-level directories and not something deeply nested like /a/b/c/foo-bar/bar.

larzknoke commented 3 years ago

Cool! Thanks a lot for your support.

Ryuno-Ki commented 3 years ago

From a Python perspective (where Nunjucks is inspired from via Jinja2), I'd check if page.url in ['/home', '/about', '/now'].

I haven't checked, whether this is supported by Nunjucks, though. The filter approach is a sensible approach, too.

zachleat commented 3 months ago

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 we will reopen the issue. Thanks!