Skip to content

Commit 9ebde3d

Browse files
author
JelteMX
committed
Rewrite of widget, version 2.0.0
1 parent c547d1c commit 9ebde3d

File tree

10 files changed

+24202
-146
lines changed

10 files changed

+24202
-146
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ A widget that sets attributes on elements, based on your attribute values in you
2525
- The widget will set the following attribute on the selected element(s): `data-custom-attribute="Value"`. This can then be used in styling your application.
2626
- You can manipulate the value before you set it: Remove spaces, make it Uppercase/Lowercase
2727

28+
## Compatibility
29+
30+
Version `2.0.0` was tested (and worked) in the following MX versions:
31+
- 8.0.0
32+
- 8.18.9
33+
- 9.4.0
2834
## Alternatives
2935

3036
The [Mendix AppStore](https://appstore.home.mendix.com/) provides a variety of widgets that do similar things. Most of them do DOM-manipulation on classes, but this is something that should be left to the Mendix Runtime as we're moving to React. The Attribute Helper Widget tries to combine the best of all alternatives, minus setting classes on HTML Elements. Feel free to use an alternative, or request a feature request on this widget.

package-lock.json

Lines changed: 23960 additions & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "attributehelper",
33
"widgetName": "AttributeHelper",
4-
"version": "1.1.0",
4+
"version": "2.0.0",
55
"description": "Set attributes on elements based on your context",
66
"copyright": "Mendix 2019",
77
"author": "Jelte Lagendijk <[email protected]>",
@@ -29,12 +29,14 @@
2929
"@types/jest": "^24.0.0",
3030
"@types/react": "^16.8.17",
3131
"@types/react-dom": "^16.8.4",
32-
"@types/react-test-renderer": "^16.8.1"
32+
"@types/react-test-renderer": "^16.8.1",
33+
"webpack-bundle-analyzer": "^4.4.2"
3334
},
3435
"dependencies": {
3536
"big.js": "^5.2.2",
3637
"cash-dom": "^4.1.5",
3738
"classnames": "^2.2.6",
39+
"mutationobserver-polyfill": "^1.3.0",
3840
"tsdom": "^0.6.9"
3941
}
4042
}

src/AttributeHelper.tsx

Lines changed: 29 additions & 117 deletions
Original file line numberDiff line numberDiff line change
@@ -1,130 +1,42 @@
1-
import { Component, ReactNode, createElement } from "react";
2-
import { findDOMNode } from "react-dom";
3-
import $, { Cash } from "cash-dom";
4-
import { hot } from "react-hot-loader/root";
1+
import "mutationobserver-polyfill";
2+
import { createElement, FC, useRef, useEffect, useCallback } from "react";
3+
import $ from "cash-dom";
4+
5+
import { useMutationObserver, getRefElement } from "./hooks/mutationObserver";
56
import { AttributeHelperContainerProps } from "../typings/AttributeHelperProps";
67

78
import "./ui/AttributeHelper.css";
89

