Skip to content

Commit

Permalink
Merge pull request #259 from 10up/feature/refactor-repeater-to-be-abs…
Browse files Browse the repository at this point in the history
…tracted-from-attributes

Refactor `Repeater` component to also work for non attribute data storage
  • Loading branch information
fabiankaegy authored Sep 26, 2023
2 parents 9dc1047 + 0d25d9e commit fe161ad
Showing 1 changed file with 133 additions and 46 deletions.
179 changes: 133 additions & 46 deletions components/repeater/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,42 +26,66 @@ import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import { CSS } from '@dnd-kit/utilities';
import { DragHandle } from '../drag-handle';

export const AttributeRepeater = ({ children, attribute, addButton, allowReordering }) => {
const { clientId, name } = useBlockEditContext();
const { updateBlockAttributes } = dispatch(blockEditorStore);

const attributeValue = useSelect((select) => {
const attributes = select(blockEditorStore).getBlockAttributes(clientId);
return attributes[attribute] || [];
});

const { defaultRepeaterData } = useSelect((select) => {
return {
defaultRepeaterData:
select(blocksStore).getBlockType(name).attributes[attribute].default,
};
});

const handleOnChange = (value) => {
updateBlockAttributes(clientId, { [attribute]: value });
};

return (
<AbstractRepeater
addButton={addButton}
allowReordering={allowReordering}
onChange={handleOnChange}
value={attributeValue}
defaultValue={defaultRepeaterData}
>
{children}
</AbstractRepeater>
);
};

/**
* The Repeater Component.
*
* @param {object} props React props
* @param {Function} props.children Render prop to render the children.
* @param {string} props.attribute property of the block attribute that will provide data for Repeater.
* @param {string} props.addButton render prop to customize the "Add item" button.
* @param {boolean} props.allowReordering boolean to toggle reordering of Repeater items.
* @param {Function} props.onChange callback function to update the block attribute.
* @param {Array} props.value array of Repeater items.
* @param {Array} props.defaultValue array of default Repeater items.
* @returns {*} React JSX
*/
export const Repeater = ({ children, attribute, addButton, allowReordering }) => {
const { clientId, name } = useBlockEditContext();
const { updateBlockAttributes } = dispatch(blockEditorStore);

export const AbstractRepeater = ({
children,
addButton,
allowReordering,
onChange,
value,
defaultValue,
}) => {
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
}),
);

const { repeaterData, defaultRepeaterData } = useSelect((select) => {
const { getBlockAttributes } = select(blockEditorStore);
const { getBlockType } = select(blocksStore);
const repeaterDataTemp = getBlockAttributes(clientId)[attribute];

if (repeaterDataTemp.length === 1 && !repeaterDataTemp[0].id) {
repeaterDataTemp[0].id = uuid();
}

return {
repeaterData: repeaterDataTemp,
defaultRepeaterData: getBlockType(name).attributes[attribute].default,
};
});

function handleDragEnd(event) {
const { active, over } = event;

Expand All @@ -73,9 +97,7 @@ export const Repeater = ({ children, attribute, addButton, allowReordering }) =>
return arrayMove(items, oldIndex, newIndex);
};

updateBlockAttributes(clientId, {
[attribute]: moveArray(repeaterData),
});
onChange(moveArray(value));
}
}

