0

I am struggling to get my head around the process of encapsulating reusable elements within a component but still allowing parental interference of the data within it.

In my current case I have a form stepper/wizard and within that, I have child form-tab components. The navigation buttons live in the form-wizard component.

I have a top-level component in my app that hosts a form-wizard and I want to delegate the task of validating steps of the form to the host/parent component. I do not want a navigation button enabled until the host/parent is satisfied that the current and previous steps are valid. I've put together an example:

Vue.component("form-tab", {
    template: '<div v-show="isActive"><slot></slot></div>',
    props: {
        title: '',
        selected: {
            default: false
        }
    },
    data() {
        return {
            isActive: false,
            isValid: false
        }
    },
    created() {
        this.isActive = this.selected
    }
});

Vue.component('form-wizard', {
    template: '#form-wizard-template',
    props: ["form_data"],
    data() {
        return {
            activeStepIndex: 0,
            maxStep: 0,
            loading: false,
            tabs: []
        }
    },
    computed: {
        totalTabs() {
            return this.tabs.length;
        },
    },
    created() {
        this.tabs = this.$children;
    },
    mounted() {
    },
    methods: {
        progress() {

        },
        previousTab() {
            this.activeStepIndex--;
            this.tabs.forEach(tab => {
                tab.isActive = false;
            });
            this.tabs[this.activeStepIndex].isActive = true;

            setTimeout(() => {
                this.progress()
            });
        },
        nextTab() {
            this.scrollToTop();
            this.activeStepIndex++;
            this.tabs.forEach(tab => {
                tab.isActive = false;
            });
            this.tabs[this.activeStepIndex].isActive = true;

            setTimeout(() => {
                this.progress()
            });

            this.maxStep = this.activeStepIndex
        },
        jumpToTab(i) {
            // Only allow a jump to previous steps
            if(i <= this.maxStep  && this.maxStep < this.tabs.length - 1) {
                this.activeStepIndex = i;
                this.tabs.forEach(tab => {
                    tab.isActive = false;
                });
                this.tabs[i].isActive = true;

                setTimeout(() => {
                    this.progress()
                });
            }
        },
        scrollToTop() {
            var top = $(window).width() >= 576 ? this.$el.offsetTop - $('header').height() : this.$el.offsetTop;
            window.scrollTo(0, top - 50);
        },
        submitform() {
            this.$emit("submit", this.form_data);
        },
        validatetab() {         
          this.tabs[this.activeStepIndex].isValid = this.$emit("validatetab", this.activeStepIndex);
        }
    }
})

Vue.component("myform", {
    template: '#myform-template',
    data() {
        return {
        form_data: {
          name: 'Example'
        }
      }
    },
    methods: {
      submit(form_data) {
        alert('Form submitted');
      },
      validateTab(index) {
        return confirm('Validating Tab ' + index);
      }
    }
});


new Vue({
  el: "#app",
  data() {
        return{};
  }
})

https://jsfiddle.net/ProNotion/gnj8uzpa/

It seems to me that this.$emit is a one way road and no return value passes back to the child?

ProNotion
  • 3,662
  • 3
  • 21
  • 30
  • 1
    $emit is upwards - attrs & v-on downwards, alternative is a vue instance on prototype or as mixin as an eventbus or a state machine like vuex – Estradiaz Aug 30 '19 at 16:12

1 Answers1

0

This post: VueJs 2.0 emit event from grand child to his grand parent component has many ways to solve this kind of a problem. I would recommend using vuex for managing state and actions. Other option:

this.$parent.$emit("submit", {somekey: somevalue})
edu
  • 53
  • 7
  • Vuex looks interesting, I'm still not 100% sure how I could use it in this situation given that my tabs (and their state) are currently a data property on the `form-wizard` component as they are set as `this.$children` - how might that work with Vuex? What about when I have multiple instances of my `form-wizard` component (which I will)? – ProNotion Aug 31 '19 at 06:35
  • Whilst its not ideal I can remove this blocker temporarily I have found by doing this `isTabDataValid(index) { return this.$parent.isTabDataValid(index); }` I would very much like to understand how I can do this using something like Vuex to remove the dependency on the parent keeping the component more reusable. – ProNotion Aug 31 '19 at 07:08
  • 1
    In general, you don't want to emit values from nested child to top-level parent. It makes the code maintaining too much of a hassle. I'm not totally sure how you were planning to use 'wizard-component' but heres one solution: |parent-component |wizard-component |buttons-component |form-tab. **1**- parent-component and wizard-component BOTH communicate with vuex-state separately **2**- buttons & form-tab -components emits actions to wizard-component which then dispatches wanted action to store e.g.: `this.$store.commit('UPDATE_PARENT', {isClicked: true});` – edu Aug 31 '19 at 19:49
  • 1
    previous msg continues: `UPDATE_PARENT(state, obj) { if (obj.isClicked) { state.isTabDataValid = true } }` parent-component listens always state value changes and updates view the way you want it to update. – edu Aug 31 '19 at 19:50