Skip to content

Commit

Permalink
Call applyBlockDefaults from addBlock/insertBlock, add initialValue()…
Browse files Browse the repository at this point in the history
… configuration option for blocks (#5320)

Co-authored-by: Steve Piercy <web@stevepiercy.com>
  • Loading branch information
tiberiuichim and stevepiercy authored Oct 25, 2023
1 parent 6520d8e commit 37baeef
Show file tree
Hide file tree
Showing 4 changed files with 238 additions and 57 deletions.
47 changes: 38 additions & 9 deletions docs/source/blocks/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ Since Volto have its own set of default blocks, you should extend them by adding
So we add these lines to the `src/config.js`:

```js
import MainSliderViewBlock from '@package/components/Blocks/MainSlider/View';
import MainSliderEditBlock from '@package/components/Blocks/MainSlider/Edit';
import MainSliderViewBlock from '@root/components/Blocks/MainSlider/View';
import MainSliderEditBlock from '@root/components/Blocks/MainSlider/Edit';
import sliderSVG from '@plone/volto/icons/slider.svg';

import SimpleTeaserView from '@package/components/Blocks/SimpleTeaserView';
import CardTeaserView from '@package/components/Blocks/CardTeaserView';
import DefaultColumnRenderer from '@package/components/Blocks/DefaultColumnRenderer';
import NumberColumnRenderer from '@package/components/Blocks/NumberColumnRenderer';
import ColoredColumnRenderer from '@package/components/Blocks/ColoredColumnRenderer';
import SimpleTeaserView from '@root/components/Blocks/SimpleTeaserView';
import CardTeaserView from '@root/components/Blocks/CardTeaserView';
import DefaultColumnRenderer from '@root/components/Blocks/DefaultColumnRenderer';
import NumberColumnRenderer from '@root/components/Blocks/NumberColumnRenderer';
import ColoredColumnRenderer from '@root/components/Blocks/ColoredColumnRenderer';

import CustomSchemaEnhancer from '@package/components/Blocks/CustomSchemaEnhancer';
import CustomSchemaEnhancer from '@root/components/Blocks/CustomSchemaEnhancer';

[...]

Expand Down Expand Up @@ -114,7 +114,7 @@ export const blocks = {
We start by importing both view and edit components of our recently created custom block.

```{note}
Notice the `@package` alias.
Notice the `@root` alias.
You can use it when importing modules/components from your own project.
```

Expand All @@ -137,6 +137,35 @@ defineMessages({

Our new block should be ready to use in the editor.

## Common block options

It is a common pattern to use the block configuration to allow customization of a block's behavior or to provide block-specific implementation of various Volto mechanisms.
Some of these common options are described in the following sections.

### `blockHasValue`

`blockHasValue` returns `true` if the provided block data represents a value for the current block.
Required for alternate default block types implementations.

```{seealso}
See also [Settings reference](/configuration/settings-reference).
```

### `initialValue`

`initialValue` is a function that can be used to get the initial value for a block.
It has the following signature.

```jsx
initialValue({id, value, formData, intl}) => newFormData
```

### `blockSchema`

A must-have for modern Volto blocks, `blockSchema` is a function, or directly the schema object, that returns the schema for the block data.
Although it's not required, defining the schema enables the block to have its initial value based on the default values declared in the schema.


## Other block options

The configuration object also exposes these options
Expand Down
5 changes: 5 additions & 0 deletions news/5320.internal
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
For blocks that define their `blockSchema`, call `applyBlockDefaults` when creating the initial data for the blocks form.
It is now possible to define a block configuration function, `initialValue` that returns the initial value for a block. This is useful in use cases such as container blocks that want to create a complex initial data structure, to avoid the need to call `React.useEffect` on their initial block rendering and thus, avoid complex async "concurent" state mutations.
The `addBlock`, `mutateBlock`, `insertBlock` now allow passing a `blocksConfig` configuration object

@tiberiuichim
168 changes: 120 additions & 48 deletions src/helpers/Blocks/Blocks.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,14 @@ export function deleteBlock(formData, blockId) {
}

/**
* Add block
* Adds a block to the blocks form
* @function addBlock
* @param {Object} formData Form data
* @param {string} type Block type
* @param {number} index Destination index
* @return {Array} New block id, New form data
*/
export function addBlock(formData, type, index) {
export function addBlock(formData, type, index, blocksConfig) {
const { settings } = config;
const id = uuid();
const idTrailingBlock = uuid();
Expand All @@ -148,81 +148,133 @@ export function addBlock(formData, type, index) {
const totalItems = formData[blocksLayoutFieldname].items.length;
const insert = index === -1 ? totalItems : index;

let value = applyBlockDefaults({
data: {
'@type': type,
},
intl: _dummyIntl,
});

return [
id,
{
...formData,
[blocksLayoutFieldname]: {
items: [
...formData[blocksLayoutFieldname].items.slice(0, insert),
id,
...(type !== settings.defaultBlockType ? [idTrailingBlock] : []),
...formData[blocksLayoutFieldname].items.slice(insert),
],
},
[blocksFieldname]: {
...formData[blocksFieldname],
[id]: {
'@type': type,
_applyBlockInitialValue({
id,
value,
blocksConfig,
formData: {
...formData,
[blocksLayoutFieldname]: {
items: [
...formData[blocksLayoutFieldname].items.slice(0, insert),
id,
...(type !== settings.defaultBlockType ? [idTrailingBlock] : []),
...formData[blocksLayoutFieldname].items.slice(insert),
],
},
[blocksFieldname]: {
...formData[blocksFieldname],
[id]: value,
...(type !== settings.defaultBlockType && {
[idTrailingBlock]: {
'@type': settings.defaultBlockType,
},
}),
},
...(type !== settings.defaultBlockType && {
[idTrailingBlock]: {
'@type': settings.defaultBlockType,
},
}),
selected: id,
},
selected: id,
},
}),
];
}

/**
* Mutate block
* Gets an initial value for a block, based on configuration
*
* This allows blocks that need complex initial data structures to avoid having
* to call `onChangeBlock` at their creation time, as this is prone to racing
* issue on block data storage.
*/
const _applyBlockInitialValue = ({ id, value, blocksConfig, formData }) => {
const blocksFieldname = getBlocksFieldname(formData);
const type = value['@type'];
blocksConfig = blocksConfig || config.blocks.blocksConfig;

if (blocksConfig[type]?.initialValue) {
value = blocksConfig[type].initialValue({
id,
value,
formData,
});
formData[blocksFieldname][id] = value;
}

return formData;
};

/**
* Mutate block, changes the block @type
* @function mutateBlock
* @param {Object} formData Form data
* @param {string} id Block uid to mutate
* @param {number} value Block's new value
* @return {Object} New form data
*/
export function mutateBlock(formData, id, value) {
export function mutateBlock(formData, id, value, blocksConfig) {
const { settings } = config;
const blocksFieldname = getBlocksFieldname(formData);
const blocksLayoutFieldname = getBlocksLayoutFieldname(formData);
const index = formData[blocksLayoutFieldname].items.indexOf(id) + 1;

value = applyBlockDefaults({
data: value,
intl: _dummyIntl,
});
let newFormData;

// Test if block at index is already a placeholder (trailing) block
const trailId = formData[blocksLayoutFieldname].items[index];
if (trailId) {
const block = formData[blocksFieldname][trailId];
if (!blockHasValue(block)) {
return {
newFormData = _applyBlockInitialValue({
id,
value,
blocksConfig,
formData: {
...formData,
[blocksFieldname]: {
...formData[blocksFieldname],
[id]: value || null,
},
};
},
});
if (!blockHasValue(block)) {
return newFormData;
}
}

const idTrailingBlock = uuid();
return {
...formData,
[blocksFieldname]: {
...formData[blocksFieldname],
[id]: value || null,
[idTrailingBlock]: {
'@type': settings.defaultBlockType,
newFormData = _applyBlockInitialValue({
id,
value,
blocksConfig,
formData: {
...formData,
[blocksFieldname]: {
...formData[blocksFieldname],
[id]: value || null,
[idTrailingBlock]: {
'@type': settings.defaultBlockType,
},
},
[blocksLayoutFieldname]: {
items: [
...formData[blocksLayoutFieldname].items.slice(0, index),
idTrailingBlock,
...formData[blocksLayoutFieldname].items.slice(index),
],
},
},
[blocksLayoutFieldname]: {
items: [
...formData[blocksLayoutFieldname].items.slice(0, index),
idTrailingBlock,
...formData[blocksLayoutFieldname].items.slice(index),
],
},
};
});
return newFormData;
}

/**
Expand All @@ -233,15 +285,29 @@ export function mutateBlock(formData, id, value) {
* @param {number} value New block's value
* @return {Array} New block id, New form data
*/
export function insertBlock(formData, id, value, current = {}, offset = 0) {
export function insertBlock(
formData,
id,
value,
current = {},
offset = 0,
blocksConfig,
) {
const blocksFieldname = getBlocksFieldname(formData);
const blocksLayoutFieldname = getBlocksLayoutFieldname(formData);
const index = formData[blocksLayoutFieldname].items.indexOf(id);

value = applyBlockDefaults({
data: value,
intl: _dummyIntl,
});

const newBlockId = uuid();
return [
newBlockId,
{
const newFormData = _applyBlockInitialValue({
id,
value,
blocksConfig,
formData: {
...formData,
[blocksFieldname]: {
...formData[blocksFieldname],
Expand All @@ -259,7 +325,9 @@ export function insertBlock(formData, id, value, current = {}, offset = 0) {
],
},
},
];
});

return [newBlockId, newFormData];
}

/**
Expand Down Expand Up @@ -570,3 +638,7 @@ export function findBlocks(blocks, types, result = []) {

return result;
}

const _dummyIntl = {
formatMessage() {},
};
Loading

0 comments on commit 37baeef

Please sign in to comment.