Skip to content

Commit

Permalink
ws-dom Attribute (#251)
Browse files Browse the repository at this point in the history
* WIP

* WIP comment, Expose in C#

* Fix casing

* Cleanup + comments

* More cleanup
  • Loading branch information
Jooseppi12 committed Dec 14, 2022
1 parent 4f3aa75 commit 32499fe
Show file tree
Hide file tree
Showing 14 changed files with 246 additions and 76 deletions.
12 changes: 12 additions & 0 deletions WebSharper.UI.CSharp.Templating/CodeGenerator.fs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ let buildHoleMethods (typeName: string) (holeName: HoleName) (holeDef: HoleDefin
[|
sv "Var<JavaScript.File array>" "VarFile" "x"
|]
| HoleKind.Var ValTy.DomElement ->
[|
sv "Var<FSharpOption<JavaScript.Dom.Element>>" "VarDomElement" "x"
|]
| HoleKind.Mapped (kind = k) -> build k
| HoleKind.Unknown -> failwithf "Error: Unknown HoleKind: %s" holeName
build holeDef.Kind
Expand Down Expand Up @@ -208,6 +212,7 @@ let finalMethodBody (ctx: Ctx) =
| HoleKind.Var AST.ValTy.Bool -> yield sprintf """Tuple.Create("%s", ValTy.Bool, FSharpOption<object>.None)""" holeName'
| HoleKind.Var AST.ValTy.DateTime -> yield sprintf """Tuple.Create("%s", ValTy.DateTime, FSharpOption<object>.None)""" holeName'
| HoleKind.Var AST.ValTy.File -> yield sprintf """Tuple.Create("%s", ValTy.File, FSharpOption<object>.None)""" holeName'
| HoleKind.Var AST.ValTy.DomElement -> yield sprintf """Tuple.Create("%s", ValTy.DomElement, FSharpOption<object>.None)""" holeName'
| _ -> ()
]
|> String.concat ", "
Expand Down Expand Up @@ -259,6 +264,12 @@ let anchorsClass (ctx: Ctx) =
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'
for KeyValue(holeName, holeDef) in ctx.Template.Holes do
let holeName' = holeName.ToLowerInvariant()
match holeDef.Kind with
| HoleKind.Var AST.ValTy.DomElement ->
yield sprintf """[Inline] public Var<FSharpOption<JavaScript.Dom.Element>> %s => (Var<FSharpOption<JavaScript.Dom.Element>>)TemplateHole.Value((As<Instance>(this)).Hole("%s"));""" holeName holeName'
| _ -> ()
]
yield "}"
]
Expand Down Expand Up @@ -299,6 +310,7 @@ let build typeName (ctx: Ctx) =
| HoleKind.Var AST.ValTy.Bool -> yield sprintf """Tuple.Create("%s", ValTy.Bool, FSharpOption<object>.None)""" holeName'
| HoleKind.Var AST.ValTy.File -> yield sprintf """Tuple.Create("%s", ValTy.File, FSharpOption<object>.None)""" holeName'
| HoleKind.Var AST.ValTy.DateTime -> yield sprintf """Tuple.Create("%s", ValTy.DateTime, FSharpOption<object>.None)""" holeName'
| HoleKind.Var AST.ValTy.DomElement -> yield sprintf """Tuple.Create("%s", ValTy.DomElement, FSharpOption<object>.None)""" holeName'
| _ -> ()
]
|> String.concat ", "
Expand Down
1 change: 1 addition & 0 deletions WebSharper.UI.CSharp.Tests/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ <h1 ws-template="tasksTitle">Tasks</h1>
<slot></slot>
</template>
</div>
<div ws-dom="TestDom"></div>
<div id="tasksTitle"></div>
<div id="tasks"></div>
<script type="text/javascript" src="Content/WebSharper.UI.CSharp.Tests.js"></script>
Expand Down
6 changes: 4 additions & 2 deletions WebSharper.UI.Templating.Common/AST.fs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type ValTy =
| Bool
| DateTime
| File
| DomElement

[<RequireQualifiedAccess>]
type HoleKind =
Expand Down Expand Up @@ -69,8 +70,8 @@ type Attr =
[<RequireQualifiedAccess>]
type Node =
| Text of StringPart[]
| Element of nodeName: string * isSvg: bool * attrs: Attr[] * children: Node[]
| Input of nodeName: string * var: HoleName * attrs: Attr[] * children: Node[]
| Element of nodeName: string * isSvg: bool * attrs: Attr[] * dom: HoleName option * children: Node[]
| Input of nodeName: string * var: HoleName * attrs: Attr[] * dom: HoleName option * children: Node[]
| DocHole of HoleName
| Instantiate of fileName: option<string> * templateName: option<string> * holeMaps: Dictionary<string, string> * attrHoles: Dictionary<string, Attr[]> * contentHoles: Dictionary<string, Node[]> * textHole: option<string>

Expand Down Expand Up @@ -115,6 +116,7 @@ let [<Literal>] AttrAttr = "ws-attr"
let [<Literal>] AfterRenderAttr = "ws-onafterrender"
let [<Literal>] EventAttrPrefix = "ws-on"
let [<Literal>] VarAttr = "ws-var"
let [<Literal>] DomAttr = "ws-dom"
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)
Expand Down
43 changes: 32 additions & 11 deletions WebSharper.UI.Templating.Common/Parsing.fs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ module Impl =
| AnchorAttr ->
addAnchor attr.Value
yield Attr.Simple(AnchorAttr, attr.Value)
| VarAttr | HoleAttr | ReplaceAttr | TemplateAttr | ChildrenTemplateAttr ->
| VarAttr | HoleAttr | ReplaceAttr | TemplateAttr | ChildrenTemplateAttr | DomAttr ->
() // These are handled separately in parseNode* and detach*Node
| s when s.StartsWith EventAttrPrefix ->
let eventName = s[EventAttrPrefix.Length..]
Expand Down Expand Up @@ -312,14 +312,27 @@ module Impl =
let attrs = parseAttributesOf n state.AddHole state.AddAnchor
match n.Attributes[VarAttr] with
| null ->
Node.Element (n.Name, isSvg, attrs, children.Value)
match n.Attributes[DomAttr] with
| null ->
Node.Element (n.Name, isSvg, attrs, None, children.Value)
| domattr ->
state.AddHole domattr.Value
{
HoleDefinition.Kind = HoleKind.Var (ValTy.DomElement)
HoleDefinition.Line = domattr.Line
HoleDefinition.Column = domattr.LinePosition + domattr.Name.Length
}
Node.Element (n.Name, isSvg, attrs, Some domattr.Value, children.Value)
| varAttr ->
let domAttr = n.Attributes[DomAttr]
let isDomAttr = domAttr <> null
let domAttrO = if isDomAttr then Some domAttr.Value else None
state.AddHole varAttr.Value {
HoleDefinition.Kind = HoleKind.Var (varTypeOf n)
HoleDefinition.Line = varAttr.Line
HoleDefinition.Column = varAttr.LinePosition + varAttr.Name.Length
}
Node.Input (n.Name, varAttr.Value, attrs, children.Value)
Node.Input (n.Name, varAttr.Value, attrs, domAttrO, children.Value)

and (|Preserve|_|) isSvg (n: HtmlNode) =
match n.GetAttributeValue("ws-preserve", null) with
Expand All @@ -336,7 +349,7 @@ module Impl =
let isSvg = isSvg || n.Name = "svg"
let attrs = [| for a in n.Attributes -> Attr.Simple(a.Name, a.Value) |]
let children = [| for c in n.ChildNodes -> preservedElement c isSvg |]
Node.Element(n.Name, isSvg, attrs, children)
Node.Element(n.Name, isSvg, attrs, None, children)

and (|Instantiation|_|) (state: ParseState) (node: HtmlNode) =
if node.Name.StartsWith "ws-" then
Expand Down Expand Up @@ -371,12 +384,12 @@ module Impl =
if c.HasAttributes then
attrs[c.Name] <- parseAttributesOf c state.AddHole state.AddAnchor
else
contentHoles[c.Name] <- parseNodeAndSiblings false state c.FirstChild
contentHoles[c.Name] <- parseNodeAndSiblings false false state c.FirstChild
None
Some (Node.Instantiate(fileName, templateName, holeMaps, attrs, contentHoles, textHole))
else None

and parseNodeAndSiblings isSvg (state: ParseState) (node: HtmlNode) =
and parseNodeAndSiblings isSvg isInsideDomAttr (state: ParseState) (node: HtmlNode) =
(isSvg, node)
|> Seq.unfold (fun (isSvg, node) ->
let addHole' name k =
Expand All @@ -394,6 +407,11 @@ module Impl =
Some ([||], (isSvg, node.NextSibling))
| node ->
let thisIsSvg = isSvg || node.Name = "svg"
if isInsideDomAttr && node.Attributes |> Seq.exists (fun a -> a.Name.StartsWith "ws-") then
eprintfn "WebSharper.UI warning WS9002: A ws-dom attribute can affect the functionality of this template artifact"
let domAttr = node.Attributes[DomAttr]
let isDomAttr = domAttr <> null
let domAttrO = if isDomAttr then Some domAttr.Value else None
match node.Attributes[ReplaceAttr] with
| null ->
let children =
Expand All @@ -409,25 +427,25 @@ module Impl =
else
let holeName = if docHole = "" then "Default" else docHole
if state.DefaultSlotUsed && holeName = "" then
parseNodeAndSiblings thisIsSvg state node.FirstChild
parseNodeAndSiblings thisIsSvg isDomAttr state node.FirstChild
else
state.AddSpecialHole(SpecialHole.FromName holeName)
if holeName = "Default" then
state.DefaultSlotUsed <- true
addHole' holeName HoleKind.Doc
[| Node.DocHole holeName |]
else
parseNodeAndSiblings thisIsSvg state node.FirstChild
parseNodeAndSiblings thisIsSvg isDomAttr state node.FirstChild
| holeAttr ->
state.AddSpecialHole(SpecialHole.FromName holeAttr.Value)
addHole' holeAttr.Value HoleKind.Doc
[| Node.DocHole holeAttr.Value |]
[| Node.DocHole (holeAttr.Value) |]
let doc = normalElement node thisIsSvg children state
Some ([| doc |], (isSvg, node.NextSibling))
| replaceAttr ->
state.AddSpecialHole(SpecialHole.FromName replaceAttr.Value)
addHole' replaceAttr.Value HoleKind.Doc
Some ([| Node.DocHole replaceAttr.Value |], (isSvg, node.NextSibling))
Some ([| Node.DocHole (replaceAttr.Value) |], (isSvg, node.NextSibling))
)
|> Array.concat

