upsiflu / less-ui

Write your views across several screen regions, and hide all Ui state in the Url.
https://package.elm-lang.org/packages/upsiflu/less-ui/latest
BSD 3-Clause "New" or "Revised" License
6 stars 1 forks source link

Url (de)serialization: Cover and document all edge cases #35

Open upsiflu opened 1 year ago

upsiflu commented 1 year ago

Situation: Links and Urls are needlessly parsed and serialized. The serialization format is not declaratively stated. Filters and Links don't fit well in a single type.

Desires:

Related issues in order of decreasing relevance:

Questions:

  1. Can we just use elm-app-url which happens to treat "a=" as "a" but "=" as (""="")? How does it treat "a=b=c" which is relevant for making filters selectors? Is it a good basis for the link encoder and queryer and for the url*state parser? A: No.
  2. Do we need an Url sanitizer?

Api and Invariants

Link.href 
   : Link -> String
  1. hrefs are always minimal, i.e. they represent only the intended transition from the current state to the new one.
  2. Relative operations are encoded in protected assignments. Custom assignments are escaped accordingly.
Link.applyUrl 
   : { makeAbsolute : Bool } -> State -> Url -> State
  1. A new path and fragment replaces the old path and fragment. New flags append to the existing ones. New assignments replace the existing ones.
  2. Relative operations are made absolute if makeAbsolute is True:
    • Toggle becomes turn on;
    • Bounce { there, here } becomes GoTo there
  3. Imply any flag or path that needs to be present such that the intended path and fragment is rendered.
Link.isActive 
   : State -> Link -> Bool

Link.getCurrentData
    : State -> Link -> Maybe Data

When to use links

The Url is a state shared by all interactive elements in a tab. It is very easy to manually edit, to share and to reproduce, but its utility diminishes when the Url gets too long:

Sample patterns

Token

token : { flag, label } -> ((String, Ui), (String, Ui))
token { flag, label } =
    let
        inner = (flag, toggle { flag, label })
    in
    ( filter ( flag )
        ( \flags -> if List.member flag then inner else [] )
    , filter ( flag )
        ( \flags -> if List.member flag then[] else inner )
    )

Set

set = 
    [ { flag = "a", label = "A" }
    , { flag = "b", label = "B" }
    , { flag = "c", label = "C" }
    ]

(activeTokenList, inactiveTokenList) =
    List.map token set
        |> List.unzip
        |> Tuple.mapBoth (Html.ol []) (Html.ol [])

view =  
    activeTokenList ++ Html.singleton [ Html.text "⇋" ] ++ inactiveTokenList
upsiflu commented 1 year ago

Implement in Html:

wrapper Link Filter Toggle
"a#b" { bounce=""} List "?category=" "?flag"
"a#b" { bounce="c#d" } List "?category=subcat=" "?parameter=value"
Html tag aaria-current="page"
attr label
label ++ input current
type_ "search" value attr
a role "switch" aria-checked..
attr label
Contingent Ui List String -> Ui ( Ui, Ui )
Attribute href
"a#b?bounce=c%23d"
onInput
(Filtered [categories])
href
"?flag&toggle=flag"
Style isInline isInline isInline
Ui.Wrapper
Stateful (State -> { label : html, isInline : Bool, contingent : Ui ))

At Ui.view, the given layout unwraps the Link/Filter/Toggle wrapper into a Stateful; and then the html part is put smewhere according to .isInline, and the Ui part of it is rendered at the current region. Both the Ui and the label are checked for emptiness (like in the current implementation) and wrapped in layout.removed etc.

Advantages:

Disadvantages:

Question: Where should the Url and href management live? (a) Completely within Restrictive; we'd need a separate State or Msg module to provide defunc primitives for building transitions, including for the onInput Msg case (b) Completely within Ui, i.e. Ui would provide these primitives (c) In Html, i.e. Apps and Uis would remain stateless, and state would be routed through the Layout exclusively.

Answer: Since Restrictive.AppState owns the Url, we want to cram all parsing and serializing functions into that module. Otoh, Html already covers all link types, and link types may not generalize across layout types... which would favor (c)... but then, where to place the codecs?

Question: Where should the Link and Filter types live?

ToDo:

upsiflu commented 11 months ago

Question: After 2 refactorings and lots of little edge cases yet to cover, I wonder which approach is more future-proof: (a) (Re)Introduce a declarative builder/codec with opaque types. Less general than elm-url-codec and with some features features that elm-app-url is missing. Perhaps such a library exists already. (b) Use tests, preferably directly in the comments, instead of types, to "describe" the functionality and cover all edge cases