2

I'm working on a worldbuilding app as my first Vue.js project. As part of this, I have several ES6 classes that describe what certain data types can look like (e.g. An entire Project, a Planet, a Star, etc). I'm trying to build a sidebar that will be used to see the nested tree structure of a project like so:

Project
|- System
   |- Star
   |- Planet
      |- Planet

The tree structure works great when I load in an existing data file or edit the name of an existing entry. The problem comes when I try to add additional items to the tree. The data and source file are updated with the new entity, but the render does not update to reflect the additional data.

My most recent attempt looks like this (addItem() is where the item gets added to the project):

//ProjectEntry.vue
<script setup>
import { nextTick, ref, computed } from 'vue';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'

import { Button, ToolButton } from '@/components';
import { Project,Planet, System, Star, Galaxy } from '@/stellarClasses';
import { useDataStore } from '@/stores';

const SOs = {Planet, System, Star, Galaxy};

const name = ref(null);
const loading = ref(false);
const edit = ref(false);
const {
    data,
    type,
    level,
    content,
    nameProp,
    project
  } = defineProps({
    data:Object,
    level:[Number,String],
    content:String,
    type:String,
    nameProp:String,
    project:Object
  });
  console.log('data',data);
const children = ref(data.children || data.value?.children);

const contained = computed(() => Object.entries(children.value));
const dataStore = useDataStore();
console.log('type',type);
const possibleAdds = {
  Project: ['System','Star','Planet'],
  System:['Star','Planet'],
  Galaxyy:['System','Star','Planet'],
  Star:[],
  Planet:['Planet']
  
};
const icons = {
  System:'solar-system',
  Star:'sun',
  Galaxy:'galaxy',
  Planet:'earth-oceania'
};
const nextLevel = +level > 5 ?
  undefined :
  +level;
const toggleEdit = () => {
  edit.value = !edit.value
  nextTick(()=>{
    name.value?.focus();
  });
};
const submitValue = async (event) => {
  if(!name.value) return;
  if(name.value.value !== data[nameProp]){
    data[nameProp] = name.value.value;
    project.save();
    // debugger;
    // await dataStore.updateWorld(
    //   data.id,
    //   data
    // );
  }
  toggleEdit();
};

/**
 * Adds a new item of the indicated type to the children of the parent.
 */
const addItem = async (type) => {
  const mySO = new SOs[type]({name:`New ${type}`,type});
  // debugger;
  data.value.__v_raw.addChild(mySO);
  await data.value.__v_raw.save();
};

</script>

<template>
<li :class="`project-entry ${type}${dataStore.loading ? ' loading' : ''}`">
  <div>
    <span v-if="icons[type]" class="fa-li">
      <FontAwesomeIcon :icon="`fa-solid fa-${icons[type]}`" />
    </span>
    <component :is="level ? `h${level}` : 'span'" v-if="!edit" @dblclick="toggleEdit">
      {{content}}
    </component>
    <input ref="name" v-else @focus="$event.target.select()" type="text" @keyup.enter="submitValue" @blur="submitValue" :value="content">
  </div>
  <div class="insert-container">
    <Button v-for="typ in possibleAdds[type]" :key="`${data.id}-${typ}`" @click="addItem(typ)" :title="`Add ${typ}`">
      <FontAwesomeIcon :icon="`fa-solid fa-${icons[typ]}`" />
    </Button>
  </div>
  <ul v-if="contained.length" class="fa-ul">
    <ProjectEntry v-for="[id,obj] in contained" :data="obj" :key="id" :level="nextLevel" :project="project" :type="obj.type" name-prop="name" :content="obj.name || `new ${obj.type}`" />
  </ul>
</li>
</template>

<style lang="scss">
// Styling omitted for brevity
</style>

The above is called from this line of the editor's view:

<ProjectEntry v-for="obj in dataStore.worlds.__v_raw" :key="obj.id || obj.name" level="4" type="Project" :data="obj" :project="obj" name-prop="name" :content="obj.name || 'new project'"></ProjectEntry>

The children of a given entity are stored in an object on the class called children indexed by the child's id. Here's one of my test datafiles to demonstrate. Note that some info has been reduced down to just it's ID instead of the full object as part of the serialization of the class stack:

{
  "type": "Project",
  "name": "Torrigoyd",
  "id": "9b33f1c0-6499-11ed-b4f9-c95a1a859b28",
  "creation": 1668484641500,
  "children": {
    "fced8ea0-6541-11ed-b960-3dcc95839585": {
      "parent": "9b33f1c0-6499-11ed-b4f9-c95a1a859b28",
      "type": "Star",
      "name": "Tor",
      "id": "fced8ea0-6541-11ed-b960-3dcc95839585",
      "creation": 1668556960906,
      "children": {},
      "mass": 1,
      "age": 4.5
    },
    "e33ef3b0-65d0-11ed-84cd-07d9a27628fb": {
      "parent": "9b33f1c0-6499-11ed-b4f9-c95a1a859b28",
      "type": "Planet",
      "name": "Torrigoyd",
      "id": "e33ef3b0-65d0-11ed-84cd-07d9a27628fb",
      "creation": 1668618335851,
      "children": {}
    },
    "252eb930-65d2-11ed-9a40-2330c26a73ef": {
      "parent": "9b33f1c0-6499-11ed-b4f9-c95a1a859b28",
      "type": "Star",
      "name": "New Star",
      "id": "252eb930-65d2-11ed-9a40-2330c26a73ef",
      "creation": 1668618875971,
      "children": {},
      "mass": 1,
      "age": 4.5
    }
  }
}

So, how can I get the file tree to update when an item is added to the children object of the project, or one of it's descendants? Do I need to rethink my data structure (I'm sure it could be better), or do I just need to reference it a different way in the vue component?


EDIT: Realized it might be helpful to see the class methods involved:

// The addChild method
addChild(...children){
  children.forEach(child => {
    child.parent = this;
    this.children[child.id] = child;
  });
}

// The save method
save() {
  const dataStore = useDataStore();
  const newWorlds = dataStore.worlds.__v_raw;
  dataStore.$patch({
    worlds:newWorlds
  });
  this.#db.data = this.toJSON();
  return this.#db.write();
}
5
  • 1
    It's a mistake to use __v_raw, it's internal. Considering that you have problems with reactivity, this is the first thing to blame, because you deliberately use non-reactive things. Not sure if there are other problems besides that, but that's a significant one. Commented Nov 16, 2022 at 19:55
  • Your structure looks very complicated. Children of an object should be probably an array of objects with the same structure. Try to simplify the structure first. then, you should be able to make use of vue's reactivity by finding and updating a specific item in an array. And as mentioned above, do not use __v_raw. Commented Nov 16, 2022 at 20:15
  • Hmm, __v_raw is the only way to get the actual class instance methods and properties out unfortunately. .value does not provide the methods unfortunately, and I'm unsure if it preserves the this context properly. As for the array vs. object, I'll try that and see if it fixes it. Commented Nov 16, 2022 at 22:05
  • Perhaps I should just move these entity classes to being their own store types and instance a new store for each one? Commented Nov 16, 2022 at 22:07
  • and found what I was doing wrong. Commented Nov 17, 2022 at 4:07

1 Answer 1

1

Found what I was doing wrong. For some reason (my past self only knows why), I was storing the class instances as refs in the array of worlds. This of course made them NOT instances directly of the class. Removed that assignment to ref, and switched to the array children storage as suggested. All is good now.

Sign up to request clarification or add additional context in comments.

1 Comment

It'd be useful for future readers, if you posted the final working solution.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.