Meteor-Community-Packages / meteor-autoform

AutoForm is a Meteor package that adds UI components and helpers to easily create basic forms with automatic insert and update events, and automatic reactive validation.
MIT License
1.44k stars 328 forks source link

Add scope attribute to support partial document updates #221

Closed Gaelan closed 9 years ago

Gaelan commented 10 years ago

Is there a way to create a separate form for an embedded schema (as if it was its own document)? I'm doing it right now with onSumbit–is there a better way?

aldeed commented 10 years ago

Can you post an example of how you're doing it now and how you would like it to work?

Gaelan commented 10 years ago

Let's use the classic employee management example:

A company can have one or more employees. In Mongo, we would express that with embedding.

EmployeeSchema = {
  [...]
}
Companies = new Meteor.Collection();
Companies.attachSchema({
  [...]
  employees: { type: [EmployeeSchema] }
})

If you create a quickForm for a Company, it will automatically give you a fieldset for adding employees. However, it would be nicer to have separate add/update/remove forms for employees. Right now, I'm just using an onSubmit-based form to add employees. However, it would be awesome if we could do something like this:

{{> quickForm collection='Companies:employees' [...]}}

to get a form for a sub-schema.

aldeed commented 10 years ago

OK, thanks for the good example. I'll have to think about how this could be done. I guess that on submission a type="insert" form would actually be treated as a $push update and an update form would $set the values at the proper index (which would have to be provided to autoform somehow).

Gaelan commented 10 years ago

In my use case, we would also need to use the index for double-nesting.

aldeed commented 10 years ago

Right. The simplest implementation, to support any level of arrays and objects nesting, would be something like

{{> quickForm collection='Companies' scope='employees.1' type="update"}} for an update form

or

{{> quickForm collection='Companies' scope='employees.$' type="insert"}} for an insert form

zimme commented 10 years ago

Would this work if employees wasn't an array, but an optional sub object employee that's possible to add/update/remove?

know this example doesn't make sense for this scenario but I'm thinking there's scenarios where you might want to add an optional object of some sub-schema. and want to use a separate form/modal form/show-hide form, but maybe this is possible with some html/css show/hide solution.

aldeed commented 10 years ago

Yes, that's why I suggested a generic scope attribute, which could be set to either an array field or an object field. A good example might be if you have an optional address object with address.street, address.city, etc. and then you want to provide an update form that just includes the address fields and sets them into the correct object.

I think an insert form would apply only when scoping to an array property, but an update form could make sense for either an array or an object (scope="addresses.0" or scope="address"). Seems simple, but could get tricky in the implementation.

aldeed commented 10 years ago

Note that this can use the new ss.pick() to get the subschema for the scope automatically, and then it's just a matter of adjusting the update modifier's set/push properly.

neoromantic commented 9 years ago

Am I getting this right: it is this issue that prevents me from partial update of a document?

Here's my example:

I have schema like that (just two simple string fields that are both optional):

mySchema = new SimpleSchema({optgroup: {type: Object, optional: true}, "optgroup.field1": {type: String, optional: true}, "optgroup.field2": {type: String, optional: true}});

And I have two separate forms (actually even on different routes):

{{#autoForm type="update" doc=mydoc}}
{{>afQuickField name="optgroup.field1"}}
{{/autoForm}}

{{#autoForm type="update" doc=mydoc}}
{{>afQuickField name="optgroup.field1"}}
{{/autoForm}}

Now what happens I when I submit one of the forms (let's say with optgroup.field2) with empty value, it overwrites whole document, even if it had optgroup.field1 set.

Can I work around that? It is related to this issue?

aldeed commented 9 years ago

@neoromantic, no what you're doing should work fine. You have to update entire arrays at once, but updating only some props of an object should work just fine. You should create a simple app that reproduces the issue and create a new issue with a link to your reproduction app.

neoromantic commented 9 years ago

@aldeed I did just that: http://autoformbug.meteor.com

Steps to recreate:

  1. Fill out first field with anything, submit. Now you have document like {innerObj: {field1: 12345}}
  2. Do not fill second field, submit second form. Autoform is creating modifier like {$unset: "innerObj"}, so, here's a problem right there.

Thanks!

neoromantic commented 9 years ago

Created separate issue: https://github.com/aldeed/meteor-autoform/issues/487

nate331 commented 9 years ago

Do I understand it right, that partial updates do not work when using arrays? For example adapting the example from above:

mySchema = new SimpleSchema({optgroup: {type: [Object], optional: true}, "optgroup.$.field1": {type: String, optional: true}, "optgroup.$.field2": {type: String, optional: true}});

and

{{#autoForm type="update" doc=mydoc}}
{{>afQuickField name="optgroup.3.field1"}}
{{/autoForm}}

Doing this would delete all other fields of the array and only set "optgroup.3.field1", right? Is there a way to do this? (without having to get the entire array, changing one field and saving the entire array)

aldeed commented 9 years ago

@nate331, did you try it? I don't recall if your example works or not.

The underlying mongo issue, for those that care, is that if you do {$set: { 'optgroup.3.field1': 'foo' }} and optgroup does NOT yet exist as an array, mongo actually creates it as an object instead of an array, like {optgroup: {3: {field1: "foo"}}}. So because of this, autoform always tries to generate modifiers that work around this issue, essentially by updating the full array when possible.

You could also write a before hook and/or your own submission logic to make it work as you need.

nate331 commented 9 years ago

I tried it (with version 2.0.2, my project is still on this one). However it behaved as described earlier: It will override everything else from the array. So I tried this way:

        before: {
            update: function(docId, modifier, template) {
                var newModifier = {
                    $set: {"optgroup.3.field1": "myNewValue"}
                };
                return newModifier;
            }
        }

This is working. It only updates the expected field. However validation does not work properly anymore then. If type of field1 is Number and the user enters a String, then the onSuccess hook gets called, however nothing gets saved into the database.

markudevelop commented 9 years ago

@nate331 @aldeed Is there anything new on how to update object which is part of array? All Arrays are overwritten :( I need to update just one.

nate331 commented 9 years ago

As a workaround I am doing it the way I described in my previous post. Number validation is not working properly, so I changed the type of the Field to String. It is not ideal, but good enough.

This is the way I do it:

        before: {
            update: function(docId, modifier, template) {
                var fieldIdentifier = Meteor.userId();
                var path = getSubObjPath(template.data.doc, "optgroupA.$.optgroupB.$.identifierField", fieldIdentifier) + ".field1";
                var mod = {};
                mod[path] = AutoForm.getFieldValue("myFormId", path);
                var newModifier = {
                    $set: mod
                };
                return newModifier;
            }
        },
/*
 * eg. getSubObj(obj, "recommendedTeams.$.members.$.userId", "a32jrlwo3jk2j2")
 * -> return something like: "recommendedTeams.2.members.0"
 */
//prevPath is optional parameter (needed for recursion)
getSubObjPath = function(obj, path, value, prevPath) {

    if (prevPath === undefined)
        prevPath = "";

    if (!obj)
        return undefined;

    var parts = path.split(".");

    if (parts.length === 1) {
        if (obj[path] === value) {
            return prevPath;
        } else {
            return undefined;
        }
    }

    if (parts[0] === "$") {
        for (var i = 0; i < obj.length; i++) {
            var prevPathTmp = prevPath === "" ? "" : prevPath + ".";
            var resultPath = getSubObjPath(obj[i], trimPath(path), value, prevPathTmp + i);
            if (resultPath) {
                return resultPath;
            }
        }
    } else {
        var prevPathTmp = prevPath === "" ? "" : prevPath + ".";
        var resultPath = getSubObjPath(obj[parts[0]], trimPath(path), value, prevPathTmp + parts[0]);
        if (resultPath) {
            return resultPath;
        }
    }
};
aldeed commented 9 years ago

Done in AutoForm 5.0, now released. See new form type update-pushArray plus other fixes related to this.