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

Add a top-level importSource attribute to VDOM components #100

Open
rmorshea opened this issue Sep 1, 2019 · 10 comments
Open

Add a top-level importSource attribute to VDOM components #100

rmorshea opened this issue Sep 1, 2019 · 10 comments

Comments

@rmorshea
Copy link
Contributor

rmorshea commented Sep 1, 2019

Summary

I think it would be useful to include a top-level importSource attribute to the VDOM spec:

{
    "importSource": {
        "source": string,
        "fallback": string,
    }
}

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 and children from the rest of the VDOM will then be passed to this component. For example, the following model:

jsx = """
function Button({ children }) {
    return <button>{ children... }</button>
}
"""

vdom = {
    "children": ["click me!"],
    "importSource": {
        "source": jsx,
        "fallback": "loading...",
    }
}

should result in the the HTML below:

<button>click me!</button>

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 proposed importSource dict):

jsx="""
import('the-package').then(pkg => pkg.default);
"""

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):

jsx = """
function Button({ children }) {
    return <button>{ children... }</button>
}
export default {
    SimpleButton: Button
}
"""

vdom = {
    "tagName": "SimpleButton",
    "children": ["click me!"],
    "importSource": {
        "source": jsx,
        "fallback": "loading...",
    }
}

The idea behind this approach is that you could create an API for JSX that looks a bit like this:

jsx_module = JsxModule(source)

button = jsx_module.SimpleButton("click me!")
something_else = jsx_module.SomeOtherComponent(...)
@gnestor
Copy link
Contributor

gnestor commented Sep 8, 2019

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.

@gnestor
Copy link
Contributor

gnestor commented Sep 8, 2019

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

@rmorshea
Copy link
Contributor Author

rmorshea commented Sep 9, 2019

@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()

@gnestor
Copy link
Contributor

gnestor commented Sep 12, 2019

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).

@rmorshea
Copy link
Contributor Author

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.

@gnestor
Copy link
Contributor

gnestor commented Sep 13, 2019

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);

@rmorshea
Copy link
Contributor Author

rmorshea commented Sep 16, 2019

@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.

@rmorshea
Copy link
Contributor Author

Transpiling could be a later addition though. For now, I think we can suggest using htm.

@gnestor
Copy link
Contributor

gnestor commented Sep 26, 2019

Agreed 👍

@rmorshea
Copy link
Contributor Author

This is implemented in IDOM now: https://idom.readthedocs.io/en/latest/javascript-modules.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants