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

Add support for synced theme patterns (with overrides) #59272

Open
fabiankaegy opened this issue Feb 22, 2024 · 22 comments
Open

Add support for synced theme patterns (with overrides) #59272

fabiankaegy opened this issue Feb 22, 2024 · 22 comments
Labels
[Block] Pattern Affects the Patterns Block [Feature] Extensibility The ability to extend blocks or the editing experience [Feature] Patterns A collection of blocks that can be synced (previously reusable blocks) or unsynced [Feature] Synced Patterns Related to synced patterns (formerly reusable blocks) [Type] Enhancement A suggestion for improvement.

Comments

@fabiankaegy
Copy link
Member

Synced Patterns with the new Pattern Overides are a really powerful feature. This ability to have a shared design with content that is unique to the instances is really powerful.

Synced patterns only exist in the database though. They need to be created by a user and can be edited visually in the site editor.

However on top of this it would be great for themes to be able to also ship pattern files with a metadata header to indicate that they should be synced. These patterns should work kind of like templates and template parts where they use the file on disc as the source of truth untill someone goes in to edit the main instance at which point it would get moved to the database.

The reason this would be so powerful is that in custom development work we mainly develop custom themes for sites. These custom themes ship with a bunch of patterns and are used across multiple environments (development, staging, production etc.). With Git workflows it is really easy to move these pattern files across the environments and manage which version should be available where. This breaks down when something only exists in the Database though. Because either you would need to somehow sync parts of the database up which is usually not a good idea, or you need to manually create a synced pattern in every environment from scratch. This would be solved if a pattern could be marked as synced even in code.

This would also make pattern overrides something that we could start to use for synced patterns via code. Because currently today you can only really use that feature through the UI.

@fabiankaegy fabiankaegy added [Type] Enhancement A suggestion for improvement. [Feature] Synced Patterns Related to synced patterns (formerly reusable blocks) [Feature] Patterns A collection of blocks that can be synced (previously reusable blocks) or unsynced [Block] Pattern Affects the Patterns Block labels Feb 22, 2024
@fabiankaegy
Copy link
Member Author

An example of what this could look like is:

<?php
/**
 * Title: Centered Statement
 * Slug: twentytwentyfour/centered-statement
 * Categories: text, about, featured
 * Keywords: statement, centered
 * Viewport width: 1400
 * Syncing: synced // or unsynced which would be the default
 */
?>

<!-- wp:group {"align":"full","style":{"spacing":{"padding":{"top":"var:preset|spacing|60","bottom":"var:preset|spacing|60","left":"var:preset|spacing|60","right":"var:preset|spacing|60"},"margin":{"top":"0","bottom":"0"}}},"backgroundColor":"base-2","layout":{"type":"constrained"}} -->
<div class="wp-block-group alignfull has-base-2-background-color has-background" style="margin-top:0;margin-bottom:0;padding-top:var(--wp--preset--spacing--60);padding-right:var(--wp--preset--spacing--60);padding-bottom:var(--wp--preset--spacing--60);padding-left:var(--wp--preset--spacing--60)"><!-- wp:group {"align":"wide","layout":{"type":"default"}} -->
<div class="wp-block-group alignwide"><!-- wp:spacer {"height":"var:preset|spacing|50"} -->
<div style="height:var(--wp--preset--spacing--50)" aria-hidden="true" class="wp-block-spacer"></div>
<!-- /wp:spacer -->

<!-- wp:paragraph {"align":"center","style":{"typography":{"lineHeight":"1.2","fontStyle":"normal","fontWeight":"400"}},"fontSize":"x-large","fontFamily":"heading"} -->
<p class="has-text-align-center has-heading-font-family has-x-large-font-size" style="font-style:normal;font-weight:400;line-height:1.2"><?php echo wp_kses_post( __( '<em>Études</em> is not confined to the past—we are passionate about the cutting edge designs shaping our world today.', 'twentytwentyfour' ) ); ?></p>
<!-- /wp:paragraph -->

<!-- wp:spacer {"height":"var:preset|spacing|50"} -->
<div style="height:var(--wp--preset--spacing--50)" aria-hidden="true" class="wp-block-spacer"></div>
<!-- /wp:spacer --></div>
<!-- /wp:group --></div>
<!-- /wp:group -->

@annezazu annezazu added the [Feature] Extensibility The ability to extend blocks or the editing experience label Feb 22, 2024
@carlomanf
Copy link

Is this not the same as template parts?

@talldan
Copy link
Contributor

talldan commented Feb 23, 2024

