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

Dynamic Template Parts #32939

Open
justintadlock opened this issue Jun 23, 2021 · 15 comments
Open

Dynamic Template Parts #32939

justintadlock opened this issue Jun 23, 2021 · 15 comments
Labels
[Feature] Site Editor Related to the overarching Site Editor (formerly "full site editing") Needs Technical Feedback Needs testing from a developer perspective. [Type] Enhancement A suggestion for improvement.

Comments

@justintadlock
Copy link
Contributor

Because block templates are HTML, the only truly dynamic pieces are the blocks themselves. However, there are many instances where theme authors need to change output based on some context. The easiest and one of the most-used examples of this is template parts.

Traditionally, to avoid repeating code, theme authors would use get_template_part() and some variable to create a bit of a hierarchical system. Despite its weakness of only allowing a string instead of array of possible parts, it was still useful to do something like get_template_part( 'content', 'post' ) to search for a content-post.php with a fallback to content.php.

For more advanced use cases, developers had to go their own route with locate_template() (allows an array of template parts) or wrapper functions around it.

Without the ability to contextually load template parts, it is creating a scenario where theme authors must break the DRY principle.

Let's suppose you have an index.html file that loads the header, content, and footer.

<!-- wp:template-part { "slug":"header" } /-->

<!-- wp:template-part { "slug":"content" } /-->

<!-- wp:template-part { "slug":"footer" } /-->

Typically, the header and footer are not going to change often. However, if you want the "content" template to change for pages, you must create a top-level page.html template and content-page.html template:

<!-- wp:template-part { "slug":"header" } /-->

<!-- wp:template-part { "slug":"content-page" } /-->

<!-- wp:template-part { "slug":"footer" } /-->

Creating a whole new template to change one slug is not ideal.

If you want a different singular-view content template part, for example, you must then double up on those templates again with singular.html and content-singular.html.

Now, if you start breaking that down into smaller template parts, it becomes a tangle of repeated code. And, that means more and more opportunities for bugs.

Ideally, template parts would allow theme authors to inject some sort of hierarchy into them. One route is directly into the template part call:

<!-- wp:template-part { "slug":"content","hierarchy":["singular","single","post"] } /-->

That would look for content-singular.html, content-single.html, content-post.html, and content.html. That limits the options though because it is not dynamic.

A filter hook would be more useful for a true contextual system:

add_filter( 'wp_block_template_part_hierarchy', function( $hierarchy, $slug ) {
        if ( 'content' === $slug ) {
                // overwrite hierarchy
        }
        
        return $hierarchy;
}, 10, 2 );

I already have around a couple dozen templates/parts that should be entirely unnecessary in my block theme. I expect that number to only grow.

@iandunn
Copy link
Member

iandunn commented Aug 18, 2021

If I'm reading this right, you're not only suggesting improvements to the flexibility of template parts; that's just one example of a much bigger issue. Is that correct?

I've run into that too, a simple example is putting the current year in a footer: Copyright 2003 - <?php echo date( 'Y' ); ?> Foo Bar Inc.

Without the ability to execute arbitrary PHP/JS inside a template, custom blocks need to be created, even for trivial things. When building a custom theme for a single site, that doesn't feel practical to me.

One workaround is to create a plugin that registers an "example.org footer" block for the entire footer, but that feels a bit hacky to me.

@iandunn
Copy link
Member

iandunn commented Aug 18, 2021

Related: #27144, #20966, #21932

@mkaz
Copy link
Member

mkaz commented Sep 2, 2021

@iandunn I created a simple block to put the date in the footer. https://github.com/mkaz/copyright-block or https://wordpress.org/plugins/copyright-block I think it is even installable from the block directory.

So a potential solution for some of these micro dynamic pieces could additional blocks.

However, the ability to do logic and other conditionals based on tags, pages, and template hierarchy is going to be a need. I tried to build a different header background color based on category and it required many templates or some custom CSS that couldn't be done using global styles.

@overclokk
Copy link

I agree, having such system we could do what we do now with PHP templates.

@iandunn
Copy link
Member

iandunn commented Oct 22, 2021

Another random small thing would be linking to the homepage. In a classic theme I'd use home_url(), but it seem like in FSE I have to hardcode the production URL. My local domain is different than prod, so the link is broken locally.

( I'm talking about a regular link that's outside a nav menu. )

Adding a separate block for every template tag seems like a lot of clutter, but maybe a single block could wrap them all? e.g.,

<!-- wp:template-tag {"function":"home_url"} /-->
<!-- wp:template-tag {"function":"bloginfo","arguments":["admin_email"]} /-->

That would still be limited compared to PHP, though.

@mkaz
Copy link
Member

mkaz commented Oct 24, 2021

The Home Link block was created for linking to the homepage, but there are numerous other PHP function/values that will be useful in templates. Here is a related issue highlighting a similar need: #31815

@markhowellsmead
Copy link

