Material-ui framework with ReactiveHTML

Awesome. Thanks @xavArtley.

I have a feeling that the roboto fonts have to be included to be really be “material”.

image

    __javascript__ = [
        "https://unpkg.com/react@17/umd/react.development.js",
        "https://unpkg.com/react-dom@17/umd/react-dom.development.js",
        "https://unpkg.com/@material-ui/core@latest/umd/material-ui.development.js",
    ]
    
    __css__ = ["https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"]
1 Like

a more advanced example:
modal

from panel.reactive import ReactiveHTML
import panel as pn
import param
import time

class MaterialButton(ReactiveHTML):
    
    clicks = param.Number(default=0)
    
    _open = param.Event()
    
    _template = """
    <div id="materialbutton"></div>
    """
    _scripts = {
        'render': """
            const {
              Button,
              colors,
              createTheme ,
              CssBaseline,
              Dialog,
              DialogActions,
              DialogContent,
              DialogContentText,
              DialogTitle,
              Icon,
              MuiThemeProvider,
              Typography,
              withStyles,
            } = MaterialUI;

            const theme = createTheme ({
              palette: {
                primary: {
                  light: colors.purple[300],
                  main: colors.purple[500],
                  dark: colors.purple[700],
                },
                secondary: {
                  light: colors.green[300],
                  main: colors.green[500],
                  dark: colors.green[700],
                },
              },
            });

            const styles = theme => ({
              root: {
                textAlign: 'center',
                paddingTop: theme.spacing(1),
              },
              icon: {
                marginRight: theme.spacing(1),
              },
            });

            class Index extends React.Component {
              
              state = {
                open: false,
              };

              handleClose = () => {
                this.setState({
                  open: false,
                });
              };

              handleClick = () => {
                this.setState({
                  open: true,
                });
              };
              
              componentDidMount = () => {
                  state.reactElem = this;
              };

              render() {
                const { classes } = this.props;
                const { open } = this.state;

                return React.createElement(MuiThemeProvider, {
                  theme: theme,
                  ref: this.myRef
                }, React.createElement("div", {
                  className: classes.root
                }, React.createElement(CssBaseline, null), React.createElement(Dialog, {
                  open: open,
                  onClose: this.handleClose
                }, React.createElement(DialogTitle, null, "Super Secret Password"), React.createElement(DialogContent, null, React.createElement(DialogContentText, null, "1-2-3-4-5")), React.createElement(DialogActions, null, React.createElement(Button, {
                  color: "primary",
                  onClick: this.handleClose
                }, "OK"))), React.createElement(Typography, {
                  variant: "subtitle1",
                  gutterBottom: true
                }, "Material-UI"), React.createElement(Typography, {
                  variant: "h6",
                  gutterBottom: true
                }, "example project"), React.createElement(Button, {
                  variant: "contained",
                  color: "secondary",
                  onClick: () => {
                    data.clicks = data.clicks + 1
                    this.handleClick()
                  }
                }, React.createElement(Icon, {
                  className: classes.icon
                }, "fingerprint"), "Super Secret Password")));
              }
            }

            const App = withStyles(styles)(Index);
            ReactDOM.render(React.createElement(App, null), materialbutton)
            console.log(data._open)
        """,
        "clicks": """
            console.log("Clicked")
        """,
        "_open": """
            if (data._open)
                state.reactElem.handleClick()
                data._open = false
        """
    }
    
    __javascript__ = [
        "https://unpkg.com/react@17.0.2/umd/react.development.js",
        "https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js",
        "https://unpkg.com/@material-ui/core@4.12.3/umd/material-ui.development.js",
        "https://unpkg.com/babel-standalone@latest/babel.min.js"
    ]
   
    __css__ = ["https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap", "https://fonts.googleapis.com/icon?family=Material+Icons"]


pn.extension()

2 Likes

@xavArtley So cool! Thank you! let me learn more about your code. It is really exited to find out this can be work!

1 Like

I feel excited about this new feature, but since I am really a beginner, I have a lot of additional questions in this scope.

At first, how do I get the error when I am using ReactiveHTML?
Here, I am trying to get the value from the Material-ui TextField. However, it seems that I found an error so the text field is not shown. I am expecting my error is on value and onChange because without those I could get the TextField.

class MaterialTextField(ReactiveHTML):
    value = param.String(default='')
    name = param.String()
    
    def __init__(self, **params):
        if not "name" in params:
            params["name"]=''
        super().__init__(**params)
    
    _template = """
    <div id="textfield">
    </div>
    """
    _dom_events = {'textfield': ['change']}
    
    _scripts = {
        'render': """
        class Index extends React.Component {
            state = {
                myValue: '',
            };
            
            _handleChange = (e) => {this.setState({ 
                myValue: e.target.value
                })
            };
            
            render() {
                    return React.createElement(
                        MaterialUI.TextField, 
                        {label:data.name, 
                        variant:"filled",
                        value: this.state.myValue, 
                        onChange: this._handleChange 
                        }
                    )
            }
        };
        const App = Index;
        ReactDOM.render(React.createElement(App, null), textfield)
        """,
    }
    
    __javascript__ = [
        "https://unpkg.com/react@17.0.2/umd/react.development.js",
        "https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js",
        "https://unpkg.com/@material-ui/core@4.12.3/umd/material-ui.development.js",
        "https://unpkg.com/babel-standalone@latest/babel.min.js"
    ]
   
    __css__ = ["https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap", "https://fonts.googleapis.com/icon?family=Material+Icons"]


pn.extension()

Edit0: my bad, I just realize the problem in my code is mainly because of ; are missing. Now the code above could show the TextField. However, it is not assigned to param.value.

You will have to play with ref and inputRef to connect React elements with ReactiveHTML params:
MaterialInput

from panel.reactive import ReactiveHTML
import panel as pn
import param
import time
class MaterialTextField(ReactiveHTML):
    value = param.String(default='')
    name = param.String()
    
    def __init__(self, **params):
        if not "name" in params:
            params["name"]=''
        super().__init__(**params)
    
    _template = """
    <div id="textfield"></div>
    """
    
    _scripts = {
        'render': """
        class TextInput extends React.Component {
            
            handleChange = (e) => {
                data.value = e.target.value
            }
            
            render() {
                const {defaultValue} = this.props
                return React.createElement(
                    MaterialUI.TextField, 
                    {
                        label:data.name, 
                        variant:"filled",
                        defaultValue: defaultValue,
                        onChange: this.handleChange,
                        inputRef: (el) => {this.textfield = el}
                    },
                )
            }
        }
        ReactDOM.render(React.createElement(TextInput, {ref: (el) => {state.el = el}, defaultValue: data.value}), textfield)
        """,
        "value": """
            state.el.textfield.value = data.value
        """
    }
    
    __javascript__ = [
        "https://unpkg.com/react@17.0.2/umd/react.development.js",
        "https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js",
        "https://unpkg.com/@material-ui/core@4.12.3/umd/material-ui.development.js",
        "https://unpkg.com/babel-standalone@latest/babel.min.js"
    ]
   
    __css__ = ["https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap", "https://fonts.googleapis.com/icon?family=Material+Icons"]


pn.extension()
2 Likes

Thanks @xavArtley for your revision! it is really helpful! I think I will have another additional questions, but I will try harder first.

Hi all,

Now I am trying to do use Material-ui Autocomplete (React Autocomplete component - Material-UI), which is one of my main target to use material-ui.
Really sorry, but still I could not get it correctly without any help.

This is my code now.

class MaterialAutocomplete(ReactiveHTML):
    value = param.String(default='')
    options = param.List(default=[])
    name = param.String()
    
    def __init__(self, **params):
        if not "name" in params:
            params["name"]=''
        super().__init__(**params)
    
    _template = """
    <div id="autocomplete">
    </div>
    """
    _dom_events = {'autocomplete': ['change']}
    
    _scripts = {
        'render': """
            const {
                Autocomplete,
                TextField,
            } = MaterialUI;
            
        class AutoComplete extends React.Component {
            
            handleChange = (e) => {
                data.value = e.target.value
            }
                       
            render() {
                return React.createElement(
                    Autocomplete, 
                    {
                        options: data.options,
                        renderInput: React.createElement(
                            TextField, {
                                label:data.name,
                                variant:"filled",
                                defaultValue: defaultValue,
                                onChange: this.handleChange,
                                inputRef: (el) => {this.textfield = el}
                            },
                        )
                    }
                )
            }
        }
        ReactDOM.render(React.createElement(
                    AutoComplete, 
                    {ref: (el) => {state.el = el}, 
                    defaultValue: data.value}
                ),
                autocomplete)
        """,
        "_update_value": """
            state.el.textfield.value = data.value
        """
    }
                        
    __javascript__ = [
        "https://unpkg.com/react@17.0.2/umd/react.development.js",
        "https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js",
        "https://unpkg.com/@material-ui/core@4.12.3/umd/material-ui.development.js",
        "https://unpkg.com/babel-standalone@latest/babel.min.js",
        "https://cdn.jsdelivr.net/npm/@material-ui/lab@4.0.0-alpha.60/index.min.js"
    ]
   
    __css__ = [
        "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap", 
        "https://fonts.googleapis.com/icon?family=Material+Icons"
    ]


pn.extension()

It seems it is not possible to use material-ui autocomplete with react CDN (reactjs - How to use material-ui's Autocomplete component when using a react CDN - Stack Overflow). However, I am not sure if this is the main issue of the present code (I believe there are a lot of problem in this code).

Thanks in advance.
Any comments or suggestions are greatly appreciated.

Arifin

PS: When using Jupyterlab, the controls panel is shrinking but could recovered if I changed tab. I also got this behavior when I use the materialui from panel gallery (Materialui — Panel 0.12.1 documentation)
Thanks to @Marc, the issue is up here (MaterialUI Gallery Example does not look nice · Issue #2680 · holoviz/panel · GitHub)

Autocomplete is not include in the umd version of the official cdn. So you won’t be able to use it.
You can try may be this one material-ui-w-autocomplete - npm

1 Like

Thanks @xavArtley for you quick comment. I tried to use one that you have suggested it but still not showing anything. Please kindly let me know if you have any other suggestions.

class MaterialAutocomplete(ReactiveHTML):
    value = param.String(default='', allow_None=True)
    options = param.List(default=[])
    label = param.String()
    
    def __init__(self, **params):
        if not "name" in params:
            params["name"]=''
        super().__init__(**params)
    
    _template = """
        <div id="autocomplete">
        </div>
    """
    
    _scripts = {
        'render': """
            function _extends() {
              _extends =
                Object.assign ||
                function (target) {
                  for (var i = 1; i < arguments.length; i++) {
                    var source = arguments[i];
                    for (var key in source) {
                      if (Object.prototype.hasOwnProperty.call(source, key)) {
                        target[key] = source[key];
                      }
                    }
                  }
                  return target;
                };
              return _extends.apply(this, arguments);
            }

            const {
                Autocomplete,
                TextField
            } = MaterialUI;
            
            class ReactiveAutoComplete extends React.Component {
            
                constructor(props) {
                    super(props)
                    this.state = {
                        value: data.value,
                        options: data.options,
                        label: data.label
                    }
                }

                render() {
                    return React.createElement(Autocomplete, {
                      value: this.state.value,
                      options: this.state.options,
                      renderInput: params => React.createElement(TextField, _extends({}, params, {
                        label:this.state.label,
                        variant:"filled",
                      })),
                      onChange: (_, value) => {
                        this.setState({value: value})
                        data.value = value
                      },
                    })
                }
            }

            ReactDOM.render(
                React.createElement(ReactiveAutoComplete, {ref: (el) => state.el=el}),
                autocomplete
            )
        """,
        "value": """
            state.el.setState({value: data.value})
        """,
        "options": """
            state.el.setState({options: data.options})
        """,
        "label": """
            state.el.setState({label: data.label})
        """
    }
                        
    __javascript__ = [
        "https://unpkg.com/react@17.0.2/umd/react.development.js",
        "https://unpkg.com/react-dom@17.0.2/umd/react-dom.development.js",
        "https://unpkg.com/material-ui-w-autocomplete@4.9.8/material-ui-w-autocomplete.development.js",
    ]
   
    __css__ = [
        "https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap", 
        "https://fonts.googleapis.com/icon?family=Material+Icons"
    ]


pn.extension()
1 Like

I think Material UI is a limit of the CDN use.
It may be more interesting to use bokeh Custom Extension to wrap Material UI

Edit : Actually not that easy to wrap React components into a Bokeh extension

2 Likes

Could be interesting to see a Vue example of bootstrap or material components in ReactiveHTML one day to see how they play together.

1 Like

@xavArtley thank you for your solutions! I am sorry for my lack of knowledges and experiences. By the way, do you use other tools to catch some error when building the rendering scripts?

@xavArtley , I saw an error when the autocomplete value becomes None (when the text field is empty or x button is used) although value = param.String(default='', allow_None=True). Is it a bug in ReactiveHTML?

Traceback (most recent call last):
  File "/envs/lib/python3.6/site-packages/pyviz_comms/__init__.py", line 325, in _handle_msg
    self._on_msg(msg)
  File "/envs/lib/python3.6/site-packages/panel/viewable.py", line 273, in _on_msg
    doc.unhold()
  File "/envs/lib/python3.6/site-packages/bokeh/document/document.py", line 669, in unhold
    self._trigger_on_change(event)
  File "/envs/lib/python3.6/site-packages/bokeh/document/document.py", line 1180, in _trigger_on_change
    self._with_self_as_curdoc(event.callback_invoker)
  File "/envs/lib/python3.6/site-packages/bokeh/document/document.py", line 1198, in _with_self_as_curdoc
    return f()
  File "/envs/lib/python3.6/site-packages/bokeh/util/callback_manager.py", line 161, in invoke
    callback(attr, old, new)
  File "/envs/lib/python3.6/site-packages/panel/reactive.py", line 301, in _comm_change
    self._process_events({attr: new})
  File "/envs/lib/python3.6/site-packages/panel/reactive.py", line 262, in _process_events
    self.param.set_param(**self_events)
  File "/envs/lib/python3.6/site-packages/param/parameterized.py", line 1526, in set_param
    self_._batch_call_watchers()
  File "/envs/lib/python3.6/site-packages/param/parameterized.py", line 1665, in _batch_call_watchers
    self_._execute_watcher(watcher, events)
  File "/envs/lib/python3.6/site-packages/param/parameterized.py", line 1627, in _execute_watcher
    watcher.fn(*args, **kwargs)
  File "/envs/lib/python3.6/site-packages/panel/param.py", line 522, in link
    widget.param.set_param(**updates)
  File "/envs/lib/python3.6/site-packages/param/parameterized.py", line 1526, in set_param
    self_._batch_call_watchers()
  File "/envs/lib/python3.6/site-packages/param/parameterized.py", line 1665, in _batch_call_watchers
    self_._execute_watcher(watcher, events)
  File "/envs/lib/python3.6/site-packages/param/parameterized.py", line 1627, in _execute_watcher
    watcher.fn(*args, **kwargs)
  File "/envs/lib/python3.6/site-packages/panel/reactive.py", line 252, in _param_change
    self._apply_update(events, msg, model, ref)
  File "/envs/lib/python3.6/site-packages/panel/reactive.py", line 206, in _apply_update
    self._update_model(events, msg, root, model, doc, comm)
  File "/envs/lib/python3.6/site-packages/panel/reactive.py", line 219, in _update_model
    model.update(**msg)
  File "/envs/lib/python3.6/site-packages/bokeh/core/has_props.py", line 441, in update
    setattr(self, k, v)
  File "/envs/lib/python3.6/site-packages/bokeh/core/has_props.py", line 298, in __setattr__
    super().__setattr__(name, value)
  File "/envs/lib/python3.6/site-packages/bokeh/core/property/descriptors.py", line 552, in __set__
    self._internal_set(obj, value, setter=setter)
  File "/envs/lib/python3.6/site-packages/bokeh/core/property/descriptors.py", line 784, in _internal_set
    value = self.property.prepare_value(obj, self.name, value)
  File "/envs/lib/python3.6/site-packages/bokeh/core/property/bases.py", line 350, in prepare_value
    raise ValueError(f"failed to validate {obj_repr}.{name}: {error}")
ValueError: failed to validate TextInput(id='1006', ...).value: expected a value of type str, got None of type NoneType

I suppose it’s a bug. It’s more a question for @philippjfr
I only use chrome dev tools to debug scritps.
I use a console.log or debugger statement to know where to insert break points
I use a lot Babel · The compiler for next generation JavaScript to convert jsx into js

1 Like

Actually I can’t reproduce your error
autocomplete

I got the error when I serve it or when I also show the controls(). If I set m.value=None, I got nothing when I restart m on the other cell.

Looks like a bug, may be there is already an issue on github or you can report it.
You can work around the error by changing the onChange callback:

onChange: (_, value) => {
                        this.setState({value: value})
                        data.value = value != null ? value : ""
                      },

and not allowing None in the value parameter

1 Like

@xavArtley Your codes help a lot! Thank you!