vue-formulate icon indicating copy to clipboard operation
vue-formulate copied to clipboard

Allow plain object in groups

Open hmaesta opened this issue 4 years ago • 23 comments

A common pattern here is the use of plain objects to allow future inclusion of data and keep things organised under "categories" on MongoDB.

user: {
  addons: {
    paid_support: true,
    private_network: false,
  }
}

👆 This way we can create add-ons without adding data for all users on the database.

For example, we can add monitoring: true for a specific user without having to add monitoring: false for all others ––– and it all will be under addons - nice and clean!

With Vue Formulate this form would be:

<FormulateForm v-model="user">
  <FormulateInput type="group" name="addons">
    <FormulateInput type="checkbox" name="paid_support" label="Paid support"/>
    <FormulateInput type="checkbox" name="private_network" label="Private network"/>
  </FormulateInput>
</FormulateForm>

The problem is that this group, even not repeatable, creates an array inside addons, so the final result is:

user: {
  addons: [
    {
      "paid_support": true
    },
    {
      "private_network": true
    }
  ]
}

Documentation anticipates this behavior:

For consistency, the value of a group will always be an array even if the field is not repeatable. If you need a plain object, we recommend destructuring the array inside a form submit handler.

But I believe a method to handle data on submit kind of limits the benefits of reactivity and can be very exhausting on a project like mine – with a different standardisation.

Suggestion

<FormulateInput type="group" :repeatable="false">

With :repeatable="false" it would be very easy to understand that this group can not have repeatable items, so no need for an array.

hmaesta avatar Jul 02 '20 17:07 hmaesta

@hmaesta yeah... 😞 I thought this would eventually creep up. First: I hear you and agree it would be a nice feature.

However, we cannot simply switch to making :repeatable="false" act this way because that behavior is already taken by the array format, so it would be a breaking change and would have to wait for v3. That said, I think we could come up with another non-breaking API to "shift" the behavior into this mode, some kind of opt-in prop, or maybe add a type to support this functionality:

<FormulateInput name="addons" type="subfields">
  <FormulateInput type="checkbox" name="paid_support" label="Paid support"/>
  <FormulateInput type="checkbox" name="private_network" label="Private network"/>
</FormulateInput />

Let's nail down a solid API and then figure out the implementation.

justin-schroeder avatar Jul 03 '20 02:07 justin-schroeder

Bleh, after thinking on it some more...I don‘t like the API I proposed here. Any ideas for another prop we could use on the group type?

justin-schroeder avatar Jul 06 '20 14:07 justin-schroeder

You mean keep type='group' but add a props to specify the different behavior?

Some ideas:

<!-- thinking that the default behavior is (implicitly) "array" -->
<FormulateInput type="group" mode="object"> 
<!-- thinking about "plain object" -->
<FormulateInput type="group" mode="plain"> 
<!-- a synonym of "plain" -->
<FormulateInput type="group" mode="flat"> 

This is an ideia, but I think could cause some confusion with :repeatable, so it's better to avoid:

<FormulateInput type="group" not-repeatable> 

hmaesta avatar Jul 07 '20 12:07 hmaesta

Mode object is my favorite here. Of course, the challenge with all of these is that object mode is incompatible with the repeatable="true". Its probably the right compromise though.

justin-schroeder avatar Jul 07 '20 14:07 justin-schroeder

Mode object is my favorite

Same here.

object mode is incompatible with the repeatable="true"

As expected. We don't even have to think that it should.

hmaesta avatar Jul 07 '20 14:07 hmaesta

Facing the same issue, any update on this?

tirumalavasu avatar Jul 27 '20 09:07 tirumalavasu

No specific update on this right now. I'd still recommend just structuring/destructuring in a computed prop or on submission.

justin-schroeder avatar Jul 27 '20 16:07 justin-schroeder

I still haven't used vue-formulate enough times to feel like I understand all of its intricacies and what the idiomatic approach here would be but, if the goal is _only to group form data together without using what Formulate defines as groups, we could introduce <FormulateFieldset> to keep things simpler in the current components and maybe solve things like #188 at the same time.

hacknug avatar Sep 03 '20 10:09 hacknug

