Link

Using Nested Attributes

If you’re unfamiliar with nested attributes and why they’re useful, you can learn more about them using this familiarization guide. ReactiveRecord provides a way to use nested attributes in a declarable way using the <Form /> component. Use the examples below to manage any kind of association. You also get the benefits of ReactiveRecord’s schemas and automatic form validation!

Contents

  1. Setting Up
  2. Creating
  3. Updating
  4. Deleting
  5. Bringing it all together
  6. Advanced scenarios
    1. Creating multiple children in the same request
    2. Destroying multiple children in the same request
    3. Updating a parent resource via nested attributes
  7. Summary

Setting Up

To set up, make sure you have the correct attributes in your parent model’s schema. Otherwise ReactiveRecord will not know how to build the nested attributes. For an article that has many tag resources, we’d need to add two attributes to our Article schema:

class Article extends Model {
  static schema = {
    tags: Array,
    tags_attributes: Array
  }
}

We also need to create a Tag model, even though it will only be used within nested attributes. The _destroy property is covered later, but is only required if you allow destroying via nested attributes.

class Tag extends Model {
  static routes = { only: [] }
  static schema = {
    label: { type: String, labelText: 'Tag Name' },
    _destroy: Boolean
  }
}

Creating

We’ll keep using the article and tag relationship example above in our form. We want to build a form that allows a user to add a new tag to an article. This will be a simple form with only one input to demonstrate the basic syntax required to enable nested attributes.

const ArticleTagForm = ({ article }) => (
  <Form for={article}>
    {fields => (
      <Fragment>
        {fields.fieldsFor('tags_attributes', 'new', new Tag())(tagFields => (
          <Input {...tagFields.label} />
        ))}
        <button {...fields.submit}>Save Tag</button>
      </Fragment>
    )}
  </Form>
);

In the example above, we’ve made a call to the fieldsFor method in the form object. The method requires three arguments, which are covered here. The 'tags_attribute' argument tells the form the article resource has many tags. The 'new' argument is just a unique key to identify the new tag. It could be anything but I chose the word “new.” The last argument is new Tag(), which is used to build the nested form object.

The fieldsFor method returns a render function with the nested form object as the only argument. This function should be for a single resource only. We can use it just like a regular form object. It even includes its own fieldsFor method for deeply nested attributes! Be sure to note that we’ve called the inner form object tagFields, which must be different than our fields variable. Make sure that you’re not shadowing that fields variable.

Updating

Submitting the form in the above example would result in creating a new tag. But what if we had existing tags that we wanted to list and update? To do that, we simply need to loop through the existing tags and give them each their own nested form object:

...
{article.tags.map(tag => (
  <span key={tag.id}>
    {fields.fieldsFor('tags_attributes', tag.id, new Tag(tag))(tagFields => (
      <Input {...tagFields.label} />
    ))}
  </span>
))}
<button {...fields.submit}>Save Tags</button>
...

In this example, we’re looping through the existing tags on the article and passing them each to their own fieldsFor block. Saving this form would save all the tags at once. Though this demonstrates how to set up a has many-type relationship, it’s not a perfect example, because this isn’t how people normally edit tags on an article. In a more realistic scenario, you’d simply want the ability to delete existing tags.

Deleting

To delete a resource via nested attributes, you simply need to update the object with a _destroy: true attribute. There are several ways to accomplish this, but in this example we want to do it with a button click. We still want to loop through each of the existing tags, but this time we’ll add a button to handle the destroy.

const handleDestroy = useCallback(id => event => {
  // Make sure we're preventing default here otherwise the form will submit!
  event.preventDefault();
  article.updateAttributes({ tags_attributes: [{ id, _destroy: true }] });
}, [article]);
...
{article.tags.map(tag => (
  <span key={tag.id}>
    {tag.label}
    <button onClick={handleDestroy(tag.id)}>Delete</button>
  </span>
))}
...

In this example, we’ve created a callback function using the useCallback React hook. When the “Delete” button is clicked, we call updateAttributes on the article resource. When the request succeeds, the tag should automatically be removed from the list due to the article reloading in place. Very simple!

Bringing it all together

Here is a full-on example for a form which is responsible for creating, updating and deleting via nested attributes using the examples above.

const ArticleTagForm = ({ article }) => {
  const handleDestroy = useCallback(id => event => {
    // Make sure we're preventing default here otherwise the form will submit!
    event.preventDefault();
    article.updateAttributes({ tags_attributes: [{ id, _destroy: true }] });
  }, [article]);

  return (
    <Form for={article}>
      {fields => (
        <Fragment>
          {article.tags.map(tag => (
            <span key={tag.id}>
              {fields.fieldsFor('tags_attributes', tag.id, new Tag(tag))(tagFields => (
                <Input {...tagFields.label} />
              ))}
              <button onClick={handleDestroy(tag.id)}>Delete</button>
            </span>
          ))}
          {fields.fieldsFor('tags_attributes', 'new', new Tag())(tagFields => (
            <Input {...tagFields.label} />
          ))}
          <button {...fields.submit}>Save Tag</button>
        </Fragment>
      )}
    </Form>
  )
};

Advanced scenarios

In the above examples, we’re using calls to fieldsFor to handle a simple parent article resource with a has many-type relationship with child tag resources. We’re able to create tags one at a time, making an API request each time. Similarly, each time we click the “Delete” button, we’re making a new API request, which results in the instant removal of a tag. But what if we want to hold off on all API requests until the user is done making several changes, and submit all the changes at once? For instance, if the parent article resource did not yet exist, we would still want to submit a list of tags along with the new article. That’s a perfect use case for nested attributes. We’ll go through this type of setup in the examples below. Spoiler alert: It involves storing references to the edited resources in state.

Creating multiple children in the same request

We want to build a form that allows the user to create a list of multiple tags, then submit the form to save all the tags at once. To do that, we’ll need to keep the list of un-saved tags in state until the user is ready to submit. Instead of keeping the attributes in state, we need only to store a unique identifier for each new resource in state, and let ReactiveRecord keep track of the attributes. We’ll set up the initial state as well as a function to call when we want to “add a new tag.” That’s going to look something like this:

const [tagIdentifiers, setTagIdentifiers] = useState([]);
const addNewTag = useCallback(() => {
  setTagIdentifiers([...tagIdentifiers, Math.random()])
}, [tagIdentifiers]);

In the above example, we’re storing our unique tag identifiers in the tagIdentifiers array. We’ve also created a function to call when we want to create a new tag in state. As you can see in the addNewTag function, we’re only adding a random number to the array to represent the tag. We now have what we need to keep track of the un-saved tags in our form. For the JSX, we need to loop through each of the items in the tagIdentifiers array, and also create a button, which will add new tag fields to the form on click. That’s going to look like this:

const ArticleTagsForm = ({ article }) => {
  const [tagIdentifiers, setTagIdentifiers] = useState([]);
  const addNewTag = useCallback(event => {
    // Make sure we're preventing default here otherwise the form will submit!
    event.preventDefault();
    setTagIdentifiers([...tagIdentifiers, Math.random()])
  }, [tagIdentifiers]);

  return (
    <Form for={article}>
      {fields => (
        <Fragment>
          <small>Tags</small>
            {tagIdentifiers.map(key => (
              <div key={key}>
                {fields.fieldsFor('tags_attributes', key, new Tag())(tagFields => (
                  <Input {...tagFields.label} />
                ))}
              </div>
            ))}
          <button onClick={addNewTag}>
            + Add New Tag
          </button>
          <button>Save</button>
        <Fragment>
      )}
    </Form>
  );
}

In the above example, we’re adding new tags to the tagIdentifiers array each time we click “+ Add New Tag.” Then, everything is submitted as an array of nested attributes when we click “Save.” You can see how for these memory-only tags, to “delete” one we would only need to create a function to splice our tagIdentifiers array.

const destroyStateTag = useCallback(key => event => {
  event.preventDefault();
  setTagIdentifiers(tagIdentifiers.filter(identifier => identifier !== key))
}, [tagIdentifiers]);
...
<button onClick={destroyStateTag(key)}>Delete</button>

Destroying multiple children in the same request

We’ve just shown how to destroy tags that have only been created in state. Now we want to be able to destroy tags that may be persisted already, in other words ones that have an ID. In a previous example, we showed how you could destroy tags one at a time with a click event handler. This is a similar method for destroying via nested attributes that waits for the final form submission. The implementation is similar to our previous method of using state to keep a record of the changes in the form. It looks something like this:

const [deletedTagIds, setDeletedTagIds] = useState({});
const markTagForDeletion = useCallback(id => () => {
  setDeletedTagIds({ ...deletedTagIds, [id]: true })
}, [deletedTagIds]);

As you can see, when we call markTagForDeletion, we’re simply adding the ID to an object of tags we intend to destroy when the form is submitted. We’re using an object here to limit the complexity of our loop down the road, as you will see. In our form, instead of rendering that tag, we’d want to both hide it from view of the user, and also include a regular hidden field tag. Old school, I know!

const ArticleTagsForm = ({ article }) => {
  const [deletedTagIds, setDeletedTagIds] = useState([]);
  const markTagForDeletion = useCallback(id => event => {
    // Make sure we're preventing default here otherwise the form will submit!
    event.preventDefault();
    setDeletedTagIds([...deletedTagIds, id]);
  }, [deletedTagIds]);

  return (
    <Form for={article}>
      {fields => (
        <Fragment>
          <small>Tags</small>
          {article.tags.map(tag => fields.fieldsFor('tags_attributes', tag.id, new Tag(tag))(tagFields => {
            if (deletedTagIds[tag.id]) {
              return <input key={tag.id} type="hidden" value="true" ref={tagFields._destroy.ref} />;
            }
            return (
              <span key={tag.id}>
                {tag.label}
                <button onClick={markTagForDeletion(tag.id)}>Delete</button>
              </span>
            );
          }))}
          <button>Save</button>
        <Fragment>
      )}
    </Form>
  );
};

In this example, if the tag ID is in the deletedTagIds object, we’re rendering a hidden field tag instead of the tag label to tell ReactiveRecord to assemble the correct nested attributes upon form submission. As soon as we click the “Delete” button, we’re saving the correct tag ID in state and re-rendering to hide it from view. Very cool!

Updating a parent resource via nested attributes

All of the examples on this page describe a simple parent resource with a has many-type relationship with a child resource. You could intuitively guess how to make the same nested attributes work with a belongs to-type relationship, but we’ll still provide an example here to get you started. In this example, we’re going to be editing an “apartment” which in our backend involves editing an Unit and a parent Building resource.

const UnitEditForm = ({ unit }) => (
  <Form for={unit}>
    {fields => (
      <Fragment>
        {fields.fieldsFor('building_attributes', unit.building_id, new Building(unit.building))(buildingFields => (
          <Fragment>
            <Input {...buildingFields.street_address} />
            <Input {...fields.unit_number} />
            <Input {...buildingFields.city} />
            <Input {...buildingFields.state} />
            <Input {...buildingFields.zip} />
          </Fragment>
        ))}
        <button {...fields.submit}>Save Unit</button>
      </Fragment>
    )}
  </Form>
);

In this example, visually to the user it appears to be a regular address form, but it’s actually editing two different resources at once via nested attributes: the child unit resource and its parent building resource. One of the key differences from earlier is the first argument to fieldsFor: 'building_attributes'. Notice “building” is singular, which tells ReactiveRecord this is not a one-to-many type relationship. This form would make an PUT request to something like /units/123, but would contain building attributes.

Summary

Nested attributes are one of the advanced uses of ReactiveRecord, but it can come in handy. The usage can appear at bit verbose, and it is. That could change in the future as we experiment with cleaner syntax. For the time being, the syntax allows you to fully implement nested attributes with Rails.