UI behaviour

UI behaviour covers features that help the user interact with data.

Submitting forms

Dataparcels is very often used with data that’s fetched from a server, and saved back to a server. When dataparcels is used like this, it’s useful to prevent the user’s changes from being immediately sent back to the server and instead hold onto them momentarily. We can either wait for the user to choose to send their changes, or wait until an amount of time has passed since the user has made a change, and then save the changes to the server.

The useParcelForm hook makes this easy to set up.

In an actual app you would still need to configure the useParcelState hook to send the changes to the server. Data Synchronisation will describe how that can be done.

personParcelState
{
    "firstname": "Robert",
    "lastname": "Clamps"
}
personParcel
{
    "firstname": "Robert",
    "lastname": "Clamps"
}
import React from 'react';
import useParcelForm from 'react-dataparcels/useParcelForm';
import ParcelBoundary from 'react-dataparcels/ParcelBoundary';
import exampleFrame from 'component/exampleFrame';

export default function PersonEditor(props) {

    let [personParcel, personParcelControl] = useParcelForm({
        value: {
            firstname: "Robert",
            lastname: "Clamps"
        }
    });

    return <div>
        <label>firstname</label>
        <ParcelBoundary parcel={personParcel.get('firstname')}>
            {(firstname) => <input type="text" {...firstname.spreadDOM()} />}
        </ParcelBoundary>

        <label>lastname</label>
        <ParcelBoundary parcel={personParcel.get('lastname')}>
            {(lastname) => <input type="text" {...lastname.spreadDOM()} />}
        </ParcelBoundary>

        <button onClick={() => personParcelControl.submit()}>Submit</button>
        <button onClick={() => personParcelControl.reset()}>Reset</button>
    </div>;
}

Autosaving forms

personParcelState
{
    "firstname": "Robert",
    "lastname": "Clamps"
}
personParcel
{
    "firstname": "Robert",
    "lastname": "Clamps"
}
import React from 'react';
import useParcelForm from 'react-dataparcels/useParcelForm';
import ParcelBoundary from 'react-dataparcels/ParcelBoundary';

export default function PersonEditor(props) {

    let [personParcel] = useParcelForm({
        value: {
            firstname: "Robert",
            lastname: "Clamps"
        },
        debounce: 500  // hold onto changes until 500ms have elapsed since last change
    });

    return <div>
        <label>firstname</label>
        <ParcelBoundary parcel={personParcel.get('firstname')}>
            {(firstname) => <input type="text" {...firstname.spreadDOM()} />}
        </ParcelBoundary>

        <label>lastname</label>
        <ParcelBoundary parcel={personParcel.get('lastname')}>
            {(lastname) => <input type="text" {...lastname.spreadDOM()} />}
        </ParcelBoundary>
    </div>;
}


Validation on user input

Dataparcels’ validation plugin provides an easy way to test whether data conforms to a set of validation rules, show errors to the user, and prevent changes from being submitted until the data is valid.

Try removing the value of the name field, or choosing a non-numeric or negative value for the amount of animals.

animalParcelState
{
    "name": "Robert Clamps",
    "animals": [
        {
            "type": "Sheep",
            "amount": 6
        }
    ]
}
animalParcel
{
    "name": "Robert Clamps",
    "animals": [
        {
            "type": "Sheep",
            "amount": 6
        }
    ]
}
import React from 'react';
import useParcelForm from 'react-dataparcels/useParcelForm';
import ParcelBoundary from 'react-dataparcels/ParcelBoundary';
import validation from 'react-dataparcels/validation';

const numberToString = (parcel) => parcel
    .modifyDown(number => `${number}`)
    .modifyUp(string => Number(string));

const InputWithError = (parcel) => <div>
    <input type="text" {...parcel.spreadDOM()} />
    {parcel.meta.invalid && <div>Error: {parcel.meta.invalid}</div>}
</div>;

export default function AnimalEditor(props) {

    let [animalParcel, animalParcelControl] = useParcelForm({
        value: {
            name: "Robert Clamps",
            animals: [
                {type: "Sheep", amount: 6}
            ]
        },
        validation: () => validation({
            'name': value => value ? null : `Name must not be blank`,
            'animals.*.type': value => value ? null : `Animal type must not be blank`,
            'animals.*.amount': [
                value => Number.isInteger(value) ? null : `Animal type must be a whole number`,
                value => value >= 0 ? null : `Animal type must be positive`
            ]
        })
    });

    let animalParcelState = animalParcelControl._outerParcel;
    return <div>
        <label>name</label>
        <ParcelBoundary parcel={animalParcel.get('name')}>
            {InputWithError}
        </ParcelBoundary>

        {animalParcel.get('animals').toArray((animalParcel) => {
            return <ParcelBoundary parcel={animalParcel} key={animalParcel.key}>
                {(animalParcel) => <div>
                    <label>type</label>
                    <ParcelBoundary parcel={animalParcel.get('type')}>
                        {InputWithError}
                    </ParcelBoundary>

                    <label>amount</label>
                    <ParcelBoundary parcel={animalParcel.get('amount').pipe(numberToString)} keepValue>
                        {InputWithError}
                    </ParcelBoundary>

                    <button onClick={() => animalParcel.swapPrev()}>^</button>
                    <button onClick={() => animalParcel.swapNext()}>v</button>
                    <button onClick={() => animalParcel.delete()}>x</button>
                </div>}
            </ParcelBoundary>;
        })}
        <button onClick={() => animalParcel.get('animals').push({type: "?", amount: 0})}>Add new animal</button>

        <button onClick={() => animalParcelControl.submit()}>Submit</button>
        <button onClick={() => animalParcelControl.reset()}>Reset</button>
    </div>;
}

Confirmation

This example shows how to display a confirmation message with options. Try deleting an item in the demo below.

This uses parcel meta, a generic way of storing extra data that pertains to parts of a data shape. In this case, confirming is being stored against each element in the array.

fruitListParcel
[
    "Apple",
    "Banana",
    "Crumpets"
]
import React from 'react';
import useParcelState from 'react-dataparcels/useParcelState';
import ParcelBoundary from 'react-dataparcels/ParcelBoundary';

export default function FruitListEditor(props) {

    let [fruitListParcel] = useParcelState({
        value: [
            "Apple",
            "Banana",
            "Crumpets"
        ]
    });

    return <div>
        {fruitListParcel.toArray((fruitParcel) => {
            return <ParcelBoundary parcel={fruitParcel} key={fruitParcel.key}>
                {(parcel) => <div>
                    <input type="text" {...parcel.spreadDOM()} />
                    {parcel.meta.confirming
                        ? <span>Are you sure?
                            <button onClick={() => parcel.delete()}>yes</button>
                            <button onClick={() => parcel.setMeta({confirming: false})}>no</button>
                        </span>
                        : <button onClick={() => parcel.setMeta({confirming: true})}>x</button>}
                </div>}
            </ParcelBoundary>;
        })}
        <button onClick={() => fruitListParcel.push("New fruit")}>Add new fruit</button>
    </div>)
}

What’s going on

  • Clicking on an “x” button sets the meta.confirming state to true, which renders a choice of two buttons.
  • “No” sets meta.confirming back to false again, while “Yes” calls delete() method on the Parcel.
  • Notice how the meta always relates to the correct element, even if other elements are deleted.

Selections

This example shows how to use meta stored against each element in an array to keep track of which items have been selected.

