1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
use crate::dom::{Application, Cmd, Component, Modifier, MountAction, MountTarget, Program};
use crate::vdom::Node;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
/// a trait for implementing WebComponent in the DOM with custom tag
pub trait WebComponent<MSG> {
/// returns the attributes that is observed by this component
/// These are the names of the attributes the component is interested in
fn observed_attributes() -> Vec<&'static str>;
/// This will be invoked when a component is used as a custom element
/// and the attributes of the custom-element has been modified
///
/// if the listed attributes in the observed attributes are modified
fn attribute_changed(
program: Program<Self, MSG>,
attr_name: &str,
old_value: Option<String>,
new_value: Option<String>,
) where
Self: Sized + Application<MSG>;
/// the component is attached to the dom
fn connected_callback(&mut self);
/// the component is removed from the DOM
fn disconnected_callback(&mut self);
/// the component is moved or attached to the dom
fn adopted_callback(&mut self);
}
thread_local!(static REGISTER_CUSTOM_ELEMENT_FUNCTION: js_sys::Function = declare_custom_element_function());
/// register using custom element define
/// # Example:
/// ```rust,ignore
/// sauron::register_web_component("date-time", "DateTimeWidgetCustomElement");
/// ```
pub fn register_web_component(custom_tag: &str, adapter: JsValue, observed_attributes: JsValue) {
log::info!("registering a custom element: {:?}", custom_tag);
REGISTER_CUSTOM_ELEMENT_FUNCTION.with(|func| {
func.call3(
&JsValue::NULL,
&JsValue::from_str(custom_tag),
&adapter,
&observed_attributes,
)
.expect("must call");
})
}
/// TODO: refer to https://github.com/gbj/custom-elements
/// for improvements
/// dynamically create the function which will register the custom tag
///
/// This is needed since there is no way to do `class extends HTMLElement` in rust code
fn declare_custom_element_function() -> js_sys::Function {
js_sys::Function::new_with_args(
"custom_tag, adapter, observed_attributes",
r#"
if (window.customElements.get(custom_tag) === undefined ){
window.customElements.define(custom_tag,
class extends HTMLElement{
constructor(){
super();
// the adapter execute the closure which attached the `new` function into `this`
// object.
adapter(this);
// we then call that newly attached `new` function to give us an instance
// of the CustomElement WebComponent
this.instance = this.new(this);
}
static get observedAttributes(){
return observed_attributes;
}
connectedCallback(){
this.instance.connectedCallback();
}
disconnectedCallback(){
this.instance.disconnectedCallback();
}
adoptedCallback(){
this.instance.adoptedCallback();
}
attributeChangedCallback(name, oldValue, newValue){
this.instance.attributeChangedCallback(name, oldValue, newValue);
}
appendChild(child){
console.log("appending a child:", child);
this.instance.appendChild(child);
}
}
);
}"#,
)
}
/// Blanket implementation of Application trait for Component that
/// has no external MSG
/// but only if that Component is intended to be a WebComponent
impl<COMP, MSG> Application<MSG> for COMP
where
COMP: Component<MSG, ()> + WebComponent<MSG> + 'static,
MSG: 'static,
{
fn init(&mut self) -> Cmd<Self, MSG> {
Cmd::from(<Self as Component<MSG, ()>>::init(self))
}
fn update(&mut self, msg: MSG) -> Cmd<Self, MSG> {
let effects = <Self as Component<MSG, ()>>::update(self, msg);
Cmd::from(effects)
}
fn view(&self) -> Node<MSG> {
<Self as Component<MSG, ()>>::view(self)
}
fn stylesheet() -> Vec<String> {
<Self as Component<MSG, ()>>::stylesheet()
}
fn style(&self) -> Vec<String> {
<Self as Component<MSG, ()>>::style(self)
}
}
/// A self contain web component
/// This is needed to move some of the code from the #custom_element macro
/// This is also necessary, since #[wasm_bindgen] macro can not process impl types which uses
/// generics, we use generics here to simplify the code and do the type checks for us, rather than
/// in the code derived from the #[web_component] macro
pub struct WebComponentWrapper<APP, MSG>
where
MSG: 'static,
{
/// the underlying program running this web component
pub program: Program<APP, MSG>,
}
impl<APP, MSG> WebComponentWrapper<APP, MSG>
where
APP: Application<MSG> + WebComponent<MSG> + Default + 'static,
MSG: 'static,
{
/// create a new web component, with the node as the target element to be mounted into
pub fn new(node: JsValue) -> Self {
let mount_node: &web_sys::Node = node.unchecked_ref();
Self {
program: Program::new(
APP::default(),
mount_node,
MountAction::Append,
MountTarget::ShadowRoot,
),
}
}
/// When the attribute of the component is changed, this method will be called
pub fn attribute_changed(&self, attr_name: &str, old_value: JsValue, new_value: JsValue) {
let old_value = old_value.as_string();
let new_value = new_value.as_string();
APP::attribute_changed(self.program.clone(), attr_name, old_value, new_value);
}
/// called when the web component is mounted
pub fn connected_callback(&mut self) {
self.program.mount();
let static_style = <APP as Application<MSG>>::stylesheet().join("");
self.program.inject_style_to_mount(&static_style);
let dynamic_style =
<APP as Application<MSG>>::style(&self.program.app_context.app.borrow()).join("");
self.program.inject_style_to_mount(&dynamic_style);
self.program
.app_context
.app
.borrow_mut()
.connected_callback();
self.program
.update_dom(&Modifier::default())
.expect("must update dom");
}
/// called when the web component is removed
pub fn disconnected_callback(&mut self) {
self.program
.app_context
.app
.borrow_mut()
.disconnected_callback();
}
/// called when web componented is moved into other parts of the document
pub fn adopted_callback(&mut self) {
self.program.app_context.app.borrow_mut().adopted_callback();
}
}