eclipsesource / jsonforms

Customizable JSON Schema-based forms with React, Angular and Vue support out of the box.
http://jsonforms.io
Other
2.12k stars 359 forks source link

Form Control for Array Element disappears when NODE_ENV = production in Vue 3 using vue-vanilla renderers #2077

Open kimamil opened 1 year ago

kimamil commented 1 year ago

Describe the bug

I use the same code base locally and switch NODE_ENV between 'development' and 'production' to test different environments.

When I use the vue-vanilla renderers, my form looks the same in both environments. When I use my custom ArrayListRenderer, it is only rendered when NODE_ENV is not "production" (it can be "development" or be unset).

HTML rendered in "development" mode:

<div class="layout-class">
   <fieldset class="array-class">...</fieldset>
</div>

HTML rendered in "production" mode:

<div class="layout-class">
   <!-- -->
</div>

There's no JS error, rendering in "production" mode.

Custom ArrayListRenderer:

<template>
  <fieldset v-if="control.visible" :class="styles.arrayList.root">
    <legend :class="styles.arrayList.legend">
      <label :class="styles.arrayList.label">
        {{ control.label }}
        <i
          :class="styles.arrayList.addButton"
          @click="addButtonClick"
          uk-icon="plus-circle"
        >
        </i>
      </label>
    </legend>
    <ul
      v-if="!noData"
      :class="styles.arrayList.list"
      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"
        :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>
  </fieldset>
</template>

<script lang="ts">
import {
  composePaths,
  createDefaultValue,
  JsonFormsRendererRegistryEntry,
  rankWith,
  ControlElement,
  schemaTypeIs,
} 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)
      )();
    },
  },
});

export default controlRenderer;

const entry: JsonFormsRendererRegistryEntry = {
  renderer: controlRenderer,
  tester: rankWith(3, schemaTypeIs("array")),
};

export { entry };
</script>

Expected behavior

I would expect the control to render with NODE_ENV = "production"

Steps to reproduce the issue

to reproduce, use the vue-vanilla example project and recreate my custom renderer.

register it as follows:

import { vanillaRenderers } from "@jsonforms/vue-vanilla";
import { entry as ArrayListRenderer } from "@/components/jsonforms/ArrayListRenderer.vue";

const myRenderers = Object.freeze([...vanillaRenderers, ArrayListRenderer]);

use the following schema:

const settingsProperties = {
  adminMail: {
    type: "string",
  },
  locale: {
    type: "string",
  },
  hscodes: {
    type: "array",
    title: "HSCODES",
    items: {
      type: "object",
      properties: {
        code: {
          type: "string",
        },
        label: {
          type: "string",
        },
      },
    },
  },
};

and this UiSchema:

{
      type: "VerticalLayout",
      elements: [
        {
          type: "Control",
          scope: "#/properties/adminMail",
        },
        {
          type: "Control",
          scope: "#/properties/locale",
        },
        {
          type: "Control",
          scope: "#/properties/hscodes",
          options: {
            childLabelProp: "label",
            detail: {
              type: "HorizontalLayout",
              initiallyExpanded: false,
              elements: [
                {
                  type: "Control",
                  scope: "#/properties/label",
                },
                {
                  type: "Control",
                  scope: "#/properties/code",
                },
              ],
            },
          },
        },
      ],
    }

and this example data:

{
  adminMail: "info@example.org",
  locale: "de-CH",
  hscodes: [
    { label: "Sculpture", code: "2345-90.1" },
    { label: "Painting", code: "454.343-1" },
  ],
}

Screenshots

image image

In which browser are you experiencing the issue?

Version 108.0.5359.124 (Offizieller Build) (arm64)

Which Version of JSON Forms are you using?

v3.0.0

Framework

Vue 3

RendererSet

Vanilla

Additional context

No response

kimamil commented 1 year ago

Finally solved the issue. Maybe there's a better way to solve this, feel free to add it here:

The issue seems to be related to type definitions. In a typescript vue project, there's the file 'src/shims-vue.d.ts' declaring *.vue modules as

/* eslint-disable */
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

using the JsonFormsRegistryEntry inside the custom renderer component like

...

export default controlRenderer;

const entry: JsonFormsRendererRegistryEntry = {
  renderer: controlRenderer,
  tester: rankWith(3, schemaTypeIs("array")),
};

export { entry };
</script>

conflicts with this module definition, as the definition has no 'entry' element.

Solution I extracted the registry entries to where I set my renderers, and now the formControls get rendered also in production environment.

Source of the issue (best guess) I am not too experienced with typescript, but I think what is missing is the correct controlRenderer type definition within my vue application (since I have only modified html parts in the template, this should still be the same type). Vue thinks, that the controller is a "simple" vue component and uses the module declaration in 'shims-vue.d.ts' which doesn't match the controllerRenderer type.

mwessendorf commented 1 year ago

Hi @kimamil, thanks for the detailed report. I ran into exact the same issue. Wrote a bunch of custom renderer to use instead of the vanilla renderer. Everything is working nicely in development. In production mode only the more or less empty HTML is rendered.

Breaking my head here a little bit since a day. I also ended up with checking the 'src/shims-vue.d.ts' and config but was not able to solve it.

Could you please help me understanding your solution a bit? 'I extracted the registry entries to where I set my renderers, and now the formControls get rendered also in production environment.'

From what I understand I could use JsonFormsRegistryEntry where I set my renderers and add the Tester part there. I just did not get it to work. It seams I missing or misunderstood something or my brain is just gone. A hint in the right direction would be highly appreciated.

Cheers!

kimamil commented 1 year ago

Hi @mwessendorf

Sure! So the issue for me was, that I declared the tester function within my SFC (Single File Component) as it is done in the vanilla-renderers. This declares an export called „entry“. When assembling the registry in a different part of my code, I imported it via ‘‘‘import { entry as CustomRendererEntry} from CustomRenderer.vue‘‘‘.

This doesn‘t work since the shims declaration for SFCs has no export „entry“ declared. Therefore, in production mode, the entry object is not exported (don‘t ask me, why it‘s working in other modes).

To work around this, I needed to declare the tester function in a part of my app where I control the object declaration. I personally ended up defining them directly before assembling the registry without any export/import.

I am sure there‘s a possibility to extend the SFC type declaration with an optional ‚entry‘ export. I just couldn‘t get my head around it under the time pressure.

Let me know if this helped clarifying my approach. If you need more support, could you please try to explain your project folder structure and where you declare the renderer, the testing function and the registry - thanks!

sdirix commented 1 year ago

This doesn‘t work since the shims declaration for SFCs has no export „entry“ declared. Therefore, in production mode, the entry object is not exported (don‘t ask me, why it‘s working in other modes).

Can you explain how you came to this conclusion? I would have expected the shim to only have an influence on Typescript type resolution and not on the actual export mechanism.

mwessendorf commented 1 year ago

Many thanks @kimamil, will dive into that again and come back with (hopefully) results.

kimamil commented 1 year ago

This doesn‘t work since the shims declaration for SFCs has no export „entry“ declared. Therefore, in production mode, the entry object is not exported (don‘t ask me, why it‘s working in other modes).

Can you explain how you came to this conclusion? I would have expected the shim to only have an influence on Typescript type resolution and not on the actual export mechanism.

Hi @sdirix - It’s quite possible that I am wrong since my knowledge on typescript is very limited.

My understanding is that when Vue 3 is setup with typescript and the options API this shims file is created in the project source: https://github.com/Code-Pop/Real-World-Vue-3-TypeScript/blob/75977c69c06e4c84d67f0709847b107687e935ca/src/shims-vue.d.ts

As far as I can tell, it declares an interface for all .vue modules. My custom renderer, copied from vanilla-renderers matches the .vue selector.

As you can see, it only declares a default export for a component. Naturally, when creating a custom renderer using additional exports, at some point linting throws errors.

I tried to modify this shims file adding an optional named „entry“ export but couldn‘t get it to work properly.

I hope this helps. If not I am happy to recreate the situation and give you more concrete feedback on the errors etc.

=== UPDATE

Here's a screenshot that shows my folder structure and the file where I import my custom renderer. When I use the entry export copied from vanilla-renderers, this is what I get:

image

And when I change the shims-vue.d.ts to:

import { JsonFormsRendererRegistryEntry } from "@jsonforms/core";
/* eslint-disable */
declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
  const entry: JsonFormsRendererRegistryEntry
  export {entry}
}

I get more typescript type declaration errors ...

image
mwessendorf commented 1 year ago

Had today some time to dive in again. Tried some ideas but ended up with the following solution which I guess is more or less the same you @kimamil have? Working example for String renderer (have some more in the project):

import StringControlRenderer from "components/jsonforms/StringControlRenderer.vue";

const stringRenderer = buildRendererRegistryEntry(StringControlRenderer, isStringControl)

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

And here the function from another file (js and ts are both used in the project):

import { JsonFormsRendererRegistryEntry, rankWith, Tester} from "@jsonforms/core";

export function buildRendererRegistryEntry(testRenderer: any, controlType: Tester) {
  const entry: JsonFormsRendererRegistryEntry = {
    renderer: testRenderer,
    tester: rankWith(3, controlType )
  };
  return entry
}

If there is any better solution I happy to follow up on it. At least happy to have now a working solution not only for dev env =) Many thanks to @kimamil again. And for sure also thanks to you guys @sdirix for all the efforts.