diff --git a/src/Bolero/Router.fs b/src/Bolero/Router.fs
index 7dacd871..b632139f 100644
--- a/src/Bolero/Router.fs
+++ b/src/Bolero/Router.fs
@@ -28,7 +28,9 @@ open System.Diagnostics.CodeAnalysis
open System.Net
open System.Runtime.CompilerServices
open System.Runtime.InteropServices
+open System.Text
open FSharp.Reflection
+open Microsoft.FSharp.Core.CompilerServices
/// A router that binds page navigation with Elmish.
/// The Elmish model type.
@@ -82,16 +84,23 @@ type Router<'ep, 'model, 'msg> =
/// Declare how an F# union case matches to a URI.
/// Routing
[]
-type EndPointAttribute
- /// Declare how an F# union case matches to a URI.
- /// The endpoint URI path.
- (endpoint: string) =
+type EndPointAttribute(path, query) =
inherit Attribute()
- let endpoint = endpoint.Trim('/').Split('/')
+ /// Declare how an F# union case matches to a URI.
+ /// The endpoint URI path and query.
+ new (endpoint: string) =
+ let path, query =
+ match endpoint.IndexOf('?') with
+ | -1 -> endpoint, ""
+ | n -> endpoint[..n-1], endpoint[n+1..]
+ EndPointAttribute(path.Trim('/').Split('/'),query)
/// The path that this endpoint recognizes.
- member this.Path = endpoint
+ member this.Path = path
+
+ /// The query string that this endpoint recognizes.
+ member this.Query = query
///
/// Declare that the given field of an F# union case matches the entire remainder of the URL path.
@@ -169,13 +178,18 @@ type PageModel<'T> =
module private RouterImpl =
open System.Text.RegularExpressions
- type ArraySegment<'T> with
- member this.Item with get i = this.Array[this.Offset + i]
+ type SingleParser = string -> option
+ type SingleWriter = obj -> option
+ type SingleSerializer =
+ {
+ parse: SingleParser
+ write: SingleWriter
+ }
type SegmentParserResult = option>
- type SegmentParser = list -> SegmentParserResult
- type SegmentWriter = obj -> list
- type Segment =
+ type SegmentParser = list -> Map -> SegmentParserResult
+ type SegmentWriter = obj -> list * Map
+ type SegmentSerializer =
{
parse: SegmentParser
write: SegmentWriter
@@ -190,64 +204,110 @@ module private RouterImpl =
else
None
- let inline defaultBaseTypeParser<'T when 'T : (static member TryParse : string * byref<'T> -> bool)> = function
- | [] -> None
- | x :: rest ->
- match tryParseBaseType<'T> x with
- | Some x -> Some (box x, rest)
- | None -> None
+ let inline baseTypeSingleSerializer<'T when 'T : (static member TryParse : string * byref<'T> -> bool)> () : SingleSerializer =
+ {
+ parse = tryParseBaseType<'T>
+ write = string >> Some
+ }
- let inline baseTypeSegment<'T when 'T : (static member TryParse : string * byref<'T> -> bool)> () =
+ let singleSegmentSerializer (s: SingleSerializer) : SegmentSerializer =
{
- parse = defaultBaseTypeParser<'T>
- write = fun x -> [string x]
+ parse = fun path _ ->
+ match path with
+ | [] -> None
+ | x :: rest ->
+ match s.parse x with
+ | Some x -> Some (x, rest)
+ | None -> None
+ write = fun x -> Option.toList (s.write x), Map.empty
}
- let baseTypes : IDictionary = dict [
+ let baseTypeSingleSerializers : IDictionary = dict [
typeof, {
- parse = function
- | [] -> None
- | x :: rest -> Some (box (WebUtility.UrlDecode x), rest)
- write = fun x -> [WebUtility.UrlEncode(unbox x)]
+ parse = fun x -> Some (box (WebUtility.UrlDecode x))
+ write = fun x -> Some (WebUtility.UrlEncode (unbox x))
}
typeof, {
- parse = defaultBaseTypeParser
+ parse = tryParseBaseType
// `string true` returns capitalized "True", but we want lowercase "true".
- write = fun x -> [(if unbox x then "true" else "false")]
+ write = fun x -> Some (if unbox x then "true" else "false")
}
- typeof, baseTypeSegment()
- typeof, baseTypeSegment()
- typeof, baseTypeSegment()
- typeof, baseTypeSegment()
- typeof, baseTypeSegment()
- typeof, baseTypeSegment()
- typeof, baseTypeSegment()
- typeof, baseTypeSegment()
- typeof, baseTypeSegment()
- typeof, baseTypeSegment()
- typeof, baseTypeSegment()
+ typeof, baseTypeSingleSerializer()
+ typeof, baseTypeSingleSerializer()
+ typeof, baseTypeSingleSerializer()
+ typeof, baseTypeSingleSerializer()
+ typeof, baseTypeSingleSerializer()
+ typeof, baseTypeSingleSerializer()
+ typeof, baseTypeSingleSerializer()
+ typeof, baseTypeSingleSerializer()
+ typeof, baseTypeSingleSerializer()
+ typeof, baseTypeSingleSerializer()
+ typeof, baseTypeSingleSerializer()
]
- let sequenceSegment getSegment (ty: Type) revAndConvert toListAndLength : Segment =
+ let baseTypeSegmentSerializers : IDictionary = dict [
+ for KeyValue(k, s) in baseTypeSingleSerializers do
+ k, singleSegmentSerializer s
+ ]
+
+ let getSingleSerializer (ty: Type) : SingleSerializer * obj voption =
+ match baseTypeSingleSerializers.TryGetValue(ty) with
+ | true, s -> s, ValueNone
+ | false, _ ->
+ if ty.IsGenericType &&
+ (let gen = ty.GetGenericTypeDefinition()
+ gen = typedefof> || gen = typedefof>)
+ then
+ match baseTypeSingleSerializers.TryGetValue(ty.GetGenericArguments()[0]) with
+ | true, s ->
+ let cases = FSharpType.GetUnionCases(ty)
+ let noneCase = cases[0]
+ let someCase = cases[1]
+ let someCtor = FSharpValue.PreComputeUnionConstructor(someCase)
+ let someDector = FSharpValue.PreComputeUnionReader(someCase)
+ let none = FSharpValue.MakeUnion(noneCase, Array.empty)
+ let getTag = FSharpValue.PreComputeUnionTagReader(ty)
+ {
+ parse = s.parse >> Option.map (fun x -> someCtor [|x|])
+ write = fun x ->
+ if getTag x = 0 then
+ None
+ else
+ s.write (someDector x).[0]
+ }, ValueSome none
+ | false, _ -> fail (InvalidRouterKind.UnsupportedType ty)
+ else
+ fail (InvalidRouterKind.UnsupportedType ty)
+
+ let merge (map1: Map<'k, 'v>) (map2: Map<'k, 'v>) =
+ Map.foldBack Map.add map1 map2
+
+ let sequenceSegment getSegment (ty: Type) revAndConvert toListAndLength : SegmentSerializer =
let itemSegment = getSegment ty
- let rec parse acc remainingLength fragments =
+ let rec parse acc remainingLength fragments query =
if remainingLength = 0 then
Some (revAndConvert acc, fragments)
else
- match itemSegment.parse fragments with
+ match itemSegment.parse fragments query with
| None -> None
| Some (x, rest) ->
- parse (x :: acc) (remainingLength - 1) rest
+ parse (x :: acc) (remainingLength - 1) rest query
{
- parse = function
+ parse = fun path query ->
+ match path with
| x :: rest ->
match Int32.TryParse(x) with
- | true, length -> parse [] length rest
+ | true, length -> parse [] length rest query
| false, _ -> None
| _ -> None
write = fun x ->
let list, (length: int) = toListAndLength x
- string length :: List.collect itemSegment.write list
+ let path, query =
+ (Map.empty, list)
+ ||> List.mapFold (fun query item ->
+ let segments, itemQuery = itemSegment.write item
+ segments, merge itemQuery query)
+ (string length :: List.concat path), query
}
let [] FLAGS_STATIC =
@@ -263,12 +323,12 @@ module private RouterImpl =
let arrayLengthAndBox<'T> (a: array<'T>) : list * int =
[for x in a -> box x], a.Length
- let arraySegment getSegment ty : Segment =
+ let arraySegment getSegment ty : SegmentSerializer =
let arrayRevAndUnbox =
- typeof.DeclaringType.GetMethod("arrayRevAndUnbox", FLAGS_STATIC)
+ typeof.DeclaringType.GetMethod("arrayRevAndUnbox", FLAGS_STATIC)
.MakeGenericMethod([|ty|])
let arrayLengthAndBox =
- typeof.DeclaringType.GetMethod("arrayLengthAndBox", FLAGS_STATIC)
+ typeof.DeclaringType.GetMethod("arrayLengthAndBox", FLAGS_STATIC)
.MakeGenericMethod([|ty|])
sequenceSegment getSegment ty
(fun l -> arrayRevAndUnbox.Invoke(null, [|l|]))
@@ -280,12 +340,12 @@ module private RouterImpl =
let listLengthAndBox<'T> (l: list<'T>) : list * int =
List.mapFold (fun l e -> box e, l + 1) 0 l
- let listSegment getSegment ty : Segment =
+ let listSegment getSegment ty : SegmentSerializer =
let listRevAndUnbox =
- typeof.DeclaringType.GetMethod("listRevAndUnbox", FLAGS_STATIC)
+ typeof.DeclaringType.GetMethod("listRevAndUnbox", FLAGS_STATIC)
.MakeGenericMethod([|ty|])
let listLengthAndBox =
- typeof.DeclaringType.GetMethod("listLengthAndBox", FLAGS_STATIC)
+ typeof.DeclaringType.GetMethod("listLengthAndBox", FLAGS_STATIC)
.MakeGenericMethod([|ty|])
sequenceSegment getSegment ty
(fun l -> listRevAndUnbox.Invoke(null, [|l|]))
@@ -308,13 +368,13 @@ module private RouterImpl =
| _ -> false
/// A {parameter} path segment.
- type Parameter =
+ type SegmentParameter =
{
/// A parameter can be common among multiple union cases.
/// `index` lists these cases, and for each of them, its total number of fields and the index of the field for this segment.
index: list
``type``: Type
- segment: Segment
+ segment: SegmentSerializer
modifier: ParameterModifier
/// Note that several cases can have the same parameter with different names.
/// In this case, the name field is taken from the first declared case.
@@ -324,7 +384,16 @@ module private RouterImpl =
/// Intermediate representation of a path segment.
type UnionParserSegment =
| Constant of string
- | Parameter of Parameter
+ | Parameter of SegmentParameter
+
+ type QueryParameter =
+ {
+ index: int
+ serializer: SingleSerializer
+ optionalDefaultValue: obj voption
+ name: string
+ propName: string
+ }
type UnionCase =
{
@@ -332,6 +401,7 @@ module private RouterImpl =
ctor: obj[] -> obj
argCount: int
segments: UnionParserSegment list
+ query: QueryParameter list
}
/// The parser for a union type at a given point in the path.
@@ -340,17 +410,17 @@ module private RouterImpl =
/// All recognized "/constant" segments, associated with the parser for the rest of the path.
constants: IDictionary
/// The recognized "/{parameter}" segment, if any.
- parameter: option
+ parameter: option
/// The union case that parses correctly if the path ends here, if any.
finalize: option
}
- let parseEndPointCasePath (case: UnionCaseInfo) : list =
- case.GetCustomAttributes()
+ let parseEndPointCasePathAndQuery (case: UnionCaseInfo) : list * string =
+ case.GetCustomAttributes(typeof)
|> Array.tryPick (function
- | :? EndPointAttribute as e -> Some (List.ofSeq e.Path)
+ | :? EndPointAttribute as e -> Some (List.ofSeq e.Path, e.Query)
| _ -> None)
- |> Option.defaultWith (fun () -> [case.Name])
+ |> Option.defaultWith (fun () -> [case.Name], "")
let isConstantFragment (s: string) =
not (s.Contains("{"))
@@ -397,7 +467,8 @@ module private RouterImpl =
else
fail (InvalidRouterKind.InvalidRestType case)
- let fragmentParameterRE = Regex(@"^\{([?*]?)([a-zA-Z0-9_]+)\}$", RegexOptions.Compiled)
+ let fragmentParameterRE = Regex(@"^\{([*]?)([a-zA-Z0-9_]+)\}$", RegexOptions.Compiled)
+ let queryParameterRE = Regex(@"^\{([a-zA-Z0-9_]+)\}$", RegexOptions.Compiled)
let isPageModel (ty: Type) =
ty.IsGenericType && ty.GetGenericTypeDefinition() = typedefof>
@@ -432,14 +503,44 @@ module private RouterImpl =
vals[i] <- model
ctor vals
+ let parseQueryParameters (case: UnionCaseInfo) (query: string) : QueryParameter list =
+ let fields = case.GetFields()
+ query.Split('&', StringSplitOptions.RemoveEmptyEntries)
+ |> Seq.map (fun q ->
+ let paramName, propName =
+ match q.IndexOf('=') with
+ | -1 ->
+ let name = queryParameterRE.Match(q).Groups[1].Value
+ name, name
+ | n ->
+ let name = q[..n-1]
+ name, queryParameterRE.Match(q[n+1..]).Groups[1].Value
+ let index =
+ fields
+ |> Array.tryFindIndex (fun f -> f.Name = propName)
+ |> Option.defaultWith(fun () -> fail (InvalidRouterKind.UnknownField(case, propName)))
+ let prop = fields[index]
+ let serializer, optionalDefaultValue = getSingleSerializer prop.PropertyType
+ {
+ index = index
+ serializer = serializer
+ optionalDefaultValue = optionalDefaultValue
+ name = paramName
+ propName = propName
+ })
+ |> List.ofSeq
+
let parseEndPointCase getSegment (defaultPageModel: obj -> unit) (case: UnionCaseInfo) =
let ctor = getCtor defaultPageModel case
let fields = case.GetFields()
+ let path, query = parseEndPointCasePathAndQuery case
+ let query = parseQueryParameters case query
let defaultFrags() =
fields
|> Array.mapi (fun i p ->
let ty = p.PropertyType
- if isPageModel ty then None else
+ if isPageModel ty then None
+ elif query |> List.exists (fun q -> q.propName = p.Name) then None else
Some <| Parameter {
index = [case, fields.Length, i]
``type`` = ty
@@ -449,20 +550,20 @@ module private RouterImpl =
})
|> Array.choose id
|> List.ofSeq
- match parseEndPointCasePath case with
+ match path with
// EndPoint "/"
- | [] -> { info = case; ctor = ctor; argCount = fields.Length; segments = defaultFrags() }
+ | [] -> { info = case; ctor = ctor; argCount = fields.Length; segments = defaultFrags(); query = query }
// EndPoint "/const"
| [root] when isConstantFragment root ->
- { info = case; ctor = ctor; argCount = fields.Length; segments = Constant root :: defaultFrags() }
+ { info = case; ctor = ctor; argCount = fields.Length; segments = Constant root :: defaultFrags(); query = query }
// EndPoint
| frags ->
let unboundFields =
fields
- |> Array.choose (fun f -> if isPageModel f.PropertyType then None else Some f.Name)
+ |> Array.choose (fun f -> if isPageModel f.PropertyType || query |> List.exists (fun q -> q.propName = f.Name) then None else Some f.Name)
|> HashSet
let fragCount = frags.Length
- let res =
+ let segments =
frags
|> List.mapi (fun fragIx frag ->
if isConstantFragment frag then
@@ -496,7 +597,7 @@ module private RouterImpl =
)
if unboundFields.Count > 0 then
fail (InvalidRouterKind.MissingField(case, Seq.head unboundFields))
- { info = case; ctor = ctor; argCount = fields.Length; segments = res }
+ { info = case; ctor = ctor; argCount = fields.Length; segments = segments; query = query }
let rec mergeEndPointCaseFragments (cases: seq) : UnionParser =
let constants = Dictionary()
@@ -512,7 +613,7 @@ module private RouterImpl =
constants[s] <- { case with segments = rest } :: existing
| Parameter param :: rest ->
match parameter with
- | Some (case', param', ps) ->
+ | Some (case', param': SegmentParameter, ps) ->
if param.``type`` <> param'.``type`` then
fail (InvalidRouterKind.ParameterTypeMismatch(case', param'.name, case.info, param.name))
if param.modifier <> param'.modifier then
@@ -540,23 +641,41 @@ module private RouterImpl =
let parser = mergeEndPointCaseFragments cases
fun l ->
let d = Dictionary()
- let rec run (parser: UnionParser) l =
+ let rec run (parser: UnionParser) segments query =
let finalize rest =
- parser.finalize |> Option.map (fun case ->
+ parser.finalize |> Option.bind (fun case ->
let args =
match d.TryGetValue(case.info) with
| true, args -> args
| false, _ -> Array.zeroCreate case.argCount
- (case.ctor args, rest))
+ let allQueryParamsAreHere =
+ case.query
+ |> List.forall (fun p ->
+ match Map.tryFind p.name query with
+ | None ->
+ match p.optionalDefaultValue with
+ | ValueSome def ->
+ args[p.index] <- def
+ true
+ | ValueNone -> false
+ | Some v ->
+ match p.serializer.parse v with
+ | Some x ->
+ args[p.index] <- x
+ true
+ | None -> false)
+ if allQueryParamsAreHere then
+ Some (case.ctor args, rest)
+ else None)
let mutable constant = Unchecked.defaultof<_>
- match l with
+ match segments with
| s :: rest when parser.constants.TryGetValue(s, &constant) ->
- run constant rest
- | l ->
+ run constant rest query
+ | segments ->
parser.parameter
|> Option.bind (function
| { modifier = Basic } as param, nextParser ->
- match param.segment.parse l with
+ match param.segment.parse segments query with
| None -> None
| Some (o, rest) ->
for case, fieldCount, i in param.index do
@@ -568,11 +687,11 @@ module private RouterImpl =
d[case] <- a
a
a[i] <- o
- run nextParser rest
+ run nextParser rest query
| { modifier = Rest(restBuild, _) } as param, nextParser ->
let restValues = ResizeArray()
- let rec parse l =
- match param.segment.parse l, l with
+ let rec parse segments =
+ match param.segment.parse segments query, segments with
| None, [] ->
for case, fieldCount, i in param.index do
let a =
@@ -583,25 +702,25 @@ module private RouterImpl =
d[case] <- a
a
a[i] <- restBuild restValues
- run nextParser []
+ run nextParser [] query
| None, _::_ -> None
| Some (o, rest), _ ->
restValues.Add(o)
parse rest
- parse l
+ parse segments
)
- |> Option.orElseWith (fun () -> finalize l)
+ |> Option.orElseWith (fun () -> finalize segments)
run parser l
let parseConsecutiveTypes getSegment (tys: Type[]) (ctor: obj[] -> obj) : SegmentParser =
let fields = Array.map getSegment tys
- fun (fragments: list) ->
+ fun (fragments: list) query ->
let args = Array.zeroCreate fields.Length
let rec go i fragments =
if i = fields.Length then
Some (ctor args, fragments)
else
- match fields[i].parse fragments with
+ match fields[i].parse fragments query with
| None -> None
| Some (x, rest) ->
args[i] <- x
@@ -611,8 +730,14 @@ module private RouterImpl =
let writeConsecutiveTypes getSegment (tys: Type[]) (dector: obj -> obj[]) : SegmentWriter =
let fields = tys |> Array.map (fun t -> (getSegment t).write)
fun (r: obj) ->
- Array.map2 (<|) fields (dector r)
- |> List.concat
+ let mutable segments = ListCollector()
+ let query =
+ (Map.empty, fields, dector r)
+ |||> Array.fold2 (fun query field item ->
+ let itemSegments, itemQuery = field item
+ segments.AddMany(itemSegments)
+ merge itemQuery query)
+ segments.Close(), query
let caseDector (case: UnionCaseInfo) : obj -> obj[] =
FSharpValue.PreComputeUnionReader(case, true)
@@ -621,17 +746,36 @@ module private RouterImpl =
let dector = caseDector case.info
fun o ->
let vals = dector o
- case.segments |> List.collect (function
- | Constant s -> [s]
- | Parameter({ modifier = Basic } as param) ->
- let _, _, i = param.index |> List.find (fun (case', _, _) -> case' = case.info)
- param.segment.write vals[i]
- | Parameter({ modifier = Rest(_, decons) } as param) ->
- let _, _, i = param.index |> List.find (fun (case', _, _) -> case' = case.info)
- [ for x in decons vals[i] do yield! param.segment.write x ]
- )
+ let mutable segments = ListCollector()
+ let query =
+ case.query
+ |> Seq.choose (fun param ->
+ param.serializer.write vals[param.index]
+ |> Option.map (fun s -> param.name, s))
+ |> Map
+ let query =
+ (query, case.segments)
+ ||> List.fold (fun query item ->
+ match item with
+ | Constant s ->
+ segments.Add(s)
+ query
+ | Parameter({ modifier = Basic } as param) ->
+ let _, _, i = param.index |> List.find (fun (case', _, _) -> case' = case.info)
+ let itemSegments, itemQuery = param.segment.write vals[i]
+ segments.AddMany(itemSegments)
+ merge itemQuery query
+ | Parameter({ modifier = Rest(_, decons) } as param) ->
+ let _, _, i = param.index |> List.find (fun (case', _, _) -> case' = case.info)
+ (query, decons vals[i])
+ ||> Seq.fold (fun query x ->
+ let itemSegments, itemQuery = param.segment.write x
+ segments.AddMany(itemSegments)
+ merge itemQuery query)
+ )
+ segments.Close(), query
- let unionSegment (getSegment: Type -> Segment) (defaultPageModel: obj -> unit) (ty: Type) : Segment =
+ let unionSegment (getSegment: Type -> SegmentSerializer) (defaultPageModel: obj -> unit) (ty: Type) : SegmentSerializer =
let cases =
FSharpType.GetUnionCases(ty, true)
|> Array.map (parseEndPointCase getSegment defaultPageModel)
@@ -660,7 +804,7 @@ module private RouterImpl =
write = writeConsecutiveTypes getSegment tys dector
}
- let rec getSegment (cache: Dictionary) (defaultPageModel: obj -> unit) (ty: Type) : Segment =
+ let rec getSegment (cache: Dictionary) (defaultPageModel: obj -> unit) (ty: Type) : SegmentSerializer =
match cache.TryGetValue(ty) with
| true, x -> unbox x
| false, _ ->
@@ -687,6 +831,20 @@ module private RouterImpl =
cache[ty] <- segment.Value
segment.Value
+ let splitPathAndQuery (pathAndQuery: string) : string list * Map =
+ match pathAndQuery.IndexOf('?') with
+ | -1 -> pathAndQuery.Split('/') |> List.ofArray, Map.empty
+ | n ->
+ let path = pathAndQuery[..n-1].Split('/') |> List.ofArray
+ let query =
+ pathAndQuery[n+1..].Split('&')
+ |> Seq.map (fun s ->
+ match s.IndexOf('=') with
+ | -1 -> s, ""
+ | n -> s[..n-1], s[n+1..])
+ |> Map
+ path, query
+
/// Functions for building Routers that bind page navigation with Elmish.
/// Routing
module Router =
@@ -707,18 +865,28 @@ module Router =
(makeMessage: 'ep -> 'msg) (getEndPoint: 'model -> 'ep) (defaultPageModel: 'ep -> unit) =
let ty = typeof<'ep>
let cache = Dictionary()
- for KeyValue(k, v) in baseTypes do cache.Add(k, v)
+ for KeyValue(k, v) in baseTypeSegmentSerializers do cache.Add(k, v)
let frag = getSegment cache (unbox >> defaultPageModel) ty
{
getEndPoint = getEndPoint
getRoute = fun ep ->
- box ep
- |> frag.write
- |> String.concat "/"
+ let segments, query = frag.write (box ep)
+ let path = String.concat "/" segments
+ if Map.isEmpty query then
+ path
+ else
+ let sb = StringBuilder(path)
+ query
+ |> Seq.iteri (fun i (KeyValue(k, v)) ->
+ sb.Append(if i = 0 then '?' else '&')
+ .Append(k)
+ .Append('=')
+ .Append(v)
+ |> ignore)
+ sb.ToString()
setRoute = fun path ->
- path.Split('/')
- |> List.ofArray
- |> frag.parse
+ splitPathAndQuery path
+ ||> frag.parse
|> Option.bind (function
| x, [] -> Some (unbox<'ep> x |> makeMessage)
| _ -> None)
diff --git a/tests/Client/Main.fs b/tests/Client/Main.fs
index 12acbe9e..348dde5d 100644
--- a/tests/Client/Main.fs
+++ b/tests/Client/Main.fs
@@ -33,7 +33,7 @@ type Page =
| [] Form
| [] Collection
| [] Item of key: int * model: PageModel
- | [] Lazy
+ | [] Lazy of value: int * value2: string option
| [] Virtual
type Item =
@@ -102,7 +102,7 @@ let initModel _ =
}
let defaultPageModel = function
- | Form | Collection | Lazy | Virtual -> ()
+ | Form | Collection | Lazy _ | Virtual -> ()
| Item (_, m) -> Router.definePageModel m 10
let router = Router.inferWithModel SetPage (fun m -> m.page) defaultPageModel
@@ -271,9 +271,10 @@ type ViewItemPage() =
}
}
-let viewLazy model dispatch =
+let viewLazy i model dispatch =
let lazyViewFunction = (fun m -> text $"Lazy values: ({m.value},{m.nonEqVal}), re-render random number check: {System.Random().Next()}")
div {
+ p { string i }
pre {
text """
let viewLazy model dispatch =
@@ -334,7 +335,9 @@ let view js model dispatch =
text " "
navLink NavLinkMatch.Prefix { router.HRef Collection; "Collection" }
text " "
- navLink NavLinkMatch.Prefix { router.HRef Lazy; "Lazy" }
+ navLink NavLinkMatch.Prefix { attr.href (router.Link (Lazy (123, Some "abc"))); "Lazy" }
+ text " "
+ navLink NavLinkMatch.Prefix { attr.href (router.Link (Lazy (123, None))); "Lazy" }
text " "
navLink NavLinkMatch.All { router.HRef Virtual; "Virtual" }
}
@@ -342,7 +345,7 @@ let view js model dispatch =
| Form -> viewForm js model dispatch
| Collection -> viewCollection model dispatch
| Item (k, m) -> ecomp (k, model.items.[k], m.Model) dispatch { attr.empty() }
- | Lazy -> viewLazy model dispatch
+ | Lazy (x, y) -> viewLazy (x, y) model dispatch
| Virtual -> viewVirtual model dispatch
}
diff --git a/tests/Unit.Client/Routing.fs b/tests/Unit.Client/Routing.fs
index b53ff434..60c86f3b 100644
--- a/tests/Unit.Client/Routing.fs
+++ b/tests/Unit.Client/Routing.fs
@@ -47,6 +47,7 @@ type Page =
| [] WithRestArray of rest: (int * string)[]
| [] WithModel of PageModel
| [] WithModelAndArgs of arg: int * PageModel
+ | [] WithQuery of arg: int * n: int * implicit: int * optional: int option * voptional: int voption
and InnerPage =
| [] InnerHome
@@ -113,6 +114,7 @@ let rec pageClass = function
| WithRestArray a -> $"""withrestarray-{a |> Seq.map (fun (i, s) -> $"{i}-{s}") |> String.concat "-"}"""
| WithModel _ -> "withmodel"
| WithModelAndArgs (a, _) -> $"withmodelargs-{a}"
+ | WithQuery(a, b, c, d, e) -> $"withquery-{a}-{b}-{c}-{d}-{e}"
let innerlinks =
[
@@ -164,6 +166,8 @@ let baseLinks =
"/with-rest-array/1/foo/2/bar", WithRestArray [|(1, "foo"); (2, "bar")|]
"/with-model", WithModel { Model = Unchecked.defaultof<_> }
"/with-model-args/42", WithModelAndArgs(42, { Model = Unchecked.defaultof<_> })
+ "/with-query/42?implicit=2&named=1&optional=3", WithQuery(42, 1, 2, Some 3, ValueNone)
+ "/with-query/42?implicit=5&named=4&voptional=3", WithQuery(42, 4, 5, None, ValueSome 3)
]
for link, page in innerlinks do
yield "/with-union" + link, WithUnion page