Skip to content

Commit

Permalink
#246 ws-anchor attribute implementation and tests (#248)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jand42 committed Oct 19, 2022
1 parent 4108b95 commit 109c749
Show file tree
Hide file tree
Showing 14 changed files with 162 additions and 62 deletions.
4 changes: 2 additions & 2 deletions WebSharper.UI.CSharp.Templating.ServerSide.Tests/Site.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ public class Home
public static void AfterRenderAction() => JavaScript.Console.Log("After render action initialized");

[JavaScript]
public static void AfterRenderOverloadTempl(UI.Templating.Runtime.Server.TemplateEvent<Vars, JavaScript.Dom.Event> m) =>
public static void AfterRenderOverloadTempl(UI.Templating.Runtime.Server.TemplateEvent<Vars, Anchors, JavaScript.Dom.Event> m) =>
m.Vars.Logger.Set("I'm initialized from after render");

[JavaScript]
public static void ClickMeTempl(UI.Templating.Runtime.Server.TemplateEvent<Vars, JavaScript.Dom.MouseEvent> m) => m.Vars.Logger.Set(m.Vars.Logger.Value + "\nI'm initialized from click");
public static void ClickMeTempl(UI.Templating.Runtime.Server.TemplateEvent<Vars, Anchors, JavaScript.Dom.MouseEvent> m) => m.Vars.Logger.Set(m.Vars.Logger.Value + "\nI'm initialized from click");

[JavaScript]
public static void ClickMe(JavaScript.Dom.Element el, JavaScript.Dom.MouseEvent ev) => JavaScript.Console.Log("Click sent", el);
Expand Down
22 changes: 18 additions & 4 deletions WebSharper.UI.CSharp.Templating/CodeGenerator.fs
Original file line number Diff line number Diff line change
Expand Up @@ -117,12 +117,12 @@ let buildHoleMethods (typeName: string) (holeName: HoleName) (holeDef: HoleDefin
|]
| HoleKind.ElemHandler ->
let eventType = "WebSharper.JavaScript.Dom.Event"
let argType = "TemplateEvent<Vars, "+eventType+">"
let argType = "TemplateEvent<Vars, Anchors, "+eventType+">"
[|
s2 "Action<DomElement>" "AfterRenderClient" "FSharpConvert.Fun<DomElement>(x)"
s2 "Action" "AfterRenderClient" "FSharpConvert.Fun<DomElement>((a) => x())"
s2 ("Action<"+argType+">") "AfterRenderClient"
("FSharpConvert.Fun<DomElement>((a) => x(new "+argType+"(instance.Vars, a, null)))")
("FSharpConvert.Fun<DomElement>((a) => x(new "+argType+"(instance.Vars, instance.Anchors, a, null)))")
yield!
serverSE ("Expression<Action<DomElement>>") "AfterRenderE" "y"
yield!
Expand All @@ -132,12 +132,12 @@ let buildHoleMethods (typeName: string) (holeName: HoleName) (holeDef: HoleDefin
|]
| HoleKind.Event eventType ->
let eventType = "WebSharper.JavaScript.Dom." + eventType
let argType = "TemplateEvent<Vars, "+eventType+">"
let argType = "TemplateEvent<Vars, Anchors, "+eventType+">"
[|
s ("Action<DomElement, "+eventType+">") "ActionEvent" "x"
s2 "Action" "EventClient" ("FSharpConvert.Fun<DomElement, DomEvent>((a,b) => x())")
s2 ("Action<"+argType+">") "EventClient"
("FSharpConvert.Fun<DomElement, DomEvent>((a,b) => x(new "+argType+"(instance.Vars, a, ("+eventType+")b)))")
("FSharpConvert.Fun<DomElement, DomEvent>((a,b) => x(new "+argType+"(instance.Vars, instance.Anchors, a, ("+eventType+")b)))")
// serverSide
yield!
serverSE ("Expression<Action<DomElement, "+eventType+">>") "EventExpr" "y"
Expand Down Expand Up @@ -251,20 +251,34 @@ let varsClass (ctx: Ctx) =
yield "}"
]

let anchorsClass (ctx: Ctx) =
[
yield "public struct Anchors"
yield "{"
yield! indent [
for anchorName in ctx.Template.Anchors do
let anchorName' = anchorName.ToLowerInvariant()
yield sprintf """[Inline] public DomElement %s => (DomElement)TemplateHole.Value((As<Instance>(this)).Anchor("%s"));""" anchorName anchorName'
]
yield "}"
]

let instanceClass (ctx: Ctx) =
[
yield "public class Instance : TemplateInstance"
yield "{"
yield! indent [
yield """public Instance(CompletedHoles v, Doc d) : base(v, d) { }"""
yield """public Vars Vars => As<Vars>(this);"""
yield """public Anchors Anchors => As<Anchors>(this);"""
]
yield "}"
]

let buildFinalMethods (ctx: Ctx) =
[|
yield! varsClass ctx
yield! anchorsClass ctx
yield! instanceClass ctx
yield "public Instance Create() {"
yield! indent (finalMethodBody ctx)
Expand Down
2 changes: 2 additions & 0 deletions WebSharper.UI.Templating.Common/AST.fs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ module SpecialHole =
type Template =
{
Holes : Dictionary<HoleName, HoleDefinition>
Anchors : HashSet<string>
Value : Node[]
Src : string
SpecialHoles : SpecialHole
Expand All @@ -113,6 +114,7 @@ let [<Literal>] AttrAttr = "ws-attr"
let [<Literal>] AfterRenderAttr = "ws-onafterrender"
let [<Literal>] EventAttrPrefix = "ws-on"
let [<Literal>] VarAttr = "ws-var"
let [<Literal>] AnchorAttr = "ws-anchor"
let TextHoleRegex = Regex(@"\$\{([a-zA-Z_][-a-zA-Z0-9_]*)\}", RegexOptions.Compiled)
let HoleNameRegex = Regex(@"^[a-zA-Z_][-a-zA-Z0-9_]*$", RegexOptions.Compiled)

21 changes: 16 additions & 5 deletions WebSharper.UI.Templating.Common/Parsing.fs
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ module Impl =
yield StringPart.Text t[l.Value ..]
|]

let parseAttributesOf (node: HtmlNode) (addHole: HoleName -> HoleDefinition -> unit) =
let parseAttributesOf (node: HtmlNode) (addHole: HoleName -> HoleDefinition -> unit) (addAnchor: string -> unit) =
[|
for attr in node.Attributes do
let holeDef kind : HoleDefinition =
Expand All @@ -194,6 +194,9 @@ module Impl =
| AfterRenderAttr ->
addHole attr.Value (holeDef HoleKind.ElemHandler)
yield Attr.OnAfterRender attr.Value
| AnchorAttr ->
addAnchor attr.Value
yield Attr.Simple(AnchorAttr, attr.Value)
| VarAttr | HoleAttr | ReplaceAttr | TemplateAttr | ChildrenTemplateAttr ->
() // These are handled separately in parseNode* and detach*Node
| s when s.StartsWith EventAttrPrefix ->
Expand Down Expand Up @@ -280,16 +283,21 @@ module Impl =
let addRef (x, y) = refs.Value <- Set.add (defaultArg (lower x) thisFileId, lower y) refs.Value
refs, addRef

type ParseState(fileId: string, ?holes: Dictionary<HoleName, HoleDefinition>, ?specialHoles: SpecialHole) =
type ParseState(fileId: string, ?holes: Dictionary<HoleName, HoleDefinition>, ?specialHoles: SpecialHole, ?anchors: HashSet<string>) =
let refs, addRef = mkRefs fileId
let mutable specialHoles = defaultArg specialHoles SpecialHole.None
let holes = match holes with Some h -> h | None -> Dictionary(System.StringComparer.InvariantCultureIgnoreCase)
let anchors = match anchors with Some h -> h | None -> HashSet(System.StringComparer.InvariantCultureIgnoreCase)

member this.Holes = holes
member this.Anchors = anchors
member this.References = refs.Value
member this.SpecialHoles = specialHoles

member this.AddHole name def = addHole holes name def
member this.AddAnchor name =
if anchors.Add(name) |> not then
failwithf "Duplicate ws-anchor found: %s" name
member this.AddRef ref = addRef ref
member this.AddSpecialHole(h) = specialHoles <- specialHoles ||| h

Expand All @@ -298,7 +306,7 @@ module Impl =
| Preserve isSvg n -> n
| Instantiation state n -> n
| n ->
let attrs = parseAttributesOf n state.AddHole
let attrs = parseAttributesOf n state.AddHole state.AddAnchor
match n.Attributes[VarAttr] with
| null ->
Node.Element (n.Name, isSvg, attrs, children.Value)
Expand Down Expand Up @@ -358,7 +366,7 @@ module Impl =
for c in node.ChildNodes do
if not (c :? HtmlTextNode || c :? HtmlCommentNode) then
if c.HasAttributes then
attrs[c.Name] <- parseAttributesOf c state.AddHole
attrs[c.Name] <- parseAttributesOf c state.AddHole state.AddAnchor
else
contentHoles[c.Name] <- parseNodeAndSiblings false state c.FirstChild
None
Expand Down Expand Up @@ -413,7 +421,7 @@ module Impl =
l parentNode.FirstChild
let line, col = parentNode.Line, parentNode.LinePosition
let value = parseNodeAndSiblings false state parentNode.FirstChild
{ Holes = state.Holes; Value = value; Src = src; References = state.References
{ Holes = state.Holes; Anchors = state.Anchors; Value = value; Src = src; References = state.References
SpecialHoles = state.SpecialHoles
Line = line; Column = col }

Expand Down Expand Up @@ -482,6 +490,7 @@ module Impl =
{
Value = [| el |]
Holes = instState.Holes
Anchors = instState.Anchors
Src = n.WriteTo()
SpecialHoles = instState.SpecialHoles
Line = n.Line
Expand All @@ -503,6 +512,7 @@ module Impl =
References = Set.union templateForChildren.References state.References
SpecialHoles = state.SpecialHoles
Holes = templateForChildren.Holes
Anchors = templateForChildren.Anchors
}
| hole ->
let holeName = hole.Value
Expand All @@ -517,6 +527,7 @@ module Impl =
{
Value = [| el |]
Holes = state.Holes
Anchors = state.Anchors
References = state.References
Src = n.WriteTo()
SpecialHoles = state.SpecialHoles
Expand Down
34 changes: 22 additions & 12 deletions WebSharper.UI.Templating.Runtime/Runtime.fs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ type ValTy =
| File = 4

[<JavaScript; Serializable>]
type TemplateInitializer(id: string, vars: array<string * ValTy * obj option>) =
type TemplateInitializer(id: string, vars: (string * ValTy * obj option)[]) =

[<NonSerialized; OptionalField>]
let mutable instance = None
Expand Down Expand Up @@ -182,10 +182,16 @@ and TemplateInstance(c: CompletedHoles, doc: Doc) =

member this.Hole(name: string): TemplateHole = failwith "Cannot access template vars from the server side"

type TemplateEvent<'TI, 'E when 'E :> DomEvent> =
member this.Anchor(name: string): DomElement = failwith "Cannot access template anchors from the server side"

member internal this.SetAnchorRoot(el : DomElement): unit = failwith "Cannot access template SetAnchorRoot from the server side"

type TemplateEvent<'TV, 'TA, 'E when 'E :> DomEvent> =
{
/// The reactive variables of this template instance.
Vars : 'TI
Vars : 'TV
/// The anchor elements defined in this template.
Anchors : 'TA
/// The DOM element targeted by this event.
Target : DomElement
/// The DOM event data.
Expand All @@ -203,12 +209,14 @@ type Handler private () =
static member EventQ (holeName: string, [<JavaScript>] f: Expr<DomElement -> DomEvent -> unit>) =
TemplateHole.EventQ(holeName, f)

static member EventQ2<'E when 'E :> DomEvent> (key: string, holeName: string, ti: (unit -> TemplateInstance), [<JavaScript>] f: Expr<TemplateEvent<obj, 'E> -> unit>) =
static member EventQ2<'E when 'E :> DomEvent> (key: string, holeName: string, ti: (unit -> TemplateInstance), [<JavaScript>] f: Expr<TemplateEvent<obj, obj, 'E> -> unit>) =
Handler.EventQ(holeName, <@ fun el ev ->
let k = key
(WebSharper.JavaScript.Pervasives.As<TemplateEvent<obj, 'E> -> unit> f)
let i = TemplateInstances.GetInstance key
i.SetAnchorRoot(el)
(WebSharper.JavaScript.Pervasives.As<TemplateEvent<obj, obj, 'E> -> unit> f)
{
Vars = box (TemplateInstances.GetInstance k)
Vars = i
Anchors = i
Target = el
Event = ev :?> 'E
}
Expand All @@ -217,18 +225,20 @@ type Handler private () =
static member AfterRenderQ (holeName: string, [<JavaScript>] f: Expr<DomElement -> unit>) =
TemplateHole.AfterRenderQ(holeName, f)

static member AfterRenderQ2(key: string, holeName: string, ti: (unit -> TemplateInstance), [<JavaScript>] f: Expr<TemplateEvent<obj, DomEvent> -> unit>) =
static member AfterRenderQ2(key: string, holeName: string, ti: (unit -> TemplateInstance), [<JavaScript>] f: Expr<TemplateEvent<obj, obj, DomEvent> -> unit>) =
Handler.AfterRenderQ(holeName, <@ fun el ->
let k = key
(WebSharper.JavaScript.Pervasives.As<TemplateEvent<obj, DomEvent> -> unit> f)
let i = TemplateInstances.GetInstance key
i.SetAnchorRoot(el)
(WebSharper.JavaScript.Pervasives.As<TemplateEvent<obj, obj, DomEvent> -> unit> f)
{
Vars = box (TemplateInstances.GetInstance k)
Vars = i
Anchors = i
Target = el
Event = null
}
@>)

static member CompleteHoles(key: string, filledHoles: seq<TemplateHole>, vars: array<string * ValTy * obj option>) : seq<TemplateHole> * CompletedHoles =
static member CompleteHoles(key: string, filledHoles: seq<TemplateHole>, vars: (string * ValTy * obj option)[]) : seq<TemplateHole> * CompletedHoles =
let filledVars = HashSet()
let hasEventHandler =
(false, filledHoles)
Expand Down
54 changes: 44 additions & 10 deletions WebSharper.UI.Templating.Runtime/RuntimeClient.fs
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,41 @@ module M = WebSharper.Core.Metadata
module I = WebSharper.Core.AST.IgnoreSourcePos
type TemplateInstance = Server.TemplateInstance
type DomEvent = WebSharper.JavaScript.Dom.Event
type TemplateEvent<'T, 'E when 'E :> DomEvent> = Server.TemplateEvent<'T, 'E>
type DomElement = WebSharper.JavaScript.Dom.Element
type TemplateEvent<'TV, 'TA, 'E when 'E :> DomEvent> = Server.TemplateEvent<'TV, 'TA, 'E>

[<JavaScript>]
let private (|Box|) x = box x

[<JavaScript; Proxy(typeof<TemplateInstance>)>]
type private TemplateInstanceProxy(c: Server.CompletedHoles, doc: Doc) =
let allVars = match c with Server.CompletedHoles.Client v -> v | _ -> failwith "Should not happen"

let mutable anchorRoot = null : DomElement

member this.Doc = doc

member this.Hole(name) = allVars[name]

member this.SetAnchorRoot(el: DomElement) =
anchorRoot <- el

member this.Anchor(name: string): DomElement =
let rec findUnder (el: DomElement) =
let e = el.QuerySelector($"[ws-anchor=\"{name}\"]")
if isNull e then
if el.TagName = "body" then
null
else
findUnder el.ParentElement
else
e
let anchorRoot =
if isNull anchorRoot then
JS.Document.Body
else
anchorRoot
findUnder anchorRoot

[<Inline>]
let AfterRenderQ(name: string, [<JavaScript>] f: Expr<Dom.Element -> unit>) =
TemplateHole.AfterRenderQ(name, f)
Expand Down Expand Up @@ -221,10 +243,13 @@ type private HandlerProxy =
static member EventQ (holeName: string, f: Expr<Dom.Element -> Dom.Event -> unit>) =
TemplateHole.EventQ(holeName, f)

static member EventQ2<'E when 'E :> DomEvent> (key: string, holeName: string, ti: (unit -> TemplateInstance), [<JavaScript>] f: Expr<TemplateEvent<obj, 'E> -> unit>) =
static member EventQ2<'E when 'E :> DomEvent> (key: string, holeName: string, ti: (unit -> TemplateInstance), [<JavaScript>] f: Expr<TemplateEvent<obj, obj, 'E> -> unit>) =
TemplateHole.EventQ(holeName, <@ fun el ev ->
let i = ti()
i.SetAnchorRoot(el)
(%f) {
Vars = box (ti())
Vars = i
Anchors = i
Target = el
Event = downcast ev
}
Expand All @@ -234,10 +259,13 @@ type private HandlerProxy =
static member AfterRenderQ (holeName: string, f: Expr<Dom.Element -> unit>) =
TemplateHole.AfterRenderQ(holeName, f)

static member AfterRenderQ2(key: string, holeName: string, ti: (unit -> TemplateInstance), [<JavaScript>] f: Expr<TemplateEvent<obj, Dom.Event> -> unit>) =
static member AfterRenderQ2(key: string, holeName: string, ti: (unit -> TemplateInstance), [<JavaScript>] f: Expr<TemplateEvent<obj, obj, Dom.Event> -> unit>) =
TemplateHole.AfterRenderQ(holeName, <@ fun el ->
let i = ti()
i.SetAnchorRoot(el)
(%f) {
Vars = box (ti())
Vars = i
Anchors = i
Target = el
Event = null
}
Expand Down Expand Up @@ -277,18 +305,24 @@ type ClientTemplateInstanceHandlers =

[<JavaScriptExport>]
static member EventQ2Client (key: string, el: Dom.Element, ev: Dom.Event, f: obj -> unit) =
let i = Server.TemplateInstances.GetInstance key
i.SetAnchorRoot(el)
f
({
Vars = box (Server.TemplateInstances.GetInstance key)
Vars = i
Anchors = i
Target = el
Event = ev
} : TemplateEvent<_, _>)
} : TemplateEvent<_, _, _>)

[<JavaScriptExport>]
static member AfterRenderQ2Client (key: string, el: Dom.Element, f: obj -> unit) =
let i = Server.TemplateInstances.GetInstance key
i.SetAnchorRoot(el)
f
({
Vars = box (Server.TemplateInstances.GetInstance key)
Vars = i
Anchors = i
Target = el
Event = null
} : TemplateEvent<_, _>)
} : TemplateEvent<_, _, _>)

0 comments on commit 109c749

Please sign in to comment.