Repository: sporto/hop
Branch: master
Commit: 3113fe73d78e
Files: 66
Total size: 77.8 KB
Directory structure:
gitextract_i1usn_9t/
├── .editorconfig
├── .gitignore
├── .vscode/
│ └── settings.json
├── Makefile
├── assets/
│ └── logo.idraw
├── 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
│ ├── 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/
│ │ ├── Address.elm
│ │ ├── AddressTest.elm
│ │ ├── In.elm
│ │ ├── InTest.elm
│ │ ├── Out.elm
│ │ ├── OutTest.elm
│ │ ├── TestHelper.elm
│ │ ├── Types.elm
│ │ └── Utils.elm
│ ├── Hop.elm
│ └── HopTest.elm
└── tests/
├── IntegrationTest.elm
├── Main.elm
├── Tests.elm
├── elm-package.json
├── install-packages.sh
└── package.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
[*]
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 2
[*.js]
indent_style = tab
indent_size = 2
[*.elm]
indent_style = space
indent_size = 4
[Makefile]
indent_style = tab
indent_size = 2
================================================
FILE: .gitignore
================================================
.DS_Store
elm-stuff
node_modules
elm.js
documentation.json
.vscode/tasks.json
npm-debug.log
.envrc
tests.js
================================================
FILE: .vscode/settings.json
================================================
// Place your settings in this file to overwrite default and user settings.
{
"search.exclude": {
"**/node_modules": true,
"**/bower_components": true,
"**/*.elmo": true,
"documentation.json": true
}
}
================================================
FILE: Makefile
================================================
# Start basic application example locally
basic-up:
cd ./examples/basic && elm reactor
full-up:
cd ./examples/full && npm run dev
# Generate documentation for preview
docs:
elm make --docs=documentation.json
# Run unit tests locally
test-unit:
npm test
test-unit-ci:
npm install -g elm
npm install -g elm-test
elm-package install -y
cd tests && elm-package install -y && cd ..
elm-test
build-basic:
cd examples/basic/ && elm make --yes Main.elm
build-full:
cd examples/full/ && elm make --yes src/Main.elm
test-ci:
make test-unit-ci
make build-basic
make build-full
.PHONY: docs test
================================================
FILE: docs/building-routes.md
================================================
# Building routes
As of version 6 Hop doesn't provide matchers anymore, instead you can use [__UrlParser__](http://package.elm-lang.org/packages/evancz/url-parser).
You build your routes by using union types:
```elm
type Route
= HomeRoute
| UserRoute Int
| UserStatusRoute Int
| NotFoundRoute
```
Then you need to create matchers for these routes:
```elm
import UrlParser exposing ((</>), format, oneOf, int, s)
matchers =
oneOf [
UrlParser.format HomeRoute (s "")
, UrlParser.format UserRoute (s "users" </> int)
, UrlParser.format UserStatusRoute (s "users" </> int </> s "status")
]
```
These matchers will match:
- "/"
- "users/1"
- "users/1/status"
## Order matters
The order of the matchers makes a big difference. See these examples.
Given you have some routes and matchers:
```elm
import UrlParser exposing (format, s, parse, int, oneOf, (</>))
type Route = UserRoute Int | UserEditRoute Int
-- match 'users/1'
userMatcher = format UserRoute (s "users" </> int)
-- match '/uses/1/edit'
userEditMatcher = format UserEditRoute (s "users" </> int </> s "edit")
```
### Incorrect order
```elm
matchers =
oneOf [userMatcher, userEditMatcher]
parse identity matchers "users/1"
== Ok (UserRoute 1) : Result.Result String Repl.Route
parse identity matchers "users/1/edit"
== Err "The parser worked, but /edit was left over."
```
The `userEditMatcher` doesn't even run in this case. The `userMatcher` fails and stops the flow.
## Correct order
```elm
matchers =
oneOf [userEditMatcher, userMatcher]
parse identity matchers "users/1"
== Ok (UserRoute 1) : Result.Result String Repl.Route
parse identity matchers "users/1/edit"
== Ok (UserEditRoute 1) : Result.Result String Repl.Route
```
This works as expected, so is important to put the more specific matchers first.
================================================
FILE: docs/changelog.md
================================================
# Changelog
### 6.0.0
- Remove matchers (Use UrlParser instead)
- Rename input and output functions
- Encode and decode path segments
### 5.0.1
- Encode query string when converting location to URL.
## 5.0.0
- Update for Elm 0.17
### 4.0.3
- Fix issue where root path wouldn't match when using hash routing.
### 4.0.2
- Futher fix for navigating to root path, use / instead of .
### 4.0.1
- Fix https://github.com/sporto/hop/issues/20
## 4.0.0
- Support setState (No hashes)
- Most functions take a config record
## 3.0.0
- Typed values in routes
- Nested routes
- Reverse routing
### 2.1.1
- Remove unnecessary dependency to `elm-test`
### 2.1.0
- Expose `Query` and `Url` types
## 2.0.0
- Remove dependency on `Erl`.
- Change order of arguments on `addQuery`, `clearQuery`, `removeQuery` and `setQuery`
### 1.2.1
- Url is normalized before navigation i.e append `#/` if necessary
### 1.2.0
- Added `addQuery`, changed behaviour of `setQuery`.
### 1.1.1
- Fixed issue where query string won't be set when no hash wash present
================================================
FILE: docs/matching-routes.md
================================================
# Matching routes
Create a parser using `Navigation.makeParser` combined with `Hop.makeResolver`.
There are serveral strategies you can use.
## Given you have some configuration
```
routes =
oneOf [...]
hopConfig =
{ ... }
```
## A parser that returns `(Route, Address)`
```
urlParserRouteAddress : Navigation.Parser ( MainRoute, Address )
urlParserRouteAddress =
let
parse path =
path
|> UrlParser.parse identity routes
|> Result.withDefault NotFoundRoute
solver =
Hop.makeResolver configWithHash parse
in
Navigation.makeParser (.href >> solver)
```
This parser:
- Takes the `.href` from the `Location` record given by `Navigation`.
- Converts that to a normalised path (done inside `makeResolver`).
- Passes the normalised path to your `parse` function, which returns a matched route or `NotFoundRoute`.
- When run returns a tuple `(Route, Address)`.
## A parser that returns only the matched route
```
urlParserOnlyRoute : Navigation.Parser MainRoute
urlParserOnlyRoute =
let
parse path =
path
|> UrlParser.parse identity routes
|> Result.withDefault NotFoundRoute
solver =
Hop.makeResolver configWithHash parse
in
Navigation.makeParser (.href >> solver >> fst)
```
This parser only returns the matched route. The `address` record is discarded.
However you probably need the address record for doing things with the query later.
## A parser that returns the parser result + Address
```
urlParserResultAddress : Navigation.Parser (Result String MainRoute, Address)
urlParserResultAddress =
let
parse path =
path
|> UrlParser.parse identity routes
solver =
Hop.makeResolver configWithHash parse
in
Navigation.makeParser (.href >> solver)
```
This parser returns the result from `parse` e.g. `Result String MainRoute` and the address record.
================================================
FILE: docs/navigating.md
================================================
# Navigating
## Changing the location
Use `Hop.outputFromPath` for changing the browser location.
Add a message:
```elm
type Msg
...
| NavigateTo String
```
Trigger this message from you view:
```elm
button [ onClick (NavigateTo "/users") ] [ text "Users" ]
```
React to this message in update:
```elm
NavigateTo path ->
let
command =
Hop.outputFromPath routerConfig path
|> Navigation.newUrl
in
( model, command )
```
## Changing the query string
Add actions for changing the query string:
```elm
type Msg
= ...
| AddQuery (Dict.Dict String String)
| SetQuery (Dict.Dict String String)
| ClearQuery
```
Change update to respond to these actions:
```elm
import Hop exposing(addQuery, setQuery, clearQuery)
update msg model =
case msg of
...
AddQuery query ->
let
command =
model.address
|> Hop.addQuery query
|> Hop.output routerConfig
|> Navigation.newUrl
in
(model, command)
```
You need to pass the current `address` record to these functions.
Then you use that `address` record to generate a url using `output`.
Trigger these messages from your views:
```elm
button [ onClick (AddQuery (Dict.singleton "color" "red")) ] [ text "Set query" ]
```
See details of available functions at <http://package.elm-lang.org/packages/sporto/hop/latest/Hop>
================================================
FILE: docs/nesting-routes.md
================================================
# Nesting routes
UrlParser supports nested routes:
```elm
type UserRoute
= UsersRoute
| UserRoute UserId
type MainRoute
= HomeRoute
| AboutRoute
| UsersRoutes UserRoute
| NotFoundRoute
usersMatchers =
[ UrlParser.format UserRoute (int)
, UrlParser.format UsersRoute (s "")
]
mainMatchers =
[ UrlParser.format HomeRoute (s "")
, UrlParser.format AboutRoute (s "about")
, UrlParser.format UsersRoutes (s "users" </> (oneOf usersMatchers))
]
matchers =
oneOf mainMatchers
```
With a setup like this UrlParser will be able to match routes like:
- "" -> HomeRoute
- "/about" -> AboutRoute
- "/users" -> UsersRoutes UsersRoute
- "/users/2" -> UsersRoutes (UserRoute 2)
================================================
FILE: docs/reverse-routing.md
================================================
# Reverse routing
Reverse routing means converting a route tag back to an url e.g.
```
UserRoute 1 --> "/users/1"
```
In the current version Hop doesn't have any helpers for reverse routing. You can do this manually:
```elm
reverse : Route -> String
reverse route =
case route of
HomeRoute ->
""
AboutRoute ->
"about"
UserRoute id ->
"users/" ++ id
NotFoundRoute ->
""
```
================================================
FILE: docs/testing.md
================================================
# Testing Hop
## Unit tests
```bash
cd tests
elm package install -y
cd ..
npm i
npm test
```
================================================
FILE: docs/upgrade-2-to-3.md
================================================
# Upgrading from 2 to 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.
## Routes
Now routes can take values, instead of:
```elm
type Route
= Home
| Users
| User
| Token
```
You can have values attached:
```elm
type Route
= Home
| Users
| User Int
| Token String
```
Routes are now defined using matchers. So instead of
```elm
routes =
[ ("/users/:int", User) ]
```
You do:
```elm
import Hop.Matchers exposing(match2)
userMatcher =
match2 User "/users/" int
matchers =
[userMatcher]
```
This is so we can have stronger types e.g. `User Int`.
## Actions
Hop.signal now returns a tuple with `(Route, Location)`. Your application needs to map this to an action. e.g.
```elm
type Action
= HopAction ()
| ApplyRoute ( Route, Location )
taggedRouterSignal : Signal Action
taggedRouterSignal =
Signal.map ApplyRoute router.signal
```
This is so routes (`Route`) are different type than the application actions.
## Payload
Before Hop returned a `payload` with a dictionary with matched parameters.
Now it returns the matched route with the values e.g. `User 1` and a `Location` record in the form of a tuple:
```elm
(User 1, location)
```
`location` has the information about the current path and the query:
```elm
{
path = ["users", "1"],
query = <Dict String String>
}
```
## Views
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`.
So instead of doing:
```elm
userId =
model.routerPayload.params
|> Dict.get "userId"
|> Maybe.withDefault ""
```
You simply get the id from the route:
```elm
case User userId ->
...
```
The query is still a dictionary. The query is now in the `location` record as shown above.
================================================
FILE: docs/upgrade-3-to-4.md
================================================
# Upgrading from 3 to 4
Version 4 introduces push state. There are two major changes:
## Config
Config now includes `hash` and `basePath`.
```elm
routerConfig : Config Route
routerConfig =
{ hash = True
, basePath = ""
, matchers = matchers
, notFound = NotFoundRoute
}
```
## Functions require the router config
Most functions in Hop now require your router config. For example instead of:
```elm
navigateTo path
addQuery query location
```
It is now:
```elm
navigateTo config path
addQuery config query location
```
This is because Hop needs to know if you are using hash or path routing.
================================================
FILE: docs/upgrade-4-to-5.md
================================================
# Upgrading from 4 to 5
Version 5 works with Navigation and Elm 0.17
## Matchers
All matchers stay the same as in version 4.
## Navigation
Navigation is now handled by the Navigation module. See example app at `examples/basic/Main.elm`.
Hop doesn't return effects / commands anymore, this should be done by passing a path to `Navigation.modifyUrl`.
================================================
FILE: docs/upgrade-5-to-6.md
================================================
# Upgrading from 5 to 6
Version 6 removes Hop matchers. Use UrlParser instead. See Building Routes document for examples.
================================================
FILE: elm-package.json
================================================
{
"version": "6.0.1",
"summary": "Routing and Navigation helpers for SPAs in Elm",
"repository": "https://github.com/sporto/hop.git",
"license": "MIT",
"source-directories": [
"examples",
"src"
],
"exposed-modules": [
"Hop",
"Hop.Types"
],
"dependencies": {
"elm-lang/core": "4.0.0 <= v < 5.0.0",
"evancz/elm-http": "3.0.1 <= v < 4.0.0"
},
"elm-version": "0.16.0 <= v < 0.18.0"
}
================================================
FILE: examples/basic/.dockerignore
================================================
Dockefile
elm-stuff
================================================
FILE: examples/basic/.gitignore
================================================
index.html
================================================
FILE: examples/basic/Main.elm
================================================
module Main exposing (..)
{-|
You will need Navigation, UrlParser and Hop.
```
elm package install elm-lang/navigation
elm package install evancz/url-parser
elm package install sporto/hop
```
-}
import Html exposing (..)
import Html.Attributes exposing (class)
import Html.Events exposing (onClick)
import Dict
import Navigation
import UrlParser exposing ((</>))
import Hop
import Hop.Types exposing (Config, Address, Query)
-- ROUTES
{-|
Define your routes as union types.
You need to provide a route for when the current URL doesn't match any known route i.e. NotFoundRoute.
-}
type Route
= AboutRoute
| MainRoute
| NotFoundRoute
{-|
Define route matchers.
See `docs/building-routes.md` for more examples.
-}
routes : UrlParser.Parser (Route -> a) a
routes =
UrlParser.oneOf
[ UrlParser.format MainRoute (UrlParser.s "")
, UrlParser.format AboutRoute (UrlParser.s "about")
]
{-|
Define your router configuration.
Use `hash = True` for hash routing e.g. `#/users/1`.
Use `hash = False` for push state e.g. `/users/1`.
The `basePath` is only used for path routing.
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.
-}
hopConfig : Config
hopConfig =
{ hash = True
, basePath = ""
}
-- MESSAGES
{-|
Add messages for navigation and changing the query.
-}
type Msg
= NavigateTo String
| SetQuery Query
-- MODEL
{-|
Add the current route and address to your model.
- `Route` is your Route union type defined above.
- `Hop.Address` is record to aid with changing the query string.
`route` will be used for determining the current route in the views.
`address` is needed because:
- Some navigation functions in Hop need this information to rebuild the current address.
- Your views might need information about the current query string.
-}
type alias Model =
{ address : Address
, route : Route
}
{-|
Respond to navigation messages in update i.e. NavigateTo and SetQuery
-}
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case (Debug.log "msg" msg) of
NavigateTo path ->
let
command =
-- First generate the URL using your config (`outputFromPath`).
-- Then generate a command using Navigation.newUrl.
Hop.outputFromPath hopConfig path
|> Navigation.newUrl
in
( model, command )
SetQuery query ->
let
command =
-- First modify the current stored address record (setting the query)
-- Then generate a URL using Hop.output
-- Finally, create a command using Navigation.newUrl
model.address
|> Hop.setQuery query
|> Hop.output hopConfig
|> Navigation.newUrl
in
( model, command )
{-|
Create a URL Parser for Navigation
-}
urlParser : Navigation.Parser ( Route, Address )
urlParser =
let
-- A parse function takes the normalised path from Hop after taking
-- in consideration the basePath and the hash.
-- This function then returns a result.
parse path =
-- First we parse using UrlParser.parse.
-- Then we return the parsed route or NotFoundRoute if the parsed failed.
-- You can choose to return the parse return directly.
path
|> UrlParser.parse identity routes
|> Result.withDefault NotFoundRoute
resolver =
-- Create a function that parses and formats the URL
-- This function takes 2 arguments: The Hop Config and the parse function.
Hop.makeResolver hopConfig parse
in
-- Create a Navigation URL parser
Navigation.makeParser (.href >> resolver)
{-|
Navigation will call urlUpdate when the address changes.
This function gets the result from `urlParser`, which is a tuple with (Route, Hop.Types.Address)
Address is a record that has:
```elm
{
path: List String,
query: Hop.Types.Query
}
```
- `path` is an array of strings that has the current path e.g. `["users", "1"]` for `"/users/1"`
- `query` Is dictionary of String String. You can access this information in your views to show the relevant content.
We store these two things in our model. We keep the address because it is needed for matching a query string.
-}
urlUpdate : ( Route, Address ) -> Model -> ( Model, Cmd Msg )
urlUpdate ( route, address ) model =
( { model | route = route, address = address }, Cmd.none )
-- VIEWS
view : Model -> Html Msg
view model =
div []
[ menu model
, pageView model
]
menu : Model -> Html Msg
menu model =
div []
[ div []
[ button
[ class "btnMain"
, onClick (NavigateTo "")
]
[ text "Main" ]
, button
[ class "btnAbout"
, onClick (NavigateTo "about")
]
[ text "About" ]
, button
[ class "btnQuery"
, onClick (SetQuery (Dict.singleton "keyword" "el/m"))
]
[ text "Set query string" ]
, currentQuery model
]
]
currentQuery : Model -> Html msg
currentQuery model =
let
query =
toString model.address.query
in
span [ class "labelQuery" ]
[ text query ]
{-|
Views can decide what to show using `model.route`.
-}
pageView : Model -> Html msg
pageView model =
case model.route of
MainRoute ->
div [] [ h2 [ class "title" ] [ text "Main" ] ]
AboutRoute ->
div [] [ h2 [ class "title" ] [ text "About" ] ]
NotFoundRoute ->
div [] [ h2 [ class "title" ] [ text "Not found" ] ]
-- APP
{-|
Your init function will receive an initial payload from Navigation, this payload is the initial matched location.
Here we store the `route` and `address` in our model.
-}
init : ( Route, Address ) -> ( Model, Cmd Msg )
init ( route, address ) =
( Model address route, Cmd.none )
{-|
Wire everything using Navigation.
-}
main : Program Never
main =
Navigation.program urlParser
{ init = init
, view = view
, update = update
, urlUpdate = urlUpdate
, subscriptions = (always Sub.none)
}
================================================
FILE: examples/basic/elm-package.json
================================================
{
"version": "1.0.0",
"summary": "helpful summary of your project, less than 80 characters",
"repository": "https://github.com/user/project.git",
"license": "BSD3",
"source-directories": [
"../../src/"
],
"exposed-modules": [],
"dependencies": {
"elm-lang/core": "4.0.0 <= v < 5.0.0",
"elm-lang/html": "1.0.0 <= v < 2.0.0",
"elm-lang/navigation": "1.0.0 <= v < 2.0.0",
"evancz/elm-http": "3.0.1 <= v < 4.0.0",
"evancz/url-parser": "1.0.0 <= v < 2.0.0"
},
"elm-version": "0.16.0 <= v < 0.18.0"
}
================================================
FILE: examples/basic/install-packages.sh
================================================
#!/bin/bash
n=0
until [ $n -ge 5 ]
do
elm package install -y && break
n=$[$n+1]
sleep 15
done
================================================
FILE: examples/basic/readme.md
================================================
# Basic Hop Example
- Install packages `elm package install -y`
- Run `elm reactor`
- Open `http://localhost:8000/Main.elm`
================================================
FILE: examples/full/.gitignore
================================================
index.html
================================================
FILE: examples/full/dev_server.js
================================================
var path = require('path');
var express = require('express');
var http = require('http');
var webpack = require('webpack');
var config = require('./webpack.config');
var app = express();
var compiler = webpack(config);
var host = 'localhost';
var port = 3000;
app.use(require('webpack-dev-middleware')(compiler, {
// contentBase: 'src',
noInfo: true,
publicPath: config.output.publicPath,
inline: true,
stats: { colors: true },
}))
app.get('/app', function(req, res) {
res.sendFile(path.join(__dirname, 'public/index.html'));
});
app.get('/app/*', function(req, res) {
res.sendFile(path.join(__dirname, 'public/index.html'));
});
// When hitting / redirect to app
app.get('/', function(req, res) {
res.redirect('/app');
});
// Server images
app.use(express.static('public'));
var server = http.createServer(app);
server.listen(port, function(err) {
if (err) {
console.log(err);
return;
}
var addr = server.address();
console.log('Listening at http://%s:%d', addr.address, addr.port);
})
================================================
FILE: examples/full/elm-package.json
================================================
{
"version": "1.0.0",
"summary": "helpful summary of your project, less than 80 characters",
"repository": "https://github.com/user/project.git",
"license": "BSD3",
"source-directories": [
"./src",
"../../src/"
],
"exposed-modules": [],
"dependencies": {
"elm-lang/core": "4.0.0 <= v < 5.0.0",
"elm-lang/html": "1.0.0 <= v < 2.0.0",
"elm-lang/navigation": "1.0.0 <= v < 2.0.0",
"evancz/elm-http": "3.0.1 <= v < 4.0.0",
"evancz/url-parser": "1.0.0 <= v < 2.0.0"
},
"elm-version": "0.16.0 <= v < 0.18.0"
}
================================================
FILE: examples/full/install-packages.sh
================================================
#!/bin/bash
n=0
until [ $n -ge 5 ]
do
elm package install -y && break
n=$[$n+1]
sleep 15
done
================================================
FILE: examples/full/package.json
================================================
{
"name": "full",
"version": "1.0.0",
"description": "",
"main": "elm.js",
"scripts": {
"dev": "node dev_server.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"ace-css": "^1.0.1",
"css-loader": "^0.23.1",
"elm-webpack-loader": "^3.0.1",
"express": "^4.13.4",
"file-loader": "^0.8.5",
"style-loader": "^0.13.1",
"url-loader": "^0.5.7",
"webpack": "^1.12.14",
"webpack-dev-middleware": "^1.5.1"
}
}
================================================
FILE: examples/full/readme.md
================================================
# Full Hop Example
This example uses push state.
To run:
```
elm package install -y
npm i
npm run dev
```
Open http://localhost:3000
## Webpack config
The Webpack config included in this example is not ready for production. Please refer to the Webpack site.
================================================
FILE: examples/full/src/Languages/Edit.elm
================================================
module Languages.Edit exposing (..)
import Html exposing (..)
import Html.Events exposing (on, targetValue)
import Html.Attributes exposing (href, style, src, value, name)
import Json.Decode as Json
import Languages.Models exposing (..)
import Languages.Messages exposing (..)
styles : Html.Attribute a
styles =
style
[ ( "float", "left" )
]
view : Language -> Html Msg
view language =
div [ styles ]
[ h2 [] [ text language.name ]
, form []
[ input
[ value language.name
, name "name"
, on "input" (Json.map (Update language.id "name") targetValue)
]
[]
]
]
================================================
FILE: examples/full/src/Languages/Filter.elm
================================================
module Languages.Filter exposing (..)
import Html exposing (..)
import Html.Events exposing (onClick)
import Html.Attributes exposing (id, class, href, style)
import Dict
import Languages.Messages exposing (..)
type alias ViewModel =
{}
styles : Html.Attribute a
styles =
style
[ ( "float", "left" )
, ( "margin-left", "2rem" )
, ( "margin-right", "2rem" )
]
view : ViewModel -> Html Msg
view model =
div [ styles ]
[ h2 [] [ text "Filter" ]
, btn "btnSetQuery" "SetQuery" (SetQuery (Dict.singleton "latests" "true"))
, div []
[ h3 [] [ text "Kind" ]
, div []
[ btn "btnAll" "All" (AddQuery (Dict.singleton "typed" ""))
, btn "btnDynamic" "Dynamic" (AddQuery (Dict.singleton "typed" "dynamic"))
, btn "btnStatic" "Static" (AddQuery (Dict.singleton "typed" "static"))
]
]
]
btn : String -> String -> Msg -> Html Msg
btn viewId label action =
button [ id viewId, class "btn btn-primary btn-small inline-block mr1", onClick action ]
[ text label ]
================================================
FILE: examples/full/src/Languages/List.elm
================================================
module Languages.List exposing (..)
import Html exposing (..)
import Html.Events exposing (onClick)
import Html.Attributes exposing (class, href, style)
import Hop.Types exposing (Address)
import Dict
import Languages.Models exposing (..)
import Languages.Messages exposing (..)
type alias ViewModel =
{ languages : List Language
, address : Address
}
styles : Html.Attribute a
styles =
style
[ ( "float", "left" )
, ( "margin-right", "2rem" )
]
hasTag : String -> Language -> Bool
hasTag tag language =
List.any (\t -> t == tag) language.tags
filteredLanguages : ViewModel -> List Language
filteredLanguages model =
let
typed =
model.address.query
|> Dict.get "typed"
|> Maybe.withDefault ""
in
case typed of
"" ->
model.languages
_ ->
List.filter (hasTag typed) model.languages
view : ViewModel -> Html Msg
view model =
div [ styles ]
[ h2 [] [ text "Languages" ]
, table []
[ tbody [] (tableRows (filteredLanguages model)) ]
]
tableRows : List Language -> List (Html Msg)
tableRows collection =
List.map rowView collection
rowView : Language -> Html Msg
rowView language =
tr []
[ td [] [ text (toString language.id) ]
, td []
[ text language.name
]
, td []
[ actionBtn (Show language.id) "Show"
, actionBtn (Edit language.id) "Edit"
]
]
actionBtn : Msg -> String -> Html Msg
actionBtn action label =
button [ class "regular btn btn-small", onClick action ]
[ text label ]
================================================
FILE: examples/full/src/Languages/Messages.elm
================================================
module Languages.Messages exposing (..)
import Dict
import Languages.Models exposing (..)
type alias Prop =
String
type alias Value =
String
type Msg
= Show LanguageId
| Edit LanguageId
| Update LanguageId Prop Value
| AddQuery (Dict.Dict String String)
| SetQuery (Dict.Dict String String)
================================================
FILE: examples/full/src/Languages/Models.elm
================================================
module Languages.Models exposing (..)
type alias LanguageId =
Int
type alias Language =
{ id : LanguageId
, name : String
, image : String
, tags : List String
}
-- ROUTING
type Route
= LanguagesRoute
| LanguageRoute LanguageId
| LanguageEditRoute LanguageId
languages : List Language
languages =
[ { id = 1
, name = "Elm"
, image = "elm"
, tags = [ "functional", "browser", "static" ]
}
, { id = 2
, name = "JavaScript"
, image = "js"
, tags = [ "functional", "oo", "browser", "dynamic", "prototypical" ]
}
, { id = 3
, name = "Go"
, image = "go"
, tags = [ "oo", "google", "static" ]
}
, { id = 4
, name = "Rust"
, image = "rust"
, tags = [ "functional", "mozilla", "static" ]
}
, { id = 5
, name = "Elixir"
, image = "elixir"
, tags = [ "functional", "erlang", "dynamic" ]
}
, { id = 6
, name = "Ruby"
, image = "ruby"
, tags = [ "oo", "japan", "1996", "dynamic", "classical" ]
}
, { id = 7
, name = "Python"
, image = "python"
, tags = [ "oo", "dynamic", "classical" ]
}
, { id = 8
, name = "Swift"
, image = "swift"
, tags = [ "functional", "apple", "static", "classical" ]
}
, { id = 9
, name = "Haskell"
, image = "haskell"
, tags = [ "functional", "static" ]
}
, { id = 10
, name = "Java"
, image = "java"
, tags = [ "oo", "static", "classical" ]
}
, { id = 11
, name = "C#"
, image = "csharp"
, tags = [ "oo", "microsoft", "static", "classical" ]
}
, { id = 12
, name = "PHP"
, image = "php"
, tags = [ "oo", "server", "1994", "dynamic", "classical" ]
}
]
================================================
FILE: examples/full/src/Languages/Routing.elm
================================================
module Languages.Routing exposing (..)
import Languages.Models exposing (..)
import UrlParser exposing ((</>), int, s, string)
matchers : List (UrlParser.Parser (Route -> a) a)
matchers =
[ UrlParser.format LanguageEditRoute (int </> s "edit")
, UrlParser.format LanguageRoute (int)
, UrlParser.format LanguagesRoute (s "")
]
toS : a -> String
toS =
toString
reverseWithPrefix : Route -> String
reverseWithPrefix route =
"/languages" ++ (reverse route)
reverse : Route -> String
reverse route =
case route of
LanguagesRoute ->
"/"
LanguageRoute id ->
"/" ++ (toS id)
LanguageEditRoute id ->
"/" ++ (toS id) ++ "/edit"
================================================
FILE: examples/full/src/Languages/Show.elm
================================================
module Languages.Show exposing (..)
import Html exposing (..)
import Html.Attributes exposing (id, href, style, src)
import Languages.Models exposing (..)
import Languages.Messages exposing (..)
styles : Html.Attribute a
styles =
style
[ ( "float", "left" )
]
view : Language -> Html Msg
view language =
div [ styles ]
[ h2 [ id "titleLanguage" ] [ text language.name ]
, img [ src ("/images/" ++ language.image ++ ".png") ] []
, tags language
]
tags : Language -> Html Msg
tags language =
div [] (List.map tag language.tags)
tag : String -> Html Msg
tag tagName =
span []
[ text (tagName ++ ", ")
]
================================================
FILE: examples/full/src/Languages/Update.elm
================================================
module Languages.Update exposing (..)
import Debug
import Navigation
import Hop exposing (output, outputFromPath, addQuery, setQuery)
import Hop.Types exposing (Config, Address)
import Routing
import Languages.Models exposing (..)
import Languages.Messages exposing (Msg(..))
import Languages.Routing
type alias UpdateModel =
{ languages : List Language
, address : Address
}
routerConfig : Config
routerConfig =
Routing.config
navigationCmd : String -> Cmd a
navigationCmd path =
path
|> outputFromPath routerConfig
|> Navigation.modifyUrl
update : Msg -> UpdateModel -> ( UpdateModel, Cmd Msg )
update message model =
case Debug.log "message" message of
Show id ->
let
path =
Languages.Routing.reverseWithPrefix (Languages.Models.LanguageRoute id)
in
( model, navigationCmd path )
Edit id ->
let
path =
Languages.Routing.reverseWithPrefix (Languages.Models.LanguageEditRoute id)
in
( model, navigationCmd path )
Update id prop value ->
let
udpatedLanguages =
List.map (updateWithId id prop value) model.languages
in
( { model | languages = udpatedLanguages }, Cmd.none )
AddQuery query ->
let
command =
model.address
|> addQuery query
|> output routerConfig
|> Navigation.modifyUrl
in
( model, command )
SetQuery query ->
let
command =
model.address
|> setQuery query
|> output routerConfig
|> Navigation.modifyUrl
in
( model, command )
updateWithId : LanguageId -> String -> String -> Language -> Language
updateWithId id prop value language =
if id == language.id then
case prop of
"name" ->
{ language | name = value }
_ ->
language
else
language
================================================
FILE: examples/full/src/Languages/View.elm
================================================
module Languages.View exposing (..)
import Html exposing (..)
import Html.Attributes exposing (href, style)
import Hop.Types exposing (Address)
import Languages.Models exposing (LanguageId, Language, Route, Route(..))
import Languages.Messages exposing (..)
import Languages.Filter
import Languages.List
import Languages.Show
import Languages.Edit
type alias ViewModel =
{ languages : List Language
, address : Address
, route : Route
}
containerStyle : Html.Attribute a
containerStyle =
style
[ ( "margin-bottom", "5rem" )
, ( "overflow", "auto" )
]
view : ViewModel -> Html Msg
view model =
div [ containerStyle ]
[ Languages.Filter.view {}
, Languages.List.view { languages = model.languages, address = model.address }
, subView model
]
subView : ViewModel -> Html Msg
subView model =
case model.route of
LanguageRoute languageId ->
let
maybeLanguage =
getLanguage model.languages languageId
in
case maybeLanguage of
Just language ->
Languages.Show.view language
Nothing ->
notFoundView model
LanguageEditRoute languageId ->
let
maybeLanguage =
getLanguage model.languages languageId
in
case maybeLanguage of
Just language ->
Languages.Edit.view language
_ ->
notFoundView model
LanguagesRoute ->
emptyView
emptyView : Html msg
emptyView =
span [] []
notFoundView : ViewModel -> Html msg
notFoundView model =
div []
[ text "Not Found"
]
getLanguage : List Language -> LanguageId -> Maybe Language
getLanguage languages id =
languages
|> List.filter (\lang -> lang.id == id)
|> List.head
================================================
FILE: examples/full/src/Main.elm
================================================
module Main exposing (..)
import Navigation
import Hop
import Hop.Types exposing (Address)
import Messages exposing (..)
import Models exposing (..)
import Update exposing (..)
import View exposing (..)
import Routing
import String
import UrlParser
urlParser : Navigation.Parser ( Route, Address )
urlParser =
let
parse path =
path
|> UrlParser.parse identity Routing.routes
|> Result.withDefault NotFoundRoute
matcher =
Hop.makeResolver Routing.config parse
in
Navigation.makeParser (.href >> matcher)
urlUpdate : ( Route, Address ) -> AppModel -> ( AppModel, Cmd Msg )
urlUpdate ( route, address ) model =
let
_ =
Debug.log "urlUpdate address" address
in
( { model | route = route, address = address }, Cmd.none )
init : ( Route, Address ) -> ( AppModel, Cmd Msg )
init ( route, address ) =
( newAppModel route address, Cmd.none )
main : Program Never
main =
Navigation.program urlParser
{ init = init
, view = view
, update = update
, urlUpdate = urlUpdate
, subscriptions = (always Sub.none)
}
================================================
FILE: examples/full/src/Messages.elm
================================================
module Messages exposing (..)
import Hop.Types exposing (Query)
import Languages.Messages
type Msg
= SetQuery Query
| LanguagesMsg Languages.Messages.Msg
| ShowHome
| ShowLanguages
| ShowAbout
================================================
FILE: examples/full/src/Models.elm
================================================
module Models exposing (..)
import Hop.Types exposing (Address, newAddress)
import Languages.Models exposing (Language, languages)
type Route
= HomeRoute
| AboutRoute
| LanguagesRoutes Languages.Models.Route
| NotFoundRoute
type alias AppModel =
{ languages : List Language
, address : Address
, route : Route
, selectedLanguage : Maybe Language
}
newAppModel : Route -> Address -> AppModel
newAppModel route address =
{ languages = languages
, address = address
, route = route
, selectedLanguage = Maybe.Nothing
}
================================================
FILE: examples/full/src/Routing.elm
================================================
module Routing exposing (..)
import Models exposing (..)
import Hop.Types exposing (Config)
import Languages.Routing
import UrlParser exposing ((</>), oneOf, int, s)
import Languages.Routing
matchers : List (UrlParser.Parser (Route -> a) a)
matchers =
[ UrlParser.format HomeRoute (s "")
, UrlParser.format AboutRoute (s "about")
, UrlParser.format LanguagesRoutes (s "languages" </> (oneOf Languages.Routing.matchers))
]
routes : UrlParser.Parser (Route -> a) a
routes =
oneOf matchers
config : Config
config =
{ basePath = "/app"
, hash = False
}
reverse : Route -> String
reverse route =
case route of
HomeRoute ->
""
AboutRoute ->
"about"
LanguagesRoutes subRoute ->
"languages" ++ Languages.Routing.reverse subRoute
NotFoundRoute ->
""
================================================
FILE: examples/full/src/Update.elm
================================================
module Update exposing (..)
import Debug
import Navigation
import Hop exposing (output, outputFromPath, setQuery)
import Hop.Types exposing (Config)
import Messages exposing (..)
import Models exposing (..)
import Routing
import Languages.Update
import Languages.Models
navigationCmd : String -> Cmd a
navigationCmd path =
path
|> outputFromPath Routing.config
|> Navigation.newUrl
routerConfig : Config
routerConfig =
Routing.config
update : Msg -> AppModel -> ( AppModel, Cmd Msg )
update message model =
case Debug.log "message" message of
LanguagesMsg subMessage ->
let
updateModel =
{ languages = model.languages
, address = model.address
}
( updatedModel, cmd ) =
Languages.Update.update subMessage updateModel
in
( { model | languages = updatedModel.languages }, Cmd.map LanguagesMsg cmd )
SetQuery query ->
let
command =
model.address
|> setQuery query
|> output routerConfig
|> Navigation.newUrl
in
( model, command )
ShowHome ->
let
path =
Routing.reverse HomeRoute
in
( model, navigationCmd path )
ShowLanguages ->
let
path =
Routing.reverse (LanguagesRoutes Languages.Models.LanguagesRoute)
in
( model, navigationCmd path )
ShowAbout ->
let
path =
Routing.reverse AboutRoute
in
( model, navigationCmd path )
================================================
FILE: examples/full/src/View.elm
================================================
module View exposing (..)
import Html exposing (..)
import Html.App
import Html.Events exposing (onClick)
import Html.Attributes exposing (id, class, href, style)
import Models exposing (..)
import Messages exposing (..)
import Languages.View
view : AppModel -> Html Msg
view model =
div []
[ menu model
, pageView model
]
menu : AppModel -> Html Msg
menu model =
div [ class "p2 white bg-black" ]
[ div []
[ menuLink ShowHome "btnHome" "Home"
, text "|"
, menuLink ShowLanguages "btnLanguages" "Languages"
, text "|"
, menuLink ShowAbout "btnAbout" "About"
]
]
menuLink : Msg -> String -> String -> Html Msg
menuLink message viewId label =
a
[ id viewId
, href "javascript://"
, onClick message
, class "white px2"
]
[ text label ]
pageView : AppModel -> Html Msg
pageView model =
case model.route of
HomeRoute ->
div [ class "p2" ]
[ h1 [ id "title", class "m0" ] [ text "Home" ]
, div [] [ text "Click on Languages to start routing" ]
]
AboutRoute ->
div [ class "p2" ]
[ h1 [ id "title", class "m0" ] [ text "About" ]
]
LanguagesRoutes languagesRoute ->
let
viewModel =
{ languages = model.languages
, route = languagesRoute
, address = model.address
}
in
div [ class "p2" ]
[ h1 [ id "title", class "m0" ] [ text "Languages" ]
, Html.App.map LanguagesMsg (Languages.View.view viewModel)
]
NotFoundRoute ->
notFoundView model
notFoundView : AppModel -> Html msg
notFoundView model =
div []
[ text "Not Found"
]
================================================
FILE: examples/full/src/index.js
================================================
'use strict';
require('ace-css/css/ace.css');
var Elm = require('./Main.elm');
var mountNode = document.getElementById('main');
var app = Elm.Main.embed(mountNode);
================================================
FILE: examples/full/webpack.config.js
================================================
var path = require("path");
/*
publicPath is used for finding the bundles during dev
e.g. http://localhost:3000/bundles/app.js
When the index.html is served using the webpack server then just specify the path.
When index.html is served using a framework e.g. from Rails, Phoenix or Go
then you must specify the full url where the webpack dev server is running e.g. http://localhost:4000/bundles/
This path is also used for resolving relative assets e.g. fonts from css. So for production and staging this path has to be
overriden. See webpack.prod.config.js
*/
var publicPath = '/bundles/'
module.exports = {
entry: {
app: [
'./src/index.js'
]
},
output: {
path: path.resolve(__dirname + '/dist'),
filename: '[name].js',
publicPath: publicPath,
},
module: {
loaders: [
{
test: /\.(css|scss)$/,
loaders: [
'style-loader',
'css-loader',
]
},
{
test: /\.html$/,
exclude: /node_modules/,
loader: 'file?name=[name].[ext]',
},
{
test: /\.elm$/,
exclude: [/elm-stuff/, /node_modules/],
loader: 'elm-webpack',
},
{
test: /\.woff(2)?(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: 'url-loader?limit=10000&minetype=application/font-woff',
},
{
test: /\.(ttf|eot|svg)(\?v=[0-9]\.[0-9]\.[0-9])?$/,
loader: 'file-loader',
},
],
noParse: /\.elm$/,
},
};
================================================
FILE: license.md
================================================
The MIT License (MIT)
Copyright (c) 2015-present, Sebastian Porto
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: package.json
================================================
{
"name": "Hop",
"version": "1.0.0",
"description": "",
"main": "",
"scripts": {
"test": "elm-test"
},
"author": "",
"license": "MIT",
"devDependencies": {
"elm-test": "^0.17.3"
}
}
================================================
FILE: readme.md
================================================
# Hop: Navigation and routing helpers for Elm SPAs
[](https://semaphoreci.com/sporto/hop)

__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.__
Hop is a helper library meant to be used with:
- [__Navigation v1__](http://package.elm-lang.org/packages/elm-lang/navigation) for listening to location changes in the browser and pushing changes to it.
- [__UrlParser v1__](http://package.elm-lang.org/packages/evancz/url-parser) for constructing routes and parsing URLs.
## What Hop provides
On top of these two packages above, Hop helps with:
- Abstracting the differences between push or hash routing
- Providing helpers for working with the query string
- Encode / Decode the location path
- Encode / Decode the query string
## Getting Started
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.
## Docs
### [Building routes](https://github.com/sporto/hop/blob/master/docs/building-routes.md)
### [Nesting routes](https://github.com/sporto/hop/blob/master/docs/nesting-routes.md)
### [Matching routes](https://github.com/sporto/hop/blob/master/docs/matching-routes.md)
### [Navigating](https://github.com/sporto/hop/blob/master/docs/navigating.md)
### [Reverse routing](https://github.com/sporto/hop/blob/master/docs/reverse-routing.md)
### [API](http://package.elm-lang.org/packages/sporto/hop/latest/)
### [Changelog](./docs/changelog.md)
### [Testing Hop](https://github.com/sporto/hop/blob/master/docs/testing.md)
## More docs
### [Upgrade guide 5 to 6](https://github.com/sporto/hop/blob/master/docs/upgrade-5-to-6.md)
### [Upgrade guide 4 to 5](https://github.com/sporto/hop/blob/master/docs/upgrade-4-to-5.md)
### [Upgrade guide 3 to 4](https://github.com/sporto/hop/blob/master/docs/upgrade-3-to-4.md)
### [Upgrade guide 2.1 to 3.0](https://github.com/sporto/hop/blob/master/docs/upgrade-2-to-3.md)
### [Version 5 documentation](https://github.com/sporto/hop/tree/v5)
### [Version 4 documentation](https://github.com/sporto/hop/tree/v4)
### [Version 3 documentation](https://github.com/sporto/hop/tree/v3)
### [Version 2 documentation](https://github.com/sporto/hop/tree/v2)
### Hash routing
A proper url should have the query before the hash e.g. `?keyword=Ja#/users/1`,
but when using hash routing, query parameters are appended after the hash path e.g. `#/users/1?keyword=Ja`.
This is done for aesthetics and so the router is fully controlled by the hash fragment.
## Examples
See `examples/basic` and `examples/full` folders. To run the example apps:
- Clone this repo
- Go to example folder
- Follow the readme in that folder
================================================
FILE: src/Hop/Address.elm
================================================
module Hop.Address exposing (..)
import Dict
import String
import Http exposing (uriEncode, uriDecode)
import Hop.Types exposing (..)
{-|
Get the Path
-}
getPath : Address -> String
getPath address =
address.path
|> List.map uriEncode
|> String.join "/"
|> String.append "/"
{-|
Get the query string from a Address.
Including ?
-}
getQuery : Address -> String
getQuery address =
if Dict.isEmpty address.query then
""
else
address.query
|> Dict.toList
|> List.map (\( k, v ) -> ( uriEncode k, uriEncode v ))
|> List.map (\( k, v ) -> k ++ "=" ++ v)
|> String.join "&"
|> String.append "?"
--------------------------------------------------------------------------------
-- PARSING
-- Parse a path into a Address
--------------------------------------------------------------------------------
parse : String -> Address
parse route =
{ path = parsePath route
, query = parseQuery route
}
extractPath : String -> String
extractPath route =
route
|> String.split "#"
|> List.reverse
|> List.head
|> Maybe.withDefault ""
|> String.split "?"
|> List.head
|> Maybe.withDefault ""
parsePath : String -> List String
parsePath route =
route
|> extractPath
|> String.split "/"
|> List.filter (\segment -> not (String.isEmpty segment))
|> List.map uriDecode
extractQuery : String -> String
extractQuery route =
route
|> String.split "?"
|> List.drop 1
|> List.head
|> Maybe.withDefault ""
parseQuery : String -> Query
parseQuery route =
route
|> extractQuery
|> String.split "&"
|> List.filter (not << String.isEmpty)
|> List.map queryKVtoTuple
|> Dict.fromList
{-| @priv
Convert a string to a tuple. Decode on the way.
"k=1" --> ("k", "1")
-}
queryKVtoTuple : String -> ( String, String )
queryKVtoTuple kv =
let
splitted =
kv
|> String.split "="
first =
splitted
|> List.head
|> Maybe.withDefault ""
firstDecoded =
uriDecode first
second =
splitted
|> List.drop 1
|> List.head
|> Maybe.withDefault ""
secondDecoded =
uriDecode second
in
( firstDecoded, secondDecoded )
================================================
FILE: src/Hop/AddressTest.elm
================================================
module Hop.AddressTest exposing (..)
import Dict
import Expect
import Hop.Address as Address
import Hop.Types as Types
import Test exposing (..)
getPathTest : Test
getPathTest =
let
inputs =
[ ( "it works"
, { path = [ "users", "1" ], query = Dict.singleton "k" "1" }
, "/users/1"
)
, ( "it encodes"
, { path = [ "us/ers", "1" ], query = Dict.empty }
, "/us%2Fers/1"
)
]
run ( testCase, address, expected ) =
test testCase
<| \() ->
let
actual =
Address.getPath address
in
Expect.equal expected actual
in
describe "getPath" (List.map run inputs)
getQuery : Test
getQuery =
let
inputs =
[ ( "it works"
, { path = [], query = Dict.singleton "k" "1" }
, "?k=1"
)
, ( "it encoders"
, { path = [], query = Dict.singleton "k" "a/b" }
, "?k=a%2Fb"
)
]
run ( testCase, address, expected ) =
test testCase
<| \() ->
let
actual =
Address.getQuery address
in
Expect.equal expected actual
in
describe "getQuery" (List.map run inputs)
parseTest : Test
parseTest =
let
inputs =
[ ( "it parses"
, "/users/1?a=1"
, { path = [ "users", "1" ], query = Dict.singleton "a" "1" }
)
, ( "it decodes"
, "/a%2Fb/1?k=x%2Fy"
, { path = [ "a/b", "1" ], query = Dict.singleton "k" "x/y" }
)
]
run ( testCase, location, expected ) =
test testCase
<| \() ->
let
actual =
Address.parse location
in
Expect.equal expected actual
in
describe "parse" (List.map run inputs)
all : Test
all =
describe "Location"
[ getPathTest
, getQuery
, parseTest
]
================================================
FILE: src/Hop/In.elm
================================================
module Hop.In exposing (..)
import Regex
import String
import Hop.Types exposing (Address, Config)
import Hop.Address exposing (parse)
{-| @priv
-}
ingest : Config -> String -> Address
ingest config href =
href
|> removeProtocol
|> removeDomain
|> getRelevantPathWithQuery config
|> parse
{-| @priv
-}
removeProtocol : String -> String
removeProtocol href =
href
|> String.split "//"
|> List.reverse
|> List.head
|> Maybe.withDefault ""
{-| @priv
-}
removeDomain : String -> String
removeDomain href =
href
|> String.split "/"
|> List.tail
|> Maybe.withDefault []
|> String.join "/"
|> String.append "/"
{-| @priv
-}
getRelevantPathWithQuery : Config -> String -> String
getRelevantPathWithQuery config href =
if config.hash then
href
|> String.split "#"
|> List.drop 1
|> List.head
|> Maybe.withDefault ""
else
href
|> String.split "#"
|> List.head
|> Maybe.withDefault ""
|> removeBase config
{-| @priv
Remove the basePath from a path
"/basepath/a/b?k=1" -> "/a/b?k=1"
-}
removeBase : Config -> String -> String
removeBase config pathWithQuery =
let
regex =
Regex.regex config.basePath
in
Regex.replace (Regex.AtMost 1) regex (always "") pathWithQuery
================================================
FILE: src/Hop/InTest.elm
================================================
module Hop.InTest exposing (..)
import Dict
import Expect
import Hop.In exposing (ingest)
import Test exposing (..)
type Route
= NotFound
config =
{ hash = True
, basePath = ""
}
configWithPath =
{ config | hash = False }
configWithPathAndBase =
{ configWithPath | basePath = "/app/v1" }
inputTest : Test
inputTest =
let
inputs =
[ ( "it parses an empty hash"
, config
, "http://localhost:3000/basepath"
, { path = [], query = Dict.empty }
)
, ( "it parses a hash"
, config
, "http://localhost:3000/basepath#/users/1"
, { path = [ "users", "1" ], query = Dict.empty }
)
, ( "it parses a path"
, configWithPath
, "http://localhost:3000/users/1"
, { path = [ "users", "1" ], query = Dict.empty }
)
, ( "it parses a path with basepath"
, configWithPathAndBase
, "http://localhost:3000/app/v1/users/1"
, { path = [ "users", "1" ], query = Dict.empty }
)
, ( "it parses a hash with query"
, config
, "http://localhost:3000/basepath#/users/1?a=1"
, { path = [ "users", "1" ], query = Dict.singleton "a" "1" }
)
, ( "it parses a path with query"
, configWithPath
, "http://localhost:3000/users/1?a=1"
, { path = [ "users", "1" ], query = Dict.singleton "a" "1" }
)
, ( "it parses a path with basepath and query"
, configWithPathAndBase
, "http://localhost:3000/app/v1/users/1?a=1"
, { path = [ "users", "1" ], query = Dict.singleton "a" "1" }
)
, ( "it decodes the query"
, config
, "http://localhost:3000/basepath#/?a%20b%26c%3Fd=1%202%263%3F4"
, { path = [], query = Dict.singleton "a b&c?d" "1 2&3?4" }
)
]
run ( testCase, config, href, expected ) =
test testCase
<| \() ->
let
actual =
ingest config href
in
Expect.equal expected actual
in
describe "ingest" (List.map run inputs)
all : Test
all =
describe "In"
[ inputTest
]
================================================
FILE: src/Hop/Out.elm
================================================
module Hop.Out exposing (..)
import String
import Hop.Types exposing (Address, Config)
import Hop.Utils exposing (dedupSlash)
import Hop.Address exposing (parse)
{-|
Make a real path from an address record.
This will add the hash and the basePath as necessary.
fromAddress config { path = ["users", "1"], query = Dict.empty }
==
"#/users/1"
-}
output : Config -> Address -> String
output config address =
let
-- path -> "/a/1"
path =
Hop.Address.getPath address
-- query -> "?a=1"
query =
Hop.Address.getQuery address
url =
if config.hash then
"#" ++ path ++ query
else if String.isEmpty config.basePath then
path ++ query
else if path == "/" then
"/" ++ config.basePath ++ query
else
"/" ++ config.basePath ++ path ++ query
realPath =
dedupSlash url
in
if realPath == "" then
"/"
else
realPath
{-|
Make a real path from a simulated path.
This will add the hash and the basePath as necessary.
toRealPath config "/users"
==
"#/users"
-}
outputFromPath : Config -> String -> String
outputFromPath config path =
path
|> Hop.Address.parse
|> output config
================================================
FILE: src/Hop/OutTest.elm
================================================
module Hop.OutTest exposing (..)
import Dict
import Expect
import Hop.Out as Out
import Hop.Types exposing (newAddress)
import Hop.TestHelper exposing (configWithHash, configWithPath, configPathAndBasePath)
import Test exposing (..)
outputTest : Test
outputTest =
let
empty =
newAddress
inputs =
[ ( "hash: it is empty when empty"
, configWithHash
, empty
, ""
, "#/"
)
, ( "path: it is empty when empty"
, configWithPath
, empty
, ""
, "/"
)
, ( "basepath: it has the basepath"
, configPathAndBasePath
, empty
, ""
, "/app/v1"
)
, ( "basepath: adds slash when missing"
, { configPathAndBasePath | basePath = "app/v1" }
, empty
, ""
, "/app/v1"
)
-- path
, ( "hash: it adds the path"
, configWithHash
, { empty | path = [ "a", "b" ] }
, "/a/b"
, "#/a/b"
)
, ( "path: it adds the path"
, configWithPath
, { empty | path = [ "a", "b" ] }
, "/a/b"
, "/a/b"
)
, ( "path: it adds the basepath and path"
, configPathAndBasePath
, { empty | path = [ "a", "b" ] }
, "/a/b"
, "/app/v1/a/b"
)
-- query
, ( "hash: it adds the query as pseudo query"
, configWithHash
, { empty | query = Dict.singleton "k" "1" }
, "?k=1"
, "#/?k=1"
)
, ( "path: it adds the query"
, configWithPath
, { empty | query = Dict.singleton "k" "1" }
, "?k=1"
, "/?k=1"
)
, ( "path: it adds the basepath query"
, configPathAndBasePath
, { empty | query = Dict.singleton "k" "1" }
, "?k=1"
, "/app/v1?k=1"
)
-- path and query
, ( "hash: it adds the path and query"
, configWithHash
, { empty | query = Dict.singleton "k" "1", path = [ "a", "b" ] }
, "/a/b?k=1"
, "#/a/b?k=1"
)
, ( "path: it adds the path and query"
, configWithPath
, { empty | query = Dict.singleton "k" "1", path = [ "a", "b" ] }
, "/a/b?k=1"
, "/a/b?k=1"
)
, ( "path: it adds the basepath, path and query"
, configPathAndBasePath
, { empty | query = Dict.singleton "k" "1", path = [ "a", "b" ] }
, "/a/b?k=1"
, "/app/v1/a/b?k=1"
)
, ( "hash: it encodes"
, configWithHash
, { empty | query = Dict.singleton "a/d" "1/4", path = [ "a/b", "1" ] }
, "/a%2Fb/1?a%2Fd=1%2F4"
, "#/a%2Fb/1?a%2Fd=1%2F4"
)
]
run ( testCase, config, address, path, expected ) =
[ test testCase
<| \() ->
let
actual =
Out.output config address
in
Expect.equal expected actual
, test testCase
<| \() ->
let
actual =
Out.outputFromPath config path
in
Expect.equal expected actual
]
tests =
List.concatMap run inputs
in
describe "output and outputFromPath" tests
all : Test
all =
describe "In"
[ outputTest
]
================================================
FILE: src/Hop/TestHelper.elm
================================================
module Hop.TestHelper exposing (..)
import Hop.Types exposing (Config)
configWithHash : Config
configWithHash =
{ basePath = ""
, hash = True
}
configWithPath : Config
configWithPath =
{ basePath = ""
, hash = False
}
configPathAndBasePath : Config
configPathAndBasePath =
{ basePath = "/app/v1"
, hash = False
}
================================================
FILE: src/Hop/Types.elm
================================================
module Hop.Types exposing (Config, Query, Address, newQuery, newAddress)
{-| Types used in Hop
#Types
@docs Config, Address, Query
#Factories
@docs newQuery, newAddress
-}
import Dict
{-| A Dict that holds query parameters
Dict.Dict String String
-}
type alias Query =
Dict.Dict String String
{-| A Record that represents the current location
Includes a `path` and a `query`
{
path: List String,
query: Query
}
-}
type alias Address =
{ path : List String
, query : Query
}
{-| Hop Configuration
- basePath: Only for pushState routing (not hash). e.g. "/app".
- hash: True for hash routing, False for pushState routing.
-}
type alias Config =
{ basePath : String
, hash : Bool
}
{-|
Create an empty Query record
-}
newQuery : Query
newQuery =
Dict.empty
{-|
Create an empty Address record
-}
newAddress : Address
newAddress =
{ query = newQuery
, path = []
}
================================================
FILE: src/Hop/Utils.elm
================================================
module Hop.Utils exposing (..)
import Regex
dedupSlash : String -> String
dedupSlash =
Regex.replace Regex.All (Regex.regex "/+") (\_ -> "/")
================================================
FILE: src/Hop.elm
================================================
module Hop
exposing
( addQuery
, clearQuery
, ingest
, makeResolver
, output
, outputFromPath
, pathFromAddress
, queryFromAddress
, removeQuery
, setQuery
)
{-| Navigation and routing utilities for single page applications. See [readme](https://github.com/sporto/hop) for usage.
# Consuming an URL from the browser
@docs ingest, makeResolver
# Preparing a URL for changing the browser location
@docs output, outputFromPath
# Work with an Address record
@docs pathFromAddress, queryFromAddress
# Modify the query string
@docs addQuery, setQuery, removeQuery, clearQuery
-}
import Dict
import String
import Hop.Address
import Hop.In
import Hop.Out
import Hop.Types exposing (Address, Config, Query)
---------------------------------------
-- INGEST
---------------------------------------
{-|
Convert a raw url to an Address record. Use this function for 'normalizing' the URL before parsing it.
This conversion will take in account your basePath and hash configuration.
E.g. with path routing
config =
{ basePath = ""
, hash = False
}
ingest config "http://localhost:3000/app/languages/1?k=1"
-->
{ path = ["app", "languages", "1" ], query = Dict.singleton "k" "1" }
E.g. with path routing and base path
config =
{ basePath = "/app/v1"
, hash = False
}
ingest config "http://localhost:3000/app/v1/languages/1?k=1"
-->
{ path = ["languages", "1" ], query = Dict.singleton "k" "1" }
E.g. with hash routing
config =
{ basePath = ""
, hash = True
}
ingest config "http://localhost:3000/app#/languages/1?k=1"
-->
{ path = ["languages", "1" ], query = Dict.singleton "k" "1" }
-}
ingest : Config -> String -> Address
ingest =
Hop.In.ingest
{-|
`makeResolver` normalizes the URL using your config and then gives that normalised URL to your parser.
Use this for creating a function to give to `Navigation.makeParser`.
See examples in `docs/matching-routes.md`.
Hop.makeResolver hopConfig parse
`makeResolver` takes 2 arguments.
### Config e.g.
{ basePath = ""
, hash = False
}
### Parse function
A function that receives the normalised path and returns the result of parsing it.
parse path =
path
|> UrlParser.parse identity routes
|> Result.withDefault NotFoundRoute
You parse function will receive the path like this:
`http://example.com/users/1` --> 'users/1/'
So it won't have a leading /, but it will have a trailing /. This is because the way UrlParse works.
### Return value from resolver
After being called with a URL the resolver will return a tuple with `(parse result, address)` e.g.
resolver =
Hop.makeResolver hopConfig parse
resolver "http://example.com/index.html#/users/2"
-->
( UserRoute 2, { path = ["users", "2"], query = ...} )
### Example
A complete example looks like:
urlParser : Navigation.Parser ( Route, Address )
urlParser =
let
parse path =
path
|> UrlParser.parse identity routes
|> Result.withDefault NotFoundRoute
resolver =
Hop.makeResolver hopConfig parse
in
Navigation.makeParser (.href >> resolver)
-}
makeResolver :
Config
-> (String -> result)
-> String
-> (result, Address)
makeResolver config parse rawInput =
let
address =
rawInput
|> ingest config
parseResult =
pathFromAddress address
++ "/"
|> String.dropLeft 1
|> parse
in
(parseResult, address)
---------------------------------------
-- CREATE OUTBOUND URLs
---------------------------------------
{-|
Convert an Address record to an URL to feed the browser.
This will take in account your basePath and hash config.
E.g. with path routing
output config { path = ["languages", "1" ], query = Dict.singleton "k" "1" }
-->
"/languages/1?k=1"
E.g. with hash routing
output config { path = ["languages", "1" ], query = Dict.singleton "k" "1" }
-->
"#/languages/1?k=1"
-}
output : Config -> Address -> String
output =
Hop.Out.output
{-|
Convert a string to an URL to feed the browser.
This will take in account your basePath and hash config.
E.g. with path routing
outputFromPath config "/languages/1?k=1"
-->
"/languages/1?k=1"
E.g. with path routing + basePath
outputFromPath config "/languages/1?k=1"
-->
"/app/languages/1?k=1"
E.g. with hash routing
output config "/languages/1?k=1"
-->
"#/languages/1?k=1"
-}
outputFromPath : Config -> String -> String
outputFromPath =
Hop.Out.outputFromPath
---------------------------------------
-- WORK WITH ADDRESS
---------------------------------------
{-|
Get the path as a string from an Address record.
address = { path = ["languages", "1" ], query = Dict.singleton "k" "1" }
pathFromAddress address
-->
"/languages/1"
-}
pathFromAddress : Address -> String
pathFromAddress =
Hop.Address.getPath
{-|
Get the query as a string from an Address record.
address = { path = ["app"], query = Dict.singleton "k" "1" }
queryFromAddress address
-->
"?k=1"
-}
queryFromAddress : Address -> String
queryFromAddress =
Hop.Address.getQuery
-------------------------------------------------------------------------------
-- QUERY MUTATION
-------------------------------------------------------------------------------
{-|
Add query string values (patches any existing values) to an Address record.
addQuery query address
addQuery (Dict.Singleton "b" "2") { path = [], query = Dict.fromList [("a", "1")] }
==
{ path = [], query = Dict.fromList [("a", "1"), ("b", "2")] }
- query is a dictionary with keys to add
To remove a key / value pair set the value to ""
-}
addQuery : Query -> Address -> Address
addQuery query location =
let
updatedQuery =
Dict.union query location.query
in
{ location | query = updatedQuery }
{-|
Set the whole query string (removes any existing values).
setQuery query address
-}
setQuery : Query -> Address -> Address
setQuery query location =
{ location | query = query }
{-|
Remove one key from the query string
removeQuery key address
-}
removeQuery : String -> Address -> Address
removeQuery key location =
let
updatedQuery =
Dict.remove key location.query
in
{ location | query = updatedQuery }
{-| Clear all query string values
clearQuery address
-}
clearQuery : Address -> Address
clearQuery location =
{ location | query = Dict.empty }
================================================
FILE: src/HopTest.elm
================================================
module HopTest exposing (..)
import Expect
import Test exposing (..)
import Hop.TestHelper exposing (configWithHash, configWithPath, configPathAndBasePath)
import Hop
makeResolverTest : Test
makeResolverTest =
let
inputs =
[ ( "path"
, configWithPath
, "http://example.com/users/1"
, "users/1/"
)
, ( "path with base"
, configPathAndBasePath
, "http://example.com/app/v1/users/1"
, "users/1/"
)
, ( "path"
, configWithHash
, "http://example.com/app#/users/1"
, "users/1/"
)
]
run ( testCase, config, href, expected ) =
test testCase
<| \() ->
let
resolver =
Hop.makeResolver config identity
( actual, _ ) =
resolver href
in
Expect.equal expected actual
in
describe "makeResolver" (List.map run inputs)
all : Test
all =
describe "Hop"
[ makeResolverTest
]
================================================
FILE: tests/IntegrationTest.elm
================================================
module IntegrationTest exposing (..)
import UrlParser exposing ((</>), oneOf, int, s)
import Navigation exposing (Location)
import Expect
import String
import Test exposing (..)
import Hop.TestHelper exposing (configWithHash, configWithPath, configPathAndBasePath)
import Hop
import Hop.Types exposing (Address, Config)
type alias UserId =
Int
type UserRoute
= UsersRoute
| UserRoute UserId
| UserEditRoute UserId
type MainRoute
= HomeRoute
| AboutRoute
| UsersRoutes UserRoute
| NotFoundRoute
usersMatchers =
[ UrlParser.format UserEditRoute (int </> s "edit")
, UrlParser.format UserRoute (int)
, UrlParser.format UsersRoute (s "")
]
mainMatchers =
[ UrlParser.format HomeRoute (s "")
, UrlParser.format AboutRoute (s "about")
, UrlParser.format UsersRoutes (s "users" </> (oneOf usersMatchers))
]
routes =
oneOf mainMatchers
newLocation : Location
newLocation =
{ hash = ""
, host = "example.com"
, hostname = "example.com"
, href = ""
, origin = ""
, password = ""
, pathname = ""
, port_ = ""
, protocol = "http"
, search = ""
, username = ""
}
parseWithUrlParser : Config -> Location -> ( MainRoute, Address )
parseWithUrlParser currentConfig =
let
parse path =
path
|> UrlParser.parse identity routes
|> Result.withDefault NotFoundRoute
in
.href >> Hop.makeResolver currentConfig parse
------------------------------
-- Example urlParsers
------------------------------
urlParserRouteAddress : Navigation.Parser ( MainRoute, Address )
urlParserRouteAddress =
let
parse path =
path
|> UrlParser.parse identity routes
|> Result.withDefault NotFoundRoute
solver =
Hop.makeResolver configWithHash parse
in
Navigation.makeParser (.href >> solver)
urlParserOnlyRoute : Navigation.Parser MainRoute
urlParserOnlyRoute =
let
parse path =
path
|> UrlParser.parse identity routes
|> Result.withDefault NotFoundRoute
solver =
Hop.makeResolver configWithHash parse
in
Navigation.makeParser (.href >> solver >> fst)
urlParserResultAddress : Navigation.Parser (Result String MainRoute, Address)
urlParserResultAddress =
let
parse path =
path
|> UrlParser.parse identity routes
solver =
Hop.makeResolver configWithHash parse
in
Navigation.makeParser (.href >> solver)
urlParserIntegrationTest : Test
urlParserIntegrationTest =
let
inputs =
[ ( "Home page"
, configWithPath
, "http://example.com"
, HomeRoute
, "/"
)
, ( "Base: Home page"
, configPathAndBasePath
, "http://example.com/app/v1"
, HomeRoute
, "/app/v1"
)
, ( "Hash: Home page with /#"
, configWithHash
, "http://example.com/#"
, HomeRoute
, "#/"
)
, ( "Hash: Home page with /#/"
, configWithHash
, "http://example.com/#/"
, HomeRoute
, "#/"
)
, ( "Hash: Home page without hash"
, configWithHash
, "http://example.com"
, HomeRoute
, "#/"
)
, ( "Hash: Home page"
, configWithHash
, "http://example.com/index.html"
, HomeRoute
, "#/"
)
-- about
, ( "AboutRoute"
, configWithPath
, "http://example.com/about"
, AboutRoute
, "/about"
)
, ( "Base: AboutRoute"
, configPathAndBasePath
, "http://example.com/app/v1/about"
, AboutRoute
, "/app/v1/about"
)
, ( "Hash: AboutRoute"
, configWithHash
, "http://example.com/#/about"
, AboutRoute
, "#/about"
)
, ( "Hash: AboutRoute with slash"
, configWithHash
, "http://example.com/app#/about"
, AboutRoute
, "#/about"
)
-- users
, ( "UsersRoute"
, configWithPath
, "http://example.com/users"
, UsersRoutes UsersRoute
, "/users"
)
, ( "Base: UsersRoute"
, configPathAndBasePath
, "http://example.com/app/v1/users"
, UsersRoutes UsersRoute
, "/app/v1/users"
)
, ( "Hash: UsersRoute"
, configWithHash
, "http://example.com/#/users"
, UsersRoutes UsersRoute
, "#/users"
)
-- users with query
, ( "UsersRoute"
, configWithPath
, "http://example.com/users?k=1"
, UsersRoutes UsersRoute
, "/users?k=1"
)
, ( "Base: UsersRoute"
, configPathAndBasePath
, "http://example.com/app/v1/users?k=1"
, UsersRoutes UsersRoute
, "/app/v1/users?k=1"
)
, ( "Hash: UsersRoute"
, configWithHash
, "http://example.com/#/users?k=1"
, UsersRoutes UsersRoute
, "#/users?k=1"
)
-- user
, ( "UserRoute"
, configWithPath
, "http://example.com/users/2"
, UsersRoutes (UserRoute 2)
, "/users/2"
)
, ( "Base: UserRoute"
, configPathAndBasePath
, "http://example.com/app/v1/users/2"
, UsersRoutes (UserRoute 2)
, "/app/v1/users/2"
)
, ( "Hash: UserRoute"
, configWithHash
, "http://example.com/#/users/2"
, UsersRoutes (UserRoute 2)
, "#/users/2"
)
-- user edit
, ( "UserRoute"
, configWithPath
, "http://example.com/users/2/edit"
, UsersRoutes (UserEditRoute 2)
, "/users/2/edit"
)
, ( "Base: UserRoute"
, configPathAndBasePath
, "http://example.com/app/v1/users/2/edit"
, UsersRoutes (UserEditRoute 2)
, "/app/v1/users/2/edit"
)
, ( "Hash: UserRoute"
, configWithHash
, "http://example.com/#/users/2/edit"
, UsersRoutes (UserEditRoute 2)
, "#/users/2/edit"
)
]
run ( testCase, currentConfig, href, expected, expectedRoundTrip ) =
[ test testCase
<| \() ->
let
location =
{ newLocation | href = href }
( actual, _ ) =
parseWithUrlParser currentConfig location
in
Expect.equal expected actual
, test (testCase ++ " - output")
<| \() ->
let
location =
{ newLocation | href = href }
( _, address ) =
parseWithUrlParser currentConfig location
actual =
Hop.output currentConfig address
in
Expect.equal expectedRoundTrip actual
]
in
describe "UrlParser integration" (List.concatMap run inputs)
all : Test
all =
describe "Integration"
[ urlParserIntegrationTest
]
================================================
FILE: tests/Main.elm
================================================
port module Main exposing (..)
import Tests
import Test.Runner.Node exposing (run)
import Json.Encode exposing (Value)
main : Program Value
main =
run emit Tests.all
port emit : ( String, Value ) -> Cmd msg
================================================
FILE: tests/Tests.elm
================================================
module Tests exposing (..)
import HopTest
import Hop.AddressTest
import Hop.InTest
import Hop.OutTest
import IntegrationTest
import Test exposing (..)
all : Test
all =
describe "Hop"
[ HopTest.all
, Hop.AddressTest.all
, Hop.InTest.all
, Hop.OutTest.all
, IntegrationTest.all
]
================================================
FILE: tests/elm-package.json
================================================
{
"version": "0.0.1",
"summary": "A router for SPAs in Elm",
"repository": "https://github.com/sporto/hop.git",
"license": "MIT",
"source-directories": [
".",
"../src"
],
"exposed-modules": [],
"dependencies": {
"elm-community/elm-test": "2.1.0 <= v < 3.0.0",
"elm-lang/core": "4.0.0 <= v < 5.0.0",
"elm-lang/navigation": "1.0.0 <= v < 2.0.0",
"evancz/elm-http": "3.0.1 <= v < 4.0.0",
"evancz/url-parser": "1.0.0 <= v < 2.0.0",
"rtfeldman/node-test-runner": "2.0.0 <= v < 3.0.0"
},
"elm-version": "0.16.0 <= v < 0.18.0"
}
================================================
FILE: tests/install-packages.sh
================================================
#!/bin/bash
n=0
until [ $n -ge 5 ]
do
elm package install -y && break
n=$[$n+1]
sleep 15
done
================================================
FILE: tests/package.json
================================================
{
"name": "test",
"version": "1.0.0",
"description": "",
"main": "elm.js",
"scripts": {
"test": "elm-test"
},
"author": "",
"license": "ISC",
"dependencies": {
"elm-test": "^0.17.3"
}
}
gitextract_i1usn_9t/
├── .editorconfig
├── .gitignore
├── .vscode/
│ └── settings.json
├── Makefile
├── assets/
│ └── logo.idraw
├── 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
│ ├── 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/
│ │ ├── Address.elm
│ │ ├── AddressTest.elm
│ │ ├── In.elm
│ │ ├── InTest.elm
│ │ ├── Out.elm
│ │ ├── OutTest.elm
│ │ ├── TestHelper.elm
│ │ ├── Types.elm
│ │ └── Utils.elm
│ ├── Hop.elm
│ └── HopTest.elm
└── tests/
├── IntegrationTest.elm
├── Main.elm
├── Tests.elm
├── elm-package.json
├── install-packages.sh
└── package.json
Condensed preview — 66 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (88K chars).
[
{
"path": ".editorconfig",
"chars": 222,
"preview": "[*]\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 2\n\n[*.js]\nindent_style = tab\nindent_"
},
{
"path": ".gitignore",
"chars": 108,
"preview": ".DS_Store\nelm-stuff\nnode_modules\nelm.js\ndocumentation.json\n.vscode/tasks.json\nnpm-debug.log\n.envrc\ntests.js\n"
},
{
"path": ".vscode/settings.json",
"chars": 213,
"preview": "// Place your settings in this file to overwrite default and user settings.\n{\n \"search.exclude\": {\n\t\t\"**/node_modules\":"
},
{
"path": "Makefile",
"chars": 606,
"preview": "# Start basic application example locally\nbasic-up:\n\tcd ./examples/basic && elm reactor\n\nfull-up:\n\tcd ./examples/full &&"
},
{
"path": "docs/building-routes.md",
"chars": 1824,
"preview": "# Building routes\n\nAs of version 6 Hop doesn't provide matchers anymore, instead you can use [__UrlParser__](http://pack"
},
{
"path": "docs/changelog.md",
"chars": 1056,
"preview": "# Changelog\n\n### 6.0.0\n\n- Remove matchers (Use UrlParser instead)\n- Rename input and output functions\n- Encode and decod"
},
{
"path": "docs/matching-routes.md",
"chars": 2013,
"preview": "# Matching routes\n\nCreate a parser using `Navigation.makeParser` combined with `Hop.makeResolver`.\nThere are serveral st"
},
{
"path": "docs/navigating.md",
"chars": 1390,
"preview": "# Navigating\n\n## Changing the location\n\nUse `Hop.outputFromPath` for changing the browser location.\n\nAdd a message:\n\n```"
},
{
"path": "docs/nesting-routes.md",
"chars": 726,
"preview": "# Nesting routes\n\nUrlParser supports nested routes:\n\n```elm\ntype UserRoute\n = UsersRoute\n | UserRoute UserId\n\ntype"
},
{
"path": "docs/reverse-routing.md",
"chars": 465,
"preview": "# Reverse routing\n\nReverse routing means converting a route tag back to an url e.g.\n\n```\nUserRoute 1 --> \"/users/1\"\n```\n"
},
{
"path": "docs/testing.md",
"chars": 96,
"preview": "# Testing Hop\n\n## Unit tests\n\n```bash\ncd tests\nelm package install -y\n\ncd ..\nnpm i\nnpm test\n```\n"
},
{
"path": "docs/upgrade-2-to-3.md",
"chars": 1923,
"preview": "# Upgrading from 2 to 3\n\nHop has changed in many ways in version 3. Please review the [Getting started guide](https://gi"
},
{
"path": "docs/upgrade-3-to-4.md",
"chars": 611,
"preview": "# Upgrading from 3 to 4\n\nVersion 4 introduces push state. There are two major changes:\n\n## Config\n\nConfig now includes `"
},
{
"path": "docs/upgrade-4-to-5.md",
"chars": 354,
"preview": "# Upgrading from 4 to 5\n\nVersion 5 works with Navigation and Elm 0.17\n\n## Matchers\n\nAll matchers stay the same as in ver"
},
{
"path": "docs/upgrade-5-to-6.md",
"chars": 123,
"preview": "# Upgrading from 5 to 6\n\nVersion 6 removes Hop matchers. Use UrlParser instead. See Building Routes document for example"
},
{
"path": "elm-package.json",
"chars": 472,
"preview": "{\n \"version\": \"6.0.1\",\n \"summary\": \"Routing and Navigation helpers for SPAs in Elm\",\n \"repository\": \"https://gi"
},
{
"path": "examples/basic/.dockerignore",
"chars": 20,
"preview": "Dockefile\nelm-stuff\n"
},
{
"path": "examples/basic/.gitignore",
"chars": 11,
"preview": "index.html\n"
},
{
"path": "examples/basic/Main.elm",
"chars": 6604,
"preview": "module Main exposing (..)\n\n{-|\nYou will need Navigation, UrlParser and Hop.\n\n```\nelm package install elm-lang/navigation"
},
{
"path": "examples/basic/elm-package.json",
"chars": 583,
"preview": "{\n \"version\": \"1.0.0\",\n \"summary\": \"helpful summary of your project, less than 80 characters\",\n \"repository\": \""
},
{
"path": "examples/basic/install-packages.sh",
"chars": 101,
"preview": "#!/bin/bash\n\nn=0\nuntil [ $n -ge 5 ]\ndo\n elm package install -y && break\n n=$[$n+1]\n sleep 15\ndone\n"
},
{
"path": "examples/basic/readme.md",
"chars": 125,
"preview": "# Basic Hop Example\n\n- Install packages `elm package install -y`\n- Run `elm reactor`\n- Open `http://localhost:8000/Main."
},
{
"path": "examples/full/.gitignore",
"chars": 11,
"preview": "index.html\n"
},
{
"path": "examples/full/dev_server.js",
"chars": 1049,
"preview": "var path = require('path');\nvar express = require('express');\nvar http = require('http');\nvar webpack = require('w"
},
{
"path": "examples/full/elm-package.json",
"chars": 600,
"preview": "{\n \"version\": \"1.0.0\",\n \"summary\": \"helpful summary of your project, less than 80 characters\",\n \"repository\": \""
},
{
"path": "examples/full/install-packages.sh",
"chars": 101,
"preview": "#!/bin/bash\n\nn=0\nuntil [ $n -ge 5 ]\ndo\n elm package install -y && break\n n=$[$n+1]\n sleep 15\ndone\n"
},
{
"path": "examples/full/package.json",
"chars": 467,
"preview": "{\n \"name\": \"full\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"elm.js\",\n \"scripts\": {\n \"dev\": \"node dev_s"
},
{
"path": "examples/full/readme.md",
"chars": 264,
"preview": "# Full Hop Example\n\nThis example uses push state.\n\nTo run:\n\n```\nelm package install -y\nnpm i\nnpm run dev\n```\n\nOpen http:"
},
{
"path": "examples/full/src/Languages/Edit.elm",
"chars": 716,
"preview": "module Languages.Edit exposing (..)\n\nimport Html exposing (..)\nimport Html.Events exposing (on, targetValue)\nimport Html"
},
{
"path": "examples/full/src/Languages/Filter.elm",
"chars": 1142,
"preview": "module Languages.Filter exposing (..)\n\nimport Html exposing (..)\nimport Html.Events exposing (onClick)\nimport Html.Attri"
},
{
"path": "examples/full/src/Languages/List.elm",
"chars": 1714,
"preview": "module Languages.List exposing (..)\n\nimport Html exposing (..)\nimport Html.Events exposing (onClick)\nimport Html.Attribu"
},
{
"path": "examples/full/src/Languages/Messages.elm",
"chars": 326,
"preview": "module Languages.Messages exposing (..)\n\nimport Dict\nimport Languages.Models exposing (..)\n\n\ntype alias Prop =\n Strin"
},
{
"path": "examples/full/src/Languages/Models.elm",
"chars": 1852,
"preview": "module Languages.Models exposing (..)\n\n\ntype alias LanguageId =\n Int\n\n\ntype alias Language =\n { id : LanguageId\n "
},
{
"path": "examples/full/src/Languages/Routing.elm",
"chars": 717,
"preview": "module Languages.Routing exposing (..)\n\nimport Languages.Models exposing (..)\nimport UrlParser exposing ((</>), int, s, "
},
{
"path": "examples/full/src/Languages/Show.elm",
"chars": 689,
"preview": "module Languages.Show exposing (..)\n\nimport Html exposing (..)\nimport Html.Attributes exposing (id, href, style, src)\nim"
},
{
"path": "examples/full/src/Languages/Update.elm",
"chars": 2252,
"preview": "module Languages.Update exposing (..)\n\nimport Debug\nimport Navigation\nimport Hop exposing (output, outputFromPath, addQu"
},
{
"path": "examples/full/src/Languages/View.elm",
"chars": 2001,
"preview": "module Languages.View exposing (..)\n\nimport Html exposing (..)\nimport Html.Attributes exposing (href, style)\nimport Hop."
},
{
"path": "examples/full/src/Main.elm",
"chars": 1188,
"preview": "module Main exposing (..)\n\nimport Navigation\nimport Hop\nimport Hop.Types exposing (Address)\nimport Messages exposing (.."
},
{
"path": "examples/full/src/Messages.elm",
"chars": 216,
"preview": "module Messages exposing (..)\n\nimport Hop.Types exposing (Query)\nimport Languages.Messages\n\n\ntype Msg\n = SetQuery Que"
},
{
"path": "examples/full/src/Models.elm",
"chars": 579,
"preview": "module Models exposing (..)\n\nimport Hop.Types exposing (Address, newAddress)\nimport Languages.Models exposing (Language,"
},
{
"path": "examples/full/src/Routing.elm",
"chars": 868,
"preview": "module Routing exposing (..)\n\nimport Models exposing (..)\nimport Hop.Types exposing (Config)\nimport Languages.Routing\nim"
},
{
"path": "examples/full/src/Update.elm",
"chars": 1828,
"preview": "module Update exposing (..)\n\nimport Debug\nimport Navigation\nimport Hop exposing (output, outputFromPath, setQuery)\nimpor"
},
{
"path": "examples/full/src/View.elm",
"chars": 1969,
"preview": "module View exposing (..)\n\nimport Html exposing (..)\nimport Html.App\nimport Html.Events exposing (onClick)\nimport Html.A"
},
{
"path": "examples/full/src/index.js",
"chars": 168,
"preview": "'use strict';\n\nrequire('ace-css/css/ace.css');\n\nvar Elm = require('./Main.elm');\nvar mountNode = document.getElementById"
},
{
"path": "examples/full/webpack.config.js",
"chars": 1479,
"preview": "var path = require(\"path\");\n\n/*\npublicPath is used for finding the bundles during dev\ne.g. http://localhost:3000/bundles"
},
{
"path": "license.md",
"chars": 1091,
"preview": "The MIT License (MIT)\n\nCopyright (c) 2015-present, Sebastian Porto\n\nPermission is hereby granted, free of charge, to any"
},
{
"path": "package.json",
"chars": 210,
"preview": "{\n \"name\": \"Hop\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"\",\n \"scripts\": {\n \"test\": \"elm-test\"\n },\n "
},
{
"path": "readme.md",
"chars": 3195,
"preview": "# Hop: Navigation and routing helpers for Elm SPAs\n\n[\n\nimport Dict\nimport String\nimport Http exposing (uriEncode, uriDecode)\nimport Hop.Types"
},
{
"path": "src/Hop/AddressTest.elm",
"chars": 2350,
"preview": "module Hop.AddressTest exposing (..)\n\nimport Dict\nimport Expect\nimport Hop.Address as Address\nimport Hop.Types as Types\n"
},
{
"path": "src/Hop/In.elm",
"chars": 1438,
"preview": "module Hop.In exposing (..)\n\nimport Regex\nimport String\nimport Hop.Types exposing (Address, Config)\nimport Hop.Address e"
},
{
"path": "src/Hop/InTest.elm",
"chars": 2511,
"preview": "module Hop.InTest exposing (..)\n\nimport Dict\nimport Expect\nimport Hop.In exposing (ingest)\nimport Test exposing (..)\n\nty"
},
{
"path": "src/Hop/Out.elm",
"chars": 1355,
"preview": "module Hop.Out exposing (..)\n\nimport String\nimport Hop.Types exposing (Address, Config)\nimport Hop.Utils exposing (dedup"
},
{
"path": "src/Hop/OutTest.elm",
"chars": 4020,
"preview": "module Hop.OutTest exposing (..)\n\nimport Dict\nimport Expect\nimport Hop.Out as Out\nimport Hop.Types exposing (newAddress)"
},
{
"path": "src/Hop/TestHelper.elm",
"chars": 356,
"preview": "module Hop.TestHelper exposing (..)\n\nimport Hop.Types exposing (Config)\n\n\nconfigWithHash : Config\nconfigWithHash =\n {"
},
{
"path": "src/Hop/Types.elm",
"chars": 941,
"preview": "module Hop.Types exposing (Config, Query, Address, newQuery, newAddress)\n\n{-| Types used in Hop\n\n#Types\n@docs Config, Ad"
},
{
"path": "src/Hop/Utils.elm",
"chars": 148,
"preview": "module Hop.Utils exposing (..)\n\nimport Regex\n\ndedupSlash : String -> String\ndedupSlash =\n Regex.replace Regex.All (Re"
},
{
"path": "src/Hop.elm",
"chars": 6859,
"preview": "module Hop\n exposing\n ( addQuery\n , clearQuery\n , ingest\n , makeResolver\n , output"
},
{
"path": "src/HopTest.elm",
"chars": 1226,
"preview": "module HopTest exposing (..)\n\nimport Expect\nimport Test exposing (..)\nimport Hop.TestHelper exposing (configWithHash, co"
},
{
"path": "tests/IntegrationTest.elm",
"chars": 8178,
"preview": "module IntegrationTest exposing (..)\n\nimport UrlParser exposing ((</>), oneOf, int, s)\nimport Navigation exposing (Locat"
},
{
"path": "tests/Main.elm",
"chars": 216,
"preview": "port module Main exposing (..)\n\nimport Tests\nimport Test.Runner.Node exposing (run)\nimport Json.Encode exposing (Value)\n"
},
{
"path": "tests/Tests.elm",
"chars": 307,
"preview": "module Tests exposing (..)\n\nimport HopTest\nimport Hop.AddressTest\nimport Hop.InTest\nimport Hop.OutTest\nimport Integratio"
},
{
"path": "tests/elm-package.json",
"chars": 625,
"preview": "{\n \"version\": \"0.0.1\",\n \"summary\": \"A router for SPAs in Elm\",\n \"repository\": \"https://github.com/sporto/hop.gi"
},
{
"path": "tests/install-packages.sh",
"chars": 101,
"preview": "#!/bin/bash\n\nn=0\nuntil [ $n -ge 5 ]\ndo\n elm package install -y && break\n n=$[$n+1]\n sleep 15\ndone\n"
},
{
"path": "tests/package.json",
"chars": 214,
"preview": "{\n \"name\": \"test\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"elm.js\",\n \"scripts\": {\n \"test\": \"elm-test\""
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the sporto/hop GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 66 files (77.8 KB), approximately 20.7k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.