[Solved] Customized component issue: For recursive components, make sure to provide the "name" option



  • Dear Quasar team,

    Could you please have a look of my issue?
    I reproduce this issue in the link https://codesandbox.io/s/quirky-williamson-ucdw9
    and the demo can be seen here https://ucdw9.sse.codesandbox.io/pur-ords-q01

    I customized a component called select-option which is used as search condition.
    I created three vue files under folder components.
    The first one is “pur-ord-listq01.vue” which is a page to search purchase order, here a define two search condition which is implemented by component select-option. (from second file select-option.vue).

    If user click the search button, it will popup a search dialog (the third file search-help.vue). In the above of this dialog, I wanna add search condition (select-option) again which is used to restrict the search result. But it throws error in the Console:
    “[Vue warn]: Unknown custom element: <select-option> - did you register the component correctly? For recursive components, make sure to provide the “name” option.”

    It’s kind of recursive call because in “pur-ord-listq01.vue”, it calls “select-option.vue”, then it goes to “search-help.vue” and in this file it calls “select-option.vue” again.
    I have no idea how to fix it, thanks a lot for your help!



  • @Stanley It looks like the error is because SelectOption includes SearchHelp, which includes SelectOption, which includes SearchHelp…

    Can you instead move this part:

        <search-help
          :open="shlpOpen"
          :fieldName="fieldName"
          @callBack="onShlpCallBack"
        />
    

    into pur-ord-listq01.vue, and emit an event from select-option to open it? That should solve the problem.



  • @beets Thanks for your proposal.
    I folked the code to new link https://codesandbox.io/s/silly-leaf-rgotu
    and the demo link is https://rgotu.sse.codesandbox.io/pur-ords-q01
    Now you can see the select option (poType) is displayed in the search help dialog.

    However, if I wanna popup another search help dialog for poType, how can I do it?
    It seems the recursive call can’t be avoided. (I did it on line 41 in file “search-help.vue”)
    And again, the same error occurs in the Console:
    [Vue warn]: Unknown custom element: <search-help> - did you register the component correctly? For recursive components, make sure to provide the “name” option.

    Thanks for your help!



  • @Stanley Okay, I misunderstood and throught you would never need to open the dialog recursively. I forked your original codepen to make it work: https://codesandbox.io/s/funny-curran-zm0yw

    What I did was:

    • Create a new boot file, modal.js
    • Added it to the boot section in quasar.conf.js, and also added the Dialog Plugin
    • The boot file adds a method to Vue.prototype called $showModal which can be called from anywhere
    • I had to change search-help.vue a bit, to follow the custom dialog template: https://quasar.dev/quasar-plugins/dialog#Invoking-custom-component
    • Now, seach-option.vue calls the dialog like this: this.$showModal({ fieldname: this.fieldname }).onOk(this.onShlpCallBack)
    • The dialog can return a payload if needed, I just returned a simple object for demonstration.


  • @beets Wow, it’s fantastic! I learned a lot.

    Actually, before seeing your reply, I also solved how to deal with recursive call.
    See this link: https://codesandbox.io/s/eloquent-frost-8vquu

    But I don’t know what’s the disadvantage of my way, could you please evaluate it?
    1). From your step 1 to 3, I register my global component in file “/router/index.js”. Is this ok?
    2). For step 4, it’s great I like this way because it’s more like of quasar way.
    Just one question how it exit recursive call? As you can see my file “search-help.vue” line 43, I add the “v-if” condition to exit the recursive call.

    Thank you so much!



  • @Stanley

    From your step 1 to 3, I register my global component in file “/router/index.js”. Is this ok?

    Normally you would use a boot file for registering global components:

    // src/boot/components.js 
    // make sure to add 'components' to quasar.conf.js boot section
    import SelectOption from '../components/select-option.vue'
    import SearchHelp from '../components/search-help.vue'
    
    export default async ({ app, router, store, Vue }) => {
      Vue.component('SelectOption', SelectOption)
      Vue.component('SearchHelp', SearchHelp)
    }
    

    It’s not much different than placing it in the routes.js file, except more clean as components don’t really belong in the router file.

    Just one question how it exit recursive call? As you can see my file “search-help.vue” line 43, I add the “v-if” condition to exit the recursive call.

    I’m not quite sure what you’re asking here. Does either your example or mine behave correctly? Or are you trying to make it so when you hit cancel all dialogs close?



  • @beets Both are working correctly for the recursive call and I don’t wanna hit cancel to close all dialogs.
    Regarding my code (using QDialog component), I checked the vue doc https://vuejs.org/v2/guide/components-edge-cases.html#Recursive-Components, it has to add “v-if” to avoid “max stack size exceeded” error. That’s why add line 44 in file “search-help.vue”.
    So for your way (using QDialog plugin), however I didn’t find such code to avoid infinite recursive call. Maybe for this way, it’s no need to worry about it.

    Regards



  • @beets Now I encounter another issue of integrating i18n, could you please have a look?
    For below code, it can’t work and the error message is "Error in render: “TypeError: Cannot read property ‘t’ of undefined”.
    However, it works when using QDialog component.
    Thanks a lot!

    <template>
      <q-dialog ref="dialog" @show="onShow" @hide="onHide">
        <q-layout view="Lhh lpR fff" container class="bg-white">
          <q-header class="bg-primary">
            <q-toolbar>
              <q-toolbar-title>
                {{ $i18n.t('sa.searchHelpDialog') }}
              </q-toolbar-title>
    


  • @Stanley I think it has something to do with the parent property here:

    export default async ({ app, router, store, Vue }) => {
      Vue.prototype.$showModal = (props) => {
        return Dialog.create({
          ...props,
          component: SearchHelp,
          parent: app.$root
        })
      }
    }
    

    Try changing app.$root to router.app.$root and see if it works, that’s what I have in my codebase.



  • @Stanley said in Customized component issue: For recursive components, make sure to provide the "name" option:

    Regarding my code (using QDialog component), I checked the vue doc https://vuejs.org/v2/guide/components-edge-cases.html#Recursive-Components, it has to add “v-if” to avoid “max stack size exceeded” error. That’s why add line 44 in file “search-help.vue”.
    So for your way (using QDialog plugin), however I didn’t find such code to avoid infinite recursive call. Maybe for this way, it’s no need to worry about it.

    Okay, yes I see what you mean. You’re correct, with the dialog plugin there’s no need for a recursive check since the dialog gets called from outside any component.



  • @beets Super!



  • @beets Thank you so much! Without your help, I can’t do it.



  • @Stanley No problem, glad I can help. So did router.app.$root work? I’m not sure exactly why it doesn’t work with app.$root, maybe there’s something I’m missing as well.



  • @beets Yes, it works with router.app.$root. However, as you know, there are two boot files on my side, axios and i18n and axios works with app.$root.
    Is it because the different way of definition? (see below link)
    https://quasar.dev/quasar-cli/boot-files#Axios
    https://quasar.dev/quasar-cli/boot-files#vue-i18n



  • @beets

    Create a new boot file, modal.js

    I was looking at your custom dialog boot file. And I was wondering why this is not working: Instead of assigning the dialog to Vue.prototype.$showModal I tried to assign it to showModel. I did that so I could use import showModel in other files.

    boot/model.js

    import {Dialog} from 'quasar'
    import SearchHelp from 'components/search-help'
    
    let showModel = null
    
    export default async ({app, router, store, Vue}) => {
      showModel = (props) => {
        return Dialog.create({
          ...props,
          component: SearchHelp,
          parent: app.$root
        })
      }
    }
    
    export { showModel }
    
    

    Some other file ( like search-help.vue ) :

    import showModal from "src/boot/modal.js"
    ...
    showModal({ fieldname: this.fieldname }).onOk(this.onShlpCallBack)
    


  • @dobbel It might just be this line:

    import showModal from "src/boot/modal.js"
    

    change it to:

    import {showModal} from "src/boot/modal.js"
    

    Edit: that would be because the boot function is the default export, while showModal is a named export.



  • @beets

    correct I needed to change the import like you suggested.

    Also I needed to change the model.js. I had to introduce a temp variable instead of showModel inside the default export .

    import {Dialog} from 'quasar'
    import SearchHelp from 'components/search-help'
    
    let showModel = null
    
    export default async ({app, router, store, Vue}) => {
      let temp = (props) => {
        return Dialog.create({
          ...props,
          component: SearchHelp,
          parent: app.$root
        })
      }
      showModel= temp
    }
    
    export { showModel }
    

    Thanks for the quick response!



  • @dobbel It seems it has to import “model.js” every file if you want to use it.
    If it is, I would prefer the previous way because it is a “real” global component.
    Just like axios, i18n, after define them in boot file, I can call it everywhere without importing anything.
    How do you think?



  • @Stanley

    That’s correct you’ll have to import it in every file. But generally globals are considered a bad practice.



  • @Stanley @dobbel Here’s another snippet I use when I want to be able to show different modals:

    import Error from 'components/modals/error'
    import Print from 'components/modals/print'
    import CategoryFilter from 'components/modals/category-filter'
    import Debug from 'components/modals/debug'
    
    const modals = {
      'error': Error,
      'print': Print,
      'category-filter': CategoryFilter,
      // 'debug': () => Debug, // Edited to fix this line below, was bad copy / paste
      'debug': Debug,
    }
    
    export default async ({ app, router, store, Vue }) => {
      Vue.prototype.$showModal = (type, props) => {
        if(!modals.hasOwnProperty(type)) {
          throw new Error('invalid modal')
        }
        return Dialog.create({
          ...props,
          component: modals[type],
          parent: app.$root
        })
      }
    }
    
    

    Then I can use it like $showModal('error', { ...props }).

    Regarding global vs importing, I attach quite a lot to vue prototype, things like $api, $logger, $ui (which is where my showModal method lives, among other things.) All of that is done via an init boot file, so there isn’t the question of where all of these objects came from (one of the arguments against global usually.)

    If you’re using SSR, there are issues with both methods here as they’re written, which you can fix it with the methods described by this awesome article: https://dev.to/quasar/quasar-ssr-using-cookies-with-other-libs-services-4nkl . With this method, you do end up calling it via $showModal instead of an import. If you don’t use SSR, either method is fine really, I just prefer the convenience of not having to import it.


Log in to reply