Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
kimamil opened this issue Jan 11, 2023 · 7 comments
Milestone

Comments

@kimamil
Copy link

kimamil commented Jan 11, 2023

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: "[email protected]",
  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
Copy link
Author

kimamil commented Jan 12, 2023

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.

@sdirix sdirix added this to the Backlog milestone Jan 18, 2023
@mwessendorf
Copy link

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
Copy link
Author

kimamil commented Jan 31, 2023

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
Copy link
Member

sdirix commented Jan 31, 2023

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
Copy link

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

@kimamil
Copy link
Author

kimamil commented Jan 31, 2023

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
Copy link

mwessendorf commented Feb 1, 2023

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants