Binding to prop from the parent component in Vue 3 for a custom renderer

How to parse data to a custom renderer in Vue3?

I’ve registered the Custom Renderer and the component renders for type object or in my case jsonb.

But I’m at a loss of how I would bind data from the parent component to a prop in the custom Renderer.

Any clues/ ideas.

Hi @kairos0ne,

The (custom) renderers are only passed some basic props for rendering, e.g. the current schema of the parent, the uischema to render etc. This is then combined with the form-wide state to determine the specific data, methods, computed etc. for the renderer code. For this we use the composition API, see for example here.

So in short: There is no custom direct data binding in this architecture. In case you need to pass in additional information from outside of JSON Forms, then you can use Vue’s provide/inject.

Thanks I will look at that… Let me be a little more specific. I’m using a custom component as a renderer for jsonb.

I’ve managed to pass a default object so can parse some data in but not sure how to ensure the output is then saved to the correct field for json forms. The custom component uses a json prop that is reactive. So what I want to do is bind a json prop to the required field so when the form is submitted the json is sent in the payload as that field.

here is my renderer:

<!-- CustomJsonbRenderer.vue -->
<template>
  <div>
    <Vue3JsonEditor
      v-model="json"
      :show-btns="false"
      :expandedOnStart="true"
      @json-change="onJsonChange"
      @mode-change="onModeChange"
      class="py-3"
    />
  </div>
</template>

<script>
import { defineComponent } from "vue";
import { Vue3JsonEditor } from "vue3-json-editor";

export default defineComponent({
  props: {
    json: {
      type: Object,
      default: () => ({
        name: "Example",
        description: "Example description",
      }),
    },
  },
  name: "JsonbRenderer",
  components: {
    Vue3JsonEditor,
  },
  setup() {
    function onJsonChange(value) {
      console.log("value:", value);
      // set the value to the parent component JsonForms
      // Emit the value to the parent component
      this.$emit("update:json", value);
    }
    function onModeChange(value) {
      console.log("value:", value);

      // value: code, tree, view
      // switch to json_viewer mode
      this.$refs.jsonEditor.mode = "view";
    }

    return {
      onJsonChange,
      onModeChange,
    };
  },
});
</script>

And Im using it like this:

<script>
import { JsonForms } from "@jsonforms/vue";
import { vanillaRenderers } from "@jsonforms/vue-vanilla";
import JsonbRenderer from "@/components/JsonbRenderer.vue";

import { getUISchema, postTableData } from "@/api/requests.js";

// add custom renderer to vanillaRenderers
// renderers is an array of objects with two properties: tester and renderer
// tester is a function that returns true or false
// renderer is a component that will be used to render the data
// see https://jsonforms.io/docs/uischema/renderers
//

const CustomTester = (uischema, schema) => {
  return (
    uischema.scope === "table" &&
    schema.type === "object" &&
    schema.properties &&
    schema.properties.type &&
    schema.properties.type.enum &&
    schema.properties.type.enum.includes("jsonb")
  );
};

const renderers = [
  ...vanillaRenderers,
  {
    tester: CustomTester,
    // bind the data to the component so it can be used in the template as `this.data` or `this.data.value`  etc
    renderer: JsonbRenderer,
  },
];

export default {
  components: {
    JsonForms,
    JsonbRenderer
  },
  computed: {
    id() {
      return this.$route.params.id;
    },
    payload() {
      return this.transformData();
    },
    crumbs() {
      return [
        {
          name: "Home",
          route: "/",
          label: "Home",
          order: 1,
        },
        {
          name: "Tables",
          route: "/tables",
          label: "Tables",
          order: 2,
        },
        {
          name: "Table",
          route: `/tables/${this.table}`,
          label: "Table",
          order: 3,
        },
        {
          name: "Create",
          route: `/tables/${this.table}/create`,
          label: "Create",
          order: 4,
        },
      ];
    },
    table() {
      return this.$route.params.id;
    },
  },
  data() {
    return {
      items: [],
      data: {},
      renderers: renderers,
      jsonb: {},
      schema: {},
      uischema: {},
    };
  },
  methods: {
    // transform data into this object format {name: "name", value: "string"}
    transformData() {
      let data = this.data;
      let payload = [];
      for (let key in data) {
        let obj = {};
        obj.key = key;
        obj.value = data[key];
        payload.push(obj);
      }
      return payload;
    },
    send() {
      let data = {
        id: this.$route.params.id,
        page: 1,
        page_size: 50,
        data: this.payload,
      };
      postTableData(data).then((res) => {
        console.log(res);
        this.$router.push({ name: "Table", params: { id: this.id } });
      });
    },
    onChange(event) {
      this.data = event.data;
    },
    getSchema() {
      const id = this.$route.params.id;
      getUISchema(id).then((response) => {
        this.schema = response.schema;
        this.uischema = response.ui_schema;
        this.data = response.data;
      });
    },
  },
  created() {
    this.getSchema();
  },
};
</script>
<template>
  <div>
    <nav class="pl-5 w-full max-w-xl flex px-5 py-5" aria-label="Breadcrumb">
      <ol class="inline-flex items-center space-x-1 md:space-x-3">
        <li
          class="inline-flex items-center"
          v-for="item in crumbs"
          v-bind:key="item.order"
        >
          <router-link
            v-if="item.name === 'Home'"
            :to="item.route"
            class="inline-flex items-center text-sm font-medium text-pickled-700 hover:text-rock-blue-600 dark:text-pickled-400 dark:hover:text-rock-blue-50"
          >
            <!-- if home is route  -->
            <svg
              aria-hidden="true"
              class="w-4 h-4 mr-2"
              fill="currentColor"
              viewBox="0 0 20 20"
              xmlns="http://www.w3.org/2000/svg"
            >
              <path
                d="M10.707 2.293a1 1 0 00-1.414 0l-7 7a1 1 0 001.414 1.414L4 10.414V17a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 011-1h2a1 1 0 011 1v2a1 1 0 001 1h2a1 1 0 001-1v-6.586l.293.293a1 1 0 001.414-1.414l-7-7z"
              ></path>
            </svg>
            {{ item.label }}
          </router-link>
          <router-link
            v-else
            :to="item.route"
            class="inline-flex items-center text-sm font-medium text-pickled-700 hover:text-rock-blue-600 dark:text-pickled-400 dark:hover:text-rock-blue-50"
          >
            <svg
              xmlns="http://www.w3.org/2000/svg"
              width="20"
              height="20"
              viewBox="0 0 20 20"
              fill="none"
              stroke="currentColor"
              stroke-width="2"
              stroke-linecap="round"
              stroke-linejoin="round"
              class="feather feather-chevron-right w-4 h-4 mr-2"
            >
              <polyline points="9 18 15 12 9 6"></polyline>
            </svg>
            {{ item.label }}
          </router-link>
        </li>
      </ol>
    </nav>
    <!-- container -->

    <div class="container mx-auto px-4 sm:px-8">
      <h1 class="text-3xl mt-5 font-bold text-pickled-500">
        Table: <span class="text-1xl">{{ id }} </span>
      </h1>
      <p class="text-pickled-500 mt-1">
        Use this form to add a record to the table
      </p>
      <!-- Tailwind submit btn -->
      <form action="submit">
        <json-forms
          :data="data"
          :schema="schema"
          :uischema="uischema"
          :renderers="renderers"
          @change="onChange"
          @update:json="jsonb = $event"
        >
          <JsonbRenderer
            v-for="renderer in renderers"
            :key="renderer.name"
            :renderer="renderer"
            :json="{ data: data, schema: schema, uischema: uischema }"
          />
        </json-forms>
      </form>
      <div class=" ">
        <button
          @click.prevent="send"
          class="bg-pickled-600 hover:bg-pickled-400 text-white font-bold py-2 px-4 rounded mt-0"
        >
          Submit
        </button>
      </div>
    </div>
  </div>
  <!-- Tailwind submit btn -->
</template>

If you look at the code I’m trying to bind json prop of the custom component from the parent. And emit a event that Im listening for on the parent.

I mean this is just my POC work I imagine there is a better way to do this, simpler even.

Hi @kairos0ne,

The JsonbRenderer does not need to be added to the template. The idea of the renderers set is that JSON Forms will dispatch to and instantiate an own instance of the JsonbRenderer component once the associated tester returns the highest priorities. So it’s not you who hands over the props, but JSON Forms.

In the setup of your renderer you can use one of the composition utils of JSON Forms to convert the handed over props to something specific for your renderer, e.g.

setup(props) {
  const jsonformsBindings = useJsonFormsControl(props);
  // add your custom setup code here
  return {
    ...jsonformsBindings,
    // add custom setup returns here
  }
}

The jsonformsBindings will contain a control object with all kind of data, e.g. control.schema, control.path etc. It will also contain a handleChange with which you can update the JSON Forms data store and trigger an onChange emit of JSON Forms. So whenever you have new data which you would like to sync with JSON Forms you can execute

// within Vue component code
handleChange(control.path, newData);

// within the "setup" method
jsonformsBindings.handleChange(jsonformsBindings.control.value.path, newData);

thanks for your assistance here, I will try this approach and my previous attempt was just first attempt poc… I ended up adding the component to the template and looping through the renderers i know its jenky as hell… and I didn’t realise I still had that in there when i posted this. I’ve since refactored it. Removed that its working without the addition of with this:

setup(props) {
  const jsonformsBindings = useJsonFormsControl(props);
  // add your custom setup code here
  return {
    ...jsonformsBindings,
    // add custom setup returns here
  }
}

I will try this and post an update for anyone that stumbles on this discussion. I think JSONforms is a fantastic library I only wish there was more documentation for a vue implementation specifically renderers as all the docs for these elements only cover react.

If you need some guidance I would recommend checking the implementation of the Vanilla renderers themselves and of the very complete Vue 2 Vuetify renderer set. There is no difference between our off-the-shelf renderers and custom renderers, besides us providing them already for you.

Our website is also open source. So if you, or someone else, would like to help, any contributions are welcomed :wink: