Skip to content
This repository was archived by the owner on Mar 9, 2018. It is now read-only.

sporto/hop

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Hop: A router for Elm SPAs

alt Hop

How this works

This router uses a list of tuples to configure routes e.g. (route, action). When a route is matched the router will call the action specified with the appropiate parameters.

To navigate to a different route you call Hop.navigateTo, this will return an effect that your application must run via ports.

This router is made to work with StartApp.

Hash routing

At the moment only hash routes are supported i.e. #/users/1.

Although a proper url should have the query before the hash e.g. ?keyword=Ja#/users/1, in Hop 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.

Setup

Import Hop

import Hop

Define actions to be called when a route matches:

type Action
  = HopAction Hop.Action
	| ShowUsers Hop.Payload
	| ShowUser Hop.Payload
	| ShowNotFound Hop.Payload

Hop.Payload is the payload that your action will receive when called. See about Payload below.

You need to define an action for when a route is not found e.g. ShowNotFound.

Define your routes:

routes : List (String, Hop.Payload -> Action)
routes = [
		("/users", ShowUsers),
		("/users/:id", ShowUser)
	]

This is a list of tuples with: (route to match, action to call).

To define dynamic parameter use :, this parameters will be filled by the router e.g. /posts/:postId/comments/:commentId.

Create the router

router : Hop.Router Action
router =
	Hop.new {
		routes = routes,
		notFoundAction = ShowNotFound
	}

routes is your list of routes defined above. notFoundAction is the action to call when a route is not found.

Hop.new will give you back a Hop.Router record:

{
	signal,
	payload,
	run
}

signal is the signal that will carry changes when the browser location changes.

payload is an initial payload when the router is created.

run is a task to match the initial route, this needs to be send to a port, more details later.

Add the router signal to StartApp inputs

Your start app configuration should include the router signal:

app =
	StartApp.start {
		init = init,
		update = update,
		view = view,
		inputs = [router.signal]
	}

This will allow the router to send signal to your application when the location changes.

Add fields to your model

Your model needs to store the router payload and an attribute for the current view to display:

type alias Model {
	routerPayload: Hop.Payload,
	currentView: String
}

See more details about Hop.Payload below.

Add update actions

Add entries to update for actions related to routing:

update action model =
	case action of
		ShowUsers payload ->
			({model | currentView = "users", routerPayload = payload}, Effects.none)

It is important that you update the router payload, this is used to store the current url and the current router parameters.

Wire up your views

Your views need to decide what to show. Use the attribute model.currentView for this. E.g.

subView address model =
  case model.currentView of
    "users" ->
      usersView address model
    "user" ->
      userView address model

Get information about the current route from routerPayload. e.g.

userId =
	model.routerPayload.params
		|> Dict.get "userId"
		|> Maybe.withDefault ""

Run the router

In order to match the initial route when the application is loaded you will need to create a port specifically for this.

port routeRunTask : Task () ()
port routeRunTask =
  router.run

About Hop.Payload

Your actions are called with a Payload record. This record has:

{
	params: Dict.Dict String String,
	url: Hop.Url
}

params Is dictionary of String String.

When a route matches the route params will be populated in this dictionary. Query string values will also be added here.

E.g. given the route "/users/:userId/projects/:projectId", when the current url is #/users/1/projects/2?color=red, params will contain:

Dict {
	"userId" => "1",
	"projectId" => "2",
	"color" => "red"
}

Navigation

You have two way to navigate:

1. Using plain a tags

	a [ href "#/users/1" ] [ text "User" ]

Note that you must add the # in this case.

2. Using effects

Add two actions

type Action
	= ...
	| HopAction Hop.Action
	| NavigateTo String

HopAction is necessary so effects from the router can be run.

Call the action from your view

button [ onClick address (NavigateTo "/users/1") ] [ text "User" ]

You don't need to add # in this case.

Respond to the action in update

update action model =
	case action of
		...
		NavigateTo path ->
			(model, Effects.map HopAction (Hop.navigateTo path))

Hop.navigateTo will respond with an effect that needs to be run by your application. When this effect is run the hash will change. After that your application will receive a location change signal as described before.

Changing the query string

Add actions for changing the query string

type Action
	= ...
	| AddQuery (Dict.Dict String String)
	| SetQuery (Dict.Dict String String)
	| ClearQuery

Change update to respond to these actions

update action model =
	case action of
		...
		AddQuery query ->
			(model, Effects.map HopAction (Hop.addQuery query model.routerPayload.url))
		SetQuery query ->
			(model, Effects.map HopAction (Hop.setQuery query model.routerPayload.url))
		ClearQuery ->
			(model, Effects.map HopAction (Hop.clearQuery model.routerPayload.url))

Call these actions from your views

button [ onClick address (SetQuery (Dict.singleton "color" "red")) ] [ text "Set query" ]

Adds the given Dict to the existing query.

Replaces the existing query with the given Dict.

Removes that key / value from the query string.

Removes the whole query string.

Examples

See examples app in ./Examples/. To run the example apps:

  • Clone this repo
  • Run elm reactor
  • Open http://localhost:8000/Examples/Basic/App.elm
  • Open http://localhost:8000/Examples/Advanced/App.elm

Test

elm reactor

Open /localhost:8000/TestRunner.elm

TODO:

  • Change hash without changing query
  • Navigate without adding to history
  • Support routes without hashes
  • Named routes maybe (Using the given action)
  • More tests

Improvements

  • In order to match the initial route we need to manually send tasks to a port. Done via route.run. This is one more thing for the user to do. Is this really necessary, can this be removed? e.g. Try to channel the initial match through the existing router.signal.

  • Remove the need to pass the current url to query methods. At the moment we need to send setQuery url dict because Hop cannot figure out the current query by itself. This project could be the solution.

Changelog

  • 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

About

[Deprecated] Navigation and routing helpers for single page applications in Elm

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 8