Skip to content

Instantly share code, notes, and snippets.

@OnurGumus
Last active July 22, 2020 16:12
Show Gist options
  • Save OnurGumus/2ee9ee40eb6cb5d5d185ec16ee0becb0 to your computer and use it in GitHub Desktop.
Save OnurGumus/2ee9ee40eb6cb5d5d185ec16ee0becb0 to your computer and use it in GitHub Desktop.
Fable Web components
module App
open Fable.Core
open Browser
open Browser.Types
open Fable.Core.JsInterop
open Fable.Core.JS
open Fable.Core.DynamicExtensions
[<AllowNullLiteral>]
type HTMLTemplateElement =
inherit HTMLElement
abstract content: DocumentFragment with get, set
[<AllowNullLiteral>]
type HTMLTemplateElementType =
[<EmitConstructor>]
abstract Create: unit -> HTMLTemplateElement
[<Global>]
type ShadowRoot() =
member this.appendChild(el: Browser.Types.Node) = jsNative
member this.querySelector(selector: string): Browser.Types.HTMLElement = jsNative
let inline attachStatic<'T> (name: string) (f: obj): unit = jsConstructor<'T>?name <- f
let inline attachStaticGetter<'T, 'V> (name: string) (f: unit -> 'V): unit =
JS.Constructors.Object.defineProperty (jsConstructor<'T>, name, !!{| get = f |})
|> ignore
[<Global; AbstractClass>]
[<AllowNullLiteral>]
type HTMLElement() =
member _.getAttribute(attr: string): string = jsNative
member _.setAttribute(attr: string, v:obj) = jsNative
member _.attachShadow(obj): ShadowRoot = jsNative
member _.dispatchEvent(e:CustomEvent): unit = jsNative
abstract connectedCallback: unit -> unit
default _.connectedCallback () = ()
abstract attributeChangedCallback: string * obj * obj -> unit
default _.attributeChangedCallback (_,_,_) = ()
module Button =
let template: HTMLTemplateElement =
downcast document.createElement ("template")
template.innerHTML <-
"""
<style>
.container {
padding: 8px;
}
button {
display: block;
overflow: hidden;
position: relative;
padding: 0 16px;
font-size: 16px;
font-weight: bold;
text-overflow: ellipsis;
white-space: nowrap;
cursor: pointer;
outline: none;
width: 100%;
height: 40px;
box-sizing: border-box;
border: 1px solid #a1a1a1;
background: #ffffff;
box-shadow: 0 2px 4px 0 rgba(0,0,0, 0.05), 0 2px 8px 0 rgba(161,161,161, 0.4);
color: #363636;
cursor: pointer;
}
</style>
<div class="container">
<button>Label</button>
</div>
"""
[<AllowNullLiteral>]
type Button() as this =
inherit HTMLElement()
let shadowRoot: ShadowRoot = base.attachShadow ({| mode = "open" |})
let button = lazy( shadowRoot.querySelector ("button"))
do
let clone = template.content.cloneNode (true)
shadowRoot.appendChild (clone)
button.Force().addEventListener("click", fun _ ->
this.dispatchEvent(
CustomEvent.Create("onClick", jsOptions<CustomEventInit>(fun o -> o.detail <-"Hello") )))
member this.render() =
button.Force().innerHTML <- this.label
abstract label : string with get,set
default this.label
with get() = this.getAttribute("label")
and set(value:string) =
printf "%A" value
this.setAttribute("label", value)
override _.connectedCallback() = printf "connected callback"
override this.attributeChangedCallback(name, oldVal, newVal) = this.render ()
attachStaticGetter<Button, _> "observedAttributes" (fun () -> [| "label" |])
window?customElements?define ("my-button", jsConstructor<Button>)
module DDL =
let template: HTMLTemplateElement =
downcast document.createElement ("template")
template.innerHTML <-
"""
<style>
:host {
font-family: sans-serif;
}
.dropdown {
padding: 3px 8px 8px;
}
.label {
display: block;
margin-bottom: 5px;
color: #000000;
font-size: 16px;
font-weight: normal;
line-height: 16px;
}
.dropdown-list-container {
position: relative;
}
.dropdown-list {
position: absolute;
width: 100%;
display: none;
max-height: 192px;
overflow-y: auto;
margin: 4px 0 0;
padding: 0;
background-color: #ffffff;
border: 1px solid #a1a1a1;
box-shadow: 0 2px 4px 0 rgba(0,0,0, 0.05), 0 2px 8px 0 rgba(161,161,161, 0.4);
list-style: none;
}
.dropdown-list li {
display: flex;
align-items: center;
margin: 4px 0;
padding: 0 7px;
font-size: 16px;
height: 40px;
cursor: pointer;
}
.dropdown.open .dropdown-list {
display: flex;
flex-direction: column;
}
.dropdown-list li.selected {
font-weight: 600;
}
</style>
<div class="dropdown">
<span class="label">Label</span>
<my-button as-atom>Content</my-button>
<div class="dropdown-list-container">
<ul class="dropdown-list"></ul>
</div>
</div>
"""
[<AllowNullLiteral>]
type Dropdown() as this =
inherit HTMLElement()
let shadowRoot: ShadowRoot = base.attachShadow ({| mode = "open" |})
let mutable opened = false
let button = lazy( shadowRoot.querySelector ("my-button"))
let labelv = lazy(shadowRoot.querySelector(".label"))
let dropdown = lazy(shadowRoot.querySelector(".dropdown"))
let dropdownList = lazy(shadowRoot.querySelector(".dropdown-list"))
do
let clone = template.content.cloneNode (true)
shadowRoot.appendChild (clone)
button.Value.addEventListener("onClick", fun e-> this.toggleOpen(e))
member _.toggleOpen(event) =
opened <- not opened
if opened then dropdown.Value.classList.add "open"
else dropdown.Value.classList.remove "open"
member this.render() =
labelv.Value.innerHTML <- this.label
if(this.options <> null) then
button.Value.setAttribute("label", this.options.[this.option]?label)
dropdownList.Value.innerHTML <- ""
printf "%A" this.options
JS.Object.keys(if this.options <> null then this.options else createEmpty )
?forEach(fun key ->
let option = this.options.[key]
let opt = document.createElement("li")
opt.innerHTML <- option?label
if (this.option <> null && this.option = key) then
opt.classList.add "selected"
opt.addEventListener("click", fun e ->
this.option <- key
this.toggleOpen(e)
this.dispatchEvent( CustomEvent.Create("onChange", jsOptions<CustomEventInit>(fun o -> o.detail <- key) ))
this.render()
)
dropdownList.Value.appendChild opt
|> ignore
)
abstract label : string with get,set
default this.label
with get() = this.getAttribute("label")
and set(value:string) = this.setAttribute("label", value)
abstract option : string with get,set
default this.option
with get() = this.getAttribute("option")
and set(value:string) = this.setAttribute("option", value)
abstract options : obj with get,set
default this.options
with get() = this.getAttribute("options") |> JSON.parse
and set(value:obj) = this.setAttribute("options", value |> JSON.stringify)
override _.connectedCallback() = printf "connected callback"
override this.attributeChangedCallback(name, oldVal, newVal) = this.render ()
attachStaticGetter<Dropdown, _> "observedAttributes"
(fun () -> [| "label"; "option"; "options" |])
window?customElements?define ("my-dropdown", jsConstructor<Dropdown>)
<html>
<head>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
<script src="__HOST__/libs/react.production.min.js"></script>
<script src="__HOST__/libs/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@webcomponents/[email protected]/custom-elements-es5-adapter.js"></script>
<script src="https://unpkg.com/@webcomponents/[email protected]/custom-elements.min.js"></script>
<script src="https://unpkg.com/@webcomponents/[email protected]/shadydom.min.js"></script>
<script src="bundle.js"></script>
</head>
<body class="app-container">
<my-button></my-button>
<script>
const element = document.querySelector('my-button');
element.label = 'Click Me';
console.log (element)
</script>
<my-dropdown
label="Dropdown"
option="option2"
></my-dropdown>
<!--
<my-dropdown label="Dropdown" option="option2"></my-dropdown> -->
<script>
document.querySelector('my-dropdown').options = {
option1: { label: 'Option 11' },
option1: { label: 'Option 12' },
option2: { label: 'Option 2' },
};
document
.querySelector('my-dropdown')
.addEventListener('onChange', event => console.log(event.detail));
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment