Skip to content

Commit

Permalink
Merge pull request #128 from moderntribe/s7/MOOSE-109/migrate-create-…
Browse files Browse the repository at this point in the history
…wp-controls-script

[MOOSE-91/109] Migrate "Create WP Controls" script
  • Loading branch information
GeoffDusome authored Jun 26, 2024
2 parents dc68cc3 + c589363 commit 0c8450d
Show file tree
Hide file tree
Showing 9 changed files with 473 additions and 179 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ All notable changes to this project will be documented in this file. The format
- Updated: Updated GitHub default & 3rd-party action versions to eliminate (node version warnings)[https://github.com/moderntribe/moose/actions/runs/9617664104].
- Chore: WP version to 6.5.5
- Chore: Composer updates including plugins: limit-login-attempts-reloaded (2.26.8 => 2.26.11), seo-by-rank-math (1.0.218 => 1.0.221), social-sharing-block (1.1.0 => 1.2.0), advanced-custom-fields-pro (6.2.9 => 6.3.2.1)
- Added: Create WP Controls script & documentation.
- Changed: Column block now uses the Create WP Controls script to create the "stacking order" controls.

## [2024.05]
- Updated: Pattern definition consistency for usage of `Inserter:`
Expand Down
90 changes: 90 additions & 0 deletions docs/create-wp-controls-script.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Create WP Controls Script

The "Create WP Controls" script is used to quickly and easily add custom, core WP controls to core blocks. Note that while this can be used to add controls to custom blocks, it should only be used with core blocks, as there are better ways to add controls to custom blocks.

## Supported Control Types

- [ToggleControl](https://wordpress.github.io/gutenberg/?path=/docs/components-togglecontrol--docs) / `toggle`: A true/false field that can be used to assign a class or property when the control is toggled.
- [NumberControl](https://wordpress.github.io/gutenberg/?path=/docs/components-experimental-numbercontrol--docs) / `number`: A text field that only accepts numbers. Can be used to set a property with the number entered into the field.
- [SelectControl](https://wordpress.github.io/gutenberg/?path=/docs/components-selectcontrol--docs) / `select`: A normal select box field that can be used to give the user options and assign a property based on the value selected.

## Usage

1. Import the script
```js
import createWPControls from 'utils/create-wp-controls';
```

2. Create your settings object
```js
const settings = {
attributes: {
stackingOrder: {
type: 'string',
},
},
blocks: [ 'core/column' ],
controls: [
{
applyClass: 'tribe-has-stacking-order',
applyStyleProperty: '--tribe-stacking-order',
attribute: 'stackingOrder',
defaultValue: 0,
helpText: __(
'The stacking order of the element at mobile breakpoints. This setting only applies if the "Stack on mobile" setting for the Columns block is turned on.',
'tribe'
),
label: __( 'Stacking Order', 'tribe' ),
type: 'number',
},
],
};
```

Let's break this down a little bit:

```js
attributes: {
stackingOrder: {
type: 'string',
},
},
```
> :bulb: First, we're creating an `attributes` object that is defining the attributes we want to add to the block. In this case, we're creating a `stackingOrder` attribute for the `core/column` block.
```js
blocks: [ 'core/column' ],
```
> :bulb: Next, we define what blocks we want the controls to appear on. In this case we're saying that this control should appear on the `core/column` block.
```js
controls: [
{
applyClass: 'tribe-has-stacking-order',
applyStyleProperty: '--tribe-stacking-order',
attribute: 'stackingOrder',
defaultValue: 0,
helpText: __(
'The stacking order of the element at mobile breakpoints. This setting only applies if the "Stack on mobile" setting for the Columns block is turned on.',
'tribe'
),
label: __( 'Stacking Order', 'tribe' ),
type: 'number',
},
],
```
> :bulb: Lastly, we're defining the controls array that will be used to define what controls we want to create for the block we've defined. In this case we're defining a `number` control with a default value of `0`. The `applyClass` property tells the create control script that when the value is changed from the default, we should apply the `tribe-has-stacking-order` class. The `applyStyleProperty` property tells the create control script that the `--tribe-stacking-order` style property should be set to the value of the control when the default is changed.
> :memo: **Note:** You do not always have to set both the `applyClass` and `applyStyleProperty` properties. You can use one or the other separately. Using them together though can be a powerful tool in order to only apply the style property if the class is applied.
3. Call the create controls script and pass in your settings array.

```js
createWPControls( settings );
```

## Limitations

- Because the script currently only supports specific controls, additional time will be required to extend the script if a different type of control needs to be added. This can be done in the `determineControlToRender` function within the script. Make sure to import your control type!
- Currently the script only supports adding a class or a style property. There are no other functions of the script in terms of block output.
- Not really a limitation but I should note that we shouldn't use this script unless it's decided with the project team that these controls should be created using this method. There are other ways that may be better for your projects (block styles, adding classes manually, etc). If you feel strongly that this script gives the client the best experience, go for it!
298 changes: 298 additions & 0 deletions wp-content/themes/core/assets/js/utils/create-wp-controls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,298 @@
/**
* @module create-wp-controls
*
* @description handles creating WP controls given an object of settings
*
* @see https://github.com/moderntribe/moose/tree/main/docs/create-wp-controls-script.md
*/

import { InspectorControls } from '@wordpress/block-editor';
import {
PanelBody,
SelectControl,
ToggleControl,
__experimentalNumberControl as NumberControl, // eslint-disable-line
} from '@wordpress/components';
import { createHigherOrderComponent } from '@wordpress/compose';
import { Fragment } from '@wordpress/element';
import { addFilter } from '@wordpress/hooks';
import { __ } from '@wordpress/i18n';

const state = {
settings: {},
};

/**
* @function applyBlockProps
*
* @description applys additional conditional props to core blocks
*
* @param {*} props
* @param {*} block
* @param {*} attributes
*
* @return {*} return original or updated props
*/
const applyBlockProps = ( props, block, attributes ) => {
// return default props if block isn't in the includes array
if ( ! state.settings.blocks.includes( block.name ) ) {
return props;
}

state.settings.controls.forEach( ( control ) => {
if ( attributes[ control.attribute ] === undefined ) {
return props;
}

// determine if we should add a class name to the block
if (
Object.keys( control ).includes( 'applyClass' ) &&
props.className &&
! props.className.includes( control.applyClass ) &&
attributes[ control.attribute ] &&
attributes[ control.attribute ] !== control.defaultValue
) {
props.className = `${ props.className } ${ control.applyClass }`;
}

// determine if we should add style properties to the block
// assumes we only have one style property to add and assigns it to the value of the control created
if (
Object.keys( control ).includes( 'applyStyleProperty' ) &&
attributes[ control.attribute ] &&
attributes[ control.attribute ] !== control.defaultValue
) {
props.style = {
...props.style,
[ control.applyStyleProperty ]: attributes[ control.attribute ],
};
}
} );

return props;
};

/**
* @function applyEditorBlockProps
*
* @description assigns new props / classes to the block
*/
const applyEditorBlockProps = createHigherOrderComponent(
( BlockListBlock ) => {
return ( props ) => {
const { name, attributes } = props;
const classes = attributes.classes
? attributes.classes.split( ' ' )
: [];
const styles = { style: {} };

// return default BlockListBlock if we're dealing with an unsupported block
if ( ! state.settings.blocks.includes( name ) ) {
return <BlockListBlock { ...props } />;
}

// loop through controls to assign classes & styles if necessary
state.settings.controls.forEach( ( control ) => {
if (
Object.keys( control ).includes( 'applyClass' ) &&
! classes.includes( control.applyClass ) &&
attributes[ control.attribute ] &&
attributes[ control.attribute ] !== control.defaultValue
) {
classes.push( control.applyClass );
}

// styles get added to the `wrapperProps` attribute on the BlockListBlock
if (
Object.keys( control ).includes( 'applyStyleProperty' ) &&
attributes[ control.attribute ] &&
attributes[ control.attribute ] !== control.defaultValue
) {
styles.style = {
[ control.applyStyleProperty ]:
attributes[ control.attribute ],
};
}
} );

return (
<BlockListBlock
{ ...props }
className={ classes }
wrapperProps={ styles }
/>
);
};
}
);

/**
* @function determineControlToRender
*
* @description based on provided settings, choose a type of WP control to render; currently supports ToggleControl & NumberControl.
*
* @param {*} control
* @param {*} attributes
* @param {*} setAttributes
*
* @return {*} returns WP React control component
*/
const determineControlToRender = ( control, attributes, setAttributes ) => {
if ( control.type === 'toggle' ) {
return (
<ToggleControl
key={ control.attribute }
label={ control.label }
checked={ attributes[ control.attribute ] }
help={ control.helpText }
onChange={ () => {
setAttributes( {
[ control.attribute ]:
! attributes[ control.attribute ],
} );
} }
/>
);
} else if ( control.type === 'number' ) {
return (
<NumberControl
key={ control.attribute }
label={ control.label }
value={ attributes[ control.attribute ] }
help={ control.helpText }
onChange={ ( value ) => {
setAttributes( {
[ control.attribute ]: value,
} );
} }
min={ 0 }
isShiftStepEnabled={ false }
/>
);
} else if ( control.type === 'select' ) {
return (
<SelectControl
key={ control.attribute }
label={ control.label }
value={ attributes[ control.attribute ] }
help={ control.helpText }
options={ control.selectOptions }
onChange={ ( value ) => {
setAttributes( {
[ control.attribute ]: value,
} );
} }
/>
);
}
};

/**
* @function addBlockControls
*
* @description based on provided settings, adds new controls to existing blocks
*/
const addBlockControls = createHigherOrderComponent( ( BlockEdit ) => {
return ( props ) => {
const { attributes, setAttributes, name, isSelected } = props;

// return default Edit function if block doesn't match blocks we want to target
if ( ! state.settings.blocks.includes( name ) ) {
return <BlockEdit { ...props } />;
}

// set default attributes if not set
state.settings.controls.forEach( ( control ) => {
if ( ! attributes[ control.attribute ] ) {
attributes[ control.attribute ] = control.defaultValue;
}
} );

return (
<Fragment>
<BlockEdit { ...props } />
{ isSelected && (
<InspectorControls>
<PanelBody
title={ __( 'Custom Block Settings', 'tribe' ) }
>
{ state.settings.controls.map( ( control ) =>
determineControlToRender(
control,
attributes,
setAttributes
)
) }
</PanelBody>
</InspectorControls>
) }
</Fragment>
);
};
}, 'addBlockControls' );

/**
* @function addBlockAttributes
*
* @description adds new attributes to existing blocks
*
* @param {*} settings
* @param {*} name
*
* @return {*} existing or updated settings
*/
const addBlockAttributes = ( settings, name ) => {
if ( ! state.settings.blocks.includes( name ) ) {
return settings;
}

if ( settings?.attributes !== undefined ) {
settings.attributes = {
...settings.attributes,
...state.settings.attributes,
};
}

return settings;
};

/**
* @function init
*
* @description assumes settings object is provided; if not, module does nothing
*
* @param {*} settings
*/
const init = ( settings = null ) => {
if ( ! settings ) {
return;
}

state.settings = settings;

addFilter(
'blocks.registerBlockType',
'tribe/add-block-attributes',
addBlockAttributes
);

addFilter(
'editor.BlockEdit',
'tribe/add-block-controls',
addBlockControls
);

addFilter(
'editor.BlockListBlock',
'tribe/add-editor-block-props',
applyEditorBlockProps
);

addFilter(
'blocks.getSaveContent.extraProps',
'tribe/apply-block-props',
applyBlockProps
);
};

export default init;
Loading

0 comments on commit 0c8450d

Please sign in to comment.