diff --git a/pom.xml b/pom.xml index e44b8c72a7..1d1c1a9fe8 100644 --- a/pom.xml +++ b/pom.xml @@ -437,6 +437,10 @@ file="${project.basedir}/node_modules/moment/min/moment.min.js" tofile="src/main/webapp/generated/moment.min.js" /> + ReactElement createElement(ReactComponentType

component, P props); + T extends ReactComponentType

, P extends ReactComponentProps + > ReactElement createElement( + ReactComponentType

componentType, + P props + ); public static native < - P extends ReactComponentProps - > ReactElement createElement( - ReactComponentType

component, + T extends ReactComponentType

, P extends ReactComponentProps + > ReactElement createElement( + ReactComponentType

componentType, P props, - ReactElement... children + ReactElement... children ); public static native T createRef(); @@ -28,9 +31,9 @@ > ReactElement createElement( */ @JsOverlay public static < - P extends ReactComponentProps - > ReactElement createElementWithThemeContext( - ReactComponentType

component, + T extends ReactComponentType

, P extends ReactComponentProps + > ReactElement createElementWithThemeContext( + ReactComponentType

componentType, P props ) { SynapseReactClientFullContextProviderProps emptyContext = @@ -38,7 +41,7 @@ > ReactElement createElementWithThemeContext( SynapseContextJsObject.create(null, false, false), null ); - return createElementWithSynapseContext(component, props, emptyContext); + return createElementWithSynapseContext(componentType, props, emptyContext); } /** @@ -46,7 +49,7 @@ > ReactElement createElementWithThemeContext( * simplifies creating the wrapper. * * For setting props, use {@link SynapseReactClientFullContextPropsProvider} - * @param component + * @param componentType * @param props * @param wrapperProps * @param

@@ -54,13 +57,13 @@ > ReactElement createElementWithThemeContext( */ @JsOverlay public static < - P extends ReactComponentProps - > ReactElement createElementWithSynapseContext( - ReactComponentType

component, + T extends ReactComponentType

, P extends ReactComponentProps + > ReactElement createElementWithSynapseContext( + T componentType, P props, SynapseReactClientFullContextProviderProps wrapperProps ) { - ReactElement componentElement = createElement(component, props); + ReactElement componentElement = createElement(componentType, props); return createElement( SRC.SynapseContext.FullContextProvider, wrapperProps, diff --git a/src/main/java/org/sagebionetworks/web/client/jsinterop/ReactComponentProps.java b/src/main/java/org/sagebionetworks/web/client/jsinterop/ReactComponentProps.java index 33c001237a..84832af49a 100644 --- a/src/main/java/org/sagebionetworks/web/client/jsinterop/ReactComponentProps.java +++ b/src/main/java/org/sagebionetworks/web/client/jsinterop/ReactComponentProps.java @@ -3,7 +3,6 @@ import com.google.gwt.dom.client.Element; import jsinterop.annotations.JsConstructor; import jsinterop.annotations.JsFunction; -import jsinterop.annotations.JsOverlay; import jsinterop.annotations.JsPackage; import jsinterop.annotations.JsType; @@ -18,29 +17,6 @@ public interface CallbackRef { void run(Element element); } - public JsArray> children; - // Either a ComponentRef or CallbackRef may be passed. A CallbackRef will be invoked when the ref is set. public Object ref; - - @JsOverlay - public final void addChild(ReactElement child) { - if (children == null) { - children = new JsArray<>(); - } - children.push(child); - } - - @JsOverlay - public final void clearChildren() { - children = new JsArray<>(); - } - - @JsOverlay - public final JsArray> getChildren() { - if (children == null) { - children = new JsArray<>(); - } - return children; - } } diff --git a/src/main/java/org/sagebionetworks/web/client/jsinterop/ReactElement.java b/src/main/java/org/sagebionetworks/web/client/jsinterop/ReactElement.java index ed9e6aa197..fbe486bcb1 100644 --- a/src/main/java/org/sagebionetworks/web/client/jsinterop/ReactElement.java +++ b/src/main/java/org/sagebionetworks/web/client/jsinterop/ReactElement.java @@ -5,11 +5,15 @@ import jsinterop.annotations.JsType; @JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object") -public class ReactElement { +public class ReactElement< + T extends ReactComponentType

, P extends ReactComponentProps +> { + + public T type; @JsNullable - public T props; + public P props; @JsNullable - public ComponentRef ref; + public String key; } diff --git a/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/Grid.java b/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/Grid.java new file mode 100644 index 0000000000..b8848a3e02 --- /dev/null +++ b/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/Grid.java @@ -0,0 +1,91 @@ +package org.sagebionetworks.web.client.jsinterop.mui; + +import org.sagebionetworks.web.client.jsinterop.ReactComponentType; +import org.sagebionetworks.web.client.jsinterop.react.HasStyle; + +public class Grid extends HasStyle, GridProps> { + + public Grid() { + super(MaterialUI.Unstable_Grid2, GridProps.create(false)); + } + + public void setId(String id) { + props.id = id; + this.render(); + } + + public void setContainer(boolean container) { + props.container = container; + this.render(); + } + + public void setXs(int xs) { + props.xs = xs; + this.render(); + } + + public void setSm(int sm) { + props.sm = sm; + this.render(); + } + + public void setMd(int md) { + props.md = md; + this.render(); + } + + public void setLg(int lg) { + props.lg = lg; + this.render(); + } + + public void setXl(int xl) { + props.xl = xl; + this.render(); + } + + public void setXsOffset(int xsOffset) { + props.xsOffset = xsOffset; + this.render(); + } + + public void setSmOffset(int smOffset) { + props.smOffset = smOffset; + this.render(); + } + + public void setMdOffset(int mdOffset) { + props.mdOffset = mdOffset; + this.render(); + } + + public void setLgOffset(int lgOffset) { + props.lgOffset = lgOffset; + this.render(); + } + + public void setXlOffset(int xlOffset) { + props.xlOffset = xlOffset; + this.render(); + } + + public void setMt(String mt) { + props.mt = mt; + this.render(); + } + + public void setPl(String pl) { + props.pl = pl; + this.render(); + } + + public void setRowSpacing(String rowSpacing) { + props.rowSpacing = rowSpacing; + this.render(); + } + + public void setColumnSpacing(String columnSpacing) { + props.columnSpacing = columnSpacing; + this.render(); + } +} diff --git a/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/GridProps.java b/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/GridProps.java new file mode 100644 index 0000000000..e374a3cd3a --- /dev/null +++ b/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/GridProps.java @@ -0,0 +1,68 @@ +package org.sagebionetworks.web.client.jsinterop.mui; + +import jsinterop.annotations.JsNullable; +import jsinterop.annotations.JsOverlay; +import jsinterop.annotations.JsPackage; +import jsinterop.annotations.JsType; +import org.sagebionetworks.web.client.jsinterop.PropsWithStyle; + +@JsType(isNative = true, namespace = JsPackage.GLOBAL, name = "Object") +public class GridProps extends PropsWithStyle { + + @JsNullable + String id; + + boolean container; + + @JsNullable + int xs; + + @JsNullable + int sm; + + @JsNullable + int md; + + @JsNullable + int lg; + + @JsNullable + int xl; + + @JsNullable + int xsOffset; + + @JsNullable + int smOffset; + + @JsNullable + int mdOffset; + + @JsNullable + int lgOffset; + + @JsNullable + int xlOffset; + + @JsNullable + String mt; + + @JsNullable + String pl; + + @JsNullable + String rowSpacing; + + @JsNullable + String columnSpacing; + + @JsOverlay + public static GridProps create(boolean container) { + GridProps props = new GridProps(); + + if (container) { + props.container = true; + } + return props; + } +} diff --git a/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/MaterialUI.java b/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/MaterialUI.java new file mode 100644 index 0000000000..8167a7958a --- /dev/null +++ b/src/main/java/org/sagebionetworks/web/client/jsinterop/mui/MaterialUI.java @@ -0,0 +1,11 @@ +package org.sagebionetworks.web.client.jsinterop.mui; + +import jsinterop.annotations.JsPackage; +import jsinterop.annotations.JsType; +import org.sagebionetworks.web.client.jsinterop.ReactComponentType; + +@JsType(isNative = true, namespace = JsPackage.GLOBAL) +public class MaterialUI { + + public static ReactComponentType Unstable_Grid2; +} diff --git a/src/main/java/org/sagebionetworks/web/client/jsinterop/react/HasStyle.java b/src/main/java/org/sagebionetworks/web/client/jsinterop/react/HasStyle.java index a4b1db39f4..a7d5bd8f85 100644 --- a/src/main/java/org/sagebionetworks/web/client/jsinterop/react/HasStyle.java +++ b/src/main/java/org/sagebionetworks/web/client/jsinterop/react/HasStyle.java @@ -3,18 +3,21 @@ import jsinterop.base.JsPropertyMap; import org.sagebionetworks.web.client.jsinterop.JsObject; import org.sagebionetworks.web.client.jsinterop.PropsWithStyle; -import org.sagebionetworks.web.client.widget.ReactComponent; +import org.sagebionetworks.web.client.jsinterop.ReactComponentType; +import org.sagebionetworks.web.client.widget.ReactComponentV2; /** * Abstract class for React component widgets that have a style prop. The style prop for the component may be manipulated * to show/hide the component based on the current state of the widget. * @param the prop type. */ -public abstract class HasStyle - extends ReactComponent { +public abstract class HasStyle< + T extends ReactComponentType

, P extends PropsWithStyle +> + extends ReactComponentV2 { - public HasStyle() { - super(); + public HasStyle(T reactComponentType, P props) { + super(reactComponentType, props); } public void setStyle(JsPropertyMap style) { @@ -33,7 +36,11 @@ public void setVisible(boolean visible) { } else { // Update the style prop to `display: none`. if (this.props == null) { - this.props = (T) JsPropertyMap.of(); + this.props = (P) JsPropertyMap.of(); + } + + if (this.props.style == null) { + this.props.style = (JsPropertyMap) new JsObject(); } this.props.style.set("display", "none"); diff --git a/src/main/java/org/sagebionetworks/web/client/widget/ReactComponent.java b/src/main/java/org/sagebionetworks/web/client/widget/ReactComponent.java index 32f37cc582..ff28cc3509 100644 --- a/src/main/java/org/sagebionetworks/web/client/widget/ReactComponent.java +++ b/src/main/java/org/sagebionetworks/web/client/widget/ReactComponent.java @@ -1,187 +1,66 @@ package org.sagebionetworks.web.client.widget; import com.google.gwt.dom.client.DivElement; -import com.google.gwt.dom.client.Document; -import com.google.gwt.dom.client.Element; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.HasClickHandlers; import com.google.gwt.event.shared.HandlerRegistration; import com.google.gwt.user.client.Timer; -import com.google.gwt.user.client.ui.ComplexPanel; -import com.google.gwt.user.client.ui.Widget; -import java.util.ArrayList; -import java.util.List; -import jsinterop.base.JsPropertyMap; -import org.sagebionetworks.web.client.jsinterop.React; -import org.sagebionetworks.web.client.jsinterop.ReactComponentProps; -import org.sagebionetworks.web.client.jsinterop.ReactComponentType; +import com.google.gwt.user.client.ui.FlowPanel; import org.sagebionetworks.web.client.jsinterop.ReactDOM; import org.sagebionetworks.web.client.jsinterop.ReactDOMRoot; import org.sagebionetworks.web.client.jsinterop.ReactElement; /** - * Widget that manages the lifecycle of a React component. To use this widget, create a {@link ReactElement} using the - * {@link React} API and call {@link #render(ReactElement)} to render the React component. - *

- * This widget also manages appending child elements if the associated React component can contain children. If all - * child widgets use this class, then the child {@link ReactElement}s will be cloned and passed as children to the React - * component. If any child of this component is a non-ReactComponent widget, then the child widgets will be injected - * into the node found using the component's `ref`. - *

- * The root element defaults to a `div`, but can be changed (e.g. to a `span`) using the {@link #ReactComponent(String)} constructor. - *

- * This widget automatically unmounts the ReactComponent (if any) when this container is detached/unloaded. + * Automatically unmounts the ReactComponent (if any) inside this div when this container is detached/unloaded. */ -public class ReactComponent - extends ComplexPanel - implements HasClickHandlers { +public class ReactComponent extends FlowPanel implements HasClickHandlers { private ReactDOMRoot root; - private ReactComponentType reactComponentType; - public T props; - - private ReactElement reactElement; + private ReactElement reactElement; public ReactComponent() { - this(DivElement.TAG); + super(DivElement.TAG); } public ReactComponent(String tag) { - setElement(Document.get().createElement(tag)); - } - - private boolean allChildrenAreReactComponents() { - boolean allChildrenAreReactComponents = getChildren().size() > 0; - for (Widget w : getChildren()) { - if (!(w instanceof ReactComponent)) { - allChildrenAreReactComponents = false; - break; - } - } - return allChildrenAreReactComponents; - } - - private boolean isRenderedAsReactComponentChild() { - return ( - getParent() instanceof ReactComponent && - ((ReactComponent) getParent()).allChildrenAreReactComponents() - ); - } - - /** - * This method returns the root ReactComponent widget, which is the only place where this React tree is attached to the DOM. - */ - private ReactComponent getRootReactComponentWidget() { - if (isRenderedAsReactComponentChild()) { - return ((ReactComponent) getParent()).getRootReactComponentWidget(); - } else { - return this; - } - } - - @Override - public HandlerRegistration addClickHandler(ClickHandler handler) { - return addDomHandler(handler, ClickEvent.getType()); + super(tag); } - private void maybeCreateRoot() { - if (root == null && !isRenderedAsReactComponentChild()) { + private void createRoot() { + if (root == null) { root = ReactDOM.createRoot(this.getElement()); } } - /** - * Override the current props of the React component. - * Because re-rendering the component will use `React.cloneElement`, old props must be explicitly set to `undefined` - * to remove them. - */ - public void overrideProps(T props) { - this.props = props; - this.rerender(); - } - - /** - * Injects the GWT children into the React component. If all children are ReactComponents, - * they will be cloned and added as React children. - */ - private void injectChildWidgetsIntoComponent() { - if (this.allChildrenAreReactComponents()) { - // If all widget children are ReactElements, clone the React component and add them as children - List> childWidgets = new ArrayList<>(); - getChildren().forEach(w -> childWidgets.add(((ReactComponent) w))); - - ReactElement[] childReactElements = childWidgets - .stream() - .map(ReactComponent::getReactElement) - .toArray(ReactElement[]::new); - - this.reactElement = - React.cloneElement(reactElement, this.props, childReactElements); - } else if (getChildren().size() > 0) { - // Create a callback ref that will allow us to inject the GWT children into the DOM - ReactComponentProps.CallbackRef refCallback = (Element node) -> { - if (node != null) { - // Once the DOM node is defined, inject each child - getChildren().forEach(w -> node.appendChild(w.getElement())); - } - }; - - if (this.props == null) { - this.props = (T) JsPropertyMap.of(); - } - this.props.ref = refCallback; - - this.reactElement = - React.cloneElement( - reactElement, - // Override the ref - this.props - ); - } - } - - public void render(ReactElement reactElement) { + public void render(ReactElement reactElement) { this.reactElement = reactElement; - injectChildWidgetsIntoComponent(); // This component may be a React child of another component. If so, we must rerender the ancestor component(s) so // that they use the new ReactElement created in this render step. - ReactComponent componentToRender = getRootReactComponentWidget(); - if (componentToRender == this) { - // Asynchronously schedule creating a root in case React is still rendering and may unmount the current root - Timer t = new Timer() { - @Override - public void run() { - maybeCreateRoot(); - // Resynchronize with the DOM - root.render(reactElement); - } - }; - t.schedule(0); - } else { - // Walk up the tree to the parent and repeat rerendering - componentToRender.rerender(); - } - } - - @Override - public void setVisible(boolean visible) { - super.setVisible(visible); - // Re-render the element - this.rerender(); + // Asynchronously schedule creating a root in case React is still rendering and may unmount the current root + Timer t = new Timer() { + @Override + public void run() { + createRoot(); + // Resynchronize with the DOM + root.render(reactElement); + } + }; + t.schedule(0); } @Override protected void onLoad() { super.onLoad(); - maybeCreateRoot(); - this.rerender(); + createRoot(); + if (reactElement != null) { + this.render(reactElement); + } } @Override protected void onUnload() { - super.onUnload(); if (root != null) { // Asynchronously schedule unmounting the root to allow React to finish the current render cycle. // https://github.com/facebook/react/issues/25675 @@ -194,66 +73,18 @@ public void run() { }; t.schedule(0); } + super.onUnload(); } - /** - * Adds a child widget. - * - * @param child the widget to be added - * @throws UnsupportedOperationException if this method is not supported (most - * often this means that a specific overload must be called) - */ @Override - public void add(Widget child) { - // See implementation in com.google.gwt.user.client.ui.ComplexPanel - - // Detach new child - child.removeFromParent(); - - // Logical attach - getChildren().add(child); - - // Physical attach (via React API!) - if (reactElement != null) { - // Rerender if possible - this.render(reactElement); - } - - // Adopt. - adopt(child); + public void clear() { + // clear doesn't typically call onUnload, but we want to for this element. + this.onUnload(); + super.clear(); } @Override - public boolean remove(Widget w) { - // See implementation in ComplexPanel - - // Validate. - if (w.getParent() != this) { - return false; - } - // Orphan. - try { - orphan(w); - } finally { - // Note - compared to ComplexPanel, we flipped logical and physical detach - // This is because our render implementation depends on logical attachment - - // Logical detach. - getChildren().remove(w); - - // Physical detach (via React API!) - this.rerender(); - } - return true; - } - - public ReactElement getReactElement() { - return reactElement; - } - - public void rerender() { - if (reactElement != null) { - this.render(reactElement); - } + public HandlerRegistration addClickHandler(ClickHandler handler) { + return addDomHandler(handler, ClickEvent.getType()); } } diff --git a/src/main/java/org/sagebionetworks/web/client/widget/ReactComponentV2.java b/src/main/java/org/sagebionetworks/web/client/widget/ReactComponentV2.java new file mode 100644 index 0000000000..2d3ec3cc66 --- /dev/null +++ b/src/main/java/org/sagebionetworks/web/client/widget/ReactComponentV2.java @@ -0,0 +1,280 @@ +package org.sagebionetworks.web.client.widget; + +import com.google.gwt.dom.client.DivElement; +import com.google.gwt.dom.client.Document; +import com.google.gwt.dom.client.Element; +import com.google.gwt.event.dom.client.ClickEvent; +import com.google.gwt.event.dom.client.ClickHandler; +import com.google.gwt.event.dom.client.HasClickHandlers; +import com.google.gwt.event.shared.HandlerRegistration; +import com.google.gwt.user.client.Timer; +import com.google.gwt.user.client.ui.ComplexPanel; +import com.google.gwt.user.client.ui.Widget; +import java.util.ArrayList; +import java.util.List; +import jsinterop.base.JsPropertyMap; +import org.sagebionetworks.web.client.jsinterop.React; +import org.sagebionetworks.web.client.jsinterop.ReactComponentProps; +import org.sagebionetworks.web.client.jsinterop.ReactComponentType; +import org.sagebionetworks.web.client.jsinterop.ReactDOM; +import org.sagebionetworks.web.client.jsinterop.ReactDOMRoot; +import org.sagebionetworks.web.client.jsinterop.ReactElement; + +/** + * Abstract widget that manages the lifecycle of a {@link React} component tree mounted with {@link ReactDOM}. + *

+ * To use it, extend the class, supply a {@link ReactComponentType} and {@link ReactComponentProps} to the constructor, + * and call {@link #render()} to render the React component. + *

+ * This widget also manages appending child elements if the associated React component can contain children. If all + * child widgets implement this class, then the child {@link ReactElement}s will be passed as children to the React + * component. If any child of this component is a non-ReactComponent widget, then all child widgets (including + * implementations of {@link ReactComponentV2}) will be injected into the node found using the component's `ref`. + *

+ * The root element defaults to a `div`, but can be changed (e.g. to a `span`) using the + * {@link #ReactComponentV2(T, P, String)} constructor. + *

+ * This widget automatically unmounts the ReactComponent (if any) when this container is detached/unloaded. + */ +public abstract class ReactComponentV2< + T extends ReactComponentType

, P extends ReactComponentProps +> + extends ComplexPanel + implements HasClickHandlers { + + private ReactDOMRoot root; + private final ReactComponentType

reactComponentType; + public P props; + + private final ArrayList nonReactChildElements = new ArrayList<>(); + + public ReactComponentV2(T reactComponentType, P props) { + this(reactComponentType, props, DivElement.TAG); + } + + public ReactComponentV2(T reactComponentType, P props, String tag) { + this.reactComponentType = reactComponentType; + this.props = props; + setElement(Document.get().createElement(tag)); + } + + /** + * If any children of this widget do not implement this class, then we will inject them into the DOM using the React + * component's `ref` prop. This method will update the props with a callback ref that handles appending the children + * to the DOM node on which the ref is forwarded. + */ + private void maybeUpdatePropsWithCallbackRef() { + if (!this.allChildrenAreReactComponents() && getChildren().size() > 0) { + // Create a callback ref that will allow us to inject the GWT children into the DOM + ReactComponentProps.CallbackRef callbackRef = (Element node) -> { + if (node != null) { + // Once the DOM node is defined, inject each child + getChildren() + .forEach(w -> { + node.appendChild(w.getElement()); + // Keep track of child elements to ensure they are removed when the component unloads or re-renders + nonReactChildElements.add(w.getElement()); + }); + } + }; + + if (this.props == null) { + this.props = (P) JsPropertyMap.of(); + } + // Override the ref + this.props.ref = callbackRef; + } + } + + private void createRoot() { + if (root == null) { + root = ReactDOM.createRoot(this.getElement()); + } + } + + private void destroyRoot() { + if (root != null) { + // Asynchronously schedule unmounting the root to allow React to finish the current render cycle. + // https://github.com/facebook/react/issues/25675 + Timer t = new Timer() { + @Override + public void run() { + root.unmount(); + root = null; + } + }; + t.schedule(0); + } + } + + private void detachNonReactChildElements() { + if (allChildrenAreReactComponents()) { + // No need to remove non-React child elements from this widget + // But a descendant may contain non-React children, so recurse! + for (Widget w : getChildren()) { + ((ReactComponentV2) w).detachNonReactChildElements(); + } + } else { + nonReactChildElements.forEach(Element::removeFromParent); + nonReactChildElements.clear(); + } + } + + private ReactElement createReactElement() { + detachNonReactChildElements(); + maybeUpdatePropsWithCallbackRef(); + return React.createElement( + reactComponentType, + props, + getChildReactElements() + ); + } + + /** + * @return true iff there are children, and all children are React components + */ + private boolean allChildrenAreReactComponents() { + boolean allChildrenAreReactComponents = getChildren().size() > 0; + for (Widget w : getChildren()) { + if (!(w instanceof ReactComponentV2)) { + allChildrenAreReactComponents = false; + break; + } + } + return allChildrenAreReactComponents; + } + + private boolean isRenderedAsReactComponentChild() { + return ( + getParent() instanceof ReactComponentV2 && + ((ReactComponentV2) getParent()).allChildrenAreReactComponents() + ); + } + + /** + * This method returns the root ReactComponent widget, which is the only place where this React tree is attached to the DOM. + */ + private ReactComponentV2 getRootReactComponentWidget() { + if (isRenderedAsReactComponentChild()) { + return ( + (ReactComponentV2) getParent() + ).getRootReactComponentWidget(); + } else { + return this; + } + } + + @Override + public HandlerRegistration addClickHandler(ClickHandler handler) { + return addDomHandler(handler, ClickEvent.getType()); + } + + private ReactElement[] getChildReactElements() { + if (this.allChildrenAreReactComponents()) { + // If all widget children are ReactNodes, get their ReactElements and add them as React children + List> childWidgets = new ArrayList<>(); + getChildren() + .forEach(w -> childWidgets.add(((ReactComponentV2) w))); + + return childWidgets + .stream() + .map(ReactComponentV2::createReactElement) + .toArray(ReactElement[]::new); + } else { + return new ReactElement[0]; + } + } + + public void render() { + if (isRenderedAsReactComponentChild()) { + // This component will be rendered as a child of another React component, so destroy the root if one exists + destroyRoot(); + } + + // This component may be a React child of another component, so retrieve the root widget that renders this component tree. + ReactComponentV2 componentToRender = getRootReactComponentWidget(); + + // Asynchronously schedule creating a root in case we queued up an unmount of the current root + // See https://stackoverflow.com/questions/73459382 + Timer t = new Timer() { + @Override + public void run() { + componentToRender.createRoot(); + // Create a fresh ReactElement tree and render it + componentToRender.root.render(componentToRender.createReactElement()); + } + }; + t.schedule(0); + } + + @Override + public void setVisible(boolean visible) { + super.setVisible(visible); + // Re-render the element + this.render(); + } + + @Override + protected void onLoad() { + super.onLoad(); + this.render(); + } + + @Override + protected void onUnload() { + super.onUnload(); + + // Detach any non-React descendants that were injected into the component tree + detachNonReactChildElements(); + + destroyRoot(); + } + + /** + * Adds a child widget. + * + * @param child the widget to be added + * @throws UnsupportedOperationException if this method is not supported (most + * often this means that a specific overload must be called) + */ + @Override + public void add(Widget child) { + // See implementation in com.google.gwt.user.client.ui.ComplexPanel + + // Detach new child + child.removeFromParent(); + + // Logical attach + getChildren().add(child); + + // Physical attach (via React API!) + this.render(); + + // Adopt. + adopt(child); + } + + @Override + public boolean remove(Widget w) { + // See implementation in ComplexPanel + + // Validate. + if (w.getParent() != this) { + return false; + } + // Orphan. + try { + orphan(w); + } finally { + // Note - compared to ComplexPanel, we flipped logical and physical detach + // This is because our render implementation depends on logical attachment + + // Logical detach. + getChildren().remove(w); + + // Physical detach (via React API!) + this.render(); + } + return true; + } +} diff --git a/src/main/java/org/sagebionetworks/web/client/widget/sharing/EntityAccessControlListModalWidgetImpl.java b/src/main/java/org/sagebionetworks/web/client/widget/sharing/EntityAccessControlListModalWidgetImpl.java index 8803e99e86..02a241c5e5 100644 --- a/src/main/java/org/sagebionetworks/web/client/widget/sharing/EntityAccessControlListModalWidgetImpl.java +++ b/src/main/java/org/sagebionetworks/web/client/widget/sharing/EntityAccessControlListModalWidgetImpl.java @@ -13,14 +13,14 @@ public class EntityAccessControlListModalWidgetImpl implements EntityAccessControlListModalWidget { - private final ReactComponent reactComponent; + private final ReactComponent reactComponent; private final SynapseReactClientFullContextPropsProvider propsProvider; private EntityAclEditorModalProps componentProps; @Inject EntityAccessControlListModalWidgetImpl( - ReactComponent reactComponent, + ReactComponent reactComponent, SynapseReactClientFullContextPropsProvider propsProvider ) { this.reactComponent = reactComponent; diff --git a/src/main/webapp/Portal.html b/src/main/webapp/Portal.html index c672c0a307..540d4974d5 100644 --- a/src/main/webapp/Portal.html +++ b/src/main/webapp/Portal.html @@ -154,7 +154,6 @@ // The Bootstrap 3 custom theme CSS is loaded through swc.scss // Note: we alias to bootstrap3 because we rely on Bootstrap 4 to be in node_modules/bootstrap to compile our scss (via synapse-react-client). loadJs(cdnEndpoint + 'generated/bootstrap/dist/js/bootstrap.min.js') - loadJs(cdnEndpoint + 'generated/bootstrap/dist/js/npm.js') loadJs(cdnEndpoint + 'js/back-forward-nav-handler.js') loadJs(cdnEndpoint + 'generated/moment.min.js') loadJs(cdnEndpoint + 'generated/jsplumb.min.js') @@ -185,6 +184,7 @@ loadJs(cdnEndpoint + 'generated/pica.min.js') loadJs(reactPath) loadJs(reactDomPath) + loadJs(cdnEndpoint + 'generated/material-ui.production.min.js') loadJs(cdnEndpoint + 'generated/react-transition-group.min.js') loadJs(cdnEndpoint + 'generated/prop-types.min.js') loadJs(cdnEndpoint + 'generated/react-measure.js')