Thanks for creating the issue.

Agree it is quite similar to template parts, but it might not be a bad thing to make template parts and patterns more similar and improve the chances that they can be unified into one concept in the future.

I think it ties in with Patterns: allow overriding of theme patterns as well as duplication, which discusses making patterns work more like template parts in terms of the customization flows.

@fabiankaegy
Copy link
Member Author

The main thing for me here really is the ability to use pattern overrides with this. Which is not possible for template parts.

Also template parts don't show up in the post editor for regular users.

@gziolo
Copy link
Member

gziolo commented Feb 23, 2024

Thank you for filing this proposal. We were chatting about this idea during initial explorations and decided to start simply by covering only these patterns that were explicitly created in the admin interface.

However on top of this it would be great for themes to be able to also ship pattern files with a metadata header to indicate that they should be synced. These patterns should work kind of like templates and template parts where they use the file on disc as the source of truth untill someone goes in to edit the main instance at which point it would get moved to the database.

We would also need to have a way to store the copy of the pattern coming with the theme in the database as a Synced Pattern as soon as it’s used in one of the templates to account for the scenarios where the theme suddenly removes the pattern or the user switches to a different theme. Otherwise, that would mean that the site no longer is able to show the content for such pattern.

@fabiankaegy
Copy link
Member Author

@gziolo i don't think it is right that we need to create a version in the DB as soon as you insert it anywhere. I think it would suffice if you created the version in the DB at the time when someone modifies the pattern in the UI.

