Skip to content

Commit f7b7cd3

Browse files
author
Illia Obukhau
authored
[WC-1312]: previews for HTML element widget (#30)
2 parents ca54a01 + 09ded62 commit f7b7cd3

File tree

6 files changed

+187
-118
lines changed

6 files changed

+187
-118
lines changed

packages/pluggableWidgets/html-element-web/src/HTMLElement.editorConfig.ts

Lines changed: 69 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,17 @@
11
import { AttributeValueTypeEnum, HTMLElementPreviewProps } from "../typings/HTMLElementProps";
22
import { hideNestedPropertiesIn, hidePropertiesIn, Problem, Properties } from "@mendix/pluggable-widgets-tools";
3+
import {
4+
container,
5+
ContainerProps,
6+
datasource,
7+
dropzone,
8+
StructurePreviewProps,
9+
text
10+
} from "@mendix/pluggable-widgets-commons";
11+
import { isVoidElement, prepareTag } from "./utils/props-utils";
312

413
type TagAttributeValuePropName = keyof HTMLElementPreviewProps["attributes"][number];
514

6-
const voidElements = [
7-
"area",
8-
"base",
9-
"br",
10-
"col",
11-
"embed",
12-
"hr",
13-
"img",
14-
"input",
15-
"link",
16-
"meta",
17-
"source",
18-
"track",
19-
"wbr",
20-
// react specific, it uses `value` prop
21-
"textarea"
22-
];
23-
2415
const disabledElements = ["script"];
2516

2617
function isValidHtmlTagName(name: string): boolean {
@@ -84,7 +75,7 @@ export function getProperties(values: HTMLElementPreviewProps, defaultProperties
8475

8576
const tagName = values.tagName === "__customTag__" ? values.tagNameCustom : values.tagName;
8677

87-
if (voidElements.includes(tagName)) {
78+
if (isVoidElement(tagName)) {
8879
// void elements don't allow children, hide all content props and the content mode switch
8980
propsToHide.push(
9081
"tagContentMode",
@@ -139,47 +130,65 @@ export function check(values: HTMLElementPreviewProps): Problem[] {
139130
}
140131
}
141132

142-
if (values.tagUseRepeat && values.tagContentRepeatDataSource === null) {
143-
// make date source required if set to repeat
144-
errors.push({
145-
severity: "error",
146-
property: "tagContentRepeatDataSource",
147-
message: "Property 'Data source' is required."
148-
});
149-
} else {
150-
const existingAttributeNames = new Set();
151-
values.attributes.forEach((attr, i) => {
152-
if (existingAttributeNames.has(attr.attributeName)) {
153-
errors.push({
154-
severity: "error",
155-
property: `attributes/${i + 1}/attributeName`,
156-
message: `Attribute with name '${attr.attributeName}' already exists.`
157-
});
158-
}
159-
existingAttributeNames.add(attr.attributeName);
160-
161-
const attributePropName = attributeValuePropNameFor(values, attr.attributeValueType);
162-
if (!attr[attributePropName].length) {
163-
errors.push({
164-
severity: "warning",
165-
property: `attributes/${i + 1}/${attributePropName}`,
166-
message: `Value is not specified for attribute '${attr.attributeName}'.`
167-
});
168-
}
169-
});
170-
171-
const existingEventNames = new Set();
172-
values.events.forEach((attr, i) => {
173-
if (existingEventNames.has(attr.eventName)) {
174-
errors.push({
175-
severity: "error",
176-
property: `attributes/${i + 1}/eventName`,
177-
message: `Event with name '${attr.eventName}' already exists.`
178-
});
179-
}
180-
existingEventNames.add(attr.eventName);
181-
});
182-
}
133+
const existingAttributeNames = new Set();
134+
values.attributes.forEach((attr, i) => {
135+
if (existingAttributeNames.has(attr.attributeName)) {
136+
errors.push({
137+
severity: "error",
138+
property: `attributes/${i + 1}/attributeName`,
139+
message: `Attribute with name '${attr.attributeName}' already exists.`
140+
});
141+
}
142+
existingAttributeNames.add(attr.attributeName);
143+
144+
const attributePropName = attributeValuePropNameFor(values, attr.attributeValueType);
145+
if (!attr[attributePropName].length) {
146+
errors.push({
147+
severity: "warning",
148+
property: `attributes/${i + 1}/${attributePropName}`,
149+
message: `Value is not specified for attribute '${attr.attributeName}'.`
150+
});
151+
}
152+
});
153+
154+
const existingEventNames = new Set();
155+
values.events.forEach((attr, i) => {
156+
if (existingEventNames.has(attr.eventName)) {
157+
errors.push({
158+
severity: "error",
159+
property: `attributes/${i + 1}/eventName`,
160+
message: `Event with name '${attr.eventName}' already exists.`
161+
});
162+
}
163+
existingEventNames.add(attr.eventName);
164+
});
183165

184166
return errors;
185167
}
168+
169+
export function getPreview(values: HTMLElementPreviewProps, _isDarkMode: boolean): StructurePreviewProps | null {
170+
const tagName = prepareTag(values.tagName, values.tagNameCustom);
171+
172+
const voidElementPreview = (tagName: keyof JSX.IntrinsicElements): ContainerProps =>
173+
container({ padding: 4 })(text()(`<${tagName} />`));
174+
175+
const flowElementPreview = (): ContainerProps =>
176+
values.tagContentMode === "innerHTML"
177+
? container({ padding: 4 })(
178+
text()(
179+
`<${tagName}>${
180+
values.tagUseRepeat ? values.tagContentRepeatHTML : values.tagContentHTML
181+
}</${tagName}>`
182+
)
183+
)
184+
: container({ padding: 0 })(
185+
text()(`<${tagName}>`),
186+
dropzone(values.tagUseRepeat ? values.tagContentRepeatContainer : values.tagContentContainer),
187+
text()(`</${tagName}>`)
188+
);
189+
190+
return container({ grow: 1, borders: true, borderWidth: 1 })(
191+
values.tagContentRepeatDataSource ? datasource(values.tagContentRepeatDataSource)() : container()(),
192+
isVoidElement(tagName) ? voidElementPreview(tagName) : flowElementPreview()
193+
);
194+
}

packages/pluggableWidgets/html-element-web/src/HTMLElement.editorPreview.tsx

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,41 @@
1-
import { ReactElement, createElement } from "react";
1+
import { ReactElement, createElement, Fragment } from "react";
22
import { HTMLElementPreviewProps } from "../typings/HTMLElementProps";
3+
import { HTMLTag } from "./components/HTMLTag";
4+
import { isVoidElement, prepareTag } from "./utils/props-utils";
35

46
export function preview(props: HTMLElementPreviewProps): ReactElement {
5-
return <div className={props.className}>HTML Element</div>;
7+
console.dir(props, { depth: 4 });
8+
const tag = prepareTag(props.tagName, props.tagNameCustom);
9+
10+
const items = props.tagUseRepeat ? [1, 2, 3] : [1];
11+
12+
return (
13+
<Fragment>
14+
{items.map(i =>
15+
isVoidElement(tag) ? (
16+
createElement(tag, { className: props.className, style: props.styleObject })
17+
) : (
18+
<HTMLTag
19+
key={i}
20+
tagName={tag}
21+
attributes={{
22+
className: props.className,
23+
style: props.styleObject
24+
}}
25+
>
26+
{props.tagContentRepeatHTML}
27+
{props.tagContentHTML}
28+
<props.tagContentRepeatContainer.renderer>
29+
<div />
30+
</props.tagContentRepeatContainer.renderer>
31+
<props.tagContentContainer.renderer>
32+
<div />
33+
</props.tagContentContainer.renderer>
34+
</HTMLTag>
35+
)
36+
)}
37+
</Fragment>
38+
);
639
}
740

841
export function getPreviewCss(): string {

packages/pluggableWidgets/html-element-web/src/HTMLElement.xml

Lines changed: 47 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<icon />
66
<properties>
77
<propertyGroup caption="General">
8-
<propertyGroup caption="Tag">
8+
<propertyGroup caption="HTML Element">
99
<property key="tagName" type="enumeration" defaultValue="div" required="true">
1010
<caption>Tag name</caption>
1111
<description />
@@ -27,67 +27,23 @@
2727
<enumerationValue key="__customTag__">Use custom name</enumerationValue>
2828
</enumerationValues>
2929
</property>
30-
3130
<property key="tagNameCustom" type="string" defaultValue="div" required="false">
3231
<caption>Custom tag</caption>
3332
<description />
3433
</property>
35-
<!-- **************************** TAG ATTRIBUTES ****************************** -->
36-
<property key="attributes" type="object" isList="true" required="false">
37-
<caption>Attributes</caption>
38-
<description />
39-
<properties>
40-
<propertyGroup caption="Attributes">
41-
<property key="attributeName" type="string" required="true">
42-
<caption>Name</caption>
43-
<description />
44-
</property>
45-
46-
<property key="attributeValueType" type="enumeration" defaultValue="expression">
47-
<caption>Value based on</caption>
48-
<description />
49-
<enumerationValues>
50-
<enumerationValue key="expression">Expression</enumerationValue>
51-
<enumerationValue key="template">Text template</enumerationValue>
52-
</enumerationValues>
53-
</property>
5434

55-
<property key="attributeValueTemplate" type="textTemplate" required="false">
56-
<caption>Value</caption>
57-
<description />
58-
</property>
59-
<property key="attributeValueExpression" type="expression" required="false">
60-
<caption>Value</caption>
61-
<description />
62-
<returnType type="String" />
63-
</property>
64-
<property key="attributeValueTemplateRepeat" type="textTemplate" dataSource="../tagContentRepeatDataSource" required="false">
65-
<caption>Value</caption>
66-
<description />
67-
</property>
68-
<property key="attributeValueExpressionRepeat" type="expression" dataSource="../tagContentRepeatDataSource" required="false">
69-
<caption>Value</caption>
70-
<description />
71-
<returnType type="String" />
72-
</property>
73-
</propertyGroup>
74-
</properties>
75-
</property>
76-
<!-- **************************** END TAG ATTRIBUTES ****************************** -->
77-
</propertyGroup>
78-
<propertyGroup caption="Repeat">
7935
<property key="tagUseRepeat" type="boolean" defaultValue="false">
8036
<caption>Repeat element</caption>
8137
<description>Repeat element for each item in data source.</description>
8238
</property>
8339

84-
<!-- Data source for repeating content -->
85-
<property key="tagContentRepeatDataSource" type="datasource" required="false" isList="true">
40+
<property key="tagContentRepeatDataSource" type="datasource" required="true" isList="true">
8641
<caption>Data source</caption>
8742
<description />
8843
</property>
89-
</propertyGroup>
90-
<propertyGroup caption="Content">
44+
45+
<!-- **************************** TAG CONTENT ****************************** -->
46+
<!-- Content mode -->
9147
<property key="tagContentMode" type="enumeration" defaultValue="container">
9248
<caption>Content</caption>
9349
<description />
@@ -97,7 +53,6 @@
9753
</enumerationValues>
9854
</property>
9955

100-
<!-- **************************** TAG CONTENT ****************************** -->
10156
<!-- HTML content, non-repeating -->
10257
<property key="tagContentHTML" type="textTemplate" multiline="true" required="false">
10358
<caption>HTML</caption>
@@ -123,6 +78,48 @@
12378
</property>
12479
<!-- **************************** END TAG CONTENT ****************************** -->
12580
</propertyGroup>
81+
<propertyGroup caption="HTML Attributes">
82+
<property key="attributes" type="object" isList="true" required="false">
83+
<caption>Attributes</caption>
84+
<description>The HTML attributes that are added to the HTML element. For example: ‘title‘, ‘href‘. If ‘class’ or ‘style’ is added as attribute this is merged with the widget class/style property. For events (e.g. onClick) use the Events section.</description>
85+
<properties>
86+
<propertyGroup caption="Attributes">
87+
<property key="attributeName" type="string" required="true">
88+
<caption>Name</caption>
89+
<description />
90+
</property>
91+
92+
<property key="attributeValueType" type="enumeration" defaultValue="expression">
93+
<caption>Value based on</caption>
94+
<description />
95+
<enumerationValues>
96+
<enumerationValue key="expression">Expression</enumerationValue>
97+
<enumerationValue key="template">Text template</enumerationValue>
98+
</enumerationValues>
99+
</property>
100+
101+
<property key="attributeValueTemplate" type="textTemplate" required="false">
102+
<caption>Value</caption>
103+
<description />
104+
</property>
105+
<property key="attributeValueExpression" type="expression" required="false">
106+
<caption>Value</caption>
107+
<description />
108+
<returnType type="String" />
109+
</property>
110+
<property key="attributeValueTemplateRepeat" type="textTemplate" dataSource="../tagContentRepeatDataSource" required="false">
111+
<caption>Value</caption>
112+
<description />
113+
</property>
114+
<property key="attributeValueExpressionRepeat" type="expression" dataSource="../tagContentRepeatDataSource" required="false">
115+
<caption>Value</caption>
116+
<description />
117+
<returnType type="String" />
118+
</property>
119+
</propertyGroup>
120+
</properties>
121+
</property>
122+
</propertyGroup>
126123
</propertyGroup>
127124
<propertyGroup caption="Events">
128125
<property key="events" type="object" isList="true" required="false">

packages/pluggableWidgets/html-element-web/src/utils/props-utils.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,3 +116,26 @@ export function prepareChildren(
116116

117117
return props.tagContentRepeatContainer?.get(item);
118118
}
119+
120+
const voidElements = [
121+
"area",
122+
"base",
123+
"br",
124+
"col",
125+
"embed",
126+
"hr",
127+
"img",
128+
"input",
129+
"link",
130+
"meta",
131+
"source",
132+
"track",
133+
"wbr",
134+
"textarea"
135+
] as const;
136+
137+
export type VoidElement = typeof voidElements[number];
138+
139+
export function isVoidElement(tag: unknown): tag is VoidElement {
140+
return voidElements.includes(tag as VoidElement);
141+
}

packages/pluggableWidgets/html-element-web/src/utils/style-utils.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,18 @@
11
import React from "react";
22

3+
// We need regexp to split rows in prop/value pairs
4+
// split by : not work for all cases, eg "background-image: url(http://localhost:8080);"
5+
const cssPropRegex = /(?<prop>[^:]+):\s+?(?<value>[^;]+);?/m;
6+
37
export function convertInlineCssToReactStyle(inlineStyle: string): React.CSSProperties {
48
return Object.fromEntries(
59
inlineStyle
610
.split(";") // split by ;
711
.filter(r => r.length) // filter out empty
8-
.map(r => r.split(":").map(v => v.trim())) // split by key and value by :
12+
.map(r => {
13+
const { prop = "", value = "" } = cssPropRegex.exec(r.trim())?.groups ?? {};
14+
return [prop, value];
15+
})
916
.filter(v => v.length === 2 && v[0].length && v[1].length) // filter out broken lines
1017
.map(([key, value]) => [convertStylePropNameToReactPropName(key), value] as [string, string])
1118
);

0 commit comments

Comments
 (0)