Add item duplication button to array item

I have a custom ArrayList Renderer in vue 3. The Array Items have buttons to move and to remove the item. I tried to add a button to duplicate the item but got stuck.

My idea was to mimick the removeItems method but I got stuck finding out how I could extend/overwrite the mapDispatchToArrayControlProps const from the core/utils/renderers.

Any input would be greatly appreciated.

Hi @kimamil,

The mapDispatchToArrayControlProps is just a util to provide you with some convenience methods like removeItems from the get go. You don’t need to override it, but you can simply create your duplicateItem method using the same patterns.

What you need to do is:

  • Retrieve the dispatch method from JSON Forms const dispatch = inject('dispatch');
  • Dispatch an update action with your new array, e.g. dispatch(Actions.update('control.path', () => newArrayWithTheDuplicatedEntry));

Hi @sdirix thank you for the insight.

I first solved it with a custom event, emitted from the array-list-element (see the ‘@duplicate’ listener on the) component, using the ‘addItem()’ wrapper function for the update dispatch.

<template>
  <fieldset v-if="control.visible" :class="styles.arrayList.root">
    <label v-if="control.label" class="uk-form-label uk-text-primary">{{
      control.label
    }}</label>
    <ul
      v-if="!noData"
      :class="styles.arrayList.itemWrapper"
      uk-accordion="multiple: true"
    >
      <array-list-element
        v-for="(element, index) in control.data"
        :moveUp="moveUp(control.path, index)"
        :moveUpEnabled="index > 0"
        :moveDown="moveDown(control.path, index)"
        :moveDownEnabled="index < control.data.length - 1"
        @duplicate="duplicateItem(index)"
        :delete="removeItems(control.path, [index])"
        :label="childLabelForIndex(index)"
        :styles="styles"
        :key="`${control.path}-${index}`"
        :initiallyExpanded="uischema.options?.detail.initiallyExpanded"
      >
        <dispatch-renderer
          :schema="control.schema"
          :uischema="childUiSchema"
          :path="composePaths(control.path, `${index}`)"
          :enabled="control.enabled"
          :renderers="control.renderers"
          :cells="control.cells"
        />
      </array-list-element>
    </ul>
    <div v-else :class="styles.arrayList.noData">No Items</div>
    <a
      href="#"
      class="uk-button uk-button-small uk-button-secondary"
      @click.prevent="addButtonClick"
      >Add an Item</a
    >
    <hr />
  </fieldset>
</template>

<script lang="ts">
import {
  composePaths,
  createDefaultValue,
  ControlElement,
  moveDown,
  moveUp,
} from "@jsonforms/core";
import { defineComponent } from "vue";
import {
  DispatchRenderer,
  rendererProps,
  useJsonFormsArrayControl,
  RendererProps,
} from "@jsonforms/vue";
import { useVanillaArrayControl } from "@jsonforms/vue-vanilla";
import ArrayListElement from "./ArrayListElement.vue";

const controlRenderer = defineComponent({
  name: "array-list-renderer",
  components: {
    ArrayListElement,
    DispatchRenderer,
  },
  props: {
    ...rendererProps<ControlElement>(),
  },
  setup(props: RendererProps<ControlElement>) {
    return useVanillaArrayControl(useJsonFormsArrayControl(props));
  },
  computed: {
    noData(): boolean {
      return !this.control.data || this.control.data.length === 0;
    },
  },
  methods: {
    composePaths,
    createDefaultValue,
    addButtonClick() {
      this.addItem(
        this.control.path,
        createDefaultValue(this.control.schema)
      )();
    },
    moveDown,
    moveUp,
    duplicateItem(index: number) {
      this.addItem(this.control.path, { ...this.control.data[index] })();
    },
  },
});

export default controlRenderer;
</script>

While this works, I am eager to solve it with the same pattern as the moveUp method. But I am stuck with a ‘this context’ issue:

  • With options api, injected variables are accessible via “this”
  • To use the pattern of the moveUp function, I think I need to return an unnamed function
  • the unknown function however, doesn’t know the vue components “this” context
<template>
  <fieldset v-if="control.visible" :class="styles.arrayList.root">
    <label v-if="control.label" class="uk-form-label uk-text-primary">{{
      control.label
    }}</label>
    <ul
      v-if="!noData"
      :class="styles.arrayList.itemWrapper"
      uk-accordion="multiple: true"
    >
      <array-list-element
        v-for="(element, index) in control.data"
        :moveUp="moveUp(control.path, index)"
        :moveUpEnabled="index > 0"
        :moveDown="moveDown(control.path, index)"
        :moveDownEnabled="index < control.data.length - 1"
        :duplicate="duplicateItem(control.path, control.data[index])"
        :delete="removeItems(control.path, [index])"
        :label="childLabelForIndex(index)"
        :styles="styles"
        :key="`${control.path}-${index}`"
        :initiallyExpanded="uischema.options?.detail.initiallyExpanded"
      >
        <dispatch-renderer
          :schema="control.schema"
          :uischema="childUiSchema"
          :path="composePaths(control.path, `${index}`)"
          :enabled="control.enabled"
          :renderers="control.renderers"
          :cells="control.cells"
        />
      </array-list-element>
    </ul>
    <div v-else :class="styles.arrayList.noData">No Items</div>
    <a
      href="#"
      class="uk-button uk-button-small uk-button-secondary"
      @click.prevent="addButtonClick"
      >Add an Item</a
    >
    <hr />
  </fieldset>
</template>

<script lang="ts">
import {
  composePaths,
  createDefaultValue,
  ControlElement,
  moveDown,
  moveUp,
  Actions,
} from "@jsonforms/core";
import { defineComponent, inject } from "vue";
import {
  DispatchRenderer,
  rendererProps,
  useJsonFormsArrayControl,
  RendererProps,
} from "@jsonforms/vue";
import { useVanillaArrayControl } from "@jsonforms/vue-vanilla";
import ArrayListElement from "./ArrayListElement.vue";

const controlRenderer = defineComponent({
  name: "array-list-renderer",
  components: {
    ArrayListElement,
    DispatchRenderer,
  },
  props: {
    ...rendererProps<ControlElement>(),
  },
  setup(props: RendererProps<ControlElement>) {
    return useVanillaArrayControl(useJsonFormsArrayControl(props));
  },
  inject: ["dispatch"],
  computed: {
    noData(): boolean {
      return !this.control.data || this.control.data.length === 0;
    },
  },
  methods: {
    composePaths,
    createDefaultValue,
    addButtonClick() {
      this.addItem(
        this.control.path,
        createDefaultValue(this.control.schema)
      )();
    },
    moveDown,
    moveUp,
    duplicateItem: (path: string, value: any) => () => {
      this.dispatch(Actions.update(path, value));
    },
  },
});

export default controlRenderer;
</script>

Any thoughts on this?