WordPress.org

Make WordPress Core

Opened 4 years ago

Closed 3 years ago

Last modified 3 years ago

#38799 closed enhancement (maybelater)

Normalize the registration of settings and control dependencies in the customize api

Reported by: nikeo Owned by:
Milestone: Priority: low
Severity: normal Version:
Component: Customize Keywords:
Focuses: javascript Cc:

Description

As the customizer is more and more prominent to design the WordPress websites, there's a growing need for making the settings and controls of the customize api interact one with another.
And as a a theme developer, I find it difficult to implement those setting and control dependencies in the current customize api, in a normalized way.

Example :
For example, it's quite complex to define dependencies like :

  • Event : Setting A gets modified
  • Reactions :
    • Visibility of Control B is set to hidden
    • Control C displays a notice
    • value of setting D gets changed

The following is a proposition of possible implementation to help solving this problem.

What is a setting dependency ?
A dependency is typically described by :

  • A master setting id, the one for which the value is listened to, let's call it dominus.
  • A set of "slaves" setting id, the one that are dependant of the dominus, let's call them servi.
  • A visiblity callback : handling the possible servi control visiblity reaction
  • An action callback : handling the possible servi settings or controls reaction like value change,

Note : Even it sounds a little pedantic :), I'll use dominus and servi, rather than master and slaves which are way to negatively connoted words...

General principles of the enhancement
A possible solution to simplify this dependency management would be to have :

  1. a simple way to populate a collection of dependencies actions (visibility or specific action) as normalized objects,
  2. fire those dependency callbacks when the dominus or servi sections are expanded

Populating the collection of dependencies
A posible improvement would be to have a new utility named api.registerDependencies(), which job would be to populate a collection of dependency objects looking like :

{
    dominus : 'setting_A',
    servi : ['setting_B', 'setting_C', 'setting_D' ],
    visibility : function( to, servusSettingId, dominusSettingId ) {
        if ( 'setting_B' == servusSettingId )
          return 'dominus_value_1' == to;
        else
          return 'dominus_value_2' == to;
    },
    actions : function( to, servusSettingId, dominusSettingId ) {
        if ( 'setting_A' == servusSettingId && 'dominus_value_2' == to)
          api.control('setting_A').container.append( '</div>', { html : '<p>I am a notice<p>'} );
        if ( 'setting_D' == servusSettingId && 'dominus_value_3' == to )
          api('setting_D').set('setting_D_value_2');
    }
}

Registering dependencies could then be done with the following code :

api.registerDependencies(
  [
     { dominus : '', servi : [], visibility : function(){}, actions : function(){} },
     { ... },
     ...
  ]
);

The api.registerDepencies() utility would store the collection in a static api property, like api._dependencies, or better, as an observable value like api.dependencies = new api.Value( [ //the collection ] ), to let us react to possible dynamic changes (like settings being dynamically added) happening to this collection during a customization session.

Firing the dependencies
The idea would be to fire the relevant registered setting dependencies of the currently active section() only, to optimize performances.

To wrap the code and initialize it, a new Class could be introduced, let's call it api.Visibilities = api.Class.extend( {} ).
Why a class ? => even if it would have only one instance in the api, instantiating this code as a class would offer a convenient and clean way to initializing it and wrapping the various methods.

The job of this new api.Visibilities() object would be to :

  1. grab the api.dependencies() and make sure the registered dependencies are well formed
  2. listen to the current section expansion
  3. may be fire dependencies callbacks : visibility and custom actions
  4. make sure that the callbacks are bound to registered and embedded controls ( or registered settings ) with the following type of syntax :
api.control.when( setId, function() {
      api.control( setId ).deferred.embedded.then( function(){
            callback();//dependency callback
      });
});

Any feedbacks and additions are welcome !

Change History (8)

#1 follow-up: @celloexpressions
3 years ago

Thanks for the detailed proposal @nikeo. I think this may be a good candidate for a make/core post to gather broader feedback before determining the best approach, and that could include comparisons of current approaches to doing this (via more complex JS, active_callbacks, etc.) that would be improved by expanding the core API.

@westonruter do you have any thoughts here?

#2 in reply to: ↑ 1 ; follow-up: @nikeo
3 years ago

Replying to celloexpressions:

Thanks for the detailed proposal @nikeo. I think this may be a good candidate for a make/core post to gather broader feedback before determining the best approach, and that could include comparisons of current approaches to doing this (via more complex JS, active_callbacks, etc.) that would be improved by expanding the core API.

@westonruter do you have any thoughts here?

OK. Yes sure. Gathering more information is a must have before deciding this kind of additions.
I've already worked on something that works quite fine.
Some key consideration are :
1) to be able to simply populate the dependency description in an object that extends the api one.
2) to make sure dynamic section / settings/ controls are taken into account ( the api.Value::when() method and the api 'change' event are our friends for that ).
3) to make sure that cross section dependencies work fine, even in a lazy load context ( which will be implemented in the future as far as I understand). For that, this dependency class has to make sure that a control not yet instantiated has to be sort of "awaken".

Let me know if / how I can help further on this. I'll be happy to if I can.

Last edited 3 years ago by nikeo (previous) (diff)

#3 in reply to: ↑ 2 @nikeo
3 years ago

fixed typos... :)

Replying to nikeo:

Replying to celloexpressions:

Thanks for the detailed proposal @nikeo. I think this may be a good candidate for a make/core post to gather broader feedback before determining the best approach, and that could include comparisons of current approaches to doing this (via more complex JS, active_callbacks, etc.) that would be improved by expanding the core API.

@westonruter do you have any thoughts here?

OK. Yes sure. Gathering more information is a must have before deciding this kind of additions.
I've already worked on something that works quite fine.
Some key consideration are :
1) to be able to simply populate the dependency description in an object that extends the api one.
2) to make sure dynamic section / settings/ controls are taken into account ( the api.Value::when() method and the api 'change' event are our friends for that ).
3) to make sure that cross section dependencies work fine, even in a lazy load context ( which will be implemented in the future as far as I understand). For that, this dependency class has to make sure that a control not yet instantiated has to be sort of "awaken".

Let me know if / how I can help further on this. I'll be happy to if I can.

#4 @westonruter
3 years ago

  • Priority changed from normal to low

@nikeo The clear use case in core seems to be #29948. Right? See also #33428. And is this not also a non-normalized implementation of this found in Dependently-Contextual Customizer Controls?

Probably what should happen first is to see some patterns for how this is implemented in core using existing APIs first. With working examples we can then more clearly distill a pattern for incorporating into core.

#5 @nikeo
3 years ago

@westonruter @celloexpressions
I have posted a gist that you can use as a plugin for TwentySeventeen in functions.php.

The goal is to illustrate a way to handle some possible patterns that are needed when developping a theme or plugin.

The dependencies are described by a dependencyMap collection, and are fired on section expansion.

You can test this code by playing with the settings, and by navigating in the preview to trigger server active_callback refreshs.

Questions / patterns adressed

Going more into details, the gist tries to address the following problems/questions that I have faced when developing themes :
active_callback
How to make sure that the server active_callback and the js visibility play nice together ? The usual problem I have is a control getting a wrong visibility state when the api.previewer.refresh() is completed, and the control.active() state has changed in the api, according to what has been sent by the server.

In the example, "dependant one" and "dependant one option" are defined server side to be only be display on home page. Those controls should then be collapsed when navigating out of home in the preview, and expanded again when back home.

consistent toggle effect
The goal is to have the same expand/collapse effect accross the api for both active callbacks and user defined visibility callbacks.The example uses the same reveal method for both : api.Control.onChangeActive()

multiple visibility dependencies
How to handle the case when a control visibility depends on several setting values ?
In the example, dependant three is only displayed when master two and three are checked. To solve this problem, I use a collection a visibility boolean populated from multiple sources : control active() states and master(s) visiblity callbacks. The final control visibility boolean is the flat result of this collection => true only if all are true.

nested visibilities
There are cases when a control is dependant of a setting which is dependant of another setting. This can typically occurs when we need to define a group of customizer setting, working together like a module.
In the example, "dependant_option_one" depends on "dependant_one" which depends on "master_one". You can test it by playing with the two masters, and by navigating in the preview to trigger server active_callbacks refreshs.

individual dependency visibility callbacks
It can be tricky to define a visibility rules that says : if setting A is set to this value, display control B and hide control C.
In the example, the visibility callback is called with the dependant control this. It makes it easy to apply specific visibility condition for each dependant controls.

defining several sets of dependency settings for the same master
Describing the dependency as a collection of normalized objects having all the same properties (master, dependants, ... ) makes it easier to define several set of dependencies for the same master.

dependencies can also be actions
While this code is mostly designed to handle the visibility dependencies, I think that it might be interesting to also allow actions callback.
A visibility callback should typically returns a boolean. An action callback returns nothing but can execute contextual actions like setting a value or changing the style of control, or even display some additional description.
In the example, the this of action callbacks is assigned to the dependant control, and the master setting id is used as parameter to the callback, just like for visibilities. This makes it easier to setup individual actions by controls.

Last edited 3 years ago by nikeo (previous) (diff)

#6 @westonruter
3 years ago

  • Milestone Awaiting Review deleted
  • Resolution set to maybelater
  • Status changed from new to closed

@nikeo thanks a lot for the initial implementation. I think that this provides a great starting point for users who need to implement the complex dependencies between constructs in the customizer. I don't think that it would be suitable for adding to core in the near term because there's not really a core need for it now, and generally new features added to core should have a direct end user feature that makes use of it. So I'm going to suggest that we close this for now as maybelater so that we can keep it on the radar to implement in core once it becomes clear 80% need. In the mean time, I encourage you to create a feature plugin/library that theme and plugin authors can incorporate.

We can continue to discuss on the closed ticket.

#7 @westonruter
3 years ago

How to make sure that the server active_callback and the js visibility play nice together ?

One way to do this is to just ignore whatever the server sends and just honor what we set in JS. For example:

control.active.validate = function() {
    return control.getDependencyActivity();
};

#8 @nikeo
3 years ago

@westonruter ok.
Thanks for your feedback and the hint on the validate method. I had never found a way to use it before, but yeah, this is definitely one.
I hope that the gist will help any user interested by this topic.

Note: See TracTickets for help on using tickets.