├── .gitignore ├── README.md ├── elm-package.json ├── example ├── Main.elm └── index.html ├── make-example.sh ├── package.json ├── rebuild.sh └── src ├── Native └── React.js ├── React.elm ├── React └── Component.elm └── wrapper.js /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff/ 2 | repl-temp-* 3 | .DS_Store 4 | node_modules/ 5 | example/elm.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # elm-react 2 | 3 | This is currently only a proof of concept: Build [React][react] applications using [Elm][elm]. This works better than I expected. The whole thing currently runs on `react 0.14.3` and `elm 0.16`. 4 | 5 | ## What's included? 6 | 7 | Currently this elm packages includes a mixture of something like [start-app][start-app] and [elm-html][elm-html], but instead of relying on `virtual-dom` the whole thing is built using `react`. Currently react components are used for the view part, and state is handled outside as suggested in [the Elm Architecture][elm-arch]. 8 | 9 | ## Tutorial / Example 10 | 11 | Example react component: 12 | 13 | ```elm 14 | testComp : R.Comp Action Model 15 | testComp = 16 | R.createClass 17 | { name = "TestComp" 18 | , render = \p -> 19 | R.html "div" [] 20 | [ R.text <| "Countervalue: " ++ toString p.model.counter 21 | , R.html "button" 22 | [ R.EventAttribute "onClick" (Signal.message p.address Increment) 23 | ] 24 | [ R.text "Increment" 25 | ] 26 | ] 27 | } 28 | ``` 29 | 30 | To see how everything works in detail, check out the `example` folder and the `make-example.sh` script to build it. 31 | 32 | ## Next steps 33 | 34 | * Support more react life-cycle events like `componentDidMount` and `componentWillUnmount` to support hooking 3rd Party libs like `leaflet` 35 | * Support for transitions 36 | * Define helper functions for html nodes and attributes 37 | * Figure out if there's a way to avoid the 'external div' hack 38 | * I'm not sure how everything will integrate with [the Elm Architecture][elm-arch]. From the first point of view it seems to go very well, but I have not explored it in depth yet. 39 | * Split into two packages: `elm-react` and `react-start-app` 40 | * Split out the DOM parts to allow building apps with `react-native` 41 | 42 | ## Contributing / Helping / Hacking 43 | 44 | If you like to help, please open an issue about what you will do and send a pull request when finished! Code should be written in a consistent style throughout the project. Avoid whitespace that is sensible to conflicts. Note that by sending a pull request you agree that your contribution can be released under the BSD3 License as part of the `elm-react` package or any related packages. 45 | 46 | ### Building the library 47 | 48 | ```bash 49 | $ npm install 50 | $ ./rebuild.sh 51 | $ elm make 52 | ``` 53 | 54 | [react]: https://facebook.github.io 55 | [elm]: http://elm-lang.org/ 56 | [start-app]: https://github.com/evancz/start-app 57 | [elm-html]: https://github.com/evancz/elm-html 58 | [elm-arch]: https://github.com/evancz/elm-architecture-tutorial/ -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "React bindings for Elm", 4 | "repository": "https://github.com/agrafix/elm-react.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "./src" 8 | ], 9 | "exposed-modules": [ 10 | "React", 11 | "React.Component" 12 | ], 13 | "native-modules": true, 14 | "dependencies": { 15 | "elm-lang/core": "3.0.0 <= v < 4.0.0", 16 | "evancz/elm-effects": "2.0.0 <= v < 3.0.0" 17 | }, 18 | "elm-version": "0.16.0 <= v < 0.17.0" 19 | } 20 | -------------------------------------------------------------------------------- /example/Main.elm: -------------------------------------------------------------------------------- 1 | module Main where 2 | 3 | import React as R 4 | import React.Component as R 5 | 6 | import Effects 7 | import Task 8 | 9 | type alias Model = 10 | { counter : Int 11 | } 12 | 13 | type Action 14 | = Increment 15 | | Nop 16 | 17 | init : (Model, Effects.Effects Action) 18 | init = 19 | ({ counter = 0 }, Effects.none) 20 | 21 | update : Action -> Model -> (Model, Effects.Effects Action) 22 | update a m = 23 | case a of 24 | Increment -> 25 | ( { m 26 | | counter = m.counter + 1 27 | } 28 | , Effects.none 29 | ) 30 | Nop -> (m, Effects.none) 31 | 32 | testButton : R.Comp Action Model 33 | testButton = 34 | R.createClass 35 | { name = "Increment button" 36 | , render = \p -> 37 | R.html "button" 38 | [ R.EventAttribute "onClick" (Signal.message p.address Increment) 39 | ] 40 | [ R.text "Increment" 41 | ] 42 | } 43 | 44 | testComp : R.Comp Action Model 45 | testComp = 46 | R.createClass 47 | { name = "TestComp" 48 | , render = \p -> 49 | R.html "div" [] 50 | [ R.text <| "Countervalue: " ++ toString p.model.counter 51 | , R.comp testButton p 52 | ] 53 | } 54 | 55 | cfg : R.Config Model Action 56 | cfg = 57 | { divId = "reactApp" 58 | , init = init 59 | , update = update 60 | , rootComponent = testComp 61 | , inputs = [] 62 | } 63 | 64 | app : R.App Model 65 | app = R.start cfg 66 | 67 | main = app.html 68 | 69 | port tasks : Signal (Task.Task Effects.Never ()) 70 | port tasks = app.tasks -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 |
9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /make-example.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | elm make example/Main.elm --output example/elm.js 4 | 5 | echo "Open example/index.html in your browser" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elm-react", 3 | "version": "", 4 | "description": "", 5 | "dependencies": { 6 | "react": "0.14.3", 7 | "react-dom": "0.14.3", 8 | "react-addons-pure-render-mixin": "0.14.3" 9 | }, 10 | "devDependencies": { 11 | "browserify": "11.1.0" 12 | } 13 | } -------------------------------------------------------------------------------- /rebuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cd "$(dirname "$0")" 4 | 5 | if [ ! -d node_modules/browserify ] || [ ! -d node_modules/react ]; then 6 | echo "Please run 'npm install' first." 7 | exit 8 | fi 9 | 10 | $(npm bin)/browserify src/wrapper.js -o src/Native/React.js -------------------------------------------------------------------------------- /src/React.elm: -------------------------------------------------------------------------------- 1 | module React 2 | ( CompProps, Comp, CompSpec 3 | , Config, App 4 | , start 5 | ) where 6 | 7 | {-| High level bindings to facebook react library in the spirit of [start-app][sa]. 8 | 9 | [sa]: https://github.com/evancz/start-app 10 | 11 | # Define components 12 | @docs CompProps, Comp, CompSpec 13 | 14 | # Start your Application 15 | @docs Config, App, start 16 | -} 17 | 18 | import React.Component as R 19 | 20 | import Task 21 | import Effects exposing (Effects, Never) 22 | import Graphics.Element as E 23 | 24 | {-| Every component will take these props -} 25 | type alias CompProps action model = 26 | { address: Signal.Address action 27 | , model: model 28 | } 29 | 30 | {-| A component -} 31 | type alias Comp action model = R.Component (CompProps action model) 32 | 33 | {-| A component spec -} 34 | type alias CompSpec action model = R.ComponentSpec (CompProps action model) 35 | 36 | {-| The configuration of an app follows the basic model / update / view pattern 37 | that you see in every Elm program. 38 | The `divId` will identify the HTML div where React should render your application to. Make sure 39 | this application exists before including elm in your application. This div must not be the same 40 | div Elm will target. 41 | The `init` transaction will give you an initial model and create any tasks that 42 | are needed on start up. 43 | The `update` and `view` fields describe how to step the model and view the 44 | model. 45 | The `inputs` field is for any external signals you might need. If you need to 46 | get values from JavaScript, they will come in through a port as a signal which 47 | you can pipe into your app as one of the `inputs`. 48 | -} 49 | type alias Config model action = 50 | { divId: String 51 | , init : (model, Effects action) 52 | , update : action -> model -> (model, Effects action) 53 | , rootComponent : Comp action model 54 | , inputs : List (Signal.Signal action) 55 | } 56 | 57 | {-| An `App` is made up of a couple signals: 58 | * `html` — a constant empty `Element` to satisfy Elm's main function type. 59 | * `model` — a signal representing the current model. Generally you 60 | will not need this one, but it is there just in case. You will know if you 61 | need this. 62 | * `tasks` — a signal of tasks that need to get run. Your app is going 63 | to be producing tasks in response to all sorts of events, so this needs to 64 | be hooked up to a `port` to ensure they get run. 65 | -} 66 | type alias App model = 67 | { html : E.Element 68 | , model : Signal model 69 | , tasks : Signal (Task.Task Never ()) 70 | } 71 | 72 | {-| Start an application. It requires a bit of wiring once you have created an 73 | `App`. It should pretty much always look like this: 74 | app = 75 | start { divId = "reactApp", init = init, update = update, rootComponent = comp, inputs = [] } 76 | main = 77 | app.html 78 | port tasks : Signal (Task.Task Never ()) 79 | port tasks = 80 | app.tasks 81 | So once we start the `App` we feed the HTML into `main` and feed the resulting 82 | tasks into a `port` that will run them all. 83 | -} 84 | start : Config model action -> App model 85 | start config = 86 | let singleton action = [ action ] 87 | messages = 88 | Signal.mailbox [] 89 | address = 90 | Signal.forwardTo messages.address singleton 91 | updateStep action (oldModel, accumulatedEffects) = 92 | let 93 | (newModel, additionalEffects) = config.update action oldModel 94 | in 95 | (newModel, Effects.batch [accumulatedEffects, additionalEffects]) 96 | update actions (model, _) = 97 | List.foldl updateStep (model, Effects.none) actions 98 | inputs = 99 | Signal.mergeMany (messages.signal :: List.map (Signal.map singleton) config.inputs) 100 | effectsAndModel = 101 | Signal.foldp update config.init inputs 102 | model = 103 | Signal.map fst effectsAndModel 104 | taskMaker (currentModel, pendingEffects) = 105 | R.renderTo config.divId (R.comp config.rootComponent { address = address, model = currentModel }) 106 | `Task.andThen` \_ -> Effects.toTask messages.address pendingEffects 107 | in { html = E.empty 108 | , model = model 109 | , tasks = Signal.map taskMaker effectsAndModel 110 | } -------------------------------------------------------------------------------- /src/React/Component.elm: -------------------------------------------------------------------------------- 1 | module React.Component 2 | ( Node, Component 3 | , ComponentSpec, createClass 4 | , comp 5 | , Attribute(..), html 6 | , text 7 | , renderTo 8 | ) where 9 | 10 | {-| This module provides a low level wrapper around react functions. 11 | 12 | # Core types 13 | @docs Node, Component 14 | 15 | # Defining components 16 | @docs ComponentSpec, createClass 17 | 18 | # Composing components 19 | @docs comp, Attribute, html, text 20 | 21 | # Rendering to div 22 | @docs renderTo 23 | -} 24 | 25 | import Native.React 26 | 27 | import Task 28 | import Signal 29 | 30 | {-| A node in the component tree -} 31 | type Node = Node 32 | 33 | {-| A component created with `createClass` -} 34 | type Component props = Component 35 | 36 | {-| A component spec -} 37 | type alias ComponentSpec props = 38 | { name : String 39 | , render : props -> Node 40 | } 41 | 42 | {-| Define a component given a `ComponentSpec` -} 43 | createClass : ComponentSpec props -> Component props 44 | createClass = Native.React.createClass 45 | 46 | {-| Convert a component to a node -} 47 | comp : Component props -> props -> Node 48 | comp = Native.React.createElement 49 | 50 | {-| An attribute for a HTML node -} 51 | type Attribute 52 | = BasicAttribute String String 53 | | EventAttribute String Signal.Message 54 | 55 | {-| Make an html node -} 56 | html : String -> List Attribute -> List Node -> Node 57 | html = Native.React.createHtmlElement 58 | 59 | {-| Make a text node -} 60 | text : String -> Node 61 | text = Native.React.createTextElement 62 | 63 | {-| Render node to a div -} 64 | renderTo : String -> Node -> Task.Task never () 65 | renderTo = Native.React.renderTo -------------------------------------------------------------------------------- /src/wrapper.js: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | var ReactDOM = require("react-dom"); 3 | var PureRenderMixin = require("react-addons-pure-render-mixin"); 4 | 5 | Elm.Native.React = {}; 6 | Elm.Native.React.make = function(elm) 7 | { 8 | elm.Native = elm.Native || {}; 9 | elm.Native.React = elm.Native.React || {}; 10 | if (elm.Native.React.values) 11 | { 12 | return elm.Native.React.values; 13 | } 14 | 15 | var List = Elm.Native.List.make(elm); 16 | var Task = Elm.Native.Task.make(elm); 17 | var Signal = Elm.Native.Signal.make(elm); 18 | 19 | function createClass(spec) { 20 | return React.createClass({ 21 | mixins: [PureRenderMixin], 22 | displayName: spec.name, 23 | render: function() { 24 | return spec.render(this.props); 25 | } 26 | }) 27 | } 28 | 29 | function createElement(node, props) { 30 | return React.createElement(node, props); 31 | } 32 | 33 | function createHtmlElement(node, attribs, children) { 34 | var arr = List.toArray(attribs); 35 | var props = {}; 36 | for (var i in arr) { 37 | var p = arr[i]; 38 | if (p.ctor === 'BasicAttribute') { 39 | props[p._0] = p._1; 40 | } else if (p.ctor === 'EventAttribute') { 41 | props[p._0] = function (e) { 42 | e.preventDefault(); 43 | Signal.sendMessage(p._1); 44 | } 45 | } else { 46 | throw new Error("Unknown attribute!"); 47 | } 48 | } 49 | return React.createElement(node, props, List.toArray(children)); 50 | } 51 | 52 | function createTextElement(txt) { 53 | return txt; // is this right? 54 | } 55 | 56 | function renderTo(divId, node) { 57 | var target = document.getElementById(divId); 58 | ReactDOM.render(node, target); 59 | return Task.succeed(); 60 | } 61 | 62 | return Elm.Native.React.values = { 63 | createClass: createClass, 64 | createElement: F2(createElement), 65 | createHtmlElement: F3(createHtmlElement), 66 | createTextElement: createTextElement, 67 | renderTo: F2(renderTo) 68 | }; 69 | } 70 | --------------------------------------------------------------------------------