Creating an editor for complex documents with Vue/Vuex/Typescript/Firebase: looking for advice

Hi! I’m fairly new to web dev but have written C/C++/python for about 35 years. I’m writing an app to create and edit complex documents (“projects”) – not just text but 3d graphics, images, video. The projects are complex hierarchical objects and I know I’ll want to add more complexity as I go on. I’m using Typescript, Vue, Vuex and Firebase as the back end.

My naive approach is to have a top-level typescript Project class that contains everything - Questions, Shots, Assets, Settings, and those have sub-objects and so on – so in theory I could write that whole object out through Vuex to Firebase as is, and read it in (using class-transformer for instance). It does seem like the Vuex representation should be the single source of truth for the app.

But that doesn’t explain how to hook things up to components, nor especially how to use Vuex. And, it’s going to get super heavyweight. Everything I’ve read (e.g. vuex best practices for complex objects) says to store things “normalized” in Vuex, like db tables, and keep references in parent objects. So a Project would have a list of shot IDs [1, 9, 33] and those index into the Shot table in Vuex. And that maps nicely to Firebase documents and collections.

But it seems to complicate the front end; instead of one Project I have all these separate bits to gather. I can’t just say for param in project.shots[s].representations[r].params – it’s not clear to me how to hook up, say, a slider component whose model is a Param inside a Representation inside a Shot so that everything stays in two-way sync and gets saved to Firebase and restored nicely, and I get nice Typescript classes for type-safety. Can this be done with enough fancy setters and getters?

All this is to say, are there examples of best practices to follow when creating a complex editor in Vue/Vuex/Firebase like this? If you have experience doing this kind of thing, I’d like to hear from you.

1 Like

Hi!
I’m fighting with a similar problem. Here is my thoughts:

  1. As you probably know, when you pass some object to Vue as the data (or part of the data), it turns object/subobject properties to getters/setters, setters do the observer role. Thus, to get object being reactive it must become a part of Vue instance data. The simplest way to do it: new Vue({data: someObject})

  2. The number of transformations between reactive object and end-component (that performs rendering) doesn’t matter, so you can for example:

Vue.prototype.$myStore = new Vue({data: {state: someObject}}));

//at app-component
computed:{ 
    project(){
           //assume  this.$myStore.state contains all data
           // for other classes of the Project 
           return new Project(this.$myStore.state);
    }
}

The disadvantage of this example is that any changes of the state trigger recreation of whole project instance, that can be too expensive and undesirable.

So, the core of the problem is how to hold data in the classes.

Mapping data from Vuex using plane JS getters/setters:

class Project{
    constructor(vuexStore){
         this.$store = vuexStore;
    }
    get prop(){ return this.$store.state.prop;}
    set prop(v){this.$store.dispatch('setProp', v);}
}

//with this approach we can just
Vue.prototype.$project = new Project(vuexStoreInstance);
//and use it directly even in the template at any component 
<input  v-model = '$project.foo.bar.prop'>

Disadvantages:

  • it may be quite verbose
  • if you want to cache something at class level you have to manage it by yourself

Class as store

// top level class def
class Project{
    constructor(){
        this.vm = new Vue({data: {state: ...}});
        this.foo = new Foo(
            this.vm, 
            ()=> this.vm.state.path.to.fooState,
            (st)=> {this.vm.state.path.to.fooState = st;}
        );
    ...
}

class Foo{
    constructor(vm, getState, setState){
          this.vm = vm;  
          // getState returns state-object using full chain of properties,
          // so we can somewhere this.vm.state.path.to.fooState = newFooState 
          this.getState = getState;
          // setState allows to replace whole foo-state from within foo
          this.setState = setState; 

          // mount computed cacheable prop
          const unwatch =  this.vm.$watch(
              this.fooComputeProp.bind(this),
              (v)=> {this.vm.getState().fooComputedProp = v;}, //assume that fooComputedProp already exists
              {immediate: true} 
          )
          // we should drop all watchers before foo destroy
          this._unwatchers = [];
          this._unwatchers.push(unwatch);

    }        
    fooComputeProp(){ 
              const state = this.getState();
              //won't be recomputed until a or b is changed
              return state.a + state.b; 
    } 
    updateX(value){
          this.getState().x = value;
    }
}