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.
Note
For example,
completed
is a boolean in our database, so in vueform the type istoggle
to create a toggle switch in our form
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]);
});
}
But wait?
Couldn’t the user inject data into the query using this method? Yes, they could change the query and have it injected. But due to our strict Permissions, they wouldn’t be able to inject anything they aren’t allowed to. Pocketbase also has injection detection, so any dangerous queries won’t be executed.
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.