8.13.0.3.7.0-alpha.0 uses 8.18.0. This is recommended to be used as AJV belo 8.18 has a severe security flaw.
Can you give a whole example to execute and specify which version of ajv errors you use.
It could be worth checking if you updated the ajv errros version with updating to JSON Forms 3.7. Maybe that leads to unexpected behavior.
Best regards,
Lucas
after update from 3.6 to 3.7 the error messages are no more customs…
my main call looks like ;
<JsonForms
//i18n={{translate: translator}}
schema={schemasQuery.data.schema as JsonSchema}
uischema={schemasQuery.data.uiSchema}
data={isEmpty(jsonFormData) ? schemasQuery.data.data : jsonFormData}
renderers={renderers}
cells={materialCells}
ajv={xcreateAjv()}
validationMode="ValidateAndShow"
onChange={({ data, errors }) => {
setJsonFormData(data);
setJsonFormErrors(errors);
}}
and my ajv.ts containng the xcreateajv is :
import ajvErrors from ‘ajv-errors’;
import { createAjv } from ‘@jsonforms/core’;
const formats = {
email: {
type: 'string',
validate: (data: string) => /^\[^\\s@\]+@\[^\\s@\]+\\.\[^\\s@\]+$/.test(data),
},
phone: {
type: 'string',
validate: (data: string) => /^\\+\[0-9\]+$/.test(data),
},
url: {
type: 'string',
validate: (data: string) => /^https:\\/\\/.+/.test(data),
},
linkedin: {
type: 'string',
validate: (data: string) => /^https:\\/\\/(www\\.)?linkedin\\.com\\/(in|company)\\/\[\\w-\]+\\/?$/.test(data),
},
// reconnaître le format SVG et arrêter les messages d’erreur //////////////////////DO NOT WORK as not in schema (in schemaui)
‘upload-logo’: {
validate: (str: string) => {
return typeof str === 'string';
},
errorMessageKey: 'validation_svgInvalid',
errorMessage: "URL invalid - must begin with https://",
}
};
export const xcreateAjv = () => {
const myAjv=createAjv({
allErrors: true,
verbose: true,
$data: true,
strict: false,
messages: true
});
ajvErrors(myAjv);
// Define the totalPercentage keyword
myAjv.addKeyword({
keyword: 'totalPercentage',
type: 'array',
validate: function (schema: any, data: any) {
if (!Array.isArray(data)) return false; // Ensure data is an array
const total = data.reduce((sum: number, entry: any) => sum + (entry.percent || 0), 0);
return total === 100; // Check if total equals 100
},
errors: false, // Set to true if you want to use custom error messages
});
Object.entries(formats).forEach((\[name, format\]) => {
myAjv.addFormat(name, format.validate);
});
return myAjv;
};
I tried with or without updating ajv package, both failed
which version of ajv should we use?
how should we write with new jsonforms?
Thx a lot!
]]>default property in the schema. Or did you already try this with
do it is with a custom generator function that extracts these values from the
schema.json
E.g. in the schema
{
"type": "string",
"default": "mydefault"
}
Cheers,
Lucas
example values as placeholders in my custom inputfieldrenderer.vue renderer:
//yaml
example:
dns:
a_record_targets:
- 127.0.0.1
- 127.0.0.1
srv_record_targets_sip:
- address: sip1-n1.example.com
port: 5060
priority: 1
weight: 50
- address: sip1-n2.example.com
port: 5060
priority: 1
weight: 50
srv_record_targets_sips:
- address: sips.example.com
port: 5061
priority: 1
weight: 100
The way i do it is with a custom generator function that extracts these values from the schema.json and sets the placeholders in the custom renderer, is there a built in option to do this? having to do this for every renderer is quite painful.
Could you open an issue, please?
Thanks and best regards,
Lucas
When building a modern Angular application using the Angular Material JSONForms package, a warning is shown because it imports the hammerjs library – the bundler complains because the package is CJS-only. On investigation, it looks like this package hasn’t been updated in a decade, and many of the browser shortcomings it aimed to address are now implemented directly in the DOM.
As far as I can tell the package is imported once at the top of angular-material/src/library/index.ts and none of its helpers are directly used in the rest of the codebase. If I’m missing something please let me know, but otherwise it seems like this import (and the package dependency) could probably be safely removed.
We currently use AJV to determine the best element to show when we encounter a oneOf, however that only works if the oneOf is self-contained. If it isn’t the warning is shown and we just show the first one.
The max recursive error seems to indicate that we try to render an endlessly deep UI, which I would not have expected as the first element of the oneOf is actually the LEAF and even if we go down the branches, the rendering should stop at an empty array.
We need to check in more detail what is going wrong. Until then, feel free to debug why it tries to render endlessly deep. I would imagine something in the oneOf handling to go wrong.
]]>Combinator subschema is not self contained, can't hand it over to AJV
and after a while, it ends up throwing a max recursive error:
Uncaught (in promise) Maximum recursive updates exceeded in component . This means you have a reactive effect that is mutating its own dependencies and thus recursively triggering itself. Possible sources include component template, render function, updated hook or watcher source function.
This is the JSON schema I’m using:
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Configuration",
"type": "object",
"definitions": {
"tree_leaf": {
"type": "object",
"properties": {
"value": {
"type": "string"
},
"label": {
"description": "A human readable name (if left out the value will be title cased)",
"type": "string"
}
},
"required": ["value"],
"additionalProperties": false
},
"tree_branch": {
"type": "object",
"properties": {
"children": {
"description": "Array of children. Either children or value needs to be provided",
"$ref": "#/definitions/tree_options"
},
"label": {
"description": "A human readable name (if left out the value will be title cased)",
"type": "string"
}
},
"required": ["children", "label"],
"additionalProperties": false
},
"tree_options": {
"type": "array",
"uniqueItems": true,
"items": {
"oneOf": [
{
"title": "Leaf",
"$ref": "#/definitions/tree_leaf"
},
{
"title": "Branch",
"$ref": "#/definitions/tree_branch"
}
]
}
},
"tree_picker": {
"properties": {
"values": {
"$ref": "#/definitions/tree_options"
}
},
"additionalProperties": false
}
},
"properties": {
"tree_picker": {
"description": "Tree picker widget",
"$ref": "#/definitions/tree_picker"
}
},
"additionalProperties": false
}
Any help will be more than welcome! Thanks! ![]()
vi.mock(‘lodash/debounce’, () => ({ default: (fn: (…args: any[]) => any) => { const debounced = (…args: any[]) => { fn(…args); }; debounced.cancel = vi.fn(); debounced.flush = vi.fn(); return debounced; },}));
but still I get the ‘window is not defined’ error sometimes and only in the CI runs.
Do you have maybe a working example of such a mock for debounce?
Or a suggestion how to achieve such a mock that will work consistently?
Uncaught MissingRefError: can’t resolve reference # from id #. JsonForms seems unable to handle the # self-reference ]]>Yes MaterialEnumControl is the component I want to customize.
I have already tried following the custom rendering guide - without success.
I’ll have a deeper look into it.
]]>You can always inject the raw JSON Forms building blocks and thereby recreate handleChange for your use case, e.g.
You can inject the form-wide dispatch:
const dispatch = inject<Dispatch>(‘dispatch’);
Dispatch works on actions, so you can import Actions from @jsonforms/core , create the UPDATE action yourself and dispatch it.
Alternatively you can reuse one of the core helpers, e.g.
const { handleChange } = mapDispatchToControlProps(dispatch);
]]>Unfortunately, seems that the useJsonFormsArrayControl() method I’m using doesn’t expose the handleChange method, so I’m at a loss on how to change form status.
Worth noting I’m using the Composition API style of component definition for styling rules reasons…
<script setup lang="ts">
import { DispatchRenderer, useJsonFormsArrayControl, type RendererProps } from '@jsonforms/vue';
import { type ControlProps } from '@jsonforms/vue';
const props = defineProps<RendererProps<ControlProps>>();
const ctrl = useJsonFormsArrayControl(props);
]]>I think you have multiple options available:
The debounce in the JSON Forms component is 10ms long, so you could wait in each test for that amount of time.
You could setup your tests to resolve lodash/debounce to your own implementation which then just applies the function immediately without debouncing.
You could run JSON Forms in a controlled style. Then you don’t need to use onChange at all and therefore don’t need to care about that it is debounced.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Unhandled Errors ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Vitest caught 1 unhandled error during the test run.
This might cause false positive tests. Resolve unhandled errors to make sure your tests are not affected.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Uncaught Exception ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
ReferenceError: window is not defined
❯ getCurrentEventPriority ../../node_modules/react-dom/cjs/react-dom.development.js:10993:22
❯ requestUpdateLane ../../node_modules/react-dom/cjs/react-dom.development.js:25495:19
❯ dispatchSetState ../../node_modules/react-dom/cjs/react-dom.development.js:16648:14
❯ Object.onChange [as current] src/lib/preview/Preview.tsx:109:13
107| cells={uiTypesCustomCells}
108| onChange={(event) => {
109| setData(event.data);
| ^
110| onFormDataChange?.(simplifyEventStructure(event));
111| }}
❯ ../../node_modules/@jsonforms/react/src/JsonFormsContext.tsx:260:59
❯ invokeFunc ../../node_modules/lodash/debounce.js:95:19
❯ trailingEdge ../../node_modules/lodash/debounce.js:144:14
❯ Timeout.timerExpired [as _onTimeout] ../../node_modules/lodash/debounce.js:132:14
❯ listOnTimeout node:internal/timers:588:17
❯ processTimers node:internal/timers:523:7
This error originated in “src/lib/preview/tests/errorMessages.test.tsx” test file. It doesn’t mean the error was thrown inside the file itself, but while it was running.
This error was caught after test environment was torn down. Make sure to cancel any running tasks before test finishes:
Hello,
I’m using @jsonforms/[email protected]
I’m writing unit tests with vitest that render JsonForms.
When I run the test I sometimes get the error:
ReferenceError: window is not defined (see full error above)
It’s caused by a timing issue of the lodash debounce that is used inside JsonForms.
The questions is what’s the proper way to mock the JsonForms debounce in my tests so it will run immediately and won’t cause timing issue.
yes the scope property is used to reference the property to render. Thus, it’s last segment always is the property itself as long as your UI Schema is configured correctly.
Best regards,
Lucas
Did you try JSON Forms without invoking json-schema-ref-parser? I think the schema in question should be supported out of the box.
]]>scope can be parsed to find the property that is being rendered?
I have a custom renderer that renders a oneOf with a discriminator. The renderer ( ui/src/forms/overrides/material/complex/MaterialOneOfRenderer_Discriminator.tsx at main · estuary/ui · GitHub ) is a copy/paste/tweak of the MaterialOneOfRenderer from y’all.
In our custom version I am calling createCombinatorRenderInfos to get back oneOfRenderInfos and then filtering out the discriminator property from the UISchema’s elements property.
oneOfRenderInfos.map((renderer) => {
const { uischema: rendererUischema } = renderer as any;
rendererUischema.elements = rendererUischema.elements.filter(
(el: any) => {
const pathSegments = el.scope?.split('/');
if (pathSegments && pathSegments.length > 0) {
const lastSegment = decode(
pathSegments[pathSegments.length - 1]
);
return lastSegment !== discriminatorProperty;
}
return true;
}
);
return renderer;
});
This feels safe - but wanted to check that it is. Will the ending of the scope property normally be the name of a property? So far in my testing that seems right and looking are your code (like how deriveLabel does something similar) it seems this is how y’all find properties already but wanted to check.
As for the previous problem, I believe the root cause was a circular reference in my schema (one of my oneOf properties has a $ref to itself for allowing multiple layers of paragraphs). I’d love a built-in way to limit circular reference depth, but for the time being I am intercepting the json-schema-ref-parser deref and replacing the subschema with a simple array of strings. I’ll provide a snippet of my schema below so you have a better view of what I’m working with, but I understand limited circular refs is likely a low priority.
Thanks for your time!
{
"type": "array",
"items": {
"oneOf": [
{
"title": "String",
"type": "string"
},
{
"title": "Advanced Text",
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"text",
"sidebar",
"example",
"read-aloud",
"codeblock"
]
},
"title": {
"type": "string"
},
"forceTitleLevel": {
"type": "integer"
},
"page": {
"type": "integer"
},
"entries": { "$ref": "#" } // this is the problem
},
"required": [
"entries"
],
"additionalProperties": false
},
]
}
}
]]>thanks for working on this.
I am not aware of the independent AJV being a problem but I have also not done it, yet. Is your installed AJV of a compatible version? Otherwise, I could imagine this leading to problems.
The bug with the switch boolean ↔ array I can reproduce. Would you like to open an issue for this at https://github.com/eclipsesource/jsonforms/issues ?
Thanks and best regards,
Lucas
the 3.7.0 release is currently planned for end of this week, latest next week.
Best regards,
Lucas
Do you have an anticipated date or timeframe for publishing the official v3.7.0 release? I see there are a few alpha tags in Github.
Thank you for all your great work!
Doug
]]>While I’m working out a reproducible demo, I’ve noticed another issue with oneOf. In the example above, the options are a boolean value or an array of strings.
- If I switch from the array to the boolean I am prompted to clear the data in order to proceed.
TypeError: array.push is not a functionI will include this example in my reproduction, but is easy to replicate by copying the data above if you want to take a look before I get mine up.
]]>Unfortunately, I cannot reproduce the error message in the JSON Forms Vue Seed.
Can you provide an example repository or or JSBin or similar where this can be reproduced?
Best regards,
Lucas
Feel free to add your ideas there and/or contribute the featues. If you want to contribute, it probably makes sense to outline your approach in the issue so it can be discussed there ![]()
Thanks and best regards,
Lucas
@jsonforms/core: 3.6.0
@jsonforms/vue: 3.6.0
@jsonforms/vue-vanilla: 3.6.0
SCHEMA
{
"type": "array",
"items": {
"type": "object",
"properties": {
"name": {
"type": "string"
},
"page": {
"type": "integer"
},
"prerequisites": {
"oneOf": [
{
"title": "True/False",
"type": "boolean"
},
{
"title": "Named Prereqs",
"type": "array",
"items": {
"type": "string"
}
}
]
}
}
}
UI SCHEMA
{
"type": "VerticalLayout",
"elements": [
{
"type": "Group",
"label": "Talent",
"elements": [
{
"type": "Control",
"scope": "#/properties/name"
},
{
"type": "Control",
"scope": "#/properties/page"
},
{
"type": "Control",
"scope": "#/properties/prerequisites"
}
]
}
]
}
ERROR MESSAGE
const schema0 = scope.schema[0];const schema1 = scope.schema[1];const schema1 = scope.schema[1];const schema7 = scope.schema[4];const schema7 = scope.schema[4];const schema9 = scope.schema[5];const schema9 = scope.schema[5];const validate1 = scope.validate[0];const validate3 = scope.validate[1];const func0 = scope.func[0];const func0 = scope.func[0];return function validate0(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){let vErrors = null;let errors = 0;if((!(data && typeof data == "object" && !Array.isArray(data))) && (typeof data !== "boolean")){const err0 = {instancePath,schemaPath:"#/type",keyword:"type",params:{type: schema0.type},message:"must be object,boolean",schema:schema0.type,parentSchema:schema0,data};if(vErrors === null){vErrors = [err0];}else {vErrors.push(err0);}errors++;}if(data && typeof data == "object" && !Array.isArray(data)){if(data.$id !== undefined){let data0 = data.$id;if(!(typeof data0 === "string")){const err1 = {instancePath:instancePath+"/$id",schemaPath:"#/properties/%24id/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema0.properties.$id.type,parentSchema:schema0.properties.$id,data:data0};if(vErrors === null){vErrors = [err1];}else {vErrors.push(err1);}errors++;}}if(data.$schema !== undefined){let data1 = data.$schema;if(!(typeof data1 === "string")){const err2 = {instancePath:instancePath+"/$schema",schemaPath:"#/properties/%24schema/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema0.properties.$schema.type,parentSchema:schema0.properties.$schema,data:data1};if(vErrors === null){vErrors = [err2];}else {vErrors.push(err2);}errors++;}}if(data.$ref !== undefined){let data2 = data.$ref;if(!(typeof data2 === "string")){const err3 = {instancePath:instancePath+"/$ref",schemaPath:"#/properties/%24ref/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema0.properties.$ref.type,parentSchema:schema0.properties.$ref,data:data2};if(vErrors === null){vErrors = [err3];}else {vErrors.push(err3);}errors++;}}if(data.$comment !== undefined){let data3 = data.$comment;if(typeof data3 !== "string"){const err4 = {instancePath:instancePath+"/$comment",schemaPath:"#/properties/%24comment/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema0.properties.$comment.type,parentSchema:schema0.properties.$comment,data:data3};if(vErrors === null){vErrors = [err4];}else {vErrors.push(err4);}errors++;}}if(data.title !== undefined){let data4 = data.title;if(typeof data4 !== "string"){const err5 = {instancePath:instancePath+"/title",schemaPath:"#/properties/title/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema0.properties.title.type,parentSchema:schema0.properties.title,data:data4};if(vErrors === null){vErrors = [err5];}else {vErrors.push(err5);}errors++;}}if(data.description !== undefined){let data5 = data.description;if(typeof data5 !== "string"){const err6 = {instancePath:instancePath+"/description",schemaPath:"#/properties/description/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema0.properties.description.type,parentSchema:schema0.properties.description,data:data5};if(vErrors === null){vErrors = [err6];}else {vErrors.push(err6);}errors++;}}if(data.readOnly !== undefined){let data6 = data.readOnly;if(typeof data6 !== "boolean"){const err7 = {instancePath:instancePath+"/readOnly",schemaPath:"#/properties/readOnly/type",keyword:"type",params:{type: "boolean"},message:"must be boolean",schema:schema0.properties.readOnly.type,parentSchema:schema0.properties.readOnly,data:data6};if(vErrors === null){vErrors = [err7];}else {vErrors.push(err7);}errors++;}}if(data.examples !== undefined){let data7 = data.examples;if(!(Array.isArray(data7))){const err8 = {instancePath:instancePath+"/examples",schemaPath:"#/properties/examples/type",keyword:"type",params:{type: "array"},message:"must be array",schema:schema0.properties.examples.type,parentSchema:schema0.properties.examples,data:data7};if(vErrors === null){vErrors = [err8];}else {vErrors.push(err8);}errors++;}}if(data.multipleOf !== undefined){let data8 = data.multipleOf;if(typeof data8 == "number"){if(data8 <= 0 || isNaN(data8)){const err9 = {instancePath:instancePath+"/multipleOf",schemaPath:"#/properties/multipleOf/exclusiveMinimum",keyword:"exclusiveMinimum",params:{comparison: ">", limit: 0},message:"must be > 0",schema:0,parentSchema:schema0.properties.multipleOf,data:data8};if(vErrors === null){vErrors = [err9];}else {vErrors.push(err9);}errors++;}}else {const err10 = {instancePath:instancePath+"/multipleOf",schemaPath:"#/properties/multipleOf/type",keyword:"type",params:{type: "number"},message:"must be number",schema:schema0.properties.multipleOf.type,parentSchema:schema0.properties.multipleOf,data:data8};if(vErrors === null){vErrors = [err10];}else {vErrors.push(err10);}errors++;}}if(data.maximum !== undefined){let data9 = data.maximum;if(!(typeof data9 == "number")){const err11 = {instancePath:instancePath+"/maximum",schemaPath:"#/properties/maximum/type",keyword:"type",params:{type: "number"},message:"must be number",schema:schema0.properties.maximum.type,parentSchema:schema0.properties.maximum,data:data9};if(vErrors === null){vErrors = [err11];}else {vErrors.push(err11);}errors++;}}if(data.exclusiveMaximum !== undefined){let data10 = data.exclusiveMaximum;if(!(typeof data10 == "number")){const err12 = {instancePath:instancePath+"/exclusiveMaximum",schemaPath:"#/properties/exclusiveMaximum/type",keyword:"type",params:{type: "number"},message:"must be number",schema:schema0.properties.exclusiveMaximum.type,parentSchema:schema0.properties.exclusiveMaximum,data:data10};if(vErrors === null){vErrors = [err12];}else {vErrors.push(err12);}errors++;}}if(data.minimum !== undefined){let data11 = data.minimum;if(!(typeof data11 == "number")){const err13 = {instancePath:instancePath+"/minimum",schemaPath:"#/properties/minimum/type",keyword:"type",params:{type: "number"},message:"must be number",schema:schema0.properties.minimum.type,parentSchema:schema0.properties.minimum,data:data11};if(vErrors === null){vErrors = [err13];}else {vErrors.push(err13);}errors++;}}if(data.exclusiveMinimum !== undefined){let data12 = data.exclusiveMinimum;if(!(typeof data12 == "number")){const err14 = {instancePath:instancePath+"/exclusiveMinimum",schemaPath:"#/properties/exclusiveMinimum/type",keyword:"type",params:{type: "number"},message:"must be number",schema:schema0.properties.exclusiveMinimum.type,parentSchema:schema0.properties.exclusiveMinimum,data:data12};if(vErrors === null){vErrors = [err14];}else {vErrors.push(err14);}errors++;}}if(data.maxLength !== undefined){let data13 = data.maxLength;if(!((typeof data13 == "number") && (!(data13 % 1) && !isNaN(data13)))){const err15 = {instancePath:instancePath+"/maxLength",schemaPath:"#/definitions/nonNegativeInteger/type",keyword:"type",params:{type: "integer"},message:"must be integer",schema:schema1.type,parentSchema:schema1,data:data13};if(vErrors === null){vErrors = [err15];}else {vErrors.push(err15);}errors++;}if(typeof data13 == "number"){if(data13 < 0 || isNaN(data13)){const err16 = {instancePath:instancePath+"/maxLength",schemaPath:"#/definitions/nonNegativeInteger/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0",schema:0,parentSchema:schema1,data:data13};if(vErrors === null){vErrors = [err16];}else {vErrors.push(err16);}errors++;}}}if(data.minLength !== undefined){if(!(validate1(data.minLength, {instancePath:instancePath+"/minLength",parentData:data,parentDataProperty:"minLength",rootData}))){vErrors = vErrors === null ? validate1.errors : vErrors.concat(validate1.errors);errors = vErrors.length;}}if(data.pattern !== undefined){let data15 = data.pattern;if(!(typeof data15 === "string")){const err17 = {instancePath:instancePath+"/pattern",schemaPath:"#/properties/pattern/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema0.properties.pattern.type,parentSchema:schema0.properties.pattern,data:data15};if(vErrors === null){vErrors = [err17];}else {vErrors.push(err17);}errors++;}}if(data.additionalItems !== undefined){if(!(validate0(data.additionalItems, {instancePath:instancePath+"/additionalItems",parentData:data,parentDataProperty:"additionalItems",rootData}))){vErrors = vErrors === null ? validate0.errors : vErrors.concat(validate0.errors);errors = vErrors.length;}}if(data.items !== undefined){let data17 = data.items;const _errs35 = errors;let valid2 = false;const _errs36 = errors;if(!(validate0(data17, {instancePath:instancePath+"/items",parentData:data,parentDataProperty:"items",rootData}))){vErrors = vErrors === null ? validate0.errors : vErrors.concat(validate0.errors);errors = vErrors.length;}var _valid0 = _errs36 === errors;valid2 = valid2 || _valid0;if(!valid2){const _errs37 = errors;if(!(validate3(data17, {instancePath:instancePath+"/items",parentData:data,parentDataProperty:"items",rootData}))){vErrors = vErrors === null ? validate3.errors : vErrors.concat(validate3.errors);errors = vErrors.length;}var _valid0 = _errs37 === errors;valid2 = valid2 || _valid0;}if(!valid2){const err18 = {instancePath:instancePath+"/items",schemaPath:"#/properties/items/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf",schema:schema0.properties.items.anyOf,parentSchema:schema0.properties.items,data:data17};if(vErrors === null){vErrors = [err18];}else {vErrors.push(err18);}errors++;}else {errors = _errs35;if(vErrors !== null){if(_errs35){vErrors.length = _errs35;}else {vErrors = null;}}}}if(data.maxItems !== undefined){let data18 = data.maxItems;if(!((typeof data18 == "number") && (!(data18 % 1) && !isNaN(data18)))){const err19 = {instancePath:instancePath+"/maxItems",schemaPath:"#/definitions/nonNegativeInteger/type",keyword:"type",params:{type: "integer"},message:"must be integer",schema:schema1.type,parentSchema:schema1,data:data18};if(vErrors === null){vErrors = [err19];}else {vErrors.push(err19);}errors++;}if(typeof data18 == "number"){if(data18 < 0 || isNaN(data18)){const err20 = {instancePath:instancePath+"/maxItems",schemaPath:"#/definitions/nonNegativeInteger/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0",schema:0,parentSchema:schema1,data:data18};if(vErrors === null){vErrors = [err20];}else {vErrors.push(err20);}errors++;}}}if(data.minItems !== undefined){if(!(validate1(data.minItems, {instancePath:instancePath+"/minItems",parentData:data,parentDataProperty:"minItems",rootData}))){vErrors = vErrors === null ? validate1.errors : vErrors.concat(validate1.errors);errors = vErrors.length;}}if(data.uniqueItems !== undefined){let data20 = data.uniqueItems;if(typeof data20 !== "boolean"){const err21 = {instancePath:instancePath+"/uniqueItems",schemaPath:"#/properties/uniqueItems/type",keyword:"type",params:{type: "boolean"},message:"must be boolean",schema:schema0.properties.uniqueItems.type,parentSchema:schema0.properties.uniqueItems,data:data20};if(vErrors === null){vErrors = [err21];}else {vErrors.push(err21);}errors++;}}if(data.contains !== undefined){if(!(validate0(data.contains, {instancePath:instancePath+"/contains",parentData:data,parentDataProperty:"contains",rootData}))){vErrors = vErrors === null ? validate0.errors : vErrors.concat(validate0.errors);errors = vErrors.length;}}if(data.maxProperties !== undefined){let data22 = data.maxProperties;if(!((typeof data22 == "number") && (!(data22 % 1) && !isNaN(data22)))){const err22 = {instancePath:instancePath+"/maxProperties",schemaPath:"#/definitions/nonNegativeInteger/type",keyword:"type",params:{type: "integer"},message:"must be integer",schema:schema1.type,parentSchema:schema1,data:data22};if(vErrors === null){vErrors = [err22];}else {vErrors.push(err22);}errors++;}if(typeof data22 == "number"){if(data22 < 0 || isNaN(data22)){const err23 = {instancePath:instancePath+"/maxProperties",schemaPath:"#/definitions/nonNegativeInteger/minimum",keyword:"minimum",params:{comparison: ">=", limit: 0},message:"must be >= 0",schema:0,parentSchema:schema1,data:data22};if(vErrors === null){vErrors = [err23];}else {vErrors.push(err23);}errors++;}}}if(data.minProperties !== undefined){if(!(validate1(data.minProperties, {instancePath:instancePath+"/minProperties",parentData:data,parentDataProperty:"minProperties",rootData}))){vErrors = vErrors === null ? validate1.errors : vErrors.concat(validate1.errors);errors = vErrors.length;}}if(data.required !== undefined){let data24 = data.required;if(Array.isArray(data24)){const len0 = data24.length;for(let i0=0; i0<len0; i0++){let data25 = data24[i0];if(typeof data25 !== "string"){const err24 = {instancePath:instancePath+"/required/" + i0,schemaPath:"#/definitions/stringArray/items/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema7.items.type,parentSchema:schema7.items,data:data25};if(vErrors === null){vErrors = [err24];}else {vErrors.push(err24);}errors++;}}let i1 = data24.length;let j0;if(i1 > 1){const indices0 = {};for(;i1--;){let item0 = data24[i1];if(typeof item0 !== "string"){continue;}if(typeof indices0[item0] == "number"){j0 = indices0[item0];const err25 = {instancePath:instancePath+"/required",schemaPath:"#/definitions/stringArray/uniqueItems",keyword:"uniqueItems",params:{i: i1, j: j0},message:"must NOT have duplicate items (items ## "+j0+" and "+i1+" are identical)",schema:true,parentSchema:schema7,data:data24};if(vErrors === null){vErrors = [err25];}else {vErrors.push(err25);}errors++;break;}indices0[item0] = i1;}}}else {const err26 = {instancePath:instancePath+"/required",schemaPath:"#/definitions/stringArray/type",keyword:"type",params:{type: "array"},message:"must be array",schema:schema7.type,parentSchema:schema7,data:data24};if(vErrors === null){vErrors = [err26];}else {vErrors.push(err26);}errors++;}}if(data.additionalProperties !== undefined){if(!(validate0(data.additionalProperties, {instancePath:instancePath+"/additionalProperties",parentData:data,parentDataProperty:"additionalProperties",rootData}))){vErrors = vErrors === null ? validate0.errors : vErrors.concat(validate0.errors);errors = vErrors.length;}}if(data.definitions !== undefined){let data27 = data.definitions;if(data27 && typeof data27 == "object" && !Array.isArray(data27)){for(const key0 in data27){if(!(validate0(data27[key0], {instancePath:instancePath+"/definitions/" + key0.replace(/~/g, "~0").replace(/\//g, "~1"),parentData:data27,parentDataProperty:key0,rootData}))){vErrors = vErrors === null ? validate0.errors : vErrors.concat(validate0.errors);errors = vErrors.length;}}}else {const err27 = {instancePath:instancePath+"/definitions",schemaPath:"#/properties/definitions/type",keyword:"type",params:{type: "object"},message:"must be object",schema:schema0.properties.definitions.type,parentSchema:schema0.properties.definitions,data:data27};if(vErrors === null){vErrors = [err27];}else {vErrors.push(err27);}errors++;}}if(data.properties !== undefined){let data29 = data.properties;if(data29 && typeof data29 == "object" && !Array.isArray(data29)){for(const key1 in data29){if(!(validate0(data29[key1], {instancePath:instancePath+"/properties/" + key1.replace(/~/g, "~0").replace(/\//g, "~1"),parentData:data29,parentDataProperty:key1,rootData}))){vErrors = vErrors === null ? validate0.errors : vErrors.concat(validate0.errors);errors = vErrors.length;}}}else {const err28 = {instancePath:instancePath+"/properties",schemaPath:"#/properties/properties/type",keyword:"type",params:{type: "object"},message:"must be object",schema:schema0.properties.properties.type,parentSchema:schema0.properties.properties,data:data29};if(vErrors === null){vErrors = [err28];}else {vErrors.push(err28);}errors++;}}if(data.patternProperties !== undefined){let data31 = data.patternProperties;if(data31 && typeof data31 == "object" && !Array.isArray(data31)){for(const key2 in data31){const _errs65 = errors;var valid11 = _errs65 === errors;if(!valid11){const err29 = {instancePath:instancePath+"/patternProperties",schemaPath:"#/properties/patternProperties/propertyNames",keyword:"propertyNames",params:{propertyName: key2},message:"property name must be valid",schema:schema0.properties.patternProperties.propertyNames,parentSchema:schema0.properties.patternProperties,data:data31};if(vErrors === null){vErrors = [err29];}else {vErrors.push(err29);}errors++;}}for(const key3 in data31){if(!(validate0(data31[key3], {instancePath:instancePath+"/patternProperties/" + key3.replace(/~/g, "~0").replace(/\//g, "~1"),parentData:data31,parentDataProperty:key3,rootData}))){vErrors = vErrors === null ? validate0.errors : vErrors.concat(validate0.errors);errors = vErrors.length;}}}else {const err30 = {instancePath:instancePath+"/patternProperties",schemaPath:"#/properties/patternProperties/type",keyword:"type",params:{type: "object"},message:"must be object",schema:schema0.properties.patternProperties.type,parentSchema:schema0.properties.patternProperties,data:data31};if(vErrors === null){vErrors = [err30];}else {vErrors.push(err30);}errors++;}}if(data.dependencies !== undefined){let data33 = data.dependencies;if(data33 && typeof data33 == "object" && !Array.isArray(data33)){for(const key4 in data33){let data34 = data33[key4];const _errs72 = errors;let valid14 = false;const _errs73 = errors;if(!(validate0(data34, {instancePath:instancePath+"/dependencies/" + key4.replace(/~/g, "~0").replace(/\//g, "~1"),parentData:data33,parentDataProperty:key4,rootData}))){vErrors = vErrors === null ? validate0.errors : vErrors.concat(validate0.errors);errors = vErrors.length;}var _valid1 = _errs73 === errors;valid14 = valid14 || _valid1;if(!valid14){const _errs74 = errors;if(Array.isArray(data34)){const len1 = data34.length;for(let i2=0; i2<len1; i2++){let data35 = data34[i2];if(typeof data35 !== "string"){const err31 = {instancePath:instancePath+"/dependencies/" + key4.replace(/~/g, "~0").replace(/\//g, "~1")+"/" + i2,schemaPath:"#/definitions/stringArray/items/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema7.items.type,parentSchema:schema7.items,data:data35};if(vErrors === null){vErrors = [err31];}else {vErrors.push(err31);}errors++;}}let i3 = data34.length;let j1;if(i3 > 1){const indices1 = {};for(;i3--;){let item1 = data34[i3];if(typeof item1 !== "string"){continue;}if(typeof indices1[item1] == "number"){j1 = indices1[item1];const err32 = {instancePath:instancePath+"/dependencies/" + key4.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/definitions/stringArray/uniqueItems",keyword:"uniqueItems",params:{i: i3, j: j1},message:"must NOT have duplicate items (items ## "+j1+" and "+i3+" are identical)",schema:true,parentSchema:schema7,data:data34};if(vErrors === null){vErrors = [err32];}else {vErrors.push(err32);}errors++;break;}indices1[item1] = i3;}}}else {const err33 = {instancePath:instancePath+"/dependencies/" + key4.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/definitions/stringArray/type",keyword:"type",params:{type: "array"},message:"must be array",schema:schema7.type,parentSchema:schema7,data:data34};if(vErrors === null){vErrors = [err33];}else {vErrors.push(err33);}errors++;}var _valid1 = _errs74 === errors;valid14 = valid14 || _valid1;}if(!valid14){const err34 = {instancePath:instancePath+"/dependencies/" + key4.replace(/~/g, "~0").replace(/\//g, "~1"),schemaPath:"#/properties/dependencies/additionalProperties/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf",schema:schema0.properties.dependencies.additionalProperties.anyOf,parentSchema:schema0.properties.dependencies.additionalProperties,data:data34};if(vErrors === null){vErrors = [err34];}else {vErrors.push(err34);}errors++;}else {errors = _errs72;if(vErrors !== null){if(_errs72){vErrors.length = _errs72;}else {vErrors = null;}}}}}else {const err35 = {instancePath:instancePath+"/dependencies",schemaPath:"#/properties/dependencies/type",keyword:"type",params:{type: "object"},message:"must be object",schema:schema0.properties.dependencies.type,parentSchema:schema0.properties.dependencies,data:data33};if(vErrors === null){vErrors = [err35];}else {vErrors.push(err35);}errors++;}}if(data.propertyNames !== undefined){if(!(validate0(data.propertyNames, {instancePath:instancePath+"/propertyNames",parentData:data,parentDataProperty:"propertyNames",rootData}))){vErrors = vErrors === null ? validate0.errors : vErrors.concat(validate0.errors);errors = vErrors.length;}}if(data.enum !== undefined){let data37 = data.enum;if(Array.isArray(data37)){if(data37.length < 1){const err36 = {instancePath:instancePath+"/enum",schemaPath:"#/properties/enum/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items",schema:1,parentSchema:schema0.properties.enum,data:data37};if(vErrors === null){vErrors = [err36];}else {vErrors.push(err36);}errors++;}let i4 = data37.length;let j2;if(i4 > 1){outer0:for(;i4--;){for(j2 = i4; j2--;){if(func0(data37[i4], data37[j2])){const err37 = {instancePath:instancePath+"/enum",schemaPath:"#/properties/enum/uniqueItems",keyword:"uniqueItems",params:{i: i4, j: j2},message:"must NOT have duplicate items (items ## "+j2+" and "+i4+" are identical)",schema:true,parentSchema:schema0.properties.enum,data:data37};if(vErrors === null){vErrors = [err37];}else {vErrors.push(err37);}errors++;break outer0;}}}}}else {const err38 = {instancePath:instancePath+"/enum",schemaPath:"#/properties/enum/type",keyword:"type",params:{type: "array"},message:"must be array",schema:schema0.properties.enum.type,parentSchema:schema0.properties.enum,data:data37};if(vErrors === null){vErrors = [err38];}else {vErrors.push(err38);}errors++;}}if(data.type !== undefined){let data38 = data.type;const _errs83 = errors;let valid20 = false;const _errs84 = errors;if(!(((((((data38 === "array") || (data38 === "boolean")) || (data38 === "integer")) || (data38 === "null")) || (data38 === "number")) || (data38 === "object")) || (data38 === "string"))){const err39 = {instancePath:instancePath+"/type",schemaPath:"#/definitions/simpleTypes/enum",keyword:"enum",params:{allowedValues: schema9.enum},message:"must be equal to one of the allowed values",schema:schema9.enum,parentSchema:schema9,data:data38};if(vErrors === null){vErrors = [err39];}else {vErrors.push(err39);}errors++;}var _valid2 = _errs84 === errors;valid20 = valid20 || _valid2;if(!valid20){const _errs86 = errors;if(Array.isArray(data38)){if(data38.length < 1){const err40 = {instancePath:instancePath+"/type",schemaPath:"#/properties/type/anyOf/1/minItems",keyword:"minItems",params:{limit: 1},message:"must NOT have fewer than 1 items",schema:1,parentSchema:schema0.properties.type.anyOf[1],data:data38};if(vErrors === null){vErrors = [err40];}else {vErrors.push(err40);}errors++;}const len2 = data38.length;for(let i5=0; i5<len2; i5++){let data39 = data38[i5];if(!(((((((data39 === "array") || (data39 === "boolean")) || (data39 === "integer")) || (data39 === "null")) || (data39 === "number")) || (data39 === "object")) || (data39 === "string"))){const err41 = {instancePath:instancePath+"/type/" + i5,schemaPath:"#/definitions/simpleTypes/enum",keyword:"enum",params:{allowedValues: schema9.enum},message:"must be equal to one of the allowed values",schema:schema9.enum,parentSchema:schema9,data:data39};if(vErrors === null){vErrors = [err41];}else {vErrors.push(err41);}errors++;}}let i6 = data38.length;let j3;if(i6 > 1){outer1:for(;i6--;){for(j3 = i6; j3--;){if(func0(data38[i6], data38[j3])){const err42 = {instancePath:instancePath+"/type",schemaPath:"#/properties/type/anyOf/1/uniqueItems",keyword:"uniqueItems",params:{i: i6, j: j3},message:"must NOT have duplicate items (items ## "+j3+" and "+i6+" are identical)",schema:true,parentSchema:schema0.properties.type.anyOf[1],data:data38};if(vErrors === null){vErrors = [err42];}else {vErrors.push(err42);}errors++;break outer1;}}}}}else {const err43 = {instancePath:instancePath+"/type",schemaPath:"#/properties/type/anyOf/1/type",keyword:"type",params:{type: "array"},message:"must be array",schema:schema0.properties.type.anyOf[1].type,parentSchema:schema0.properties.type.anyOf[1],data:data38};if(vErrors === null){vErrors = [err43];}else {vErrors.push(err43);}errors++;}var _valid2 = _errs86 === errors;valid20 = valid20 || _valid2;}if(!valid20){const err44 = {instancePath:instancePath+"/type",schemaPath:"#/properties/type/anyOf",keyword:"anyOf",params:{},message:"must match a schema in anyOf",schema:schema0.properties.type.anyOf,parentSchema:schema0.properties.type,data:data38};if(vErrors === null){vErrors = [err44];}else {vErrors.push(err44);}errors++;}else {errors = _errs83;if(vErrors !== null){if(_errs83){vErrors.length = _errs83;}else {vErrors = null;}}}}if(data.format !== undefined){let data40 = data.format;if(typeof data40 !== "string"){const err45 = {instancePath:instancePath+"/format",schemaPath:"#/properties/format/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema0.properties.format.type,parentSchema:schema0.properties.format,data:data40};if(vErrors === null){vErrors = [err45];}else {vErrors.push(err45);}errors++;}}if(data.contentMediaType !== undefined){let data41 = data.contentMediaType;if(typeof data41 !== "string"){const err46 = {instancePath:instancePath+"/contentMediaType",schemaPath:"#/properties/contentMediaType/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema0.properties.contentMediaType.type,parentSchema:schema0.properties.contentMediaType,data:data41};if(vErrors === null){vErrors = [err46];}else {vErrors.push(err46);}errors++;}}if(data.contentEncoding !== undefined){let data42 = data.contentEncoding;if(typeof data42 !== "string"){const err47 = {instancePath:instancePath+"/contentEncoding",schemaPath:"#/properties/contentEncoding/type",keyword:"type",params:{type: "string"},message:"must be string",schema:schema0.properties.contentEncoding.type,parentSchema:schema0.properties.contentEncoding,data:data42};if(vErrors === null){vErrors = [err47];}else {vErrors.push(err47);}errors++;}}if(data.if !== undefined){if(!(validate0(data.if, {instancePath:instancePath+"/if",parentData:data,parentDataProperty:"if",rootData}))){vErrors = vErrors === null ? validate0.errors : vErrors.concat(validate0.errors);errors = vErrors.length;}}if(data.then !== undefined){if(!(validate0(data.then, {instancePath:instancePath+"/then",parentData:data,parentDataProperty:"then",rootData}))){vErrors = vErrors === null ? validate0.errors : vErrors.concat(validate0.errors);errors = vErrors.length;}}if(data.else !== undefined){if(!(validate0(data.else, {instancePath:instancePath+"/else",parentData:data,parentDataProperty:"else",rootData}))){vErrors = vErrors === null ? validate0.errors : vErrors.concat(validate0.errors);errors = vErrors.length;}}if(data.allOf !== undefined){if(!(validate3(data.allOf, {instancePath:instancePath+"/allOf",parentData:data,parentDataProperty:"allOf",rootData}))){vErrors = vErrors === null ? validate3.errors : vErrors.concat(validate3.errors);errors = vErrors.length;}}if(data.anyOf !== undefined){if(!(validate3(data.anyOf, {instancePath:instancePath+"/anyOf",parentData:data,parentDataProperty:"anyOf",rootData}))){vErrors = vErrors === null ? validate3.errors : vErrors.concat(validate3.errors);errors = vErrors.length;}}if(data.oneOf !== undefined){if(!(validate3(data.oneOf, {instancePath:instancePath+"/oneOf",parentData:data,parentDataProperty:"oneOf",rootData}))){vErrors = vErrors === null ? validate3.errors : vErrors.concat(validate3.errors);errors = vErrors.length;}}if(data.not !== undefined){if(!(validate0(data.not, {instancePath:instancePath+"/not",parentData:data,parentDataProperty:"not",rootData}))){vErrors = vErrors === null ? validate0.errors : vErrors.concat(validate0.errors);errors = vErrors.length;}}}validate0.errors = vErrors;return errors === 0;}
]]>readonly and disabled controls.readOnly: true fields from the JSON Schema are effectively treated as disabled.
This causes a problem for renderer integrations like Vuetify (and likely others). In Vuetify, a disabled field appears gray and inactive, while a readonly field stays visually active but cannot be changed — a much better representation for non-editable but still relevant data.
The distinction between readonly and disabled is important not only for styling but also for semantics and accessibility:
readonly → visible, interactive in appearance, but not editable
disabled → unavailable, excluded from form interaction
Currently, renderers cannot reflect this difference because the core simply exposes an “enabled” flag.
It would be ideal if the core framework exposed both:
whether the control is enabled/disabled, and
whether the control is readonly (based on JSON Schema’s readOnly keyword and others).
That way, all renderers (Vuetify, Material, etc.) could properly represent readonly fields according to their design systems — without each renderer having to reinvent its own detection logic.
This feels like something that should be supported at the core level, since the concept of readOnly comes directly from JSON Schema and others, and should be part of the base control state model.
For backward compatibility we can still make the enabled to be false when the readonly is true but that should be more like an option that can be changed in order to support the new concept.
]]>We typically use an options object in a UISchemaElement to hand in custom properties. For instance, the abstract JsonFormsAbstractControl from the angular package uses this to determine whether the description should always be shown. See here: jsonforms/packages/angular/src/library/abstract-control.ts at b20bc25d8d546077c519f17288b83ea6f2091c6d · eclipsesource/jsonforms · GitHub
For instance, define your Group like this:
{
type: "Group",
scope: "#/properties/args",
options: {
someCustomParam: "some value"
}
}
As your renderer transitively extends JsonFormsBaseRenderer, you can access the UI schema via this.uischema and the options via this.uischema.options. See here for the uischema definition in the base renderer: jsonforms/packages/angular/src/library/base.renderer.ts at b20bc25d8d546077c519f17288b83ea6f2091c6d · eclipsesource/jsonforms · GitHub
Hope that helps and best regards,
Lucas
@Component({
selector: 'GroupLayoutRenderer',
template: `
<div class="card mb-1" [style.display]="hidden ? 'none' : ''">
<div class="card-body px-2 pb-1 pt-{{ label ? 1 : 2 }}">
<label class="card-title ps-1 text-muted small" *ngIf="label">{{ label }}</label>
<div *ngFor="let props of uischema | layoutChildrenRenderProps: schema : path; trackBy: trackElement">
<jsonforms-outlet [renderProps]="props"></jsonforms-outlet>
</div>
</div>
</div>
`,
styles: [``],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class GroupLayoutRenderer extends LayoutRenderer<GroupLayout> {
constructor(jsonFormsService: JsonFormsAngularService, changeDetectionRef: ChangeDetectorRef) {
super(jsonFormsService, changeDetectionRef);
}
}
export const GroupLayoutTester: RankedTester = rankWith(1, uiTypeIs('Group'));
So, I can use a UISchema, like, for example:
{ type: "Group", scope: "#/properties/args", elements: [ ... ]}
What I need is to set a custom value into the UISchema and recover it on the renderer, so I can make this component change based on that custom parameter. For example:
{ type: "Group", someCustomParam: "some value", ... }
I already know how to generate the custom UISchema, the question is: How to get the custom parameter in the Angular custom renderer?
Thanks,
Cheers.
You have a look at the docs regarding re-using existing controls: Custom Renderers - JSON Forms
In any case you have to do the async fetching within the custom part of your code. You can look into reusing the MaterialEnumControl . To use it within your custom renderer’s code, use the unwrapped variant:
import { Unwrapped } from ‘@jsonforms/material-renderers’;
const { MaterialEnumControl } = Unwrapped;
]]>enum or oneOf list for a text by fetching options.
I got a working solution with a custom renderer but using @mui/material
I would be pleased to have some hints about how I could use @jsonform/* Components instead;
There is a working demo on github: GitHub - perki/jsonforms-autocomplete-async-test: JsonForms AutoComplete Async Test
App.tsx see full content here
The Renderer takes an async callback options autoCompleteAsyncCallBack to fetch the data:
const uischema = {
type: 'Control',
scope: '#/properties/api_field',
options: {
autoCompleteAsyncCallBack,
autoCompleteHelperText // optional, render a text when a choice is made
}
};
AutoCompleteAsync.tsx
// AutoCompleteAsyncControl.js
import { useState, useEffect } from 'react';
import { withJsonFormsControlProps } from '@jsonforms/react';
import { Autocomplete , FormControl, TextField } from '@mui/material';
// no direct API import here; options are provided via uischema callback
interface AutoCompleteAsyncProps {
data: any;
handleChange(path: string, value: any): void;
path: string;
schema: JsonSchema;
uischema: UISchemaElement;
}
export type AutoCompleteOption = {
data: any,
const: string,
title: string
}
/**
* @return a textual description of the selected item if any
*/
export type AutoCompleteAsyncCallBack = (query: string) => Promise<AutoCompleteOption[]>;
/**
* @return a textual description of the selected item if any
*/
export type AutoCompleteHelperText = (option: AutoCompleteOption) => string;
const AutoCompleteAsyncControl = ({ data, handleChange, path, schema, uischema }: AutoCompleteAsyncProps) => {
// -- load from usischema config
const autoCompleteAsyncCallBack: AutoCompleteAsyncCallBack = uischema?.options?.autoCompleteAsyncCallBack;
if (autoCompleteAsyncCallBack == null) throw new Error('Missing uischema.options.autoCompleteAsyncCallBack');
const autoCompleteHelperText: AutoCompleteHelperText = uischema?.options?.autoCompleteHelperText || (() => schema.description);
// --
const [options, setOptions] = useState<AutoCompleteOption[]>([]);
const [loading, setLoading] = useState(true);
const [inputValue, setInputValue] = useState( data?.title || '');
// API call on input change
useEffect(() => {
const fetchOptions = async () => {
setLoading(true);
try {
const newOptions = await autoCompleteAsyncCallBack(inputValue);
if (Array.isArray(newOptions)) {
setOptions(newOptions);
} else {
setOptions([]);
}
} catch (error) {
setOptions([]);
console.error('Failed to fetch options:', error);
} finally {
setLoading(false);
}
};
fetchOptions();
}, [inputValue]);
const myOnChange = function (event: any, newValue: AutoCompleteOption | null) {
handleChange(path, newValue);
};
const newInputValue = function (event: any, newInputValue: string) {
if (event == null) return; // avoid change on init
setInputValue(newInputValue);
};
return (
<FormControl fullWidth>
<Autocomplete
options={options}
loading={loading}
forcePopupIcon={false}
onChange={myOnChange}
inputValue={inputValue}
onInputChange={newInputValue}
getOptionLabel={(option) => option?.title ?? ''}
isOptionEqualToValue={(option, value) => option?.const === value?.const}
renderOption={(liProps, option) => (
<li {...liProps} key={option.const}>{option.title}</li>
)}
renderInput={(params) => (
<TextField
{...params}
label={schema.title || path}
helperText={autoCompleteHelperText(data)}
/>
)}
/>
</FormControl>
);
};
// --- Tester
import { rankWith, hasOption, isStringControl, and, UISchemaElement, JsonSchema } from '@jsonforms/core';
// This tester will trigger the custom renderer when a UI schema control has
// a specific custom property, like "render: 'autocomplete-api'".
export const AutocompleteAsyncTester = rankWith(
3, // A higher rank gives this renderer precedence
and(
isStringControl,
hasOption('autoCompleteAsyncCallBack')
)
);
const AutoCompleteAsyncRenderer = withJsonFormsControlProps(AutoCompleteAsyncControl);
export default AutoCompleteAsyncRenderer;
]]>.MuiGrid-container{
row-gap: 20px !important;
height: 100px !important;
}
]]>You are right, currently the text is rendered in the stepper item’s default slot (see code below) and, thus, is not hidden when the mobile flag is set to true. I think we could change the code to render the label in the title slot instead allowing it to be hidden by the mobile flag. If you’d like, you can open a PR for that.
Unfortunately, you cannot currently set the title or subtitle directly via the UI Schema: The UI Schema only allows settings component props but not slots.
Kind regards,
Lucas
]]>this might actually be a (refresh) problem in JSON Forms. Or a problem of the locale being overridden somewhere in JSON Forms.
I can reproduce something similar to this:
I define the dayjs.locale(‘fr’) and it’s work because when i use a DatePicker in the same component i have correct french labels
I tested in the JSON Forms React seed. After configuring dayjs.locale(‘fr’) either once in main.tsx or in the JsonFormsDemo component, the date renderer uses french locale on first open. However, after closing it and opening the picker again, the texts return to English.
The locale for JSON Forms general translation provided via the i18n prop also does not affect this.
Could you open an issue for this at GitHub · Where software is built ?
Thanks and kind regards,
Lucas
]]>it seems that you have only one control pointing to #/properties/affectedPerson that is rendered by your custom renderer that then uses the ui schema given in the detail somehow?
While we don’t have a oneOf Angular renderer so far that you can look at, you could look at the react material oneof renderer as hints on how to render the child properties: jsonforms/packages/material-renderers/src/complex/MaterialOneOfRenderer.tsx at 016f1dff9653ac2e977564cbd6d850593309c551 · eclipsesource/jsonforms · GitHub
Best regards,
Lucas
]]>{
"instancePath": "/states/1",
"schemaPath": "#/properties/states/items/required",
"keyword": "required",
"params": {
"missingProperty": "state_name"
},
"message": " Required",
"schema": [
"state_name"
],
"data": {
"state_properties": {},
"cities": []
},
"child_error_message": "1 - Required"
}
]]>I define the dayjs.locale(‘fr’) and it’s work because when i use a DatePicker in the same component i have correct french labels
Despite all these configurations pointing to French locale, the DatePicker still displays: Month names in English (January, February, etc.) Button labels in English (“Cancel”, “OK” instead of “Annuler”, “OK”) Day abbreviations in English
Is there an additional step I’m missing for Material-UI DatePicker localization within JSONForms? Do I need to import specific Material-UI locale files or configure something else?
Any help would be greatly appreciated!
Environment:
@jsonforms/react
@jsonforms/material-renderers
@mui/x-date-pickers
dayjs
I’d really appreciate any feedback you can share.
Thanks again!
]]>oneOf.But when I select an option, no fields are rendered under the radio, even though the radio changes.
“type”: “object”,
“properties”: {
“affectedPerson”: {
“oneOf”: [
{
“title”: “Natural”,
“type”: “object”,
“properties”: {
“affectedPersonNatural”: {
“type”: “object”,
“properties”: {
“firstName”: { “type”: “string” },
“lastName”: { “type”: “string” }
}
}
},
“required”: [“affectedPersonNatural”]
},
{
“title”: “Legal”,
“type”: “object”,
“properties”: {
“affectedPersonLegal”: {
“type”: “object”,
“properties”: {
“companyName”: { “type”: “string” },
“legalForm”: { “type”: “string” }
}
}
},
“required”: [“affectedPersonLegal”]
}
]
}
}
}
UI Schema
{
“type”: “VerticalLayout”,
“elements”: [
{
“type”: “Control”,
“scope”: “#/properties/affectedPerson”,
“label”: “Affected Person”,
“options”: {
“detail”: {
“type”: “VerticalLayout”,
“elements”: [
{
“type”: “Group”,
“label”: “Natural person”,
“elements”: [
{
“type”: “Control”,
“scope”: “#/properties/affectedPersonNatural/properties/firstName”,
“label”: “First name”
},
{
“type”: “Control”,
“scope”: “#/properties/affectedPersonNatural/properties/lastName”,
“label”: “Last name”``}
]
},
{
“type”: “Group”,
“label”: “Legal person”,
“elements”: [`
{
“type”: “Control”,
“scope”: “#/properties/affectedPersonLegal/properties/companyName”,
“label”: “Company name”
},
{
“type”: “Control”,
“scope”: “#/properties/affectedPersonLegal/properties/legalForm”,
“label”: “Legal form”`
}
]
}
]
}
}
}
]
} ]]>I’ve just started out with JSONForms and I’m trying to create a stepper for a mobile friendly site using the Vuetify render set. The issue that I’m having is with label rendering. I want the stepper labels to disappear when I add the mobile flag to my UI schema.
"uischema":
{
"type": "Categorization",
"elements":
[
{
"type": "Category",
"label": "Age",
"elements":
[
{
"type": "Control",
"scope": "#/properties/age"
}
]
},
...
"options":
{
"variant": "stepper",
"showNavButtons": true,
"vertical": false,
"vuetify": {
"v-stepper": {
"mobile": true
}
}
}
}
Unfortunately when I use the ‘label’ key the text gets rendered as a text node inside the v-stepper-item__content element and doesn’t get hidden by the css rules when the v-stepper--mobile class is added to the stepper container div. Ideally I’d like to populate the VStepperItem component slots for title and subtitle but I’m not clear on how to do this from the uischema?
Any help appreciated!
In any case this works, It is mostly a concern for tests and not tripping our build linters. In the majority of cases the schemas will come in as JSON from the server. It would be nice if there was a documented way to just extend UISchemaElement using declare module, but I haven’t been able to figure out the right way to describe it so that the JSONForms interface accepts the new type. Note I used this for brevity:
export interface AutocompleteControlElement extends Omit<ControlElement, 'type'> {
type: 'AutocompleteControl';
}
This way you only have one import and are a bit shielded from future versions extending Control with other interfaces.
]]>The issue stems from the definition of the UISchemaElement type:
/**
* A union of all available UI schema elements.
* This includes all layout elements, control elements, label elements,
* group elements, category elements and categorization elements.
*/
export type UISchemaElement = BaseUISchemaElement | ControlElement | Layout | LabelElement | GroupLayout | Category | Categorization | VerticalLayout | HorizontalLayout;
This marks your scope as an unknown property.
You should be able to define an interface like this for an autocomplete control.
import { BaseUISchemaElement, Scoped } from '@jsonforms/core';
interface AutocompleteControl extends BaseUISchemaElement, Scoped {
type: 'Autocomplete';
}
You then need to explicitly cast your element to this type. Then it is recognized as a UISchemaElement because BaseUISchemaElement is allowed.
You can also directly cast you element to BaseUISchemaElement.
Each Control element can have arbitrary options like so:
{
type: "Control",
scope: "#/properties/users",
options: {
customAutocomplete: true
}
}
Then, in your tester you can check this:
export const autocompleteJsonFormControlTester = rankWith(
3,
optionIs("customAutocomplete", true)
);
I would recommend solution 2.
]]>