Expand All @@ -84,41 +106,40 @@ export const Repeater = ({ children, attribute, addButton, allowReordering }) =>
*/
function addItem() {
/*
* [...defaultRepeaterData] does a shallow copy. To ensure deep-copy,
* [...defaultValue] does a shallow copy. To ensure deep-copy,
* we do JSON.parse(JSON.stringify()).
*/
const defaultRepeaterDataCopy = JSON.parse(JSON.stringify(defaultRepeaterData));
const defaultValueCopy = JSON.parse(JSON.stringify(defaultValue));

if (!defaultRepeaterData.length) {
defaultRepeaterDataCopy.push([]);
if (!defaultValue.length) {
defaultValueCopy.push([]);
}

defaultRepeaterDataCopy[0].id = uuid();
defaultValueCopy[0].id = uuid();

updateBlockAttributes(clientId, {
[attribute]: [...repeaterData, ...defaultRepeaterDataCopy],
});
onChange([...value, ...defaultValueCopy]);
}

/**
* Updates the item currently being edited.
*
* @param {string|number|boolean} value The value that should be used to updated the item.
* @param {string|number|boolean} newValue The value that should be used to updated the item.
* @param {number} index The index at which the item should be updated.
*/
function setItem(value, index) {
function setItem(newValue, index) {
/*
* [...repeaterData] does a shallow copy. To ensure deep-copy,
* [...value] does a shallow copy. To ensure deep-copy,
* we do JSON.parse(JSON.stringify()).
*/
const repeaterDataCopy = JSON.parse(JSON.stringify(repeaterData));
const valueCopy = JSON.parse(JSON.stringify(value));

if (typeof value === 'object' && value !== null) {
repeaterDataCopy[index] = { ...repeaterDataCopy[index], ...value };
if (typeof newValue === 'object' && newValue !== null) {
valueCopy[index] = { ...valueCopy[index], ...newValue };
} else {
repeaterDataCopy[index] = value;
valueCopy[index] = newValue;
}
updateBlockAttributes(clientId, { [attribute]: repeaterDataCopy });

onChange(valueCopy);
}

/**
Expand All @@ -127,13 +148,13 @@ export const Repeater = ({ children, attribute, addButton, allowReordering }) =>
* @param {number} index The index of the item that needs to be removed.
*/
function removeItem(index) {
const repeaterDataCopy = JSON.parse(JSON.stringify(repeaterData)).filter(
const valueCopy = JSON.parse(JSON.stringify(value)).filter(
(item, innerIndex) => index !== innerIndex,
);
updateBlockAttributes(clientId, { [attribute]: repeaterDataCopy });
onChange(valueCopy);
}

const itemIds = repeaterData.map((item) => item.id);
const itemIds = value.map((item) => item.id);

return (
<>
Expand All @@ -145,7 +166,7 @@ export const Repeater = ({ children, attribute, addButton, allowReordering }) =>
modifiers={[restrictToVerticalAxis]}
>
<SortableContext items={itemIds} strategy={verticalListSortingStrategy}>
{repeaterData.map((item, key) => {
{value.map((item, key) => {
return (
<SortableItem
item={item}
Expand All @@ -168,7 +189,7 @@ export const Repeater = ({ children, attribute, addButton, allowReordering }) =>
</SortableContext>
</DndContext>
) : (
repeaterData.map((item, key) => {
value.map((item, key) => {
return children(
item,
item.id,
Expand All @@ -188,6 +209,40 @@ export const Repeater = ({ children, attribute, addButton, allowReordering }) =>
);
};

export const Repeater = ({
children,
addButton,
allowReordering,
onChange,
value,
defaultValue,
attribute,
}) => {
if (attribute) {
return (
<AttributeRepeater
attribute={attribute}
addButton={addButton}
allowReordering={allowReordering}
>
{children}
</AttributeRepeater>
);
}

return (
<AbstractRepeater
addButton={addButton}
allowReordering={allowReordering}
onChange={onChange}
value={value}
defaultValue={defaultValue}
>
{children}
</AbstractRepeater>
);
};

/**
* The Sortable Item Component.
*
Expand Down Expand Up @@ -231,19 +286,51 @@ const SortableItem = ({ children, item, setItem, removeItem, id }) => {
return clonedRepeaterChild;
};

Repeater.propTypes = {
children: PropTypes.func.isRequired,
addButton: PropTypes.func,
attribute: PropTypes.string,
allowReordering: PropTypes.bool,
onChange: PropTypes.func.isRequired,
value: PropTypes.array.isRequired,
defaultValue: PropTypes.array,
};

Repeater.defaultProps = {
attribute: 'items',
attribute: null,
addButton: null,
allowReordering: false,
defaultValue: [],
};

Repeater.propTypes = {
AttributeRepeater.propTypes = {
children: PropTypes.func.isRequired,
attribute: PropTypes.string,
addButton: PropTypes.func,
allowReordering: PropTypes.bool,
};

AttributeRepeater.defaultProps = {
attribute: null,
addButton: null,
allowReordering: false,
};

AbstractRepeater.propTypes = {
children: PropTypes.func.isRequired,
addButton: PropTypes.func,
allowReordering: PropTypes.bool,
onChange: PropTypes.func.isRequired,
value: PropTypes.array.isRequired,
defaultValue: PropTypes.array,
};

AbstractRepeater.defaultProps = {
addButton: null,
allowReordering: false,
defaultValue: [],
};

SortableItem.defaultProps = {
attribute: 'items',
addItem: null,
Expand Down

0 comments on commit fe161ad

Please sign in to comment.