1

I created simple php abstract widget class that can generate form with fields from an array. When I try to save fields, all changes are removed.

Abstract Widget Class:

<?php
/**
 * Abstract widget class
 *
 * @class MOD_Widget
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * MOD_Widget
 *
 * @version  1.0.0
 * @extends  WP_Widget
 */
abstract class MOD_Widget extends WP_Widget {

    /**
     * CSS class.
     *
     * @var string
     */
    public $widget_cssclass;

    /**
     * Widget description.
     *
     * @var string
     */
    public $widget_description;

    /**
     * Widget ID.
     *
     * @var string
     */
    public $widget_id;

    /**
     * Widget name.
     *
     * @var string
     */
    public $widget_name;

    /**
     * Fields.
     *
     * @var array
     */
    public $fields;

    /**
     * Constructor.
     */
    public function __construct() {
        $widget_ops = array(
            'classname'   => $this->widget_cssclass,
            'description' => $this->widget_description,
            'customize_selective_refresh' => true,
        );

        parent::__construct( $this->widget_id, $this->widget_name, $widget_ops );
    }

    public function save_fields( $new_instance, $old_instance, $parent_container = null ) {

        // Vars
        $instance = $old_instance;
        $widget_fields = $this->fields;
        if( isset( $parent_container ) ){
            $widget_fields = $widget_fields[ $parent_container ]['sub_fields'];
        }

        // Loop fields and get values to save.
        foreach ( $widget_fields as $key => $setting ) {
            $setting_type = isset( $setting['type'] ) ? $setting['type'] : '';

            // Format the value based on fields type.
            switch ( $setting_type ) {
                case 'group':
                        $group_instance = $this->save_fields( $new_instance, $old_instance, $key );
                        $instance = array_merge( $group_instance, $instance );
                    break;
                case 'number':
                    $instance[ $key ] = (int) $new_instance[ $key ];

                    if ( isset( $setting['min'] ) && '' !== $setting['min'] ) {
                        $instance[ $key ] = max( $instance[ $key ], $setting['min'] );
                    }

                    if ( isset( $setting['max'] ) && '' !== $setting['max'] ) {
                        $instance[ $key ] = min( $instance[ $key ], $setting['max'] );
                    }
                    break;
                case 'text':
                    $instance[ $key ] = isset( $new_instance[ $key ] ) ? sanitize_text_field( $new_instance[ $key ] ) : ( isset( $setting['default_value'] ) ? $setting['default_value'] : '' );
                    break;
                case 'textarea':
                    $instance[ $key ] = wp_kses_post( $new_instance[ $key ] );
                    break;
                case 'checkbox':
                    $instance[ $key ] = empty( $new_instance[ $key ] ) ? 0 : 1;
                    break;
                default:
                    $instance[ $key ] = isset( $new_instance[ $key ] ) ? sanitize_text_field( $new_instance[ $key ] ) : ( isset( $setting['default_value'] ) ? $setting['default_value'] : '' );
                    break;
            }
        }

        return $instance;
    }

    /**
     * Handles updating settings for the current widget instance.
     */
    public function update( $new_instance, $old_instance ) {

        $instance = $old_instance;

        if ( empty( $this->fields ) ) {
            return $instance;
        }

        $instance = $this->save_fields( $new_instance, $old_instance );

        return $instance;
    }

    /**
    * Back-end widget fields
    */
    public function field_generator( $instance, $parent_container = null ) {

        // Vars
        $widget_fields = $this->fields;
        if( isset( $parent_container ) ){
            $widget_fields = $widget_fields[ $parent_container ]['sub_fields'];
        }

        foreach ( $widget_fields as $key => $setting ) {
            $setting_type = isset( $setting['type'] ) ? $setting['type'] : '';
            $input_css_classes = isset( $setting['class'] ) ? $setting['class'] : '';

            if( !isset( $setting_type ) ){
                return;
            }

            if( 'group' !== $setting_type ){
                $default_value = isset( $setting['default_value'] ) ? $setting['default_value'] : '';
                $value = isset( $instance[ $key ] ) ? $instance[ $key ] : $default_value;
            } else {
                $value = '';
            }

            switch ( $setting_type ) {
                case 'group':
                    ?>
                    <div id="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" class="mod-widget-section<?php if ( ! empty( $input_css_classes ) ) echo ' ' . implode( ' ', $input_css_classes ); ?>">
                        <div class="section-header"><?php echo esc_html( $setting['label'] ); ?></div>
                        <div class="section-content">

                            <?php
                            if ( !isset( $setting['sub_fields'] ) || empty( $setting['sub_fields'] ) ) {
                                echo '<p>' . esc_html( 'This section is empty.', 'mod' ) . '</p></div></div></div>';

                                return;
                            } ?>

                            <?php $this->field_generator( $instance, $key ); ?>

                        </div>
                    </div>
                    <?php
                    break;

                case 'text':
                    ?>
                    <div class="mod-widget-option mod-option-type-<?php echo esc_attr( $setting['type'] ); ?>">
                        <label for="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" class="mod-field-label"><?php echo esc_html( $setting['label'] ); ?></label>
                        <input id="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" type="text" class="widefat<?php if ( ! empty( $input_css_classes ) ) echo ' ' . implode( ' ', $input_css_classes ); ?>" name="<?php echo esc_attr( $this->get_field_name( $key ) ); ?>" value="<?php echo esc_attr( $value ); ?>" />
                        <?php if ( ! empty( $setting['desc'] ) ) : ?>
                            <p class="small-desc"><?php echo esc_html( $setting['desc'] ); ?></p>
                        <?php endif; ?>
                    </div>
                    <?php
                    break;

                case 'textarea':
                    ?>
                    <div class="mod-widget-option mod-option-type-<?php echo esc_attr( $setting['type'] ); ?>">
                        <label for="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" class="mod-field-label"><?php echo esc_html( $setting['label'] ); ?></label>
                        <textarea id="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" class="widefat<?php if ( ! empty( $input_css_classes ) ) echo ' ' . implode( ' ', $input_css_classes ); ?>" name="<?php echo esc_attr( $this->get_field_name( $key ) ); ?>" rows="4" cols="20"><?php echo esc_textarea( $value ); ?></textarea>
                        <?php if ( ! empty( $setting['desc'] ) ) : ?>
                            <p class="small-desc"><?php echo esc_html( $setting['desc'] ); ?></p>
                        <?php endif; ?>
                    </div>
                    <?php
                    break;

                case 'number':
                    ?>
                    <div class="mod-widget-option mod-option-type-<?php echo esc_attr( $setting['type'] ); ?>">
                        <label for="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" class="mod-field-label"><?php echo esc_html( $setting['label'] ); ?></label>
                        <input id="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" type="number" class="tiny-text<?php if ( ! empty( $input_css_classes ) ) echo ' ' . implode( ' ', $input_css_classes ); ?>" name="<?php echo esc_attr( $this->get_field_name( $key ) ); ?>" step="<?php echo esc_attr( $setting['step'] ); ?>" min="<?php echo esc_attr( $setting['min'] ); ?>" <?php if ( ! empty( $setting['max'] ) ) : ?>max="<?php echo esc_attr( $setting['max'] ); ?>"<?php endif; ?> value="<?php echo esc_attr( $value ); ?>" />
                        <?php if ( ! empty( $setting['desc'] ) ) : ?>
                            <p class="small-desc"><?php echo esc_html( $setting['desc'] ); ?></p>
                        <?php endif; ?>
                    </div>
                    <?php
                    break;

                case 'checkbox':
                    ?>
                    <div class="mod-widget-option mod-option-type-<?php echo esc_attr( $setting['type'] ); ?>">
                        <input id="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" type="checkbox" class="checkbox<?php if ( ! empty( $input_css_classes ) ) echo ' ' . implode( ' ', $input_css_classes ); ?>" name="<?php echo esc_attr( $this->get_field_name( $key ) ); ?>" <?php echo checked( $value, 1 ); ?> />
                        <label for="<?php echo esc_attr( $this->get_field_id( $key ) ); ?>" class="mod-checkbox-label"><?php echo esc_html( $setting['label'] ); ?></label>
                        <?php if ( ! empty( $setting['desc'] ) ) : ?>
                            <p class="small-desc"><?php echo esc_html( $setting['desc'] ); ?></p>
                        <?php endif; ?>
                    </div>
                    <?php
                    break;

                // Default: run an action.
                default:
                    do_action( 'mod_widget_field_' . $setting_type, $key, $value, $setting, $instance );
                    break;

            }
        }
    }

    /**
     * Outputs the settings form for the widget.
     */
    public function form( $instance ) {

        if ( empty( $this->fields ) ) {
            return;
        }

        $this->field_generator( $instance );

    }

}

Widget code:

<?php
/**
 * Widget - 
 *
 * @version 1.0.0
 */

defined( 'ABSPATH' ) || exit;

/**
 * Widget - Test.
 */
class MOD_Widget_Test extends MOD_Widget {

    /**
     * Constructor.
     */
    public function __construct() {
        $this->widget_cssclass = 'mod mod_widget_test_abstract';
        $this->widget_description = __( 'A widget description.', 'mod' );
        $this->widget_id = 'mod-widget-test';
        $this->widget_name = __( 'MOD Widget - Test', 'mod' );
        $this->fields = array(

            'base_title' => array(
                'label' => __( 'Base Title', 'mod' ),
                'type' => 'text',
                'class' => '',
                'default_value' => 'Test',
            ),
            'about_title' => array(
                'label' => __( 'About Title', 'mod' ),
                'type' => 'text',
                'class' => '',
                'default_value' => '',
            ),
            'base_num' => array(
                'label' => __( 'Base Number', 'mod' ),
                'type' => 'number',
                'class' => '',
                'default_value' => 10,
                'min' => -1,
                'step' => 1,
            ),
            'base_checkbox' => array(
                'label' => __( 'Base Checkbox', 'mod' ),
                'type' => 'checkbox',
                'class' => '',
                'default_value' => 0,
            ),

            // General
            'group_general' => array(
                'label' => __( 'General', 'mod' ),
                'type' => 'group',
                'sub_fields' => array(
                    'general_title' => array(
                        'label' => __( 'General Title', 'mod' ),
                        'type' => 'text',
                        'class' => '',
                        'default_value' => '',
                    ),
                    'general_num' => array(
                        'label' => __( 'General Number', 'mod' ),
                        'type' => 'number',
                        'class' => '',
                        'default_value' => '',
                        'min' => -1,
                        'step' => 1,
                    ),
                    'general_checkbox' => array(
                        'label' => __( 'General Checkbox', 'mod' ),
                        'type' => 'checkbox',
                        'class' => '',
                        'default_value' => 1,
                    ),
                ),
            ),

            // Special
            'group_special' => array(
                'label' => __( 'Special Parameters', 'mod' ),
                'type' => 'group',
                'sub_fields' => array(
                    'special_title' => array(
                        'label' => __( 'Special Title', 'mod' ),
                        'type' => 'text',
                        'class' => '',
                        'default_value' => '',
                    ),
                    'special_desc' => array(
                        'label' => __( 'Special Description', 'mod' ),
                        'type' => 'textarea',
                        'class' => '',
                        'default_value' => '',
                    ),
                ),
            ),
        );

        parent::__construct();
    }

    /**
     * Output widget.
     *
     * @see WP_Widget
     *
     * @param array $args     Arguments.
     * @param array $instance Widget instance.
     */
    public function widget( $args, $instance ) {
        ob_start();

        echo $args['before_widget'];

        echo '<pre>';
        var_dump( $instance );
        echo '</pre>';

        echo $args['after_widget'];
    }

}

Thank you so much in advance!

1
+50

OK, so I've done some debugging and I know where your problem lies...

First of all in update function:

public function update( $new_instance, $old_instance ) {

    $instance = $old_instance;

    if ( empty( $this->fields ) ) {
        return $instance;
    }

    $instance = $this->save_fields( $new_instance, $old_instance );

    return $instance;
}

You shouldn't use $old_instance as starting point. What if the list of fields is changed? Old values shouldn't get copied to new instance. So you should use this instead:

public function update( $new_instance, $old_instance ) {

    $instance = array();

    if ( empty( $this->fields ) ) {
        return $instance;
    }

    $instance = $this->save_fields( $new_instance, $old_instance );

    return $instance;
}

But then there is one more problem in save_fields function... Again, you start with $old_instance as starting point:

public function save_fields( $new_instance, $old_instance, $parent_container = null ) {

    // Vars
    $instance = $old_instance;
    $widget_fields = $this->fields;
    if( isset( $parent_container ) ){
        $widget_fields = $widget_fields[ $parent_container ]['sub_fields'];
    }

    // Loop fields and get values to save.
    foreach ( $widget_fields as $key => $setting ) {
        $setting_type = isset( $setting['type'] ) ? $setting['type'] : '';

        // Format the value based on fields type.
        switch ( $setting_type ) {
            case 'group':
                    $group_instance = $this->save_fields( $new_instance, $old_instance, $key );
                    $instance = array_merge( $group_instance, $instance );
                break;

So if given field is a group, then:

  1. You get its fields values using recursive call.
  2. In that call you:
    1. Initiate values with old_instance.
    2. Get fields from that group and set their values.
  3. Merge group values with instance values.

So there are few problems here:

  • in 3. the values for group fields contains not only values for that group, because they were initiated with old_instance,
  • in 3. you overwrite new values from group_instance with old values from instance (which contains old values, because it is also initiated with old_instance).

So how to fix that?

It's a really simple fix, this time. All you need to do is to change:

$instance = $old_instance;

to:

$instance = array();

in both update and save_fields functions.

It works like a charm after that change.

| improve this answer | |
  • Thanks! I found similar solution after many tries ;) if( isset( $parent_container ) ){ $instance = array(); – Michael Jan 8 '19 at 12:54
  • @Michael $instance should always be initiated with empty array(). You don't want to copy old values, so that if doesn't make much sense either ;) – Krzysiek Dróżdż Jan 8 '19 at 12:56
  • I have one problem. I use Elementor page builder. When I'll try to add my widget then my output is empty array(0) {} – Michael Jan 8 '19 at 13:08
  • You have ob_start in your widget function, but you never flush that buffer. And no output buffering is needed here - widget function should echo the content, not return it. – Krzysiek Dróżdż Jan 8 '19 at 13:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.