Just wanted to add that we have some fairly complicated objects that we're working with from our APIs the can nest down 3-4 levels, and include arrays with objects as children. We're unable to easily get any kind of structuring/destructuring working with them, and it's preventing us from using this library for our more complex objects.

Curious if there's been any work put into this, or if you accept PR's for this sort of thing. Thanks!

HMilbradt avatar Sep 22 '20 16:09 HMilbradt

Not yet, but Im tentatively tagging it for 2.5. PRs are always welcome as long as youve got backing tests :)

justin-schroeder avatar Sep 22 '20 16:09 justin-schroeder

Right now the easiest way is to use v-model on each formulate-input and ignore the automatically generated items.

For example:

data() {
    group: {
        style: {
            icon: {
                background: '#000',
                color: '#FFF',
            },
        },
    },
}
<formulate-input 
    v-model="group.style.icon.background"
    name="group_style_icon_background" />

<formulate-input 
    v-model="group.style.icon.color"
    name="group_style_icon_color" />

This will generate some duplicated items (from v-model and from name), like that:

group: {
    style: {
        icon: {
            background: '#000',
            color: '#FFF',
        },
    },
    group_style_icon_background: '#000',
    group_style_icon_color: '#FFF',
},

For me it's "acceptable" because my back-end will ignore what's not expected, but I can imagine that's not valid for everyone.

Ps: if you skip declaring name on formulate-input it will keep generating duplicated items, but with unpredictable names like "text_9".

hmaesta avatar Sep 22 '20 16:09 hmaesta

@justin-schroeder thanks for the update! If I don't see any progress in the next few weeks, I'll try to carve out the time this deserves to take a crack at it.

@hmaesta My apologies, I should have mentioned we are using the schema generated forms, as we have quite a lot of these objects to work with. However, thank you for the snippet, this may hold us over on some of our more important features that we want until support does drop.

HMilbradt avatar Sep 22 '20 16:09 HMilbradt

This is a breaker for us as well. We have a lot of forms in our app, and they're constructed by many different sub-forms. Those subforms are also used in many different parent forms. The parent that submits the form rarely knows exactly what child items there are, so we can't destructure them at submit time because we can't know what should be an array vs an object. We also can't use @hmaesta's workaround for the same reason.

I made an attempt at a FormulateFieldset based on FormulateGrouping and it seems to work. It uses a singular instance of FormulateRepeatableProvider to achieve the expected control. There's probably a better way to do this, but I don't understand all of the Formulate code. This also most certainly has some bugs, but maybe it would be a useful starting point for someone that understands it better: https://gist.github.com/bbugh/47b6cee02ec38afafbb54505bf793efb

<FormulateInput
  type="fieldset"
  name="groupName">
  <FormulateInput type="text" name="someField" />
</FormulateInput>

Results in:

{ groupName: { someField: "Hello, World" } }

(There's also probably a better name for it since fieldset is an HTML tag)

bbugh avatar Oct 14 '20 18:10 bbugh

Nice work @bbugh!

justin-schroeder avatar Oct 16 '20 13:10 justin-schroeder

@bbugh thank for your FormulateFieldset example, I used this to make a group of my children fields, and it can set values smoothly.

But I encountered a problem about how to bring the original values into the children field when the form is edited?

I checked the proxy data of FormulateInput inside the FormulateRepeatable and find that is set to empty string, just as below,

But the model data in the FormulateRepeatable is a object such as below, so I expected get the string Alice in the child proxy data:

model: {
  name: 'Alice'
}

Do you have any idea about why the value does not exist in the children field? thanks!

DezChuang avatar Dec 30 '20 03:12 DezChuang

Moving this to a 3.0 feature since it will require some breaking changes to do it the way it should "really" be done.

justin-schroeder avatar Jan 11 '21 20:01 justin-schroeder

@justin-schroeder would you consider a pull request in 2.x if I implement mode="object" idea ?

I really need this feature right now while staying on Vue 2 so I will fork anyway to implement this for my project.

It's a show stopper when trying to use schema with {type: 'group', repeatable: false}. Maybe it makes more sense to introduce a new type to avoid confusion, something like {type: 'object'} ?

Toilal avatar Feb 12 '21 16:02 Toilal

Instead of forking and loosing upstream support, maybe consider registering a custom component in your project that adds the functionality. Type "object" seems like a reasonable approach to me and if it's works well and has tests I'd be happy to consider a PR to 2.x 👍

justin-schroeder avatar Feb 12 '21 17:02 justin-schroeder

Im using Generated forms with NoSQL. I solved this issue creating a custom input with nested Formulate Form. New input type called object.

<template>
    <FormulateForm
      v-model="context.model"
      :schema="context.options"
    />
</template>

Schema example:

{
      "name": "user",
      "label": "User",
      "type": "object",
      "options": [
          {
            "name": "firstName",
            "label": "Name"
          },
          {
            "name": "email",
            "label": "Email"
          },
          {
            "name": "gender",
            "label": "Gender",
            "type": "select",
            "options": [
                {  "value": 1, "label": "Male"  },
                { "value": 2, "label": "Female" }
            ]
          }
        ]
    }

Works as expected by type 'group' with 'repeatable=false'.

I hope that's help.

minasvisual avatar Mar 19 '21 23:03 minasvisual

Im using Generated forms with NoSQL. I solved this issue creating a custom input with nested Formulate Form. New input type called object.

<template>
    <FormulateForm
      v-model="context.model"
      :schema="context.options"
    />
</template>

Schema example:

{
      "name": "user",
      "label": "User",
      "type": "object",
      "options": [
          {
            "name": "firstName",
            "label": "Name"
          },
          {
            "name": "email",
            "label": "Email"
          },
          {
            "name": "gender",
            "label": "Gender",
            "type": "select",
            "options": [
                {  "value": 1, "label": "Male"  },
                { "value": 2, "label": "Female" }
            ]
          }
        ]
    }

Works as expected by type 'group' with 'repeatable=false'.

I hope that's help.

@minasvisual - Could you publish the full source code?

kjanusz avatar Aug 15 '21 18:08 kjanusz

This is a breaker for us as well. We have a lot of forms in our app, and they're constructed by many different sub-forms. Those subforms are also used in many different parent forms. The parent that submits the form rarely knows exactly what child items there are, so we can't destructure them at submit time because we can't know what should be an array vs an object. We also can't use @hmaesta's workaround for the same reason.

I made an attempt at a FormulateFieldset based on FormulateGrouping and it seems to work. It uses a singular instance of FormulateRepeatableProvider to achieve the expected control. There's probably a better way to do this, but I don't understand all of the Formulate code. This also most certainly has some bugs, but maybe it would be a useful starting point for someone that understands it better: https://gist.github.com/bbugh/47b6cee02ec38afafbb54505bf793efb

<FormulateInput
  type="fieldset"
  name="groupName">
  <FormulateInput type="text" name="someField" />
</FormulateInput>

Results in:

{ groupName: { someField: "Hello, World" } }

(There's also probably a better name for it since fieldset is an HTML tag)

@bbugh - Does the solution work in Vue Formulate 2.5.X ?

kjanusz avatar Aug 15 '21 18:08 kjanusz

Sorry y'all, I'm not working on that project anymore, or Vue. Good luck sorting it out!

bbugh avatar Aug 16 '21 15:08 bbugh

Im using Generated forms with NoSQL. I solved this issue creating a custom input with nested Formulate Form. New input type called object.

<template>
    <FormulateForm
      v-model="context.model"
      :schema="context.options"
    />
</template>

Schema example:

{
      "name": "user",
      "label": "User",
      "type": "object",
      "options": [
          {
            "name": "firstName",
            "label": "Name"
          },
          {
            "name": "email",
            "label": "Email"
          },
          {
            "name": "gender",
            "label": "Gender",
            "type": "select",
            "options": [
                {  "value": 1, "label": "Male"  },
                { "value": 2, "label": "Female" }
            ]
          }
        ]
    }

Works as expected by type 'group' with 'repeatable=false'. I hope that's help.

@minasvisual - Could you publish the full source code?

This is the entire code, just add on library. You project looks like more efficient, tks too.

minasvisual avatar Aug 23 '21 20:08 minasvisual