├── .gitignore ├── LICENSE ├── README.md ├── elm-package.json ├── examples ├── Example.elm ├── README.md └── elm-package.json └── src ├── Native └── Navigation.js └── Navigation.elm /.gitignore: -------------------------------------------------------------------------------- 1 | elm-stuff -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Evan Czaplicki 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above 12 | copyright notice, this list of conditions and the following 13 | disclaimer in the documentation and/or other materials provided 14 | with the distribution. 15 | 16 | * Neither the name of Evan Czaplicki nor the names of other 17 | contributors may be used to endorse or promote products derived 18 | from this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 21 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 22 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 23 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 24 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 25 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 26 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 27 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 28 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 29 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Moved to [`elm/browser`](https://github.com/elm/browser) 2 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.1.0", 3 | "summary": "Manage browser navigation yourself (a.k.a. SPA routing, browser history)", 4 | "repository": "http://github.com/elm-lang/navigation.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [ 10 | "Navigation" 11 | ], 12 | "native-modules": true, 13 | "dependencies": { 14 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 15 | "elm-lang/dom": "1.1.1 <= v < 2.0.0", 16 | "elm-lang/html": "2.0.0 <= v < 3.0.0" 17 | }, 18 | "elm-version": "0.18.0 <= v < 0.19.0" 19 | } 20 | -------------------------------------------------------------------------------- /examples/Example.elm: -------------------------------------------------------------------------------- 1 | import Html exposing (..) 2 | import Html.Attributes exposing (..) 3 | import Html.Events exposing (..) 4 | import Navigation 5 | 6 | 7 | 8 | main = 9 | Navigation.program UrlChange 10 | { init = init 11 | , view = view 12 | , update = update 13 | , subscriptions = (\_ -> Sub.none) 14 | } 15 | 16 | 17 | 18 | -- MODEL 19 | 20 | 21 | type alias Model = 22 | { history : List Navigation.Location 23 | } 24 | 25 | 26 | init : Navigation.Location -> ( Model, Cmd Msg ) 27 | init location = 28 | ( Model [ location ] 29 | , Cmd.none 30 | ) 31 | 32 | 33 | 34 | -- UPDATE 35 | 36 | 37 | type Msg 38 | = UrlChange Navigation.Location 39 | 40 | 41 | {- We are just storing the location in our history in this example, but 42 | normally, you would use a package like evancz/url-parser to parse the path 43 | or hash into nicely structured Elm values. 44 | 45 | 46 | 47 | -} 48 | update : Msg -> Model -> (Model, Cmd Msg) 49 | update msg model = 50 | case msg of 51 | UrlChange location -> 52 | ( { model | history = location :: model.history } 53 | , Cmd.none 54 | ) 55 | 56 | 57 | 58 | -- VIEW 59 | 60 | 61 | view : Model -> Html msg 62 | view model = 63 | div [] 64 | [ h1 [] [ text "Pages" ] 65 | , ul [] (List.map viewLink [ "bears", "cats", "dogs", "elephants", "fish" ]) 66 | , h1 [] [ text "History" ] 67 | , ul [] (List.map viewLocation model.history) 68 | ] 69 | 70 | 71 | viewLink : String -> Html msg 72 | viewLink name = 73 | li [] [ a [ href ("#" ++ name) ] [ text name ] ] 74 | 75 | 76 | viewLocation : Navigation.Location -> Html msg 77 | viewLocation location = 78 | li [] [ text (location.pathname ++ location.hash) ] -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Run the Examples 2 | 3 | To run the examples in this folder, follow the following steps: 4 | 5 | ```bash 6 | git clone https://github.com/elm-lang/navigation.git 7 | cd navigation 8 | cd examples 9 | elm-reactor 10 | ``` 11 | 12 | This will navigate into the `examples/` directory and start `elm-reactor`. From here, go to [http://localhost:8000](http://localhost:8000) and start clicking on `.elm` files to see them in action. -------------------------------------------------------------------------------- /examples/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "Examples of using the elm-lang/navigation library", 4 | "repository": "http://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "src" 8 | ], 9 | "exposed-modules": [ 10 | "Navigation" 11 | ], 12 | "dependencies": { 13 | "elm-lang/core": "5.0.0 <= v < 6.0.0", 14 | "elm-lang/html": "2.0.0 <= v < 3.0.0", 15 | "elm-lang/navigation": "2.0.0 <= v < 3.0.0" 16 | }, 17 | "elm-version": "0.18.0 <= v < 0.19.0" 18 | } -------------------------------------------------------------------------------- /src/Native/Navigation.js: -------------------------------------------------------------------------------- 1 | var _elm_lang$navigation$Native_Navigation = function() { 2 | 3 | 4 | // FAKE NAVIGATION 5 | 6 | function go(n) 7 | { 8 | return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) 9 | { 10 | if (n !== 0) 11 | { 12 | history.go(n); 13 | } 14 | callback(_elm_lang$core$Native_Scheduler.succeed(_elm_lang$core$Native_Utils.Tuple0)); 15 | }); 16 | } 17 | 18 | function pushState(url) 19 | { 20 | return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) 21 | { 22 | history.pushState({}, '', url); 23 | callback(_elm_lang$core$Native_Scheduler.succeed(getLocation())); 24 | }); 25 | } 26 | 27 | function replaceState(url) 28 | { 29 | return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) 30 | { 31 | history.replaceState({}, '', url); 32 | callback(_elm_lang$core$Native_Scheduler.succeed(getLocation())); 33 | }); 34 | } 35 | 36 | 37 | // REAL NAVIGATION 38 | 39 | function reloadPage(skipCache) 40 | { 41 | return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) 42 | { 43 | document.location.reload(skipCache); 44 | callback(_elm_lang$core$Native_Scheduler.succeed(_elm_lang$core$Native_Utils.Tuple0)); 45 | }); 46 | } 47 | 48 | function setLocation(url) 49 | { 50 | return _elm_lang$core$Native_Scheduler.nativeBinding(function(callback) 51 | { 52 | try 53 | { 54 | window.location = url; 55 | } 56 | catch(err) 57 | { 58 | // Only Firefox can throw a NS_ERROR_MALFORMED_URI exception here. 59 | // Other browsers reload the page, so let's be consistent about that. 60 | document.location.reload(false); 61 | } 62 | callback(_elm_lang$core$Native_Scheduler.succeed(_elm_lang$core$Native_Utils.Tuple0)); 63 | }); 64 | } 65 | 66 | 67 | // GET LOCATION 68 | 69 | function getLocation() 70 | { 71 | var location = document.location; 72 | 73 | return { 74 | href: location.href, 75 | host: location.host, 76 | hostname: location.hostname, 77 | protocol: location.protocol, 78 | origin: location.origin, 79 | port_: location.port, 80 | pathname: location.pathname, 81 | search: location.search, 82 | hash: location.hash, 83 | username: location.username, 84 | password: location.password 85 | }; 86 | } 87 | 88 | 89 | // DETECT IE11 PROBLEMS 90 | 91 | function isInternetExplorer11() 92 | { 93 | return window.navigator.userAgent.indexOf('Trident') !== -1; 94 | } 95 | 96 | 97 | return { 98 | go: go, 99 | setLocation: setLocation, 100 | reloadPage: reloadPage, 101 | pushState: pushState, 102 | replaceState: replaceState, 103 | getLocation: getLocation, 104 | isInternetExplorer11: isInternetExplorer11 105 | }; 106 | 107 | }(); 108 | -------------------------------------------------------------------------------- /src/Navigation.elm: -------------------------------------------------------------------------------- 1 | effect module Navigation where { command = MyCmd, subscription = MySub } exposing 2 | ( back, forward 3 | , load, reload, reloadAndSkipCache 4 | , newUrl, modifyUrl 5 | , program, programWithFlags 6 | , Location 7 | ) 8 | 9 | {-| This is a library for managing browser navigation yourself. 10 | 11 | The core functionality is the ability to “navigate” to new URLs, 12 | changing the address bar of the browser *without* the browser kicking off a 13 | request to your servers. Instead, you manage the changes yourself in Elm. 14 | 15 | 16 | # Change the URL 17 | @docs newUrl, modifyUrl 18 | 19 | # Navigation 20 | @docs back, forward 21 | 22 | # Force Page Loads 23 | @docs load, reload, reloadAndSkipCache 24 | 25 | # Programs with Locations 26 | @docs program, programWithFlags, Location 27 | 28 | -} 29 | 30 | 31 | import Dom.LowLevel exposing (onWindow) 32 | import Html exposing (Html) 33 | import Json.Decode as Json 34 | import Native.Navigation 35 | import Process 36 | import Task exposing (Task) 37 | 38 | 39 | 40 | -- PROGRAMS 41 | 42 | 43 | {-| Same as [`Html.program`][doc], but your `update` function gets messages 44 | whenever the URL changes. 45 | 46 | [doc]: http://package.elm-lang.org/packages/elm-lang/html/latest/Html#program 47 | 48 | The first difference is the `Location -> msg` argument. This converts a 49 | [`Location`](#location) into a message whenever the URL changes. That message 50 | is fed into your `update` function just like any other one. 51 | 52 | The second difference is that the `init` function takes `Location` as an 53 | argument. This lets you use the URL on the first frame. 54 | 55 | **Note:** A location message is produced every time the URL changes. This 56 | includes things exposed by this library, like `back` and `newUrl`, as well as 57 | whenever the user clicks the back or forward buttons of the browsers. So if 58 | the URL changes, you will hear about it in your `update` function. 59 | -} 60 | program 61 | : (Location -> msg) 62 | -> 63 | { init : Location -> (model, Cmd msg) 64 | , update : msg -> model -> (model, Cmd msg) 65 | , view : model -> Html msg 66 | , subscriptions : model -> Sub msg 67 | } 68 | -> Program Never model msg 69 | program locationToMessage stuff = 70 | let 71 | subs model = 72 | Sub.batch 73 | [ subscription (Monitor locationToMessage) 74 | , stuff.subscriptions model 75 | ] 76 | 77 | init = 78 | stuff.init (Native.Navigation.getLocation ()) 79 | in 80 | Html.program 81 | { init = init 82 | , view = stuff.view 83 | , update = stuff.update 84 | , subscriptions = subs 85 | } 86 | 87 | 88 | {-| Works the same as [`program`](#program), but it can also handle flags. 89 | See [`Html.programWithFlags`][doc] for more information. 90 | 91 | [doc]: http://package.elm-lang.org/packages/elm-lang/html/latest/Html#programWithFlags 92 | -} 93 | programWithFlags 94 | : (Location -> msg) 95 | -> 96 | { init : flags -> Location -> (model, Cmd msg) 97 | , update : msg -> model -> (model, Cmd msg) 98 | , view : model -> Html msg 99 | , subscriptions : model -> Sub msg 100 | } 101 | -> Program flags model msg 102 | programWithFlags locationToMessage stuff = 103 | let 104 | subs model = 105 | Sub.batch 106 | [ subscription (Monitor locationToMessage) 107 | , stuff.subscriptions model 108 | ] 109 | 110 | init flags = 111 | stuff.init flags (Native.Navigation.getLocation ()) 112 | in 113 | Html.programWithFlags 114 | { init = init 115 | , view = stuff.view 116 | , update = stuff.update 117 | , subscriptions = subs 118 | } 119 | 120 | 121 | 122 | -- TIME TRAVEL 123 | 124 | 125 | {-| Go back some number of pages. So `back 1` goes back one page, and `back 2` 126 | goes back two pages. 127 | 128 | **Note:** You only manage the browser history that *you* created. Think of this 129 | library as letting you have access to a small part of the overall history. So 130 | if you go back farther than the history you own, you will just go back to some 131 | other website! 132 | -} 133 | back : Int -> Cmd msg 134 | back n = 135 | command (Jump -n) 136 | 137 | 138 | {-| Go forward some number of pages. So `forward 1` goes forward one page, and 139 | `forward 2` goes forward two pages. If there are no more pages in the future, 140 | this will do nothing. 141 | 142 | **Note:** You only manage the browser history that *you* created. Think of this 143 | library as letting you have access to a small part of the overall history. So 144 | if you go forward farther than the history you own, the user will end up on 145 | whatever website they visited next! 146 | -} 147 | forward : Int -> Cmd msg 148 | forward n = 149 | command (Jump n) 150 | 151 | 152 | {-| Leave the current page and load the given URL. **This always results in a 153 | page load**, even if the provided URL is the same as the current one. 154 | 155 | load "http://elm-lang.org" 156 | 157 | Use [`newUrl`](#newUrl) and [`modifyUrl`](#modifyUrl) if you want to change 158 | the URL without a page load. 159 | -} 160 | load : String -> Cmd msg 161 | load url = 162 | command (Visit url) 163 | 164 | 165 | {-| Reload the current page. **This always results in a page load!** 166 | This may grab resources from the browser cache, so use 167 | [`reloadAndSkipCache`](reloadAndSkipCache) if you want to be sure 168 | that you are not loading any cached resources. 169 | -} 170 | reload : Cmd msg 171 | reload = 172 | command (Reload False) 173 | 174 | 175 | {-| Reload the current page without using the browser cache. **This always 176 | results in a page load!** It is more common to want [`reload`](reload). 177 | -} 178 | reloadAndSkipCache : Cmd msg 179 | reloadAndSkipCache = 180 | command (Reload True) 181 | 182 | 183 | 184 | -- CHANGE HISTORY 185 | 186 | 187 | {-| Step to a new URL. This will add a new entry to the browser history. 188 | 189 | **Note:** If the user has gone `back` a few pages, there will be “future 190 | pages” that the user can go `forward` to. Adding a new URL in that 191 | scenario will clear out any future pages. It is like going back in time and 192 | making a different choice. 193 | -} 194 | newUrl : String -> Cmd msg 195 | newUrl url = 196 | command (New url) 197 | 198 | 199 | {-| Modify the current URL. This *will not* add a new entry to the browser 200 | history. It just changes the one you are on right now. 201 | -} 202 | modifyUrl : String -> Cmd msg 203 | modifyUrl url = 204 | command (Modify url) 205 | 206 | 207 | 208 | -- LOCATION 209 | 210 | 211 | {-| A bunch of information about the address bar. 212 | 213 | **Note 1:** Almost everyone will want to use a URL parsing library like 214 | [`evancz/url-parser`][parse] to turn a `Location` into something more useful 215 | in your `update` function. 216 | 217 | [parse]: https://github.com/evancz/url-parser 218 | 219 | **Note 2:** These fields correspond exactly with the fields of `document.location` 220 | as described [here](https://developer.mozilla.org/en-US/docs/Web/API/Location). 221 | -} 222 | type alias Location = 223 | { href : String 224 | , host : String 225 | , hostname : String 226 | , protocol : String 227 | , origin : String 228 | , port_ : String 229 | , pathname : String 230 | , search : String 231 | , hash : String 232 | , username : String 233 | , password : String 234 | } 235 | 236 | 237 | -- EFFECT MANAGER 238 | 239 | 240 | type MyCmd msg 241 | = Jump Int 242 | | New String 243 | | Modify String 244 | | Visit String 245 | | Reload Bool 246 | 247 | 248 | cmdMap : (a -> b) -> MyCmd a -> MyCmd b 249 | cmdMap _ myCmd = 250 | case myCmd of 251 | Jump n -> 252 | Jump n 253 | 254 | New url -> 255 | New url 256 | 257 | Modify url -> 258 | Modify url 259 | 260 | Visit url -> 261 | Visit url 262 | 263 | Reload skipCache -> 264 | Reload skipCache 265 | 266 | 267 | type MySub msg = 268 | Monitor (Location -> msg) 269 | 270 | 271 | subMap : (a -> b) -> MySub a -> MySub b 272 | subMap func (Monitor tagger) = 273 | Monitor (tagger >> func) 274 | 275 | 276 | 277 | -- STATE 278 | 279 | 280 | type alias State msg = 281 | { subs : List (MySub msg) 282 | , popWatcher : Maybe PopWatcher 283 | } 284 | 285 | 286 | type PopWatcher 287 | = Normal Process.Id 288 | | InternetExplorer Process.Id Process.Id 289 | 290 | 291 | 292 | -- INIT 293 | 294 | 295 | init : Task Never (State msg) 296 | init = 297 | Task.succeed (State [] Nothing) 298 | 299 | 300 | 301 | -- SELF MESSAGES 302 | 303 | 304 | onSelfMsg : Platform.Router msg Location -> Location -> State msg -> Task Never (State msg) 305 | onSelfMsg router location state = 306 | notify router state.subs location 307 | &> Task.succeed state 308 | 309 | 310 | (&>) task1 task2 = 311 | Task.andThen (\_ -> task2) task1 312 | 313 | 314 | 315 | -- APP MESSAGES 316 | 317 | 318 | onEffects : Platform.Router msg Location -> List (MyCmd msg) -> List (MySub msg) -> State msg -> Task Never (State msg) 319 | onEffects router cmds subs {popWatcher} = 320 | let 321 | stepState = 322 | case (subs, popWatcher) of 323 | ([], Just watcher) -> 324 | killPopWatcher watcher 325 | &> Task.succeed (State subs Nothing) 326 | 327 | (_ :: _, Nothing) -> 328 | Task.map (State subs << Just) (spawnPopWatcher router) 329 | 330 | (_, _) -> 331 | Task.succeed (State subs popWatcher) 332 | 333 | in 334 | Task.sequence (List.map (cmdHelp router subs) cmds) 335 | &> stepState 336 | 337 | 338 | cmdHelp : Platform.Router msg Location -> List (MySub msg) -> MyCmd msg -> Task Never () 339 | cmdHelp router subs cmd = 340 | case cmd of 341 | Jump n -> 342 | go n 343 | 344 | New url -> 345 | pushState url 346 | |> Task.andThen (notify router subs) 347 | 348 | Modify url -> 349 | replaceState url 350 | |> Task.andThen (notify router subs) 351 | 352 | Visit url -> 353 | setLocation url 354 | 355 | Reload skipCache -> 356 | reloadPage skipCache 357 | 358 | 359 | 360 | notify : Platform.Router msg Location -> List (MySub msg) -> Location -> Task x () 361 | notify router subs location = 362 | let 363 | send (Monitor tagger) = 364 | Platform.sendToApp router (tagger location) 365 | in 366 | Task.sequence (List.map send subs) 367 | &> Task.succeed () 368 | 369 | 370 | setLocation : String -> Task x () 371 | setLocation = 372 | Native.Navigation.setLocation 373 | 374 | 375 | reloadPage : Bool -> Task x () 376 | reloadPage = 377 | Native.Navigation.reloadPage 378 | 379 | 380 | go : Int -> Task x () 381 | go = 382 | Native.Navigation.go 383 | 384 | 385 | pushState : String -> Task x Location 386 | pushState = 387 | Native.Navigation.pushState 388 | 389 | 390 | replaceState : String -> Task x Location 391 | replaceState = 392 | Native.Navigation.replaceState 393 | 394 | 395 | 396 | -- POP WATCHER STUFF 397 | 398 | 399 | spawnPopWatcher : Platform.Router msg Location -> Task x PopWatcher 400 | spawnPopWatcher router = 401 | let 402 | reportLocation _ = 403 | Platform.sendToSelf router (Native.Navigation.getLocation ()) 404 | in 405 | if Native.Navigation.isInternetExplorer11 () then 406 | Task.map2 InternetExplorer 407 | (Process.spawn (onWindow "popstate" Json.value reportLocation)) 408 | (Process.spawn (onWindow "hashchange" Json.value reportLocation)) 409 | 410 | else 411 | Task.map Normal <| 412 | Process.spawn (onWindow "popstate" Json.value reportLocation) 413 | 414 | 415 | 416 | killPopWatcher : PopWatcher -> Task x () 417 | killPopWatcher popWatcher = 418 | case popWatcher of 419 | Normal pid -> 420 | Process.kill pid 421 | 422 | InternetExplorer pid1 pid2 -> 423 | Process.kill pid1 424 | &> Process.kill pid2 425 | --------------------------------------------------------------------------------