Skip to content
This repository was archived by the owner on Nov 20, 2025. It is now read-only.
This repository was archived by the owner on Nov 20, 2025. It is now read-only.

Creating custom Vuetify renderer help #105

@ribrewguy

Description

@ribrewguy

It would be very helpful to understand how to create a custom Vuetify renderer that properly handles custom types. I am trying to create a renderer for a complex object in my application that I typically use with a compound component I wrote for the case. The primary issues I've been facing are below. Any ideas or help would be greatly appreciated as this is currently a blocker for moving forward with this library.

  1. No matter what I put in as a tester, it will not rank my renderer ahead of the built-in vuetify renderers. I've had to remove the built in renderers in order to allow the custom renderer a chance to render the data. Even using tester: scopeEndsWith('amount') directly didn't work until I removed the vuetify renderers. Note that the income.amount scope certainly ends in "amount".
  2. Once I forced the renderer to render, it appears to send the entire income object to the custom component rather than just the amount property. I've verified this by logging the modelValue from within the field itself.
  3. There is a warning that I cannot seem to resolve in the console that begins as follows: [Vue warn]: Invalid prop: type check failed for prop "id". Expected String with value "undefined", got Undefined at <ControlWrapper id=undefined description=undefined errors="" ... > at <MonetaryAmountControlRenderer renderers= Array [ {…} ] cells= Array [] schema=

I have a test bed (test.vue) configured as such:

<script setup lang="ts">
import {
  defaultStyles,
  mergeStyles,
  vuetifyRenderers,
} from '@jsonforms/vue-vuetify';

import { JsonForms } from '@jsonforms/vue';
import { type IncomeStream } from '~/types/income';
import { entry } from '~/forms/renderers/MonetaryAmountRenderer.vue';

// mergeStyles combines all classes from both styles definitions into one
const myStyles = mergeStyles(defaultStyles, { control: { label: 'mylabel' } });

const renderers = Object.freeze([
  // ...vuetifyRenderers,
  // here you can add custom renderers
  entry,
]);

const { salaryFormDataSchema, salaryFormUiSchema } = useIncome().formSchemas;

const data: Ref<IncomeStream> = ref({
  type: 'SALARY',
  name: 'Salary',
  description: 'Monthly salary',
  amount: {
    value: 1000,
    code: 'USD',
  },
  frequency: '',
});
</script>

<template>
  <div>
    <json-forms
      :data="data"
      :renderers="renderers"
      :schema="salaryFormDataSchema"
      :uischema="salaryFormUiSchema"
    />
  </div>
</template>

You'll note that the amount property is a complex object. I have developed a MonetaryAmountField.vue that accepts the amount complex object via the v-model strategy and handles it appropriately. That is well tested at this point.

The json schema for an salaryFormDataSchema that gets passed to the schema attribute evaluates to:

{
  "type": "object",
  "properties": {
    "id": {
      "type": "string"
    },
    "type": {
      "type": "string",
      "const": "SALARY"
    },
    "name": {
      "type": "string"
    },
    "source": {
      "type": "string"
    },
    "owner": {
      "type": "string"
    },
    "description": {
      "type": "string"
    },
    "createdAt": {
      "allOf": [
        {
          "type": "string"
        },
        {
          "type": "string",
          "format": "date-time"
        }
      ]
    },
    "updatedAt": {
      "allOf": [
        {
          "type": "string"
        },
        {
          "type": "string",
          "format": "date-time"
        }
      ]
    },
    "frequency": {
      "type": "string",
    },
    "amount": {
      "$ref": "#/definitions/amountSchema"
    }
  },
  "required": [
    "type",
    "name",
    "amount"
  ],
  "additionalProperties": false,
  "definitions": {
    "amountSchema": {
      "type": "object",
      "properties": {
        "value": {
          "type": "number",
          "exclusiveMinimum": 0
        },
        "code": {
          "type": "string",
          "default": "USD"
        }
      },
      "required": [
        "value"
      ],
      "additionalProperties": false
    }
  },
  "$schema": "http://json-schema.org/draft-07/schema#"
}

The value for salaryFormUiSchema evaluates to

{
  type: 'VerticalLayout',
  elements: [
    {
      type: 'Control',
      scope: '#/properties/name',
    },
    {
      type: 'Control',
      scope: '#/properties/description',
    },
    {
      type: 'Control',
      scope: '#/properties/amount',
      options: {
        placeholder: 'Enter your continent',
        format: 'monetary-amount',
      },
    },
  ],
}

I attempted to reverse engineer one of the existing renderers and so have wrapped my custom MonetaryAmountField in the ControlWrapper as follows:

<template>
  <control-wrapper
    v-bind="controlWrapper"
    :styles="styles"
    :isFocused="isFocused"
    :appliedOptions="appliedOptions"
  >
    <MonetaryAmountField
      :id="control.id + '-input'"
      :class="styles.control.input"
      :disabled="!control.enabled"
      :autofocus="appliedOptions.focus"
      :placeholder="appliedOptions.placeholder"
      :label="computedLabel"
      :hint="control.description"
      :persistent-hint="persistentHint()"
      :required="control.required"
      :error-messages="control.errors"
      :model-value="control.data"
      :maxlength="
        appliedOptions.restrict ? control.schema.maxLength : undefined
      "
      :size="
        appliedOptions.trim && control.schema.maxLength !== undefined
          ? control.schema.maxLength
          : undefined
      "
      v-bind="vuetifyProps('monetary-amount-field')"
      @update:model-value="onChange"
      @focus="isFocused = true"
      @blur="isFocused = false"
    />
  </control-wrapper>
</template>

<script lang="ts">
import {
  type ControlElement,
  type JsonFormsRendererRegistryEntry,
  rankWith,
  isStringControl,
  and,
  formatIs,
  scopeEndsWith,
  isObjectControl,
} from '@jsonforms/core';
import { defineComponent, ref } from 'vue';
import {
  type RendererProps,
  rendererProps,
  useJsonFormsControl,
} from '@jsonforms/vue';
import { ControlWrapper } from '@jsonforms/vue-vuetify';
import { useVuetifyControl } from '@jsonforms/vue-vuetify';
import MonetaryAmountField from '~/components/MonetaryAmountField.vue';

const controlRenderer = defineComponent({
  name: 'monetary-amount-control-renderer',
  components: {
    ControlWrapper,
    MonetaryAmountField,
  },
  props: {
    ...rendererProps<ControlElement>(),
  },
  setup(props: RendererProps<ControlElement>) {
    console.log('monetary-amount-control-renderer', props);
    return {
      ...useVuetifyControl(
        useJsonFormsControl(props),
        (value) => value || undefined,
        300,
      ),
    };
  },
});

export default controlRenderer;

export const entry: JsonFormsRendererRegistryEntry = {
  renderer: controlRenderer,
  tester: scopeEndsWith('amount'),
};
</script>

For completeness, here is the MonetaryAmountField file:

<script setup lang="ts">
import type { MonetaryAmount } from '~/types/common';

type Props = {
  modelValue?: MonetaryAmount;
  supportedCurrencyCodes?: string[];
};

const props = withDefaults(defineProps<Props>(), {
  supportedCurrencyCodes: () => ['USD'],
  modelValue: () => ({
    value: 0,
    code: 'USD',
  }),
});

const emit = defineEmits(['update:modelValue']);

console.log('MonetaryAmountField', JSON.stringify(props.modelValue, null, 2));

const amount: Ref<MonetaryAmount> = ref(unref(props.modelValue));

watch(amount.value,
  (value) => {
    emit('update:modelValue', value);
  },
);
</script>

<template>
  <v-text-field
    v-model.number="amount.value"
    type="number"
  >
    <template #append>
      <v-select
        v-model="amount.code"
        :items="supportedCurrencyCodes"
        hide-details
      />
    </template>
  </v-text-field>
</template>

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions