ohanhi / elm-native-ui

[CLOSED] Experiment: mobile apps in Elm using React Native.
BSD 3-Clause "New" or "Revised" License
1.54k stars 76 forks source link

Use virtual-dom + main instead of VTree + ports #23

Closed yusefnapora closed 8 years ago

yusefnapora commented 8 years ago

Hey, I had some time today to play with this issue I opened yesterday, and managed to get things working. It involves quite a few nasty hacks, but comes with some nice benefits :)

The big change is the replacement of the VTree type with VirtualDom.Node, which is aliased as ReactNative.Node. This allows you to return a signal of ReactNative.Node from main and avoid the ports hack. Since we don't have to go through ports, you can attach properties that are not directly json encodable, so you can attach event handlers directly as properties, using the same technique as in elm-html.

Styles are also attached as a style object property, rather than being special-cased.

Now on to the hacks :)

First, including the virtual-dom package causes the React Native packager to fail, since virtual-dom is built with browserify, which defines a require function. The packager overrides this with its own require implementation and gets confused. So I added a perl one-liner to the postcompile npm run script to replace require with _browserify_require in the compiled elm.js.

Next, to actually get main to render, I defined a super barebones global.document object, so that Elm.fullscreen won't fail with a bunch of undefined references.

In the componentWillMount hook of the main AppWrapper component, I'm patching Elm.Native.VirtualDom.make to override the render and updateAndReplace functions with an implementation that just calls setState on the AppWrapper instance, sending in the new root virtual-dom node.

After patching the runtime, componentWillMount calls Elm.fullscreen.

Then a vdomToReactElement function in AppWrapper.render recursively converts from virtual-dom node to react element, just like vtreeToReactElement did before.

There's still plenty of ways this could be improved, but at least now we know it's possible :)

Hopefully I'll have time to pull the rendering hacks out into their own module and wire up Android support tomorrow.

yusefnapora commented 8 years ago

The last couple of commits add Android support and limit the scope of the require rename hack to just the VirtualDom.js file in the virtual-dom package. That seems much less invasive than altering the compiled elm.js.

There's still some cleanup stuff to be done; the Native.ReactNative module is actually not used anymore, so it could be removed. And I think that the init port can be removed as well... might not have time to work on this during the week though.

yusefnapora commented 8 years ago

Realized over lunch that using VirtualDom.Node as the output type means you can use StartApp directly :smiley:

yusefnapora commented 8 years ago

man, this is too fun :)

I pulled elm-effects into the mix with the last commit and implemented the random gif viewer from the elm architecture tutorial.

I had to add the addEventListener method to the XMLHttpRequest prototype to get elm-http to work, but now you can request a random cat gif from your phone with elm!

The XHR shim is incomplete; it doesn't handle timeout or progress events. Also, I still haven't figured out a clean solution for local images, since they need to be statically require'd for the react packager to pick them up. So there's no loading spinner.

I'm thinking for bundled images (and custom javascript components, etc) you'll probably have to require them in javascript, maybe in an ElmExports module or something. Then you could have a Native-backed bundledImage function that looks things up in that module, given a string key. vdomToReactElement could look there too, but you'd have to somehow indicate that you want to look in there instead of in the React module.

Maybe the hypothetical ElmExports object could always have a React member, so vtreeToReactElement could look up all component classes in there using something like lodash's get function, which will look up nested object paths, e.g, get(someObject, "foo.bar.baz").

Basically I'm thinking it'd look like this:

export default const ElmExports = {
  React: require('react-native'),

  AppComponents: {
    MySpecialComponent: require('./components/MySpecialComponent')
  },

  Images: {
    'foo.png': require('./images/foo.png'),
    'bar.gif': require('./images/bar.gif'),
  }
};

Then from elm, components that are included in React could be defined with e.g.

image : List Property -> List Node -> Node
image =
  node "React.Image" 

and you could get to MySpecialComponent with

special : List RN.Property -> List RN.Node -> Node
special =
  RN.node "AppComponents.MySpecialComponent"

then when we map the vdom nodes to react elements you'd just do

let componentClass = _.get(ElmExports, vdomNode.tagName);
return React.createElement(componentClass, props, children);

getting the bundled images out might require a Native-backed elm function... not sure yet.

ohanhi commented 8 years ago

Wow, @yusefnapora, you've really put some effort into this! I really appreciate the work you've done here.

I think I will go with the modified compiler thing however, since that's what Evan suggested when I talked with him. Most of your work should be very easily adaptable to that solution, though, since it would also use the main directly. So if you don't mind, I will pursue that goal for now.

I would love it if you can take your work on the modified compiler main version -- there's a lot of great stuff on here!

yusefnapora commented 8 years ago

Thanks! I think the modified compiler + elm-core route is probably the way to go for the long term. I'm not crazy about the hacks involved in this branch, and using virtual-dom kind of feels like lying to the compiler :) I'll start experimenting with that and see if I can come up with a sensible rendering method for VTree.

yusefnapora commented 8 years ago

I'm going to close this and keep chipping away at helping implement #24. I'll leave the branch up on my fork for reference if anyone wants to check it out.