Skip to content
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

Implemented MReact. #993

Open
wants to merge 24 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bc5650e
Init
NikitaFil Mar 22, 2022
9f557ac
MReact. Support state changes from React side.
NikitaFil Mar 24, 2022
0d88509
MReact. Support listener props.
NikitaFil Mar 25, 2022
3586f4f
Added customly created component testcase.
NikitaFil Mar 25, 2022
24d2ca8
MReact. Generate set function for each parameter.
NikitaFil Mar 25, 2022
c413742
MReact. Move to separate file.
NikitaFil Mar 25, 2022
f966b86
Cleaning up.
NikitaFil Mar 25, 2022
f173846
Prettify testcase.
NikitaFil Mar 25, 2022
2856a3d
MReact. ReactListener. Support event object as an argument.
NikitaFil Mar 31, 2022
3754d9d
Moved testcase to extra
NikitaFil Apr 1, 2022
e69caf9
Append css from the testcase directly.
NikitaFil Apr 1, 2022
f559086
ReactContainer. Fixed clicks on mobile.
NikitaFil Apr 1, 2022
d285f89
Fixed set* functions initialization on first render.
NikitaFil Apr 1, 2022
5f70301
MReact. Add some comments.
NikitaFil Apr 1, 2022
e572764
Delete console.logs
NikitaFil Apr 1, 2022
46a4e19
Merge branch 'master' into react_support
NikitaFil Apr 1, 2022
26b8464
ReactContainer. Refactored component detection.
NikitaFil Apr 11, 2022
9bc8f6d
Fixed css for external components.
NikitaFil Apr 11, 2022
391102a
Added testcase for AntDesign.
NikitaFil Apr 11, 2022
8280aad
Merge branch 'master' into react_support
NikitaFil Apr 11, 2022
a4cba92
MReact. Added missed unsubscribe.
NikitaFil Apr 12, 2022
128a511
Comments fix
NikitaFil Apr 12, 2022
49ae936
Added more comments
NikitaFil Apr 15, 2022
0353d21
Merge branch 'master' into react_support
NikitaFil Apr 15, 2022
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
94 changes: 94 additions & 0 deletions lib/material/extra/react/material_react.flow
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import material/material2tropic;

export {
// This component imports /js/react_bundle.js file, which should be created from 'react', 'react-dom' and any number of different libraries, bundled together.
// The easiest way to make this bundle by hand is copying content of .js file, distributed by CDN
// (for example, for 'react'/'react-dom' - https://unpkg.com/react/umd/react.production.min.js and https://unpkg.com/react-dom/umd/react-dom.production.min.js)
// If you use your custom .jsx, transpile it it to .js beforehand, using Babel. For example, you can use https://babeljs.io/repl
// Make sure, that library/component is available from global object(window).

// As 'element' you can use
// 1. Simple DOM element (lowercase)
// 2. Name of React-component, exported from library by default (uppercase)
// 3. <Lib>.<Component> format is also supported
// See test_mreact.flow for usage examples.

MReact(
element : string,
props : JsonObject,
state : Tree<string, DynamicBehaviour<Json>>,
listeners : [ReactListener]
) -> Material;

ReactListener(
name : string,
fn : (event : native) -> void // Use getReactEventAttribute to have an access to event's attributes
);
}

MReact(element : string, props : JsonObject, state : Tree<string, DynamicBehaviour<Json>>, listeners : [ReactListener]) -> Material {
wh = makeWH();
TFForm(
FReact(wh, element, props, state, listeners),
TFormMetrics(
fwidth(wh),
fheight(wh),
fheight(wh)
)
)
}

FReact(wh : DynamicBehaviour<WidthHeight>, element : string, props : JsonObject, state : Tree<string, DynamicBehaviour<Json>>, listeners : [ReactListener]) -> FForm {
metrics = make(FormMetrics(0., 0., 0., 0.));
blockResponse = ref false;

FNativeForm(
FEmpty(),
metrics,
\ -> FEmpty(),
\__, __, __ -> {
propsStr = json2string(props);
stateInit = foldTree(state, [], \k, v, acc -> arrayPush(acc, Pair(k, getValue(v))));
stateInitStr = json2string(JsonObject(stateInit));

onStateChange = \str -> {
blockResponse := true;
stateJson = parseJsonSafe(str);
fields = getJsonObjectValue(stateJson, []);
iter(fields, \field -> {
maybeApply(lookupTree(state, field.first), \stateValue -> nextDistinct(stateValue, field.second))
});
blockResponse := false;
}

container = makeReactContainer(element, propsStr, stateInitStr, onStateChange);

uns = makeSubscribe(metrics, \mt -> nextDistinct(wh, WidthHeight(mt.width, mt.height)))();
unsTree = mapTree2(state, \key, val -> {
makeSubscribe2(val, \v -> {
if (!^blockResponse) {
updateReactState(container, key, json2string(v));
}
}
)()
});

iter(listeners, \listener -> setReactListener(container, listener.name, listener.fn));

unsResizeListener = addExtendedEventListener(container, "resize", \met -> {
wd = met[0];
hgt = met[1];
nextDistinct(metrics, FormMetrics(wd, hgt, hgt, hgt));
});

NativeRenderResult(
[container],
\ -> {
uns();
traverseInOrder(unsTree, \__, unsFn -> unsFn());
unsResizeListener();
}
)
}
)
}
99 changes: 99 additions & 0 deletions lib/material/extra/react/test_mreact.flow
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import material/extra/react/material_react;

