| class MountableWatcher extends HTMLElement { | |
| constructor() { | |
| console.log("creating component"); | |
| super(); | |
| } | |
| connectedCallback() { | |
| this.render(); | |
| } | |
| disconnectedCallback() { | |
| console.log("disconnected"); | |
| this.dispatchEvent(new CustomEvent("unmounted", { bubbles: false })); | |
| } | |
| attributeChangedCallback(attrName, oldVal, newVal) { | |
| if (attrName === "onunmount" && oldVal !== newVal) { | |
| if (newVal === null) { | |
| this.onunmount = null; | |
| } else { | |
| } | |
| } | |
| } | |
| static get observedAttributes() { | |
| return ["onunmount"]; | |
| } | |
| get onunmount() { | |
| return this._onUnmountFn; | |
| } | |
| set onunmount(handler) { | |
| if (this._onUnmountFn) { | |
| this.removeEventListener("unmount", this._onCheckFn); | |
| this._onUnmountFn = null; | |
| } | |
| if (typeof handler === "function") { | |
| this._onUnmountFn = handler; | |
| this.addEventListener("unmount", this._onCheckFn); | |
| } | |
| } | |
| render() { | |
| this.innerHTML = "<p>Hello from watcher</p>"; | |
| } | |
| } | |
| customElements.define("mountable-watcher", MountableWatcher); | |
| // React Elm Component | |
| var ElmComponent = React.createClass({ | |
| initialize: function(node) { | |
| if (node === null) return; | |
| var app = this.props.src.embed(node, this.props.flags); | |
| if (typeof this.props.ports !== "undefined") { | |
| this.props.ports(app.ports); | |
| } | |
| }, | |
| shouldComponentUpdate: function(prevProps) { | |
| return false; | |
| }, | |
| render: function() { | |
| return React.createElement("div", { ref: this.initialize }); | |
| } | |
| }); | |
| var node = document.getElementById("elm-app"); | |
| function remount(el) { | |
| ReactDOM.unmountComponentAtNode(el); | |
| ReactDOM.render(React.createElement(ElmComponent, { src: Elm.Main }), el); | |
| } | |
| document.getElementById("button").addEventListener("click", function() { | |
| remount(node); | |
| }); |
| <html> | |
| <head> | |
| <script src="https://unpkg.com/react@15.3.1/dist/react.min.js"></script> | |
| <script src="https://unpkg.com/react-dom@15.3.1/dist/react-dom.min.js"></script> | |
| <style> | |
| html { | |
| background: #F7F7F7; | |
| } | |
| #elm-app { | |
| color: red; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div id="elm-app"></div> | |
| <label> | |
| Click this button and watch the handlers leak on the console. Each time the button is clicked, the React component remounts | |
| and adds a new handler and does not clean up old ones. | |
| <button id="button">remount</button> | |
| </label> | |
| <script src="/component.js"></script> | |
| <script src="/_compile/Main.elm"></script> | |
| </body> | |
| </html> |
| module Main exposing (..) | |
| import Html exposing (Html, program, text) | |
| import MountableProgram as Mountable exposing (Mountable, mountableProgram) | |
| import Time | |
| type alias Model = | |
| () | |
| type Msg | |
| = Tick Time.Time | |
| main : Program Never (Mountable Model) (Mountable.Msg Msg) | |
| main = | |
| mountableProgram | |
| { init = init | |
| , view = \_ -> text "Hello, I am leaking." | |
| , update = update | |
| , subscriptions = \_ -> Time.every (2 * Time.second) Tick | |
| } | |
| init : ( (), Cmd msg ) | |
| init = | |
| () ! [] | |
| getSeconds : a -> String | |
| getSeconds = | |
| toString >> String.slice 0 -3 | |
| update : Msg -> a -> ( a, Cmd Msg ) | |
| update msg model = | |
| case msg of | |
| Tick time -> | |
| let | |
| _ = | |
| Debug.log "tick" (getSeconds time) | |
| in | |
| model ! [] |
| module MountableProgram exposing (Mountable, Msg, mountableProgram) | |
| import Html exposing (Html, program) | |
| import Html.Events exposing (on) | |
| import Json.Decode as Decode | |
| type alias Mountable a = | |
| { model : a | |
| , isMounted : Bool | |
| } | |
| type Msg msg | |
| = Unmounted | |
| | AppMsg msg | |
| mountableProgram : | |
| { init : ( model, Cmd msg ) | |
| , update : msg -> model -> ( model, Cmd msg ) | |
| , view : model -> Html msg | |
| , subscriptions : model -> Sub msg | |
| } | |
| -> Program Never (Mountable model) (Msg msg) | |
| mountableProgram stuff = | |
| let | |
| init = | |
| let | |
| ( model, cmd ) = | |
| stuff.init | |
| in | |
| ( { model = model, isMounted = True }, Cmd.map AppMsg cmd ) | |
| update msg model = | |
| case msg of | |
| Unmounted -> | |
| Debug.log "unmounted" { model | isMounted = False } ! [] | |
| AppMsg msg -> | |
| let | |
| ( newModel, cmd ) = | |
| stuff.update msg model.model | |
| in | |
| ( { model = newModel, isMounted = model.isMounted }, Cmd.map AppMsg cmd ) | |
| view model = | |
| Html.section [] [ Html.node "mountable-watcher" [ on "unmounted" (Decode.succeed Unmounted) ] [], Html.map AppMsg (stuff.view model.model) ] | |
| subscriptions model = | |
| Sub.batch | |
| [ if model.isMounted then | |
| Sub.map AppMsg (stuff.subscriptions model.model) | |
| else | |
| Sub.none | |
| ] | |
| in | |
| program | |
| { init = init | |
| , update = update | |
| , view = view | |
| , subscriptions = subscriptions | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment