The Editor is a flexible custom form component for quickly generating forms using JSON. It saves us the work of creating a unique component for editing each data type.

The component is located at:
components/common/Edit.vue


Vueform

The component is powered by @vueform/nuxt.
Our usage is possible thanks to the vueform json-schema, which enables forms to be built using a json object instead of inline elements.

Vueform handles almost every type of input, and is more than capable enough for our use of editing database items.

Schema

The form schemas are stored in a util file at utils/forms.ts

The schema is dynamically loaded into the editor by running the getSchema() function, which uses the type variable from the url path as the input. route: /edit/{type}


Create New Item

Lets see how this works using an example, adding a new task.

Let navigate to the editor path, with the route variable as task. https://tansy.app/edit/tasks

we will pull the type from the route
const type = route.params.type;

and fetch the schema
let formSchema = getSchema(type);

This returns

{
  title: { type: "text", label: "Title", rules: ["required"] },
  completed: { type: "toggle", label: "Completed" },
  status: { type: "select", label: "Status", items: ["Not Started", "In Progress", "Blocked", "Done", "Canceled"], default: "Not Started" },
  details: { type: "editor", label: "Details" },
  hidden: { type: "toggle", label: "Hidden from member", default: true },
}

by pulling it from the schemas dictionary stored in utils/forms.ts

The keys are the same as the fields in our table in pocketbase, with the object value being configured based on vueform for the datatype of the field we are editing.

The form that we generate for this json looks like this:

You can see how each key in the JSON corresponds to an entry field in the form.


Edit Existing Item

The editor supports both creating a new entry as well as editing existing ones.
The only difference is the url path.
If the path contains an id after the type, it will edit instead of creating a new entry.
/edit/{type}/{id}

Lets use another example to show how this works:
https://tansy.app/edit/tasks/mkqox5y260r50h2

This opens the same editor component, it just detects that we have an id: const id = route.params.id;

and knowing the type and id, we can do a simple fetch from the database to load in the existing data in out init() loader:

if (id) {
  // If there is an id, get the record
  const record = await pb.collection(type).getOne(id);
  // Set the data to the record using schema keys
  populateFormData(record);
}
const populateFormData = (record) => {
  Object.keys(formSchema).forEach(key => {
    if(record[key] !== undefined && record[key] !== null) {
      if(formSchema[key]?.type == 'date' && 'timezone' in record) {
        // handle dates with timezone
        data.value[key] = Utils.reverseFormatDateWithTimezoneToISO(record[key],          record['timezone']);
      } else {
        data.value[key] = record[key];
      }
    }
  });
}

Using the same getSchema function as above, we can set the existing data using v-model and have our form be populated:
<Vueform :schema="schema" v-model="data" @submit="handleSubmit" sync></Vueform>

in our handleSubmit function, we just switch between creating or updating by detecting the id

handleSubmit = () => {
  if (id) {
    await updateItem();
  } else {
    await createItem();
  }
});

Prefill Data

Often we will need to pass some data into our submit query without the user’s input. In order to keep this component dynamic, these are included as a profill query parameter in the url.

When adding a task, we don’t know whether the current user is the member the task is for, or the counselor making the task for them. We can attach this as a prefill variable: https://tansy.app/edit/tasks?prefill=%7B%22member%22%3A%22yq8ezdoikxyhb14%22%2C%22counselor%22%3A%225jfbq8zi96d5fpf%22%7D

The editor component retrieves this as an object:
let prefill = route.query.prefill;
if(prefill) prefill = JSON.parse(decodeURIComponent(prefill));

{
  member: 22yq8ezdoikxyhb14
  counselor: 225jfbq8zi96d5fpf
}

in handleSubmit, we just attach these values to our query:

// add prefill data
if(prefill) {
  Object.keys(prefill).forEach(key => {
    formData.append(key, prefill[key]);
  });
}

Back Relations

One final consideration is creating something that needs to be back related to something else. An example is creating an address for an appointment. In our system, the address entry does not contain any foreign keys, so the appointment needs to also be updated when the address is created with its key.

For this case, we have 3 more query parameters we can use:

const field = route.query.field; // the field that this item is occupying
const collection = route.query.collection; // the collection that this item is related to
const relationid = route.query.relationid; // the id of the item that this item is related to

The url path for this editor is:
https://tansy.app/edit/addresses?collection=appointments&relationid=wv3mx7hnmilgab3&field=address

This contains the:

  • collection - the related table we need to place the foreign key in
  • field - the field name in the related table
  • relationid - the id of the item in the related table

So in this example, we are adding an address to an appointment. The field is address, meaning you would access this relation through appointment.address, the collection is appointments, and we also supply the appointment id.

All of this is to run a simple back relation query during our create function:

// the following adds a back-relation if needed (included in url query)
if(field && collection && relationid) {
  await pb.collection(collection).update(relationid, { [field]: record.id });
}

Which places the newly created item id as the foreign key in the related table item.


This gives us a powerful editor that can be configured only through a JSON schema and URL parameters.