vuejs / pinia

🍍 Intuitive, type safe, light and flexible Store for Vue using the composition api with DevTools support
https://pinia.vuejs.org
MIT License
12.97k stars 1.03k forks source link

Request for MapWritableState nested object access. #634

Open davidboom95 opened 3 years ago

davidboom95 commented 3 years ago

What problem is this solving

Modifying deeply nested properties in the state.

Proposed solution

// Object
...mapWritableState(useStore, {
     myCity: 'form.address.city',
})

// Array ( outputs the latest key as the variable name, in this case city)
...mapWritableState(useStore, ['form.address.city'])
posva commented 3 years ago

I'm personally not a fan of this syntax unless it manages to:

Another alternative that could work in TS but is way more verbose and IMO not intuitive would be supplying a function and a key

mapWritableState(useStore, { city: [state => state.adress, 'city'] })
mapWritableState(useStore, { city: { getter: state => state.adress, key: 'city'] }})
davidboom95 commented 3 years ago

How about ?

mapWritableState(useStore, ['form', 'address', 'city'])
posva commented 2 years ago

that syntax would be confusing with the existing one mapState(useStore, ['name', 'email']) that maps 2 properties.

posva commented 2 years ago

This should be doable with this TS type:

export type ObjectPaths<
  O extends Record<string, any>,
  Path extends string = ``
> = keyof O extends string
  ?
      | `${Path}${keyof O}`
      | _ObjectValues<{
          [K in keyof O as O[K] extends Record<string, any>
            ? K extends string
              ? K
              : never
            : never]: ObjectPaths<O[K], `${Path}${K}.`>
        }>
  : never

type _ObjectValues<T> = T extends Record<string, infer R>
  ? Extract<R, string>
  : never

const nestedObj = {
  a: 'hey',
  b: { a: 'hey', b: 'hello', c: { d: 'hey', e: 'f' } },
  nested: { a: { b: { c: { d: 'e' } } } },
}

function acceptKeys<T>(o: T, keys: ObjectPaths<T>[]) {}
acceptKeys(nestedObj, ['a', 'b', 'b.a', 'no', ''])

Feel free to pick it up from here if you want. It's worth noting that right now we accept property keys with a . in them so maybe we still have to find a way to allow both behaviours but I'm not sure yet.

cefn commented 2 years ago

An alternative might be to create partitioned 'child' stores from a parent store. Then the addressing of any particular entry would be 'top level' again.

This could even potentially be done within a setup function (and have the dynamic binding change as the values bound from setup changed reactively).

Syntax for OP's case would end up like...

const useFormStore = partitionStore(useStore. 'form');
const useAddressStore = partitionStore(useFormStore. 'address');
return {
  ...mapWritableState(useAddressStore, ["city"])
}

...which could potentially be accelerated by a path-oriented syntax...

return {
  ...mapWritableState(partitionStore(useStore, ["form", "address"]), "city")
}

It also has the beneficial side-effect of being able to author form items that do not know whether the item they are bound to is the WHOLE store, or just a sub-part, since they just address their binding relative to the partitioned store. This potentially makes it easier to refactor stores.

aldencolerain commented 2 years ago

I also think this would be very helpful. I'm not using typescript or keys with . so it was easy to write my own functions to do this for my application. However, I would love to see this idea merged in at some point.

In the past (Vue2) we used vuex-pathify and they had some nice helper functions get and sync to do this. A lot of the problems solved by vuex-pathify are non existent in Pinia, but the mapping functions were very helpful for those using the Options API. To me though, the nested keys were cleaner when mapped individually. Although it sounds like adding additional functions may be a non-starter.

computed: {
  id: get(store, 'profile.id'),
  name: sync(store, 'profile.name'),
},

In regard to . being allowed in keys, perhaps a third argument indicating its nested (and dots will be interpreted in that way):

mapWritableState(useStore, {
 foo: 'a[0].b.c'
}, true) //  nested = true

Or a helper function that generates a function:

mapWritableState(useStore, {
 foo: nested('a[0].b.c')
})

I'm don't use TS autocomplete etc, so not sure if those ideas would translate.

ferrykranenburgcw commented 2 years ago

We have lots of code where we pass the full path to the state and use only the last 'key' as the local name. Would love to see this working in Pinia, because it is the only thing holding us back of migrating from Vuex -> Pinia.

Bassadin commented 2 years ago

Is there any update to this? We'd also love to be able to do this or just pass a function like with mapState(). :)

STachiR20 commented 1 year ago

+1, this would be great to have