Open rmorshea opened 5 years ago
I agree that it would be great to be able to use vdom to render React components in addition to native HTML elements. However, I prefer the following interface:
from vdom import import_component
CanvasDraw = import_component('react-canvas-draw')
CanvasDraw(width='100%')
I don't think that we want to encourage people to write Javascript in Python. The above approach allows people to reference Javascript from Python (in this case its referencing react-canvas-draw
on npm but they can also reference local Javascript served from the Jupyter server, e.g. import_component('/js/myLib/index.js')
).
Additionally, the import_component
function is inspired by vdom's create_component
function which is used to create a component that vdom doesn't provide a helper function for (e.g. select
). This above approach provides a consistent interface for working with native HTML elements and React components.
I have created an example notebook and a branch of jupyterlab (that patches @nteract/transform-vdom to support import_component
) that demonstrates how this approach would work: https://mybinder.org/v2/gh/gnestor/jupyterlab/vdom-react-demo?urlpath=lab/tree/packages/vdom-extension/notebooks/vdom-react.ipynb
@gnestor I agree that import_component('react-canvas-draw')
is the ideal interface, but by using javascript in the underlying model it allows for greater flexibility while still enabling you to write higher level functions that are much simpler:
def import_component(module):
return {'importSource': {'source': f"import('https://dev.jspm.io/{module})"}}
IDOM profides a similar interface. You should be able to paste the following code into the IDOM editor and see the canvas:
import idom
Canvas = idom.Import("react-canvas-draw")
Canvas()
by using javascript in the underlying model it allows for greater flexibility while still enabling you to write higher level functions that are much simpler:
I agree that there's a need for more expressiveness than just providing to an npm module name. Allow me to clarify what I mean by "reference local Javascript served from the Jupyter server, e.g. import_component('/js/myLib/index.js')
":
You can create a JS file in JupyterLab (for example) that imports some npm modules and exports a higher-level component. I have an example in the binder above that looks like:
// ReactPianoComponent.js
// Written using vanilla JS vs. JSX so that it can be imported by vdom without requiring transpilation
import React from '//dev.jspm.io/react';
import PianoDefault from '//dev.jspm.io/react-piano-component';
const { default: Piano } = PianoDefault;
function PianoContainer({ children }) {
return React.createElement(
'div',
{
className: 'interactive-piano__piano-container',
onMouseDown: event => event.preventDefault()
},
children
);
}
function AccidentalKey({ isPlaying, text, eventHandlers }) {
return React.createElement(
'div',
{ className: 'interactive-piano__accidental-key__wrapper' },
React.createElement(
'button',
{
className: `interactive-piano__accidental-key ${
isPlaying ? 'interactive-piano__accidental-key--playing' : ''
}`,
...eventHandlers
},
React.createElement('div', { className: 'interactive-piano__text' }, text)
)
);
}
function NaturalKey({ isPlaying, text, eventHandlers }) {
return React.createElement(
'button',
{
className: `interactive-piano__natural-key ${
isPlaying ? 'interactive-piano__natural-key--playing' : ''
}`,
...eventHandlers
},
React.createElement('div', { className: 'interactive-piano__text' }, text)
);
}
function PianoKey({
isNoteAccidental,
isNotePlaying,
startPlayingNote,
stopPlayingNote,
keyboardShortcuts
}) {
function handleMouseEnter(event) {
if (event.buttons) {
startPlayingNote();
}
}
const KeyComponent = isNoteAccidental ? AccidentalKey : NaturalKey;
const eventHandlers = {
onMouseDown: startPlayingNote,
onMouseEnter: handleMouseEnter,
onTouchStart: startPlayingNote,
onMouseUp: stopPlayingNote,
onMouseOut: stopPlayingNote,
onTouchEnd: stopPlayingNote
};
return React.createElement(KeyComponent, {
isPlaying: isNotePlaying,
text: keyboardShortcuts.join(' / '),
eventHandlers: eventHandlers
});
}
export default function InteractivePiano() {
return React.createElement(
PianoContainer,
null,
React.createElement(Piano, {
startNote: 'C4',
endNote: 'B5',
renderPianoKey: PianoKey,
keyboardMap: {
Q: 'C4',
2: 'C#4',
W: 'D4',
3: 'D#4',
E: 'E4',
R: 'F4',
5: 'F#4',
T: 'G4',
6: 'G#4',
Y: 'A4',
7: 'A#4',
U: 'B4',
V: 'C5',
G: 'C#5',
B: 'D5',
H: 'D#5',
N: 'E5',
M: 'F5',
K: 'F#5',
',': 'G5',
L: 'G#5',
'.': 'A5',
';': 'A#5',
'/': 'B5'
}
})
);
}
This is an example of a React component that requires some callback props that you can't provide via vdom (e.g. Python callbacks), so we need to provide a higher-level component that provides these props in JS.
Now from your notebook, we can write:
Piano = import_component('./ReactPianoComponent')
Piano()
This allows us to accomplish the same result as using importSource
while separating Python and Javascript in a reasonable way (using files).
Is there some way that we could still transpile JSX client-side? I just don't see anyone wanting to write vanilla JS for React. Even if you did, you'd probably end up re-writing it in JSX once you turned it into a real NPM package.
Ya, vdom could transpile any files passed to import_component
, however if that file imports any other files (using ES modules), then those would not be transpiled. Webpack and rollup do that kind of stuff (transpiling all dependencies and bundling them) but I don't think that babel (which does the transpiling) knows how to do that.
I did a little searching and I think the solution to using JSX in pure ES6 is template literals:
import { React, ReactDOM } from ‘//unpkg.com/es-react';
import htm from ‘//unpkg.com/htm'
const html = htm.bind(React.createElement)
class App extends React.Component {
render() {
return html`
<div>
App goes here
</div>
`;
}
}
ReactDOM.render(html`<${App} />`, document.body);
@gnestor the template literals definitely are better.
Perhaps transpiling could be optional? Maybe a field in importSource
could tell the client how to transpile (if at all)? Not sure what information would need to be provided to enable that though.
Transpiling could be a later addition though. For now, I think we can suggest using htm
.
Agreed 👍
This is implemented in IDOM now: https://idom.readthedocs.io/en/latest/javascript-modules.html
Summary
I think it would be useful to include a top-level
importSource
attribute to the VDOM spec:The purpose of this attribute is to include ReactJS components in your VDOM that you write or import. You can see this demonstrated in the "ReactJS Components" example from my project
idom
:https://idom-sandbox.herokuapp.com/client/index.html
How it Should Work
The source string should be JSX that, when eval'd, will return a component. The
attributes
andchildren
from the rest of the VDOM will then be passed to this component. For example, the following model:should result in the the HTML below:
Importing Other Libraries
You should also be able to return a promise from the JSX that results in a component (hence the presence of the
fallback
key in the proposedimportSource
dict):Things to Think About
What if the JSX were like a module, and the
tagName
referred to a member of the exported object (this is similar to what I'm doing in iDOM right now):The idea behind this approach is that you could create an API for JSX that looks a bit like this: