mozilla / nunjucks

A powerful templating engine with inheritance, asynchronous control, and more (jinja2 inspired)
https://mozilla.github.io/nunjucks/
BSD 2-Clause "Simplified" License
8.55k stars 638 forks source link

Passing data to includes #539

Open sjelfull opened 8 years ago

sjelfull commented 8 years ago

Twig supports sending data to includes, like so:

{% include 'include.twig' with {
    name: 'whatever'
} %}

This is really useful in many cases, like re-using a block in different contexts, with different behaviour / data. Have anyone looked into adding support for this?

ricordisamoa commented 8 years ago

522

carljm commented 8 years ago

Reopening to track this feature request. Some discussion in #522.

I'm not opposed to this feature, but since it's not in Jinja2 I'm also uninterested in implementing it or reviewing or merging the implementation. Anyone interested in the feature is welcome to submit a pull request, it may be that one of the other maintainers is interested in reviewing and merging it.

sbussard commented 8 years ago

it would be great to be able to pass blocks into includes to make it easier to create one-off versions

carljm commented 8 years ago

I don't know what "pass blocks into includes" means, concretely speaking.

sbussard commented 8 years ago

Here's the use case I'm concerned about:

I have an include for a site banner. The banner has links in it, but the links need to change based on the page. The banner include is used in multiple templates.

On a page I would like to define a block with the links to be used, then be able to pass that block to the banner include to replace the default markup for that section.

carljm commented 8 years ago

The "nunjucks way" to approach that use case is to make the banner-display a macro, which takes arguments (perhaps with default values) for the various links that may need to change.

I still don't understand how your proposed solution would work. If there are multiple links that need to be changed, wouldn't you need multiple blocks? Why are blocks better here than simple variables or macro arguments?

sbussard commented 8 years ago

In that particular case all the links are side by side. The nunjucks way is good.

keelii commented 8 years ago

+1

sbussard commented 8 years ago

