vuejs / vueify

Browserify transform for single-file Vue components
MIT License
1.17k stars 153 forks source link

Is this the right tool? #66

Closed curtisblackwell closed 8 years ago

curtisblackwell commented 8 years ago

I'm working on a front-end framework and have built a handful of Vue components into it. I decided I want to split the components out separately and use npm to install them as necessary.

I've got a simple component working, but I'm having difficulty with another, slightly more complex component. The second one is an accordion and uses child components (though I'm guessing not correctly, which may be my problem). I'm sure I could get it to work by installing each piece separately via npm, but that seems silly. I want to be able to keep them all in a single repo and to use npm install my-accordion.

If you could take a look at my code and let me know if I'm doing something wrong and/or if I'm wasting my time trying to get vueify working for this, I'd appreciate it.

The markup looks something like this:

<accordions>
  <accordion>
    <accordion-tab>One</accordion-tab>
    <accordion-panel><p>Lorem ipsum</p></accordion-panel>
  </accordion>

  <accordion>
    <accordion-tab>Two</accordion-tab>
    <accordion-panel><p>Dolor sit amet</p></accordion-panel>
  </accordion>
</accordions>

I've got four separate .vue files for my accordion component:

accordions.vue

<template>
  <div class="accordions" role="tablist">
    <slot></slot>
  </div>
</template>

<script>
  export default {
    props: {
      // This gets used for `:transition` in `accordion-panel.vue`.
      effect: String,
      // Set to `true` to allow only one accordion to be open at a time.
      single: Boolean,
    },
  }
</script>

accordion.vue

<template>
  <!-- Wrap in a div to prevent fragmentation. -->
  <div class="accordion">
    <slot></slot>
  </div>
</template>

<script>
  export default {
    props: {
      // This shows/hides the accordion panel.
      // Used in `accordion-tab.vue` and `accordion-panel.vue`.
      active: Boolean,
    },

    computed: {
      // Set the index by looping through all children of the parent (this and its siblings).
      index() {
        for (var i in this.$parent.$children) {
          if (this.$parent.$children[i].$el === this.$el) {
            return i;
          }
        }
      },
    },
  }
</script>

accordion-tab.vue

<template>
  <button
    class          = "accordion-tab"
    :class         = "{ active: this.$parent.active }"
    :id            = "tabId"
    role           = "tab"
    :aria-controls = "tabPanelId"
    :aria-selected = "active.toString()"
    tabindex       = "0"
    @click         = "toggleAccordion"
  >
    <slot></slot>
  </button>
</template>

<script>
  export default {
    props: {
      active:       Boolean,
      ariaControls: String,
      id:           String,
    },

    computed: {
      tabId() {
        // Allow the dev to override the `id`.
        if (typeof this.id !== 'undefined') {
          return this.id;
        }

        return [
          'accordion-group',
          this.$parent.$parent._uid,
          'tab',
          this.$parent.index
        ].join('-');
      },

      tabPanelId() {
        // Allow the dev to override the `aria-controls`.
        if (typeof this.ariaControls !== 'undefined') {
          return this.ariaControls;
        }

        return [
          'accordion-group',
          this.$parent.$parent._uid,
          'panel',
          this.$parent.index
        ].join('-');
      },
    },

    methods: {
      /**
       * Toggle the accordion panel's visibility.
       *
       * @author Curtis Blackwell
       * @return {void}
       */
      toggleAccordion() {
        // If the accordion group is set to single mode, close any open accordions.
        if (this.$parent.$parent.single) {
          for (var i in this.$parent.$parent.$children) {
            this.$parent.$parent.$children[i].active = false;
          }
        }

        this.$parent.active = !this.$parent.active;
      }
    }
  }
</script>

accordion-panel.vue

<template>
  <div
    class            = "accordion-panel"
    :id              = "tabPanelId"
    role             = "tabpanel"
    :aria-labelledby = "tabId"
    :aria-hidden     = "!$parent.active.toString()"
    v-show           = "show"
    :transition      = "transition"
  >
    <slot></slot>
  </div>
</template>

<script>
  export default {
    computed: {
      show() {
        return this.$parent.active;
      },

      tabId() {
        // Allow the dev to override the `id`.
        if (typeof this.id !== 'undefined') {
          return this.id;
        }

        return [
          'accordion-group',
          this.$parent.$parent._uid,
          'tab',
          this.$parent.index
        ].join('-');
      },

      tabPanelId() {
        // Allow the dev to override the `aria-controls`.
        if (typeof this.ariaControls !== 'undefined') {
          return this.ariaControls;
        }

        return [
          'accordion-group',
          this.$parent.$parent._uid,
          'panel',
          this.$parent.index
        ].join('-');
      },

      transtion() {
        return this.$parent.$parent.effect;
      },
    },
  }
</script>
curtisblackwell commented 8 years ago

P.S. I have tried figuring out local registration, but I can't run Vue.extend() without require()-ing Vue, which I'm pretty sure I'm not supposed to do in the .vue file.

yyx990803 commented 8 years ago

You can have an index.js that simply imports and exports these:

exports.Accordion = require('./accordian.vue')
exports.Accordions = require('./accordians.vue')
// ...

Then in your app code (with ES2015 modules):

import { Accordion, Accordions, ... } from 'my-accordion'

export default {
  components: {
    Accordion,
    Accordions,
    ...
  }
}

In local registration you don't need to call Vue.extend().

curtisblackwell commented 8 years ago

Thanks Evan, that works!

In case anyone else has this issue, after setting up the module as Evan above, you use the components after installing with npm like this:

components: {
  Accordions:     Accordion.Accordions,
  Accordion:      Accordion.Accordion,
  AccordionTab:   Accordion.AccordionTab,
  AccordionPanel: Accordion.AccordionPanel
}
curtisblackwell commented 8 years ago

I had re-opened this thinking there was something wrong, but I just had a misunderstanding of how child components work. I was trying to use them outside of the parents' templates, which was causing issues.

As my components are written now, the above works. I'll reevaluate whether I should use child components at a later date.