├── .editorconfig ├── .gitignore ├── .vscode └── settings.json ├── Makefile ├── assets ├── logo.idraw └── logo.png ├── docs ├── building-routes.md ├── changelog.md ├── matching-routes.md ├── navigating.md ├── nesting-routes.md ├── reverse-routing.md ├── testing.md ├── upgrade-2-to-3.md ├── upgrade-3-to-4.md ├── upgrade-4-to-5.md └── upgrade-5-to-6.md ├── elm-package.json ├── examples ├── basic │ ├── .dockerignore │ ├── .gitignore │ ├── Main.elm │ ├── elm-package.json │ ├── install-packages.sh │ └── readme.md └── full │ ├── .gitignore │ ├── dev_server.js │ ├── elm-package.json │ ├── install-packages.sh │ ├── package.json │ ├── public │ ├── images │ │ ├── csharp.png │ │ ├── elixir.png │ │ ├── elm.png │ │ ├── go.png │ │ ├── haskell.png │ │ ├── java.png │ │ ├── js.png │ │ ├── php.png │ │ ├── python.png │ │ ├── ruby.png │ │ ├── rust.png │ │ └── swift.png │ └── index.html │ ├── readme.md │ ├── src │ ├── Languages │ │ ├── Edit.elm │ │ ├── Filter.elm │ │ ├── List.elm │ │ ├── Messages.elm │ │ ├── Models.elm │ │ ├── Routing.elm │ │ ├── Show.elm │ │ ├── Update.elm │ │ └── View.elm │ ├── Main.elm │ ├── Messages.elm │ ├── Models.elm │ ├── Routing.elm │ ├── Update.elm │ ├── View.elm │ └── index.js │ └── webpack.config.js ├── license.md ├── package.json ├── readme.md ├── src ├── Hop.elm ├── Hop │ ├── Address.elm │ ├── AddressTest.elm │ ├── In.elm │ ├── InTest.elm │ ├── Out.elm │ ├── OutTest.elm │ ├── TestHelper.elm │ ├── Types.elm │ └── Utils.elm └── HopTest.elm └── tests ├── IntegrationTest.elm ├── Main.elm ├── Tests.elm ├── elm-package.json ├── install-packages.sh └── package.json /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | end_of_line = lf 3 | insert_final_newline = true 4 | indent_style = space 5 | indent_size = 2 6 | 7 | [*.js] 8 | indent_style = tab 9 | indent_size = 2 10 | 11 | [*.elm] 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [Makefile] 16 | indent_style = tab 17 | indent_size = 2 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | elm-stuff 3 | node_modules 4 | elm.js 5 | documentation.json 6 | .vscode/tasks.json 7 | npm-debug.log 8 | .envrc 9 | tests.js 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "search.exclude": { 4 | "**/node_modules": true, 5 | "**/bower_components": true, 6 | "**/*.elmo": true, 7 | "documentation.json": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Start basic application example locally 2 | basic-up: 3 | cd ./examples/basic && elm reactor 4 | 5 | full-up: 6 | cd ./examples/full && npm run dev 7 | 8 | # Generate documentation for preview 9 | docs: 10 | elm make --docs=documentation.json 11 | 12 | # Run unit tests locally 13 | test-unit: 14 | npm test 15 | 16 | test-unit-ci: 17 | npm install -g elm 18 | npm install -g elm-test 19 | elm-package install -y 20 | cd tests && elm-package install -y && cd .. 21 | elm-test 22 | 23 | build-basic: 24 | cd examples/basic/ && elm make --yes Main.elm 25 | 26 | build-full: 27 | cd examples/full/ && elm make --yes src/Main.elm 28 | 29 | test-ci: 30 | make test-unit-ci 31 | make build-basic 32 | make build-full 33 | 34 | .PHONY: docs test 35 | -------------------------------------------------------------------------------- /assets/logo.idraw: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporto/hop/3113fe73d78e4ce6841d414f0c138cf4eb0602f2/assets/logo.idraw -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporto/hop/3113fe73d78e4ce6841d414f0c138cf4eb0602f2/assets/logo.png -------------------------------------------------------------------------------- /docs/building-routes.md: -------------------------------------------------------------------------------- 1 | # Building routes 2 | 3 | As of version 6 Hop doesn't provide matchers anymore, instead you can use [__UrlParser__](http://package.elm-lang.org/packages/evancz/url-parser). 4 | 5 | You build your routes by using union types: 6 | 7 | ```elm 8 | type Route 9 | = HomeRoute 10 | | UserRoute Int 11 | | UserStatusRoute Int 12 | | NotFoundRoute 13 | ``` 14 | 15 | Then you need to create matchers for these routes: 16 | 17 | ```elm 18 | import UrlParser exposing ((), format, oneOf, int, s) 19 | 20 | matchers = 21 | oneOf [ 22 | UrlParser.format HomeRoute (s "") 23 | , UrlParser.format UserRoute (s "users" int) 24 | , UrlParser.format UserStatusRoute (s "users" int s "status") 25 | ] 26 | ``` 27 | 28 | These matchers will match: 29 | 30 | - "/" 31 | - "users/1" 32 | - "users/1/status" 33 | 34 | ## Order matters 35 | 36 | The order of the matchers makes a big difference. See these examples. 37 | 38 | Given you have some routes and matchers: 39 | 40 | ```elm 41 | import UrlParser exposing (format, s, parse, int, oneOf, ()) 42 | 43 | type Route = UserRoute Int | UserEditRoute Int 44 | 45 | -- match 'users/1' 46 | userMatcher = format UserRoute (s "users" int) 47 | 48 | -- match '/uses/1/edit' 49 | userEditMatcher = format UserEditRoute (s "users" int s "edit") 50 | ``` 51 | 52 | ### Incorrect order 53 | 54 | ```elm 55 | matchers = 56 | oneOf [userMatcher, userEditMatcher] 57 | 58 | parse identity matchers "users/1" 59 | == Ok (UserRoute 1) : Result.Result String Repl.Route 60 | 61 | parse identity matchers "users/1/edit" 62 | == Err "The parser worked, but /edit was left over." 63 | ``` 64 | 65 | The `userEditMatcher` doesn't even run in this case. The `userMatcher` fails and stops the flow. 66 | 67 | ## Correct order 68 | 69 | ```elm 70 | matchers = 71 | oneOf [userEditMatcher, userMatcher] 72 | 73 | parse identity matchers "users/1" 74 | == Ok (UserRoute 1) : Result.Result String Repl.Route 75 | 76 | parse identity matchers "users/1/edit" 77 | == Ok (UserEditRoute 1) : Result.Result String Repl.Route 78 | ``` 79 | 80 | This works as expected, so is important to put the more specific matchers first. 81 | -------------------------------------------------------------------------------- /docs/changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 6.0.0 4 | 5 | - Remove matchers (Use UrlParser instead) 6 | - Rename input and output functions 7 | - Encode and decode path segments 8 | 9 | ### 5.0.1 10 | 11 | - Encode query string when converting location to URL. 12 | 13 | ## 5.0.0 14 | 15 | - Update for Elm 0.17 16 | 17 | ### 4.0.3 18 | 19 | - Fix issue where root path wouldn't match when using hash routing. 20 | 21 | ### 4.0.2 22 | 23 | - Futher fix for navigating to root path, use / instead of . 24 | 25 | ### 4.0.1 26 | 27 | - Fix https://github.com/sporto/hop/issues/20 28 | 29 | ## 4.0.0 30 | 31 | - Support setState (No hashes) 32 | - Most functions take a config record 33 | 34 | ## 3.0.0 35 | 36 | - Typed values in routes 37 | - Nested routes 38 | - Reverse routing 39 | 40 | ### 2.1.1 41 | 42 | - Remove unnecessary dependency to `elm-test` 43 | 44 | ### 2.1.0 45 | 46 | - Expose `Query` and `Url` types 47 | 48 | ## 2.0.0 49 | 50 | - Remove dependency on `Erl`. 51 | - Change order of arguments on `addQuery`, `clearQuery`, `removeQuery` and `setQuery` 52 | 53 | ### 1.2.1 54 | 55 | - Url is normalized before navigation i.e append `#/` if necessary 56 | 57 | ### 1.2.0 58 | 59 | - Added `addQuery`, changed behaviour of `setQuery`. 60 | 61 | ### 1.1.1 62 | 63 | - Fixed issue where query string won't be set when no hash wash present 64 | -------------------------------------------------------------------------------- /docs/matching-routes.md: -------------------------------------------------------------------------------- 1 | # Matching routes 2 | 3 | Create a parser using `Navigation.makeParser` combined with `Hop.makeResolver`. 4 | There are serveral strategies you can use. 5 | 6 | ## Given you have some configuration 7 | 8 | ``` 9 | routes = 10 | oneOf [...] 11 | 12 | hopConfig = 13 | { ... } 14 | ``` 15 | 16 | ## A parser that returns `(Route, Address)` 17 | 18 | ``` 19 | urlParserRouteAddress : Navigation.Parser ( MainRoute, Address ) 20 | urlParserRouteAddress = 21 | let 22 | parse path = 23 | path 24 | |> UrlParser.parse identity routes 25 | |> Result.withDefault NotFoundRoute 26 | 27 | solver = 28 | Hop.makeResolver configWithHash parse 29 | in 30 | Navigation.makeParser (.href >> solver) 31 | ``` 32 | 33 | This parser: 34 | 35 | - Takes the `.href` from the `Location` record given by `Navigation`. 36 | - Converts that to a normalised path (done inside `makeResolver`). 37 | - Passes the normalised path to your `parse` function, which returns a matched route or `NotFoundRoute`. 38 | - When run returns a tuple `(Route, Address)`. 39 | 40 | ## A parser that returns only the matched route 41 | 42 | ``` 43 | urlParserOnlyRoute : Navigation.Parser MainRoute 44 | urlParserOnlyRoute = 45 | let 46 | parse path = 47 | path 48 | |> UrlParser.parse identity routes 49 | |> Result.withDefault NotFoundRoute 50 | 51 | solver = 52 | Hop.makeResolver configWithHash parse 53 | in 54 | Navigation.makeParser (.href >> solver >> fst) 55 | ``` 56 | 57 | This parser only returns the matched route. The `address` record is discarded. 58 | However you probably need the address record for doing things with the query later. 59 | 60 | ## A parser that returns the parser result + Address 61 | 62 | ``` 63 | urlParserResultAddress : Navigation.Parser (Result String MainRoute, Address) 64 | urlParserResultAddress = 65 | let 66 | parse path = 67 | path 68 | |> UrlParser.parse identity routes 69 | 70 | solver = 71 | Hop.makeResolver configWithHash parse 72 | in 73 | Navigation.makeParser (.href >> solver) 74 | ``` 75 | 76 | This parser returns the result from `parse` e.g. `Result String MainRoute` and the address record. 77 | -------------------------------------------------------------------------------- /docs/navigating.md: -------------------------------------------------------------------------------- 1 | # Navigating 2 | 3 | ## Changing the location 4 | 5 | Use `Hop.outputFromPath` for changing the browser location. 6 | 7 | Add a message: 8 | 9 | ```elm 10 | type Msg 11 | ... 12 | | NavigateTo String 13 | ``` 14 | 15 | Trigger this message from you view: 16 | 17 | ```elm 18 | button [ onClick (NavigateTo "/users") ] [ text "Users" ] 19 | ``` 20 | 21 | React to this message in update: 22 | 23 | ```elm 24 | NavigateTo path -> 25 | let 26 | command = 27 | Hop.outputFromPath routerConfig path 28 | |> Navigation.newUrl 29 | in 30 | ( model, command ) 31 | ``` 32 | 33 | ## Changing the query string 34 | 35 | Add actions for changing the query string: 36 | 37 | ```elm 38 | type Msg 39 | = ... 40 | | AddQuery (Dict.Dict String String) 41 | | SetQuery (Dict.Dict String String) 42 | | ClearQuery 43 | ``` 44 | 45 | Change update to respond to these actions: 46 | 47 | ```elm 48 | import Hop exposing(addQuery, setQuery, clearQuery) 49 | 50 | update msg model = 51 | case msg of 52 | ... 53 | 54 | AddQuery query -> 55 | let 56 | command = 57 | model.address 58 | |> Hop.addQuery query 59 | |> Hop.output routerConfig 60 | |> Navigation.newUrl 61 | in 62 | (model, command) 63 | ``` 64 | 65 | You need to pass the current `address` record to these functions. 66 | Then you use that `address` record to generate a url using `output`. 67 | 68 | Trigger these messages from your views: 69 | 70 | ```elm 71 | button [ onClick (AddQuery (Dict.singleton "color" "red")) ] [ text "Set query" ] 72 | ``` 73 | 74 | See details of available functions at 75 | -------------------------------------------------------------------------------- /docs/nesting-routes.md: -------------------------------------------------------------------------------- 1 | # Nesting routes 2 | 3 | UrlParser supports nested routes: 4 | 5 | ```elm 6 | type UserRoute 7 | = UsersRoute 8 | | UserRoute UserId 9 | 10 | type MainRoute 11 | = HomeRoute 12 | | AboutRoute 13 | | UsersRoutes UserRoute 14 | | NotFoundRoute 15 | 16 | usersMatchers = 17 | [ UrlParser.format UserRoute (int) 18 | , UrlParser.format UsersRoute (s "") 19 | ] 20 | 21 | mainMatchers = 22 | [ UrlParser.format HomeRoute (s "") 23 | , UrlParser.format AboutRoute (s "about") 24 | , UrlParser.format UsersRoutes (s "users" (oneOf usersMatchers)) 25 | ] 26 | 27 | matchers = 28 | oneOf mainMatchers 29 | ``` 30 | 31 | With a setup like this UrlParser will be able to match routes like: 32 | 33 | - "" -> HomeRoute 34 | - "/about" -> AboutRoute 35 | - "/users" -> UsersRoutes UsersRoute 36 | - "/users/2" -> UsersRoutes (UserRoute 2) 37 | -------------------------------------------------------------------------------- /docs/reverse-routing.md: -------------------------------------------------------------------------------- 1 | # Reverse routing 2 | 3 | Reverse routing means converting a route tag back to an url e.g. 4 | 5 | ``` 6 | UserRoute 1 --> "/users/1" 7 | ``` 8 | 9 | In the current version Hop doesn't have any helpers for reverse routing. You can do this manually: 10 | 11 | ```elm 12 | reverse : Route -> String 13 | reverse route = 14 | case route of 15 | HomeRoute -> 16 | "" 17 | 18 | AboutRoute -> 19 | "about" 20 | 21 | UserRoute id -> 22 | "users/" ++ id 23 | 24 | NotFoundRoute -> 25 | "" 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/testing.md: -------------------------------------------------------------------------------- 1 | # Testing Hop 2 | 3 | ## Unit tests 4 | 5 | ```bash 6 | cd tests 7 | elm package install -y 8 | 9 | cd .. 10 | npm i 11 | npm test 12 | ``` 13 | -------------------------------------------------------------------------------- /docs/upgrade-2-to-3.md: -------------------------------------------------------------------------------- 1 | # Upgrading from 2 to 3 2 | 3 | Hop has changed in many ways in version 3. Please review the [Getting started guide](https://github.com/sporto/hop/blob/master/docs/getting-started.md). Following is an overview of the major changes. 4 | 5 | ## Routes 6 | 7 | Now routes can take values, instead of: 8 | 9 | ```elm 10 | type Route 11 | = Home 12 | | Users 13 | | User 14 | | Token 15 | ``` 16 | 17 | You can have values attached: 18 | 19 | ```elm 20 | type Route 21 | = Home 22 | | Users 23 | | User Int 24 | | Token String 25 | ``` 26 | 27 | Routes are now defined using matchers. So instead of 28 | 29 | ```elm 30 | routes = 31 | [ ("/users/:int", User) ] 32 | ``` 33 | 34 | You do: 35 | 36 | ```elm 37 | import Hop.Matchers exposing(match2) 38 | 39 | userMatcher = 40 | match2 User "/users/" int 41 | 42 | matchers = 43 | [userMatcher] 44 | ``` 45 | 46 | This is so we can have stronger types e.g. `User Int`. 47 | 48 | ## Actions 49 | 50 | Hop.signal now returns a tuple with `(Route, Location)`. Your application needs to map this to an action. e.g. 51 | 52 | ```elm 53 | 54 | type Action 55 | = HopAction () 56 | | ApplyRoute ( Route, Location ) 57 | 58 | taggedRouterSignal : Signal Action 59 | taggedRouterSignal = 60 | Signal.map ApplyRoute router.signal 61 | ``` 62 | 63 | This is so routes (`Route`) are different type than the application actions. 64 | 65 | ## Payload 66 | 67 | Before Hop returned a `payload` with a dictionary with matched parameters. 68 | 69 | Now it returns the matched route with the values e.g. `User 1` and a `Location` record in the form of a tuple: 70 | 71 | ```elm 72 | (User 1, location) 73 | ``` 74 | 75 | `location` has the information about the current path and the query: 76 | 77 | ```elm 78 | { 79 | path = ["users", "1"], 80 | query = 81 | } 82 | ``` 83 | 84 | ## Views 85 | 86 | In your views you don't need to 'search' for the correct parameter in the payload anymore. The parameters are now in the route e.g. `User 1`. 87 | 88 | So instead of doing: 89 | 90 | ```elm 91 | userId = 92 | model.routerPayload.params 93 | |> Dict.get "userId" 94 | |> Maybe.withDefault "" 95 | ``` 96 | 97 | You simply get the id from the route: 98 | 99 | ```elm 100 | case User userId -> 101 | ... 102 | ``` 103 | 104 | The query is still a dictionary. The query is now in the `location` record as shown above. 105 | -------------------------------------------------------------------------------- /docs/upgrade-3-to-4.md: -------------------------------------------------------------------------------- 1 | # Upgrading from 3 to 4 2 | 3 | Version 4 introduces push state. There are two major changes: 4 | 5 | ## Config 6 | 7 | Config now includes `hash` and `basePath`. 8 | 9 | ```elm 10 | routerConfig : Config Route 11 | routerConfig = 12 | { hash = True 13 | , basePath = "" 14 | , matchers = matchers 15 | , notFound = NotFoundRoute 16 | } 17 | ``` 18 | 19 | ## Functions require the router config 20 | 21 | Most functions in Hop now require your router config. For example instead of: 22 | 23 | ```elm 24 | navigateTo path 25 | addQuery query location 26 | ``` 27 | 28 | It is now: 29 | 30 | ```elm 31 | navigateTo config path 32 | addQuery config query location 33 | ``` 34 | 35 | This is because Hop needs to know if you are using hash or path routing. 36 | -------------------------------------------------------------------------------- /docs/upgrade-4-to-5.md: -------------------------------------------------------------------------------- 1 | # Upgrading from 4 to 5 2 | 3 | Version 5 works with Navigation and Elm 0.17 4 | 5 | ## Matchers 6 | 7 | All matchers stay the same as in version 4. 8 | 9 | ## Navigation 10 | 11 | Navigation is now handled by the Navigation module. See example app at `examples/basic/Main.elm`. 12 | Hop doesn't return effects / commands anymore, this should be done by passing a path to `Navigation.modifyUrl`. 13 | -------------------------------------------------------------------------------- /docs/upgrade-5-to-6.md: -------------------------------------------------------------------------------- 1 | # Upgrading from 5 to 6 2 | 3 | Version 6 removes Hop matchers. Use UrlParser instead. See Building Routes document for examples. 4 | -------------------------------------------------------------------------------- /elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "6.0.1", 3 | "summary": "Routing and Navigation helpers for SPAs in Elm", 4 | "repository": "https://github.com/sporto/hop.git", 5 | "license": "MIT", 6 | "source-directories": [ 7 | "examples", 8 | "src" 9 | ], 10 | "exposed-modules": [ 11 | "Hop", 12 | "Hop.Types" 13 | ], 14 | "dependencies": { 15 | "elm-lang/core": "4.0.0 <= v < 5.0.0", 16 | "evancz/elm-http": "3.0.1 <= v < 4.0.0" 17 | }, 18 | "elm-version": "0.16.0 <= v < 0.18.0" 19 | } 20 | -------------------------------------------------------------------------------- /examples/basic/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockefile 2 | elm-stuff 3 | -------------------------------------------------------------------------------- /examples/basic/.gitignore: -------------------------------------------------------------------------------- 1 | index.html 2 | -------------------------------------------------------------------------------- /examples/basic/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (..) 2 | 3 | {-| 4 | You will need Navigation, UrlParser and Hop. 5 | 6 | ``` 7 | elm package install elm-lang/navigation 8 | elm package install evancz/url-parser 9 | elm package install sporto/hop 10 | ``` 11 | -} 12 | 13 | import Html exposing (..) 14 | import Html.Attributes exposing (class) 15 | import Html.Events exposing (onClick) 16 | import Dict 17 | import Navigation 18 | import UrlParser exposing (()) 19 | import Hop 20 | import Hop.Types exposing (Config, Address, Query) 21 | 22 | 23 | -- ROUTES 24 | 25 | 26 | {-| 27 | Define your routes as union types. 28 | You need to provide a route for when the current URL doesn't match any known route i.e. NotFoundRoute. 29 | -} 30 | type Route 31 | = AboutRoute 32 | | MainRoute 33 | | NotFoundRoute 34 | 35 | 36 | {-| 37 | Define route matchers. 38 | See `docs/building-routes.md` for more examples. 39 | -} 40 | routes : UrlParser.Parser (Route -> a) a 41 | routes = 42 | UrlParser.oneOf 43 | [ UrlParser.format MainRoute (UrlParser.s "") 44 | , UrlParser.format AboutRoute (UrlParser.s "about") 45 | ] 46 | 47 | 48 | {-| 49 | Define your router configuration. 50 | 51 | Use `hash = True` for hash routing e.g. `#/users/1`. 52 | Use `hash = False` for push state e.g. `/users/1`. 53 | 54 | The `basePath` is only used for path routing. 55 | This is useful if you application is not located at the root of a url e.g. `/app/v1/users/1` where `/app/v1` is the base path. 56 | -} 57 | hopConfig : Config 58 | hopConfig = 59 | { hash = True 60 | , basePath = "" 61 | } 62 | 63 | 64 | 65 | -- MESSAGES 66 | 67 | 68 | {-| 69 | Add messages for navigation and changing the query. 70 | -} 71 | type Msg 72 | = NavigateTo String 73 | | SetQuery Query 74 | 75 | 76 | 77 | -- MODEL 78 | 79 | 80 | {-| 81 | Add the current route and address to your model. 82 | 83 | - `Route` is your Route union type defined above. 84 | - `Hop.Address` is record to aid with changing the query string. 85 | 86 | `route` will be used for determining the current route in the views. 87 | 88 | `address` is needed because: 89 | 90 | - Some navigation functions in Hop need this information to rebuild the current address. 91 | - Your views might need information about the current query string. 92 | 93 | -} 94 | type alias Model = 95 | { address : Address 96 | , route : Route 97 | } 98 | 99 | 100 | {-| 101 | Respond to navigation messages in update i.e. NavigateTo and SetQuery 102 | -} 103 | update : Msg -> Model -> ( Model, Cmd Msg ) 104 | update msg model = 105 | case (Debug.log "msg" msg) of 106 | NavigateTo path -> 107 | let 108 | command = 109 | -- First generate the URL using your config (`outputFromPath`). 110 | -- Then generate a command using Navigation.newUrl. 111 | Hop.outputFromPath hopConfig path 112 | |> Navigation.newUrl 113 | in 114 | ( model, command ) 115 | 116 | SetQuery query -> 117 | let 118 | command = 119 | -- First modify the current stored address record (setting the query) 120 | -- Then generate a URL using Hop.output 121 | -- Finally, create a command using Navigation.newUrl 122 | model.address 123 | |> Hop.setQuery query 124 | |> Hop.output hopConfig 125 | |> Navigation.newUrl 126 | in 127 | ( model, command ) 128 | 129 | 130 | {-| 131 | Create a URL Parser for Navigation 132 | -} 133 | urlParser : Navigation.Parser ( Route, Address ) 134 | urlParser = 135 | let 136 | -- A parse function takes the normalised path from Hop after taking 137 | -- in consideration the basePath and the hash. 138 | -- This function then returns a result. 139 | parse path = 140 | -- First we parse using UrlParser.parse. 141 | -- Then we return the parsed route or NotFoundRoute if the parsed failed. 142 | -- You can choose to return the parse return directly. 143 | path 144 | |> UrlParser.parse identity routes 145 | |> Result.withDefault NotFoundRoute 146 | 147 | resolver = 148 | -- Create a function that parses and formats the URL 149 | -- This function takes 2 arguments: The Hop Config and the parse function. 150 | Hop.makeResolver hopConfig parse 151 | in 152 | -- Create a Navigation URL parser 153 | Navigation.makeParser (.href >> resolver) 154 | 155 | 156 | {-| 157 | Navigation will call urlUpdate when the address changes. 158 | This function gets the result from `urlParser`, which is a tuple with (Route, Hop.Types.Address) 159 | 160 | Address is a record that has: 161 | 162 | ```elm 163 | { 164 | path: List String, 165 | query: Hop.Types.Query 166 | } 167 | ``` 168 | 169 | - `path` is an array of strings that has the current path e.g. `["users", "1"]` for `"/users/1"` 170 | - `query` Is dictionary of String String. You can access this information in your views to show the relevant content. 171 | 172 | We store these two things in our model. We keep the address because it is needed for matching a query string. 173 | 174 | -} 175 | urlUpdate : ( Route, Address ) -> Model -> ( Model, Cmd Msg ) 176 | urlUpdate ( route, address ) model = 177 | ( { model | route = route, address = address }, Cmd.none ) 178 | 179 | 180 | 181 | -- VIEWS 182 | 183 | 184 | view : Model -> Html Msg 185 | view model = 186 | div [] 187 | [ menu model 188 | , pageView model 189 | ] 190 | 191 | 192 | menu : Model -> Html Msg 193 | menu model = 194 | div [] 195 | [ div [] 196 | [ button 197 | [ class "btnMain" 198 | , onClick (NavigateTo "") 199 | ] 200 | [ text "Main" ] 201 | , button 202 | [ class "btnAbout" 203 | , onClick (NavigateTo "about") 204 | ] 205 | [ text "About" ] 206 | , button 207 | [ class "btnQuery" 208 | , onClick (SetQuery (Dict.singleton "keyword" "el/m")) 209 | ] 210 | [ text "Set query string" ] 211 | , currentQuery model 212 | ] 213 | ] 214 | 215 | 216 | currentQuery : Model -> Html msg 217 | currentQuery model = 218 | let 219 | query = 220 | toString model.address.query 221 | in 222 | span [ class "labelQuery" ] 223 | [ text query ] 224 | 225 | 226 | {-| 227 | Views can decide what to show using `model.route`. 228 | 229 | -} 230 | pageView : Model -> Html msg 231 | pageView model = 232 | case model.route of 233 | MainRoute -> 234 | div [] [ h2 [ class "title" ] [ text "Main" ] ] 235 | 236 | AboutRoute -> 237 | div [] [ h2 [ class "title" ] [ text "About" ] ] 238 | 239 | NotFoundRoute -> 240 | div [] [ h2 [ class "title" ] [ text "Not found" ] ] 241 | 242 | 243 | 244 | -- APP 245 | 246 | 247 | {-| 248 | Your init function will receive an initial payload from Navigation, this payload is the initial matched location. 249 | Here we store the `route` and `address` in our model. 250 | -} 251 | init : ( Route, Address ) -> ( Model, Cmd Msg ) 252 | init ( route, address ) = 253 | ( Model address route, Cmd.none ) 254 | 255 | 256 | {-| 257 | Wire everything using Navigation. 258 | -} 259 | main : Program Never 260 | main = 261 | Navigation.program urlParser 262 | { init = init 263 | , view = view 264 | , update = update 265 | , urlUpdate = urlUpdate 266 | , subscriptions = (always Sub.none) 267 | } 268 | -------------------------------------------------------------------------------- /examples/basic/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "../../src/" 8 | ], 9 | "exposed-modules": [], 10 | "dependencies": { 11 | "elm-lang/core": "4.0.0 <= v < 5.0.0", 12 | "elm-lang/html": "1.0.0 <= v < 2.0.0", 13 | "elm-lang/navigation": "1.0.0 <= v < 2.0.0", 14 | "evancz/elm-http": "3.0.1 <= v < 4.0.0", 15 | "evancz/url-parser": "1.0.0 <= v < 2.0.0" 16 | }, 17 | "elm-version": "0.16.0 <= v < 0.18.0" 18 | } 19 | -------------------------------------------------------------------------------- /examples/basic/install-packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | n=0 4 | until [ $n -ge 5 ] 5 | do 6 | elm package install -y && break 7 | n=$[$n+1] 8 | sleep 15 9 | done 10 | -------------------------------------------------------------------------------- /examples/basic/readme.md: -------------------------------------------------------------------------------- 1 | # Basic Hop Example 2 | 3 | - Install packages `elm package install -y` 4 | - Run `elm reactor` 5 | - Open `http://localhost:8000/Main.elm` 6 | -------------------------------------------------------------------------------- /examples/full/.gitignore: -------------------------------------------------------------------------------- 1 | index.html 2 | -------------------------------------------------------------------------------- /examples/full/dev_server.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var http = require('http'); 4 | var webpack = require('webpack'); 5 | var config = require('./webpack.config'); 6 | 7 | var app = express(); 8 | var compiler = webpack(config); 9 | var host = 'localhost'; 10 | var port = 3000; 11 | 12 | app.use(require('webpack-dev-middleware')(compiler, { 13 | // contentBase: 'src', 14 | noInfo: true, 15 | publicPath: config.output.publicPath, 16 | inline: true, 17 | stats: { colors: true }, 18 | })) 19 | 20 | app.get('/app', function(req, res) { 21 | res.sendFile(path.join(__dirname, 'public/index.html')); 22 | }); 23 | 24 | app.get('/app/*', function(req, res) { 25 | res.sendFile(path.join(__dirname, 'public/index.html')); 26 | }); 27 | 28 | // When hitting / redirect to app 29 | app.get('/', function(req, res) { 30 | res.redirect('/app'); 31 | }); 32 | 33 | // Server images 34 | app.use(express.static('public')); 35 | 36 | var server = http.createServer(app); 37 | 38 | server.listen(port, function(err) { 39 | if (err) { 40 | console.log(err); 41 | return; 42 | } 43 | 44 | var addr = server.address(); 45 | 46 | console.log('Listening at http://%s:%d', addr.address, addr.port); 47 | }) 48 | -------------------------------------------------------------------------------- /examples/full/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.0", 3 | "summary": "helpful summary of your project, less than 80 characters", 4 | "repository": "https://github.com/user/project.git", 5 | "license": "BSD3", 6 | "source-directories": [ 7 | "./src", 8 | "../../src/" 9 | ], 10 | "exposed-modules": [], 11 | "dependencies": { 12 | "elm-lang/core": "4.0.0 <= v < 5.0.0", 13 | "elm-lang/html": "1.0.0 <= v < 2.0.0", 14 | "elm-lang/navigation": "1.0.0 <= v < 2.0.0", 15 | "evancz/elm-http": "3.0.1 <= v < 4.0.0", 16 | "evancz/url-parser": "1.0.0 <= v < 2.0.0" 17 | }, 18 | "elm-version": "0.16.0 <= v < 0.18.0" 19 | } 20 | -------------------------------------------------------------------------------- /examples/full/install-packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | n=0 4 | until [ $n -ge 5 ] 5 | do 6 | elm package install -y && break 7 | n=$[$n+1] 8 | sleep 15 9 | done 10 | -------------------------------------------------------------------------------- /examples/full/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "full", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "elm.js", 6 | "scripts": { 7 | "dev": "node dev_server.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "ace-css": "^1.0.1", 13 | "css-loader": "^0.23.1", 14 | "elm-webpack-loader": "^3.0.1", 15 | "express": "^4.13.4", 16 | "file-loader": "^0.8.5", 17 | "style-loader": "^0.13.1", 18 | "url-loader": "^0.5.7", 19 | "webpack": "^1.12.14", 20 | "webpack-dev-middleware": "^1.5.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/full/public/images/csharp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporto/hop/3113fe73d78e4ce6841d414f0c138cf4eb0602f2/examples/full/public/images/csharp.png -------------------------------------------------------------------------------- /examples/full/public/images/elixir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporto/hop/3113fe73d78e4ce6841d414f0c138cf4eb0602f2/examples/full/public/images/elixir.png -------------------------------------------------------------------------------- /examples/full/public/images/elm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporto/hop/3113fe73d78e4ce6841d414f0c138cf4eb0602f2/examples/full/public/images/elm.png -------------------------------------------------------------------------------- /examples/full/public/images/go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporto/hop/3113fe73d78e4ce6841d414f0c138cf4eb0602f2/examples/full/public/images/go.png -------------------------------------------------------------------------------- /examples/full/public/images/haskell.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporto/hop/3113fe73d78e4ce6841d414f0c138cf4eb0602f2/examples/full/public/images/haskell.png -------------------------------------------------------------------------------- /examples/full/public/images/java.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporto/hop/3113fe73d78e4ce6841d414f0c138cf4eb0602f2/examples/full/public/images/java.png -------------------------------------------------------------------------------- /examples/full/public/images/js.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporto/hop/3113fe73d78e4ce6841d414f0c138cf4eb0602f2/examples/full/public/images/js.png -------------------------------------------------------------------------------- /examples/full/public/images/php.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporto/hop/3113fe73d78e4ce6841d414f0c138cf4eb0602f2/examples/full/public/images/php.png -------------------------------------------------------------------------------- /examples/full/public/images/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporto/hop/3113fe73d78e4ce6841d414f0c138cf4eb0602f2/examples/full/public/images/python.png -------------------------------------------------------------------------------- /examples/full/public/images/ruby.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporto/hop/3113fe73d78e4ce6841d414f0c138cf4eb0602f2/examples/full/public/images/ruby.png -------------------------------------------------------------------------------- /examples/full/public/images/rust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporto/hop/3113fe73d78e4ce6841d414f0c138cf4eb0602f2/examples/full/public/images/rust.png -------------------------------------------------------------------------------- /examples/full/public/images/swift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sporto/hop/3113fe73d78e4ce6841d414f0c138cf4eb0602f2/examples/full/public/images/swift.png -------------------------------------------------------------------------------- /examples/full/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hop Example 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/full/readme.md: -------------------------------------------------------------------------------- 1 | # Full Hop Example 2 | 3 | This example uses push state. 4 | 5 | To run: 6 | 7 | ``` 8 | elm package install -y 9 | npm i 10 | npm run dev 11 | ``` 12 | 13 | Open http://localhost:3000 14 | 15 | ## Webpack config 16 | 17 | The Webpack config included in this example is not ready for production. Please refer to the Webpack site. 18 | -------------------------------------------------------------------------------- /examples/full/src/Languages/Edit.elm: -------------------------------------------------------------------------------- 1 | module Languages.Edit exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Events exposing (on, targetValue) 5 | import Html.Attributes exposing (href, style, src, value, name) 6 | import Json.Decode as Json 7 | import Languages.Models exposing (..) 8 | import Languages.Messages exposing (..) 9 | 10 | 11 | styles : Html.Attribute a 12 | styles = 13 | style 14 | [ ( "float", "left" ) 15 | ] 16 | 17 | 18 | view : Language -> Html Msg 19 | view language = 20 | div [ styles ] 21 | [ h2 [] [ text language.name ] 22 | , form [] 23 | [ input 24 | [ value language.name 25 | , name "name" 26 | , on "input" (Json.map (Update language.id "name") targetValue) 27 | ] 28 | [] 29 | ] 30 | ] 31 | -------------------------------------------------------------------------------- /examples/full/src/Languages/Filter.elm: -------------------------------------------------------------------------------- 1 | module Languages.Filter exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Events exposing (onClick) 5 | import Html.Attributes exposing (id, class, href, style) 6 | import Dict 7 | import Languages.Messages exposing (..) 8 | 9 | 10 | type alias ViewModel = 11 | {} 12 | 13 | 14 | styles : Html.Attribute a 15 | styles = 16 | style 17 | [ ( "float", "left" ) 18 | , ( "margin-left", "2rem" ) 19 | , ( "margin-right", "2rem" ) 20 | ] 21 | 22 | 23 | view : ViewModel -> Html Msg 24 | view model = 25 | div [ styles ] 26 | [ h2 [] [ text "Filter" ] 27 | , btn "btnSetQuery" "SetQuery" (SetQuery (Dict.singleton "latests" "true")) 28 | , div [] 29 | [ h3 [] [ text "Kind" ] 30 | , div [] 31 | [ btn "btnAll" "All" (AddQuery (Dict.singleton "typed" "")) 32 | , btn "btnDynamic" "Dynamic" (AddQuery (Dict.singleton "typed" "dynamic")) 33 | , btn "btnStatic" "Static" (AddQuery (Dict.singleton "typed" "static")) 34 | ] 35 | ] 36 | ] 37 | 38 | 39 | btn : String -> String -> Msg -> Html Msg 40 | btn viewId label action = 41 | button [ id viewId, class "btn btn-primary btn-small inline-block mr1", onClick action ] 42 | [ text label ] 43 | -------------------------------------------------------------------------------- /examples/full/src/Languages/List.elm: -------------------------------------------------------------------------------- 1 | module Languages.List exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Events exposing (onClick) 5 | import Html.Attributes exposing (class, href, style) 6 | import Hop.Types exposing (Address) 7 | import Dict 8 | import Languages.Models exposing (..) 9 | import Languages.Messages exposing (..) 10 | 11 | 12 | type alias ViewModel = 13 | { languages : List Language 14 | , address : Address 15 | } 16 | 17 | 18 | styles : Html.Attribute a 19 | styles = 20 | style 21 | [ ( "float", "left" ) 22 | , ( "margin-right", "2rem" ) 23 | ] 24 | 25 | 26 | hasTag : String -> Language -> Bool 27 | hasTag tag language = 28 | List.any (\t -> t == tag) language.tags 29 | 30 | 31 | filteredLanguages : ViewModel -> List Language 32 | filteredLanguages model = 33 | let 34 | typed = 35 | model.address.query 36 | |> Dict.get "typed" 37 | |> Maybe.withDefault "" 38 | in 39 | case typed of 40 | "" -> 41 | model.languages 42 | 43 | _ -> 44 | List.filter (hasTag typed) model.languages 45 | 46 | 47 | view : ViewModel -> Html Msg 48 | view model = 49 | div [ styles ] 50 | [ h2 [] [ text "Languages" ] 51 | , table [] 52 | [ tbody [] (tableRows (filteredLanguages model)) ] 53 | ] 54 | 55 | 56 | tableRows : List Language -> List (Html Msg) 57 | tableRows collection = 58 | List.map rowView collection 59 | 60 | 61 | rowView : Language -> Html Msg 62 | rowView language = 63 | tr [] 64 | [ td [] [ text (toString language.id) ] 65 | , td [] 66 | [ text language.name 67 | ] 68 | , td [] 69 | [ actionBtn (Show language.id) "Show" 70 | , actionBtn (Edit language.id) "Edit" 71 | ] 72 | ] 73 | 74 | 75 | actionBtn : Msg -> String -> Html Msg 76 | actionBtn action label = 77 | button [ class "regular btn btn-small", onClick action ] 78 | [ text label ] 79 | -------------------------------------------------------------------------------- /examples/full/src/Languages/Messages.elm: -------------------------------------------------------------------------------- 1 | module Languages.Messages exposing (..) 2 | 3 | import Dict 4 | import Languages.Models exposing (..) 5 | 6 | 7 | type alias Prop = 8 | String 9 | 10 | 11 | type alias Value = 12 | String 13 | 14 | 15 | type Msg 16 | = Show LanguageId 17 | | Edit LanguageId 18 | | Update LanguageId Prop Value 19 | | AddQuery (Dict.Dict String String) 20 | | SetQuery (Dict.Dict String String) 21 | -------------------------------------------------------------------------------- /examples/full/src/Languages/Models.elm: -------------------------------------------------------------------------------- 1 | module Languages.Models exposing (..) 2 | 3 | 4 | type alias LanguageId = 5 | Int 6 | 7 | 8 | type alias Language = 9 | { id : LanguageId 10 | , name : String 11 | , image : String 12 | , tags : List String 13 | } 14 | 15 | 16 | 17 | -- ROUTING 18 | 19 | 20 | type Route 21 | = LanguagesRoute 22 | | LanguageRoute LanguageId 23 | | LanguageEditRoute LanguageId 24 | 25 | 26 | languages : List Language 27 | languages = 28 | [ { id = 1 29 | , name = "Elm" 30 | , image = "elm" 31 | , tags = [ "functional", "browser", "static" ] 32 | } 33 | , { id = 2 34 | , name = "JavaScript" 35 | , image = "js" 36 | , tags = [ "functional", "oo", "browser", "dynamic", "prototypical" ] 37 | } 38 | , { id = 3 39 | , name = "Go" 40 | , image = "go" 41 | , tags = [ "oo", "google", "static" ] 42 | } 43 | , { id = 4 44 | , name = "Rust" 45 | , image = "rust" 46 | , tags = [ "functional", "mozilla", "static" ] 47 | } 48 | , { id = 5 49 | , name = "Elixir" 50 | , image = "elixir" 51 | , tags = [ "functional", "erlang", "dynamic" ] 52 | } 53 | , { id = 6 54 | , name = "Ruby" 55 | , image = "ruby" 56 | , tags = [ "oo", "japan", "1996", "dynamic", "classical" ] 57 | } 58 | , { id = 7 59 | , name = "Python" 60 | , image = "python" 61 | , tags = [ "oo", "dynamic", "classical" ] 62 | } 63 | , { id = 8 64 | , name = "Swift" 65 | , image = "swift" 66 | , tags = [ "functional", "apple", "static", "classical" ] 67 | } 68 | , { id = 9 69 | , name = "Haskell" 70 | , image = "haskell" 71 | , tags = [ "functional", "static" ] 72 | } 73 | , { id = 10 74 | , name = "Java" 75 | , image = "java" 76 | , tags = [ "oo", "static", "classical" ] 77 | } 78 | , { id = 11 79 | , name = "C#" 80 | , image = "csharp" 81 | , tags = [ "oo", "microsoft", "static", "classical" ] 82 | } 83 | , { id = 12 84 | , name = "PHP" 85 | , image = "php" 86 | , tags = [ "oo", "server", "1994", "dynamic", "classical" ] 87 | } 88 | ] 89 | -------------------------------------------------------------------------------- /examples/full/src/Languages/Routing.elm: -------------------------------------------------------------------------------- 1 | module Languages.Routing exposing (..) 2 | 3 | import Languages.Models exposing (..) 4 | import UrlParser exposing ((), int, s, string) 5 | 6 | 7 | matchers : List (UrlParser.Parser (Route -> a) a) 8 | matchers = 9 | [ UrlParser.format LanguageEditRoute (int s "edit") 10 | , UrlParser.format LanguageRoute (int) 11 | , UrlParser.format LanguagesRoute (s "") 12 | ] 13 | 14 | 15 | toS : a -> String 16 | toS = 17 | toString 18 | 19 | 20 | reverseWithPrefix : Route -> String 21 | reverseWithPrefix route = 22 | "/languages" ++ (reverse route) 23 | 24 | 25 | reverse : Route -> String 26 | reverse route = 27 | case route of 28 | LanguagesRoute -> 29 | "/" 30 | 31 | LanguageRoute id -> 32 | "/" ++ (toS id) 33 | 34 | LanguageEditRoute id -> 35 | "/" ++ (toS id) ++ "/edit" 36 | -------------------------------------------------------------------------------- /examples/full/src/Languages/Show.elm: -------------------------------------------------------------------------------- 1 | module Languages.Show exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (id, href, style, src) 5 | import Languages.Models exposing (..) 6 | import Languages.Messages exposing (..) 7 | 8 | 9 | styles : Html.Attribute a 10 | styles = 11 | style 12 | [ ( "float", "left" ) 13 | ] 14 | 15 | 16 | view : Language -> Html Msg 17 | view language = 18 | div [ styles ] 19 | [ h2 [ id "titleLanguage" ] [ text language.name ] 20 | , img [ src ("/images/" ++ language.image ++ ".png") ] [] 21 | , tags language 22 | ] 23 | 24 | 25 | tags : Language -> Html Msg 26 | tags language = 27 | div [] (List.map tag language.tags) 28 | 29 | 30 | tag : String -> Html Msg 31 | tag tagName = 32 | span [] 33 | [ text (tagName ++ ", ") 34 | ] 35 | -------------------------------------------------------------------------------- /examples/full/src/Languages/Update.elm: -------------------------------------------------------------------------------- 1 | module Languages.Update exposing (..) 2 | 3 | import Debug 4 | import Navigation 5 | import Hop exposing (output, outputFromPath, addQuery, setQuery) 6 | import Hop.Types exposing (Config, Address) 7 | import Routing 8 | import Languages.Models exposing (..) 9 | import Languages.Messages exposing (Msg(..)) 10 | import Languages.Routing 11 | 12 | 13 | type alias UpdateModel = 14 | { languages : List Language 15 | , address : Address 16 | } 17 | 18 | 19 | routerConfig : Config 20 | routerConfig = 21 | Routing.config 22 | 23 | 24 | navigationCmd : String -> Cmd a 25 | navigationCmd path = 26 | path 27 | |> outputFromPath routerConfig 28 | |> Navigation.modifyUrl 29 | 30 | 31 | update : Msg -> UpdateModel -> ( UpdateModel, Cmd Msg ) 32 | update message model = 33 | case Debug.log "message" message of 34 | Show id -> 35 | let 36 | path = 37 | Languages.Routing.reverseWithPrefix (Languages.Models.LanguageRoute id) 38 | in 39 | ( model, navigationCmd path ) 40 | 41 | Edit id -> 42 | let 43 | path = 44 | Languages.Routing.reverseWithPrefix (Languages.Models.LanguageEditRoute id) 45 | in 46 | ( model, navigationCmd path ) 47 | 48 | Update id prop value -> 49 | let 50 | udpatedLanguages = 51 | List.map (updateWithId id prop value) model.languages 52 | in 53 | ( { model | languages = udpatedLanguages }, Cmd.none ) 54 | 55 | AddQuery query -> 56 | let 57 | command = 58 | model.address 59 | |> addQuery query 60 | |> output routerConfig 61 | |> Navigation.modifyUrl 62 | in 63 | ( model, command ) 64 | 65 | SetQuery query -> 66 | let 67 | command = 68 | model.address 69 | |> setQuery query 70 | |> output routerConfig 71 | |> Navigation.modifyUrl 72 | in 73 | ( model, command ) 74 | 75 | 76 | updateWithId : LanguageId -> String -> String -> Language -> Language 77 | updateWithId id prop value language = 78 | if id == language.id then 79 | case prop of 80 | "name" -> 81 | { language | name = value } 82 | 83 | _ -> 84 | language 85 | else 86 | language 87 | -------------------------------------------------------------------------------- /examples/full/src/Languages/View.elm: -------------------------------------------------------------------------------- 1 | module Languages.View exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.Attributes exposing (href, style) 5 | import Hop.Types exposing (Address) 6 | import Languages.Models exposing (LanguageId, Language, Route, Route(..)) 7 | import Languages.Messages exposing (..) 8 | import Languages.Filter 9 | import Languages.List 10 | import Languages.Show 11 | import Languages.Edit 12 | 13 | 14 | type alias ViewModel = 15 | { languages : List Language 16 | , address : Address 17 | , route : Route 18 | } 19 | 20 | 21 | containerStyle : Html.Attribute a 22 | containerStyle = 23 | style 24 | [ ( "margin-bottom", "5rem" ) 25 | , ( "overflow", "auto" ) 26 | ] 27 | 28 | 29 | view : ViewModel -> Html Msg 30 | view model = 31 | div [ containerStyle ] 32 | [ Languages.Filter.view {} 33 | , Languages.List.view { languages = model.languages, address = model.address } 34 | , subView model 35 | ] 36 | 37 | 38 | subView : ViewModel -> Html Msg 39 | subView model = 40 | case model.route of 41 | LanguageRoute languageId -> 42 | let 43 | maybeLanguage = 44 | getLanguage model.languages languageId 45 | in 46 | case maybeLanguage of 47 | Just language -> 48 | Languages.Show.view language 49 | 50 | Nothing -> 51 | notFoundView model 52 | 53 | LanguageEditRoute languageId -> 54 | let 55 | maybeLanguage = 56 | getLanguage model.languages languageId 57 | in 58 | case maybeLanguage of 59 | Just language -> 60 | Languages.Edit.view language 61 | 62 | _ -> 63 | notFoundView model 64 | 65 | LanguagesRoute -> 66 | emptyView 67 | 68 | 69 | emptyView : Html msg 70 | emptyView = 71 | span [] [] 72 | 73 | 74 | notFoundView : ViewModel -> Html msg 75 | notFoundView model = 76 | div [] 77 | [ text "Not Found" 78 | ] 79 | 80 | 81 | getLanguage : List Language -> LanguageId -> Maybe Language 82 | getLanguage languages id = 83 | languages 84 | |> List.filter (\lang -> lang.id == id) 85 | |> List.head 86 | -------------------------------------------------------------------------------- /examples/full/src/Main.elm: -------------------------------------------------------------------------------- 1 | module Main exposing (..) 2 | 3 | import Navigation 4 | import Hop 5 | import Hop.Types exposing (Address) 6 | import Messages exposing (..) 7 | import Models exposing (..) 8 | import Update exposing (..) 9 | import View exposing (..) 10 | import Routing 11 | import String 12 | import UrlParser 13 | 14 | 15 | urlParser : Navigation.Parser ( Route, Address ) 16 | urlParser = 17 | let 18 | parse path = 19 | path 20 | |> UrlParser.parse identity Routing.routes 21 | |> Result.withDefault NotFoundRoute 22 | 23 | matcher = 24 | Hop.makeResolver Routing.config parse 25 | in 26 | Navigation.makeParser (.href >> matcher) 27 | 28 | 29 | urlUpdate : ( Route, Address ) -> AppModel -> ( AppModel, Cmd Msg ) 30 | urlUpdate ( route, address ) model = 31 | let 32 | _ = 33 | Debug.log "urlUpdate address" address 34 | in 35 | ( { model | route = route, address = address }, Cmd.none ) 36 | 37 | 38 | init : ( Route, Address ) -> ( AppModel, Cmd Msg ) 39 | init ( route, address ) = 40 | ( newAppModel route address, Cmd.none ) 41 | 42 | 43 | main : Program Never 44 | main = 45 | Navigation.program urlParser 46 | { init = init 47 | , view = view 48 | , update = update 49 | , urlUpdate = urlUpdate 50 | , subscriptions = (always Sub.none) 51 | } 52 | -------------------------------------------------------------------------------- /examples/full/src/Messages.elm: -------------------------------------------------------------------------------- 1 | module Messages exposing (..) 2 | 3 | import Hop.Types exposing (Query) 4 | import Languages.Messages 5 | 6 | 7 | type Msg 8 | = SetQuery Query 9 | | LanguagesMsg Languages.Messages.Msg 10 | | ShowHome 11 | | ShowLanguages 12 | | ShowAbout 13 | -------------------------------------------------------------------------------- /examples/full/src/Models.elm: -------------------------------------------------------------------------------- 1 | module Models exposing (..) 2 | 3 | import Hop.Types exposing (Address, newAddress) 4 | import Languages.Models exposing (Language, languages) 5 | 6 | 7 | type Route 8 | = HomeRoute 9 | | AboutRoute 10 | | LanguagesRoutes Languages.Models.Route 11 | | NotFoundRoute 12 | 13 | 14 | type alias AppModel = 15 | { languages : List Language 16 | , address : Address 17 | , route : Route 18 | , selectedLanguage : Maybe Language 19 | } 20 | 21 | 22 | newAppModel : Route -> Address -> AppModel 23 | newAppModel route address = 24 | { languages = languages 25 | , address = address 26 | , route = route 27 | , selectedLanguage = Maybe.Nothing 28 | } 29 | -------------------------------------------------------------------------------- /examples/full/src/Routing.elm: -------------------------------------------------------------------------------- 1 | module Routing exposing (..) 2 | 3 | import Models exposing (..) 4 | import Hop.Types exposing (Config) 5 | import Languages.Routing 6 | import UrlParser exposing ((), oneOf, int, s) 7 | import Languages.Routing 8 | 9 | 10 | matchers : List (UrlParser.Parser (Route -> a) a) 11 | matchers = 12 | [ UrlParser.format HomeRoute (s "") 13 | , UrlParser.format AboutRoute (s "about") 14 | , UrlParser.format LanguagesRoutes (s "languages" (oneOf Languages.Routing.matchers)) 15 | ] 16 | 17 | 18 | routes : UrlParser.Parser (Route -> a) a 19 | routes = 20 | oneOf matchers 21 | 22 | 23 | config : Config 24 | config = 25 | { basePath = "/app" 26 | , hash = False 27 | } 28 | 29 | reverse : Route -> String 30 | reverse route = 31 | case route of 32 | HomeRoute -> 33 | "" 34 | 35 | AboutRoute -> 36 | "about" 37 | 38 | LanguagesRoutes subRoute -> 39 | "languages" ++ Languages.Routing.reverse subRoute 40 | 41 | NotFoundRoute -> 42 | "" 43 | -------------------------------------------------------------------------------- /examples/full/src/Update.elm: -------------------------------------------------------------------------------- 1 | module Update exposing (..) 2 | 3 | import Debug 4 | import Navigation 5 | import Hop exposing (output, outputFromPath, setQuery) 6 | import Hop.Types exposing (Config) 7 | import Messages exposing (..) 8 | import Models exposing (..) 9 | import Routing 10 | import Languages.Update 11 | import Languages.Models 12 | 13 | 14 | navigationCmd : String -> Cmd a 15 | navigationCmd path = 16 | path 17 | |> outputFromPath Routing.config 18 | |> Navigation.newUrl 19 | 20 | 21 | routerConfig : Config 22 | routerConfig = 23 | Routing.config 24 | 25 | 26 | update : Msg -> AppModel -> ( AppModel, Cmd Msg ) 27 | update message model = 28 | case Debug.log "message" message of 29 | LanguagesMsg subMessage -> 30 | let 31 | updateModel = 32 | { languages = model.languages 33 | , address = model.address 34 | } 35 | 36 | ( updatedModel, cmd ) = 37 | Languages.Update.update subMessage updateModel 38 | in 39 | ( { model | languages = updatedModel.languages }, Cmd.map LanguagesMsg cmd ) 40 | 41 | SetQuery query -> 42 | let 43 | command = 44 | model.address 45 | |> setQuery query 46 | |> output routerConfig 47 | |> Navigation.newUrl 48 | in 49 | ( model, command ) 50 | 51 | ShowHome -> 52 | let 53 | path = 54 | Routing.reverse HomeRoute 55 | in 56 | ( model, navigationCmd path ) 57 | 58 | ShowLanguages -> 59 | let 60 | path = 61 | Routing.reverse (LanguagesRoutes Languages.Models.LanguagesRoute) 62 | in 63 | ( model, navigationCmd path ) 64 | 65 | ShowAbout -> 66 | let 67 | path = 68 | Routing.reverse AboutRoute 69 | in 70 | ( model, navigationCmd path ) 71 | -------------------------------------------------------------------------------- /examples/full/src/View.elm: -------------------------------------------------------------------------------- 1 | module View exposing (..) 2 | 3 | import Html exposing (..) 4 | import Html.App 5 | import Html.Events exposing (onClick) 6 | import Html.Attributes exposing (id, class, href, style) 7 | import Models exposing (..) 8 | import Messages exposing (..) 9 | import Languages.View 10 | 11 | 12 | view : AppModel -> Html Msg 13 | view model = 14 | div [] 15 | [ menu model 16 | , pageView model 17 | ] 18 | 19 | 20 | menu : AppModel -> Html Msg 21 | menu model = 22 | div [ class "p2 white bg-black" ] 23 | [ div [] 24 | [ menuLink ShowHome "btnHome" "Home" 25 | , text "|" 26 | , menuLink ShowLanguages "btnLanguages" "Languages" 27 | , text "|" 28 | , menuLink ShowAbout "btnAbout" "About" 29 | ] 30 | ] 31 | 32 | 33 | menuLink : Msg -> String -> String -> Html Msg 34 | menuLink message viewId label = 35 | a 36 | [ id viewId 37 | , href "javascript://" 38 | , onClick message 39 | , class "white px2" 40 | ] 41 | [ text label ] 42 | 43 | 44 | pageView : AppModel -> Html Msg 45 | pageView model = 46 | case model.route of 47 | HomeRoute -> 48 | div [ class "p2" ] 49 | [ h1 [ id "title", class "m0" ] [ text "Home" ] 50 | , div [] [ text "Click on Languages to start routing" ] 51 | ] 52 | 53 | AboutRoute -> 54 | div [ class "p2" ] 55 | [ h1 [ id "title", class "m0" ] [ text "About" ] 56 | ] 57 | 58 | LanguagesRoutes languagesRoute -> 59 | let 60 | viewModel = 61 | { languages = model.languages 62 | , route = languagesRoute 63 | , address = model.address 64 | } 65 | in 66 | div [ class "p2" ] 67 | [ h1 [ id "title", class "m0" ] [ text "Languages" ] 68 | , Html.App.map LanguagesMsg (Languages.View.view viewModel) 69 | ] 70 | 71 | NotFoundRoute -> 72 | notFoundView model 73 | 74 | 75 | notFoundView : AppModel -> Html msg 76 | notFoundView model = 77 | div [] 78 | [ text "Not Found" 79 | ] 80 | -------------------------------------------------------------------------------- /examples/full/src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('ace-css/css/ace.css'); 4 | 5 | var Elm = require('./Main.elm'); 6 | var mountNode = document.getElementById('main'); 7 | 8 | var app = Elm.Main.embed(mountNode); 9 | -------------------------------------------------------------------------------- /examples/full/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require("path"); 2 | 3 | /* 4 | publicPath is used for finding the bundles during dev 5 | e.g. http://localhost:3000/bundles/app.js 6 | When the index.html is served using the webpack server then just specify the path. 7 | When index.html is served using a framework e.g. from Rails, Phoenix or Go 8 | then you must specify the full url where the webpack dev server is running e.g. http://localhost:4000/bundles/ 9 | This path is also used for resolving relative assets e.g. fonts from css. So for production and staging this path has to be 10 | overriden. See webpack.prod.config.js 11 | */ 12 | var publicPath = '/bundles/' 13 | 14 | module.exports = { 15 | entry: { 16 | app: [ 17 | './src/index.js' 18 | ] 19 | }, 20 | 21 | output: { 22 | path: path.resolve(__dirname + '/dist'), 23 | filename: '[name].js', 24 | publicPath: publicPath, 25 | }, 26 | 27 | module: { 28 | loaders: [ 29 | { 30 | test: /\.(css|scss)$/, 31 | loaders: [ 32 | 'style-loader', 33 | 'css-loader', 34 | ] 35 | }, 36 | { 37 | test: /\.html$/, 38 | exclude: /node_modules/, 39 | loader: 'file?name=[name].[ext]', 40 | }, 41 | { 42 | test: /\.elm$/, 43 | exclude: [/elm-stuff/, /node_modules/], 44 | loader: 'elm-webpack', 45 | }, 46 | { 47 | test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/, 48 | loader: 'url-loader?limit=10000&minetype=application/font-woff', 49 | }, 50 | { 51 | test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/, 52 | loader: 'file-loader', 53 | }, 54 | ], 55 | 56 | noParse: /\.elm$/, 57 | }, 58 | 59 | }; 60 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-present, Sebastian Porto 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hop", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "", 6 | "scripts": { 7 | "test": "elm-test" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "elm-test": "^0.17.3" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Hop: Navigation and routing helpers for Elm SPAs 2 | 3 | [![Build Status](https://semaphoreci.com/api/v1/sporto/hop/branches/master/badge.svg)](https://semaphoreci.com/sporto/hop) 4 | 5 | ![alt Hop](https://raw.githubusercontent.com/sporto/hop/master/assets/logo.png) 6 | 7 | __With the release of Elm 0.18 the official libraries `Navigation` and `UrlParser` have become a lot more robust and useful. They now integrate a lot of the functionality that Hop used to provide for previous versions of Elm. For example `UrlParser` now has `parseHash` and `parsePath`. Because of this I'm not convinced that Hop needs to be upgraded to Elm 0.18. I'll wait and see if this library could provide value in 0.18.__ 8 | 9 | Hop is a helper library meant to be used with: 10 | 11 | - [__Navigation v1__](http://package.elm-lang.org/packages/elm-lang/navigation) for listening to location changes in the browser and pushing changes to it. 12 | - [__UrlParser v1__](http://package.elm-lang.org/packages/evancz/url-parser) for constructing routes and parsing URLs. 13 | 14 | ## What Hop provides 15 | 16 | On top of these two packages above, Hop helps with: 17 | 18 | - Abstracting the differences between push or hash routing 19 | - Providing helpers for working with the query string 20 | - Encode / Decode the location path 21 | - Encode / Decode the query string 22 | 23 | ## Getting Started 24 | 25 | Please see this [example app](https://github.com/sporto/hop/blob/master/examples/basic/Main.elm). It explains how to wire everything in the comments. 26 | 27 | ## Docs 28 | 29 | ### [Building routes](https://github.com/sporto/hop/blob/master/docs/building-routes.md) 30 | ### [Nesting routes](https://github.com/sporto/hop/blob/master/docs/nesting-routes.md) 31 | ### [Matching routes](https://github.com/sporto/hop/blob/master/docs/matching-routes.md) 32 | ### [Navigating](https://github.com/sporto/hop/blob/master/docs/navigating.md) 33 | ### [Reverse routing](https://github.com/sporto/hop/blob/master/docs/reverse-routing.md) 34 | ### [API](http://package.elm-lang.org/packages/sporto/hop/latest/) 35 | ### [Changelog](./docs/changelog.md) 36 | ### [Testing Hop](https://github.com/sporto/hop/blob/master/docs/testing.md) 37 | 38 | ## More docs 39 | 40 | ### [Upgrade guide 5 to 6](https://github.com/sporto/hop/blob/master/docs/upgrade-5-to-6.md) 41 | ### [Upgrade guide 4 to 5](https://github.com/sporto/hop/blob/master/docs/upgrade-4-to-5.md) 42 | ### [Upgrade guide 3 to 4](https://github.com/sporto/hop/blob/master/docs/upgrade-3-to-4.md) 43 | ### [Upgrade guide 2.1 to 3.0](https://github.com/sporto/hop/blob/master/docs/upgrade-2-to-3.md) 44 | 45 | ### [Version 5 documentation](https://github.com/sporto/hop/tree/v5) 46 | ### [Version 4 documentation](https://github.com/sporto/hop/tree/v4) 47 | ### [Version 3 documentation](https://github.com/sporto/hop/tree/v3) 48 | ### [Version 2 documentation](https://github.com/sporto/hop/tree/v2) 49 | 50 | ### Hash routing 51 | 52 | A proper url should have the query before the hash e.g. `?keyword=Ja#/users/1`, 53 | but when using hash routing, query parameters are appended after the hash path e.g. `#/users/1?keyword=Ja`. 54 | This is done for aesthetics and so the router is fully controlled by the hash fragment. 55 | 56 | ## Examples 57 | 58 | See `examples/basic` and `examples/full` folders. To run the example apps: 59 | 60 | - Clone this repo 61 | - Go to example folder 62 | - Follow the readme in that folder 63 | -------------------------------------------------------------------------------- /src/Hop.elm: -------------------------------------------------------------------------------- 1 | module Hop 2 | exposing 3 | ( addQuery 4 | , clearQuery 5 | , ingest 6 | , makeResolver 7 | , output 8 | , outputFromPath 9 | , pathFromAddress 10 | , queryFromAddress 11 | , removeQuery 12 | , setQuery 13 | ) 14 | 15 | {-| Navigation and routing utilities for single page applications. See [readme](https://github.com/sporto/hop) for usage. 16 | 17 | # Consuming an URL from the browser 18 | @docs ingest, makeResolver 19 | 20 | # Preparing a URL for changing the browser location 21 | @docs output, outputFromPath 22 | 23 | # Work with an Address record 24 | @docs pathFromAddress, queryFromAddress 25 | 26 | # Modify the query string 27 | @docs addQuery, setQuery, removeQuery, clearQuery 28 | 29 | -} 30 | 31 | import Dict 32 | import String 33 | import Hop.Address 34 | import Hop.In 35 | import Hop.Out 36 | import Hop.Types exposing (Address, Config, Query) 37 | 38 | 39 | --------------------------------------- 40 | -- INGEST 41 | --------------------------------------- 42 | 43 | 44 | {-| 45 | Convert a raw url to an Address record. Use this function for 'normalizing' the URL before parsing it. 46 | This conversion will take in account your basePath and hash configuration. 47 | 48 | E.g. with path routing 49 | 50 | config = 51 | { basePath = "" 52 | , hash = False 53 | } 54 | 55 | ingest config "http://localhost:3000/app/languages/1?k=1" 56 | --> 57 | { path = ["app", "languages", "1" ], query = Dict.singleton "k" "1" } 58 | 59 | E.g. with path routing and base path 60 | 61 | config = 62 | { basePath = "/app/v1" 63 | , hash = False 64 | } 65 | 66 | ingest config "http://localhost:3000/app/v1/languages/1?k=1" 67 | --> 68 | { path = ["languages", "1" ], query = Dict.singleton "k" "1" } 69 | 70 | E.g. with hash routing 71 | 72 | config = 73 | { basePath = "" 74 | , hash = True 75 | } 76 | 77 | ingest config "http://localhost:3000/app#/languages/1?k=1" 78 | --> 79 | { path = ["languages", "1" ], query = Dict.singleton "k" "1" } 80 | -} 81 | ingest : Config -> String -> Address 82 | ingest = 83 | Hop.In.ingest 84 | 85 | 86 | {-| 87 | `makeResolver` normalizes the URL using your config and then gives that normalised URL to your parser. 88 | 89 | Use this for creating a function to give to `Navigation.makeParser`. 90 | See examples in `docs/matching-routes.md`. 91 | 92 | Hop.makeResolver hopConfig parse 93 | 94 | `makeResolver` takes 2 arguments. 95 | 96 | ### Config e.g. 97 | 98 | { basePath = "" 99 | , hash = False 100 | } 101 | 102 | ### Parse function 103 | 104 | A function that receives the normalised path and returns the result of parsing it. 105 | 106 | parse path = 107 | path 108 | |> UrlParser.parse identity routes 109 | |> Result.withDefault NotFoundRoute 110 | 111 | You parse function will receive the path like this: 112 | 113 | `http://example.com/users/1` --> 'users/1/' 114 | 115 | So it won't have a leading /, but it will have a trailing /. This is because the way UrlParse works. 116 | 117 | ### Return value from resolver 118 | 119 | After being called with a URL the resolver will return a tuple with `(parse result, address)` e.g. 120 | 121 | resolver = 122 | Hop.makeResolver hopConfig parse 123 | 124 | resolver "http://example.com/index.html#/users/2" 125 | 126 | --> 127 | 128 | ( UserRoute 2, { path = ["users", "2"], query = ...} ) 129 | 130 | ### Example 131 | 132 | A complete example looks like: 133 | 134 | urlParser : Navigation.Parser ( Route, Address ) 135 | urlParser = 136 | let 137 | parse path = 138 | path 139 | |> UrlParser.parse identity routes 140 | |> Result.withDefault NotFoundRoute 141 | 142 | resolver = 143 | Hop.makeResolver hopConfig parse 144 | in 145 | Navigation.makeParser (.href >> resolver) 146 | 147 | -} 148 | makeResolver : 149 | Config 150 | -> (String -> result) 151 | -> String 152 | -> (result, Address) 153 | makeResolver config parse rawInput = 154 | let 155 | address = 156 | rawInput 157 | |> ingest config 158 | 159 | parseResult = 160 | pathFromAddress address 161 | ++ "/" 162 | |> String.dropLeft 1 163 | |> parse 164 | in 165 | (parseResult, address) 166 | 167 | 168 | 169 | --------------------------------------- 170 | -- CREATE OUTBOUND URLs 171 | --------------------------------------- 172 | 173 | 174 | {-| 175 | Convert an Address record to an URL to feed the browser. 176 | This will take in account your basePath and hash config. 177 | 178 | E.g. with path routing 179 | 180 | output config { path = ["languages", "1" ], query = Dict.singleton "k" "1" } 181 | --> 182 | "/languages/1?k=1" 183 | 184 | E.g. with hash routing 185 | 186 | output config { path = ["languages", "1" ], query = Dict.singleton "k" "1" } 187 | --> 188 | "#/languages/1?k=1" 189 | -} 190 | output : Config -> Address -> String 191 | output = 192 | Hop.Out.output 193 | 194 | 195 | {-| 196 | Convert a string to an URL to feed the browser. 197 | This will take in account your basePath and hash config. 198 | 199 | E.g. with path routing 200 | 201 | outputFromPath config "/languages/1?k=1" 202 | --> 203 | "/languages/1?k=1" 204 | 205 | E.g. with path routing + basePath 206 | 207 | outputFromPath config "/languages/1?k=1" 208 | --> 209 | "/app/languages/1?k=1" 210 | 211 | E.g. with hash routing 212 | 213 | output config "/languages/1?k=1" 214 | --> 215 | "#/languages/1?k=1" 216 | -} 217 | outputFromPath : Config -> String -> String 218 | outputFromPath = 219 | Hop.Out.outputFromPath 220 | 221 | 222 | 223 | --------------------------------------- 224 | -- WORK WITH ADDRESS 225 | --------------------------------------- 226 | 227 | 228 | {-| 229 | Get the path as a string from an Address record. 230 | 231 | address = { path = ["languages", "1" ], query = Dict.singleton "k" "1" } 232 | 233 | pathFromAddress address 234 | --> 235 | "/languages/1" 236 | -} 237 | pathFromAddress : Address -> String 238 | pathFromAddress = 239 | Hop.Address.getPath 240 | 241 | 242 | {-| 243 | Get the query as a string from an Address record. 244 | 245 | address = { path = ["app"], query = Dict.singleton "k" "1" } 246 | 247 | queryFromAddress address 248 | --> 249 | "?k=1" 250 | -} 251 | queryFromAddress : Address -> String 252 | queryFromAddress = 253 | Hop.Address.getQuery 254 | 255 | 256 | 257 | ------------------------------------------------------------------------------- 258 | -- QUERY MUTATION 259 | ------------------------------------------------------------------------------- 260 | 261 | 262 | {-| 263 | Add query string values (patches any existing values) to an Address record. 264 | 265 | addQuery query address 266 | 267 | addQuery (Dict.Singleton "b" "2") { path = [], query = Dict.fromList [("a", "1")] } 268 | 269 | == 270 | 271 | { path = [], query = Dict.fromList [("a", "1"), ("b", "2")] } 272 | 273 | - query is a dictionary with keys to add 274 | 275 | To remove a key / value pair set the value to "" 276 | -} 277 | addQuery : Query -> Address -> Address 278 | addQuery query location = 279 | let 280 | updatedQuery = 281 | Dict.union query location.query 282 | in 283 | { location | query = updatedQuery } 284 | 285 | 286 | {-| 287 | Set the whole query string (removes any existing values). 288 | 289 | setQuery query address 290 | -} 291 | setQuery : Query -> Address -> Address 292 | setQuery query location = 293 | { location | query = query } 294 | 295 | 296 | {-| 297 | Remove one key from the query string 298 | 299 | removeQuery key address 300 | -} 301 | removeQuery : String -> Address -> Address 302 | removeQuery key location = 303 | let 304 | updatedQuery = 305 | Dict.remove key location.query 306 | in 307 | { location | query = updatedQuery } 308 | 309 | 310 | {-| Clear all query string values 311 | 312 | clearQuery address 313 | -} 314 | clearQuery : Address -> Address 315 | clearQuery location = 316 | { location | query = Dict.empty } 317 | -------------------------------------------------------------------------------- /src/Hop/Address.elm: -------------------------------------------------------------------------------- 1 | module Hop.Address exposing (..) 2 | 3 | import Dict 4 | import String 5 | import Http exposing (uriEncode, uriDecode) 6 | import Hop.Types exposing (..) 7 | 8 | {-| 9 | Get the Path 10 | -} 11 | getPath : Address -> String 12 | getPath address = 13 | address.path 14 | |> List.map uriEncode 15 | |> String.join "/" 16 | |> String.append "/" 17 | 18 | 19 | {-| 20 | Get the query string from a Address. 21 | Including ? 22 | -} 23 | getQuery : Address -> String 24 | getQuery address = 25 | if Dict.isEmpty address.query then 26 | "" 27 | else 28 | address.query 29 | |> Dict.toList 30 | |> List.map (\( k, v ) -> ( uriEncode k, uriEncode v )) 31 | |> List.map (\( k, v ) -> k ++ "=" ++ v) 32 | |> String.join "&" 33 | |> String.append "?" 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- 38 | -- PARSING 39 | -- Parse a path into a Address 40 | -------------------------------------------------------------------------------- 41 | 42 | 43 | parse : String -> Address 44 | parse route = 45 | { path = parsePath route 46 | , query = parseQuery route 47 | } 48 | 49 | 50 | extractPath : String -> String 51 | extractPath route = 52 | route 53 | |> String.split "#" 54 | |> List.reverse 55 | |> List.head 56 | |> Maybe.withDefault "" 57 | |> String.split "?" 58 | |> List.head 59 | |> Maybe.withDefault "" 60 | 61 | 62 | parsePath : String -> List String 63 | parsePath route = 64 | route 65 | |> extractPath 66 | |> String.split "/" 67 | |> List.filter (\segment -> not (String.isEmpty segment)) 68 | |> List.map uriDecode 69 | 70 | 71 | extractQuery : String -> String 72 | extractQuery route = 73 | route 74 | |> String.split "?" 75 | |> List.drop 1 76 | |> List.head 77 | |> Maybe.withDefault "" 78 | 79 | 80 | parseQuery : String -> Query 81 | parseQuery route = 82 | route 83 | |> extractQuery 84 | |> String.split "&" 85 | |> List.filter (not << String.isEmpty) 86 | |> List.map queryKVtoTuple 87 | |> Dict.fromList 88 | 89 | 90 | {-| @priv 91 | Convert a string to a tuple. Decode on the way. 92 | 93 | "k=1" --> ("k", "1") 94 | -} 95 | queryKVtoTuple : String -> ( String, String ) 96 | queryKVtoTuple kv = 97 | let 98 | splitted = 99 | kv 100 | |> String.split "=" 101 | 102 | first = 103 | splitted 104 | |> List.head 105 | |> Maybe.withDefault "" 106 | 107 | firstDecoded = 108 | uriDecode first 109 | 110 | second = 111 | splitted 112 | |> List.drop 1 113 | |> List.head 114 | |> Maybe.withDefault "" 115 | 116 | secondDecoded = 117 | uriDecode second 118 | in 119 | ( firstDecoded, secondDecoded ) 120 | -------------------------------------------------------------------------------- /src/Hop/AddressTest.elm: -------------------------------------------------------------------------------- 1 | module Hop.AddressTest exposing (..) 2 | 3 | import Dict 4 | import Expect 5 | import Hop.Address as Address 6 | import Hop.Types as Types 7 | import Test exposing (..) 8 | 9 | 10 | getPathTest : Test 11 | getPathTest = 12 | let 13 | inputs = 14 | [ ( "it works" 15 | , { path = [ "users", "1" ], query = Dict.singleton "k" "1" } 16 | , "/users/1" 17 | ) 18 | , ( "it encodes" 19 | , { path = [ "us/ers", "1" ], query = Dict.empty } 20 | , "/us%2Fers/1" 21 | ) 22 | ] 23 | 24 | run ( testCase, address, expected ) = 25 | test testCase 26 | <| \() -> 27 | let 28 | actual = 29 | Address.getPath address 30 | in 31 | Expect.equal expected actual 32 | in 33 | describe "getPath" (List.map run inputs) 34 | 35 | 36 | getQuery : Test 37 | getQuery = 38 | let 39 | inputs = 40 | [ ( "it works" 41 | , { path = [], query = Dict.singleton "k" "1" } 42 | , "?k=1" 43 | ) 44 | , ( "it encoders" 45 | , { path = [], query = Dict.singleton "k" "a/b" } 46 | , "?k=a%2Fb" 47 | ) 48 | ] 49 | 50 | run ( testCase, address, expected ) = 51 | test testCase 52 | <| \() -> 53 | let 54 | actual = 55 | Address.getQuery address 56 | in 57 | Expect.equal expected actual 58 | in 59 | describe "getQuery" (List.map run inputs) 60 | 61 | 62 | parseTest : Test 63 | parseTest = 64 | let 65 | inputs = 66 | [ ( "it parses" 67 | , "/users/1?a=1" 68 | , { path = [ "users", "1" ], query = Dict.singleton "a" "1" } 69 | ) 70 | , ( "it decodes" 71 | , "/a%2Fb/1?k=x%2Fy" 72 | , { path = [ "a/b", "1" ], query = Dict.singleton "k" "x/y" } 73 | ) 74 | ] 75 | 76 | run ( testCase, location, expected ) = 77 | test testCase 78 | <| \() -> 79 | let 80 | actual = 81 | Address.parse location 82 | in 83 | Expect.equal expected actual 84 | in 85 | describe "parse" (List.map run inputs) 86 | 87 | 88 | all : Test 89 | all = 90 | describe "Location" 91 | [ getPathTest 92 | , getQuery 93 | , parseTest 94 | ] 95 | -------------------------------------------------------------------------------- /src/Hop/In.elm: -------------------------------------------------------------------------------- 1 | module Hop.In exposing (..) 2 | 3 | import Regex 4 | import String 5 | import Hop.Types exposing (Address, Config) 6 | import Hop.Address exposing (parse) 7 | 8 | 9 | {-| @priv 10 | -} 11 | ingest : Config -> String -> Address 12 | ingest config href = 13 | href 14 | |> removeProtocol 15 | |> removeDomain 16 | |> getRelevantPathWithQuery config 17 | |> parse 18 | 19 | 20 | {-| @priv 21 | -} 22 | removeProtocol : String -> String 23 | removeProtocol href = 24 | href 25 | |> String.split "//" 26 | |> List.reverse 27 | |> List.head 28 | |> Maybe.withDefault "" 29 | 30 | 31 | {-| @priv 32 | -} 33 | removeDomain : String -> String 34 | removeDomain href = 35 | href 36 | |> String.split "/" 37 | |> List.tail 38 | |> Maybe.withDefault [] 39 | |> String.join "/" 40 | |> String.append "/" 41 | 42 | 43 | {-| @priv 44 | -} 45 | getRelevantPathWithQuery : Config -> String -> String 46 | getRelevantPathWithQuery config href = 47 | if config.hash then 48 | href 49 | |> String.split "#" 50 | |> List.drop 1 51 | |> List.head 52 | |> Maybe.withDefault "" 53 | else 54 | href 55 | |> String.split "#" 56 | |> List.head 57 | |> Maybe.withDefault "" 58 | |> removeBase config 59 | 60 | 61 | {-| @priv 62 | Remove the basePath from a path 63 | 64 | "/basepath/a/b?k=1" -> "/a/b?k=1" 65 | -} 66 | removeBase : Config -> String -> String 67 | removeBase config pathWithQuery = 68 | let 69 | regex = 70 | Regex.regex config.basePath 71 | in 72 | Regex.replace (Regex.AtMost 1) regex (always "") pathWithQuery 73 | -------------------------------------------------------------------------------- /src/Hop/InTest.elm: -------------------------------------------------------------------------------- 1 | module Hop.InTest exposing (..) 2 | 3 | import Dict 4 | import Expect 5 | import Hop.In exposing (ingest) 6 | import Test exposing (..) 7 | 8 | type Route 9 | = NotFound 10 | 11 | 12 | config = 13 | { hash = True 14 | , basePath = "" 15 | } 16 | 17 | 18 | configWithPath = 19 | { config | hash = False } 20 | 21 | 22 | configWithPathAndBase = 23 | { configWithPath | basePath = "/app/v1" } 24 | 25 | 26 | inputTest : Test 27 | inputTest = 28 | let 29 | inputs = 30 | [ ( "it parses an empty hash" 31 | , config 32 | , "http://localhost:3000/basepath" 33 | , { path = [], query = Dict.empty } 34 | ) 35 | , ( "it parses a hash" 36 | , config 37 | , "http://localhost:3000/basepath#/users/1" 38 | , { path = [ "users", "1" ], query = Dict.empty } 39 | ) 40 | , ( "it parses a path" 41 | , configWithPath 42 | , "http://localhost:3000/users/1" 43 | , { path = [ "users", "1" ], query = Dict.empty } 44 | ) 45 | , ( "it parses a path with basepath" 46 | , configWithPathAndBase 47 | , "http://localhost:3000/app/v1/users/1" 48 | , { path = [ "users", "1" ], query = Dict.empty } 49 | ) 50 | , ( "it parses a hash with query" 51 | , config 52 | , "http://localhost:3000/basepath#/users/1?a=1" 53 | , { path = [ "users", "1" ], query = Dict.singleton "a" "1" } 54 | ) 55 | , ( "it parses a path with query" 56 | , configWithPath 57 | , "http://localhost:3000/users/1?a=1" 58 | , { path = [ "users", "1" ], query = Dict.singleton "a" "1" } 59 | ) 60 | , ( "it parses a path with basepath and query" 61 | , configWithPathAndBase 62 | , "http://localhost:3000/app/v1/users/1?a=1" 63 | , { path = [ "users", "1" ], query = Dict.singleton "a" "1" } 64 | ) 65 | , ( "it decodes the query" 66 | , config 67 | , "http://localhost:3000/basepath#/?a%20b%26c%3Fd=1%202%263%3F4" 68 | , { path = [], query = Dict.singleton "a b&c?d" "1 2&3?4" } 69 | ) 70 | ] 71 | 72 | run ( testCase, config, href, expected ) = 73 | test testCase 74 | <| \() -> 75 | let 76 | actual = 77 | ingest config href 78 | in 79 | Expect.equal expected actual 80 | in 81 | describe "ingest" (List.map run inputs) 82 | 83 | 84 | all : Test 85 | all = 86 | describe "In" 87 | [ inputTest 88 | ] 89 | -------------------------------------------------------------------------------- /src/Hop/Out.elm: -------------------------------------------------------------------------------- 1 | module Hop.Out exposing (..) 2 | 3 | import String 4 | import Hop.Types exposing (Address, Config) 5 | import Hop.Utils exposing (dedupSlash) 6 | import Hop.Address exposing (parse) 7 | 8 | 9 | {-| 10 | Make a real path from an address record. 11 | This will add the hash and the basePath as necessary. 12 | 13 | fromAddress config { path = ["users", "1"], query = Dict.empty } 14 | 15 | == 16 | 17 | "#/users/1" 18 | 19 | -} 20 | output : Config -> Address -> String 21 | output config address = 22 | let 23 | -- path -> "/a/1" 24 | path = 25 | Hop.Address.getPath address 26 | 27 | -- query -> "?a=1" 28 | query = 29 | Hop.Address.getQuery address 30 | 31 | url = 32 | if config.hash then 33 | "#" ++ path ++ query 34 | else if String.isEmpty config.basePath then 35 | path ++ query 36 | else if path == "/" then 37 | "/" ++ config.basePath ++ query 38 | else 39 | "/" ++ config.basePath ++ path ++ query 40 | 41 | realPath = 42 | dedupSlash url 43 | in 44 | if realPath == "" then 45 | "/" 46 | else 47 | realPath 48 | 49 | 50 | {-| 51 | Make a real path from a simulated path. 52 | This will add the hash and the basePath as necessary. 53 | 54 | toRealPath config "/users" 55 | 56 | == 57 | 58 | "#/users" 59 | -} 60 | outputFromPath : Config -> String -> String 61 | outputFromPath config path = 62 | path 63 | |> Hop.Address.parse 64 | |> output config 65 | -------------------------------------------------------------------------------- /src/Hop/OutTest.elm: -------------------------------------------------------------------------------- 1 | module Hop.OutTest exposing (..) 2 | 3 | import Dict 4 | import Expect 5 | import Hop.Out as Out 6 | import Hop.Types exposing (newAddress) 7 | import Hop.TestHelper exposing (configWithHash, configWithPath, configPathAndBasePath) 8 | import Test exposing (..) 9 | 10 | 11 | outputTest : Test 12 | outputTest = 13 | let 14 | empty = 15 | newAddress 16 | 17 | inputs = 18 | [ ( "hash: it is empty when empty" 19 | , configWithHash 20 | , empty 21 | , "" 22 | , "#/" 23 | ) 24 | , ( "path: it is empty when empty" 25 | , configWithPath 26 | , empty 27 | , "" 28 | , "/" 29 | ) 30 | , ( "basepath: it has the basepath" 31 | , configPathAndBasePath 32 | , empty 33 | , "" 34 | , "/app/v1" 35 | ) 36 | , ( "basepath: adds slash when missing" 37 | , { configPathAndBasePath | basePath = "app/v1" } 38 | , empty 39 | , "" 40 | , "/app/v1" 41 | ) 42 | -- path 43 | , ( "hash: it adds the path" 44 | , configWithHash 45 | , { empty | path = [ "a", "b" ] } 46 | , "/a/b" 47 | , "#/a/b" 48 | ) 49 | , ( "path: it adds the path" 50 | , configWithPath 51 | , { empty | path = [ "a", "b" ] } 52 | , "/a/b" 53 | , "/a/b" 54 | ) 55 | , ( "path: it adds the basepath and path" 56 | , configPathAndBasePath 57 | , { empty | path = [ "a", "b" ] } 58 | , "/a/b" 59 | , "/app/v1/a/b" 60 | ) 61 | -- query 62 | , ( "hash: it adds the query as pseudo query" 63 | , configWithHash 64 | , { empty | query = Dict.singleton "k" "1" } 65 | , "?k=1" 66 | , "#/?k=1" 67 | ) 68 | , ( "path: it adds the query" 69 | , configWithPath 70 | , { empty | query = Dict.singleton "k" "1" } 71 | , "?k=1" 72 | , "/?k=1" 73 | ) 74 | , ( "path: it adds the basepath query" 75 | , configPathAndBasePath 76 | , { empty | query = Dict.singleton "k" "1" } 77 | , "?k=1" 78 | , "/app/v1?k=1" 79 | ) 80 | -- path and query 81 | , ( "hash: it adds the path and query" 82 | , configWithHash 83 | , { empty | query = Dict.singleton "k" "1", path = [ "a", "b" ] } 84 | , "/a/b?k=1" 85 | , "#/a/b?k=1" 86 | ) 87 | , ( "path: it adds the path and query" 88 | , configWithPath 89 | , { empty | query = Dict.singleton "k" "1", path = [ "a", "b" ] } 90 | , "/a/b?k=1" 91 | , "/a/b?k=1" 92 | ) 93 | , ( "path: it adds the basepath, path and query" 94 | , configPathAndBasePath 95 | , { empty | query = Dict.singleton "k" "1", path = [ "a", "b" ] } 96 | , "/a/b?k=1" 97 | , "/app/v1/a/b?k=1" 98 | ) 99 | , ( "hash: it encodes" 100 | , configWithHash 101 | , { empty | query = Dict.singleton "a/d" "1/4", path = [ "a/b", "1" ] } 102 | , "/a%2Fb/1?a%2Fd=1%2F4" 103 | , "#/a%2Fb/1?a%2Fd=1%2F4" 104 | ) 105 | ] 106 | 107 | run ( testCase, config, address, path, expected ) = 108 | [ test testCase 109 | <| \() -> 110 | let 111 | actual = 112 | Out.output config address 113 | in 114 | Expect.equal expected actual 115 | , test testCase 116 | <| \() -> 117 | let 118 | actual = 119 | Out.outputFromPath config path 120 | in 121 | Expect.equal expected actual 122 | ] 123 | 124 | tests = 125 | List.concatMap run inputs 126 | in 127 | describe "output and outputFromPath" tests 128 | 129 | 130 | all : Test 131 | all = 132 | describe "In" 133 | [ outputTest 134 | ] 135 | -------------------------------------------------------------------------------- /src/Hop/TestHelper.elm: -------------------------------------------------------------------------------- 1 | module Hop.TestHelper exposing (..) 2 | 3 | import Hop.Types exposing (Config) 4 | 5 | 6 | configWithHash : Config 7 | configWithHash = 8 | { basePath = "" 9 | , hash = True 10 | } 11 | 12 | 13 | configWithPath : Config 14 | configWithPath = 15 | { basePath = "" 16 | , hash = False 17 | } 18 | 19 | 20 | configPathAndBasePath : Config 21 | configPathAndBasePath = 22 | { basePath = "/app/v1" 23 | , hash = False 24 | } 25 | -------------------------------------------------------------------------------- /src/Hop/Types.elm: -------------------------------------------------------------------------------- 1 | module Hop.Types exposing (Config, Query, Address, newQuery, newAddress) 2 | 3 | {-| Types used in Hop 4 | 5 | #Types 6 | @docs Config, Address, Query 7 | 8 | #Factories 9 | @docs newQuery, newAddress 10 | -} 11 | 12 | import Dict 13 | 14 | {-| A Dict that holds query parameters 15 | 16 | Dict.Dict String String 17 | -} 18 | type alias Query = 19 | Dict.Dict String String 20 | 21 | {-| A Record that represents the current location 22 | Includes a `path` and a `query` 23 | 24 | { 25 | path: List String, 26 | query: Query 27 | } 28 | -} 29 | type alias Address = 30 | { path : List String 31 | , query : Query 32 | } 33 | 34 | {-| Hop Configuration 35 | 36 | - basePath: Only for pushState routing (not hash). e.g. "/app". 37 | - hash: True for hash routing, False for pushState routing. 38 | 39 | -} 40 | type alias Config = 41 | { basePath : String 42 | , hash : Bool 43 | } 44 | 45 | {-| 46 | Create an empty Query record 47 | -} 48 | newQuery : Query 49 | newQuery = 50 | Dict.empty 51 | 52 | 53 | {-| 54 | Create an empty Address record 55 | -} 56 | newAddress : Address 57 | newAddress = 58 | { query = newQuery 59 | , path = [] 60 | } 61 | -------------------------------------------------------------------------------- /src/Hop/Utils.elm: -------------------------------------------------------------------------------- 1 | module Hop.Utils exposing (..) 2 | 3 | import Regex 4 | 5 | dedupSlash : String -> String 6 | dedupSlash = 7 | Regex.replace Regex.All (Regex.regex "/+") (\_ -> "/") 8 | -------------------------------------------------------------------------------- /src/HopTest.elm: -------------------------------------------------------------------------------- 1 | module HopTest exposing (..) 2 | 3 | import Expect 4 | import Test exposing (..) 5 | import Hop.TestHelper exposing (configWithHash, configWithPath, configPathAndBasePath) 6 | import Hop 7 | 8 | 9 | makeResolverTest : Test 10 | makeResolverTest = 11 | let 12 | inputs = 13 | [ ( "path" 14 | , configWithPath 15 | , "http://example.com/users/1" 16 | , "users/1/" 17 | ) 18 | , ( "path with base" 19 | , configPathAndBasePath 20 | , "http://example.com/app/v1/users/1" 21 | , "users/1/" 22 | ) 23 | , ( "path" 24 | , configWithHash 25 | , "http://example.com/app#/users/1" 26 | , "users/1/" 27 | ) 28 | ] 29 | 30 | run ( testCase, config, href, expected ) = 31 | test testCase 32 | <| \() -> 33 | let 34 | resolver = 35 | Hop.makeResolver config identity 36 | 37 | ( actual, _ ) = 38 | resolver href 39 | in 40 | Expect.equal expected actual 41 | in 42 | describe "makeResolver" (List.map run inputs) 43 | 44 | 45 | all : Test 46 | all = 47 | describe "Hop" 48 | [ makeResolverTest 49 | ] 50 | -------------------------------------------------------------------------------- /tests/IntegrationTest.elm: -------------------------------------------------------------------------------- 1 | module IntegrationTest exposing (..) 2 | 3 | import UrlParser exposing ((), oneOf, int, s) 4 | import Navigation exposing (Location) 5 | import Expect 6 | import String 7 | import Test exposing (..) 8 | import Hop.TestHelper exposing (configWithHash, configWithPath, configPathAndBasePath) 9 | import Hop 10 | import Hop.Types exposing (Address, Config) 11 | 12 | 13 | type alias UserId = 14 | Int 15 | 16 | 17 | type UserRoute 18 | = UsersRoute 19 | | UserRoute UserId 20 | | UserEditRoute UserId 21 | 22 | 23 | type MainRoute 24 | = HomeRoute 25 | | AboutRoute 26 | | UsersRoutes UserRoute 27 | | NotFoundRoute 28 | 29 | 30 | usersMatchers = 31 | [ UrlParser.format UserEditRoute (int s "edit") 32 | , UrlParser.format UserRoute (int) 33 | , UrlParser.format UsersRoute (s "") 34 | ] 35 | 36 | 37 | mainMatchers = 38 | [ UrlParser.format HomeRoute (s "") 39 | , UrlParser.format AboutRoute (s "about") 40 | , UrlParser.format UsersRoutes (s "users" (oneOf usersMatchers)) 41 | ] 42 | 43 | 44 | routes = 45 | oneOf mainMatchers 46 | 47 | 48 | newLocation : Location 49 | newLocation = 50 | { hash = "" 51 | , host = "example.com" 52 | , hostname = "example.com" 53 | , href = "" 54 | , origin = "" 55 | , password = "" 56 | , pathname = "" 57 | , port_ = "" 58 | , protocol = "http" 59 | , search = "" 60 | , username = "" 61 | } 62 | 63 | 64 | parseWithUrlParser : Config -> Location -> ( MainRoute, Address ) 65 | parseWithUrlParser currentConfig = 66 | let 67 | parse path = 68 | path 69 | |> UrlParser.parse identity routes 70 | |> Result.withDefault NotFoundRoute 71 | in 72 | .href >> Hop.makeResolver currentConfig parse 73 | 74 | 75 | 76 | ------------------------------ 77 | -- Example urlParsers 78 | ------------------------------ 79 | 80 | 81 | urlParserRouteAddress : Navigation.Parser ( MainRoute, Address ) 82 | urlParserRouteAddress = 83 | let 84 | parse path = 85 | path 86 | |> UrlParser.parse identity routes 87 | |> Result.withDefault NotFoundRoute 88 | 89 | solver = 90 | Hop.makeResolver configWithHash parse 91 | in 92 | Navigation.makeParser (.href >> solver) 93 | 94 | 95 | urlParserOnlyRoute : Navigation.Parser MainRoute 96 | urlParserOnlyRoute = 97 | let 98 | parse path = 99 | path 100 | |> UrlParser.parse identity routes 101 | |> Result.withDefault NotFoundRoute 102 | 103 | solver = 104 | Hop.makeResolver configWithHash parse 105 | in 106 | Navigation.makeParser (.href >> solver >> fst) 107 | 108 | urlParserResultAddress : Navigation.Parser (Result String MainRoute, Address) 109 | urlParserResultAddress = 110 | let 111 | parse path = 112 | path 113 | |> UrlParser.parse identity routes 114 | 115 | solver = 116 | Hop.makeResolver configWithHash parse 117 | in 118 | Navigation.makeParser (.href >> solver) 119 | 120 | 121 | urlParserIntegrationTest : Test 122 | urlParserIntegrationTest = 123 | let 124 | inputs = 125 | [ ( "Home page" 126 | , configWithPath 127 | , "http://example.com" 128 | , HomeRoute 129 | , "/" 130 | ) 131 | , ( "Base: Home page" 132 | , configPathAndBasePath 133 | , "http://example.com/app/v1" 134 | , HomeRoute 135 | , "/app/v1" 136 | ) 137 | , ( "Hash: Home page with /#" 138 | , configWithHash 139 | , "http://example.com/#" 140 | , HomeRoute 141 | , "#/" 142 | ) 143 | , ( "Hash: Home page with /#/" 144 | , configWithHash 145 | , "http://example.com/#/" 146 | , HomeRoute 147 | , "#/" 148 | ) 149 | , ( "Hash: Home page without hash" 150 | , configWithHash 151 | , "http://example.com" 152 | , HomeRoute 153 | , "#/" 154 | ) 155 | , ( "Hash: Home page" 156 | , configWithHash 157 | , "http://example.com/index.html" 158 | , HomeRoute 159 | , "#/" 160 | ) 161 | -- about 162 | , ( "AboutRoute" 163 | , configWithPath 164 | , "http://example.com/about" 165 | , AboutRoute 166 | , "/about" 167 | ) 168 | , ( "Base: AboutRoute" 169 | , configPathAndBasePath 170 | , "http://example.com/app/v1/about" 171 | , AboutRoute 172 | , "/app/v1/about" 173 | ) 174 | , ( "Hash: AboutRoute" 175 | , configWithHash 176 | , "http://example.com/#/about" 177 | , AboutRoute 178 | , "#/about" 179 | ) 180 | , ( "Hash: AboutRoute with slash" 181 | , configWithHash 182 | , "http://example.com/app#/about" 183 | , AboutRoute 184 | , "#/about" 185 | ) 186 | -- users 187 | , ( "UsersRoute" 188 | , configWithPath 189 | , "http://example.com/users" 190 | , UsersRoutes UsersRoute 191 | , "/users" 192 | ) 193 | , ( "Base: UsersRoute" 194 | , configPathAndBasePath 195 | , "http://example.com/app/v1/users" 196 | , UsersRoutes UsersRoute 197 | , "/app/v1/users" 198 | ) 199 | , ( "Hash: UsersRoute" 200 | , configWithHash 201 | , "http://example.com/#/users" 202 | , UsersRoutes UsersRoute 203 | , "#/users" 204 | ) 205 | -- users with query 206 | , ( "UsersRoute" 207 | , configWithPath 208 | , "http://example.com/users?k=1" 209 | , UsersRoutes UsersRoute 210 | , "/users?k=1" 211 | ) 212 | , ( "Base: UsersRoute" 213 | , configPathAndBasePath 214 | , "http://example.com/app/v1/users?k=1" 215 | , UsersRoutes UsersRoute 216 | , "/app/v1/users?k=1" 217 | ) 218 | , ( "Hash: UsersRoute" 219 | , configWithHash 220 | , "http://example.com/#/users?k=1" 221 | , UsersRoutes UsersRoute 222 | , "#/users?k=1" 223 | ) 224 | -- user 225 | , ( "UserRoute" 226 | , configWithPath 227 | , "http://example.com/users/2" 228 | , UsersRoutes (UserRoute 2) 229 | , "/users/2" 230 | ) 231 | , ( "Base: UserRoute" 232 | , configPathAndBasePath 233 | , "http://example.com/app/v1/users/2" 234 | , UsersRoutes (UserRoute 2) 235 | , "/app/v1/users/2" 236 | ) 237 | , ( "Hash: UserRoute" 238 | , configWithHash 239 | , "http://example.com/#/users/2" 240 | , UsersRoutes (UserRoute 2) 241 | , "#/users/2" 242 | ) 243 | -- user edit 244 | , ( "UserRoute" 245 | , configWithPath 246 | , "http://example.com/users/2/edit" 247 | , UsersRoutes (UserEditRoute 2) 248 | , "/users/2/edit" 249 | ) 250 | , ( "Base: UserRoute" 251 | , configPathAndBasePath 252 | , "http://example.com/app/v1/users/2/edit" 253 | , UsersRoutes (UserEditRoute 2) 254 | , "/app/v1/users/2/edit" 255 | ) 256 | , ( "Hash: UserRoute" 257 | , configWithHash 258 | , "http://example.com/#/users/2/edit" 259 | , UsersRoutes (UserEditRoute 2) 260 | , "#/users/2/edit" 261 | ) 262 | ] 263 | 264 | run ( testCase, currentConfig, href, expected, expectedRoundTrip ) = 265 | [ test testCase 266 | <| \() -> 267 | let 268 | location = 269 | { newLocation | href = href } 270 | 271 | ( actual, _ ) = 272 | parseWithUrlParser currentConfig location 273 | in 274 | Expect.equal expected actual 275 | , test (testCase ++ " - output") 276 | <| \() -> 277 | let 278 | location = 279 | { newLocation | href = href } 280 | 281 | ( _, address ) = 282 | parseWithUrlParser currentConfig location 283 | 284 | actual = 285 | Hop.output currentConfig address 286 | in 287 | Expect.equal expectedRoundTrip actual 288 | ] 289 | in 290 | describe "UrlParser integration" (List.concatMap run inputs) 291 | 292 | 293 | all : Test 294 | all = 295 | describe "Integration" 296 | [ urlParserIntegrationTest 297 | ] 298 | -------------------------------------------------------------------------------- /tests/Main.elm: -------------------------------------------------------------------------------- 1 | port module Main exposing (..) 2 | 3 | import Tests 4 | import Test.Runner.Node exposing (run) 5 | import Json.Encode exposing (Value) 6 | 7 | 8 | main : Program Value 9 | main = 10 | run emit Tests.all 11 | 12 | 13 | port emit : ( String, Value ) -> Cmd msg 14 | -------------------------------------------------------------------------------- /tests/Tests.elm: -------------------------------------------------------------------------------- 1 | module Tests exposing (..) 2 | 3 | import HopTest 4 | import Hop.AddressTest 5 | import Hop.InTest 6 | import Hop.OutTest 7 | import IntegrationTest 8 | import Test exposing (..) 9 | 10 | 11 | all : Test 12 | all = 13 | describe "Hop" 14 | [ HopTest.all 15 | , Hop.AddressTest.all 16 | , Hop.InTest.all 17 | , Hop.OutTest.all 18 | , IntegrationTest.all 19 | ] 20 | -------------------------------------------------------------------------------- /tests/elm-package.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.0.1", 3 | "summary": "A router for SPAs in Elm", 4 | "repository": "https://github.com/sporto/hop.git", 5 | "license": "MIT", 6 | "source-directories": [ 7 | ".", 8 | "../src" 9 | ], 10 | "exposed-modules": [], 11 | "dependencies": { 12 | "elm-community/elm-test": "2.1.0 <= v < 3.0.0", 13 | "elm-lang/core": "4.0.0 <= v < 5.0.0", 14 | "elm-lang/navigation": "1.0.0 <= v < 2.0.0", 15 | "evancz/elm-http": "3.0.1 <= v < 4.0.0", 16 | "evancz/url-parser": "1.0.0 <= v < 2.0.0", 17 | "rtfeldman/node-test-runner": "2.0.0 <= v < 3.0.0" 18 | }, 19 | "elm-version": "0.16.0 <= v < 0.18.0" 20 | } -------------------------------------------------------------------------------- /tests/install-packages.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | n=0 4 | until [ $n -ge 5 ] 5 | do 6 | elm package install -y && break 7 | n=$[$n+1] 8 | sleep 15 9 | done 10 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "elm.js", 6 | "scripts": { 7 | "test": "elm-test" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "elm-test": "^0.17.3" 13 | } 14 | } 15 | --------------------------------------------------------------------------------