How to create a field (such as a featured flag) that can only be applied to one item in the field array?

55 Views Asked by At

We are currently working on migrating our profile editor form from Redux Form to React Final Form. In this, we have a field array representing a list of object values - in this case, a list of websites. The format of the data is as such:

websites: [
  {
    url: "https://yikes.com",
    name: "Example 1",
    featured: true
   },
  {
    url: "https://foo.bar",
    name: "Example 2",
    featured: false
   },
   ...
]

In this array, only one website can have featured=true. We handled this in Redux using the following action:

export const setItemAsFeatured = j => (dispatch, getState) => {
  // set all the other items as featured=false
  const items = selector(getState(), "items");
  items.forEach((_, i) => {
    if (i !== j) {
      dispatch(change(FORM, `items[${i}].featured`, false));
    }
  });
};

We are currently deprecating Redux from this editor in order to follow best practices, but we would still like to keep this functionality. Has anyone implemented something similar?

Current Implementation

The current implementation uses react-final-form-arrays inside of a react-beautiful-dnd component. The implementation is complex, but the basic gist of is that I pass the arrayMutators through props into the DnD component. The implementation is similar to the CodeSandbox Example, but I have made the component extensible by putting a field component prop that renders the fields:


const makeOnDragEndFunction = fields => result => {
  // dropped outside the list
  if (!result.destination) {
    return;
  }
  fields.move(result.source.index, result.destination.index);
};

export function DraggableFieldArrayBase(props) {
  const {
    name,
    classes,
    featuredOption
  } = props;
  return (
    <FieldArray name={name}>
      {({ fields }) => (
        <DragDropContext onDragEnd={makeOnDragEndFunction(fields)}>
          <Droppable droppableId="droppable">
            {(dProvided, snapshot) => (
              <div
                ref={dProvided.innerRef}
                style={getListStyle(snapshot.isDraggingOver)}
                className="draggable__container"
              >
                {fields.map((field, index) => (
                  <Draggable className={`${classes}`} tabindex={0} key={field} draggableId={field} index={index}>
                    {(provided, snapshot) => (
                      <div
                        className="profile-edit__field-group draggable__field-group"
                        ref={provided.innerRef}
                        {...provided.draggableProps}
                        {...provided.dragHandleProps}
                        style={getItemStyle(snapshot.isDragging, provided.draggableProps.style)}
                      >
                        <DragIndicatorIcon className="draggable__indicator" />
                        <props.fieldComponent
                          name={field}
                          index={index}
                          fields={fields}
                          featuredOption={featuredOption}
                          classes={"draggable__component"}
                        />
                        <RemoveItemButton
                          srText={`Remove item number ${index + 1} of ${fields.name.replace(".", ", ")}`}
                          onClick={() => fields.remove(index)}
                        />
                        {provided.placeholder}
                      </div>
                    )}
                  </Draggable>
                ))}
                {dProvided.placeholder}
              </div>
            )}
          </Droppable>
        </DragDropContext>
      )}
    </FieldArray>
  );
}

Ideally, the featured option functionality will also be extensible.

Potential Solutions

I think there are a number of potential solutions. I have not tried them yet because I was hoping to hear some feedback first. Here are my ideas:

  • Some kind of onChange function on the field that can access the form state and perform the function previously handled in Redux actions (going through the other values in the array and setting the featured flag to false)
  • Field-level validation that checks if the user has more than one featured flag checked, and it provides them an error saying that can only have one.
  • The same validation as above, but on submit.
0

There are 0 best solutions below