Skip to content

Commit

Permalink
Find Browser.Navigation.Key anywhere in the model
Browse files Browse the repository at this point in the history
Fixes #10
  • Loading branch information
Keith Lazuka committed Sep 7, 2018
1 parent 882f438 commit c62849c
Show file tree
Hide file tree
Showing 6 changed files with 405 additions and 53 deletions.
136 changes: 83 additions & 53 deletions resources/hmr.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,21 +107,6 @@ if (module.hot) {
return modules;
}

function getPublicModule(Elm, path) {
var parts = path.split('.');
var parent = Elm;
for (var i = 0; i < parts.length; ++i) {
var part = parts[i];
if (part in parent) {
parent = parent[part]
}
if (!parent) {
return null;
}
}
return parent
}

function registerInstance(domNode, flags, path, portSubscribes, portSends) {
var id = getId();

Expand All @@ -132,6 +117,7 @@ if (module.hot) {
flags: flags,
portSubscribes: portSubscribes,
portSends: portSends,
navKeyPath: null, // array of JS property names by which the Browser.Navigation.Key can be found in the model
lastState: null // last Elm app state (root model)
};

Expand Down Expand Up @@ -202,7 +188,7 @@ if (module.hot) {
containerNode.removeChild(containerNode.lastChild);
}

var m = getPublicModule(Elm, instance.path);
var m = getAt(instance.path.split('.'), Elm);
var elm;
if (m) {
// prepare to initialize the new Elm module
Expand Down Expand Up @@ -306,32 +292,57 @@ if (module.hot) {
return portSubscribes;
}

function isDebuggerModel(model) {
return model && model.hasOwnProperty("expando") && model.hasOwnProperty("state");
}
/*
Breadth-first search for a `Browser.Navigation.Key` in the user's app model.
Returns the key and keypath or null if not found.
*/
function findNavKey(rootModel) {
var queue = [];
if (isDebuggerModel(rootModel)) {
/*
Extract the user's app model from the Elm Debugger's model. The Elm debugger
can hold multiple references to the user's model (e.g. in its "history"). So
we must be careful to only search within the "state" part of the Debugger.
*/
queue.push({value: rootModel['state'], keypath: ['state']});
} else {
queue.push({value: rootModel, keypath: []});
}

while (queue.length !== 0) {
var item = queue.shift();

// The nav key is identified by a runtime tag added by the elm-hot injector.
if (item.value.hasOwnProperty("elm-hot-nav-key")) {
// found it!
return item;
}

function findNavKey(model) {
for (var propName in model) {
if (!model.hasOwnProperty(propName)) continue;
var prop = model[propName];
if (prop.hasOwnProperty("elm-hot-nav-key")) {
return {name: propName, value: prop};
for (var propName in item.value) {
if (!item.value.hasOwnProperty(propName)) continue;
var newKeypath = item.keypath.slice();
newKeypath.push(propName);
queue.push({value: item.value[propName], keypath: newKeypath})
}
}

return null;
}

function removeNavKeyListeners(model) {
var navKey = null;
if (isDebuggerModel(model)) {
navKey = findNavKey(model['state']['a']);
} else {
navKey = findNavKey(model);
}
if (navKey) {
window.removeEventListener('popstate', navKey.value);
window.navigator.userAgent.indexOf('Trident') < 0 || window.removeEventListener('hashchange', navKey.value);
}

function isDebuggerModel(model) {
return model && model.hasOwnProperty("expando") && model.hasOwnProperty("state");
}

function getAt(keyPath, obj) {
return keyPath.reduce(function (xs, x) {
return (xs && xs[x]) ? xs[x] : null
}, obj)
}

function removeNavKeyListeners(navKey) {
window.removeEventListener('popstate', navKey.value);
window.navigator.userAgent.indexOf('Trident') < 0 || window.removeEventListener('hashchange', navKey.value);
}

// hook program creation
Expand All @@ -347,27 +358,34 @@ if (module.hot) {
var newModel = initialStateTuple.a;

if (typeof elm$browser$Browser$application !== 'undefined') {
// remove old navigation listeners
removeNavKeyListeners(oldModel);

// attempt to find the Browser.Navigation.Key in the newly-constructed model
// and bring it along with the rest of the old data.
var newKey = null;
if (isDebuggerModel(newModel)) {
newKey = findNavKey(newModel['state']['a']);
if (newKey) {
oldModel['state']['a'][newKey.name] = newKey.value;
}
var newKeyLoc = findNavKey(newModel);
var error = null;
if (newKeyLoc === null) {
error = "could not find Browser.Navigation.Key in the new app model";
} else if (instance.navKeyPath === null) {
error = "could not find Browser.Navigation.Key in the old app model.";
} else if (newKeyLoc.keypath.toString() !== instance.navKeyPath.toString()) {
error = "the location of the Browser.Navigation.Key in the model has changed.";
} else {
newKey = findNavKey(newModel);
if (newKey) {
oldModel[newKey.name] = newKey.value;
var oldNavKey = getAt(instance.navKeyPath, oldModel);
if (oldNavKey === null) {
error = "keypath " + instance.navKeyPath + " is invalid. Please report a bug."
} else {
// remove event listeners attached to the old nav key
removeNavKeyListeners(oldNavKey);

// insert the new nav key into the old model in the exact same location
var parentKeyPath = newKeyLoc.keypath.slice(0, -1);
var lastSegment = newKeyLoc.keypath.slice(-1)[0];
var oldParent = getAt(parentKeyPath, oldModel);
oldParent[lastSegment] = newKeyLoc.value;
}
}
if (!newKey) {
console.error("[elm-hot] Hot-swapping " + instance.path + " not possible. "
+ "You can fix this error by storing the Browser.Navigation.Key at the root "
+ "of your app's model.");

if (error !== null) {
console.error("[elm-hot] Hot-swapping " + instance.path + " not possible: " + error);
oldModel = newModel;
}
}
Expand All @@ -380,6 +398,18 @@ if (module.hot) {
} else {
// capture the initial state for later
initializingInstance.lastState = initialStateTuple.a;

// capture Browser.application's navigation key for later
if (typeof elm$browser$Browser$application !== 'undefined') {
var navKeyLoc = findNavKey(initializingInstance.lastState);
if (!navKeyLoc) {
console.error("[elm-hot] Hot-swapping disabled for " + instance.path
+ ": could not find Browser.Navigation.Key in your model.");
instance.navKeyPath = null;
} else {
instance.navKeyPath = navKeyLoc.keypath;
}
}
}

return initialStateTuple
Expand Down Expand Up @@ -452,7 +482,7 @@ if (module.hot) {
});
}
})();

scope['_elm_hot_loader_init'](scope['Elm']);
}
//////////////////// HMR END ////////////////////
133 changes: 133 additions & 0 deletions test/fixtures/BrowserApplicationCounterDeepKey.elm
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
module BrowserApplicationCounterDeepKey exposing (..)

import Browser exposing (UrlRequest)
import Browser.Navigation as Nav
import Html exposing (a, button, div, h1, p, span, text)
import Html.Attributes exposing (href, id)
import Html.Events exposing (onClick)
import Url exposing (Url)


main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = \_ -> Sub.none
, onUrlRequest = LinkClicked
, onUrlChange = UrlChanged
}


type alias Model =
{ count : Int
-- IMPORTANT: store the Nav.Key in a nested record to make sure that we an recover it during HMR
, keyHolder : { navKey: Nav.Key }
, page : Page
}


type Page
= NotFound
| Incrementer
| Decrementer


init : () -> Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
( loadPage url
{ count = 0
, keyHolder = { navKey = key }
, page = NotFound
}
, Cmd.none
)


type Msg
= Increment
| Decrement
| LinkClicked UrlRequest
| UrlChanged Url


update msg model =
case msg of
Increment ->
( { model | count = model.count + 1 }
, Cmd.none
)

Decrement ->
( { model | count = model.count - 1 }
, Cmd.none
)

LinkClicked req ->
case req of
Browser.Internal url ->
( model, Nav.pushUrl model.keyHolder.navKey (Url.toString url) )

Browser.External href ->
( model, Nav.load href )

UrlChanged url ->
( loadPage url model
, Cmd.none
)


loadPage : Url -> Model -> Model
loadPage url model =
{ model
| page =
case url.fragment of
Nothing ->
Incrementer

Just "/incrementer" ->
Incrementer

Just "/decrementer" ->
Decrementer

_ ->
NotFound
}


view model =
let
pageBody =
case model.page of
Incrementer ->
div [ id "incrementer" ]
[ h1 [] [ text "Incrementer" ]
, p []
[ text "Counter value is: "
, span [ id "counter-value" ] [ text (String.fromInt model.count) ]
]
, button [ onClick Increment, id "counter-button" ] [ text "+" ]
, p [] [ text "Switch to ", a [ id "nav-decrement", href "#/decrementer" ] [ text "decrementer" ] ]
]

Decrementer ->
div [ id "decrementer" ]
[ h1 [] [ text "Decrementer" ]
, p []
[ text "Counter value is: "
, span [ id "counter-value" ] [ text (String.fromInt model.count) ]
]
, button [ onClick Decrement, id "counter-button" ] [ text "-" ]
, p [] [ text "Switch to ", a [ id "nav-increment", href "#/incrementer" ] [ text "incrementer" ] ]
]

NotFound ->
text "Page not found"
in
{ title = "BrowserApplicationCounterDeepKey"
, body =
[ span [ id "code-version" ] [ text "code: v1" ]
, pageBody
]
}
16 changes: 16 additions & 0 deletions test/fixtures/BrowserApplicationCounterDeepKey.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>Main</title>
<script type="text/javascript" src="client.js"></script>
<script type="text/javascript" src="build/BrowserApplicationCounterDeepKey.js"></script>
</head>

<body>
<script>
connect("BrowserApplicationCounterDeepKey");
Elm.BrowserApplicationCounterDeepKey.init({});
</script>
</body>
</html>
Loading

0 comments on commit c62849c

Please sign in to comment.