Skip to content

feat(Form): replace with UI5 Web Component #5925

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

Merged
merged 7 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ jobs:
- uses: preactjs/[email protected]
with:
repo-token: '${{ secrets.GITHUB_TOKEN }}'
pattern: 'packages/**/dist/**/*.js'
pattern: 'packages/**/dist/**/*.{js,css}'
compression: 'gzip'
clean-script: 'clean:remove-modules'

Expand Down
5 changes: 5 additions & 0 deletions .storybook/preview-head.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@
user-select: none;
}

.formAlignLabelStart::part(label) {
padding-block-start: 0.25rem;
align-self: start;
}

/* TODO remove this workaround as soon as https://github.com/storybookjs/storybook/issues/20497 is fixed */
.docs-story > div > div[scale] {
min-height: 20px;
Expand Down
59 changes: 59 additions & 0 deletions docs/MigrationGuide.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,63 @@ function MyComponent() {
}
```

### Form

The `Form` component has been replaced with the `ui5-form` Web Component.
You can use the new `Form` component as a feature complete replacement of the old Form component with the important differences to mention:

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.
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).

```tsx
// v1
import { Form, FormGroup, FormItem } from '@ui5/webcomponents-react';

function MyComponent() {
return (
<Form
backgroundDesign="Solid"
titleText="My Form"
labelSpanS={1}
labelSpanM={2}
labelSpanL={3}
labelSpanXL={4}
columnsS={1}
columnsM={2}
columnsL={3}
columnsXL={4}
as={'form'}
>
<FormGroup titleText="My Form Group" as="h5">
<FormItem label={'MyLabel'}>{/* FormItem Content */}</FormItem>
</FormGroup>
</Form>
);
}

// v2
import { Form, FormGroup, FormItem, Label } from '@ui5/webcomponents-react';

function MyComponent() {
return (
// `backgroundDesign` and `as` have been removed without replacement
<Form
// `titleText` has been renamed to `headerText`
headerText="My Form"
// the `columnsX` props have been merged into the `layout` string
layout="S1 M2 L3 XL4"
// the `labelSpanX` props have been merged into the `labelSpan` string
labelSpan="S1 M2 L3 XL4"
>
{/* `titleText` has been renamed to `headerText`, `as` has been removed */}
<FormGroup headerText="My Form Group">
{/* the `label` prop has been renamed to a `labelContent` slot.
It doesn't support strings anymore, it's recommended to use the `Label` component in this slot. */}
<FormItem labelContent={<Label>MyLabel</Label>}>{/* FormItem Content */}</FormItem>
</FormGroup>
</Form>
);
}
```

<Footer />
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,33 @@
},
"FilterItem": {},
"FilterItemOption": {},
"Form": {
"comment": "merge labelSpan and columns props",
"changedProps": {
"titleText": "headerText",
"labelSpanS": "labelSpan",
"labelSpanM": "labelSpan",
"labelSpanL": "labelSpan",
"labelSpanXL": "labelSpan",
"columnsS": "layout",
"columnsM": "layout",
"columnsL": "layout",
"columnsXL": "layout"
},
"removedProps": ["backgroundDesign", "as"]
},
"FormGroup": {
"changedProps": {
"titleText": "headerText"
},
"removedProps": ["as"]
},
"FormItem": {
"comment": "if label is string, convert it to Label Component",
"changedProps": {
"label": "labelContent"
}
},
"GroupHeaderListItem": {
"newComponent": "ListItemGroup"
},
Expand Down
167 changes: 122 additions & 45 deletions packages/cli/src/scripts/codemod/transforms/v2/main.cts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { API, Collection, FileInfo, JSCodeshift, Options } from 'jscodeshift';
import type { API, ASTPath, Collection, FileInfo, JSCodeshift, JSXElement, Options } from 'jscodeshift';

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

Expand Down Expand Up @@ -45,6 +45,32 @@ function addWebComponentsReactImport(j: JSCodeshift, root: Collection, importNam
}
}

function extractValueFromProp(
j: JSCodeshift,
el: ASTPath<JSXElement>,
componentName: string,
propName: string
): string | null {
const prop = j(el).find(j.JSXAttribute, { name: { name: propName } });

if (prop.size()) {
const s = prop.get();
const stringLiteral = prop.find(j.StringLiteral);
const numericLiteral = prop.find(j.NumericLiteral);
prop.remove();

if (stringLiteral.size() > 0) {
return stringLiteral.get().value.value;
} else if (numericLiteral.size() > 0) {
return numericLiteral.get().value.value;
} else {
console.warn(`Unable to read value for prop '${propName}' (${componentName}). Please check the code manually.`);
return null;
}
}
return null;
}

export default function transform(file: FileInfo, api: API, options?: Options): string | undefined {
const j = api.jscodeshift;
const root = j(file.source);
Expand Down Expand Up @@ -82,52 +108,21 @@ export default function transform(file: FileInfo, api: API, options?: Options):

if (componentName === 'Carousel') {
jsxElements.forEach((el) => {
const itemsPerPageS = j(el).find(j.JSXAttribute, { name: { name: 'itemsPerPageS' } });
const itemsPerPageM = j(el).find(j.JSXAttribute, { name: { name: 'itemsPerPageM' } });
const itemsPerPageL = j(el).find(j.JSXAttribute, { name: { name: 'itemsPerPageL' } });

const sizeValues: string[] = [];

if (itemsPerPageS.size()) {
const s = itemsPerPageS.get();
const stringLiteral = itemsPerPageS.find(j.StringLiteral);
const numericLiteral = itemsPerPageS.find(j.NumericLiteral);

if (stringLiteral.size() > 0) {
sizeValues.push(`S${stringLiteral.get().value.value}`);
} else if (numericLiteral.size() > 0) {
sizeValues.push(`S${numericLiteral.get().value.value}`);
} else {
console.warn(`Unable to read value for prop 'itemsPerPageS' (Carousel). Please check the code manually.`);
}
}

if (itemsPerPageM.size()) {
const stringLiteral = itemsPerPageM.find(j.StringLiteral);
const numericLiteral = itemsPerPageM.find(j.NumericLiteral);
if (stringLiteral.size() > 0) {
sizeValues.push(`M${stringLiteral.get().value.value}`);
} else if (numericLiteral.size() > 0) {
sizeValues.push(`M${numericLiteral.get().value.value}`);
} else {
console.warn(`Unable to read value for prop 'itemsPerPageM' (Carousel). Please check the code manually.`);
}
}

if (itemsPerPageL.size()) {
const stringLiteral = itemsPerPageL.find(j.StringLiteral);
const numericLiteral = itemsPerPageL.find(j.NumericLiteral);
if (stringLiteral.size() > 0) {
sizeValues.push(`L${stringLiteral.get().value.value}`);
} else if (numericLiteral.size() > 0) {
sizeValues.push(`L${numericLiteral.get().value.value}`);
} else {
console.warn(`Unable to read value for prop 'itemsPerPageL' (Carousel). Please check the code manually.`);
}
}
const sizeValues: string[] = [
['S', 'itemsPerPageS'],
['M', 'itemsPerPageM'],
['L', 'itemsPerPageL']
]
.map(([key, prop]) => {
const val = extractValueFromProp(j, el, componentName, prop);
if (val != null) {
return `${key}${val}`;
}
return '';
})
.filter((val) => val.length > 0);

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

if (componentName === 'Form') {
jsxElements.forEach((el) => {
const labelSpan: string[] = [
['S', 'labelSpanS'],
['M', 'labelSpanM'],
['L', 'labelSpanL'],
['XL', 'labelSpanXL']
]
.map(([key, prop]) => {
const val = extractValueFromProp(j, el, componentName, prop);
if (val != null) {
return `${key}${val}`;
}
return '';
})
.filter((val) => val.length > 0);

if (labelSpan.length > 0) {
j(el)
.find(j.JSXOpeningElement)
.get()
.value.attributes.push(j.jsxAttribute(j.jsxIdentifier('labelSpan'), j.stringLiteral(labelSpan.join(' '))));
isDirty = true;
}

const layout: string[] = [
['S', 'columnsS'],
['M', 'columnsM'],
['L', 'columnsL'],
['XL', 'columnsXL']
]
.map(([key, prop]) => {
const val = extractValueFromProp(j, el, componentName, prop);
if (val != null) {
return `${key}${val}`;
}
return '';
})
.filter((val) => val.length > 0);

if (layout.length > 0) {
j(el)
.find(j.JSXOpeningElement)
.get()
.value.attributes.push(j.jsxAttribute(j.jsxIdentifier('layout'), j.stringLiteral(layout.join(' '))));
isDirty = true;
}
});
}

if (componentName === 'FormItem') {
jsxElements.forEach((el) => {
const label = j(el).find(j.JSXAttribute, { name: { name: 'label' } });
if (label.size()) {
const labelNode = label.get();
let value: string | undefined;
if (labelNode.value.value.type === 'StringLiteral') {
value = labelNode.value.value.value;
}
if (
labelNode.value.value.type === 'JSXExpressionContainer' &&
labelNode.value.value.expression.type === 'StringLiteral'
) {
value = labelNode.value.value.expression.value;
}

if (value) {
addWebComponentsReactImport(j, root, 'Label');
const labelComponent = j.jsxElement(
j.jsxOpeningElement(j.jsxIdentifier('Label'), [], false),
j.jsxClosingElement(j.jsxIdentifier('Label')),
[j.jsxText(value)]
);
label.replaceWith(
j.jsxAttribute(j.jsxIdentifier('labelContent'), j.jsxExpressionContainer(labelComponent))
);
isDirty = true;
}
}
});
}

if (componentName === 'Icon') {
jsxElements.forEach((el) => {
const interactive = j(el).find(j.JSXAttribute, { name: { name: 'interactive' } });
Expand Down
Loading
Loading