Source: management-system/src/frontend/components/processes/processForm/ProcessesDataProviderMixin.vue

<script>
import uuid from 'uuid';

import { asyncMap } from '@/shared-frontend-backend/helpers/javascriptHelpers.js';

import ExecutionQueue from '@/shared-frontend-backend/helpers/execution-queue.js';

/**
 * @module components
 */
/**
 * @memberof module:components
 * @module processes
 */
/**
 * This mixin provides a way for nested components inside the component using it to use and update the shared processesData component in a reactive way
 *
 * @memberof module:components.module:processes
 * @module Vue:ProcessesDataProviderMixin
 *
 */
export default {
  data() {
    return {
      /**
       * Provide the processesData inside an object so injecting components can react to changes
       */
      reactiveData: { processesData: [] },
      /**
       * The callbacks other components can provide to react to new entries being added
       */
      initCallbacks: [],
      /**
       * The callbacks other components can provide to react to changes on an entry
       */
      changeCallbacks: [],
      /**
       * Used to make sure that the processesData is only updated by one function at a time
       */
      executionQueue: new ExecutionQueue(),
    };
  },
  // this allows nested components to use and apply changes to the base data
  provide() {
    return {
      updateData: this.updateProcessesData,
      reactiveData: this.reactiveData,
      onInit: this.registerInitCallback,
      offInit: this.unregisterInitCallback,
      onChange: this.registerChangeCallback,
      offChange: this.unregisterChangeCallback,
    };
  },
  methods: {
    // creates a new id that can be used to identify a specific element inside processesData through changes
    createProcessDataId() {
      return uuid.v4();
    },
    /**
     * Will set processesData making sure that every entry contains an id that makes it identifyable
     *
     * @param {Array} entries the entries the new processesData should be initialized with
     */
    async setProcessesData(entries) {
      await this.executionQueue.enqueue(async () => {
        if (Array.isArray(entries)) {
          let newlyAdded = [];

          this.reactiveData.processesData = await asyncMap(entries, async (entry, index) => {
            // do nothing if this entry was already initialized
            if (entry.formEntryId) {
              return entry;
            } else {
              // consider the data of the added entry as changes so change handlers are able to be reused here
              let initChanges = { ...entry };

              // execute all registered init handlers
              for (const cb of this.initCallbacks) {
                initChanges = { ...initChanges, ...(await cb(entry)) };
              }

              newlyAdded.push({ index, initChanges });

              // add the entry with an identifying id
              return { formEntryId: this.createProcessDataId() };
            }
          });

          // some components are only rendered if there is data so wait for them to be created and add their change handlers and then trigger them
          if (newlyAdded.length) {
            this.$nextTick(() => {
              newlyAdded.forEach(({ index, initChanges }) => {
                this.updateProcessesData(index, initChanges);
              });
            });
          }
        } else {
          this.reactiveData.processesData = [];
        }
      });
    },
    /**
     * Will trigger change handlers with the current change and then recursively for all resulting changes until there is no new change
     *
     * BEWARE: this can lead to infinite cycles if some components have a cyclical change dependency
     *
     * @param {Object} currentData the current state of the entry
     * @param {Object} currentChanges the changes to apply
     *
     * @returns {Object} the result of all the changes applied to the existing entry
     *
     */
    async handleChanges(currentData, currentChanges) {
      let newChanges;
      do {
        newChanges = {};

        // execute all change handlers and save resulting changes
        for (const cb of this.changeCallbacks) {
          newChanges = { ...newChanges, ...(await cb(currentData, currentChanges)) };
        }

        // calculate the changed entry
        currentData = { ...currentData, ...currentChanges };

        // repeat for the resulting changes until there is no new change
        currentChanges = newChanges;
      } while (Object.keys(newChanges).length);

      return currentData;
    },
    /**
     * Will change the entry at the given index in a way that vue can react to
     *
     *
     * @param {Number} index the index of the element inside the processesData array
     * @param {Object} changes the changes to make
     */
    async updateProcessesData(index, changes) {
      await this.executionQueue.enqueue(async () => {
        const entry = this.reactiveData.processesData[index];

        // overwrite entry in the current processesData with the changes
        const newEntry = await this.handleChanges(entry, changes);

        // overwrite the old processesData to force vue to react to the changes
        this.reactiveData.processesData = [
          ...this.reactiveData.processesData.slice(0, index),
          newEntry,
          ...this.reactiveData.processesData.slice(index + 1),
        ];
      });
    },
    registerInitCallback(callback) {
      this.initCallbacks.push(callback);
    },
    unregisterInitCallback(callback) {
      this.initCallbacks = this.initCallbacks.filter((cb) => cb !== callback);
    },
    registerChangeCallback(callback) {
      this.changeCallbacks.push(callback);
    },
    unregisterChangeCallback(callback) {
      this.changeCallbacks = this.changeCallbacks.filter((cb) => cb !== callback);
    },
  },
};
</script>