Make WordPress Core

Opened 15 years ago

Closed 15 years ago

Last modified 15 years ago

#8441 closed task (blessed) (fixed)

New Widget API based on MultiWidget Class

Reported by: thornomad's profile thornomad Owned by: azaozz's profile azaozz
Milestone: 2.8 Priority: normal
Severity: normal Version: 2.8
Component: Widgets Keywords: widget, plugin, multi-widget
Focuses: Cc:

Description

I have a feature enhancement for consideration in the trunk -- not sure how to go about getting it in there, so I thought I would post it here.

This came about when I tried to add mult-widget functionaility to a plugin I was contributing to ... I found the process to be far from easy and, although I got it to work, it required a lot of clunky coding.

Alex Tingle (lead devloper on the plugin) agreed, and created this enhancement to reduce the clunk: a php class called MultiWidget. The class handles all the nefarious multi-widget "tricks" required to implement a multi-widget plugin.

The benefit is that it will provide widget developers with a clean way to extend their plugins to multi-widget functionality without things "getting ugly" (as I see it).

See blog post explaining this at: http://blog.firetree.net/2008/11/30/wordpress-multi-widget/

See the code for this at: http://ec3.pastebin.com/fccfe0a0

See example usage of this class at: http://ec3.pastebin.com/f56a1539d

Again, not sure how to get it considered for inclusion in the Wordpress trunk, but I think it would benefit countless widget developers who, like me, run into some barriers when implementing a multi-widget.

Thanks,
Damon

Attachments (3)

multi_widget_class.php (11.3 KB) - added by dcole07 15 years ago.
Multi-Widget Class with commented Example
widget_setup.patch (6.3 KB) - added by scribu 15 years ago.
widgets.php (32.8 KB) - added by ceenz 15 years ago.

Download all attachments as: .zip

Change History (59)

#1 @thornomad
15 years ago

  • Cc Alex Tingle added; damontimm@… alex@… removed

#2 @alex_tingle
15 years ago

  • Version set to 2.8

@dcole07
15 years ago

Multi-Widget Class with commented Example

#3 @dcole07
15 years ago

I added (array) to Line 228 to clear up an error with with wrong data type.

This class or something similar should be added to WordPress 2.8. I don't see a point to having a single use widget and this Class make creating Multi-Widgets a breeze.

Whom ever is in charge of the Widgets for WordPress 2.8 should be assigned to this ticket.

#4 @GamerZ
15 years ago

Nice, hope this will make it into WP2.8

#5 @westi
15 years ago

  • Cc westi added
  • Component changed from General to Widgets
  • Milestone changed from 2.8 to 2.9
  • Owner anonymous deleted

A good idea in theory.

For this to go into core I would prefer something lighterweight and something which gets used by the core widget code.

#6 @Otto42
15 years ago

+1 brilliant. A base class for multiple-instance widgets is a dang good idea. A base class for a single instance widget would be a dang good idea as well.

#7 @ryan
15 years ago

  • Owner set to azaozz

#8 @azaozz
15 years ago

(In [10764]) Add WP_Widget class, first run - only the Links widget is using it at the moment, see #8441

#9 @azaozz
15 years ago

(In [10781]) WP_Widgets: make save_settings() and get_settings() separate methods, convert defined single widgets settings to multi-widget format, small fixes for the Links widget, see #8441

#10 @ryan
15 years ago

(In [10784]) Convert search to WP_Widget. Handle upgrade of widgets without settings. Make form and update override optional. see #8441

#11 @ryan
15 years ago

(In [10785]) Convert pages to WP_Widget. see #8441

#12 @ryan
15 years ago

(In [10786]) Prune. see #8441

#13 @kretzschmar
15 years ago

This is really amazing. While changing widgets to support this class add a standard way for widgets to be (optionally) displayed without a title.

#14 @scribu
15 years ago

  • Milestone changed from 2.9 to 2.8

Great to see this made it into 2.8

However, since the class can't be used by itself, wouldn't it be better to rename the constructor setup() ?

Another alternative would be to make setup() another method that needs to be overritten.

Thus, instead of

class WP_Widget_Pages extends WP_Widget {
	function WP_Widget_Pages() {
		$widget_ops = array('classname' => 'widget_pages', 'description' => __( "Your blog's WordPress Pages") );
		$this->WP_Widget('pages', __('Pages'), $widget_ops);
	}

we would have something much cleaner:

class WP_Widget_Pages extends WP_Widget {
	function setup() {
		$this->id_base = 'pages';
		$this->name = __('Pages');
		$this->widget_ops = array('classname' => 'widget_pages', 'description' => __( "Your blog's WordPress Pages") );
	}

setup() would be called form the WP_Widget constructor.

I'll write a patch for it if there are no objections.

#15 @scribu
15 years ago

  • Cc scribu@… added

#16 @ryan
15 years ago

(In [10794]) Move archives widget to WP_Widget. see #8441

#17 @ryan
15 years ago

(In [10796]) Move meta widget to WP_Widget. see #8441

#18 @ryan
15 years ago

(In [10797]) Move meta widget to WP_Widget. see #8441

#19 @ryan
15 years ago

(In [10799]) Move calendar widget to WP_Widget. see #8441

#20 @ryan
15 years ago

(In [10800]) Move calendar widget to WP_Widget. see #8441

#21 @ryan
15 years ago

(In [10802]) Move text widget to WP_Widget. see #8441

#22 @ryan
15 years ago

(In [10809]) Move categories widget to WP_Widget. see #8441

#23 @ryan
15 years ago

  • Summary changed from Add "MultiWidget" Class for Plugin Development to New Widget API based on MultiWidget Class
  • Type changed from enhancement to task (blessed)

#24 @ryan
15 years ago

(In [10812]) Add some phpdoc to WP_Widget. see #8441

#25 @Viper007Bond
15 years ago

Some PHP notices displayed in my sidebar and on the widgets page:

{{{Notice: Undefined index: links in [...]\wp-includes\widgets.php on line 722

Notice: Undefined index: links in [...]\wp-includes\widgets.php on line 723

Notice: Undefined index: links in [...]\wp-includes\widgets.php on line 728

Notice: Undefined index: links in [...]\wp-admin\includes\widgets.php on line 207

Notice: Undefined index: links in D:\Webserver\htdocs\wordpress-dev\wp-includes\widgets.php on line 739}}}

The error goes away if I hit Save Changes, so it looks to be an issue with the upgrade process or something.

#26 @Viper007Bond
15 years ago

Erm, paste screwed up:

Notice: Undefined index: links in [...]\wp-includes\widgets.php on line 722

Notice: Undefined index: links in [...]\wp-includes\widgets.php on line 723

Notice: Undefined index: links in [...]\wp-includes\widgets.php on line 728

Notice: Undefined index: links in [...]\wp-admin\includes\widgets.php on line 207

Notice: Undefined index: links in [...]\wp-includes\widgets.php on line 739

#27 @scribu
15 years ago

widget_setup.patch adds a new mandatory method called setup() where the widget args are defined.

This method is then called from the WP_Widget constructor.

The advantage is that you don't have to deal with messy constructors for each widget. Also, the code is easier to read.

#28 @ryan
15 years ago

Does calling a child method from the parent constructor work in all versions of PHP? Other languages won't allow that.

#29 @scribu
15 years ago

I'm not sure. Can somebody test this code in PHP4?

class parent_class {
  function parent_class() {
    $this->method();
  }
}

class child_class extends parent_class {
  function method() {
    echo "howdy";
  }
}

new child_class();

#31 @westi
15 years ago

That is so wrong and so ug.

I much prefer the cleanliness of constructors anyway.

-1 to setup function.

#32 @hakre
15 years ago

Id like to add A Title Option to the Search Widget as well an Option to not display the label for the Search Box. Any Problems with that?

#33 follow-up: @kretzschmar
15 years ago

  • Priority changed from normal to high
  • Type changed from task (blessed) to defect (bug)

There seems to be no 'safe' way to unregister default widgets and register new theme specif ones with the same name(overwriting).
I was able (with the use of 'do_action('widgets_init');) to register my pages widgets with register_widget(). But the widget is using the options array ($instance) from the default widget in the front end. In the backend everything is good.

#34 @ryan
15 years ago

  • Type changed from defect (bug) to task (blessed)

#35 @ryan
15 years ago

(In [10979]) unregister_widget(). see #8441

#36 follow-up: @ryan
15 years ago

I added unregister_widget(). Does that help? You should be able to unregister_widget('WP_Widget_Pages') and then register_widget('My_Widget_Pages') from a function hooked onto widgets_init.

#37 in reply to: ↑ 33 @azaozz
15 years ago

Replying to kretzschmar:

There seems to be no 'safe' way to unregister default widgets and register new theme specif ones with the same name(overwriting).

You can actually do that if you use different id_base. This is the bit used to identify the widget, save the settings, etc. and is set independently from the widget name. As long as the id_base is different, the widget is different.

#38 in reply to: ↑ 36 @kretzschmar
15 years ago

Replying to ryan:

I added unregister_widget(). Does that help? You should be able to unregister_widget('WP_Widget_Pages') and then register_widget('My_Widget_Pages') from a function hooked onto widgets_init.

This doesn't work here. There is no error showing up but the widgets aren't unregistered.

#39 follow-up: @DD32
15 years ago

This doesn't work here. There is no error showing up but the widgets aren't unregistered.

Try hooking later:

add_action('init', 'my_func', 20); //10 is default priority, higher runs later

#40 in reply to: ↑ 39 ; follow-up: @kretzschmar
15 years ago

Replying to DD32:

This doesn't work here. There is no error showing up but the widgets aren't unregistered.

Try hooking later:

add_action('init', 'my_func', 20); //10 is default priority, higher runs later

I already did this. And changing the id_base (as long as you meant the 'classname') also doesn't work.

#41 follow-up: @ryan
15 years ago

(In [10991]) Fix unregister_widget(). see #8441

#42 in reply to: ↑ 40 @azaozz
15 years ago

Replying to kretzschmar:

I already did this. And changing the id_base (as long as you meant the 'classname') also doesn't work.

The id_base is not the classname. It's the first arg passed to WP_Widget. Check some of the widgets subclasses in default-widgets.php:

class WP_Widget_Links extends WP_Widget {

	function WP_Widget_Links() {
		$widget_ops = array('description' => __( "Your blogroll" ) );
		$this->WP_Widget('links', __('Links'), $widget_ops);
	}

In this case 'links' is the id_base.

#43 in reply to: ↑ 41 @kretzschmar
15 years ago

Replying to ryan:

(In [10991]) Fix unregister_widget(). see #8441

Thanks Ryan, now everything works like charm even with the same 'id_base'.

@ceenz
15 years ago

#44 @ceenz
15 years ago

Hi,
I was wondering if it was possible to add a couple of hooks to the new widget api to allow for filtering the display of individual widgets based on options or criteria from a plugin, and secondly allow individual widgets to have applied to them unique options or display criteria applied to them by plugin.

This would be achieved by adding two filter hooks and one action hook to the update_callback, display_callback, and form_callback functions. I have included my recommendation.

New Filters:
display_widget_test
default_widget_options_update

New Action:
default_widget_options_display

	function update_callback( $widget_args = 1 ) {
		global $wp_registered_widgets;

		if ( is_numeric($widget_args) )
			$widget_args = array( 'number' => $widget_args );

		$widget_args = wp_parse_args( $widget_args, array( 'number' => -1 ) );
		$all_instances = $this->get_settings();

		// We need to update the data
		if ( !$this->updated && !empty($_POST['sidebar']) ) {

			// Tells us what sidebar to put the data in
			$sidebar = (string) $_POST['sidebar'];

			$sidebars_widgets = wp_get_sidebars_widgets();
			if ( isset($sidebars_widgets[$sidebar]) )
				$this_sidebar =& $sidebars_widgets[$sidebar];
			else
				$this_sidebar = array();

			if ( isset($_POST['delete_widget']) && $_POST['delete_widget'] ) {
				// Delete the settings for this instance of the widget
				if ( isset($_POST['widget-id']) )
					$del_id = $_POST['widget-id'];
				else
					return;

				if ( $this->_get_display_callback() == $wp_registered_widgets[$del_id]['callback'] && isset($wp_registered_widgets[$del_id]['params'][0]['number']) ) {
					$number = $wp_registered_widgets[$del_id]['params'][0]['number'];

					if ( $this->id_base . '-' . $number == $del_id ) {
						unset($all_instances[$number]);
					}
				}
			} else {
				foreach ( (array) $_POST['widget-' . $this->id_base] as $number => $new_instance ) {
					$new_instance = stripslashes_deep($new_instance);
					$this->_set($number);

					if ( isset($all_instances[$number]) )
						$instance = $this->update($new_instance, $all_instances[$number]);
					else
						$instance = $this->update($new_instance, array());

					if ( false !== $instance ) {
					  $widget_details_compare = array($new_instance,$instance); 
            $instance = apply_filters('default_widget_options_update',$widget_details_compare);  //Callback to apply default or other options to be added to list of widget options to store in database
						$all_instances[$number] = $instance;
						
					}
				}
			}

			$this->save_settings($all_instances);
			$this->updated = true;
		}
	}
	function display_callback( $args, $widget_args = 1 ) {
		if ( is_numeric($widget_args) )
			$widget_args = array( 'number' => $widget_args );

		$widget_args = wp_parse_args( $widget_args, array( 'number' => -1 ) );
		$this->_set( $widget_args['number'] );
		$settings = $this->get_settings();

		if ( array_key_exists( $this->number, $settings ) ) {
      $hide_widget = apply_filters('widget_display_test', $settings[$this->number]); //Add hook to test if widget should be displayed.
      if (!$hide_widget) $this->widget($args, $settings[$this->number]); //Test and display widget.
      $this->widget($args, $settings[$this->number]);
    }
	}

	function form_callback( $widget_args = 1 ) {
		if ( is_numeric($widget_args) )
			$widget_args = array( 'number' => $widget_args );

		$widget_args = wp_parse_args( $widget_args, array( 'number' => -1 ) );
		$all_instances = $this->get_settings();

		if ( -1 == $widget_args['number'] ) {
			// We echo out a form where 'number' can be set later
			$this->_set('__i__');
			$instance = array();
		} else {
			$this->_set($widget_args['number']);
			$instance = $all_instances[ $widget_args['number'] ];
		}

		$this->form($instance);
  	$widget_details = array($this->id_base,$this->number,$instance);
    do_action('default_widget_options_display', $widget_details); //Add action hook to display default widget information or options
	}

Please let me know what you think. I believe that these three small changes would eaisly allow for the adding of plugins to filter the display of individual widgets and add widget options to unique instances of multiwidgets.

Many thanks,
Chris

#45 follow-up: @Denis-de-Bernardy
15 years ago

+1 to that. can you make a patch?

#46 @ryan
15 years ago

  • Resolution set to fixed
  • Status changed from new to closed

Closing this tracker bug. New tickets for further issues.

#47 @azaozz
15 years ago

(In [11087]) Move recent posts widget to WP_Widget. see #8441

#48 @azaozz
15 years ago

(In [11088]) Fix conversion of old widget settings for Recent Posts, see #8441

#49 @azaozz
15 years ago

(In [11090]) Move recent comments widget to WP_Widget, extend is_active_widget() to use $id_base, see #8441

#50 in reply to: ↑ 45 @ceenz
15 years ago

Replying to Denis-de-Bernardy:

+1 to that. can you make a patch?

See Ticket #9651

#51 @azaozz
15 years ago

(In [11093]) Move RSS widget to WP_Widget, see #8441

#52 @azaozz
15 years ago

(In [11094]) Move Tag Cloud widget to WP_Widget, see #8441

#53 @azaozz
15 years ago

(In [11095]) Add hooks to WP_Widget, props ceenz, fixes #9651, see #8441

#54 @hakre
15 years ago

For the improvements of the Search Widget I opened a new ticket: #9756 .

#55 @junsuijin
15 years ago

  • Priority changed from high to low
  • Severity changed from normal to minor
  • Type changed from task (blessed) to defect (bug)

I really like the new widgets administration. I've remade all my widgets with the new API (fairly trivial since I was already utilizing the MultiWidget Class), and I've recently come across one bit of strange behavior while using the nightly build:

When attempting to display widgets via returning the cached contents in the way that the default recent posts widget does, my 3rd sidebar (always the 3rd registered, no matter which is registered 3rd, but possibly other sidebars also) will only display additional instances (past the first instance in the sidebar) of any given widget. That is to say, I would have to place 2 recent posts widgets in my 3rd sidebar to get just 1 to output.

In order to counter the problem, I've changed my own widgets that use similar caching methods, so that this bit of code:

if (isset($cache[$args['widget_id']]))
   return $cache[$args['widget_id']];

reads instead as such:

if (isset($cache[$args['widget_id']]))
   echo substr($cache[$args['widget_id']][1], 1);

Is this a bug that could/should be easily fixed before the official 2.8 release?

#56 @junsuijin
15 years ago

  • Priority changed from low to normal
  • Severity changed from minor to normal
  • Type changed from defect (bug) to task (blessed)
Note: See TracTickets for help on using tickets.