Property sync'ed with parent data is seen as unchanged by a child component, unless put in the timeout handler


#1

Hello everyone!

On vuex in my code. I started building the modal component with the following article in my mind: https://markus.oberlehner.net/blog/building-a-modal-dialog-with-vue-and-vuex/. The centralized visibility management is left untouched though I think this should be refactored somehow.

There are two components: Modal (child) and DepartmentEditModal (parent). DepartmentEditModal adds the department CREATE/EDIT handling. The modal should be closed when parent’s CREATE/EDIT promise is resolved. While the promise is still pending user shouldn’t be able to close the modal.

In the DepartmentEditModal’s SubmitChanges() method, in the promise success handler, setting the isPromisePending = false doesn’t affect the Modal’s closingBlocked sync’ed property unless the closeModal() call is put inside the timeout(cb, 0) callback.

Can you explain why does this happen? Thank you!

// DepartmentEditModal.vue
submitChanges() {
  this.isPromisePending = true;
  DepartmentsHttpService.saveDepartment(this.model).then(
    (response) => { 
      this.isPromisePending = false;
      /* this.isPromisePending is still true when 'this.closeModal();' is called immediately */
      /* this.isPromisePending is false when called like this */
      setTimeout(() => {
        this.closeModal();
      });
    },
    (error) => { 
      this.isPromisePending = false;
      this.errors = error.data.errors;
    }
  );
},
closeModal() {
  this.$refs.modal.closeModal();
}

// Modal.vue
closeModal() {
  /* here the 'closingBlocked' is a property synchronized with the parent's isPromisePending */
  if(!this.closingBlocked) {
    this.hideModal();
    this.$emit('on--modal-closed');
  }
},

Here’s the DepartmentEditModal component (minus form fields & styling):

<template>
  <modal ref="modal" :closing-blocked.sync="isPromisePending" @on--modal-closed="onModalClosed">
    <!-- inputs... -->
    <div class="modal-actions clearfix">
      <div class="float-right">
        <button class="button" :disabled="isPromisePending" @click="submitChanges()">Save</button>
        <button class="button secondary" :disabled="isPromisePending" @click="closeModal()">Close</button>
      </div>
    </div>
  </modal>
</template>

<script>

  import Modal from '../ui/Modal';
  import { mapState } from 'vuex';

  import DepartmentsHttpService from '../../services/DepartmentsHttpService';

  export default {
    name: 'DepartmentEditModal',
    components: { Modal },
    props: {
      model: { type: Object, required: true }
    },
    data() {
      return {
        isPromisePending: false,
        errors: []
      };
    },
    methods: {
      submitChanges() {
        this.isPromisePending = true;
        DepartmentsHttpService.saveDepartment(this.model).then(
          (response) => { 
            this.isPromisePending = false;
            this.$emit('on--changes-applied');
            setTimeout(() => {
              this.closeModal();
            });
          },
          (error) => { 
            this.isPromisePending = false;
            this.errors = error.data.errors;
          }
        );
      },
      closeModal() {
        this.$refs.modal.closeModal();
      },
      onModalClosed() {
        this.errors = [];
      }
    }
  }

</script>

Here’s the Modal component (minus styling):

<template>
  <div class="ui-modal">
    <div class="ui-modal__overlay" v-if="visible" @click="closeOnOverlayClicked && closeModal()">
      <div class="ui-modal__container" :class="cssContainerClasses" @click.stop>
        <slot></slot>
      </div>
    </div>
  </div>
</template>

<script>

  import { mapState, mapActions } from 'vuex';

  export default {
    name: 'Modal',
    props: {
      closeOnOverlayClicked: { type: Boolean, default: true },
      closeOnEscPressed: { type: Boolean, default: true },
      closingBlocked: { type: Boolean, default: false },
      cssContainerClasses: { type: String },
    },
    mounted() {
      if(this.closeOnEscPressed) {
        this.registerEscPressedHandler();
      }
    },
    computed: {
      ...mapState('modal', { visible: state => state.modalVisible, }),
    },
    methods: {
      ...mapActions('modal', ['hideModal',]),
      closeModal() {
        if(!this.closingBlocked) {
          this.hideModal();
          this.$emit('on--modal-closed');
        }
      },
      registerEscPressedHandler() {
        const closeOnEscHandler = (e) => { if(e.keyCode === 27) this.closeModal(); };
        document.addEventListener('keydown', closeOnEscHandler);
        this.$once('hook:destroyed', () => { document.removeEventListener('keydown', closeOnEscHandler); });        
      }
    },
  }

</script>