Navegación
Ya vimos cómo servir una página, pero imaginemos que estamos creando un sitio web como package.elm-lang.org
, que tiene varias páginas como la búsqueda, el readme de cada paquete y la documentación, todas las cuales funcionan distinto. ¿Cómo podemos hacer eso?
Múltiples páginas
La forma más simple sería servir múltiples archivos HTML. Si vas a la página principal, cargas un nuevo HTML. Si vas a la documentación de elm/core
, cargas otro HTML. Si vas a la documentación de elm/json
, cargas otro HTML.
Hasta Elm 0.19, eso era lo que este sitio web hacía. Funciona, y es simple. Pero tiene sus debilidades:
- Páginas en blanco. La página se pone blanca cada vez que cargas nuevo HTML. ¿Podemos lograr una mejor transición?
- Solicitudes redundantes. Cada paquete tiene un sólo archivo
docs.json
, pero éste tiene que cargar cada vez que visitas la página de un módulo, comoString
oMaybe
. ¿Podemos compartir datos entre páginas? - Código redundante. La página principal y la documentación comparten muchas funciones, como
Html.text
yHtml.div
. ¿Podemos hacer que el mismo código sea compartido entre páginas?
Podemos mejorar los tres casos anteriores. La idea es sólo cargar el HTML una vez, y después manejar por nuestra cuenta los cambios de URL.
Página única
En vez de crear nuestro programa usando Browser.element
o Browser.document
, podemos crear una aplicación con Browser.application
para evitar cargar más HTML cuando cambia la URL.
application :
{ init : flags -> Url -> Key -> ( model, Cmd msg )
, view : model -> Document msg
, update : msg -> model -> ( model, Cmd msg )
, subscriptions : model -> Sub msg
, onUrlRequest : UrlRequest -> msg
, onUrlChange : Url -> msg
}
-> Program flags model msg
Extiende la funcionalidad de Browser.document
en tres situaciones importantes.
Cuando la aplicación inicia, la función init
recibe la Url
actual de la barra de navegación. Esto te permite mostrar distintas cosas dependiendo de la Url
.
Cuando alguien apreta un link, como <a href="/home">Inicio</a>
, es interceptado por UrlRequest
. Entonces en vez de cargar nuevo HTML y todo lo que eso conlleva, onUrlRequest
crea un mensaje para tu función update
donde puedes decidir qué quieres hacer. Puedes guardar la posición de desplazamiento en la página, persistir datos, cambiar la URL manualmente, etc.
Cuando la URL cambia, la nueva Url
es enviada a onUrlChange
. El mensaje resultante llegará a update
, donde puedes decidir cómo mostrar la nueva página.
En vez de cargar nuevo HTML, estas tres adiciones te dan control completo de los cambios de URL. Veámoslo en acción
Ejemplo
Partamos con el más básico programa que usa Browser.application
. Sólo guarda la URL actual en el modelo. Revisa el código y observa que lo nuevo e interesante ocurre casi todo dentro de la función update
. Nos adentraremos en esos detalles más abajo.
import Browser
import Browser.Navigation as Nav
import Html exposing (..)
import Html.Attributes exposing (..)
import Url
-- MAIN
main : Program () Model Msg
main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}
-- MODEL
type alias Model =
{ key : Nav.Key
, url : Url.Url
}
init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init flags url key =
( Model key url, Cmd.none )
-- UPDATE
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LinkClicked urlRequest ->
case urlRequest of
Browser.Internal url ->
( model, Nav.pushUrl model.key (Url.toString url) )
Browser.External href ->
( model, Nav.load href )
UrlChanged url ->
( { model | url = url }
, Cmd.none
)
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
-- VIEW
view : Model -> Browser.Document Msg
view model =
{ title = "URL Interceptor"
, body =
[ text "The current URL is: "
, b [] [ text (Url.toString model.url) ]
, ul []
[ viewLink "/home"
, viewLink "/profile"
, viewLink "/reviews/the-century-of-the-self"
, viewLink "/reviews/public-opinion"
, viewLink "/reviews/shah-of-shahs"
]
]
}
viewLink : String -> Html msg
viewLink path =
li [] [ a [ href path ] [ text path ] ]
La función update
puede manejar mensajes LinkClicked
y UrlChanged
. Hay harto nuevo en la rama que corresponde a LinkClicked
, así que revisemos eso primero.
UrlRequest
Cuando alguien hace clic en un link como <a href="/home">/home</a>
, esto produce un valor UrlRequest
:
type UrlRequest
= Internal Url.Url
| External String
La variante Internal
es para un link que tiene el mismo dominio actual. O sea que si estás dentro de https://example.com
, todos estos son links Internal
: settings#privacy
, /home
, https://example.com/home
, //example.com/home
.
La variante External
es para links que apuntan a un dominio distinto. Links como https://elm-lang.org/examples
, https://static.example.com
, y http://example.com/home
todos apuntan a dominios distintos. Nótese que al cambiar el protocolo de https
a http
, ya se considera un dominio diferente.
Cualquiera sea el link que se aprete, nuestro programa generará un mensaje LinkClicked
y lo enviará a la función update
. Y es ahí donde veremos el código más interesante para esta ocasión.
LinkClicked
La mayor parte de la lógica en update
es decidir qué hacer con estos valores UrlRequest
:
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
LinkClicked urlRequest ->
case urlRequest of
Browser.Internal url ->
( model, Nav.pushUrl model.key (Url.toString url) )
Browser.External href ->
( model, Nav.load href )
UrlChanged url ->
( { model | url = url }
, Cmd.none
)
Las funciones más interesantes son Nav.load
y Nav.pushUrl
. Ambas provienen del módulo Browser.Navigation
, que nos ayuda a cambiar la URL en diversas maneras. Estamos usando las dos funciones más comunes del módulo:
load
carga un nuevo HTML. Es equivalente a escribir la URL en la barra de dirección y apretar enter. Sin importar lo que que tenga el modelo, éste se descartará y una nueva página será cargada desde cero.pushUrl
cambia la URL, pero no carga nuevo HTML. En vez, se gatilla un mensajeUrlChanged
que podemos manejar manualmente. También añade una entrada en la historia de navegación para permitir que los botones “atrás” y “adelante” del navegador funcionen normalmente.
Si examinamos la función update
nuevamente, ya podremos entender un poco mejor cómo funciona todo en conjunto. Cuando el usuario apreta un link https://elm-lang.org
, recibimos un mensaje External
y usamos load
para cargar nuevo HTML desde esos servidores. Pero cuando el usuario apreta un link /home
, recibimos un mensaje Internal
y usamos pushUrl
para cambiar la URL sin cargar nuevo HTML.
Nota 1: Tanto los links
Internal
comoExternal
producen comandos inmediatamente en este ejemplo, pero eso no es necesario. Si alguien apreta un linkExternal
, tal vez quieres primero guardar contenido ingresado en un campo de texto, o tal vez quieres usargetViewport
para guardar la posición de desplazamiento vertical, en caso de que el usuario navegue de vuelta usando el botón “atrás”. Todo esto es posible ya que no es más que la misma funciónupdate
que conocemos, y puedes retrasar la navegación efectivamente todo lo que quieras.Nota 2: Si quieres restaurar “lo que se ve” al momento de navegar una vez que el usuario aprete el botón “atrás”, la posición de desplazamiento no es información perfecta. Si el usuario ajusta el tamaño de la ventana, o gira su dispositivo y lo pone apaisado, puede que la posición sea bastante incorrecta. Por eso, es mejor guardar justamente “lo que se ve”. Tal vez eso signifique usar
getViewportOf
para determinar exactamente qué hay en pantalla en ese momento. Los detalles dependen de cómo funciona tu aplicación, así que no puedo darte consejos más específicos.
UrlChanged
Hay más de una manera de que se generen mensajes UrlChanged
. Acabamos de ver que pushUrl
los produce, pero apretar los botones “atrás” y “adelante” es otra manera. Y como mencioné en las notas más arriba, cuando obtienes un mensaje LinkClicked
no hace falta emitir el comando pushUrl
inmediatamente.
Lo bueno de tener un mensaje UrlChanged
específico es que no importa cómo ni cuándo cambió la URL. Todo lo que necesitas saber es que cambió.
En nuestro ejemplo solamente estamos guardando la URL, pero en una aplicación web real necesitarías analizar la URL para determinar el contenido a mostrar. En la próxima página vamos a hablar de esto.
Nota: No expliqué nada sobre
Nav.Key
, para enfocarme en los conceptos más importantes. Pero para los interesados, aquí está la explicación.Un valor
Key
es una “llave de navegación”; es necesaria para crear comandos que cambian la URL, comopushUrl
. Sólo tienes acceso a un valorKey
cuando creas un programa usandoBrowser.application
, garantizando que tu programa tiene lo necesario para detectar estos cambios de URL. Si los valoresKey
estuvieran disponibles para otros tipos de programa, un programador despistado podría toparse con molestos bugs y aprender ciertas técnicas a tropezones.Por eso es que tenemos una línea en nuestro
Modelo
dedicada a almacenar laKey
. Es un bajo precio a pagar para que todos eviten una muy sutil categoría de problemas.