Selected fruit:

    fruitListParcel
    [
        "Apple",
        "Banana",
        "Crumpets"
    ]
    import React from 'react';
    import useParcelState from 'react-dataparcels/useParcelState';
    import ParcelBoundary from 'react-dataparcels/ParcelBoundary';
    import shape from 'react-dataparcels/shape';
    
    export default function FruitListEditor(props) {
    
        let [fruitListParcel] = useParcelState({
            value: [
                "Apple",
                "Banana",
                "Crumpets"
            ]
        });
    
        let selectedFruit = fruitListParcel
            .toArray()
            .filter(fruit => fruit.meta.selected);
    
        let allSelected = fruitListParcel.value.length === selectedFruit.length;
    
        let selectAll = (selected) => fruitListParcel.map(shape(
            fruit => fruit.setMeta({selected})
        ));
    
        let deleteSelectedFruit = () => fruitListParcel.update(shape(
            fruitListShape => fruitListShape
                .toArray()
                .filter(fruitShape => !fruitShape.meta.selected)
        ));
    
        return <div>
            {fruitListParcel.toArray((fruitParcel) => {
                return <ParcelBoundary parcel={fruitParcel} key={fruitParcel.key}>
                    {(parcel) => {
                        let selectedParcel = parcel.metaAsParcel('selected');
                        return <div>
                            <input type="text" {...parcel.spreadDOM()} />
                            <input type="checkbox" style={{width: '2rem'}} {...selectedParcel.spreadDOMCheckbox()} />
                            <button onClick={() => parcel.swapPrev()}>^</button>
                            <button onClick={() => parcel.swapNext()}>v</button>
                            <button onClick={() => parcel.delete()}>x</button>
                        </div>;
                    }}
                </ParcelBoundary>;
            })}
            <button onClick={() => fruitListParcel.push("New fruit")}>Add new fruit</button>
            {allSelected
                ? <button onClick={() => selectAll(false)}>Select none</button>
                : <button onClick={() => selectAll(true)}>Select all</button>
            }
            <button onClick={() => deleteSelectedFruit()}>Delete selected fruit</button>
    
            <h4>Selected fruit:</h4>
            <ul>
                {selectedFruit.map((fruitParcel) => {
                    return <li key={fruitParcel.key}>
                        <button onClick={() => fruitParcel.setMeta({selected: false})}>x</button>
                        {fruitParcel.value}
                    </li>;
                })}
            </ul>
        </div>;
    }
    

    Drag and drop sorting

    Drag and drop is easy using react-dataparcels-drag, which uses react-sortable-hoc. Drag items up and down to change their order.

    fruitListParcel
    [
        "Apple",
        "Banana",
        "Crumpets"
    ]
    import React from 'react';
    import useParcelState from 'react-dataparcels/useParcelState';
    import ParcelBoundary from 'react-dataparcels/ParcelBoundary';
    import ParcelDrag from 'react-dataparcels-drag';
    
    export default function FruitListEditor(props) {
    
        let [fruitListParcel] = useParcelState({
            value: [
                "Apple",
                "Banana",
                "Crumpets"
            ]
        });
    
        return <div>
            <ParcelDrag parcel={fruitListParcel}>
                {(fruitParcel) => <ParcelBoundary parcel={fruitParcel}>
                    {(parcel) => <div>
                        <input type="text" {...parcel.spreadDOM()} />
                        <button onClick={() => parcel.insertAfter(`${parcel.value} copy`)}>+</button>
                        <button onClick={() => parcel.delete()}>x</button>
                    </div>}
                </ParcelBoundary>}
            </ParcelDrag>
            <button onClick={() => fruitListParcel.push("New fruit")}>Add new fruit</button>
        </div>;
    }
    

    Debouncing changes

    Debouncing can be used to increase rendering performance for parcels that change value many times in rapid succession, such as text inputs. This feature is available through use of ParcelBoundary and useParcelBuffer.

    Debouncing can be good for rendering performance because parcels outside the ParcelBoundary don’t needlessly update every time a small change occurs (e.g. each time the user presses a key).

    foodParcel
    {
        "mains": "Soup",
        "dessert": "Strudel"
    }
    import React from 'react';
    import useParcelState from 'react-dataparcels/useParcelState';
    import ParcelBoundary from 'react-dataparcels/ParcelBoundary';
    
    export default function FoodEditor(props) {
    
        let [foodParcel] = useParcelState({
            value: {
                mains: "Soup",
                dessert: "Strudel"
            }
        });
    
        return <div>
            <label>mains (with 300ms debounce)</label>
            <ParcelBoundary parcel={foodParcel.get('mains')} debounce={300}>
                {(mains) => <input type="text" {...mains.spreadDOM()} />}
            </ParcelBoundary>
    
            <label>dessert (without debounce)</label>
            <ParcelBoundary parcel={foodParcel.get('dessert')}>
                {(dessert) => <input type="text" {...dessert.spreadDOM()} />}
            </ParcelBoundary>
        </div>;
    }
    

    Pure rendering

    Pure rendering is achieved automatically through the use of ParcelBoundaries. In this example, ParcelBoundaries render as coloured boxes. As you type in an input, the colours will change to indicate which ParcelBoundaries have re-rendered.

    personParcel
    {
        "name": {
            "first": "Robert",
            "last": "Clamps"
        },
        "age": "33",
        "height": "160"
    }
    import React from 'react';
    import useParcelState from 'react-dataparcels/useParcelState';
    import ParcelBoundary from 'react-dataparcels/ParcelBoundary';
    
    const DebugRender = ({children}) => {
        // each render, have a new, random background colour
        let rand = () => Math.floor((Math.random() * 0.75 + 0.25) * 256);
        let style = {
            backgroundColor: `rgb(${rand()},${rand()},${rand()})`,
            padding: "1rem",
            marginBottom: "1rem"
        };
        return <div style={style}>{children}</div>;
    };
    
    export default function PersonEditor(props) {
    
        let [personParcel] = useParcelState({
            value: {
                name: {
                    first: "Robert",
                    last: "Clamps"
                },
                age: "33",
                height: "160"
            }
        });
    
        return <div>
            <label>name</label>
            <ParcelBoundary parcel={personParcel.get('name')}>
                {(name) => <DebugRender>
                    <label>first</label>
                    <ParcelBoundary parcel={name.get('first')}>
                        {(first) => <DebugRender>
                            <input type="text" {...first.spreadDOM()} />
                        </DebugRender>}
                    </ParcelBoundary>
    
                    <label>last</label>
                    <ParcelBoundary parcel={name.get('last')}>
                        {(last) => <DebugRender>
                            <input type="text" {...last.spreadDOM()} />
                        </DebugRender>}
                    </ParcelBoundary>
                </DebugRender>
            }
            </ParcelBoundary>
    
            <label>age</label>
            <ParcelBoundary parcel={personParcel.get('age')}>
                {(age) => <DebugRender>
                    <input type="text" {...age.spreadDOM()} />
                </DebugRender>}
            </ParcelBoundary>
        </div>;
    }