Following up on this: I was able to do much of what I needed to do when I realized that variables are globally scoped (but you don't have to worry about leaking to sibling pages). For more complicated situations, it's good to know that includes have their own inheritance tree. If that's not enough, macros come to the rescue. In my opinion it's not a matter of being able to do something with nunjucks, it's a matter of how intuitive it will be to do.

@keelii I'd be interested in hearing your use case to test this hypothesis.

carljm commented 8 years ago

To be clear, the original feature request here is to pass simple extra context data to include, using syntax like {% include "somefile.html" with {'foo': 'bar'} %} This ticket is open because I don't have a problem with that feature in theory if someone wants to implement it, though as I've said it's not a high priority for me personally.

The ticket has since been clouded with discussion of some not-entirely-clear-to-me additional hypothetical feature involving "passing blocks into includes." If that's still something you want to pursue or discuss, please open a new separate issue for it, as IMO it's quite a different thing from the feature proposed in this issue, and I don't think anything will be gained by confusing them in a single issue.

Thanks!

sbussard commented 8 years ago

@carljm right, that is not what I'm trying to do. That's why I posted my follow up. I should have been more clear.

keelii commented 8 years ago

@sbussard What i want to do is :

{% component name="foo" data="{bar: 1, orz: '2'}" %}

and i convert this component tag to nunjucks syntax in my program, then use nunjucks compile to html file. looks ugly,but it works for me.

{% for i in range(0, 1)  %}
    {% set bar = 1 %}
    {% set orz = '2' %}
    {% include 'path/to/my/component_template'  %}
{% endfor %}
carljm commented 8 years ago

@keelii Why not just write {% component %} tag as a nunjucks extension?

devoidfury commented 8 years ago

@keelii You might be able to use a macro:

{% macro component(bar, orz) %}
 // template here. this can also be included and then called. args can also be objects
{% endmacro %}

{% for i in range(0, 1)  %}
    {{ component(1, '2') }}
{% endfor %}
keelii commented 8 years ago

@carljm The component tag contains a sub-template, but i want pass some variable to this sub-template. these variable works for component private scope, and share some parent's variables. just like The JavaScript closure.

I've tried to use nunjucks extension, but it is hard to understand for me.

checkout this:

https://github.com/keelii/wo/blob/master/boilerplate/app/views/index.html#L26 https://github.com/keelii/wo/blob/master/boilerplate/app/components/module/module.html

@devoidfury macro content can not be maintained in a separate file.

elcontraption commented 8 years ago

@keelii here's an example component tag. I'm new to this API but it seems to work:

// nunjucks-component.js

// I'm using gulp-nunjucks-render, so I have to grab the nunjucks object:
import {nunjucks as nunjucks} from 'gulp-nunjucks-render';

/**
 * {% component 'name', {title: 'Example', subtitle: 'An example component'} %}
 */
var ComponentTag = function() {
    this.tags = ['component'];

    this.parse = function(parser, nodes, lexer) {
        var token = parser.nextToken();

        var args = parser.parseSignature(null, true);
        parser.advanceAfterBlockEnd(token.value);

        return new nodes.CallExtension(this, 'run', args);
    };

    this.run = function(context, name, data) {
        // My components are organized in this manner: components/componentName/componentName.html, but this is easily changed.
        // Note that the component is only given the data object, and the context is not exposed.
        return nunjucks.render('components/' + name + '/' + name + '.html', data);
    };

};

module.exports = new ComponentTag();
// Enabling the component tag extension
import nunjucksComponent from 'nunjucks-component';

nunjucks.addExtension('component', nunjucksComponent);

I hope that helps!

keelii commented 8 years ago

@elcontraption Awesome! it looks sexy, I will try.

neysimoes commented 8 years ago

Not the best solution, but works for me.

Input:

{% set sectionHeader = { title: 'Title 1', subtitle: 'Subtitle 1' } %}
{% include "_partials/section-header.html" %}

{% set sectionHeader = { title: 'Title 2', subtitle: 'Subtitle 2' } %}
{% include "_partials/section-header.html" %}

_partials/section-header.html

<header class="section-header">
    <h3 class="section-title">{{sectionHeader.title}}</h3>
    <p class="section-subtitle">{{sectionHeader.subtitle}}</p>
</header>

Output:

<header class="section-header">
    <h3 class="section-title">Title 1</h3>
    <p class="section-subtitle">Subtitle 1</p>
</header>

<header class="section-header">
    <h3 class="section-title">Title 2</h3>
    <p class="section-subtitle">Subtitle 2</p>
</header>
ettoredn commented 7 years ago

Any news on this one? Handlebars already allows passing custom data to the included template and it's incredibly useful.

fdintino commented 7 years ago

As the new maintainer I'm slowly making my way through old tickets. This seems like something worth adding.

dreki commented 7 years ago

Thank you @fdintino! Right now we can use macros but jinja2 gives parent context to partials, which would be wonderful. You're the best!

ArmorDarks commented 7 years ago

@dreki You can import macros with context too.

misscs commented 7 years ago

In theory not a bad add, but I feel like this feature can be accomplished with macros and/or custom components.

fdintino commented 7 years ago

@misscs In some cases that is true, but there have been arguments put forward elsewhere about the utility of this feature and how it would differ from a macro implementation.

There is actually a two-year-old pull request for this feature on jinja2, pallets/jinja#512. That PR hasn't been closed, but it has this bizarre response from the repo maintainer:

I really hate the include tag and wish I would never have added it. Because of that I'm not so sure if I want to extend it at this point. It's so buggy :(

It is the rare case where the django templating engine is actually more featured than jinja2.

ArmorDarks commented 7 years ago

In some cases that is true, but there have been arguments put forward elsewhere about the utility of this feature and how it would differ from a macro implementation.

Hm, that seems to be strange argument. We're using solely macros, and they all stored as Components, each macro in standalone file: example. Doesn't seem to be much different from storing standalone templates which later will be included with specific context.

There is actually a two-year-old pull request for this feature on jinja2, pallets/jinja#512. That PR hasn't been closed, but it has this bizarre response from the repo maintainer:

Well, I actually share his opinion. We've used includes in the beginning too, but after it always started to rise debates "should this component be include or macro", we've ended up using only macros. And after that usefulness of includes became completely unclear for me. Just my two cents.

fdintino commented 7 years ago

Yeah, that is a strange point. It would be nice if there was something like an ES6 "default export" for macros, though I suppose a naming convention can address that. I can definitely see the advantage to having the context variables all listed explicitly in a file. Maybe I'm just biased due to familiarity—we're in the middle of transitioning our templates from django to jinja.

ArmorDarks commented 7 years ago

It would be nice if there was something like an ES6 "default export" for macros,

Well, in Jinja all macros are like "exported by default", so you can import all ({% import '_components/_Link.nj' as linksComponents with context %}, or what you need ({% from '_components/_Link.nj' import Link, ActiveLink with context %}), or nothing...

So, in some sense, explicit exporting isn't necessary.

I can definitely see the advantage to having the context variables all listed explicitly in a file. Maybe I'm just biased due to familiarity—we're in the middle of transitioning our templates from django to jinja.

To be honest, didn't get that part :) Hard to gasp without example

fdintino commented 7 years ago

Oh, that was an argument in favor of macros over includes.

ArmorDarks commented 7 years ago

Ah, now I get. Actually, it never occurred to me that includes with context also have this issue when you really don't know which context they can accept... Good point.

misscs commented 7 years ago

Well, I actually share his opinion. We've used includes in the beginning too, but after it always started to rise debates "should this component be include or macro", we've ended up using only macros. And after that usefulness of includes became completely unclear for me. Just my two cents

I'm with @ArmorDarks (and creator of Jinja): macros cover most all use cases. I believe what folks are trying to accomplish can be done with what the language already provides.

KayakinKoder commented 7 years ago

An alternative to @neysimoes solution with macros. I have used his solution a lot but over time found macros to be better. With macros, you avoid globals e.g. "sectionHeader.Title", and can easily implement default values. In this example we set a custom color in the first instance but easily revert to the default value in the second (which is not possible if you use include; you would need to re-set color before the second instance). Both of those benefits can be big time and error-savers if you have complex templates with a lot of variables.

Input:

{% import '_partials/section-header.html' as header %}

{% set sh1 = { title: 'Title 1', subtitle: 'Subtitle 1' } %}
{% set color = 'red' %}
{{ header.h(sh1, color) }}

<!-- now we want to revert to the default color in the macro, blue; no action is 
required on our part. if we were using include instead of a macro, we would 
need to remember to do a set color = blue here -->

{% set sh2 = { title: 'Title 2', subtitle: 'Subtitle 2' } %}
{{ header.h(sh2) }}

_partials/section-header.html

{% macro h(sectionHeader, color="blue") %}
<header class="section-header">
    <h3 class="section-title">{{sectionHeader.title}}</h3>
    <p class="section-subtitle">{{sectionHeader.subtitle}}</p>
    <p>{{color}}</p>
</header>
{% endmacro %}

Output:

<header class="section-header">
    <h3 class="section-title">Title 1</h3>
    <p class="section-subtitle">Subtitle 1</p>
    <p>red</p>
</header>

<header class="section-header">
    <h3 class="section-title">Title 2</h3>
    <p class="section-subtitle">Subtitle 2</p>
    <p>blue</p>
</header>
sney-js commented 6 years ago

I've found a simple way to achieve this. Wrote a method to merge two JSONs. Then we pass this merged json to our templates. add in your filter.js:

  const mergeJSON = function (target, add) {
    function isObject(obj) {
      if (typeof obj == "object") {
        for (var key in obj) {
          if (obj.hasOwnProperty(key)) {
            return true; // search for first object prop
          }
        }
      }
      return false;
    }

    for (var key in add) {
      if (add.hasOwnProperty(key)) {
        if (target[key] && isObject(target[key]) && isObject(add[key])) {
          this.mergeJSON(target[key], add[key]);
        } else {
          target[key] = add[key];
        }
      }
    }
    return target;
  };

  env.addFilter('mergeJSON', function (original, appendJson) {
    return mergeJSON(Object.assign({}, original, {}), appendJson);
  });

then inside your template html files, use like:

{% set item = item | mergeJSON({small:true}) %}
{% include "../" + contentType + "/" + contentType + ".html" ignore missing %}

Then access inside the partial access by: item.small

lodi-g commented 5 years ago

I've picked nunjucks randomly to change from my usual template engine. It seems incredible to me that nunjucks doesn't allow this simple and intuitive behavior.

I'll use the following syntax:

{% set barChartData = ...%}
{% include 'reveal/barchart.njk' %}

{% set barChartData = ... %}
{% include 'reveal/barchart.njk' %}

But that's a bit sad tbh.


Edit: Nvm I used macros and import. Works perfectly. Thanks & sorry.

FCO commented 4 years ago

I'd like this feature to use WITH macros. Something like:

{% macro HandleOn(on = {}) %}
   {% for event in required %}
      {{ throwError(event + " is required") if not in(event, on) }}
   {% endfor %}
   {% for key, value in on %}
      on{{ key }}="{{ value }}"
   {% endfor %}
{% endmacro %}

{% macro DocumentationPiece() %}
   Handles `on` events. The following events are required: {{ requires | join(", ") }}
{% endmacro %}

and on the other template I use it, I could say:

{{ from "handles_on.njk" import HandleOn, DocumentationPiece with { requires: ["click", "hover"] } }}

And I could use HandleOn and DocumentationPiece without passing the events they require.

(that's a simplified version of a code I'm really using, throwError and in are global functions)

dbwodlf3 commented 3 years ago

It works for me.

test.njk

{{ tagName }}

main.njk

{% set tagName = someVariable %}
{% include 'test.njk' %}

someVariable is just variable. it can be represented {{someVariable}} in njk.

janielMartell commented 3 years ago

The solution proposed by @dbwodlf3 worked for me, but I would still like to have a way of passing arguments/having parameters in my includes.

edwardhorsford commented 3 years ago

I've been using a similar approach to @dbwodlf3 for those cases where I have an include rather than a macro. It makes me a little wary as it relies on the current context / changing something on the current context - which presumably for safety you might set in the line proceeding the include.

I like the 'neatness' / readability of directly passing the data to use, just like you can with a macro.


Includes can actually be really powerful when used with macros - one nice thing with them (though I love macros) is that the included file doesn't have to have any Nunjucks syntax in it. It can be pure text, html, whatever. If it were a macro, that content would need to be wrapped in the macro block (unless I'm wrong).

The UK Government uses this to good effect for their macros. The actual html is stored as html in a file, and then the macro includes that. This enables other things to make use of the html / compose with it (tests, rendering of examples for the deisgn system), without the macro block syntax getting in the way.

I've also used this as a way of mass-importing macros as a workaround for no default export or easy macro file management. I can all the source code for them in separate files, and then have a single macros.njk file that defines a bunch of macros, each just importing the relevant file.

example:

{% macro govukButton(params) %}
  {% include "govuk/components/button/template.njk" %}
{% endmacro %}

{% macro govukCharacterCount(params) %}
  {% include "govuk/components/character-count/template.njk" %}
{% endmacro %}

{% macro govukCheckboxes(params) %}
  {% include "govuk/components/checkboxes/template.njk" %}
{% endmacro %}

{% macro govukDateInput(params) %}
  {% include "govuk/components/date-input/template.njk" %}
{% endmacro %}
ryanolee commented 2 years ago

Just as a heads up we did a POC for this a while back in https://github.com/Financial-Times/nunjucks-loader. This is not really supported anymore but can be used as a reference if anyone want to look to get it implemented properly.