In a lot of my projects I've started using an alternative to get_template_part that allows you to pass data. I'm wondering whether something of this ilk would be good to include with Sage.
Function
/**
* Load a component into a template while supplying data.
*
* @param string $slug The slug name for the generic template.
* @param array $params An associated array of data that will be extracted into the templates scope
* @param bool $output Whether to output component or return as string.
* @return string
*/
function get_component($slug, array $params = array(), $output = true) {
if(!$output) ob_start();
$template_file = locate_template("components/{$slug}.php", false, false);
extract($params, EXTR_SKIP);
require($template_file);
if(!$output) return ob_get_clean();
}
:+1:
/**
* Load a component into a template while supplying data.
*
* @param string $slug The slug name for the generic template.
* @param array $params An associated array of data that will be extracted into the templates scope
* @param bool $output Whether to output component or return as string.
* @return string
*/
function get_component($slug, array $params = array(), $output = true) {
if(!$output) ob_start();
if (!$template_file = locate_template("components/{$slug}.php", false, false)) {
trigger_error(sprintf(__('Error locating %s for inclusion', 'sage'), $file), E_USER_ERROR);
}
extract($params, EXTR_SKIP);
require($template_file);
if(!$output) return ob_get_clean();
}
I've had similar issues with get_template_part(), and apparently the WP Core team doesn't see a need to be able to pass data from one template part to another.
However, as it stands, there isn't a need in the Sage theme to do this with any of the current template parts. So while it's definitely a handy function to have set up if you need it, I don't think it's something that's required in the Sage theme by default.
I've had this issue and ended up with the same solution. I think this would be a nice functionality to have built in to Sage, as a stater theme that could be used to build complex ones.
But I too see that there is a really small number of people who would beneficiate.
Nice function! Needed this for when I was loading the same loop with different data based on what template it was in.
Probably a good idea to allow filtering as well.
I'm new to sage, and having trouble figuring out _where_ to add this code and/or _how_ to call it. Tried just pasting it in to lib/extras but it doesn't seem to be available - at least not from a template page. Am I missing a step?
I've done something similar myself before too, this would be a nice to have.
However, I'm not a fan of the bool echo parameter. It makes the function harder to understand when you see it called, and makes it less intuitive to use.
My implementation is usually something like this:
function render_view($handle, $data) {
// output the template
}
function capture_view($handle, $data) {
ob_start();
render_view($handle, $data);
return ob_get_clean();
}
Lately though I've just been shortening this to view() which returns the output and using the short echo syntax when I want to output that.
<?= view('some-part', ['title' => 'sup']) ?>
That way I can use the same function for returning the compiled template where a return is appropriate, like a shortcode callback.
I realize that function implies that the template output is always buffered and some may frown on that for performance reasons, but the jury is out as to whether or not there is any negative (or even positive) impact on performance.
Personally, I'd like to see Sage become more modular in regards to templating. Similar to how there are different drop-in solutions for bedrock deployments (capistrano, ansible), it would be nice to see similar flexibility in this regard for other php templating solutions.
This is what the Codex write https://codex.wordpress.org/Function_Reference/get_template_part#Passing_Variables_to_Template :smile:
This is what we use @ Level Level https://github.com/humanmade/hm-core/blob/master/hm-core.functions.php#L1236
Just adding my vote for this one. In my case, I always call it render($path, $stuff) ;)
Timber looks interesting.
Timber::render solves this nicely, and it has a lot of other features that could be handy (caching, image resizing).
I've solved this type of thing using a set_query_var just before get_template_part.
e.g.
$mydata = [ 'roots.io', 'Roots' ];
set_query_var( 'mydata_var', $mydata );
get_template_part( 'template', 'part' );
then inside the template you can now use the $mydata_var variable:
<a href="<?= esc_url( $mydata_var[0] ); ?>"><?= esc_html( $mydata_var[1] ); ?></a>
@fgilio No, it's not a global, that's the cool thing about it :sunglasses:
Looking at the load_template function located in wp-includes/template.php, you will find this line:
extract( $wp_query->query_vars, EXTR_SKIP );
So all variables that you add to the query_vars get extracted and made available here, only in this scope :wink:
Be careful to use a unique name though, as there may be other variables set already. Just prefix them or something.
(For the current 4.3 release, look here: https://core.trac.wordpress.org/browser/tags/4.3/src/wp-includes/template.php#L547)
edit: Variable is not limited to the scope, but is available to all templates that get loaded after the set_query_var call.
Continue reading the upcoming comments for @Foxaii's explanation.
Sorry @noplanman, $wp_query is a global. But it is the recommended way to do things if you follow the Codex.
@Foxaii I know $wp_query is a global, but I don't think that's what @fgilio was asking about. I understood that he was asking about the $mydata_var variable in my example above, because the variable itself is no global and only accessible in the scope of the template being loaded.
Quoting the codex page you linked:
...get_template_part() extracts all of the WP_Query query variables, into the scope of the loaded template.
Well, taking into account what @Foxaii said and how i works... It seams like a really good workaround.
@noplanman
the variable itself is ... only accessible in the scope of the template being loaded.
Nope. set_query_var('mydata_var', $my_data) adds $mydata_var to the global $wp_query. If you var_dump($GLOBALS) (after it's been set) you'll see it in there somewhere.
This means that the $mydata_var variable will be accessible to every template loaded by get_template_part(); all because of the same function that you point out.
You won't need to declare global $mydata_var; to make it accessible in the get_template_part included templates but its scope is not limited to any template alone.
Ah right, now I understand what you mean. Thanks for the clarification!
Out of curiosity and to learn a new approach, how do you solve it? Do you make sure to use a unique name using set_query_var or a different way altogether?
There's nothing wrong with the solutions proposed here. As @kalenjohnson said, we don't need to pass variables to templates to make Sage work, so it should remain an optional extra.
I tend to use classes and get the data and load templates from there.
If I'm being lazy I will sometimes use the following to maintain scope:
$vars = "Hiya! I'm in the scope of the file below."
include locate_template('templates/whatever.php', false, false);
I wouldn't call it from base.php though, it would be included via a separate file included by get_template_part. This prevents the $vars entering the scope of base.php.
I avoid set_query_var unless I actually need to set a variable for the query.
Cool, thanks for sharing :+1:
We've decided to forgo this idea. I had it implemented in an early rendition of Sage9, but we decided to get rid of it, as referenced above.
You can see my implementation here: https://github.com/roots/sage/blob/639092e04456ffa94d6509710ff1966e76a5051f/src/helpers.php#L29-L34
It worked in tandem with a Template class that has since been removed: https://github.com/roots/sage/blob/639092e04456ffa94d6509710ff1966e76a5051f/src/lib/Sage/Template.php
Obviously you don't need an entire class just for this. Here I've reduced the meat and potatoes of it to a single function.
<?php namespace App;
/**
* Include a template file with a set of variables
*
* There are 3 main stages to this function.
* 1. Converts a delimeted template file into an array of parts
* 2. Passes those parts to WP's locate_template()
* 3. Load template
*
* To better explain stage (1), here's what happens:
* template_part('partials/content-single-audio.php');
* // $templates => ['partials/content-single-audio.php', 'partials/content-single.php', 'partials/content.php']
*
* @param string $template Delimeted template path
* @param array $context Context to be extracted and available within template
* @param string $delimeter
* @return void
*/
function template_part($template, array $context = [], $delimeter = '-')
{
$templateParts = explode($delimeter, str_replace('.php', '', (string) $template));
$templates[] = array_shift($templateParts);
foreach ($templateParts as $i => $templatePart) {
$templates[] = $templates[$i] . $delimeter . $templatePart;
}
$templates = array_map(function ($template) {
return $template . '.php';
}, $templates);
$template = locate_template(array_reverse($templates));
extract($context);
/** @noinspection PhpIncludeInspection */
include apply_filters('sage/locate_template', $template, $templates) ?: $template;
}
Call it in your template files as something like...
<?php App\template_part('partials/content-single-audio', ['user'=>'johndoe']); ?>
Now the variable $user will be available in the included template (see code comments above for how the specified template is broken down), and its value will be 'johndoe'. There will also be a variable named $context that is available, which is just the array you passed to template_part().
The function above is untested. I just based it on the code I had in my Template class, which was also largely untested. You'll have to test it and troubleshoot it on your own if you decide to use it.
I'm currently using @Foxaii approach and works perfect :+1:
Maybe most of the people don't see a need to pass data from one template part to another though I can say it's super useful! I've worked with Twig (the Symfony template engine) in the past and it incorporates the ability to do that by default. As I said is super handy when you want to write DRY code, reuse partials and create CSS modifiers.
Example...
_source-template.php_
<?php
$css_class = 'contact--light';
include locate_template('./templates/target-template.php', false, false);
?>
_target-template.php_
<div class="contact <?php echo $css_class; ?>">
<!-- Your HTML code here -->
</div>
I ran across this solution which is pretty nice. Provides cache ability as well.
In a similar way, I had to refactor a block of HTML which contents could vary. Parameters wouldn't be sufficient because the needed inner HTML had to be quite different. I ended up with this: https://gist.github.com/nicooprat/2fbdfbf08344a820097cc62166772136
````php
Child 1
Child 2
@nicooprat
I'm using @ferromariano 's get_component from above but passing it a child component like this (set $output false on the child component)
get_component('templates/foo', array(
'child' => get_component('templates/bar', array(), false)
));
then in the foo component you would just output the child with echo $child
In blade (Sage 9) you can simply pass variables as the second parameter without polluting the globals, like:
@include('partials.component', $componentData)
@christianmagill do you have an example use case for this? Data passed, and how it's used in the component? This is a great idea and somehting I've been thinking about - more intelligent components than get_template_part()...
for anyone coming across this issue and still using older versions of sage, as of wp 5.5 get_template_part now supports passing variables
https://make.wordpress.org/core/2020/07/17/passing-arguments-to-template-files-in-wordpress-5-5/
Most helpful comment
I've solved this type of thing using a
set_query_varjust beforeget_template_part.e.g.
then inside the template you can now use the
$mydata_varvariable: