Create Dynamic Form

Hello! I have a use case that I’m wondering how to implement, and if I’d be able to see it in action in the editor or only on a live deployed site (I’m currently still building and haven’t upgraded to deploy yet).

In the application I’m building we would like our users to set up form templates and set one of their templates to ‘active’. In Supabase I have a table for Forms tied to a user and then a table for Form_Fields that are tied to a form. I am pulling this data correctly and can see it when I fetch from Supabase.

Now, I would like to dynamically create the form based on the form fields tied to the form that is currently active. Natively, it doesn’t appear to allow for binding a form element to data to dynamically generate this. I could create segmented sections for inputs, select boxes, other types of form fields and bind each of those sections to data and filter based on the field_type, but it won’t necessarily be in the order set in the database because I’ll have to group field_types together. I’ve worked up a Javascript function with the help of ChatGPT (which I’ve added in the code block) that will take the data and dynamically create the html elements. But here is where I’m having a hard time adding the function/binding it to data and figuring out how to see it in action, but then would it even be able to create those elements in the editor?

Any help here would be very welcome! Thanks!

// The createForm function
function createForm(containerId, formFields) {
    const container = document.getElementById(containerId);

    const form = document.createElement('form');
    form.setAttribute('id', 'dynamic-form');

    formFields
        .sort((a, b) => a.field_order - b.field_order)
        .forEach(field => {
            const fieldWrapper = document.createElement('div');
            fieldWrapper.classList.add('form-field-wrapper');

            const label = document.createElement('label');
            label.setAttribute('for', field.field_name.toLowerCase());
            label.textContent = field.field_name;

            let inputElement;

            if (field.field_type === 'text' || field.field_type === 'email' || field.field_type === 'password') {
                inputElement = document.createElement('input');
                inputElement.setAttribute('type', field.field_type);
                inputElement.setAttribute('id', field.field_name.toLowerCase());
                inputElement.setAttribute('name', field.field_name.toLowerCase());
            } else if (field.field_type === 'checkbox') {
                inputElement = document.createElement('input');
                inputElement.setAttribute('type', 'checkbox');
                inputElement.setAttribute('id', field.field_name.toLowerCase());
                inputElement.setAttribute('name', field.field_name.toLowerCase());
            } else if (field.field_type === 'radio') {
                inputElement = document.createElement('div');
                inputElement.setAttribute('id', field.field_name.toLowerCase());
                inputElement.setAttribute('name', field.field_name.toLowerCase());
                field.options.forEach(option => {
                    const radioWrapper = document.createElement('div');
                    const radioInput = document.createElement('input');
                    const radioLabel = document.createElement('label');

                    radioInput.setAttribute('type', 'radio');
                    radioInput.setAttribute('name', field.field_name.toLowerCase());
                    radioInput.setAttribute('value', option.label.toLowerCase());

                    radioLabel.textContent = option.label;

                    radioWrapper.appendChild(radioInput);
                    radioWrapper.appendChild(radioLabel);
                    inputElement.appendChild(radioWrapper);
                });
            } else if (field.field_type === 'select') {
                inputElement = document.createElement('select');
                inputElement.setAttribute('id', field.field_name.toLowerCase());
                inputElement.setAttribute('name', field.field_name.toLowerCase());
                field.options.forEach(option => {
                    const optionElement = document.createElement('option');
                    optionElement.setAttribute('value', option.label.toLowerCase());
                    optionElement.textContent = option.label;
                    inputElement.appendChild(optionElement);
                });
            }

            if (field.is_required) {
                inputElement.setAttribute('required', 'required');
            }

            fieldWrapper.appendChild(label);
            if (inputElement) {
                fieldWrapper.appendChild(inputElement);
            }

            form.appendChild(fieldWrapper);
        });

    const submitButton = document.createElement('button');
    submitButton.setAttribute('type', 'submit');
    submitButton.textContent = 'Submit';
    form.appendChild(submitButton);

    container.innerHTML = '';
    container.appendChild(form);
}

// Example form fields data (THIS WILL BE DATA FROM SUPABASE)
const formFields = [
    { id: 1, field_name: 'First Name', field_type: 'text', is_required: true, field_order: 1 },
    { id: 2, field_name: 'Gender', field_type: 'radio', is_required: true, field_order: 2, options: [{ label: 'Male' }, { label: 'Female' }] },
    { id: 3, field_name: 'Country', field_type: 'select', is_required: false, field_order: 3, options: [{ label: 'USA' }, { label: 'Canada' }] },
    { id: 4, field_name: 'Subscribe', field_type: 'checkbox', is_required: false, field_order: 4 }
];

// Call the function to generate the form
createForm('form-container', formFields);

You’d do this by simply leveraging the possibility to repeat items on a div. This way you don’t need any JS, maybe just to format the array properly. It’s kinda hard to explain, but I had a mentee last week asking me the exact same thing at a 1:1 consultation.

If I had to give you a hint, you need to get the fields as an array and then display them by repeating them inside of a form container by binding that array to a div, this way you can repeat dynamic content (fields) inside your form.

This approach will only work partially, like I mentioned in my original post, because I can’t bind the field type to anything. So email or phone fields will end up just being basic short answer text fields for instance. I don’t think that solution is robust enough for what I need.

On top of that, WeWeb isn’t registering those input fields that are created by binding data. I’m adding two screenshots, one showing the form container with the elements created by binding divs with inputs inside, the other showing attempting to bind data to something in a work flow for that form container. As you can see there aren’t any value options to use in the workflow making it worthless as I can’t access any value entered in by the user.

You need to stick all of the fields into one and display them conditionally :slight_smile: Not ideal, but only way to do this.

1 Like

I’ll add to what Broberto said:
When you are repeating inputs in a collection list then the input’s variable is lost.
What you can do is creating an object variable that will hold the form data. On change on the inputs, you can change the object at a specific path. In you case, that can be field_name. That would be the result:

{
  "First Name": "toto",
  "Gender": "male",
  "Country": "fr",
  "Subscribe": "true"
}

Also, if you are handling the conditional inputs inside of a component, that could be easier to manage :slight_smile:

3 Likes