No More Posting New Topics!

If you have a question or an issue, please start a thread in our Github Discussions Forum.
This forum is closed for new threads/ topics.

Navigation

    Quasar Framework

    • Login
    • Search
    • Categories
    • Recent
    • Tags
    • Popular
    • Users
    • Groups
    • Search

    Q-tree within Q-menu

    Help
    5
    15
    2279
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • CWoodman
      CWoodman last edited by

      I’ve built a component I call ‘ComboTree’ which provides a text-input box that combines an auto-fill feature (using q-select) and a drop-down selection menu using q-menu with an imbedded q-tree. This shows a tree-view of available options.
      Here are a couple of screen shots showing it in action:

      1. using the drop-down tree-view:
        ComboTree1.png

      2. Using the autofill feature:
        ComboTree2.png

      It works great as you can see in these screenshots. BUT, I tried to use the same component within a q-dialog and as soon as I click on an arrow in the tree-view to expand or collapse the item, the menu closes! I’ve spent the better part of a day trying to figure out what’s different, but with no luck.

      I even copied the code into this codepen, and it doesn’t work either - in fact it doesn’t even show the tree structure; just to top level nodes, and the autofill doesn’t work at all.

      Here’s the codepen, so you can see the code…
      https://codepen.io/crawfordw/pen/abdrNRN?editors=1111

      1 Reply Last reply Reply Quote 0
      • CWoodman
        CWoodman last edited by

        May have found a solution, but it’s a kludge…
        I made the menu persistent, and now it remains open while you expand a tree node. But because it’s persistent, I have to force close the menu when an item is selected, and had to add a delay before doing this! Also have to force it closed if user takes any other unrelated action or the main component loses focus, so lots of extra checks in the code. A better solution would be appreciated.

        1 Reply Last reply Reply Quote 0
        • T
          turigeza last edited by turigeza

          Although I don’t know how you could solve your case I did something similar but I used popup proxy. It takes some hacking to make it work. Like I had to have 2 tree’s to be able to reference it and use its functions. I also change if the popup is persistent or not.

          https://quasar.dev/vue-components/popup-proxy#QPopupProxy-API

          I leave the code here in case it helps but I am sure someone will come up a better way of doing this.

          <template>
              <div class="relative-position v_tree_select" @keydown="on_field_keydown" @click="on_field_click">
          
                  <q-field
                      ref="field"
                      stack-label
                      tabindex="0"
                      v-model="filter"
                      :disable="_disable"
                      :hint="hint"
                      :label="label"
                      :loading="loading"
                      @focus="on_focus"
                      @input="on_input"
                  >
                      <template v-slot:append>
                          <q-icon name="arrow_drop_down" />
                      </template>
          
                      <!-- :borderless="borderless" -->
                      <template v-slot:control>
                          <div class="v_tree_select_field_wrap">
                              <template v-if="_tree.length !== 0">
                                  <template v-if="multi_select && ticked_nodes.length > 0 ">
                                      <q-chip color="primary" text-color="white" dense v-for="node in ticked_nodes" :key="node[tree_key]">
                                          {{ node[tree_label_key] }}
                                          <svg
                                              v-if="clearable"
                                              @click.stop="remove_chip(node[tree_key])"
                                              aria-hidden="true"
                                              role="presentation"
                                              focusable="false"
                                              viewBox="0 0 24 24"
                                              class="q-chip__icon q-chip__icon--remove cursor-pointer q-icon notranslate"
                                              tabindex="-1">
                                              <path d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z">
                                              </path>
                                          </svg>
                                      </q-chip>
                                  </template>
          
                                  <template v-if="!multi_select && selected_node">
                                      <q-chip color="primary" text-color="white" dense>
                                          {{ selected_node[tree_label_key] }}
                                          <svg
                                              v-if="clearable"
                                              @click.stop="remove_chip()"
                                              aria-hidden="true"
                                              role="presentation"
                                              focusable="false"
                                              viewBox="0 0 24 24"
                                              class="q-chip__icon q-chip__icon--remove cursor-pointer q-icon notranslate"
                                              tabindex="-1">
                                              <path d="M12,2C17.53,2 22,6.47 22,12C22,17.53 17.53,22 12,22C6.47,22 2,17.53 2,12C2,6.47 6.47,2 12,2M15.59,7L12,10.59L8.41,7L7,8.41L10.59,12L7,15.59L8.41,17L12,13.41L15.59,17L17,15.59L13.41,12L17,8.41L15.59,7Z">
                                              </path>
                                          </svg>
                                      </q-chip>
                                  </template>
                              </template>
                              <template v-else>
                                  <div class="text-grey">No nodes available</div>
                              </template>
                          </div>
                      </template>
          
                      <!-- HIDDEN TREE SO WE CAN GET THE REFRENCE TO IT AND USE ITS METHODS-->
                      <q-tree
                          style="display:none"
                          ref="hidden_tree"
                          :nodes="_tree"
                          :node-key="tree_key"
                          :label-key="tree_label_key"
                          :default-expand-all="default_expand_all"
                          :selected="selected"
                          :ticked="ticked"
                          @update:selected="node_selected"
                          @update:ticked="node_ticked"
                          :key="hidden_key"
                          :tickStrategy="_tick_strategy"
                      >
                      </q-tree>
          
                      <q-popup-proxy
                          :breakpoint="600"
                          fit
                          ref="popup"
                          full-width
                          content-class="v_tree_select_dialog"
                          v-model="dialog"
                          :persistent="persistent"
                          no-parent-event
                      >
                          <!-- no-parent-event -->
                          <!-- maximized -->
                          <div class="column v_tree_select_popup_wrap">
                              <!-- <q-input bottom-slots v-model="filter" filled :dense="dense"  placeholder="Placeholder"  >
                                  <template v-slot:prepend>
                                      <q-icon name="filter_list"></q-icon>
                                  </template>
                              </q-input> -->
                              <q-input v-show="searchable" ref="filter" v-model="filter" class="v_tree_select_filter_input" clearable placeholder="Filter" filled @keydown="on_filter_keydown">
                                  <template v-slot:prepend>
                                      <q-icon name="filter_list" />
                                  </template>
                              </q-input>
                              <div class="col scroll" @keydown="on_keydown"
                              tabindex="0">
                                  <q-tree
                                      ref="tree"
                                      class="q-ma-sm col"
                                      :default-expand-all="default_expand_all"
                                      :filter="filter"
                                      :key="key"
                                      :label-key="tree_label_key"
                                      :nodes="_tree"
                                      :node-key="tree_key"
                                      :selected="selected"
                                      :ticked="ticked"
                                      :tickStrategy="_tick_strategy"
                                      @update:selected="node_selected"
                                      @update:ticked="node_ticked"
          
                                  >
                                  </q-tree>
                              </div>
                              <div class="q-pa-md col-auto v_tree_select_dialog_close">
                                  <q-btn label="Close" unelevated class="full-width" color="primary" @click="hide_dialog"  />
                              </div>
          
                              <!-- list_loading -->
                              <q-inner-loading :showing="list_loading" >
                                  <q-spinner-gears size="50px" color="primary" />
                              </q-inner-loading>
                          </div>
                      </q-popup-proxy>
                  </q-field>
          
              </div>
          </template>
          
          <script>
          
          export default {
              props: {
                  after_change: {
                      type: Function
                  },
                  borderless: {
                      type: Boolean,
                      default: true
                  },
                  clearable: {
                      type: Boolean,
                      default: true
                  },
                  close_after_select: {
                      type: Boolean,
                      default: true
                  },
                  confirm_action: {
                      type: String
                  },
                  default_expand_all: {
                      type: Boolean,
                      default: true
                  },
                  disable: {
                      type: Boolean,
                      default: false
                  },
                  field: {
                      type: String
                  },
                  hint: {
                      type: String
                  },
                  label: {
                      type: String,
                      default: ''
                  },
                  load_trigger: {
                      type: [String, Number, Object]
                  },
                  multi_select: {
                      type: Boolean,
                      default: false
                  },
                  section_open: {
                      type: Boolean
                  },
                  searchable: {
                      type: Boolean,
                      default: true
                  },
                  sync_data: {
                      type: [String, Object, Array]
                  },
                  sync_selected_key: {
                      type: String,
                      default: 'relations'
                  },
                  sync_action: {
                      type: String
                  },
          
                  tick_strategy: {
                      type: String,
                      default: 'strict'
                  },
          
                  // tree
                  tree: {
                      type: Array
                  },
                  tree_key: {
                      type: String,
                      required: true
                  },
                  tree_label_key: {
                      type: String,
                      required: true
                  },
                  tree_field: {
                      type: String
                  },
                  tree_action: {
                      type: String
                  },
                  tree_getter_data: {
                      type: [String, Number, Array, Object]
                  },
                  tree_getter: {
                      type: String,
                      default: ''
                  },
          
                  // value
                  value: {
                      type: [String, Array, Number]
                  },
                  value_action: {
                      type: String
                  },
                  value_action_data: {
                      type: [String, Object, Array]
                  },
                  value_getter: {
                      type: String
                  }
          
              },
              data: function () {
                  return {
                      dialog: false,
                      expanded: [],
                      filter: '',
                      hidden_key: 1,
                      key: 1,
                      list_loading: false,
                      loading: false,
                      model: null,
                      persistent: false,
                      selected_node: null,
                      ticked_nodes: []
                  };
              },
              computed: {
                  _disable () {
                      if (this._tree.length === 0) {
                          return true;
                      } else if (this.loading) {
                          return true;
                      } else {
                          return this.disable;
                      }
                  },
                  selected () {
                      if (this.multi_select) {
                          return '';
                      }
                      return this._value;
                  },
                  _tick_strategy () {
                      if (this.multi_select) {
                          return this.tick_strategy;
                      } else {
                          return undefined;
                      }
                  },
                  _value () {
                      let v = '';
          
                      if (typeof this.value !== 'undefined') {
                          v = this.value;
                      } else if (this.value_getter) {
                          if (typeof this.$store.getters[this.value_getter] === 'function') {
                              v = this.$store.getters[this.value_getter](this.field);
                          } else {
                              v = this.$store.getters[this.value_getter];
                          }
                      } else if (this.field) {
                          v = $younit._get(this.field);
                      } else {
                          throw new Error('There is no value provided for tree select.');
                      }
          
                      return v;
                  },
                  _tree () {
                      if (this.tree) {
                          return this.tree;
                      } else if (this.tree_getter) {
                          return this.$store.getters[this.tree_getter](this.tree_getter_data);
                      } else if (this.tree_field) {
                          return $younit._get(this.tree_field);
                      }
          
                      return throw new Error('NO SOURCE FOR THE TREE');
                  },
                  ticked () {
                      if (!this.multi_select) {
                          return [];
                      }
          
                      return this._value || [];
                  }
              },
              watch: {
                  section_open: function (n, o) {
                      this.load();
                  },
                  load_trigger: function (n, o) {
                      this.load();
                  }
              },
              methods: {
                  hide_dialog () {
                      setTimeout(() => {
                          this.dialog = false;
                      }, 30);
                  },
                  focus () {
                      this.$refs.field.focus();
                  },
                  async node_selected (v) {
                      if (!this.sync_action) {
                          this.$emit('input', v);
                          if (this.close_after_select) {
                              this.hide_dialog();
                          }
                          return;
                      }
          
                      if (this.multi_select) {
                          // if (this.tickStrategy !== 'strict') {
                          //     // for now only this startegy is supported because the tick strategy of leaf I can not reproduce the behaviour
                          //     return;
                          // }
          
                          const ticked = [...this.ticked];
                          // const ticked = this.$refs.hidden_tree.getTickedNodes().map(item => item[this.tree_key]);
                          const index = ticked.indexOf(v);
                          if (index > -1) {
                              this.$refs.tree.setTicked([v], false);
                          } else {
                              this.$refs.tree.setTicked([v], true);
                          }
          
                          return;
          
                          // // selected node is already ticked
                          // if (ticked.indexOf(v) > -1) {
                          //     ticked.splice(index, 1);
                          // } else {
                          //     ticked.push(v);
                          // }
          
                          // // return this.node_ticked(this.ticked);
                          // // return;
                          // // return this.node_ticked(ticked);
                      }
          
                      try {
                          const old_value = this._value;
                          this.list_loading = true;
          
                          this.selected_node = this.$refs.hidden_tree.getNodeByKey(v);
          
                          if (this.confirm_action) {
                              this.persistent = true;
                              const rs = await this.$store.dispatch(this.confirm_action, {});
                              if (!rs) {
                                  this.selected_node = this.$refs.hidden_tree.getNodeByKey(old_value);
                                  this.list_loading = false;
                                  this.persistent = false;
                                  return;
                              }
                              this.persistent = false;
                          }
          
                          const sync_data = { ...{ [this.sync_selected_key]: v }, ...this.sync_data };
                          const r = await this.$store.dispatch(this.sync_action, sync_data);
          
                          if (r && r.r) {
                              this.hide_dialog();
                          } else {
                              this.selected_node = this.$refs.hidden_tree.getNodeByKey(old_value);
                          }
                      } catch ($e) {
                          this.list_loading = false;
                          throw $e;
                      }
          
                      this.list_loading = false;
                  },
                  async node_ticked (v) {
                      try {
                          const old_ticked_nodes = this.$refs.hidden_tree.getTickedNodes();
                          this.list_loading = true;
                          this.ticked_nodes = this.$refs.hidden_tree.getTickedNodes();
          
                          if (this.confirm_action) {
                              this.persistent = true;
                              const rs = await this.$store.dispatch(this.confirm_action, {});
                              if (!rs) {
                                  this.ticked_nodes = old_ticked_nodes;
                                  this.list_loading = false;
                                  this.persistent = false;
                                  return;
                              }
                              this.persistent = false;
                          }
          
                          const sync_data = { ...{ [this.sync_selected_key]: v }, ...this.sync_data };
                          const r = await this.$store.dispatch(this.sync_action, sync_data);
          
                          if (r && r.r) {
                              this.ticked_nodes = this.$refs.hidden_tree.getTickedNodes();
                          } else {
                              this.ticked_nodes = old_ticked_nodes;
                          }
                      } catch ($e) {
                          this.list_loading = false;
                          throw $e;
                      }
          
                      this.list_loading = false;
                  },
                  async load () {
                      if (this.section_open) {
                          this.loading = true;
          
                          let value_action_promise = true;
                          if (this.value_action) {
                              value_action_promise = this.$store.dispatch(this.value_action, this.value_action_data);
                          }
          
                          let tree_action_promise = true;
                          if (this.tree_action) {
                              tree_action_promise = this.$store.dispatch(this.tree_action, {});
                          }
          
                          await Promise.all([
                              value_action_promise,
                              tree_action_promise
                          ]);
          
                          this.loading = false;
                          this.key = Math.random();
                          this.hidden_key = Math.random();
                          if (this.multi_select) {
                              this.ticked_nodes = this.$refs.hidden_tree.getTickedNodes();
                          } else {
                              this.selected_node = this.$refs.hidden_tree.getNodeByKey(this._value);
                          }
                      }
                  },
                  // get_ticked_nodes (v) {
                  //     const ticked_nodes = [];
                  //     v.forEach((id) => {
                  //         ticked_nodes.push(this.$refs.hidden_tree.getNodeByKey(id));
                  //     });
                  //     return ticked_nodes;
                  // },
                  on_focus () {
                      // this.show_dialog();
                  },
                  on_keydown (e) {
                      // 37 left 38 up, 39 right 40 down
                      const $el = this.$refs.tree.$el;
                      const $focus = $el.querySelector('.q-tree__node-header:focus');
                      const all = [].slice.call($el.querySelectorAll('.q-tree__node-header'));
          
                      let index = 0;
                      const key = e.which;
          
                      // down and up
                      if (key === 40 || key === 38) {
                          if (key === 40) {
                              if ($focus) {
                                  const i = all.indexOf($focus);
                                  if (i !== all.length - 1) {
                                      index = i + 1;
                                  }
                              }
                          } else if (key === 38) {
                              if ($focus) {
                                  const i = all.indexOf($focus);
                                  if (i !== 0) {
                                      index = i - 1;
                                  } else {
                                      index = all.length - 1;
                                  }
                              } else {
                                  index = all.length - 1;
                              }
                          }
                          all[index].focus();
                      }
                  },
                  on_input () {
                      this.show_dialog();
                  },
                  on_filter_keydown (e) {
                      this.on_keydown(e);
                  },
                  on_field_click (e) {
                      if (this.dialog) {
                          this.hide_dialog();
                      } else {
                          this.show_dialog();
                      }
                      this.show_dialog();
                  },
                  on_field_keydown (e) {
                      this.$emit('keydown', e);
          
                      // down
                      if ((e.keyCode === 40 || e.keyCode === 13) && !this.dialog) {
                          this.show_dialog();
                          setTimeout(() => {
                              this.$refs.filter.focus();
                          }, 100);
                      }
          
                      return true;
                  },
                  remove_chip (node_id) {
                      if (this.multi_select) {
                          const ticked = [...this.ticked];
                          const index = ticked.indexOf(node_id);
                          ticked.splice(index, 1);
                          this.node_ticked(ticked);
                      } else {
                          this.node_selected(null);
                      }
                  },
                  show_dialog () {
                      if (this.dialog) { return; }
                      this.dialog = true;
                      setTimeout(() => {
                          // this.$refs.filter.focus();
                      }, 100);
                  }
              }
          };
          
          </script>
          
          <style lang="stylus">
          
          .v_tree_select{
          
          }
          .v_tree_select .q-tree{
              width: 100%;
          }
          .v_tree_select_field_wrap{
              width: 100%;
          }
          .v_tree_select_dialog{
              // background-color: white;
          }
          .v_tree_select_dialog .q-dialog__backdrop{
              // display: none;
          }
          .v_tree_select_dialog .q-dialog__inner > div{
              box-shadow: none;
          }
          .v_tree_select_dialog .v_tree_select_popup_wrap{
               background-color: white;
               flex-wrap: nowrap;
          }
          .v_tree_select_dialog.q-menu .v_tree_select_dialog_close{
              display: none;
          }
          .v_tree_select_dialog .q-tree {
              margin: 16px;
          }
          .v_tree_select_filter_input{
              margin-bottom: 0 ;
          }
          </style>
          
          

          Screenshot 2020-08-20 at 16.56.58.png

          1 Reply Last reply Reply Quote 1
          • CWoodman
            CWoodman last edited by

            Thanks. Maybe we just need to hack our way through this. Anyway, my approach seems to be working - just had to detect all the lost focus and click elsewhere events to hide the persistent tree.

            qyloxe 1 Reply Last reply Reply Quote 0
            • qyloxe
              qyloxe @CWoodman last edited by

              @CWoodman said in Q-tree within Q-menu:

              (…) just had to detect all the lost focus and click elsewhere events to hide the persistent tree.

              does your solution works on the mobile, too?

              It seems, as there’s a lot of code in both solutions, it should be simple imho.
              It could mean, that there is a fundamental problem in Quasar abstractions if such use cases are hard to grasp 🙂

              1 Reply Last reply Reply Quote 0
              • CWoodman
                CWoodman last edited by

                Yes it works on my phone. I agree it should be simpler!

                1 Reply Last reply Reply Quote 0
                • metalsadman
                  metalsadman last edited by metalsadman

                  It would help if you show some working code. Obviously not all use case are covered since it varies for each. the use case isn’t simple either.

                  If you think there’s something missing on the component or its api that would help you, then it would help to make a feature request about it so that it can be improved if said feature is deemed logical to be added.

                  CWoodman qyloxe 2 Replies Last reply Reply Quote 1
                  • CWoodman
                    CWoodman @metalsadman last edited by

                    @metalsadman There’s a codepen of an earlier version at https://codepen.io/crawfordw/pen/abdrNRN but it doesn’t work correctly there for some reason.

                    Anyway, what I needed was a Select component that would show a tree-view in the drop-down, and support autofill with items from any level of the tree.

                    I don’t know if this is a general enough need to warrant building it into Quasar, but I intend to use it in many places in my app. I have it working well now, though needed to use a few kludgy techniques to get there.

                    If you’re interested, I can provide the complete code here.

                    1 Reply Last reply Reply Quote 0
                    • qyloxe
                      qyloxe @metalsadman last edited by

                      @metalsadman yeah, you’re obviously right: it needs specification. It is not a problem I need to solve just now, but it is an interesting case, and I always assumed that at some point of its way, Quasar will need an abstraction of “compound components”.

                      We have something similar already - as in q-page or q-card, which are examples of compound components. Now, in this use case, OP encountered a need for overlayed, sibling information exchange, shared state component. His implementation in the context of framework provided abstractions, ended up complicated and not obvious.

                      Maybe it’s about time to create a new abstraction in Quasar ie q-compound or q-overlayed which allows to build something like that?

                      I think it is an excellent example, where app extensions are too heavy. If you wanted to build such light user-space and app-specific components as an AE, then this AE would be hard to maintain, debug, big and error prone.

                      The needed q-compound should:

                      • take care of events propagated and emitted
                      • work in overlays / popups
                      • support “decorator” interfaces (animations, transitions for example)
                      • … and more?

                      The actual code which OP used, is not elegant. I consider, that when framework forces you to write “not elegant” code, it is a strong sign of lacking abstractions in said framework. And Quasar is highly elegant. This is just a new use case which needs new abstraction.

                      Honestly just a bunch of thoughts, hoping this would start a fruitful discussion.

                      CWoodman 1 Reply Last reply Reply Quote 0
                      • CWoodman
                        CWoodman @qyloxe last edited by

                        @qyloxe Interesting discussion. Probably out of my depth on this. I’m just hacking up something that meets my UX needs. The original title of this post re: ‘tree within a menu’ doesn’t really reflect my starting point… Maybe I should have titled it ‘Challenges making a Select component that supports tree-structured options list’. If there was a native Quasar Select component that did that, I wouldn’t need to add a menu and a button and a bunch of styles and methods to make the whole mess look and behave like a regular Select box.

                        1 Reply Last reply Reply Quote 0
                        • T
                          turigeza last edited by

                          I personally feel this component should be part of the framework.

                          It is very hard to avoid tree structures. Everybody wants categories and folders I find. So we will all start building our own tree select eventually and not very elegantly (or at least most of us).

                          dobbel 1 Reply Last reply Reply Quote 0
                          • CWoodman
                            CWoodman last edited by

                            I just posted a feature request for this…
                            https://forum.quasar-framework.org/topic/6639/requested-feature-select-with-tree-structured-options

                            1 Reply Last reply Reply Quote 2
                            • CWoodman
                              CWoodman last edited by

                              Just added my code and description of how it works to the feature request post above

                              1 Reply Last reply Reply Quote 0
                              • dobbel
                                dobbel @turigeza last edited by dobbel

                                @turigeza you could see this as a tree select: its’a tree and you can select.

                                https://quasar.dev/vue-components/tree#Example--Selectable-nodes

                                Combine it with the filter example and you have the functionality of a q-select with a tree.

                                CWoodman 1 Reply Last reply Reply Quote 0
                                • CWoodman
                                  CWoodman @dobbel last edited by CWoodman

                                  @dobbel Yes, that’s what I did.
                                  See my code at https://forum.quasar-framework.org/topic/6639/requested-feature-select-with-tree-structured-options

                                  1 Reply Last reply Reply Quote 0
                                  • First post
                                    Last post