Guration
A module that allows you to validate drag and drop actions on a tree of data, culminating in 'edits' that describe the modification on a normalized data structure (rather than the whole tree). There are two types of edits a Move
and an Insert
. The drag and drop logic is handle by the Level
component, so references below to drag zones and drop zones will refer to the drag zones created using the Level
component.
Note that a valid drop doesn't changed the rendered tree, instead the expectation is that state updates will be made in the consumer appilcation in response to these edits that cause a render to the Guration
part of the app that then reflects these edits.
Edits
First it will be worth describing edits. Edits are objects that describe an update to the tree and will only be fired when they are deemed to be valid (i.e. a drop of some type
into a position that accepts that type
). Moves into the same position (i.e. moving an node into the drop zone either side of itself) will not fire edits, and edits that are invalid will fire errors.
Move
A Move
edit describes a move of a node from inside the Guration Root
context back into another valid position inside the same Guration Root
context. It has the following shape:
type Move = {
type: 'MOVE',
payload: {
type,
id,
from: {
parent: {
type: string,
childrenField: string,
index: number,
id: string
}
},
to: {
parent: {
type: string,
childrenField: string,
index: number,
id: string
},
index: newIndex
}
},
meta: Object
};
Insert
An Insert
edit will fire for an insert of some item from outside that has been mapped in through mapIn
. It has the following shape:
type Insert = {
type: 'INSERT',
payload: {
type: string,
id: string,
path: {
index: number,
parent: {
type: string,
childrenField: string,
index: number,
id: string
}
}
},
meta: Object
};
Component API
<Root />
This is the wrapper around a Guration
context and Levels
cannot be rendered outside of a Root
. It's component that allows you to listen for edits made from drag and drop actions.
Props
id: string
This is the root id that will be used as the parent of the whole tree and will appear in edits that drop into drop zones for the root level of the tree.
type: string
Similarly to the id
this will describe the type of the root node (again, used in edits) but this is also what limits drops into this position: only drops of the same type can be made at the root level.
field?: string
This will set the childrenField
in an edit, which can allow for easier reflection on the type of edit to be made.
onChange?: (edit: Edit) => void
This expects a callback function that will receive an of (edit
)[#Edits] each time an action has happened.
onError?: (error: string) => void
A callback that will recieve strings describing errors regarding invalid drops. For example, dropping an node of one type into a level of another type or dropping an node into a child of itself.
mapIn?: { [string]: string => { id: string, type: string, meta?: Object } }
An object whose keys represent a type
on e.dataTransfer.types
that can be handle by the callback that is in the value position of the object. The callback will receive any data that is found when e.dataTranfer.getData(type)
is called and is expected to return an object of { id: string, type: string }
that can be used to validate and then generate an edit in a drop zone. This object can also have an optional meta
key to pass through to the any subsequent if required.
mapOut?: { [string]: (el: Object, type: string, id: string, path: Path[]) => string }
An object that does the opposite of mapIn
and describes how to transform a node into drag data. The keys on the object are the keys that will be called using e.dataTransfer.setData(key)
, allowing drags from here to other drop zones (possibly other Guration contexts).
<Level />
A Level
is repsonsible for defining the types for a specific level in the tree as well as defining the types for the nodes that are currently rendered in that position. It also provides the props for draggable nodes and renders drop zones between these nodes.
Props
arr: <T: Object>[]
The array of nodes to map over. Passing this in allows the component to handle laying out drop zones between each node (using fragments) and plucking the id of each node in order to construct edits.
children: (item, getNodeProps, index) => ReactElement
This is not a React element but a function child. item
is an item in the array, getNodeProps()
is a function that will return the node props (such as the drag event handlers etc.) to spread on a React DOM node to make it draggable. In future it will taking a prop argument that will allow adding other props to the same Node. Currently, all event handlers that are added by these props, would be remove if adding the same event handlers to the same node.
type: string
Much like Root
this specifies both the time of the draggable nodes at this level and the type of node that can be dragged to this level.
field: ?string
Again much like Root
this specifies the childrenField
field of an edit that can help for making updates.
renderDrop: ?(getDropProps, { canDrop: boolean, isTarget: boolean }, index) => ReactElement
This is a function that will be used to render the drops between the draggable nodes rendered by children
. isOver
is much like :hover
pseduo-selector except that when dropOnNode
is true isTarget
will also be true when that position is the target position for a drop while hovering a node.
getKey: ?(el: T) => string
A function that returns the key from each object in the array, defaults to ({ id }) => id
dedupeType: ?string
Specifying this on a Level
will ensure that anything below this level that is of the same type
and has the same dedupeKey
will act as a move rather than an insert.
getDedupeKey: ?(el: T) => string
The function that returns the key for comapring items for deduping, defaults to getKey
.
dropOnNode: ?boolean
A boolean that defaults to true
, which specifics whether getNodeProps
will return props that allow dropping on top of the node. If this is true, dropping in the top 50% of the node will result in a drop at that node's index, and likewise dropping in the bottom 50% will result in a drop at the index after that node.
Example
const renderDrop = (getProps, { canDrop, isTarget }) =>
<DropZone {...getProps()} canDrop={canDrop} isTarget={isTarget} />;
const Front = ({ front }) => (
<Root
id={front.id}
type="front"
onChange={console.log}
onError={console.log}
>
<Level
arr={front.collections}
type="collection"
renderDrop={renderDrop}
dedupeType="articleFragment"
>
{({ title, articleFragments }) => (
<div>
<h1>{title}</h1>
<Indent>
<Level
arr={articleFragments}
type="articleFragment"
renderDrop={renderDrop}
>
{({ title, meta: { supporting } }, afNodeProps) => (
<div>
<h1 {...afNodeProps()}>{title}</h1>
<Indent>
<Level
arr={supporting}
type="articleFragment"
renderDrop={renderDrop}
>
{({ title }, sNodeProps) => (
<div>
<h1 {...sNodeProps()}>{title}</h1>
</div>
)}
</Level>
</Indent>
</div>
)}
</Level>
</Indent>
</div>
)}
</Level>
</Root>
);