Expand All @@ -440,7 +458,7 @@ module Impl =
| (n : HtmlNode) -> n.WriteTo s; l n.NextSibling
l parentNode.FirstChild
let line, col = parentNode.Line, parentNode.LinePosition
let value = parseNodeAndSiblings false state parentNode.FirstChild
let value = parseNodeAndSiblings false false state parentNode.FirstChild
{ Holes = state.Holes; Anchors = state.Anchors; Value = value; Src = src; References = state.References
SpecialHoles = state.SpecialHoles
Line = line; Column = col; IsHtml5Template = parentNode.Name.ToLower() = "template"; }
Expand Down Expand Up @@ -547,6 +565,9 @@ module Impl =
IsHtml5Template = false
}
| hole ->
let domAttr = n.Attributes[DomAttr]
let isDomAttr = domAttr <> null
let domAttrO = if isDomAttr then Some domAttr.Value else None
let holeName = hole.Value
let state = ParseState(fileId)
state.AddHole holeName {
Expand Down
31 changes: 22 additions & 9 deletions WebSharper.UI.Templating.Runtime/Runtime.fs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ type ValTy =
| Bool = 2
| DateTime = 3
| File = 4
| DomElement = 5

[<JavaScript; Serializable>]
type TemplateInitializer(id: string, vars: (string * ValTy * obj option)[]) =
Expand All @@ -67,7 +68,7 @@ type TemplateInitializer(id: string, vars: (string * ValTy * obj option)[]) =
init el
View.Sink (set el) view

static let applyVarHole el tpl =
static let applyVarHole (el: JavaScript.Dom.Element) tpl =
match tpl with
| TemplateHole.VarStr (_, v) ->
applyTypedVarHole BindVar.StringApply v el
Expand All @@ -85,6 +86,8 @@ type TemplateInitializer(id: string, vars: (string * ValTy * obj option)[]) =
applyTypedVarHole BindVar.FloatApplyChecked v el
| TemplateHole.VarFloatUnchecked (_, v) ->
applyTypedVarHole BindVar.FloatApplyUnchecked v el
| TemplateHole.VarDomElement (_, v) ->
()
| TemplateHole.Elt (n, _)
| TemplateHole.Text (n, _)
| TemplateHole.TextView (n, _)
Expand Down Expand Up @@ -129,6 +132,7 @@ type TemplateInitializer(id: string, vars: (string * ValTy * obj option)[]) =
| ValTy.DateTime -> TemplateHole.VarDateTime (n, Var.Create (ov |> Option.map (fun x -> x :?> DateTime) |> Option.defaultValue DateTime.MinValue))
| ValTy.Number -> TemplateHole.VarFloatUnchecked (n, Var.Create (ov |> Option.map (fun x -> x :?> float) |> Option.defaultValue 0.))
| ValTy.String -> TemplateHole.VarStr (n, Var.Create (ov |> Option.map (fun x -> x :?> string) |> Option.defaultValue ""))
| ValTy.DomElement -> TemplateHole.VarDomElement (n, Var.Create (ov |> Option.map (fun x -> x :?> JavaScript.Dom.Element) |> Option.defaultValue (JavaScript.JS.Document.QuerySelector("[ws-dom=" + n + "]")) |> Some))
| _ -> failwith "Invalid value type"
let i = TemplateInstance(CompletedHoles.Client(d), Doc.Empty)
instance <- Some i
Expand Down Expand Up @@ -251,6 +255,7 @@ type Handler private () =
| TemplateHole.VarFloat(n, _)
| TemplateHole.VarBool(n, _)
| TemplateHole.VarDateTime(n, _)
| TemplateHole.VarDomElement(n, _)
| TemplateHole.VarFile(n, _) ->
filledVars.Add n |> ignore
hasEventHandler
Expand Down Expand Up @@ -306,6 +311,8 @@ type Handler private () =
(n, t, Option.Some (box v.Value))
| Some (TemplateHole.VarDateTime(n, v)) ->
(n, t, Option.Some (box v.Value))
| Some (TemplateHole.VarDomElement(n, v)) ->
(n, t, Option.Some (box v.Value))
| Some (TemplateHole.VarFile(n, v)) ->
(n, t, Option.Some (box v.Value))
| _ ->
Expand Down Expand Up @@ -473,6 +480,11 @@ type ProviderBuilder =
member this.With(hole: string, value: Var<DateTime>) =
this.With(TemplateHole.VarDateTime(hole, value))

/// Fill a hole of the template.
[<Inline>]
member this.With(hole: string, value: Var<DomElement option>) =
this.With(TemplateHole.VarDomElement(hole, value))

/// Fill a hole of the template.
[<Inline>]
member this.With(hole: string, value: DateTime) =
Expand Down Expand Up @@ -648,8 +660,8 @@ type Runtime private () =

let rec writeWrappedTemplate templateName (template: Template) ctx =
let tagName = template.Value |> Array.tryPick (function
| Node.Element (name, _, _, _)
| Node.Input (name, _, _, _) -> Some name
| Node.Element (name, _, _,_, _)
| Node.Input (name, _, _, _, _) -> Some name
| Node.Text _ | Node.DocHole _ | Node.Instantiate _ -> None
)
let before, after = defaultArg (Map.tryFind tagName templateWrappers) defaultTemplateWrappers
Expand Down Expand Up @@ -727,12 +739,13 @@ type Runtime private () =
if ctx.FillWith.ContainsKey holeName then
failwithf "Invalid hole, expected onafterrender: %s" holeName
elif keepUnfilled then doPlain()
let rec writeElement isRoot plain tag attrs wsVar children =
let rec writeElement isRoot plain tag attrs wsVar children wsDom =
ctx.Writer.WriteBeginTag(tag)
attrs |> Array.iter (fun a -> writeAttr plain a)
if isRoot then
extraAttrs |> List.iter (fun a -> a.Write(ctx.Context.Metadata, ctx.Context.Json, ctx.Writer, true))
wsVar |> Option.iter (fun v -> ctx.Writer.WriteAttribute("ws-var", v))
wsDom |> Option.iter (fun v -> ctx.Writer.WriteAttribute("ws-dom", v))
if Array.isEmpty children && HtmlTextWriter.IsSelfClosingTag tag then
ctx.Writer.Write(HtmlTextWriter.SelfClosingTagEnd)
else
Expand All @@ -746,10 +759,10 @@ type Runtime private () =
)
ctx.Writer.WriteEndTag(tag)
and writeNode parent plain = function
| Node.Element (tag, _, attrs, children) ->
writeElement (Option.isNone parent) plain tag attrs None children
| Node.Input (tag, holeName, attrs, children) ->
let doPlain() = writeElement (Option.isNone parent) plain tag attrs (Some holeName) children
| Node.Element (tag, _, attrs, domAttr, children) ->
writeElement (Option.isNone parent) plain tag attrs None children domAttr
| Node.Input (tag, holeName, attrs, domAttr, children) ->
let doPlain() = writeElement (Option.isNone parent) plain tag attrs (Some holeName) children domAttr
if plain then doPlain() else
let wsVar, attrs =
match ctx.FillWith.TryGetValue holeName with
Expand All @@ -763,7 +776,7 @@ type Runtime private () =
Some (if String.IsNullOrEmpty id then n else id + "::" + n), attrs
| _ ->
Some holeName, attrs
writeElement (Option.isNone parent) plain tag attrs wsVar children
writeElement (Option.isNone parent) plain tag attrs wsVar children domAttr
| Node.Text text ->
writeStringParts text ctx.Writer
| Node.DocHole holeName ->
Expand Down
5 changes: 5 additions & 0 deletions WebSharper.UI.Templating.Runtime/RuntimeClient.fs
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,11 @@ type private HandlerProxy =
Server.TemplateInitializer.GetOrAddHoleFor(key, name, fun () -> TemplateHole.VarDateTime (name, Var.Create DateTime.MinValue))
| Server.ValTy.File ->
Server.TemplateInitializer.GetOrAddHoleFor(key, name, fun () -> TemplateHole.VarFile (name, Var.Create [||]))
| Server.ValTy.DomElement ->
Server.TemplateInitializer.GetOrAddHoleFor(key, name, fun () ->
let el = JavaScript.JS.Document.QuerySelector("[ws-dom=" + name + "]")
el.RemoveAttribute("ws-dom")
TemplateHole.VarDomElement (name, Var.Create <| Some el))
| _ -> failwith "Invalid value type"
allVars[name] <- r
Some r
Expand Down

0 comments on commit 32499fe

Please sign in to comment.