11ty / eleventy

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

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

Open querkmachine opened 9 months ago

querkmachine commented 9 months ago

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 commented 9 months ago

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 commented 9 months ago

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 commented 9 months ago

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 commented 8 months ago

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 https://github.com/11ty/eleventy/issues/3114#issuecomment-1826369696. 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 commented 8 months ago

@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 commented 8 months ago

@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 commented 8 months ago

@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 commented 8 months ago

If you put your filters wrapped in a plugin, as suggested by https://github.com/11ty/eleventy/issues/3114#issuecomment-1826369696 (and originally https://www.lenesaile.com/en/blog/organizing-the-eleventy-config-file/#method-3-adding-another-config-file-as-a-plugin), or just anywhere you have access to eleventyConfig, you can access the eleventyConfig.javascriptFunctions object to get the slugify function as well:

eleventyConfig.javascriptFunctions["slugify"]("Hello, world!");
// "hello-world"
Snapstromegon commented 8 months ago

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

uncenter commented 8 months ago

Ah, good to know!