FiguredLimited / vue-mc

Models and Collections for Vue
https://vuemc.io
MIT License
626 stars 98 forks source link

Added Computed Properties #108

Closed sifex closed 5 years ago

sifex commented 5 years ago

Synopsis

Example

// Setup our computed property
class User extends Model {
    computed () {
        return {
            full_name: () => this.first_name + ' ' + this.last_name
        }
    }
}

let user = new User({ first_name: 'Tim', last_name: 'Apple' });
user.full_name; // 'Tim Apple'

Todo:

Future Plans

sifex commented 5 years ago

A good example of the v-model.lazy problem that occurs when you do this: https://i.imgur.com/Ei336EM.gifv

rtheunissen commented 5 years ago

I don't really understand the need for this, because Vue's computed properties can just access vue-mc state and still be "computed" and memoized within Vue itself. I'd be interested to hear what problem this solves, or why attributes + computed in Vue does not cover the use case that motivated this PR.

sifex commented 5 years ago

I don't really understand the need for this, because Vue's computed properties can just access vue-mc state and still be "computed" and memoized within Vue itself.

Good point, right now without memorisation it's hard to see why this is any better than they're alternatives. It essentially revolves around one-way data-binding (for get-only computed properties) and to be able to control the flow of data better with bi-directional bindings.

Example 1 – Moment Timezome Conversion

I recently came into this one regarding moment and timezone conversion. Because I want to convert the timezone in the vue component to the user's local time zone, whilst keeping the time zone within the model in GMT/UTC (same as the API endpoints requests).

Given moment.tz(..., 'UTC') is used for interpreting time, and moment(...).tz('Sydney/Australia') is used for shifting time-zones – An example of this is shown here:

export default class Membership extends Model {
        ...
    mutations () {
        return {
            current_period_end: (currentPeriodEnd) => moment.tz(currentPeriodEnd, 'UTC')
        }
    }
        ...
}
Next renewal date is <b>{{ membership.current_period_end.tz($local_tz).format("dddd, MMMM Do YYYY, h:mm a z") }}</b>

The above example surprisingly causes an infinite loop within Vue, as .tz('Sydney/Australia') caused a mutation in timezone to current_period_end, which was not the same output as current_period_end.tz('UTC'), and got recomputed back and forth between the two timezones.

One fix for this was to import moment into the component's method and clone the moment instance:

Next renewal date is <b>{{ moment(membership.current_period_end).tz($local_tz).format("dddd, MMMM Do YYYY, h:mm a z") }}</b>

As you said, you can also create a computed property of the membership.current_period_end to the formatted current timezone, which is fine, but grows a pain once you start doing this for every component that shows this. Computed (forked version for me) looks like this:

export default class Membership extends Model {
        ...
    computed () {
        return {
            moment_period_end: () => moment.tz(this.current_period_end, 'UTC')
        }
    }
        ...
}
Next renewal date is <b>{{ moment_period_end.tz($local_tz).format("dddd, MMMM Do YYYY, h:mm a z") }}</b>

Example 2 – Translation in Data into and out of the Model

This is kinda where it makes more sense – where 'computed' probably loses its original meaning, and it more acts as a translation layer, could be called "translations".

I have an input that takes it's value in dollars (user input) called cost_in_dollars and the model that takes the value in cents called cost. There's a need to translate back and forth such that mutations definitely causes a hassle. (Something functionally equivalent to v-model="{ in: x => x * 100, out: x => x / 100")

export default class Plan extends Model {
        ...
    computed () {
        return {
            cost_in_dollars: {
                get: () => parseFloat(this.cost / 100).toFixed(2),
                set: (dollars) => { this.cost = parseInt(dollars * 100) }
            }
        }
    }
        ...
}

Now I can do v-model="plan.cost_in_dollars" and it's all handled outside of the component level.


Hopefully this makes sense

sifex commented 5 years ago

Found another big one around input translation, where "25/06/1953" should be converted in (v-model:lazy) realtime to a Date Object in the Model. The forward translation should be Date formatted to "25/06/1953"

rtheunissen commented 5 years ago

I see what you mean now. It seems to me though that you're using the model for component logic. The model should really only be a data model with minimal logic. In your example for cost, where the model stores cents but the input is dollars, I would expect to see something like this in the component.

computed() {
    return {
        cost {
            get: ()  => plan.cost / 100,
            set: (v) => plan.cost = v * 100,
        }
    } 
}

You could have methods on the model like setCostFromCents or setCostFromDollars, but to override the data attribute in the model directly doesn't make as much sense to me.

So the suggestion put forward is to do the translations in the component, according to the requirements of the component. The vue-mc model is like a data-transfer / named object that tries to keep as close to its raw data model as possible. Maybe mutations were a mis-step also.

rtheunissen commented 5 years ago

We're not closed to this idea though, just not sure it has a place yet.

sifex commented 5 years ago

Thanks @rtheunissen for looking over this 😊 I'll see if I can find a way to abstract this away from both the component layer and the data layer.