main() {
setRendererType("html");

content =
[
MText("FLOW TOP TEXT", []),
// Case 1 : Just a DOM-element
makeSimpleParagraph(),
// Case 2 : Static element from a library
makeBootstrapSpinner(),
// Case 3 : Button element from a library, change state from flow, provide an onClick listener
makeBootstrapButton(),
// Case 4 : Customly created ToggleButtonGroupControlled element, based on elements from library. State is changed from both flow and React sides.
makeBootstrapToggleButtonGroup(),
MText("FLOW BOTTOM TEXT", []),
]
|> (\arr -> map(arr, \el -> MBorder4(8., el)))
|> MLines
|> appendBootstrapCss;

mrender(makeMaterialManager([]), true, content);
}

makeSimpleParagraph() -> Material {
element = "p";
props = JsonObject([
Pair("style", JsonObject([
Pair("marginBottom", JsonString("auto"))
])),
Pair("children", JsonString("paragraph text"))
]);

MReact(element, props, makeTree(), []);
}

makeBootstrapSpinner() -> Material {
element = "ReactBootstrap.Spinner";
props = JsonObject([
Pair("animation", JsonString("border"))
]);

MReact(element, props, makeTree(), []);
}

makeBootstrapButton() -> Material {
element = "ReactBootstrap.Button";
props = JsonObject([
Pair("variant", JsonString("primary")),
Pair("children", JsonString("Button"))
]);

isDisabled = make(JsonBool(true));
state = makeTree1("disabled", isDisabled);
timer(3000, \ -> nextDistinct(isDisabled, JsonBool(false)));

MReact(element, props, state, [ReactListener("onClick", \event -> {
println("react onClick listener. screenX = " + getReactEventAttribute(event, "screenX"));
})]);
}

makeBootstrapToggleButtonGroup() -> Material {
element = "ToggleButtonGroupControlled";
props = JsonObject([]);

selected = make(JsonArray([
JsonDouble(1.)
]));

timer(3000, \ -> nextDistinct(selected, JsonArray([
JsonDouble(1.),
JsonDouble(3.)
])));

size = make(JsonString("sm"));
timer(6000, \ -> nextDistinct(size, JsonString("lg")));

state = pairs2tree([
Pair("value", selected),
Pair("size", size)
]);

MReact(element, props, state, []);
}

// For testing purposes : css library is required to be used along with 'react-bootstrap'
appendBootstrapCss(content : Material) -> Material {
isReady = make(false);

link = createElement("link");
setAttribute(link, "rel", "stylesheet", true);
setAttribute(link, "href", "https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css", true);
addEventListener(link, "load", \ -> nextDistinct(isReady, true));
head = getElementBySelector("head");
appendChild(head, link);

MShow(isReady, content)
}
92 changes: 92 additions & 0 deletions lib/material/extra/react/test_mreact_ant.flow
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import material/extra/react/material_react;

// Do not forget to switch to 'js/react_bundle_ant.js' in ReactContainer.hx for this testcase

main() {
setRendererType("html");

content =
[
MText("FLOW TOP TEXT", []),

makeSimpleParagraph(),

makeAntDatePicker(),

makeAntTooltip(),

makeAntButton(),

makeAntInputNumber(),

MText("FLOW BOTTOM TEXT", []),
]
|> (\arr -> map(arr, \el -> MBorder4(8., el)))
|> MLines
|> appendAntCss;

mrender(makeMaterialManager([]), true, content);
}

makeSimpleParagraph() -> Material {
element = "p";
props = JsonObject([
Pair("style", JsonObject([
Pair("marginBottom", JsonString("auto"))
])),
Pair("children", JsonString("paragraph text"))
]);

MReact(element, props, makeTree(), []);
}

makeAntDatePicker() -> Material {
MReact("antd.DatePicker", JsonObject([]), makeTree(), []);
}

makeAntTooltip() -> Material {
MReact("antd.Tooltip", JsonObject([
Pair("title", JsonString("prompt text")),
Pair("children", JsonString("Tooltip will show on mouse enter."))
]), makeTree(), []);
}

makeAntButton() -> Material {
loading = make(JsonBool(true));
state = makeTree1("loading", loading);
timer(5000, \ -> nextDistinct(loading, JsonBool(false)));

MReact("antd.Button", JsonObject([
Pair("type", JsonString("primary")),
Pair("style", JsonObject([
Pair("marginLeft", JsonString("8px"))
])),
Pair("children", JsonString("Button text"))
]), state, [ReactListener("onClick", \event -> {
println("react onClick listener. screenX = " + getReactEventAttribute(event, "screenX"));
})])
}

makeAntInputNumber() -> Material {
MReact("antd.InputNumber", JsonObject([
Pair("min", JsonDouble(1.)),
Pair("max", JsonDouble(10.)),
Pair("defaultValue", JsonDouble(3.)),
]), makeTree(), [ReactListener("onChange", \event -> {
println("onChange Listener");
})])
}

// For testing purposes : css library is required to be used along with 'antd'
appendAntCss(content : Material) -> Material {
isReady = make(false);

link = createElement("link");
setAttribute(link, "rel", "stylesheet", true);
setAttribute(link, "href", "https://cdnjs.cloudflare.com/ajax/libs/antd/4.19.5/antd.compact.min.css", true);
addEventListener(link, "load", \ -> nextDistinct(isReady, true));
head = getElementBySelector("head");
appendChild(head, link);

MShow(isReady, content)
}
13 changes: 13 additions & 0 deletions lib/rendersupport.flow
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,17 @@ export {
native createTextNode : io (text : string) -> native = RenderSupport.createTextNode;
native changeNodeValue : io (textNode : native, text : string) -> void = RenderSupport.changeNodeValue;
native getElementById : io (selector : string) -> native = RenderSupport.getElementById;
native getElementBySelector : io (selector : string) -> native = RenderSupport.getElementBySelector;
native getElementChildren : io (element : native) -> [native] = RenderSupport.getElementChildren;
native getElementNextSibling : io (element : native) -> native = RenderSupport.getElementNextSibling;
native isElementNull : io (element : native) -> bool = RenderSupport.isElementNull;

native makeReactContainer : io (element : string, props : string, state : string, onStateChange : (state : string) -> void) -> native = RenderSupport.makeReactContainer;
native updateReactState : io (container : native, key : string, val : string) -> void = RenderSupport.updateReactState;
native setReactListener : io (container : native, name : string, fn : (event : native) -> void) -> void = RenderSupport.setReactListener;
// returns "" if attribute can't be stringified
native getReactEventAttribute : io (event : native, name : string) -> string = RenderSupport.getReactEventAttribute;

// Set html element attribute
// If "safe" parameter is true calls sanitize from DOMPurify library before applying the attribute
// DOMPurify sanitizes HTML and prevents XSS attacks, so in most cases "safe" parameter should be true
Expand Down Expand Up @@ -483,10 +490,16 @@ createElement(tagName : string) { makeClip() }
createTextNode(text : string) { makeClip() }
changeNodeValue(textNode : native, text : string) {}
getElementById(selector : string) -> native { makeClip() }
getElementBySelector(selector : string) -> native { makeClip() }
getElementChildren(element : native) -> [native] { [] }
getElementNextSibling(element : native) -> native { makeClip() }
isElementNull(element : native) -> bool { true; }

makeReactContainer(element : string, props : string, state : string, onStateChange : (state : string) -> void) { makeClip() }
updateReactState(container : native, key : string, val : string) {}
setReactListener(container : native, name : string, fn : (native) -> void) {}
getReactEventAttribute(event : native, name : string) -> string {""}

setAttribute(element : native, name : string, value : string, safe : bool) {}
removeAttribute(element : native, name : string) {}
reloadPage(forced : bool) {}
Expand Down
28 changes: 19 additions & 9 deletions platforms/js/DisplayObjectHelper.hx
Original file line number Diff line number Diff line change
Expand Up @@ -1907,7 +1907,13 @@ class DisplayObjectHelper {
public static function addNativeWidget(clip : DisplayObject) : Void {
if (untyped clip.addNativeWidget != null) {
untyped clip.addNativeWidget();
} else if (isHTMLRenderer(clip)) {
} else {
addNativeWidgetDefault(clip);
}
}

public static function addNativeWidgetDefault(clip : DisplayObject) : Void {
if (isHTMLRenderer(clip)) {
if (isNativeWidget(clip) && untyped clip.parent != null && clip.visible && (clip.renderable || clip.keepNativeWidgetChildren)) {
if (untyped clip.forceParentNode != null) {
untyped clip.forceParentNode.append(clip.nativeWidget);
Expand All @@ -1927,16 +1933,20 @@ class DisplayObjectHelper {
if (untyped clip.removeNativeWidget != null) {
untyped clip.removeNativeWidget();
} else {
if (untyped isNativeWidget(clip)) {
var nativeWidget : Dynamic = untyped clip.nativeWidget;
removeNativeWidgetDefault(clip);
}
}

if (untyped nativeWidget.parentNode != null) {
nativeWidget.parentNode.removeChild(nativeWidget);
public static function removeNativeWidgetDefault(clip : DisplayObject) : Void {
if (untyped isNativeWidget(clip)) {
var nativeWidget : Dynamic = untyped clip.nativeWidget;

if (untyped clip.parentClip != null) {
applyScrollFn(untyped clip.parentClip);
untyped clip.parentClip = null;
}
if (untyped nativeWidget.parentNode != null) {
nativeWidget.parentNode.removeChild(nativeWidget);

if (untyped clip.parentClip != null) {
applyScrollFn(untyped clip.parentClip);
untyped clip.parentClip = null;
}
}
}
Expand Down
Loading