9-
const PROHIBITED_ATTRIBUTES = ["class", "style", "widgetid", "data-mendix-id"];
10-
11-
class AttributeHelper extends Component<AttributeHelperContainerProps> {
12-
domNode: HTMLElement | null = null;
13-
14-
render(): ReactNode {
15-
// We'll render an element, as we are using the dom
16-
return <div className="attributeHelper" />;
17-
}
10+
import { doTransformations } from "./transformer";
1811

19-
componentDidUpdate(): void {
20-
const domNode = findDOMNode(this);
12+
const AttributeHelper: FC<AttributeHelperContainerProps> = props => {
13+
const elRef = useRef<HTMLDivElement>(null);
14+
const bodyRef = getRefElement(document.body);
2115

22-
if (domNode instanceof Element) {
23-
this.domNode = domNode as HTMLElement;
24-
this.doTransformations();
16+
const runTransformer = useCallback(() => {
17+
if (elRef.current) {
18+
const domNode = $(elRef.current);
19+
doTransformations(domNode, props);
2520
}
26-
}
21+
}, [elRef.current, props]);
2722

28-
private doTransformations(): void {
29-
const {
30-
transformations,
31-
selectorSiblingFilter,
32-
selectorSelection,
33-
selectorSiblingSubFilter,
34-
selectorParentsSelector
35-
} = this.props;
36-
if (this.domNode === null) {
37-
return;
23+
const handleMutations = useCallback(() => {
24+
if (props.miscUseMutationObserver) {
25+
runTransformer();
3826
}
39-
const $el = $(this.domNode);
40-
41-
transformations.forEach(transformation => {
42-
const {
43-
transformAttribute,
44-
transformElement,
45-
transformSiblingFilter,
46-
transformTextTemplate,
47-
transformSiblingSubFilter,
48-
transformParentsSelector,
49-
transformRemoveSpaces,
50-
transformTextTransform
51-
} = transformation;
52-
if (transformTextTemplate.status !== "available") {
53-
return;
54-
}
55-
if (PROHIBITED_ATTRIBUTES.indexOf(transformAttribute) !== -1) {
56-
console.warn(`Widget tries to change ${transformAttribute} attribute, this is prohibited`);
57-
return;
58-
}
59-
60-
let value = transformTextTemplate.value;
61-
62-
if (transformRemoveSpaces) {
63-
value = value.replace(/\s/g, "");
64-
}
27+
}, []);
6528

66-
if (transformTextTransform === "lowercase") {
67-
value = value.toLowerCase();
68-
} else if (transformTextTransform === "uppercase") {
69-
value = value.toUpperCase();
70-
}
29+
useMutationObserver({
30+
target: bodyRef,
31+
options: { attributes: true, childList: true },
32+
callback: handleMutations
33+
});
7134

72-
if ((transformElement === "general" && selectorSelection === "parent") || transformElement === "parent") {
73-
const selector = transformElement === "general" ? selectorParentsSelector : transformParentsSelector;
74-
this.handleParent($el, transformAttribute, value, selector);
75-
} else if (
76-
(transformElement === "general" && selectorSelection === "sibling") ||
77-
transformElement === "sibling"
78-
) {
79-
this.handleSiblings(
80-
$el,
81-
transformAttribute,
82-
value,
83-
transformElement === "general" ? selectorSiblingFilter : transformSiblingFilter,
84-
transformElement === "general" ? selectorSiblingSubFilter : transformSiblingSubFilter
85-
);
86-
}
87-
});
88-
}
35+
useEffect(() => {
36+
runTransformer();
37+
}, [elRef.current, props]);
8938

90-
private handleSiblings(
91-
$el: Cash,
92-
attributeName: string,
93-
attributeValue: string,
94-
siblingFilter?: string,
95-
siblingSubFilter?: string
96-
): void {
97-
if (!$el) {
98-
return;
99-
}
100-
const $generalSiblings = $el.siblings(siblingFilter ? siblingFilter : undefined);
101-
if ($generalSiblings.length === 0) {
102-
return;
103-
}
104-
if (typeof siblingSubFilter === "undefined" || siblingSubFilter === "") {
105-
$generalSiblings.attr(attributeName, attributeValue);
106-
} else {
107-
$generalSiblings.each(function() {
108-
$(this)
109-
.find(siblingSubFilter)
110-
.attr(attributeName, attributeValue);
111-
});
112-
}
113-
}
114-
115-
private handleParent($el: Cash, attributeName: string, attributeValue: string, parentSelector?: string): void {
116-
if (!$el) {
117-
return;
118-
}
119-
if (parentSelector) {
120-
const closestParent = $el.closest(parentSelector);
121-
if (closestParent.length === 1) {
122-
closestParent.attr(attributeName, attributeValue);
123-
return;
124-
}
125-
}
126-
$el.parent().attr(attributeName, attributeValue);
127-
}
128-
}
39+
return <div ref={elRef} className={"attributeHelper"} />;
40+
};
12941

130-
export default hot(AttributeHelper);
42+
export default AttributeHelper;

src/AttributeHelper.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,5 +93,11 @@
9393
<description>While using 'Parent' as the Selector, you can add a class name (e.g. '.mx-data-view') as the parent selector. The widget will traverse up, so you can set an attribute on a parent. When it does not find one using the selector, it will use the direct parent.</description>
9494
</property>
9595
</propertyGroup>
96+
<propertyGroup caption="Misc">
97+
<property key="miscUseMutationObserver" type="boolean" defaultValue="false">
98+
<caption>Use Mutation Observer</caption>
99+
<description>This will enable a mutation observer that will trigger everytime the page changes. Please only use this when needed, because it can have a performance impact</description>
100+
</property>
101+
</propertyGroup>
96102
</properties>
97103
</widget>

src/hooks/mutationObserver.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { RefObject, useEffect, useMemo } from "react";
2+
interface Props {
3+
target?: RefObject<Element> | Element | Node | null;
4+
options?: MutationObserverInit;
5+
callback?: MutationCallback;
6+
}
7+
8+
export const getRefElement = <T>(element?: RefObject<Element> | T): Element | T | undefined | null => {
9+
if (element && "current" in element) {
10+
return element.current;
11+
}
12+
13+
return element;
14+
};
15+
16+
export const useMutationObserver = ({ target, options = {}, callback }: Props): void => {
17+
const observer = useMemo(
18+
() =>
19+
new MutationObserver((mutationRecord, mutationObserver) => {
20+
if (callback) {
21+
callback(mutationRecord, mutationObserver);
22+
}
23+
}),
24+
[callback]
25+
);
26+
27+
useEffect(() => {
28+
const element = getRefElement(target);
29+
30+
if (observer && element) {
31+
observer.observe(element, options);
32+
return () => observer.disconnect();
33+
}
34+
}, [target, observer, options]);
35+
};

src/package.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="utf-8" ?>
22
<package xmlns="http://www.mendix.com/package/1.0/">
3-
<clientModule name="AttributeHelper" version="1.1.0" xmlns="http://www.mendix.com/clientModule/1.0/">
3+
<clientModule name="AttributeHelper" version="2.0.0" xmlns="http://www.mendix.com/clientModule/1.0/">
44
<widgetFiles>
55
<widgetFile path="AttributeHelper.xml"/>
66
</widgetFiles>

src/transformer/index.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import $, { Cash } from "cash-dom";
2+
import { AttributeHelperContainerProps } from "../../typings/AttributeHelperProps";
3+
4+
const PROHIBITED_ATTRIBUTES = ["class", "style", "widgetid", "data-mendix-id"];
5+
6+
const handleParent = ($el: Cash, attributeName: string, attributeValue: string, parentSelector?: string): void => {
7+
if (!$el) {
8+
return;
9+
}
10+
if (parentSelector) {
11+
const closestParent = $el.closest(parentSelector);
12+
if (closestParent.length === 1) {
13+
closestParent.attr(attributeName, attributeValue);
14+
return;
15+
}
16+
}
17+
$el.parent().attr(attributeName, attributeValue);
18+
};
19+
20+
const handleSiblings = (
21+
$el: Cash,
22+
attributeName: string,
23+
attributeValue: string,
24+
siblingFilter?: string,
25+
siblingSubFilter?: string
26+
): void => {
27+
if (!$el) {
28+
return;
29+
}
30+
const $generalSiblings = $el.siblings(siblingFilter ? siblingFilter : undefined);
31+
if ($generalSiblings.length === 0) {
32+
return;
33+
}
34+
if (typeof siblingSubFilter === "undefined" || siblingSubFilter === "") {
35+
$generalSiblings.attr(attributeName, attributeValue);
36+
} else {
37+
$generalSiblings.each(function() {
38+
$(this)
39+
.find(siblingSubFilter)
40+
.attr(attributeName, attributeValue);
41+
});
42+
}
43+
};
44+
45+
export const doTransformations = (
46+
$el: Cash,
47+
{
48+
transformations,
49+
selectorSiblingFilter,
50+
selectorSelection,
51+
selectorSiblingSubFilter,
52+
selectorParentsSelector
53+
}: AttributeHelperContainerProps
54+
): void => {
55+
transformations.forEach(transformation => {
56+
const {
57+
transformAttribute,
58+
transformElement,
59+
transformSiblingFilter,
60+
transformTextTemplate,
61+
transformSiblingSubFilter,
62+
transformParentsSelector,
63+
transformRemoveSpaces,
64+
transformTextTransform
65+
} = transformation;
66+
if (transformTextTemplate.status !== "available") {
67+
return;
68+
}
69+
if (PROHIBITED_ATTRIBUTES.indexOf(transformAttribute) !== -1) {
70+
console.warn(`Widget tries to change ${transformAttribute} attribute, this is prohibited`);
71+
return;
72+
}
73+
74+
let value = transformTextTemplate.value;
75+
76+
if (transformRemoveSpaces) {
77+
value = value.replace(/\s/g, "");
78+
}
79+
80+
if (transformTextTransform === "lowercase") {
81+
value = value.toLowerCase();
82+
} else if (transformTextTransform === "uppercase") {
83+
value = value.toUpperCase();
84+
}
85+
86+
if ((transformElement === "general" && selectorSelection === "parent") || transformElement === "parent") {
87+
const selector = transformElement === "general" ? selectorParentsSelector : transformParentsSelector;
88+
handleParent($el, transformAttribute, value, selector);
89+
} else if (
90+
(transformElement === "general" && selectorSelection === "sibling") ||
91+
transformElement === "sibling"
92+
) {
93+
handleSiblings(
94+
$el,
95+
transformAttribute,
96+
value,
97+
transformElement === "general" ? selectorSiblingFilter : transformSiblingFilter,
98+
transformElement === "general" ? selectorSiblingSubFilter : transformSiblingSubFilter
99+
);
100+
}
101+
});
102+
};

typings/AttributeHelperProps.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export interface AttributeHelperContainerProps extends CommonProps {
5959
selectorSiblingFilter?: string;
6060
selectorSiblingSubFilter?: string;
6161
selectorParentsSelector?: string;
62+
miscUseMutationObserver: boolean;
6263
}
6364

6465
export interface AttributeHelperPreviewProps {
@@ -70,6 +71,7 @@ export interface AttributeHelperPreviewProps {
7071
selectorSiblingFilter?: string;
7172
selectorSiblingSubFilter?: string;
7273
selectorParentsSelector?: string;
74+
miscUseMutationObserver: boolean;
7375
}
7476

7577
export interface VisibilityMap {
@@ -78,4 +80,5 @@ export interface VisibilityMap {
7880
selectorSiblingFilter: boolean;
7981
selectorSiblingSubFilter: boolean;
8082
selectorParentsSelector: boolean;
83+
miscUseMutationObserver: boolean;
8184
}

0 commit comments

Comments
 (0)