Skip to content

WP_Widget::update() called multiple times, sometimes without any form data, when creating a widget #33597

@anastis

Description

@anastis

Description

It looks like the new block widgets screen (or the legacy widget block) doesn't work correctly when there are instance data returned from WP_Widget::update() that are not present as form fields in WP_Widget::form().

This is usually the case with repeating fields, where you might have a few html array inputs e.g. name="text[]"
and name="url[]" but want to store them in the widget's instance structured properly as a multi-dimensional array, e.g.

array(
	array(
		'text' => 'value',
		'url'   => 'value',
	),
)

It looks like that when pressing the top-right Update button of the widget's screen, a previous copy of $instance is passed to WP_Widget::update(), instead of the WP_Widget::form()'s form fields.
In contrast, when the widget validates in the background (e.g. when the title is changed), the correct data are passed to WP_Widget::update(), however, since the Update button is the last thing save action, the correct instance data are overwritten.

The above can be observed on the new block widgets screen, but not inside the Customizer's new block widgets interface.

Furthermore, as can be seen in the reproduction step below, it looks like the widget validates 2 or 3 times on every field change. This can be observed both in dev tool and in the php log.

Step-by-step reproduction instructions

  1. In a plugin or theme, add the widget code from the "Code snippet" section below.
  2. Open your php log file.
  3. Go to Appearance > Widgets. Assign the Test Legacy Widget into a sidebar.
  4. Type a single letter into the title field. Don't do anything else.
  5. Check the log. 2 or 3 entries will have been logged for some reason. $new_instance contains the field 'form_repeating_field' (which is present in form()) which is correct.
  6. Press the page's Update button.
  7. Check the log. $new_instance does not contain the field 'form_repeating_field' as expected, which is not correct. However, it contains 'instance_repeating_field' which suggests that a previous copy of the widget's instance data are passed to update().

Expected behaviour

Widget's instance should persist/contain the key 'instance_repeating_field' along with its data.

Actual behaviour

Widget's instance does not persist/contain the key 'instance_repeating_field'.

Screenshots or screen recording (optional)

https://www.dropbox.com/s/3jwurg1qvgvlnhf/2021-07-21%2016.27.46.gif?dl=0

Code snippet (optional)

class Test_Legacy_Widget_Block_Screen extends WP_Widget {

	protected $defaults = array(
		'title'       => '',
		'text'        => '',
		'data_fields' => array(),
	);

	public function __construct() {
		$widget_ops  = array( 'description' => 'Test legacy widget for the block widgets scrren' );
		$control_ops = array();
		parent::__construct( 'test-legacy-widget', 'Test Legacy Widget', $widget_ops, $control_ops );
	}

	public function widget( $args, $instance ) {
		$before_widget = $args['before_widget'];
		$after_widget  = $args['after_widget'];

		$title = apply_filters( 'widget_title', empty( $instance['title'] ) ? '' : $instance['title'], $instance, $this->id_base );

		echo $args['before_widget'];

		if ( $title ) {
			echo $args['before_title'] . $title . $args['after_title'];
		}

		if ( $instance['instance_repeating_field'] ) {
			foreach ( $instance['instance_repeating_field'] as $field ) {
				echo sprintf( '<p>%s</p>', $field );
			}
		}

		echo $args['after_widget'];
	}

	public function update( $new_instance, $old_instance ) {
		$instance = array();

		$instance['title'] = sanitize_text_field( $new_instance['title'] );

		if ( ! empty( $new_instance['form_repeating_field'] ) ) {
			foreach ( $new_instance['form_repeating_field'] as $field ) {
				$instance['instance_repeating_field'][] = sanitize_text_field( $field );
			}
			error_log( "new_instance['form_repeating_field'] is not empty. That's good." );
		} else {
			error_log( "new_instance['form_repeating_field'] is empty. That's bad." );
			if ( ! empty( $new_instance['instance_repeating_field'] ) ) {
				error_log( "...However new_instance['instance_repeating_field'] is NOT empty!" );
			}
		}

		return $instance;
	}

	public function form( $instance ) {
		$instance = wp_parse_args( (array) $instance, $this->defaults );

		$title = ! empty( $instance['title'] ) ? $instance['title'] : '';

		?>
		<p>
			<label for="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>">Title:</label>
			<input
				type="text"
				id="<?php echo esc_attr( $this->get_field_id( 'title' ) ); ?>"
				name="<?php echo esc_attr( $this->get_field_name( 'title' ) ); ?>"
				value="<?php echo esc_attr( $title ); ?>"
			/>
		</p>

		<input
			type="hidden"
			name="<?php echo esc_attr( $this->get_field_name( 'form_repeating_field' ) . '[]' ); ?>"
			value="Repeating 1"
		/>
		<input
			type="hidden"
			name="<?php echo esc_attr( $this->get_field_name( 'form_repeating_field' ) . '[]' ); ?>"
			value="Repeating 2"
		/>
		<input
			type="hidden"
			name="<?php echo esc_attr( $this->get_field_name( 'form_repeating_field' ) . '[]' ); ?>"
			value="Repeating 3"
		/>
		<?php
	}
}

register_widget( 'Test_Legacy_Widget_Block_Screen' );

WordPress information

  • WordPress version: 5.8
  • Gutenberg version: 11.1.0
  • Are all plugins except Gutenberg deactivated? Yes
  • Are you using a default theme (e.g. Twenty Twenty-One)? Yes - Twenty Twenty-One

Device information

  • Device: Desktop
  • Operating system: MacOS Big Sur 11.4
  • Browser: Firefox 90.0.1

Metadata

Metadata

Assignees

No one assigned

    Labels

    REST API InteractionRelated to REST API[Block] Legacy WidgetAffects the Legacy Widget Block - used for displaying Classic Widgets[Type] BugAn existing feature does not function as intended

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions