Web components by AngelMunoz · Pull Request #36 · davedawkins/Sutil · GitHub
source link: https://github.com/davedawkins/Sutil/pull/36
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
As we've been discussing in #32 I took a step into a rough implementation (at least on the JS side) of a class factory that ties some of the Sutil semantics with the class and it's declaration as a custom element
Please take a look at the two main files here
export function customElFactory(defaultValues, viewFn) { return class extends HTMLElement { // allow property names to be observed attributes static get observedAttributes() { return Object.keys(defaultValues); } constructor() { super(); // create a shadowRoot and make it the context this.attachShadow({ mode: 'open' }); const ctx = makeContext(this.shadowRoot); // generate a SutilNode build and asDomElement already do this this.__sutilNode = asDomElement(build(viewFn(defaultValues), ctx), ctx); // implement getter/setters for properties for (const key of Object.keys(defaultValues)) { Object.defineProperty(this, key, { configurable: false, enumerable: true, get: () => this.__getStorePropValue(key), // use the attribute changedCallback from the observedAttributes set: value => this.attributeChangedCallback(key, this.__getStorePropValue(key), value) }); } } attributeChangedCallback(name, oldVal, newVal) { this.__setStorePropValue(name, newVal); } disconnectedCallback() { iterate(item => item?.Dispose(), this.shadowRoot?.firstChild?.__sutil_disposables); iterate(item => item?._dispose(), this.shadowRoot?.firstChild?.__sutil_groups); } // utility methods to get/set values from the store get __sutilFistElStore() { return head(this.shadowRoot?.firstChild?.__sutil_disposables || empty()); } __getStorePropValue(key) { const store = this.__sutilFistElStore; return store?.Value?.[key]; } __setStorePropValue(key, value) { const store = this.__sutilFistElStore; if (!key || !store) return; store.Update(store => ({ ...store, [key]: value })); } }; } export function defineCustomElement(name, props, viewFn) { customElements.define(name, customElFactory(props, viewFn)); }
Above we're Simply exporting two functions that will help us define web components, I had to make this a JS file because (1st familiarity, 2nd I'm not sure we can return anonymous classes in F#)
basically when we attach a shadow root (not required for custom elements but helpful for style encapsulation) we can use it as the context for the SutilElement the build
and asDomElement
functions will internally append the view function result to the shadow DOM (since it is the context)
at this point it already works but it is unable to respond to external manipulation (e.g. attribute changes, property changes), since that is a common case for WebComponents/CustomElements I just added getters and setters via Object.defineProperty
which read/write values from the elemen'ts store, in addition the static get observedAttributes() { return Object.keys(defaultValues); }
also allows us to observe HTML Attributes and since we're using the same attribute changed callback invoked when attributes change we're able to do the following
const sutilCustomEl = document.querySelector("my-element") // does the update via the element's store sutilCustomEl.age = 10;
or update the attributes in the HTML to update the values, please note that we're not reflecting attributes hence why the attributes don't change when the store does so.
module Main open Sutil open Sutil.DOM open Fable.Core open Fable.Core.DynamicExtensions open Fable.Core.JsInterop open Browser.Types open Browser.Dom open Sutil.Attr importSideEffects "./styles.css" let defineCustomElement (name: string, defaults: obj, el: obj) = importMember "./web-component.js" type Stuff = { name: string; age: int } let view (props: Stuff) = let store = Store.make props let age = store .> (fun store -> store.age) let name = store .> (fun store -> store.name) Html.div [ disposeOnUnmount [ store ] bindFragment2 name age <| (fun (name, age) -> text $"name: {name} age: {age}") Html.button [ on "click" (fun _ -> Store.modify (fun store -> { store with age = store.age + 1 }) store) [] text "Update age" ] ] defineCustomElement ("my-element", { name = "Frank"; age = 0 }, view) // same view, different component with different defaults defineCustomElement ("my-element-2", { name = "Peter"; age = 10 }, view)
In the case of the F# side, we don't really need to do much different things from what we already do
This approach does have some caveats though
- The View Function has to have an object as the parameter (
Object.keys
in the js function), either an anonymoys object or a record - The View Function should have only one store and it should be marked as
disposeOnUnmount [ store ]
for it to show on the JS side, also the store should be based of the function's first parameter for it to behave as it does in the gif above.
What are we missing?
- This just defines a web component, it does not provide an idiomatic API like the one's we're used to (e.g. Html.myElement [ ]`
- Styling, I have not dive into styling and how will things work with Shadow DOM
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK