Whats the proper Way to change control.data in a vue3 custom renderer?

Hi,
i have the following problem, i want to use the validation feature but everytime i use handleChange(path, data), it doesnt do anything for me. setting control.data = newdata directly changes control.data, but doesnt trigger the validation.

Im using vue3.

RouteHandler contains the JsonForms

<script setup lang="ts">
import TableView from '../views/TableView.vue';
import HeadlineWithBreadcrumps from '../components/HeadlineWithBreadcrumps/HeadlineWithBreadcrumps.vue';
import { computed, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useMainStore } from '../store';
import { JsonForms, JsonFormsChangeEvent, useJsonFormsControl } from "@jsonforms/vue";
import { getDataViewActionByRel } from '../helpers';
import { storeToRefs } from 'pinia';
import { markRaw } from 'vue';

// TODO switch with capeable Renderes from old project
import RendererList from '../renderers'
import { vanillaRenderers } from "@jsonforms/vue-vanilla";
import { useActionStore } from '../store/ActionStore/ActionStore';

import { useRerenderStore } from '../store/RerenderStore';
import { useRowStore } from '../store/RowStore';

const reRenderStore = useRerenderStore();
const rerenderKey = computed(() => reRenderStore.rerenderKey);

const route = useRoute();
const DataView = ref<any>();
const JSONFormData = ref('');
const store = useMainStore();
const { activeAction, isLoggedIn } = storeToRefs(store);

const schema = computed(() => {

    if (activeAction.value && Object.keys(activeAction.value).length) {
        const index = useActionStore()._activeAction.steps.findIndex((step: { id: any; }) => step.id === useActionStore()._activeStep.id);
        if (activeAction.value.steps[index]?.dataSchema) {
            const { dataSchema } = activeAction.value.steps[index];
            if (typeof dataSchema === 'string') {
                return JSON.parse(dataSchema);
            }
            return dataSchema || {};
        } else {
            console.log("Active Step has no Data Scheme");
        }
    }
    return {};
});

const uiSchema = computed(() => {

    if (activeAction.value && Object.keys(activeAction.value).length) {
        const index = useActionStore()._activeAction.steps.findIndex((step: { id: any; }) => step.id === useActionStore()._activeStep.id);
        if (activeAction.value?.steps[index]?.uiSchema) {
            const { uiSchema } = activeAction.value.steps[index];
            if (typeof uiSchema === 'string') {
                return JSON.parse(uiSchema);
            }
            return uiSchema || {};
        } else {
            console.log("Active Step has no UI Scheme");
        }
    }
    return {};
});


const renderers = [
    ...Object.freeze(vanillaRenderers),
    ...Object.freeze(RendererList)
]

const getDataViewFromStore = async () => {
    await store.setDataView();
    const { dataview }: any = store.getDataView;
    DataView.value = dataview;
}

const determineView = computed(() => {
    const formRoutes = ['/login', '/forgot-password', '/reset-password', '/change-password'];

    if ((useActionStore()._activeAction)) {
        if (useActionStore()._activeStep.type === 'FORM') return JsonForms;
    }
    // Check if the route is in the formRoutes array
    if (formRoutes.includes(route.path)) return JsonForms;

    // Default to TableView for other routes
    return TableView;
});


getDataViewFromStore();


watch(DataView, () => {
    if (route.params.path === 'change-password') return;
    console.log("Updating DataView: " + getDataViewActionByRel(DataView.value.actions, route.params.path));
    console.log(DataView.value.actions);
    console.log(route.params.path);
    store.setActiveAction(getDataViewActionByRel(DataView.value.actions, route.params.path));
    if (store.activeAction) {
        store.setActiveStep(store.activeAction.steps[0]);
    }
});

</script>
<template>
    <HeadlineWithBreadcrumps v-if="isLoggedIn" />
    <component :is="determineView" :key="rerenderKey" :data="useRowStore().getRow" :renderers="markRaw(renderers)" :schema="schema"
        :uischema="uiSchema" />
</template>

InputRenderer:

<template>
  <div class="input-renderer">
    <DefaultInput
      :label="control.label"
      :value="control.data"
      :type="getInputType()"
      @add="setFormData"
      @input="onInput"
      :secondary="getHierarchy()"
      :loading="false"
      :icon="getIconPath()"
      :required="control.required"
      :disabled="!control.enabled || appliedOptions.readonly"
      :search="true"
      :errorMessage="control.errors"
      :focus="appliedOptions.focus"
    >
    </DefaultInput>
    {{ control.errors || "Ok" }}
  </div>
</template>

and some parts of the code:

const onInput = (event: Event) => {
  const target = event.target as HTMLInputElement;
  const newvalue = target.value;
  console.log(
    "Updating control.data with: " + preProcessInput(sanitizeData(newvalue))
  );

  console.log(control.path);
  console.log(preProcessInput(sanitizeData(newvalue)));

  jsonformsBindings.handleChange(control.path, preProcessInput(sanitizeData(newvalue)));
  console.log(jsonformsBindings.handleChange);
  //control.data = preProcessInput(sanitizeData(newvalue));

  const isValid = computed(() => ajv.validate(control.schema, control.data));

  console.log("Updated control.data: ", control.data);
  console.log("Possible Errors: " + control.errors);

  return isValid;
};

isValid is always right, so i guess the validation is fine. But i cant get control.errors to update with the data (that doesnt update with any use of handleChange)

and

DefaultInput:

<template>
  <div :class="[
    'default-input',
    'form-group',
    { 'has-validation-feedback': hasValidationFeedback, disabled },
  ]">
    <label v-if="type !== 'hidden'" :for="field">{{ label }}</label>
    <div class="input-group">
      <input ref="input" :id="field" :type="vType" class="default-input__input" :class="{
    'default-input__input--secondary': secondary,
    'default-input__input--loading': loading,
    'default-input__input--disabled': disabled,
    'default-input__input--search': search,
    'is-valid': isValid === true,
    'is-invalid': isValid === false,
  }" :disabled="disabled" v-model="value" @change="$emit('add', value)" @keydown="validateKey" />
      <div v-if="type === 'password'" class="input-group-append" @click="forceVisible = !forceVisible">
        <font-awesome-icon class="toggle-password-visibility" :icon="forceVisible ? 'eye' : 'eye-slash'" />
      </div>
    </div>
    </div>
</template>

<script setup lang="ts">
import { v4 } from 'uuid';
import { computed, ref, onMounted, useSlots } from 'vue';

interface InputProps {
  label: string,
  value: string | number,
  type: string,
  defaultValue?: string,
  validationFeedback?: string,
  primary?: boolean,
  secondary?: boolean,
  loading?: boolean,
  icon?: string,
  action?: string,
  disabled?: boolean,
  search?: boolean,
  errorMessage?: any,
  isValid?: boolean,
  focus?: boolean,
};

const props = defineProps<InputProps>();
const slots = useSlots();

const forceVisible = ref(false);
const field = computed(() => `default-input-${v4()}`);
const value = ref(props.value);

const vType = computed(() => (props.type === 'password' ? (forceVisible.value ? 'text' : 'password') : props.type));
const hasValidationFeedback = computed(() => !!slots['validation-feedback'] && props.isValid !== null);

const inputRef = ref<HTMLInputElement | null>(null);

onMounted(() => {
  if (props.focus && inputRef.value) {
    inputRef.value.focus();
  }
});

const emit = (event: string) => {
  const eventDetail = new CustomEvent(event);
  window.dispatchEvent(eventDetail);
};

const validateKey = (event: KeyboardEvent) => {
  emit("form-validate");

  if (props.type === 'number') {
    const regex = new RegExp(/^[0-9.,]+$/);
    if (
      event.key !== 'Backspace' &&
      event.key !== 'Delete' &&
      !((event.ctrlKey || event.metaKey) && event.key === 'a') &&
      !regex.test(event.key)
    ) {
      event.preventDefault();
    }
  }
};
</script>

What am i overlooking?

const onChange = (event: Event) => {

  const { handleChange } = useJsonFormsControl(props);
  handleChange(control.path, (event.target as HTMLInputElement).value);

  const isValid = computed(() => ajv.validate(control.schema, control.data));

  console.log("Updated control.data: ", control.data);
  console.log("Possible Errors: " + control.errors);
  console.log(isValid);

};

doesnt work for me either. i get a vue error:

[Vue warn]: inject() can only be used inside setup() or functional components.

and

Uncaught 'jsonforms' or 'dispatch' couldn't be injected. Are you within JSON Forms?

Hi @Till,

In general handleChange is the way to go. It will update the form-wide data storage and your renderer will receive the new value as part of control.data.

There are some parts of the code which I don’t understand.

Why is the handleChange referred to as jsonformsBindings.handleChange while the control is referred to directly? Can you post your setup code?

You never need to validate the data yourself. This is done automatically on the form-wide data update triggered by handleChange. You will receive all errors in the control.errors attribute.

It should update. As mentioned above, can you post your setup code?

As indicated by Vue, this will never work as useJsonFormsControl is part of the Vue composition API and therefore must be called in setup.

<template>
  <div class="input-renderer">
    <DefaultInput
      :label="control.label"
      :value="control.data"
      :type="getInputType()"
      @add="setFormData"
      @input="onChange"
      :secondary="getHierarchy()"
      :loading="false"
      :icon="getIconPath()"
      :required="control.required"
      :disabled="!control.enabled || appliedOptions.readonly"
      :search="true"
      :errorMessage="control.errors"
      :focus="appliedOptions.focus"
    >
    </DefaultInput>
    {{ control.errors || "Ok" }}
  </div>
</template>

<script setup lang="ts">
import { rendererProps, useJsonFormsControl } from "@jsonforms/vue";
import DefaultInput from "../../components/DefaultInput/DefaultInput.vue";
import { useMainStore } from "../../store";
import { useFormStore } from "../../store/FormStore";
import { useControlRenderer } from "../../compositions/useControlRenderer";
import { computed, ref } from "vue";
import { createAjv } from "@jsonforms/core";

const store = useMainStore();

const props = defineProps({
  ...rendererProps<any>(),
});

const { handleChange } = useJsonFormsControl(props);

const { control, appliedOptions } = useControlRenderer(
  props as any
);

const preProcessInput = (input: String | Number | undefined) => {
  if (typeof input === "number") {
    return !isNaN(input) ? input : undefined;
  } else {
    return typeof input === "string" && input.length > 0 ? input : undefined;
  }
};

const onChange = (event: Event) => {

  handleChange(control.path, (event.target as HTMLInputElement).value);

  console.log("Updated control.data: ", control.data);
  console.log("Possible Errors: " + control.errors);
  console.log(isValid.value);

};



const sanitizeData = (value: String | Number | undefined) => {
  if (!value) return;

  if (control?.schema.type === "number") {
    return Number(value);
  }
  return String(value);
};

const sanitizedData = computed(() => sanitizeData(control.data));

const getInputType = () => {
  if (appliedOptions.hidden) {
    console.log("field hidden");
    return "hidden";
  } else if (appliedOptions.password) {
    console.log("field password");
    return "password";
  } else if (control.schema.type === "number") {
    console.log("field number");
    return "number";
  } else {
    console.log("field text");
    return "text";
  }
};

const setFormData = (value: string) => {
  const { scope } = props.uischema;
  const croppedScope = scope.split("/").pop();
  if (croppedScope) {
    const currentFormData = useFormStore().formData;
    const updatedFormData = { ...currentFormData, [croppedScope]: value };
    store.setFormData(updatedFormData);
  }
};

const getHierarchy = () => {
  const { options } = props.uischema;
  if (options) {
    const { color, outline } = options;
    return color === "secondary" || outline === "secondary";
  }
  return false;
};

const getIconPath = () => {
  const { click } = props.uischema;
  const iconMapping: { [key: string]: string } = {
    buttonConfirm: "fa-check",
    "forgot-password": "fa-lock",
    registration: "fa-address-card",
  };
  return iconMapping[click] || "";
};
</script>

<style scoped lang="scss">
.input-renderer {
  &.hidden {
    width: 0;
    height: 0;
    display: none;
    opacity: 0;
  }
}
</style>

thats what i have at the moment, im constantly trying things and changing ways so it might be pretty messy and maybe even some double logic.

The useControlRenderer seems to be a custom composition from your side. I guess that one is not correctly implemented.

If it’s similar to the useVanillaControl from JSON Forms, then it should be implemented like this and used like this:

const { control, handleChange } = useControlRenderer(useJsonFormsControl(props))

My guess is that the control of your useControlRenderer is not correctly updated when the form wide state changes.

Thank you, i found the mistake. control wasnt the control im looking for! GREAT help!