Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make it possible to use dynamic field names in mapFields #24

Open
rob-wood-browser opened this issue May 17, 2018 · 24 comments
Open

Make it possible to use dynamic field names in mapFields #24

rob-wood-browser opened this issue May 17, 2018 · 24 comments

Comments

@rob-wood-browser
Copy link

Hey, thanks for the great library!

I'm wondering if it's possible to use dynamic field names in the mapFields function? For example, I'm keen to use ...mapFields([`pages[${this.pageNumber}].inputs[${this.questionNumber}].value`]) where pageNumber and questionNumber are props passed to the component.

I'm able to use those props in the template of the component so I know they're being passed down, but when I try to use them in mapFields I get Cannot read property 'inputs' of undefined.

This could just be me misunderstanding Vue - apologies if so!

Thanks

@maoberlehner maoberlehner self-assigned this May 17, 2018
@maoberlehner maoberlehner changed the title Using dynamic field names in mapFields Make it possible to use dynamic field names in mapFields May 17, 2018
@maoberlehner
Copy link
Owner

Hey @rob-wood-browser, unfortunately, this is not possible because this.pageNumber and this.questionNumber are not defined in the context of ...mapFields().

// This works:
const pageNumber = 1;
const questionNumber = 2;

export default {
  // ...
  computed: {
    ...mapFields([`pages[${pageNumber}].inputs[${questionNumber}].value`])
  }
  // ...
}

// This doesn't work:
export default {
  // ...
  props: ['pageNumber', 'questionNumber'],
  computed: {
    // `this` in the context of `mapFields()` does not reference the component.
    // Therefore `this.pageNumber` and `this.questionNumber` are undefined!
    ...mapFields([`pages[${this.pageNumber}].inputs[${this.questionNumber}].value`])
  }
  // ...
}

I edited your issue title and I'll handle this as a feature request. Although I currently have no idea how I could implement this. Suggestions and / or a pull request are welcome.

@rob-wood-browser
Copy link
Author

Hey @maoberlehner,

Thanks for clarifying this. I'll absolutely have a think about how this feature could be implemented, and equally if you happen to think of a nice solution to my problem that'd be greatly appreciated too!

@badertscher
Copy link

Hi @maoberlehner,
I just found out, that the context of mapFields has no idea of the component scope. As I am working with a tree of components and I am mapping components to an object in an array, it would be very helpful to composed the mapFields-String dynamically. For now I will use getter and setters and I will tell you, if I have an idea for an implementation.

Beside that, you're package is very helpful!
Moka

@wanxe
Copy link

wanxe commented Jul 7, 2018

Sometimes we need to inject the index of the fields to map with vuex, so it can be helpfull. For the moment I'm doing manually with something like this:

    vmodel_to_map: {
      get () {
        return this.$store.state[nameSpace][modelName][this.index].prop_name;
      },
      set (value) {
        this.$store.commit(`${nameSpace}/updateField`, {path: `obj_prop[${this.index}].prop_name`, value});
      },
....
    },

So if you want to create it dynamically with an index passed as a prop:

const mapDynamicFields = (namespace, model, resource) => {
  const result = Object.keys(model).reduce((prev, key) => {
    const field = {
      get () {
        return this.$store.state[namespace][resource][this.index][key];
      },
      set (value) {
        const path = `${resource}[${this.index}].${key}`;
        this.$store.commit(`${namespace}/updateField`, {path, value});
      },
    };
    prev[key] = field;
    return prev;
  }, {});
  return result;
};

@anikolaev
Copy link

@wanxe Thanks for your ideas. I tried different approaches and just realized that it is better to store in Vuex store all the information required for calculation of the dynamic properties. In my case there is an object received from backend which has the following structure which is saved to the Vuex store:

data: {
  series: [
    {
      name: 'ser1',
      y-axis: {
        name: 'axis',
        enabled: true,
        ...
      }
    },
    ...
  ]
}

In UI there is a master-detail form with the list of chart series when user can select one series and edit its parameters like name, enabled state, etc.

I tried to access series as multi-row fields first but in this case I was unable to use the library to map y-axis's sub-properties. So I have to write separate computed properties with getters and setters similar to the example above for each y-axis sub-property.

So I've switched to storing selectedSeriesIndex in the Vuex's store making the following changes in it:

export default {
  namespaced: true,

  state: {
    widgetEditForm: {
      data: {},
      selectedSeriesIndex: -1
    }
  },
  getters: {
    selectedSeriesIndex(state) {
      return state['widgetEditForm'].selectedSeriesIndex
    },

    selectedSeries(state, getters) {
      return (state['widgetEditForm'].data.series && state['widgetEditForm'].data.series[getters.selectedSeriesIndex]) || null
    },

    getSelectedSeriesField(state, getters) {
      if (!getters.selectedSeries) {
        return getField(undefined)
      }

      return getField(getters.selectedSeries)
    },
  },

  mutations: {
    updateSelectedSeriesField(state, field) {
      let series = state['widgetEditForm'].data.series
      let selectedSeriesIndex = state['widgetEditForm'].selectedSeriesIndex
      if (!series || selectedSeriesIndex == -1) {
        return
      }

      updateField(series[selectedSeriesIndex], field)
    },
  }

In *.vue I just map selectedSeries using the library passing it my getter/mutator. To simplify access to y-axis sub-properties I also modified the library. See here: #43 (comment)

@Nicholaiii
Copy link

@maoberlehner
I stumbled upon this very issue today.
I personally think the issue is as easy as allowing keys to be a function, which should carry over the closure of the component. (Check this related comment out)

@maoberlehner
Copy link
Owner

@Nicholaiii interesting idea! I'd consider accepting a pull request which adds this feature. Thx!

@AousAnwar
Copy link

AousAnwar commented May 4, 2019

Run into the same problem where I needed a prop to get the id of the field within a list, the solution I came up with is not very elegant, but it gets the job done :

1 _ Define a global variable let self = '';
2 _ Assign self = this in the beforeCreate hook
3 _ You have access to the vue instance from the global variable self (self.propName)
4_ Didn't test it yet with multiple instance of the same component

     let self = '';
     export default {
        props:['item_ID'],
        ...
        computed:{
             mapFields('form', [
                 `list[${slef.item_ID}].name`,
                 `list[${slef.item_ID}].type`,
                 `list[${slef.item_ID}].topic`,
                 `list[${slef.item_ID}].location`,
              ])
        },
        ...
        beforeCreate(){self = this}
     }

Hope this helps

PS: Thanks for this great package, really helped me out

@maoberlehner
Copy link
Owner

@AousAnwar that's actually pretty clever 😅

@spyesx
Copy link

spyesx commented May 14, 2019

Tested on a multiple instances. It doesn't work :/
Plus, be careful to your slef.item... instead of self.item....

@agm1984
Copy link

agm1984 commented May 18, 2019

I had the same problem here when the inputs' names were like blah[0], blah[1], etc.

I ended up leaving ...mapFields(['sample', 'a', 'b', 'c']) for the normal fields, and for my "generated fields" with array names, I made a method.

Normally, for my field like 'sample', I would have markup like this:

<span v-show="sample.touched && errors.first('sample')">
    {{ errors.first('sample') }}
</span

But it wasn't actually a big deal to fall-back to the old technique that doesn't use mapFields. I made this method:

isMessageShowing(fieldName) {
    if (!this.fields[fieldName]) return false;
    return (this.fields[fieldName].touched && this.errors.first(fieldName));
},

and then markup:

<span v-show="isMessageShowing('blah[0]')">
    {{ errors.first('blah[0]') }}
</span

The most annoying thing is you can't just use fields.name in template markup because during initialization, fields.name is falsy so it will break any code that isn't something like fields.name && fields.name.touched. That is the main benefit for me to use mapFields.

But as I just showed, it's not too bad to work around it.

I found this example code here, maybe it's helpful towards augmenting mapFields, but I'm not sure: https://jsfiddle.net/Linusborg/3p4kpz1t/ I found it by Googling "dynamic computed props". It seems to use a similar strategy but it's incorporating an additional parameter.

Something like that might actually work as a non-breaking change, because mapFields only has one input parameter. Adding a second parameter for something like "and also map these fields that we don't know the names for until run-time" could be viable.

Sorry I probably can't be of any more help than that; I'd have to spend WAY more time looking at the library code to make a coherent idea!

[edit] upon further inspection, I'm not super confident that example code I linked could get us any farther. The more I look at it, the more I understand why maoberlehner said "I currently have no idea how I could implement this". It's looking like a very tight spot.

@fabien-michel
Copy link

Here an updated version of @wanxe solution mapDynamicField we are using in our project

import arrayToObject from 'vuex-map-fields/src/lib/array-to-object';

/*
Special vuex-map-field like helper to manage fields inside dynamically defined index or object property

Usage :

computed: {
    ...mapDynamicFields('my/store/module', ['bigArray[].fieldName'], 'indexArray'),
    indexArray() {
        return 42;
    }
}

So you get a fieldName computed property mapped to store my.store.module.bigArray[42].fieldName property
You can use any computed, props, etc... as index property

Fields can also be passed as object to be renamed { myField: 'bigArray[].fieldName' }
*/
export function mapDynamicFields(namespace, fields, indexField) {
    const fieldsObject = Array.isArray(fields) ? arrayToObject(fields) : fields;

    return Object.keys(fieldsObject).reduce((prev, key) => {
        const field = {
            get() {
                // 'this' refer to vue component
                const path = fieldsObject[key].replace('[]', `[${this[indexField]}]`);
                return this.$store.getters[`${namespace}/getField`](path);
            },
            set(value) {
                // 'this' refer to vue component
                const path = fieldsObject[key].replace('[]', `[${this[indexField]}]`);
                this.$store.commit(`${namespace}/updateField`, { path, value });
            },
        };

        prev[key] = field;
        return prev;
    }, {});
};

@joshkpeterson
Copy link

There's another example in this SO answer for nested fields instead of namespaced:
https://stackoverflow.com/a/55544842

@miketimeturner
Copy link

If anyone is interested this is how I do it

import { mapFields } from 'vuex-map-fields';

export default {
    computed: {
        supplier(){
            return this.get('suppliers').find(supplier => supplier.id === this.supplier_id)
        },
        ...mapFields('supplier', ['name']),
    },
}

export const mapFields = function (state, fields) {
    return fields.reduce((prev, field) => {
        prev[field] = {
            get() {
                return this[state][field];
            },
            set(value) {
                this[state][field] = value
            },
        };
        return prev;
    }, {});
};

@widavies
Copy link

widavies commented Jan 5, 2020

Building off @wanxe and @fabien-michel's solutions, I've modified mapMultiRowFields to a dynamic function as well:

export function mapDynamicMultiRowFields(namespace, paths, indexField) {
  const pathsObject = Array.isArray(paths) ? arrayToObject(paths) : paths;

  return Object.keys(pathsObject).reduce((entries, key) => {
    // eslint-disable-next-line no-param-reassign
    entries[key] = {
      get() {
        const store = this.$store;
        const path = pathsObject[key].replace("[]",`[${this[indexField]}]`);
        const rows = store.getters[`${namespace}/getField`](path);

        return rows.map((fieldsObject, index) =>
          Object.keys(fieldsObject).reduce((prev, fieldKey) => {
            const fieldPath = `${path}[${index}].${fieldKey}`;

            return Object.defineProperty(prev, fieldKey, {
              get() {    
                return store.getters[`${namespace}/getField`](fieldPath);
              },
              set(value) {
                store.commit(`${namespace}/updateField`, {
                  path: fieldPath,
                  value
                });
              }
            });
          }, {})
        );
      }
    };

    return entries;
  }, {});
}

Regardless of this issue, thanks for the great library, for my use case, Vuex would be almost worthless if not for your library (I'm building a relatively complicated form builder that has LOTS of v-models that need to bind to the Vuex store). Thanks!

@geoidesic
Copy link
Collaborator

@miketimeturner what does this.get('suppliers') do in context? Could you show your state please? I'm not sure I understand your example.

@geoidesic
Copy link
Collaborator

geoidesic commented Jun 11, 2020

@wdavies973 Could you please show how this would be implemented in a component?

@miketimeturner
Copy link

miketimeturner commented Jun 11, 2020

@miketimeturner what does this.get('suppliers') do in context? Could you show your state please? I'm not sure I understand your example.

That's just a helper function that I created that gets the state "$store.state.suppliers" not really important for this to work. The mapFields function just uses the result to map the properties

@widavies
Copy link

@geoidesic
Copy link
Collaborator

@miketimeturner That's just a helper function that I created that gets the state "$store.state.suppliers" not really important for this to work. The mapFields function just uses the result to map the properties

Ok tx. Why do you first import mapFields, then export your own function with the same name?

@miketimeturner
Copy link

The mapFields export is what you're importing. (Not this package) Just put it in its own file and import it where you need it. And use it like this...

...mapFields('propertyName', ['fieldOne', 'fieldTwo'])

@v1nc3n4
Copy link

v1nc3n4 commented Sep 13, 2020

Hi all,

Based on the great solutions provided by @fabien-michel and @wanxe above, hereafter is an extended version that should allow generating getters/setters with nested arrays.
I needed to map a field located at rootArray[].nestedArray[].field with different indexes. The syntax evolves a little, to provide index of index fields, but I didn't find a more elegant solution yet.

// Index 1 => prop2
// Index 0 => prop1
...mapDynamicFields(namespace, ['rootArray[=1].nestedArray[=0].field'], ['prop1', 'prop2'])

Then, the computed property field is mapped dynamically to the rootArray[this.prop2].nestedArray[this.prop1].field Vuex store property. I needed to use an additional character =, to prevent unexpected replacements during path reduction (see buildFieldPath function below).

import arrayToObject from 'vuex-map-fields/src/lib/array-to-object';

function buildFieldPath(vm, fieldsObject, field, indexFields) {
    if (Array.isArray(indexFields)) {
        return indexFields.reduce(
            (path, indexField, index) => path.replace(new RegExp(`\\[=${index}\\]`), `[${vm[indexField]}]`),
            fieldsObject[field]
        );
    }

    return fieldsObject[field].replace('[]', `[${vm[indexFields]}]`);
}

export function mapDynamicFields(namespace, fields, indexFields) {
    const fieldsObject = Array.isArray(fields) ? arrayToObject(fields) : fields;

    return Object.keys(fieldsObject).reduce((prev, key) => {
        prev[key] = {
            get() {
                // 'this' refer to vue component
                const path = buildFieldPath(this, fieldsObject, key, indexFields);
                return this.$store.getters[`${namespace}/getField`](path);
            },
            set(value) {
                // 'this' refer to vue component
                const path = buildFieldPath(this, fieldsObject, key, indexFields);
                this.$store.commit(`${namespace}/updateField`, { path, value });
            }
        };

        return prev;
    }, {});
}

There may be additional features to implement, essentially to provide shortcuts, and a more elegant code may exist. Feel free to comment/improve.
BR

@D1mon
Copy link

D1mon commented Dec 3, 2020

#24 (comment) message deleted. please update examples.

@tonviet712
Copy link

Hey @rob-wood-browser, unfortunately, this is not possible because this.pageNumber and this.questionNumber are not defined in the context of ...mapFields().

// This works:
const pageNumber = 1;
const questionNumber = 2;

export default {
  // ...
  computed: {
    ...mapFields([`pages[${pageNumber}].inputs[${questionNumber}].value`])
  }
  // ...
}

// This doesn't work:
export default {
  // ...
  props: ['pageNumber', 'questionNumber'],
  computed: {
    // `this` in the context of `mapFields()` does not reference the component.
    // Therefore `this.pageNumber` and `this.questionNumber` are undefined!
    ...mapFields([`pages[${this.pageNumber}].inputs[${this.questionNumber}].value`])
  }
  // ...
}

I edited your issue title and I'll handle this as a feature request. Although I currently have no idea how I could implement this. Suggestions and / or a pull request are welcome.

with this method, what if pageNumber is dynamic ?
like I have button to choose pageNumber (1|2|3...) => choose 3, but the data pages[3] is not loaded

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests