diff --git a/components/index.js b/components/index.js index fa74a066..15d8672e 100644 --- a/components/index.js +++ b/components/index.js @@ -25,3 +25,4 @@ export { PostPrimaryTerm } from './post-primary-term'; export { PostPrimaryCategory } from './post-primary-category'; export { RichTextCharacterLimit, getCharacterCount } from './rich-text-character-limit'; export { CircularProgressBar, Counter } from './counter'; +export { Required } from './required'; diff --git a/components/required/index.js b/components/required/index.js new file mode 100644 index 00000000..9897cc1a --- /dev/null +++ b/components/required/index.js @@ -0,0 +1,45 @@ +import PropTypes from 'prop-types'; +import { useDispatch, useSelect } from '@wordpress/data'; +import { useEffect, useRef } from '@wordpress/element'; +import { v4 as uuidv4 } from 'uuid'; +import { store as editorStore } from '@wordpress/editor'; +import { store as requiredFieldsStore } from './store'; + +const lockKey = 'tenup-required-fields-lock-post' + +export const Required = ({ value, children }) => { + const { current: componentId } = useRef( uuidv4() ); + const { lockPostSaving, unlockPostSaving } = useDispatch( editorStore ); + const { setIsFilled } = useDispatch( requiredFieldsStore ); + + useEffect( () => { + setIsFilled( componentId, !! value ); + }, [ value ] ); + + const { shouldLock } = useSelect( ( select ) => { + return { + shouldLock: select( requiredFieldsStore ).shouldLock(), + } + } ); + + useEffect( () => { + if ( shouldLock ) { + lockPostSaving( lockKey ); + } else { + unlockPostSaving( lockKey ); + } + + return () => unlockPostSaving( lockKey ); + }, [ shouldLock ] ); + + return children; +}; + +Required.defaultProps = { + value: '', +}; + +Required.propTypes = { + value: PropTypes.string || PropTypes.bool, + children: PropTypes.node.isRequired, +}; diff --git a/components/required/readme.md b/components/required/readme.md new file mode 100644 index 00000000..b7153564 --- /dev/null +++ b/components/required/readme.md @@ -0,0 +1 @@ +# Required diff --git a/components/required/store.js b/components/required/store.js new file mode 100644 index 00000000..8535d700 --- /dev/null +++ b/components/required/store.js @@ -0,0 +1,38 @@ +import { createReduxStore, register } from '@wordpress/data'; + +const DEFAULT_STATE = { + fields: {}, +}; + +export const store = createReduxStore( 'required-fields-store', { + reducer( state = DEFAULT_STATE, action ) { + switch ( action.type ) { + case 'SET_FIELD_REQUIRED_STATUS': + return { + ...state, + fields: { + ...state.fields, + [ action.componentId ]: action.status + } + }; + } + + return state; + }, + actions: { + setIsFilled( componentId, status ) { + return { + type: 'SET_FIELD_REQUIRED_STATUS', + componentId, + status, + }; + }, + }, + selectors: { + shouldLock( state ) { + return Object.keys( state.fields ).filter( field => ! state.fields[ field ] ).length > 0; + } + }, +} ); + +register( store ); diff --git a/example/src/blocks/required-component-example/index.js b/example/src/blocks/required-component-example/index.js new file mode 100644 index 00000000..32a6489d --- /dev/null +++ b/example/src/blocks/required-component-example/index.js @@ -0,0 +1,55 @@ +import { registerBlockType } from '@wordpress/blocks'; +import { useBlockProps } from '@wordpress/block-editor'; +import { TextControl } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; + +import { Required } from '@10up/block-components'; + +const NAMESPACE = 'example'; + +registerBlockType( `${ NAMESPACE }/required-component-example`, { + apiVersion: 2, + title: __( 'Required Component Example', NAMESPACE ), + icon: 'smiley', + category: 'common', + example: {}, + supports: { + html: false + }, + attributes: { + fname: { + type: 'string', + default: '', + }, + lname: { + type: 'string', + default: '' + } + }, + transforms: {}, + variations: [], + edit: ( { attributes, setAttributes } ) => { + const blockProps = useBlockProps(); + const { fname, lname } = attributes; + + return ( +