Is this the correct ticket in which to mention conditional display? For example, I need to be able to switch between a query loop and a "no posts found" message in e.g. block-templates/index.html. Is this not possible, or have I just not found the documentation yet? (See also #29076 (comment).)

@iandunn
Copy link
Member

iandunn commented Nov 8, 2021

The query-title block should output "no results found", but doesn't do it in all situations yet: #33476

@markhowellsmead
Copy link

markhowellsmead commented Nov 9, 2021

Thanks for the information. But that's not sufficient for displaying a completely different combination of blocks when no posts are found. For example, adding text information, a sitemap, or a search form with more detailed instructions.

@iandunn
Copy link
Member

iandunn commented Nov 9, 2021

Ah, I see. Then yeah, this seems like a good issue to me 👍🏻

@lsl
Copy link
Contributor

lsl commented Nov 10, 2021

<!-- wp:template-part { "slug":"content","hierarchy":["singular","single","post"] } /-->

I like this api for the problem/solution.

I'm not sure on how and where this page/post/single/singular context is coming from? Is this something the rendering function must provide at a global level?

One thing I like about block html is that it can be rendered anywhere and, ignoring server side rendering, you always get the same result.

A filter hook would be more useful for a true contextual system:

I'd prefer any template hooks to be done in JS leaving the option to escape to php if one needs but doesn't require it for all template rendering. (I'm assuming there is an optimization where you can compile all the templates into a single network request or some sort of js bundle)

@tellyworth
Copy link
Contributor

tellyworth commented Nov 24, 2021

I was able to hack together a filter that could be used to emulate a crude version of dynamic templates from a theme's functions.php.

function conditional_template_part( $path, $file ) {
	// Crudely simulate the $name parameter to get_template_part() for the wp:template-part block
	// Example: <!-- wp:template-part {"slug":"foo-bar{-test}"} -->
	// will attempt to use "foo-bar-test", and fall back to "foo-bar" if that template file does not exist
	if ( false !== strpos( $path, '{' ) && !file_exists( $path ) ) {
		if ( preg_match( '/[{]([-\w]+)[}]/', $path, $matches ) ) {
			$name = $matches[1];
			// Try "foo-bar-test"
			$new_path = str_replace( '{' . $name . '}', $name, $path );
			if ( file_exists( $new_path ) ) {
				$path = $new_path;
			} else {
				// If that doesn't exist, try "foo-bar"
				$new_path = str_replace( '{' . $name . '}', '', $path );
				if ( file_exists( $new_path ) ) {
					$path = $new_path;
				}
			}
		}

	}

	return $path;
}
add_filter( 'theme_file_path', 'conditional_template_part', 10, 2 );

Obviously it only handles simple string values and doesn't handle any of the hierarchical or truly dynamic ideas discussed above. But it does work for simple cases equivalent to get_template_part( 'content', 'post' ) (ie <!-- wp:template-part { "slug":"content{-post}" } /-->). And I could imagine expanding the filter to support pseudo-variables, perhaps something like <!-- wp:template-part { "slug":"content{-TAGNAME}" } /-->

Note: this is only lightly tested, and I haven't really considered security.

@ryelle
Copy link
Contributor

ryelle commented May 4, 2022

I don't know that the "hierarchy":[…] solution really solves the issue in the description — or at least, it doesn't solve my issue 🙂 Even with this, you'd still need different page.html & single.html templates so you could define different fallbacks.

Maybe something like this, where the part value is one of a set of tokens that appends to the corresponding value to form a dynamic file path? ex, POST_TYPE would return the post type slug, giving content-post.html or content-page.html. It could still fall back to content.html if not found. I'm not sure what other tokens there could be, but thinking of the classic themes template hierarchy, taxonomy terms or author name come to mind.

<!-- wp:template-part { "slug":"content", "part": "POST_TYPE" } /-->

(naming things is hard, totally not tied to part as a name)

@justintadlock
Copy link
Contributor Author

The hierarchy solution would only solve issues where possible template part names are known and can be hardcoded. But, it won't solve everything because we cannot inject dynamic data into it.

I like the idea of having a set of tokens. It would be an extensive set and need to cover pretty much all the variables of global template hierarchy and object level (post, term, user, etc.).

With that said, starting with a POST_TYPE token would cover a lot of use cases.

@maharzan
Copy link

maharzan commented Oct 7, 2022

I am trying to make a block theme. I have a 3 column layout on the homepage (index.html) and want to add a 2 column layout as well. Its as easy as changing the columns:3 to columns:2 in the query (index.html) but I haven't found a way to do this. I want to have an option (in customizer) to set homepage columns. I do not want to use page templates (or set static front page). I have been searching for a solution but haven't found anything so far. Any suggestions are highly appreciated.

A simple code idea would have been

if($customizer_option_col_3) {
$query="columns=3";
} else {
$query="columns=2";
}

run wp_query($query);

@annezazu annezazu added [Feature] Site Editor Related to the overarching Site Editor (formerly "full site editing") and removed [Feature] Full Site Editing labels Jul 24, 2023
@jordesign jordesign added the [Type] Enhancement A suggestion for improvement. label Sep 14, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Feature] Site Editor Related to the overarching Site Editor (formerly "full site editing") Needs Technical Feedback Needs testing from a developer perspective. [Type] Enhancement A suggestion for improvement.
Projects
None yet
Development

No branches or pull requests