To me deleting the pattern from the theme actually is the same thing as someone deleting the synced pattern in the UI. It should get rid of the instances (if it wasn't modified)

The issue we run into if we copy it to the DB as soon as someone uses it anywhere is that we loose the ability to push code updates via git. Which would again make the feature much less usable.

@justintadlock
Copy link
Contributor

+💯 - There's not a week that goes by where I don't see this request from extenders.

Is this not the same as template parts?

In addition to what others have said, template parts are also HTML only and don't allow PHP.

@StreetDog71
Copy link

+💯 - There's not a week that goes by where I don't see this request from extenders.

Is this not the same as template parts?

In addition to what others have said, template parts are also HTML only and don't allow PHP.

Thanks for the heads up Justin. This would be a really powerful feature.
Let's make this happen!!! :)

@StreetDog71
Copy link

Besides all the advantages mentioned by @fabiankaegy concerning migration from one environment to another, wouldn't this also be great for dynamic stuff inside the patterns?
Being a PHP file, a pattern could have some dynamic stuff like this:

<?php if( get_post_meta( $post->ID, 'some-meta-field', true ) ) { ?>
   <!--whatever-block-you-want-to-show -->
<?php } else { ?>
   <!--some-other-block -->
<?php } ?>

It's a pretty basic example, but having the ability to use PHP in fully synced patterns would be a game changer. Or, am I missing something?

@talldan
Copy link
Contributor

talldan commented Mar 1, 2024

@StreetDog71 That's an interesting one. You might also be interested in this proposal - #57719.

I think it would work, except for when a user duplicates that pattern—that essentially turns it into a wp_block post, so it would be converted to static content. That might not be what a user would expect. So it's probably not the right solution for dynamic content like that long term.

@fabiankaegy
Copy link
Member Author

I think there may be an additional feature request that comes after this one which is to allow for setting a pattern as "locked" meaning it cannot get modified via the UI. This would allow for some of these usecases and would prevent the copy in the database to get created.

This ties closely to the discussion in #59480

@carolinan
Copy link
Contributor

I can also second that I receive questions about synced pattern workflows and requests for this feature.

@brettsmason
Copy link

I can only echo the other comments here - this is a really crucial feature to me as as a developer in an agency. Being able to version control synced patterns with overrides is the dream and will greatly improve the flow.

@StreetDog71
Copy link

StreetDog71 commented Mar 9, 2024

Besides all the advantages mentioned by @fabiankaegy concerning migration from one environment to another, wouldn't this also be great for dynamic stuff inside the patterns? Being a PHP file, a pattern could have some dynamic stuff like this:

<?php if( get_post_meta( $post->ID, 'some-meta-field', true ) ) { ?>
   <!--whatever-block-you-want-to-show -->
<?php } else { ?>
   <!--some-other-block -->
<?php } ?>

It's a pretty basic example, but having the ability to use PHP in fully synced patterns would be a game changer. Or, am I missing something?

Looks like I was actually missing something and that it's possible to do what I want, although it might not be related to the original issue.

If I create a pattern named main-header.php, for instance, and insert it in a template part, it will be rendered by the template and lose connection to the original php file.
However, I can add the pattern like this to a template part (e.g. header.html):
<!-- wp:pattern {"slug”:”namespace/main-header"} /-->

In this case, the pattern will be rendered each time the template part is displayed. Whenever I make some changes to the php file, they will be shown when the template part is displayed.

As I said before, it won't fix the original issue, but it allows me to use synced dynamic data inside template parts, which is pretty damn sweet! ;)

@talldan
Copy link
Contributor

talldan commented Mar 13, 2024

@StreetDog71 I think the downside is that as soon as you edit the template part and save the changes, you'll lose the connection to the PHP file. The wp:pattern block automatically inserts its content. See the code here:

// Run this effect when the component loads.
// This adds the Pattern's contents to the post.
// This change won't be saved.
// It will continue to pull from the pattern file unless changes are made to its respective template part.
useEffect( () => {
if ( ! hasRecursionError && selectedPattern?.blocks ) {
try {
parsePatternDependencies( selectedPattern );
} catch ( error ) {
setHasRecursionError( true );
return;
}
// We batch updates to block list settings to avoid triggering cascading renders
// for each container block included in a tree and optimize initial render.
// Since the above uses microtasks, we need to use a microtask here as well,
// because nested pattern blocks cannot be inserted if the parent block supports
// inner blocks but doesn't have blockSettings in the state.
window.queueMicrotask( () => {
const rootClientId = getBlockRootClientId( clientId );
// Clone blocks from the pattern before insertion to ensure they receive
// distinct client ids. See https://github.com/WordPress/gutenberg/issues/50628.
const clonedBlocks = selectedPattern.blocks.map( ( block ) =>
cloneBlock(
injectThemeAttributeInBlockTemplateContent( block )
)
);
// If the pattern has a single block and categories, we should add the
// categories of the pattern to the block's metadata.
if (
clonedBlocks.length === 1 &&
selectedPattern.categories?.length > 0
) {
clonedBlocks[ 0 ].attributes = {
...clonedBlocks[ 0 ].attributes,
metadata: {
...clonedBlocks[ 0 ].attributes.metadata,
categories: selectedPattern.categories,
},
};
}
const rootEditingMode = getBlockEditingMode( rootClientId );
registry.batch( () => {
// Temporarily set the root block to default mode to allow replacing the pattern.
// This could happen when the page is disabling edits of non-content blocks.
__unstableMarkNextChangeAsNotPersistent();
setBlockEditingMode( rootClientId, 'default' );
__unstableMarkNextChangeAsNotPersistent();
replaceBlocks( clientId, clonedBlocks );
// Restore the root block's original mode.
__unstableMarkNextChangeAsNotPersistent();
setBlockEditingMode( rootClientId, rootEditingMode );
} );
} );
}
}, [
clientId,
hasRecursionError,
selectedPattern,
__unstableMarkNextChangeAsNotPersistent,
replaceBlocks,
getBlockEditingMode,
setBlockEditingMode,
getBlockRootClientId,
] );

You could probably minimize the risk by having a template part that contains only the pattern, and then making sure you don't edit it 😄

I think this is why making Gutenberg use wp:block as a persistent wrapper for patterns and using slugs everywhere would be better. With the 'States' proposal perhaps being the eventual north star for this kind of dynamic behavior.

@gaambo
Copy link
Contributor

gaambo commented Mar 15, 2024

Besides all the advantages mentioned by @fabiankaegy concerning migration from one environment to another, wouldn't this also be great for dynamic stuff inside the patterns? Being a PHP file, a pattern could have some dynamic stuff like this:

<?php if( get_post_meta( $post->ID, 'some-meta-field', true ) ) { ?>
   <!--whatever-block-you-want-to-show -->
<?php } else { ?>
   <!--some-other-block -->
<?php } ?>

It's a pretty basic example, but having the ability to use PHP in fully synced patterns would be a game changer. Or, am I missing something?

Looks like I was actually missing something and that it's possible to do what I want, although it might not be related to the original issue.

If I create a pattern named main-header.php, for instance, and insert it in a template part, it will be rendered by the template and lose connection to the original php file. However, I can add the pattern like this to a template part (e.g. header.html): <!-- wp:pattern {"slug”:”namespace/main-header"} /-->

In this case, the pattern will be rendered each time the template part is displayed. Whenever I make some changes to the php file, they will be shown when the template part is displayed.

As I said before, it won't fix the original issue, but it allows me to use synced dynamic data inside template parts, which is pretty damn sweet! ;)

FYI: I think time of evaluation is important here to use really dynamic PHP.
In my testing, the pattern's PHP is loaded upfront as soon as it is registered via include/require → which means PHP is evaluated.
That means the PHP Is evaluated when the pattern is added to the post. So any dynamic PHP in patterns that uses "contextual" data (post ID, context from parent blocks, ...) might break or at least work different from expected.
In my testing, even putting the pattern inside a template-part via wp:pattern block did not output a get_the_ID() correctly.

At least that is the case in 6.4. I think 6.5 added some lazy loading/caching for theme pattern files (?).
IIRC, the reason to allow PHP in patterns was to allow using i18n functions (__,_x) in them. For these functions, it doesn't matter when they get evaluated.

For patterns that should always be loaded and evaluated from the theme, this would be great, though. It will replace the usage of some custom dynamic blocks. Although, just using (=outputting) a meta value should be possible with block bindings now.

@StreetDog71
Copy link

@gaambo yep, I figured this out the hard way.
PHP code directly inside the pattern files won't return the post ID, but a custom block inside the same file will.
I guess custom blocks is the way to go for now.

@gziolo
Copy link
Member

gziolo commented Mar 18, 2024

We are getting to the point where two special types of core blocks would become very handy when working with patterns in particular.

The Condition/If block would allow replacing the following code shared:

<?php if( get_post_meta( $post->ID, 'some-meta-field', true ) ) { ?>
   <!--whatever-block-you-want-to-show -->
<?php } else { ?>
   <!--some-other-block -->
<?php } ?>

with something closer to:

<!-- wp/if {"something": "allowing", "to": "run", "the":"condition"} -->
    <!--whatever-block-you-want-to-show -->
<!-- /wp:if -->
<!-- wp/if {"something-else": "allowing", "to": "run", "the":"condition"} -->
    <!--some-other-block -->
<!-- /wp:if -->

We could use Block Bindings API now to have the opportunity to compute the condition on the server.

Another similar block that would be interesting to explore is Repeater/Loop block.

@ndiego
Copy link
Member

ndiego commented Mar 18, 2024

<!-- wp/if {"something": "allowing", "to": "run", "the":"condition"} -->
    <!--whatever-block-you-want-to-show -->
<!-- /wp:if -->
<!-- wp/if {"something-else": "allowing", "to": "run", "the":"condition"} -->
    <!--some-other-block -->
<!-- /wp:if -->

Just wanted to note that this may impact layout and dimension settings in the Editor if container blocks are used.

@gziolo
Copy link
Member

gziolo commented Mar 18, 2024

Just wanted to note that this may impact layout and dimension settings in the Editor if container blocks are used.

Similar challenges exist for the Synced Pattern block, or for Post Template block. I'm not sure if they print wrappers on the frontend. In particular, if you want to hide a group of blocks it would be more convienient to wrap them with a virtual block. An alternative is always to make it another attribute on individual blocks, and that would be much simpler to implement.

@fabiankaegy
Copy link
Member Author

Having thought this through a little more I really think that essentially the ideal implementation of this in my head would be something very close to how block-based template parts are working today.

Instead of an ID, the pattern should store a slug which then either gets resolved to the modified version in the Database or to the file on disk.

If the user theme deletes/renames a pattern and there is no version in the DB it works the same as when you delete a template part or a synced pattern today. The content is lost and no longer displays.

@bacoords
Copy link
Contributor

bacoords commented Apr 4, 2024

We are getting to the point where two special types of core blocks would become very handy when working with patterns in particular.
The Condition/If block...

Another thought on this is to bring it closer in line with Block Bindings AND what Block Visibility Plugin does, rather than adding a new structural element, conditional rendering could be based on an attribute on a block.

A (very rough) concept of this would be:

<!-- wp:group {
	"metadata":{
		"bindings":{
			"visible":{
				"source":"namespace/slug",
				"args":{
					"key":"some-field"
					"compare": "EXISTS"
				}
			},
		}
	}
} -->

This would eliminate the need for additional elements as the condition would be baked into existing blocks. Then visibility could be directly tied to key/value/compare arguments similar to a meta query (but not limited to it).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Block] Pattern Affects the Patterns Block [Feature] Extensibility The ability to extend blocks or the editing experience [Feature] Patterns A collection of blocks that can be synced (previously reusable blocks) or unsynced [Feature] Synced Patterns Related to synced patterns (formerly reusable blocks) [Type] Enhancement A suggestion for improvement.
Projects
Development

No branches or pull requests