Pass variables from child template to parent in Jinja2 - jinja2

I want to have one parent template and many children templates with their own variables that they pass to the parent, like so:
parent.html:
{% block variables %}
{% endblock %}
{% if bool_var %}
{{ option_a }}
{% else %}
{{ option_b }}
{% endif %}
child.html:
{% extends "parent.html" %}
{% block variables %}
{% set bool_var = True %}
{% set option_a = 'Text specific to this child template' %}
{% set option_b = 'More text specific to this child template' %}
{% endblock %}
But the variables end up undefined in the parent.

Ah. Apparently they won't be defined when they are passed through blocks. The solution is to just remove the block tags and set it up like so:
parent.html:
{% if bool_var %}
{{ option_a }}
{% else %}
{{ option_b }}
{% endif %}
child.html:
{% extends "parent.html" %}
{% set bool_var = True %}
{% set option_a = 'Text specific to this child template' %}
{% set option_b = 'More text specific to this child template' %}

If Nathron's solution does not fix your problem, you can use a function in combination with a global python variable to pass a variable value.
Advantage: The variable's value will available in all templates. You can set the variable inside a block.
Disadvantage: More overhead.
This is what I did:
child.j2:
{{ set_my_var('new var value') }}
base.j2
{% set my_var = get_my_var() %}
python code
my_var = ''
def set_my_var(value):
global my_var
my_var = value
return '' # a function returning nothing will print a "none"
def get_my_var():
global my_var
return my_var
# make functions available inside jinja2
config = { 'set_my_var': set_my_var,
'get_my_var': get_my_var,
...
}
template = env.get_template('base.j2')
generated_code = template.render(config)

In some cases, you can avoid this 'parameter-passing' by creating another variant of the parent that adds/removes some block and extends it instead.
{% extends [condition]|yesno:'parent_1.html,parent_2.html' %}

Related

Idiomatic way in Jekyll to inherit default data if project specific data unavailable

I am completely new to Jekyll. I did something like this:
{% assign top_nav = site.data.menus %}
{% if site.data.orgs[site.orgData].menus %}
{% assign top_nav = site.data.orgs[site.orgData].menus %}
{% endif %}
<ul>
{% for menu in top_nav %}
<li>
{{ menu.title }}
</li>
{% endfor %}
</ul>
Basically, I will grab an array of navigation items from a default folder. But if I notice the existence of a menu for a specific organization, then I will override the menu provided by the default folder.
What I don't like about this approach is I now have hundreds of places in my jekyll templates that does this if statement check. If this were any other scripting programming language, I would define a function like function($org_name,$prop) {return $site.data.orgs[$org_name][$prop] ? $site.data.orgs[$org_name][$prop] : $site.data[$prop]; } . What would be the idiomatic way to achieve the same objective in jekyll?
I tried a variation of David Jacquel's suggestion by doing this
./org_var.html
{% assign prop = include.prop %}
{% assign orgVar = site.data[prop] %}
{% if site.data.orgs[site.orgData][prop] %}
{% assign orgVar = site.data.orgs[site.orgData][prop] %}
{% endif %}
./_include/nav.html
{% include_relative ../org_var.html prop=menus %}
{% for menu in orgVar %}
... print menu items
./_layout/header.html
{% include_relative ../org_var prop='electronics.televisions' %}
{% for tv in orgVar%}
{{ tv.modelName }}
... print tv values
{% endfor %}
But I get a syntax error in ../org_var.html saying {% include_relative file.ext param='value' param2='value' %} . The documentation says I can't use relative path with include or include_relative. How do I make my org_var.html a reusable and global function? And will electronics.televisions even evaluate properly to the proper path of my site.data.org.[site.orgData][...path] variable?
Just realized there is a default: modifier for a variable like smarty templates.
{% assign top_nav = site.data.orgs[site.orgData].menus | default: site.data.menus %}
You can use Jekyll includes.
From anywhere you want to use your include :
{% include nav.html org_name=org_name prop=prop %}
Will call _include/nav.html that can be something like this :
{% assign org_name = include.org_name %}
{% assign prop = include.prop %}
{% if site.data.orgs[org_name][prop] %}
{% assign top_nav = site.data.orgs[site.orgData].menus %}
{% else %}
{% assign top_nav = site.data.orgs[site.orgData].menus %}
{% endif %}
<ul>
{% for menu in top_nav %}
...

Are variables set in include global in scope? If yes, any way to create a local variable?

Are variables set in Jekyll {% include %} files global in scope? That is, do they leak into the page that included then and subsequent includes?
For example, I have an include file with the following contents:
{% assign ai__attributes = "" %}
{% if include.width %}
{% capture ai__attributes %}{{ ai__attributes }}width="{{include.width}}" {% endcapture %}
{% endif %}
{% if ai__attributes != "" %}
{% capture ai__ial %}{:{{ai__attributes}}}{% endcapture %}
{% endif %}
![{{include.alt | default image }}]({{assetpath}}/{{include.path}}){{ai__ial}}
This sets teh ai__ial variable if include.width has been set. If I call this once with width set, and then again with it inset, will the ai__ial from the first call leak into the second? Is there any way to avoid this, e.g., by scoping the variable?
No way to set a local variable, but you can reset ai__ial in your include.
{% assign ai__attributes = "" %}
{% assign ai__ial = "" %}
...

How can I concatenate a string using Jinja for loop?

