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

Add a method of using getFilter in files other than the Eleventy config #3114

Open
querkmachine opened this issue Nov 25, 2023 · 10 comments
Open

Comments

@querkmachine
Copy link

Is your feature request related to a problem? Please describe.

Eleventy 0.11.0 added the ability to use Eleventy's built-in filters from within the configuration file using the eleventyConfig.getFilter function.

However, my projects can end up having a lot of custom filters, shortcodes and other bits-n-bobs. To keep things tidy, I place my custom functions definitions in their own files rather than directly within the config function.

For example, part of my configuration might look more like this:

const { myCoolFilter, myLessCoolFilter } = require("./config/filters/cool-ranking.js");

module.exports = function (eleventyConfig) {
  // ...
  eleventyConfig.addFilter("myCoolFilter", myCoolFilter); 
  eleventyConfig.addFilter("myLessCoolFilter", myLessCoolFilter); 
  // ...
};

The filter function not being inline within the file prevent me from accessing the eleventyConfig and getFilter from within cool-ranking.js unless I manually pass it into the function, but this would seemingly make it difficult to use as a filter within templates.

I've also encountered times where being able to access filters in .11tydata.js files would be useful, but there similarly doesn't seem to be a method of doing so, such as here:

module.exports = {
  eleventyComputed: {
    opengraphImageUrl: (data) => {
      // OpenGraph image URLs need to be absolute. It'd be really cool if
      // the `url` filter was available here!
      return `/images/opengraph/${data.page.fileSlug}.png`;
    },
  },
};

Describe the solution you'd like

I'm not sure how practical or easy it would be, especially as filters like url and get*CollectionItem are at least somewhat dependent on the configuration itself, but it'd be nice if the built-in filters could be available in more contexts.

Describe alternatives you've considered

At the moment I've resorted to reproducing the built-in features as custom functions, sometimes using the same dependencies as Eleventy itself, such as using the @sindresorhus/slugify package to duplicate the slugify filter.

This works in the short term, but there feels like there's a risk of the configurations being changed or falling out of sync over time, so it's not an ideal alternative in my mind.

Additional context

No response

@Snapstromegon
Copy link
Member

Hey @querkmachine,
I think there are two already working solutions for your problem (if I understand it correctly that you want to access the eleventyConfig object from inside your filters). Both are related.

First: Wrap them in a plugin
To do this, you can write your filters in ./config/filters/cool-ranking.js as:

module.exports = (eleventyConfig) => {
  eleventyConfig.addFilter("myCoolFilter", () => {/*...*/}); 
  eleventyConfig.addFilter("myLessCoolFilter", () => {/*...*/}); 
}

And in your normal eleventy config you'd do:

const filters = require("./config/filters/cool-ranking.js");

module.exports = function (eleventyConfig) {
  // ...
  eleventyConfig.addPlugin(filters);
  // ...
};

Or second: Use higher order functions.
Higher order functions are often used for plugin systems. To do this you wrap your filter functions in a function that returns the filter function.
So your ./config/filters/cool-ranking.js would be:

const myCoolFilter = (eleventyConfig) => {
  return (filterParam) => {
    // Your filter impl. using eleventyConfig
  }
}

module.exports = {
  myCoolFilter,
  //...
}

And then you can use it via:

const { myCoolFilter, myLessCoolFilter } = require("./config/filters/cool-ranking.js");

module.exports = function (eleventyConfig) {
  // ...
  eleventyConfig.addFilter("myCoolFilter", myCoolFilter(eleventyConfig));
  // ...
};

Recommendation
I personally prefer the first option over the second, because it's more concise and is closer to an "intended" way of doing things.

@pdehaan
Copy link
Contributor

pdehaan commented Nov 25, 2023

Also, in the context of eleventyComputed, as long as you aren't using arrow functions, the filters should be accessible in the this. scope:

module.exports = {
  eleventyComputed: {
    opengraphImageUrl(data) {
      console.log({
        "this": this,
        page: data.page,
      });
      return this.url(`/images/opengraph/${data.page.fileSlug}.png`);
    },
  },
};

Wouldn't solve all your use cases, but big 👍 to everything that @Snapstromegon pointed out above.

@querkmachine
Copy link
Author

Thank you for the answers, both! I'll probably follow the recommendation of bundling things together into plugins.

I'm gonna leave this open as I still think there might be value in the means of accessing filters being more direct, or at least having these use cases be better documented on the website.

@asbjornu
Copy link

I would like to access the built-in slugify filter from a data function, so I can't just require it from the <data>.js file as @Snapstromegon suggests in #3114 (comment). I can see that something which looks like a context object of some sort is passed into the data function:

// _data/some_data_file.js
module.exports = async function(context) {
    console.log(context); // some sort of context object, not sure what use it has
    console.log(this.eleventy); // undefined
    // …
    return data;
}

If I log this context object to the console, it looks like the following:

{
  eleventy: {
    version: '2.0.1',
    generator: 'Eleventy v2.0.1',
    env: {
      source: 'cli',
      runMode: 'build',
      config: '…/.eleventy.js',
      root: '…',
      isServerless: false
    }
  }
}

It doesn't seem like I can do anything useful with this object, and this.eleventy is undefined within the data function.

Also, I don't quite understand whether or how I can use the eleventyComputed return object from my data function, as currently, it's returning an array of data objects. What I would like to do is use slugify for every data object being returned, to create the URL of each data object centrally, avoiding having to build the URL with | slugify everywhere in my templates.

Any suggestions and help would be highly appreciated.

@Snapstromegon
Copy link
Member

@asbjornu What do you mean with "having to build the URL with | slugify everywhere"?
I think you should only need it when setting the permalink of a page manually. When using the page's url, it should already be in a slugified form.
Maybe you could provide an example?

@asbjornu
Copy link

asbjornu commented Dec 20, 2023

@Snapstromegon, I want to set the URL on the data objects returned from the data function, not on a page. These data objects are used in various places in my templates and I would like the URL to be set once in the data function before the objects are returned, so I don't have to repeat the URL building everywhere these data objects are used in my templates.

Here's an example:

// _data/some_data.js
module.exports = async function(context) {
    const data = [
        { name: 'Lorem ipsum' },
        { name: 'Dolor sit' },
        { name: 'Amet, consectetur' }
    ];

    for (const item of data) {
        let slug = context.eleventy.slugify(item.name); // TypeError: context.eleventy.slugify is not a function
        slug = this.eleventy.slugify(item.name);        // Cannot read properties of undefined (reading 'slugify')
        data.url = `/prefix/${slug}/`;
    }

    return data;
};

My use case seems related to #2790, but for data functions instead of template functions.

@Snapstromegon
Copy link
Member

@asbjornu
In your special case, you could also import the slugify filter directly via (esm syntax here, works the same for cjs):

import slugify from "@11ty/eleventy/src/Filters/Slugify.js";

But this is not a general solution. Maybe we could expose the UserConfig object on the context.eleventy object passed into a data function.
If I find some time, I will take a look at this.

@uncenter

This comment was marked as duplicate.

@Snapstromegon
Copy link
Member

@uncenter I'd recommend using the eleventyConfig.getFilter() method instead, but yes, this also works.

@uncenter
Copy link
Contributor

Ah, good to know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants