2

I'm new to Vue and I'm trying to bind a component value to a property of an exported object. The initial value is set correctly but it's not reactive. I'm not sure I'm using the right terminology, but the relevant sections are

// Settings.js
export const settings = { showOverlay: true }

// Overlay.vue
<template>
  <div v-show="enabled"> Some stuff </div>
</template>

<script>
import { settings } from "../js/Settings.js";
export default {
  data() {
    return {
        enabled: settings.showOverlay
    };
  }
};
</script>

Now, I know that the exported object (settings) is a read-only view onto the object, because that's how modules work, so probably Vue can't put its hooks into it. The thing is, I want the setting to be "owned" by this Settings service, which is responsible for persisting the values between page loads, but I don't feel like the service should have to be aware that the component wants to watch a value and take care of manually triggering updates on the component when the value changes -- I probably just misunderstand the pattern I'm supposed to use for cases like this.

This is being built with Webpack / babel, if that makes any difference.

Coderer
  • 25,844
  • 28
  • 99
  • 154
  • 1
    I think you're looking for some statefulness, I would take a look at [vuex](http://vuex.vuejs.org/en/intro.html) – dops Jun 19 '17 at 09:13
  • One of the reasons I started using Vue is because it's supposed to be "lightweight" and you're supposed to be able to adopt it a little bit at a time. I didn't want to cut my whole application state over to a Vue-specific store yet (and I think I'd spend more time switching over to a global message bus model than I'd save using Vue in the first place). Isn't there some way to use "just a little Vue"? – Coderer Jun 19 '17 at 09:35
  • I think the broader problem might be that I'm looking to observe objects that have properties that already use a getter/setter -- I believe that under the hood, exported modules provide a read-only view by hiding the actual properties and exposing only a generated getter. So, if I have an object and want to observe a getter, is there a good pattern for doing that? – Coderer Jun 19 '17 at 10:16

1 Answers1

3

I'm feeling a little bit sheepish at the moment. I went down a little rabbit hole based on some syntax I saw in your question and that let to a whole bunch of unnecessary gyrations. The syntax was this:

data() {
  return {
    enabled: settings.showOverlay
  };
}

Which, for some reason, I interpreted as "well sure, whenever enabled changes, settings.showOverlay is going to change because Vue is reactive".

Yeah, no.

In that code, settings.showOverlay is just the initial value for the enabled property. The enabled property will be reactive, but in no way is it going to pass values to the settings object. Basically the data function returns an object with an enabled property that has an initial value of whatever settings.showOverlay is and then that object is turned into a reactive object.

If you want the changes made in Vue to be passed along to your settings object then all you need to do is expose the settings object on Vue's data object.

data() {
  return {
    settings,
  };
}

Now if you have code like

<div v-show="settings.showOverlay"> Some stuff </div>
<button @click="settings.showOverlay= !settings.showOverlay"></button>

settings.showOverlay will not only be reactive in the Vue, but in the settings object. No need for any of the hoops I jumped through below (/facepalm).

FWIW I believe some of the links I mentioned in the comments are referring to the data object itself. The data object needs to be a plain javascript object, not necessarily all the properties on it.

In other words, in

data() {
  return something
}

something must be a plain javascript object.

Original Answer

I've done this in a couple ways in my Vue apps. In my first app I wanted to do the same thing, store the settings in an external module that could manage persisting the settings and expose those settings on my Vue. I ended up writing a class that looks like this.

class Settings {
  constructor(){
    // read settings from persisted solution
  }
  get(key){
    // return "key" from settings
  }
  set(key){
    // set "key" in settings
  }
  save(){
    // save settings to persisted solution
  }
}

export default Settings

And then used that in my Vue like this.

import Settings from "./settings"

new Vue({
  data:{
    someSetting: Settings.get("someSetting")
  }
})

And then some point later, trigger set() and save(). That point for me was whenever a route change was triggered, I'd just set all the settings back to the Settings object and then save.

It sounds like what you have is you're exporting an object that has getter/setter properties possibly something like this.

export const settings = { 
  overlay: stored.showOverlay,
  get showOverlay(){
    return this.overlay
  },
  set showOverlay(v){
    this.overlay = v
  }
}

Where you maybe trigger a save when set is triggered. I like that idea better than the solution I described above. But getting it to work is a little more work. First I tried using a computed.

new Vue({
  computed:{
     showOverlay: {
       get(){ return settings.showOverlay }
       set(v) { settings.showOverlay = v }
     }
  }
})

But that doesn't quite work because it doesn't reflect changes to the Vue. That makes sense because Vue doesn't really know the value changed. Adding a $forceUpdate to the setter doesn't work either, I expect because of the caching nature of computed values. Using a computed in combination with a data property, however, does work.

new Vue({
  data(){
    return {
      showOverlay_internal: settings.showOverlay
    }
  },
  computed:{
     showOverlay: {
       get(){ return this.showOverlay_internal }
       set(v) { 
         settings.showOverlay = v 
         this.showOverlayInternal = v
       }
     }
  }
})

That changes both the state of the Vue and triggers the change in the settings object (which in turn can trigger persisting it).

But, damn, that's a lot of work.

It's important to remember sometimes, though, that the objects we use to instantiate Vue are just plain old javascript objects and we can manipulate them. I wondered if I could write some code that creates the data property and the computed value for us. Taking a cue from Vuex, yes we can.

What I ended up with was this.

import {settings, mapSetting} from "./settings"

const definition = {
  name:"app"
}

mapSetting(definition, "showOverlay"

export default definition

mapSetting does all the work we did above for us. showOverlay is now a computed property that reacts to changes in Vue and updates our settings object. The only drawback at the moment is that it exposes a showOverlay_internal data property. I'm not sure how much that matters. It could be improved to map multiple properties at a time.

Here is the complete code I wrote that uses localStorage as a persistence medium.

function saveData(s){
  localStorage.setItem("settings", JSON.stringify(s))
}

let stored = JSON.parse(localStorage.getItem("settings"))
if (null == stored) {
  stored = {}
}

export const settings = { 
  overlay: stored.showOverlay,
  get showOverlay(){
    return this.overlay
  },
  set showOverlay(v){
    this.overlay = v
    saveData(this)
  }
}

function generateDataFn(definition, setting, internalName){
  let originalDataFn = definition.data
  return function(){
    let data = originalDataFn ? originalDataFn() : {}
    data[internalName] = settings[setting]
    return data
  }
}

function generateComputed(internalName, setting){
  return {
      get(){
        return this[internalName]
      },
      set(v){
        settings[setting] = v
        this[internalName] = v
      }
  }
}

export function mapSetting(definition, setting){
  let internalName = `${setting}_internal` 
  definition.data = generateDataFn(definition, setting, internalName)

  if (!definition.computed)
    definition.computed = {}

  definition.computed[setting] = generateComputed(internalName, setting)
}
Bert
  • 80,741
  • 17
  • 199
  • 164
  • That's a really exhaustive answer -- thanks for your hard work! I think I'm going to sideline my Vue port for now. I think you can agree that the above solution does a lot of dancing around to support binding, but I have other (non-Vue-centric) logic that I'd have to Vue-ify just to get it working at all, and I simply don't have the time right now. Maybe I'll do some more reading on Vuex later on, when it's time for a total rewrite. – Coderer Jun 20 '17 at 13:50
  • @Coderer That's a fair statement. I've done some searching on this after I came up with a solution because it does seem a little too complicated. Vue really does want to [work with plain objects](https://vuejs.org/v2/api/#data) in data. That said, Vue is typically the framework I find most easily integrates into other projects. I'm a proponent of holding off on Vuex until you *need* it. I think this case in particular, where your properties on your external object are already getters/setters, is the main source of pain here. I use objects with methods and plain properties all the time. – Bert Jun 20 '17 at 16:08
  • @Coderer Evan You also talks about it briefly [here](https://github.com/vuejs/vue/issues/2718), "All prototype methods are ignored. Using non-plain objects as state is a rabbit hole to complexity and it is not supported by design". – Bert Jun 20 '17 at 16:10
  • That quote exactly captures my issue, thank you. I think what's missing for me is a good example/pattern to follow. It's not just this Settings module that I wrote, there are classes from a library I'm using and I need to "bind" (logically) to their properties, bi-directionally. I think if I crack that, it would also resolve my Settings issue. – Coderer Jun 21 '17 at 07:30
  • I went ahead and [asked the other question](https://stackoverflow.com/questions/44670492/how-should-i-control-the-state-of-a-class-instance-property-with-a-vue-component). Maybe it's a clearer statement of my larger problem? – Coderer Jun 21 '17 at 08:18
  • @Coderer I made a really dumb mistake. Check the updated answer above. – Bert Jun 22 '17 at 03:59
  • Is there some place to post a Fiddle that works with ES6 import statements, maybe by simulating Webpack behavior or similar? I believe your updated answer won't work in that case because `data() { return { settings:settings }; }` means that `data` is a plain object but `data.settings` is a read-only generated view of the object that only "really" exists in the global namespace of `Settings.js`, due to [the way `import` works](http://exploringjs.com/es6/ch_modules.html#sec_imports-as-views-on-exports). – Coderer Jun 22 '17 at 13:07
  • @Coderer Try this. Clicking the toggle button and refreshing between clicks will preserve the setting. https://www.webpackbin.com/bins/-KnFHKDZGHZdc0P-7BTH – Bert Jun 22 '17 at 15:05
  • That's fantastic, thank you so much Bert. I guess my understanding of the "read-only nature" of imports is misguided -- if I can add properties to an exported object, then what's "read-only" about it? Anyway, that's a different question. I will pursue your suggested approach. – Coderer Jun 26 '17 at 08:13