Looking for advanced QTree example with lazy loading



  • Hi, I’m relatively new to Quasar, but already love it.

    I have written some basic form components with Vue and Quasar which connect to my Node/MongoDB backend code to display and update data.

    My next challenge is to create a component basd on QTree, which should be able to lazy load tree-structured information from my database schema. I prepared already some REST API calls in my backend which e.g. allow to get all the direct child nodes of a specific parent node in the tree from the database. I expected that I will need this kind of backend function when writing the new QTree based component at the frontend.

    Is there somewhere (on this forum or elsewhere) a good example or tutorial which explains how to do the QTree lazy loading and other related code for QTree? I would prefer to start from such a a boilerplate rather than re-inventing all the code myself. I saw there are nice QTree examples in the Quasar doc, but they seem to be more basic, no lazy loading and other backend interaction.

    Thanks, and sorry to ask these beginner’s questions.
    Michael



  • https://quasar.dev/vue-components/tree#Lazy-loading just swap the settimeout function with your actual api call.



  • Thanks a lot! Will try that.

    (It seems I had overlooked the lazy loading one in the examples for QTree - my fault)



  • With lazy loading auto and expand doesnot work. You may look at my example here

    https://forum.quasar-framework.org/topic/4033/not-solved-by-quasar-but-manual-coding-auto-expand-for-lazy-qtree-node



  • @maxxiris - thanks for the hint.

    I’m now thinking to start my implementation without lazy loading, to avoid these complications at the beginning.
    This means I do not need my new backend API function that returns (only) the populated children…
    Instead I will need a backend API that supplies the whole tree and load that into data() of QTree, to start with.
    Will try to add lazy loading at a later point then.



  • @maxxiris - I have changed my mind - I’m now using your code as the boilerplate for my qTree. I intend to implement lazy loading based on mounted, onLazyLoad, and data(). I would like to omit the watch initially, which seems to deal with the QTree restrictions you have discoverd.

    I have one question, probably due to my limited experience with Vue and Quasar:
    Why does your code not require any declarations in Data() of how the treeData array is structured? Is this totally transparent for QTree?
    I realize that QTree relies on textual matching of the id and label names in the JSON that makes up the treeData array in data(), to bind them with ids and labels needed to render the tree.

    But isn’t it necessary to define the same properties in data(), and preset them with some values, like
    data() return {
    treedata; [
    label: “root”,
    lazy: true,
    id: null,
    children: []
    parent_id: null ]}
    I saw another example that has such definitions, while yours has an empty treeData[], and I’m struggling now a bit how to implement it in my code.

    Thanks for your help.



  • I notice I made a mistake in the declaration, forgot { } around the name: value pairs. Maybe that answers also my own question. It seems that data() is kind of an initialization to the array, which is optional? At which point in the lifecycle of the component is this initialization then done?

    data() return {
    treedata; [ {
    label: “root”,
    lazy: true,
    id: null,
    children: []
    parent_id: null } ]}



  • You can put a v-if on your qtree to only show it when the data you are passing resolves coming from your api, to avoid error or yeah init it with an empty array. Data should already be available on create.



  • Thanks for all your help for a Quasar and QTree newbie!

    There is this specific “magic” with QTree that I still don’t fully understand:

    When onLazyLoad ({ node, key, done, fail }) is called, it looks like the done function expects an array of new children? Is this correct? It then detects the children in the JSON by their unique ids, as defined by node-key, and does all the rendering of the new children under the parent node automagically?

    Does it also automatically push the new children to the array of existing ones? Or do I need to call another API for that?



  • If you see the example it actually does that.



  • Oh yes. Sorry it is getting late, and I’m getting a bit tired 😉



    1. First when render Tree, data[] is empty. Because it is lazy load, we get root node from Ajax in mounted: async function() {
    2. done is truly magic :D. done(treeNodes ) //Draw tree at the end of lazy function mean the final node need to be draw. Any code above this line is just for generating treeNodes object


  • @maxxiris - thanks a lot! I’m slowly getting it 😉 I now got the mounted() part to work for me., but I’m still getting errors in my onLazyLoad.

    Before I get to the problem in onLazyLoad: Another difference between my code and your code is in the REST call to the server to retrieve the children. You use axios, while (currently) my code relies on simple Vue http.get to the backend.

    My backend’s response to the get in my first onLazyLoad call has this format:

    {"kinder": [ {"_id":"5d4349ad68e8cc05f4694c10","... }, {"_id":"5d4349ad68e8cc05f4694c16"," ...} ] }
    

    It contains an array with 2 children. The node-id in my case is “_id” (String, different from your id, but that should not matter)
    But the [array] within the JSON is prefixed with {“kinder”: [array] }
    Not sure I can easily change that at the backend, since it comes from a call to Mongoose/MongoDB.
    Do you have an idea whether that format is a problem for the children input to Vue/QTree and how to deal with that?

    Back to my other problem: I have partial success, since my mounted(), derived from your code, seems to run correctly. It displays the root node including its label (called “fachlicherTyp” in my code). This is the good news part. But my onLazyLoad is still messed up. I can’t even get to the above get call to pick up the children from the backend in onLazyLoad, since for some reason the parent id in onLazyLoad is null.

    Note: My QTree node-key is “_id”, which is an id generated by Mongoose/MongoDB in String format, my lable-key is “fachlicherTyp”, another String.

    When running the code, in the console, there is this warning which indicates some things are still wrong:

    [Vue warn]: Invalid prop: type check failed for prop "nodes". Expected Array, got Object 
    found in
    ---> <QTree>
           <BaustoffTree> at src/components/BaustoffListe.vue
             <QPageContainer>
               <QLayout>
                 <App> at src/App.vue
                   <Root>
    

    Through console log messages I inserted, I see that parent_id in onLazyLoad is null (although correctly set earlier in mounted(), as proven by other log messages there). It is worse, the whole access to the nodes in onLazyLoad is corrupted, probably due to the warning above. For my _id, I’m not sure whether parent_id = node._id; is correct or parent_id = node.id – I assume node._id is correct per the QTree API doc. But it doesn’t matter, I tried both and get the same errors in both cases.

    Can you help me fix this problem?

    My current code is this:

    <template>
      <div class="list row">
        <div class="col-md-6">
          <h4>Baustoffbaum</h4>
          <div class="q-pa-md">
            <q-tree
              :nodes="treeData"
              default-expand-all
              node-key="_id"
              label-key="fachlicherTyp"
              @lazy-load="onLazyLoad"
            />
          </div>
        </div>
      </div>
    </template>
    
    <script>
    import http from "../http-common";
    import Vue from "vue/dist/vue.js";
    
    export default {
      name: "baustoff-tree",
      data() {
        return {
          treeChange: 0, // from boilerplate code
          treeData: [
            {
              _id: null, // set to null initially, will be set to root node's _id by mounted()
              fachlicherTyp: "Baustoffe", // used as label-key in QTree
              children: [], // defines children in QTree
              lazy: true
              parent_id: null, // added after first trials
            }
          ] // /Array
        };
      },
    

    OnLazyLoad:

     methods: {
           /* eslint-disable no-console */
      LazyLoad({ node, key, done, fail }) {
      // Reusing code from https://forum.quasar-framework.org/topic/4033/not-solved-by-quasar-but-manual-coding-auto-expand-for-lazy-qtree-node/3
      let parent_id = node._id; // ?????? Is this correct ?????
        console.log("In onLazyLoad - parent_id: "+parent_id); // Displays _id correctly after code changes in mounted()
        console.log("In onLazyLoad - key:       "+key); // Displays key correctly after code changes in mounted()      
      http // REST call to get children from backend
        .get("/baustoff/kinder/"+parent_id)
        .then(response => {
          console.log(response.data);
          // If no children tree return
          if (response.data == null) {
            done([]);
            return;
          }
          // Process to create tree
          let donvis = response.data; // tribute to my Quasar forum friend @maxxiris 
          let treeNodes = []; // this code from @maxxiris uses treeNodes, not treeData[] in data() !!!
          donvis.kinder.forEach(child => {  // New code to handle donvis object from Mongoose
                 treeNodes.push({
                 fachlicherTyp: child.fachlicherTyp,
                 _id: child._id,
                 lazy: true,
                  parent_id: parent_id,
              })     }
          // done(treeNodes); //Draw tree
          this.treeChange++; //Marking tree change
          return null;
      })
        .catch(e => {
          console.log(e);
        });
    }, // /onLazyLoad()
    }, // /methods:
    

    Mounted:

    mounted() async function() {
        http // REST call to get root node - working fine!
        .get("/baustoffe/wurzel/")
        .then(response => {
          console.log(response.data);
          // If no data return
          if (response.data == null) {
            return;
          }
          let responseData = response.data[0]; // Had to add [0], since response is an array [_id: ..., fachlicherTyp: ...]
          this.treeData = [ { fachlicherTyp:responseData.fachlicherTyp, _id:responseData._id, lazy: true } ]; // Need [] for arrray, otherwise error in onLazyLoad
           // parent_id not needed for root node
            this.treeChange++; // Inform changing in tree
            console.log("After mounted: _id of root node: "+this.treeData[0]._id+" fachlicherTyp: "+this.treeData[0].fachlicherTyp);        // ...and the root node shows up correctly in QTree, and displays correctly fachlicherTyp as label - success for the root node!
        })
        .catch(e => {
          console.log(e);
     });  } // /mounted()
        
     }; // /export default:
        
     </script>
    

    Style:

    <style>
    .list {
      text-align: left;
      max-width: 450px;
      margin: auto;
    }
    </style>
    


  • After some debugging, I noticed that I made a small but serious mistake in mounted() - I assigned an object to treeData, not an array. Changing the code in my last post, adding [ ].

    Now the parent_id and node are correctly recognized in onLazyLoad. I can see the children after the GET call in Vue DevTools.

    The QTree is however not expanding and rendering the children after the lazy loading of the children, it is just “spinning”, continuing to show the root node.

    Is this the problem that you encountered, that a lazy node does not expand, even after lazy loading, or is this still a different problem in my code?

    Do I need to add watch() now to correct this problem?

    Still debugging…will keep you posted.



  • I found out that the onLazyLoad code as above still has a problem in my case. It looks like the GET to fetch the children is successful, it returns again an array of 3 children. However in this format {“kinder”: [ { …}, {…} ] }, which seems to be an array wrapped into an object. Different from mounted(), where my REST call returned a “pure” arrray [ {name: value, name: value, … } ] which caused different problems, solved through use of [].

    Using your boilerplate code for onLazyLoad, I’m trying to iterate in onLazyLoad now over this “wrapped” array, after copying response.data to donvis, I can check through console log that donvis.length is 0 however! Therefore the for loop in onLazyLoad never gets executed. No surprise QTree never gets the new children…

    I solved this array issue for mounted(), using [] at two spots in the code, but I don’t know how to solve this for onLazyLoad.

    I guess your code relies on not getting an array back, while my backend returns these special formats, pure arrays (get root) or wrapped arrays (get children). I can see the children in Vue tools nicely though (array wrapped in object), but the mapping to the array that qTree needs (treeNodes) is a mystery to me.

    I know I need to work on my backend code to more consistently return the data, but isn’t there some simple way to deal with it for the time being, in the Vue code?

    Help needed…



  • Job done for today. I got an answer from Stackoverflow how to deal with this, see https://stackoverflow.com/questions/57331677/how-to-map-json-coming-from-mongoose-to-vue-and-quasar/57331883#57331883

    It looks like what is coming in from Mongoose in my REST calls needs to be treated as an object, not as a JSON string or the like. It needs to be handled with methods for the object. Changing the code accordingly in onLazyLoad above.

    QTree now works perfefctly for me. Haven’t looked yet at the restrictions discovered by @maxxiris .

    @metalsadman and @maxxiris - thanks for help and the great boilerplate. Without it it would have taken me probably more than a week rather than 2 days to master QTree.



  • Sorry to bother you again with a question that is probably quite basic for experienced Quasar/Vue developers:

    Now that I have my Qtree component with lazy loading working, I need to connect the tree component with another QForm component that I already have prepared before. That other component renders a Form that allows to display and edit the data that is behind a node in the QTree.

    It’s a pretty basice scenario: When the user selects a certain node in the tree, I would like that other component fire up the form that allows to edit the data in the tree node. The other component has props defined that reflect the same data that is underneath each tree node in my tree code above.

    How do I connect QTree with that QForm, such that the QForm is called with the right information, based on which node (called “baustoff”) the user selects in QTree?

    My old code (before I implemented the tree) had a list of router links (instead of the QTree) that called that form with the right “baustoff” object… But I believe I need to do this differently with QTree.

    I can supply the code for the QForm component if needed, but would appreciate some hints what to add in QTree to link the two, based on selection.



  • I was able to connect my Qtree with a form through the use of :selected.sync=“SelectedKey” on qTree. The form renders side-by-side with QTree, and I’m able to see
    the correct value of selectedKey through a <div>{{ selectedKey }}</div>, which displays the id (named _id in my code) of the selected QTree node.

    But that’s about all. I don’t know how to get all the other properties of that node (which is usually a parent node) to display them in the form.

    The QTree doc says one can access the node through QTree API getNodeByKey(selectedKey), but all my attempts to call that e.g. within a v-model of a qInput field within the form have failed.

    Is someone experienced with Quasar/Vue able to provide me with some sample code to solve this problem? Thanks a lot in advance for any hints.



  • I solved my last problem through a new event added to my q-tree:

    • @update:selected=“handleSelectedNode”

    • That calls a new event handler handleSelectedNode(key) when the user selects a new node in the tree (= an update to the selected node).

    • The key (as defined to QTree, together with label) of the selected tree node is passed to the event handler as a parameter

    In order to be able to issue Q-Tree API calls from within handleSelectedNode(key):

    • One needs to add a so-called reference in the q-tree template: q-tree ref=“tree

    • Then, from methods of the component, one can do API calls that refer to that reference of the component.

    • Example: this.selectedNode = this.$refs.tree.getNodeByKey(key); stores the node information of the node defined by key in data() - as mentioned above, key is passed by the @update:selected event as a parameter to handleSelectedNode(key)

    • The Quasar API docs are not very clear in that regard. It took me a long time to find out that this reference to tree is critically needed to do Quasar API calls.

    • Through that API I copy the complete node information of the selected node to a new object in data().

    • My form to render the selected node in the tree uses that object filled by handleSelectedNode in data(), through v-model.

    That’s it - now I’m able to display the tree with QTree and the data for a selected node with a QForm - will soon try a QTable there.

    Quasar is great once you master some of the hurdles I described above. Hope others can make use of these lessons I learned. 🙂



  • This post is deleted!

Log in to reply