Skip to content

Commit 9618206

Browse files
author
FalkWolsky
committed
Extending the JSON Schema Forms to alow responsive view in the container
1 parent 1658c18 commit 9618206

File tree

3 files changed

+238
-63
lines changed

3 files changed

+238
-63
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import React from "react";
2+
import ObjectFieldTemplate from "./ObjectFieldTemplate"; // Import the existing ObjectFieldTemplate
3+
4+
export const LayoutFieldTemplate = (props: any) => {
5+
const { schema, uiSchema, children, ...rest } = props; // Spread to include all props
6+
7+
// Handle custom layouts
8+
switch (schema.type) {
9+
case "Group":
10+
return (
11+
<div style={{ border: "1px solid #ccc", padding: "15px", marginBottom: "10px" }}>
12+
<h3>{schema.label || "Group"}</h3>
13+
{children}
14+
</div>
15+
);
16+
case "HorizontalLayout":
17+
return <div style={{ display: "flex", gap: "10px" }}>{children}</div>;
18+
case "VerticalLayout":
19+
return <div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>{children}</div>;
20+
default:
21+
// Delegate to the existing ObjectFieldTemplate, ensuring all props are passed
22+
return <ObjectFieldTemplate schema={schema} uiSchema={uiSchema} {...rest} />;
23+
}
24+
};
25+
26+
export default LayoutFieldTemplate;

client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ObjectFieldTemplate.tsx

Lines changed: 208 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
1-
import React from 'react';
2-
import { Row, Col } from 'antd';
3-
import { ObjectFieldTemplateProps, getTemplate, getUiOptions, descriptionId, titleId, canExpand } from '@rjsf/utils';
4-
import { ConfigConsumer } from 'antd/es/config-provider/context';
1+
import React, { useEffect, useRef, useState } from "react";
2+
import { Row, Col } from "antd";
3+
import {
4+
ObjectFieldTemplateProps,
5+
getTemplate,
6+
getUiOptions,
7+
descriptionId,
8+
titleId,
9+
canExpand,
10+
} from "@rjsf/utils";
11+
import { ConfigConsumer } from "antd/es/config-provider/context";
512

613
const DESCRIPTION_COL_STYLE = {
7-
paddingBottom: '8px',
14+
paddingBottom: "8px",
815
};
916

17+
interface ColSpan {
18+
xs: number;
19+
sm: number;
20+
md: number;
21+
lg: number;
22+
xl: number;
23+
}
24+
25+
interface UiOptions {
26+
colSpan: ColSpan;
27+
rowGutter: number;
28+
// other properties...
29+
}
30+
1031
const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => {
1132
const {
1233
title,
@@ -22,80 +43,204 @@ const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => {
2243
registry,
2344
} = props;
2445

46+
const containerRef = useRef<HTMLDivElement>(null);
47+
const [containerWidth, setContainerWidth] = useState(0);
48+
49+
// Monitor the container's width
50+
useEffect(() => {
51+
const updateWidth = () => {
52+
if (containerRef.current) {
53+
setContainerWidth(containerRef.current.offsetWidth);
54+
}
55+
};
56+
57+
// Create a ResizeObserver to watch for width changes
58+
const resizeObserver = new ResizeObserver(() => {
59+
updateWidth();
60+
});
61+
62+
if (containerRef.current) {
63+
resizeObserver.observe(containerRef.current);
64+
}
65+
66+
// Initial update
67+
updateWidth();
68+
69+
// Cleanup observer on unmount
70+
return () => {
71+
resizeObserver.disconnect();
72+
};
73+
}, []);
74+
2575
const uiOptions = getUiOptions(uiSchema);
26-
const TitleFieldTemplate = getTemplate('TitleFieldTemplate', registry, uiOptions);
27-
const DescriptionFieldTemplate = getTemplate('DescriptionFieldTemplate', registry, uiOptions);
76+
const TitleFieldTemplate = getTemplate("TitleFieldTemplate", registry, uiOptions);
77+
const DescriptionFieldTemplate = getTemplate("DescriptionFieldTemplate", registry, uiOptions);
2878
const {
2979
ButtonTemplates: { AddButton },
3080
} = registry.templates;
3181

32-
// Define responsive column spans based on the ui:props or fallback to defaults
33-
const defaultResponsiveColSpan = {
34-
xs: 24, // Extra small devices
35-
sm: 24, // Small devices
36-
md: 12, // Medium devices
37-
lg: 12, // Large devices
38-
xl: 8, // Extra large devices
82+
const defaultResponsiveColSpan = (width: number) => {
83+
if (width > 1200) return 8; // Wide screens
84+
if (width > 768) return 12; // Tablets
85+
return 24; // Mobile
3986
};
4087

41-
const { rowGutter = 4, colSpan = defaultResponsiveColSpan } = uiSchema?.['ui:props'] || {};
88+
const { rowGutter = 4 } = uiSchema?.["ui:props"] || {};
4289

43-
// Generate responsive colSpan props for each element
44-
const calculateResponsiveColSpan = (element: any) => {
45-
const { type } = element.content.props.schema;
46-
const widget = getUiOptions(element.content.props.uiSchema).widget;
90+
const calculateResponsiveColSpan = (element: any): { span: number } => {
4791

48-
const defaultSpan = widget === 'textarea' || type === 'object' || type === 'array' ? 24 : colSpan;
92+
const uiSchemaProps = getUiOptions(element.content.props.uiSchema)?.["ui:props"] as
93+
| { colSpan?: Record<string, number> | number }
94+
| undefined;
4995

50-
// Ensure the returned object is properly formatted for AntD responsive properties
51-
return typeof defaultSpan === 'object' ? defaultSpan : { span: defaultSpan };
96+
const uiSchemaColSpan = uiSchemaProps?.colSpan;
97+
const defaultSpan = containerWidth > 1200 ? 8 : containerWidth > 768 ? 12 : 24;
98+
99+
if (uiSchemaColSpan) {
100+
if (typeof uiSchemaColSpan === "number") {
101+
return { span: uiSchemaColSpan };
102+
} else if (typeof uiSchemaColSpan === "object") {
103+
if (containerWidth > 1200 && uiSchemaColSpan.xl !== undefined) {
104+
return { span: uiSchemaColSpan.xl };
105+
} else if (containerWidth > 992 && uiSchemaColSpan.lg !== undefined) {
106+
return { span: uiSchemaColSpan.lg };
107+
} else if (containerWidth > 768 && uiSchemaColSpan.md !== undefined) {
108+
return { span: uiSchemaColSpan.md };
109+
} else if (containerWidth > 576 && uiSchemaColSpan.sm !== undefined) {
110+
return { span: uiSchemaColSpan.sm };
111+
} else if (uiSchemaColSpan.xs !== undefined) {
112+
return { span: uiSchemaColSpan.xs };
113+
}
114+
}
115+
}
116+
117+
return { span: defaultSpan };
52118
};
53119

54-
return (
55-
<ConfigConsumer>
56-
{(configProps) => (
57-
<fieldset id={idSchema.$id} className="form-section">
58-
<Row gutter={rowGutter}>
59-
{schema.type === 'object' && title && (
60-
<legend>
61-
<TitleFieldTemplate id={titleId(idSchema)} title={title} required={props.required} schema={schema} uiSchema={uiSchema} registry={registry} />
62-
</legend>
63-
)}
64-
{description && (
65-
<Col span={24} style={DESCRIPTION_COL_STYLE}>
66-
<DescriptionFieldTemplate id={descriptionId(idSchema)} description={description} schema={schema} uiSchema={uiSchema} registry={registry} />
120+
const renderSectionLayout = (properties: any[], uiGrid: any, section: string) => {
121+
122+
if (uiGrid && Array.isArray(uiGrid)) {
123+
return (
124+
<Row gutter={rowGutter} key={section}>
125+
{uiGrid.map((ui_row: Record<string, any>) =>
126+
Object.keys(ui_row).map((row_item) => {
127+
const element = properties.find((p) => p.name === row_item);
128+
if (element) {
129+
const span = calculateResponsiveColSpan(element).span;
130+
return (
131+
<Col key={element.name} span={span}>
132+
{element.content}
133+
</Col>
134+
);
135+
}
136+
return null;
137+
})
138+
)}
139+
</Row>
140+
);
141+
}
142+
143+
// Default layout if no grid is provided
144+
return (
145+
<Row gutter={rowGutter} key={section}>
146+
{properties.map((element) => (
147+
<Col key={element.name} {...calculateResponsiveColSpan(element)}>
148+
{element.content}
149+
</Col>
150+
))}
151+
</Row>
152+
);
153+
};
154+
155+
const renderCustomLayout = () => {
156+
const schemaType = schema.type as string;
157+
switch (schemaType) {
158+
case "Group":
159+
return (
160+
<div style={{ border: "1px solid #ccc", padding: "15px", marginBottom: "10px" }}>
161+
<h3>{schema.label || "Group"}</h3>
162+
{renderSectionLayout(properties, uiSchema?.["ui:grid"], schema.label)}
163+
</div>
164+
);
165+
case "HorizontalLayout":
166+
return (
167+
<Row gutter={rowGutter} style={{ display: "flex", gap: "10px" }}>
168+
{properties.map((element) => (
169+
<Col key={element.name} {...calculateResponsiveColSpan(element)}>
170+
{element.content}
67171
</Col>
68-
)}
69-
{uiSchema?.['ui:grid'] && Array.isArray(uiSchema['ui:grid']) ? (
70-
uiSchema['ui:grid'].map((ui_row: Record<string, any>) => {
71-
return Object.keys(ui_row).map((row_item) => {
72-
const element = properties.find((p) => p.name === row_item);
73-
return element ? (
74-
// Pass responsive colSpan props using the calculated values
75-
<Col key={element.name} {...ui_row[row_item]}>
76-
{element.content}
172+
))}
173+
</Row>
174+
);
175+
case "VerticalLayout":
176+
return (
177+
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
178+
{properties.map((element) => (
179+
<div key={element.name}>{element.content}</div>
180+
))}
181+
</div>
182+
);
183+
default:
184+
return null; // Fall back to default rendering if no match
185+
}
186+
};
187+
188+
// Check if the schema is a custom layout type
189+
const schemaType = schema.type as string; // Extract schema type safely
190+
const isCustomLayout = ["Group", "HorizontalLayout", "VerticalLayout"].includes(schemaType);
191+
192+
return (
193+
<div ref={containerRef}>
194+
<ConfigConsumer>
195+
{(configProps) => (
196+
<fieldset id={idSchema.$id} className="form-section">
197+
{!isCustomLayout && (
198+
<>
199+
{schema.type === "object" && title && (
200+
<legend>
201+
<TitleFieldTemplate
202+
id={titleId(idSchema)}
203+
title={title}
204+
required={props.required}
205+
schema={schema}
206+
uiSchema={uiSchema}
207+
registry={registry}
208+
/>
209+
</legend>
210+
)}
211+
{description && (
212+
<Col span={24} style={DESCRIPTION_COL_STYLE}>
213+
<DescriptionFieldTemplate
214+
id={descriptionId(idSchema)}
215+
description={description}
216+
schema={schema}
217+
uiSchema={uiSchema}
218+
registry={registry}
219+
/>
77220
</Col>
78-
) : null;
79-
});
80-
})
81-
) : (
82-
properties.map((element) => (
83-
<Col key={element.name} {...calculateResponsiveColSpan(element)}>
84-
{element.content}
221+
)}
222+
{renderSectionLayout(properties, uiSchema?.["ui:grid"], "root")}
223+
</>
224+
)}
225+
226+
{isCustomLayout && renderCustomLayout()}
227+
228+
{canExpand(schema, uiSchema, formData) && (
229+
<Row justify="end" style={{ marginTop: "24px" }}>
230+
<Col>
231+
<AddButton
232+
className="object-property-expand"
233+
onClick={onAddClick(schema)}
234+
disabled={disabled || readonly}
235+
registry={registry}
236+
/>
85237
</Col>
86-
))
238+
</Row>
87239
)}
88-
</Row>
89-
{canExpand(schema, uiSchema, formData) && (
90-
<Row justify="end" style={{ marginTop: '24px' }}>
91-
<Col>
92-
<AddButton className="object-property-expand" onClick={onAddClick(schema)} disabled={disabled || readonly} registry={registry} />
93-
</Col>
94-
</Row>
95-
)}
96-
</fieldset>
97-
)}
98-
</ConfigConsumer>
240+
</fieldset>
241+
)}
242+
</ConfigConsumer>
243+
</div>
99244
);
100245
};
101246

client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { useContext, useEffect } from "react";
2626
import { EditorContext } from "comps/editorState";
2727
import ObjectFieldTemplate from './ObjectFieldTemplate';
2828
import ArrayFieldTemplate from './ArrayFieldTemplate';
29+
// import { LayoutFieldTemplate } from "./LayoutFieldTemplate";
2930
import { Select } from 'antd';
3031
import Title from 'antd/es/typography/Title';
3132

@@ -82,6 +83,8 @@ const Container = styled.div<{
8283
.help-block {
8384
margin-bottom: 0px;
8485
}
86+
87+
8588
`;
8689

8790
function convertData(schema?: JSONSchema7, data?: any) {
@@ -262,6 +265,7 @@ let FormBasicComp = (function () {
262265
templates={{
263266
ObjectFieldTemplate: ObjectFieldTemplate,
264267
ArrayFieldTemplate: ArrayFieldTemplate,
268+
// FieldTemplate: LayoutFieldTemplate,
265269
}}
266270
widgets={{ searchableSelect: SearchableSelectWidget }}
267271
// ErrorList={ErrorList}

0 commit comments

Comments
 (0)