#30936 closed enhancement (fixed)
Dynamically create WP_Customize_Settings for settings created on JS client
Reported by: | westonruter | Owned by: | ocean90 |
---|---|---|---|
Milestone: | 4.2 | Priority: | normal |
Severity: | normal | Version: | 3.9 |
Component: | Customize | Keywords: | has-patch |
Focuses: | Cc: |
Description (last modified by )
When developing the Widget Customizer plugin, there was some hackery needed to support previewing the addition of new widgets. Normally when Widget Customizer boots up it creates a WP_Customize_Setting
for each widget instance that exists in the DB.
Another problem is that widgets_init
action also fires before customize_register
, so it is too late to call $setting->preview()
anyway to ensure that added widgets get registered.
To account for these issues, I implemented an ugly “pre-preview” system to intercept the incoming $_POST['customized']
JSON and basically duplicate the WP_Customize_Setting::preview()
logic to make sure that the newly-added widgets were being supplied via filters when widgets_init
happened, and then when customize_register
happened it could remove those filters, and then add the WP_Customize_Setting
objects properly.
This is all now in Core, and it is hacky.
I've been working on an alternate solution which would clean up Widget Customizer in core, and will be very helpful for Menu Customizer feature-as-plugin, in addition to other plugins (e.g. Customize Posts) that dynamically create settings on the client.
The work I've been doing is currently part of the introduction transactions for the Customizer, but the logic could be extracted to apply to the current system which relies on inspecting settings sent via $_POST[customized]
.
The work can be seen here: https://github.com/xwp/wordpress-develop/pull/61/files
The main piece is this new WP_Customize_Manager::add_dynamic_settings()
:
<?php final class WP_Customize_Manager { /* ... */ public function __construct() { /* ... */ add_action( 'customize_register', array( $this, 'register_controls' ) ); add_action( 'customize_register', array( $this, 'register_dynamic_settings' ), 11 ); // allow code to create settings first /* ... */ } /* ... */ /** * Register any dynamically-created settings, such as those in a transaction that have no corresponding setting created. * * This is a mechanism to "wake up" settings that have been dynamically created * on the frontend and have been added to a transaction. When the transaction is * loaded, the dynamically-created settings then will get created and previewed * even though they are not directly created statically with code. * * @todo $customized should store more than just key/value, but also serialized settings. The update_transaction call should include the setting configs. * * @param array $customized mapping of settings IDs to values * @return WP_Customize_Setting[] */ public function add_dynamic_settings( $customized ) { $new_settings = array(); foreach ( $customized as $setting_id => $value ) { if ( isset( $this->settings[ $setting_id ] ) || $this->get_setting( $setting_id ) ) { continue; } $setting_class = 'WP_Customize_Setting'; $args = false; /** * Allow non-statically created settings to be constructed with custom WP_Customize_Setting subclass. * * @since 4.2.0 * * @param string $class * @param string $setting_id */ $setting_class = apply_filters( 'customize_dynamic_setting_class', $setting_class, $setting_id ); /** * Filter a dynamic setting's constructor args. * * This filter must return an array, overriding the false default, to be * * @since 4.2.0 * * @param false|array $args * @param string $setting_id */ $setting_args = apply_filters( 'customize_dynamic_setting_args', $args, $setting_id ); if ( false === $setting_args ) { continue; } $setting = new $setting_class( $this, $setting_id, $setting_args ); $this->add_setting( $setting ); $new_settings[] = $setting; } return $new_settings; } /* ... */ /** * Add settings in the transaction that were not added with code, e.g. dynamically-created settings for Widgets * * @since 4.2.0 */ public function register_dynamic_settings() { // note here I'm using a transaction, but it could instead use json_decode( wp_unslash( $_POST['customized'] ) ) $this->add_dynamic_settings( $this->transaction->data() ); } }
So then for how this is actually used, see WP_Customize_Widgets
:
<?php final class WP_Customize_Widgets { /* ... */ /** * Mapping of setting type to setting ID pattern. * * @since 4.2.0 * @access protected * @var array */ protected $setting_id_patterns = array( 'widget_instance' => '/^(widget_.+?)(?:\[(\d+)\])?$/', 'sidebar_widgets' => '/^sidebars_widgets\[(.+?)\]$/', ); /* ... */ public function __construct( $manager ) { $this->manager = $manager; add_filter( 'customize_dynamic_setting_args', array( $this, 'filter_customize_dynamic_setting_args' ), 10, 2 ); add_action( 'after_setup_theme', array( $this, 'register_settings' ) ); /* ... */ } /* ... */ /** * Get the widget setting type given a setting ID. * * @since 4.2.0 * * @param $setting_id * * @return string|null */ protected function get_setting_type( $setting_id ) { static $cache = array(); if ( isset( $cache[ $setting_id ] ) ) { return $cache[ $setting_id ]; } foreach ( $this->setting_id_patterns as $type => $pattern ) { if ( preg_match( $pattern, $setting_id ) ) { $cache[ $setting_id ] = $type; return $type; } } return null; } /** * Inspect the transaction for any widget settings, and dynamically add them up-front so widgets will be initialized properly. * * @since 4.2.0 */ public function register_settings() { $widget_customized = array(); $all_customized = $this->manager->transaction->data(); // or this could get from json_decode( wp_unslash( $_POST['customized'] ) ) foreach ( $all_customized as $setting_id => $value ) { if ( $this->get_setting_type( $setting_id ) ) { $widget_customized[ $setting_id ] = $value; } } $settings = $this->manager->add_dynamic_settings( $widget_customized ); /* * Preview settings right away so that widgets and sidebars will get registered properly. * But don't do this if a customize_save because this will cause WP to think there is nothing * changed that needs to be saved. */ if ( ! $this->manager->doing_ajax( 'customize_save' ) ) { foreach ( $settings as $setting ) { $setting->preview(); } } } /* ... */ /** * Determine the arguments for a dynamically-created setting. This is used * when updating the transaction. * * @since 4.2.0 * * @param false|array $args * @param string $setting_id * @return false|array */ public function filter_customize_dynamic_setting_args( $args, $setting_id ) { if ( $this->get_setting_type( $setting_id ) ) { $args = $this->get_setting_args( $setting_id ); } return $args; } }
So you can see here that Widget Customizer is updated to register settings up front, eliminating the need for pre-preview. There is actually no need to defer settings to be created at customize_register
. Additionally, the current customized
data (aka the transaction) is looked at and any widget-specific settings are picked out and created as settings. This will ensure that added widgets will get recognized in time for widgets_Init
since the filters would be applying.
So as long as dynamically-created settings have IDs that follow a certain pattern (e.g. scheduled_background_colors[2015-03-01]
or term[421]
), then new settings can be created for them on the JS client and they'll be automatically created in PHP when the customized data is received if a filters are added such as:
<?php add_filter( 'customize_dynamic_setting_args', function ( $args, $setting_id ) { if ( preg_match( '/^scheduled_background_colors\[.+?\]$/', $setting_id ) ) { $args = array( 'type' => 'option', 'transport' => 'postMessage' ); } else if ( preg_match( '/^term\[\d+\]$/', $setting_id ) ) { $args = array( 'type' => 'term', ); } return $args; }, 10, 2 );
This would accompany any $wp_customize->add_setting()
calls for existing data in the customize_register
action.
This is distinct from #28580.
Attachments (7)
Change History (25)
This ticket was mentioned in Slack in #core-customize by westonruter. View the logs.
10 years ago
This ticket was mentioned in Slack in #core-customize by westonruter. View the logs.
10 years ago
This ticket was mentioned in Slack in #core-customize by westonruter. View the logs.
10 years ago
#7
@
10 years ago
Depends on #30988 (see comment:16)
#8
@
10 years ago
- Owner changed from westonruter to oecan90
- Status changed from assigned to reviewing
@ocean90: I've isolated the logic from the transactions patch and attached it here in 30936.diff. However, it needed to build on work done for #30988. So once you commit #30988, then I can refresh the patch to contain just the changes specific to this ticket.
This ticket was mentioned in Slack in #core-customize by westonruter. View the logs.
10 years ago
This ticket was mentioned in Slack in #core-customize by westonruter. View the logs.
10 years ago
This ticket was mentioned in Slack in #core by westonruter. View the logs.
10 years ago
@
10 years ago
Restore removed public methods as no-op ones marked as deprecated: https://github.com/xwp/wordpress-develop/commit/53f4c0ae033294a91b928fb43c5ce385d24a4887
This ticket was mentioned in Slack in #core-customize by ocean90. View the logs.
10 years ago
@
10 years ago
Additional change: https://github.com/xwp/wordpress-develop/commit/c5f40f11d1d9d998f3335a4dcca1d71c8a126559 With code style improvements from ocean90: https://github.com/xwp/wordpress-develop/commit/ee71fbe1ab64b0dc4768b074538c92b71033da73
#15
@
10 years ago
In 30936.6.diff, I fixed the issue that ocean90 identified where the form for a newly-created widget gets reverted to its default state after user input.
The solution is to override the incoming $_POST['customized']
for a newly-created widget's setting with the new $instance
so that the preview filter currently in place from WP_Customize_Setting::preview()
will use this value instead of the default widget instance value (an empty array). This uses a newly-introduced method WP_Customize_Manager::set_post_value()
.
@ocean90: thoughts?
I also tried to preserve your changes which I applied here: https://github.com/xwp/wordpress-develop/commit/ee71fbe1ab64b0dc4768b074538c92b71033da73
Assigning this to 4.2 milestone because there is a patch in #30937, and because it will facilitate the Menu Customizer feature plugin, as well as clean up some mess that was needed to incorporate widgets into the Customizer in 3.9