I am trying to iteratively concatenate a string to build url params with a 'for' loop, but I believe I am having scoping issues.
The output should be: url_param = "&query_param=hello&query_param=world"
array_of_objects = [{'id':'hello'},{'id':'world'}]
{% set url_param = "" %}
{% set array_of_ids = array_of_objects|map(attribute='id')|list%} // correctly returns [1,2]
{% for id in array_of_ids %}
{% set param = '&query_param='~id %}
{% set url_param = url_param~param %}
{% endfor %}
//url_param is still an empty string
I also tried namespace(), but to no avail:
{% set ns = namespace() %}
{% set ns.output = '' %}
{% set array_of_ids = array_of_objects|map(attribute='id')|list%} // correctly returns [1,2]
{% for id in array_of_ids %}
{% set param = '&industries='~id%}
{% set ns.output = ns.output~param %}
{% endfor %}
//ns.output returns namespace
That is indeed a scope issue. One "hacky" way of dealing with this is using a list that you append to like so:
{% set array_of_objects = [{'id':'hello'},{'id':'world'}] %}
{% set array_of_ids = array_of_objects|map(attribute='id')|list%}
{{ array_of_ids|pprint }} {# output: ['hello', 'world'] #}
{% set ids = [] %} {# Temporary list #}
{% for id in array_of_ids %}
{% set param = '&query_param='~id %}
{% set url_param = url_param~param %}
{{ ids.append(url_param) }}
{% endfor %}
{{ ids|pprint }} {# output: [u'&query_param=hello', u'&query_param=world'] #}
{{ ids|join|pprint }} {# output: "&query_param=hello&query_param=world" #}
The above gets you what you need, but for this specific example I would take a look at using jinja's join filter. It's more declarative and feels a little less hacky.
{% set array_of_objects = [{'id':'hello'},{'id':'world'}] %}
{# set to a variable #}
{% set query_string = "&query_param=" ~ array_of_objects|join("&query_param=", attribute="id") %}
{{ query_string|pprint }}
{# output: u'&query_param=hello&query_param=world' #}
{# or just use it inline #}
{{ "&query_param=" ~ array_of_objects|join("&query_param=", attribute="id") }}
You should change the initialization of your namespace.
Here is an example from the docs that will help you out:
{% set ns = namespace(found=false) %}
{% for item in items %}
{% if item.check_something() %}
{% set ns.found = true %}
{% endif %}
* {{ item.title }}
{% endfor %}
Found item having something: {{ ns.found }}

Change variable in included jinja2 template

Say I have two templates:
main.j2
{% include "vars.j2" %}
main: {{ var1 }}
vars.j2
{% set var1 = 123 %}
vars: {{ var1 }}
When run, only this line is output:
vars: 123
i.e. var1 is undefined in main.j2, even though it gets set to a value in the included vars.j2 template.
How can I pass variables from included template back to template that includes it? I considered chaining extends, but wondered if there's a more elegant approach.
I recently had a need to do the same thing, and found 2 solutions.
If you have Jinja version 2.10 or later, namespaces can be used:
main_ns.j2:
{% set ns = namespace() %}
{% include "vars_ns.j2" %}
main_ns: {{ ns.var1 }}
vars_ns.j2:
{% set ns.var1 = 123 %}
vars_ns: {{ ns.var1 }}
In Jinja 2.2 or later, it can be accomplished with block scoping of variables. I put the variable settings in the base template so that multiple children can extend it.
vars_block.j2:
{% set var1 = 123 %}
vars_block: {{ var1 }}
{% block content scoped %}{% endblock %}
main_block.j2:
{% extends "vars_block.j2" %}
{% block content %}
main_block: {{ var1 }}
{% endblock %}
You can try using with:
{% with var1=0 %}
{% include "vars.j2" %}
vars: {{ var1 }}
{% endwith %}

Access child variable from an include in parent template

I'm trying to specify a variable in a child template and access it from another template which is included in the parent template.
Here's what I'm trying to do (code trimmed to the bare minimum):
child.html
{% set var = 'foo' %}
{% extends 'base.html' %}
base.html
{% include 'bar.html' %}
bar.html
{{ var }}
At this point, I render the child.html file and nothing is output.
This is what I've tried:
If I specify the variable in the base.html file instead of the child.html file, it works.
If I pass the variable in my render call, it works. template.render(var = 'foo')
Chaging the base.html file to be {% include 'bar.html' with context %} doesn't fix it.
I am able to access the variable in my base.html file just fine, and in fact, I've managed to create a workaround by adding {% set foo = var %} to my base.html, and changing bar.html to {{ foo }}
So concisely put, my question is: Is there a way for a template that's included in a parent template to access a variable set in a child template? (Without having to define a new variable in the parent file like my workaround)
I was able to get this by passing the variable within the child block on the parent template.
Ex. I have active_page declared on my child template pricing.html.
At first, I was not able to access active_page within my included file navbar-alt.html but when I set the variables inside of the navbar block it worked.
{% block wrapper %}
<div id="wrapper">
{% block navbar %}
{% set navigation_bar = [
('/what-we-do', 'what-we-do', 'what we do'),
('/pricing', 'pricing', 'pricing'),
('/demo', 'demo', 'demo'),
('http://indico.readme.io/v1.0/docs', 'docs', 'docs')
] -%}
{% set active_page = active_page|default('index') -%}
{% if "dashboard" not in request.path %}
{% include 'includes/navbar-alt.html' %}
{% else %}
{% include 'includes/navbar.html' %}
{% endif %}
{% endblock navbar %}
{% if "dashboard" in request.path %}
{% block sidebar %}
{% include 'includes/sidebar.html' %}
{% endblock sidebar %}
{% endif %}
{% block content_wrapper %}
<div id="content">
{% block content %}{% endblock content %}
</div>
{% endblock content_wrapper %}
{% include 'includes/flashes.html' %}
</div>
{% endblock wrapper %}
Original inspiration:
Jinja Tricks