Skip to content

Commit 1e246ee

Browse files
feat(Form): replace with UI5 Web Component (#5925)
BREAKING CHANGE: The `Form` component has been replaced with the `ui5-form` UI5 Web Component, please visit our [Migration Guide](https://sap.github.io/ui5-webcomponents-react/main/?path=/docs/migration-guide--docs) for more details. --------- Co-authored-by: Lukas Harbarth <[email protected]>
1 parent 8348998 commit 1e246ee

27 files changed

+881
-2051
lines changed

.github/workflows/main.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
- uses: preactjs/[email protected]
3232
with:
3333
repo-token: '${{ secrets.GITHUB_TOKEN }}'
34-
pattern: 'packages/**/dist/**/*.js'
34+
pattern: 'packages/**/dist/**/*.{js,css}'
3535
compression: 'gzip'
3636
clean-script: 'clean:remove-modules'
3737

.storybook/preview-head.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@
6363
user-select: none;
6464
}
6565

66+
.formAlignLabelStart::part(label) {
67+
padding-block-start: 0.25rem;
68+
align-self: start;
69+
}
70+
6671
/* TODO remove this workaround as soon as https://github.com/storybookjs/storybook/issues/20497 is fixed */
6772
.docs-story > div > div[scale] {
6873
min-height: 20px;

docs/MigrationGuide.mdx

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,63 @@ function MyComponent() {
7676
}
7777
```
7878

79+
### Form
80+
81+
The `Form` component has been replaced with the `ui5-form` Web Component.
82+
You can use the new `Form` component as a feature complete replacement of the old Form component with the important differences to mention:
83+
84+
1. You can't mix `FormGroup`s and `FormItem`s as children of the Form. Either only use `FormItem`s or only `FormGroup`s with `FormItem`s inside.
85+
2. Additional HTML elements between `Form / FormGroup / FormItem` are not allowed. You can still use custom React components if they render a `FormGroup` or `FormItem` as most outer element (or a fragment).
86+
87+
```tsx
88+
// v1
89+
import { Form, FormGroup, FormItem } from '@ui5/webcomponents-react';
90+
91+
function MyComponent() {
92+
return (
93+
<Form
94+
backgroundDesign="Solid"
95+
titleText="My Form"
96+
labelSpanS={1}
97+
labelSpanM={2}
98+
labelSpanL={3}
99+
labelSpanXL={4}
100+
columnsS={1}
101+
columnsM={2}
102+
columnsL={3}
103+
columnsXL={4}
104+
as={'form'}
105+
>
106+
<FormGroup titleText="My Form Group" as="h5">
107+
<FormItem label={'MyLabel'}>{/* FormItem Content */}</FormItem>
108+
</FormGroup>
109+
</Form>
110+
);
111+
}
112+
113+
// v2
114+
import { Form, FormGroup, FormItem, Label } from '@ui5/webcomponents-react';
115+
116+
function MyComponent() {
117+
return (
118+
// `backgroundDesign` and `as` have been removed without replacement
119+
<Form
120+
// `titleText` has been renamed to `headerText`
121+
headerText="My Form"
122+
// the `columnsX` props have been merged into the `layout` string
123+
layout="S1 M2 L3 XL4"
124+
// the `labelSpanX` props have been merged into the `labelSpan` string
125+
labelSpan="S1 M2 L3 XL4"
126+
>
127+
{/* `titleText` has been renamed to `headerText`, `as` has been removed */}
128+
<FormGroup headerText="My Form Group">
129+
{/* the `label` prop has been renamed to a `labelContent` slot.
130+
It doesn't support strings anymore, it's recommended to use the `Label` component in this slot. */}
131+
<FormItem labelContent={<Label>MyLabel</Label>}>{/* FormItem Content */}</FormItem>
132+
</FormGroup>
133+
</Form>
134+
);
135+
}
136+
```
137+
79138
<Footer />

packages/cli/src/scripts/codemod/transforms/v2/codemodConfig.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,33 @@
115115
},
116116
"FilterItem": {},
117117
"FilterItemOption": {},
118+
"Form": {
119+
"comment": "merge labelSpan and columns props",
120+
"changedProps": {
121+
"titleText": "headerText",
122+
"labelSpanS": "labelSpan",
123+
"labelSpanM": "labelSpan",
124+
"labelSpanL": "labelSpan",
125+
"labelSpanXL": "labelSpan",
126+
"columnsS": "layout",
127+
"columnsM": "layout",
128+
"columnsL": "layout",
129+
"columnsXL": "layout"
130+
},
131+
"removedProps": ["backgroundDesign", "as"]
132+
},
133+
"FormGroup": {
134+
"changedProps": {
135+
"titleText": "headerText"
136+
},
137+
"removedProps": ["as"]
138+
},
139+
"FormItem": {
140+
"comment": "if label is string, convert it to Label Component",
141+
"changedProps": {
142+
"label": "labelContent"
143+
}
144+
},
118145
"GroupHeaderListItem": {
119146
"newComponent": "ListItemGroup"
120147
},

packages/cli/src/scripts/codemod/transforms/v2/main.cts

Lines changed: 122 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { API, Collection, FileInfo, JSCodeshift, Options } from 'jscodeshift';
1+
import type { API, ASTPath, Collection, FileInfo, JSCodeshift, JSXElement, Options } from 'jscodeshift';
22

33
const config = require('./codemodConfig.json');
44

@@ -45,6 +45,32 @@ function addWebComponentsReactImport(j: JSCodeshift, root: Collection, importNam
4545
}
4646
}
4747

48+
function extractValueFromProp(
49+
j: JSCodeshift,
50+
el: ASTPath<JSXElement>,
51+
componentName: string,
52+
propName: string
53+
): string | null {
54+
const prop = j(el).find(j.JSXAttribute, { name: { name: propName } });
55+
56+
if (prop.size()) {
57+
const s = prop.get();
58+
const stringLiteral = prop.find(j.StringLiteral);
59+
const numericLiteral = prop.find(j.NumericLiteral);
60+
prop.remove();
61+
62+
if (stringLiteral.size() > 0) {
63+
return stringLiteral.get().value.value;
64+
} else if (numericLiteral.size() > 0) {
65+
return numericLiteral.get().value.value;
66+
} else {
67+
console.warn(`Unable to read value for prop '${propName}' (${componentName}). Please check the code manually.`);
68+
return null;
69+
}
70+
}
71+
return null;
72+
}
73+
4874
export default function transform(file: FileInfo, api: API, options?: Options): string | undefined {
4975
const j = api.jscodeshift;
5076
const root = j(file.source);
@@ -82,52 +108,21 @@ export default function transform(file: FileInfo, api: API, options?: Options):
82108

83109
if (componentName === 'Carousel') {
84110
jsxElements.forEach((el) => {
85-
const itemsPerPageS = j(el).find(j.JSXAttribute, { name: { name: 'itemsPerPageS' } });
86-
const itemsPerPageM = j(el).find(j.JSXAttribute, { name: { name: 'itemsPerPageM' } });
87-
const itemsPerPageL = j(el).find(j.JSXAttribute, { name: { name: 'itemsPerPageL' } });
88-
89-
const sizeValues: string[] = [];
90-
91-
if (itemsPerPageS.size()) {
92-
const s = itemsPerPageS.get();
93-
const stringLiteral = itemsPerPageS.find(j.StringLiteral);
94-
const numericLiteral = itemsPerPageS.find(j.NumericLiteral);
95-
96-
if (stringLiteral.size() > 0) {
97-
sizeValues.push(`S${stringLiteral.get().value.value}`);
98-
} else if (numericLiteral.size() > 0) {
99-
sizeValues.push(`S${numericLiteral.get().value.value}`);
100-
} else {
101-
console.warn(`Unable to read value for prop 'itemsPerPageS' (Carousel). Please check the code manually.`);
102-
}
103-
}
104-
105-
if (itemsPerPageM.size()) {
106-
const stringLiteral = itemsPerPageM.find(j.StringLiteral);
107-
const numericLiteral = itemsPerPageM.find(j.NumericLiteral);
108-
if (stringLiteral.size() > 0) {
109-
sizeValues.push(`M${stringLiteral.get().value.value}`);
110-
} else if (numericLiteral.size() > 0) {
111-
sizeValues.push(`M${numericLiteral.get().value.value}`);
112-
} else {
113-
console.warn(`Unable to read value for prop 'itemsPerPageM' (Carousel). Please check the code manually.`);
114-
}
115-
}
116-
117-
if (itemsPerPageL.size()) {
118-
const stringLiteral = itemsPerPageL.find(j.StringLiteral);
119-
const numericLiteral = itemsPerPageL.find(j.NumericLiteral);
120-
if (stringLiteral.size() > 0) {
121-
sizeValues.push(`L${stringLiteral.get().value.value}`);
122-
} else if (numericLiteral.size() > 0) {
123-
sizeValues.push(`L${numericLiteral.get().value.value}`);
124-
} else {
125-
console.warn(`Unable to read value for prop 'itemsPerPageL' (Carousel). Please check the code manually.`);
126-
}
127-
}
111+
const sizeValues: string[] = [
112+
['S', 'itemsPerPageS'],
113+
['M', 'itemsPerPageM'],
114+
['L', 'itemsPerPageL']
115+
]
116+
.map(([key, prop]) => {
117+
const val = extractValueFromProp(j, el, componentName, prop);
118+
if (val != null) {
119+
return `${key}${val}`;
120+
}
121+
return '';
122+
})
123+
.filter((val) => val.length > 0);
128124

129125
if (sizeValues.length > 0) {
130-
[itemsPerPageS, itemsPerPageM, itemsPerPageL].forEach((e) => e.remove());
131126
j(el)
132127
.find(j.JSXOpeningElement)
133128
.get()
@@ -139,6 +134,88 @@ export default function transform(file: FileInfo, api: API, options?: Options):
139134
});
140135
}
141136

137+
if (componentName === 'Form') {
138+
jsxElements.forEach((el) => {
139+
const labelSpan: string[] = [
140+
['S', 'labelSpanS'],
141+
['M', 'labelSpanM'],
142+
['L', 'labelSpanL'],
143+
['XL', 'labelSpanXL']
144+
]
145+
.map(([key, prop]) => {
146+
const val = extractValueFromProp(j, el, componentName, prop);
147+
if (val != null) {
148+
return `${key}${val}`;
149+
}
150+
return '';
151+
})
152+
.filter((val) => val.length > 0);
153+
154+
if (labelSpan.length > 0) {
155+
j(el)
156+
.find(j.JSXOpeningElement)
157+
.get()
158+
.value.attributes.push(j.jsxAttribute(j.jsxIdentifier('labelSpan'), j.stringLiteral(labelSpan.join(' '))));
159+
isDirty = true;
160+
}
161+
162+
const layout: string[] = [
163+
['S', 'columnsS'],
164+
['M', 'columnsM'],
165+
['L', 'columnsL'],
166+
['XL', 'columnsXL']
167+
]
168+
.map(([key, prop]) => {
169+
const val = extractValueFromProp(j, el, componentName, prop);
170+
if (val != null) {
171+
return `${key}${val}`;
172+
}
173+
return '';
174+
})
175+
.filter((val) => val.length > 0);
176+
177+
if (layout.length > 0) {
178+
j(el)
179+
.find(j.JSXOpeningElement)
180+
.get()
181+
.value.attributes.push(j.jsxAttribute(j.jsxIdentifier('layout'), j.stringLiteral(layout.join(' '))));
182+
isDirty = true;
183+
}
184+
});
185+
}
186+
187+
if (componentName === 'FormItem') {
188+
jsxElements.forEach((el) => {
189+
const label = j(el).find(j.JSXAttribute, { name: { name: 'label' } });
190+
if (label.size()) {
191+
const labelNode = label.get();
192+
let value: string | undefined;
193+
if (labelNode.value.value.type === 'StringLiteral') {
194+
value = labelNode.value.value.value;
195+
}
196+
if (
197+
labelNode.value.value.type === 'JSXExpressionContainer' &&
198+
labelNode.value.value.expression.type === 'StringLiteral'
199+
) {
200+
value = labelNode.value.value.expression.value;
201+
}
202+
203+
if (value) {
204+
addWebComponentsReactImport(j, root, 'Label');
205+
const labelComponent = j.jsxElement(
206+
j.jsxOpeningElement(j.jsxIdentifier('Label'), [], false),
207+
j.jsxClosingElement(j.jsxIdentifier('Label')),
208+
[j.jsxText(value)]
209+
);
210+
label.replaceWith(
211+
j.jsxAttribute(j.jsxIdentifier('labelContent'), j.jsxExpressionContainer(labelComponent))
212+
);
213+
isDirty = true;
214+
}
215+
}
216+
});
217+
}
218+
142219
if (componentName === 'Icon') {
143220
jsxElements.forEach((el) => {
144221
const interactive = j(el).find(j.JSXAttribute, { name: { name: 'interactive' } });

0 commit comments

Comments
 (0)