Skip to content

Vue Tasks App

Vue implementation of the tasks app.

description

Learn vue and make a usable tasks app. It should implement most of gqueues functionality but with

  • faster handling
  • documentation in markdown
  • links
  • better priorities

investigation

So I want a detailed page with graphic support to be able to understand this later. Try to graphically explain any step you did or at least are going to do.

subtasks

Find a suitable picture

Since we are browsing for images anyway combine this with searching for good example picks.

This link is interesting for vue anyway, but also contains examples for annotated pictures.

https://medium.com/js-dojo/component-communication-in-vue-js-ca8b591d7efa

emit to parent

Svg file

Document v-model

V-model seems to be just a shorthand for adding a prop (into the component) and a v-on: handler (out of the component)

But it seems it is always the input method .

So this :

not working
<StretchArea class="task-descr" 
            v-model="task.description">
[visit](/StretchArea)

This ends up (in the console debugger) as

console debugger log
[visit](StretchArea class="task-descr" modelValue="ddd\n        \n   " onUpdate:modelValue=fn )

So modelValue is the internal variable used for the content but also the handler for changing the content.

We better just assign input and output manually.

better split data and handler
1
2
3
4
<StretchArea class="task-descr" 
            v-bind:description="task.description" 
            @blur="updateText">
[visit](/StretchArea)

Note the v-bind

if you are wondering why you get the string ‘task.description’ in your component instead of the value, you forgot to put v-bind before the prop.

NOT Working
<StretchArea class="task-descr" 
            description="task.description"
            @blur="updateText">
[visit](/StretchArea)

We had to do this for the StretchArea component to work

actual StretchArea code
1
2
3
4
5
6
7
8
9
[visit](template)
 [visit](div class="grow-wrap")
   <textarea name="descr" id="descr" ref="input"
        @blur="update_text" 
        @input="resize_area">
        {{description}}
   [visit](/textarea)
  [visit](/div)
[visit](/template)

And the code for the component is below, note the prop is description and blur is an emitter that is called explicitly in the callback update_text. So you capture onblur here and then ‘trigger’ the onblur of the parent component yourself. Be sure to pass the data !

vue code
export default {
  name: 'StretchArea',
  props: ['description'],
  emits: ['blur'],
  methods: {
        resize_area () { 
            // references let you access the dom element of this component
            let ref = this.$refs['input']
            // so we can de this resize trick from :
            // https://css-tricks.com/the-cleanest-trick-for-autogrowing-textareas/
            ref.parentNode.dataset.replicatedValue = ref.value
        },
        update_text (e) {
            // pass this on to the parents onblur() handler
            console.log(e.target.value)
            this.$emit('blur', e.target.value)
        },
  },
  data() {
    return {
    }
  } 
}   

documentation solution

We should be able to click on a documentation button and have an editor popup to enter markdown. Solution we need to investigate:

But since we use mkdocs now, the markdown-it rendering is not so much of an issue, also it is part of the documentation project, not tasks. The main problem is entering and displaying markdown while editing and diamondfsd seems one of the candidates.

vuex getters

Getters are like computed state in vue

computed is a VUE property, not vuex.

So you can add a section in the store where you can construct a value rather than just returning the state.

getter example
const store = createStore({
  state: {
    todos: [
      { id: 1, text: '...', done: true },
      { id: 2, text: '...', done: false }
    ]
  },
  getters: {
    doneTodos (state) {
      return state.todos.filter(todo => todo.done)
    },
    getTodoById: (state) => (id) => {
      return state.todos.find(todo => todo.id === id)
    } 
  }
})

# so todos will give you all items, doneTodo gives the done item
store.getters.doneTodos // -> [{ id: 1, text: '...', done: true }]
store.state.todos // -> both items
# if you use parameters you do need the function() call type
store.getters.getTodoById(2)

firebase model

We recognize a number of data objects inside the tasks app..

  • tasks : paint the fence
  • topics : garden jobs
  • groups : house
  • layouts : priority list, tree

task

A task is a single piece of work and the endleaf of our trees.

  • a caption : 1 line
  • color : a string with name or hex value
  • description : a multi line text describing the task better
  • priority : a number
  • status : done for instance
  • tags : list of tags to add
  • documents : a list of .md documents
  • subtasks : a list of children

Maybe more, but at least we cannot present all data in the tree itself. For instance description and documents will make the tree data too big.

So there will be at least a tasks list in the database, identified by id that will be referenced from the tree structure.

A task tree is unlimited in levels and size. There is no root node, but rather an array of level 1 entries.

We have a tasktrees and a tasks collection in the app.

layout

A tree holds the data for the dragdrop trees for various components. Trees have a json format that HE-TREE-VUE can understand, mostly with 'caption', 'color' fields, etc. But trees can vary because they will be maintained by different components and in different database collections.

For now we recognize :

  • task trees : right pane tree
  • topic trees : left pane tree, 2 levels but free to edit
  • sorted trees : display as a list, but draggable and sortable
  • next trees : display next x entries of every topics as a 2 level tree
  • tags tree : maybe later, like the tags in gqueues

From now we will call these arrangements: layout's

topic

Topics are groups of tasks, and these can also be hierarchical. However we keep a maximum level of 2, so that everything beneath that is a task + possible subtasks. For instance :

  • uvt
    • OWB
    • BHV
  • klopt
    • task list
    • planner
    • house

House is divided into kitchen, bedroom etc, but that is resolved in subtasks. Every endpoint in the topic list is selectable and will load a complete task tree in the right/tasks tab.

We have a topictrees collection in the app, if we need a separate topics is not clear yet.

group

In gqueues they call this queue. It is the groups of trees in the left pane.

  • topic trees
  • sortable lists
  • next lists
  • tags

These will probably be hardcoded into separate components anyway ?!

The left pane will be something like this


  • My Tasks (layout)
    • uvt (group)
      • owb (topic)
      • bhv (topic)
    • klopt(group)
      • tasks (topic)
      • home (topic)
      • planner (topic)
    • messy (group)

  • priority lists
    • uvt tasks
      • owb
      • bhv
    • klopt tasks
      • home
      • planner
    • tasklist
      • tasks
    • everything
      • uvt
      • klopt

  • next lists
    • everything
      • uvt
      • klopt
    • uvt
      • uvt
    • klopt
      • tasks

  • tags
    • tag1
    • tag2

Depending on what you select in the leafs will change the right pane. A topictree does really different things than a next tree, so make all of them separate components.

The collections

topics : topictree, because the bulk of this tree is topics, only the root should says 'categories or groups' priorities : priority tree next : next tree tags : tag tree

If we look closer, all trees are roughly the same except for the handling and the data of each leaf. But fetching a tree, updating and adding a leaf is almost the same boilerplate code.

We have now split the store in different modules.

Current Store
1
2
3
4
5
6
let store = createStore({
    modules: {  
        tasks: taskstore,
        topics: topicstore
    }
})

The topicstore is also namespaced, so for each tree there could be e different object maintained. This was born when we created the topicstore like this.

Previous setup
    [visit](hr)
    [visit](TopicTree group="categories"/)
    [visit](hr)
    [visit](TopicTree group="special"/)

So this meant group was a prop to the TopicTree and we address the store like this.

    ...
    props: {
       group: String, 
       value: String
    },
    ...

    methods: {
        ...
            this.$store.dispatch(`${this.group}/fetchTopicTree`, this.group);
        ...

However, we decided to limit the trees to the list below, and also the store and databases collections.

  • TopicTree : topicstore writing to "topictree" in the database
  • TaskTree : taskstore writing to "tasktree" in the database
  • SortTree : sort store, ...
  • NextTree : next store,
  • TagTree : tags tore

Maybe we can even combine the logic into a single store writing to different collections or even one big tree collection !.

First we need to fix this bug by removing the group prop and writing to TopicTree. This also means we don't have to registerModule() the group dynamically. I leave the beforeCreate method as an example of how it works.

Unused dynamic module registration
  beforeCreate() {
        //this.$store.registerModule(this.group, TopicStore);
  },

segment problem

javascript console
Uncaught FirebaseError: Invalid document reference. Document references must have an even number of segments, but topictrees has 1

This is always a problem with the build-up of your documents in combination with the query. For instance, the next structure was built up earlier :

segment

Now you see that it goes Collection -> Document -> Collection, where documents have a generated id, and collections have a name you provided.

This alternating continues if you keep adding one-to-many relations downward, sop this error says you are trying to access a document where a collection is expected or vice-versa.

Note that the above error says the document reference is 'topictrees' (, but topictrees has 1'. For deeper paths this would read something like

Document references must have an even number of segments, but topictrees/IZQynt1j4Zk0D8bPaDJU/something has 1. 

So it says your path only has 1 segment topictrees and it should read 'topictrees/topics' as seen in the picture.

In short: I removed the extra name from this call

    const docRefTree = doc(db, "topictrees");
    // should be :
    const docRefTree = doc(db, "topictrees", "topics");

component naming

This is for the overview, open in new tab to enlarge.

segment

troubleshooting

Error messages and solutions.

javascript console
[Vue warn]: Failed to resolve component: TopicBlock
If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement. 

Simple solution, add TopicBlock to the components object of the enclosing component.

solution
components: {TopicBlock, Tree: Tree.mixPlugins([Draggable,Fold])},

bootstrap.css.map warning

javascript console
DevTools failed to load source map: Could not load content for http://localhost:3000/bootstrap.css.map: HTTP error: status code 404, net::ERR_HTTP_RESPONSE_CODE_FAILURE

As this comment points out this is not an error saying the map file is not loaded.

visit

It is just a comment at the end of the bootstrap.css file
.d-print-none {
    display: none !important;
  }
}

/*# sourceMappingURL=bootstrap.css.map */

So just delete it !. And also restart your ‘npm run dev’ server. The error will be gone!

unhandled error

This warning in the console got me searching much too long. If you try to follow the stack trace there is not one recognizable function, so heed this :

Just read the error itself !!!

value=undefined for the tasktree data !!

errors in console
runtime-core.esm-bundler.js:38 [Vue warn]: Unhandled error during execution of render function 
  at [visit](Tree class="tasktree" value=undefined edgeScroll=""  ... ) 
  at [visit](TaskTree) 
  at [visit](Pane style= {height: '100%', overflow: 'visible'} ) 
  at [visit](Splitpanes class="default-theme" vertical="" style= {height: '100%'} ) 
  at [visit](App)

This happened after splitting the vuex store, so follow that data. 

tasktree data
<Tree class="tasktree" :value="treeData" edgeScroll
        @drop="writeTree" v-slot="{index,node,path,tree}">

If we print the store inside computed.treeData() 

debug log statements
computed: {
        treeData() {
            console.log(this.$store.state)
            console.log(this.$store.state.taskTree)
            return this.$store.state.taskTree
        }
  }

It will print :

console log output
Proxy {tasks: {...}, topics: {...}}
TaskTree.vue:73 undefined

There you have it, the store now has two submodules, use this.$store.state.tasks !!, not just state.

Unhandled error during execution of beforeCreate hook

Nothing yet.. .see if it happens again.