Clone in a terminal:
init.rs
src/ - Log less
use proc_macro as pm; use proc_macro2::Span; use quote::quote; use syn::parse2; use syn::{Attribute, Fields, Ident, ItemStruct, LitStr}; pub fn component_impl(attr: pm::TokenStream, item: pm::TokenStream) -> pm::TokenStream { // Parse this #[component("my-tag")]) for a struct let html_tag_ts = proc_macro2::TokenStream::from(attr.clone()); let tag_lit = match parse2::<LitStr>(html_tag_ts) { Ok(lit) => lit, Err(err) => return err.to_compile_error().into(), }; if tag_lit.value().is_empty() { return syn::Error::new_spanned( &tag_lit, "expected a tag name or component: #[component(\"example-component-tag\")]", ) .to_compile_error() .into(); } // Parse the annotated item as a struct let component_struct_ts = proc_macro2::TokenStream::from(item.clone()); let component_struct_item = match syn::parse2::<ItemStruct>(component_struct_ts.clone()) { Ok(s) => s, Err(_) => { return syn::Error::new_spanned( component_struct_ts, "the #[component] attribute can only be applied to structs", ) .to_compile_error() .into(); } }; let struct_ident = &component_struct_item.ident; let emitted_struct_item = sanitize_component_struct(&component_struct_item); let create_class_js = format!("createComponentClass{}", struct_ident); let init_self_js = format!("initSelf{}", struct_ident); let attach_js = format!("attachComponent{}", struct_ident); let detach_js = format!("detachComponent{}", struct_ident); let observed_js = format!("observedComponentAttribute{}", struct_ident); let changed_js = format!("changedComponentAttribute{}", struct_ident); let validate_attribute_js = format!("validateAttribute{}", struct_ident); let create_class_js_indent = Ident::new(&create_class_js, Span::call_site()); let init_self_js_ident = Ident::new(&init_self_js, Span::call_site()); let create_js_class_fn = create_class( create_class_js_indent, tag_lit.clone(), init_self_js.as_str(), attach_js.as_str(), detach_js.as_str(), observed_js.as_str(), changed_js.as_str(), validate_attribute_js.as_str(), ); let rs_self = gen_rust_self(component_struct_item.clone(), init_self_js_ident); let attach_fn = attach_quote(&attach_js, struct_ident); let detach_fn = detach_quote(&detach_js, struct_ident); let observed_fn = observed_quote(&observed_js, struct_ident); let changed_fn = changed_quote(&changed_js, struct_ident); let component_impl = component_impl_quote(struct_ident); let component_methods = create_component(&component_struct_item); let validate_attribute_fn = validate_attribute_quote(&validate_attribute_js, struct_ident); let expanded = quote! { #[allow(dead_code, unreachable_code)] #emitted_struct_item #component_impl #component_methods #create_js_class_fn #rs_self #attach_fn #detach_fn #observed_fn #changed_fn #validate_attribute_fn }; expanded.into() } fn sanitize_component_struct(struct_item: &ItemStruct) -> ItemStruct { let mut sanitized = struct_item.clone(); sanitized.attrs.push(syn::parse_quote!(#[derive(Default)])); if let Fields::Named(fields) = &mut sanitized.fields { for field in &mut fields.named { field .attrs .retain(|attr| !attr.path().is_ident("attribute")); } } sanitized } fn attach_quote(atach_state_js_fn: &str, struct_name: &Ident) -> proc_macro2::TokenStream { let js_callback_ident = Ident::new(atach_state_js_fn, Span::call_site()); let rs_name_str = from_pascal_to_snake(atach_state_js_fn); let rs_name_ident = Ident::new(&rs_name_str, Span::call_site()); let component_name = Ident::new(&format!("{}Component", struct_name), Span::call_site()); let expanded = quote! { #[callback(#js_callback_ident)] fn #rs_name_ident(id: String) { if let Some(component) = #component_name::get_component(&id) { component.connected(); } else { error!("Component id not found in connect.", id.clone()); } } }; expanded } fn detach_quote(detach_state_js_fn: &str, struct_name: &Ident) -> proc_macro2::TokenStream { let js_callback_ident = Ident::new(detach_state_js_fn, Span::call_site()); let rs_name_str = from_pascal_to_snake(detach_state_js_fn); let rs_name_ident = Ident::new(&rs_name_str, Span::call_site()); let store_name_str = store_name(struct_name); let store_ident = Ident::new(&store_name_str, Span::call_site()); let component_name = Ident::new(&format!("{}Component", struct_name), Span::call_site()); let expanded = quote! { #[callback(#js_callback_ident)] fn #rs_name_ident(wasm_id: String) { if let Some(component) = #component_name::get_component(&wasm_id) { component.disconnected(); #store_ident.with(|store| { store.borrow_mut().remove(&wasm_id); }); } else { error!("Component id not found in disconnected.", wasm_id.clone()); } } }; expanded } fn validate_attribute_quote( validate_attribute_js: &str, struct_name: &Ident, ) -> proc_macro2::TokenStream { let js_callback_ident = Ident::new(validate_attribute_js, Span::call_site()); let rs_name_str = from_pascal_to_snake(validate_attribute_js); let rs_name_ident = Ident::new(&rs_name_str, Span::call_site()); let component_name = Ident::new(&format!("{}Component", struct_name), Span::call_site()); let expanded = quote! { #[callback(#js_callback_ident)] fn #rs_name_ident(wasm_id: String, name: String, value: String) -> bool { if let Some(component) = #component_name::get_component(&wasm_id) { return component.is_valid_attribute(name.as_str(), value.as_str()); } error!("No component found for", wasm_id.clone()); false } }; expanded } fn component_impl_quote(struct_name: &Ident) -> proc_macro2::TokenStream { let snake_name = from_pascal_to_snake(&struct_name.to_string()); let init_fn = format!("{}_init_component", snake_name); let init_component_fn = Ident::new(&init_fn, struct_name.span()); let expanded = quote! { ::inventory::submit! { InitComponentFn(#init_component_fn) } pub fn #init_component_fn() { } }; expanded } fn changed_quote(changed_state_js_fn: &str, struct_name: &Ident) -> proc_macro2::TokenStream { let js_callback_ident = Ident::new(changed_state_js_fn, Span::call_site()); let rs_name_str = from_pascal_to_snake(changed_state_js_fn); let rs_name_ident = Ident::new(&rs_name_str, Span::call_site()); let component_name = Ident::new(&format!("{}Component", struct_name), Span::call_site()); let expanded = quote! { #[callback(#js_callback_ident)] fn #rs_name_ident(element_id: String, key: String, from: String, to: String) { if let Some(component) = #component_name::get_component(&element_id) { let name = key.as_str(); component.internal_attributes_update(name, &from, &to); component.attribute_changed(name, &from, &to); } else { error!("Component id not found in changed attribute.", element_id.clone()); } } }; expanded } fn observed_quote(observed_js_fn: &str, struct_name: &Ident) -> proc_macro2::TokenStream { let js_callback_ident = Ident::new(observed_js_fn, Span::call_site()); let rs_name_str = from_pascal_to_snake(observed_js_fn); let rs_name_ident = Ident::new(&rs_name_str, Span::call_site()); let component_name = Ident::new(&format!("{}Component", struct_name), Span::call_site()); let expanded = quote! { #[callback(#js_callback_ident)] fn #rs_name_ident() -> &'static [&'static str] { #component_name::observed_attributes() } }; expanded } fn store_name(struct_name: &Ident) -> String { let store_name = from_pascal_to_snake(&struct_name.to_string()).to_uppercase() + "_COMPONENTS"; store_name } fn from_pascal_to_snake(fn_name: &str) -> String { let mut result = String::new(); for (i, c) in fn_name.chars().enumerate() { if c.is_uppercase() { if i != 0 { result.push('_'); } result.extend(c.to_lowercase()); } else { result.push(c); } } result } fn struct_name_to_shouty_snake(struct_name: &Ident) -> String { from_pascal_to_snake(&struct_name.to_string()).to_uppercase() } fn gen_rust_self(struct_item: ItemStruct, js_name: Ident) -> proc_macro2::TokenStream { let struct_ident = &struct_item.ident; let store_name_str = store_name(struct_ident); let store_ident = Ident::new(&store_name_str, Span::call_site()); let struct_name_str = struct_ident.to_string(); let var_name_str = from_pascal_to_snake(struct_name_str.as_str()); let var_ident = Ident::new(&var_name_str, Span::call_site()); let rust_name = format!( "init_component_self_{}", from_pascal_to_snake(struct_name_str.as_str()) ); let mut kebab_case_name = from_pascal_to_snake(struct_name_str.as_str()); kebab_case_name = kebab_case_name.replace("_", "-"); let rust_ident = Ident::new(&rust_name, Span::call_site()); let struct_ref_container = Ident::new(&format!("{}RefContainer", struct_ident), Span::call_site()); let expanded = quote! { #[callback(#js_name)] #[allow(non_snake_case)] pub fn #rust_ident(current_id: String) -> String { // init_component_state_struct_name, returns id for the instance of the element let #var_ident = #struct_ident::default(); let key = if current_id.is_empty() { let component_counter = ::kaja_web::NEXT_COMPONENT_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); format!("{}-{}", #kebab_case_name, component_counter) } else { current_id }; let component = std::rc::Rc::new(std::cell::RefCell::new(#struct_ref_container { id: key.clone(), attribute_values: std::rc::Rc::new(std::cell::RefCell::new(#var_ident)), })); #store_ident.with(|store| { store.borrow_mut().insert(key.clone(), component); }); key } }; expanded } fn create_class( class_creator: Ident, tag: LitStr, init_self_fn: &str, attach_state_fn: &str, detach_state_fn: &str, observed_fn: &str, change_fn: &str, validate_attribute_fn: &str, ) -> proc_macro2::TokenStream { let name_str_lit = LitStr::new(&class_creator.to_string(), Span::call_site()); let expanded = quote! { ::inventory::submit! { InitComponentFn(#class_creator) } #[allow(non_snake_case)] pub fn #class_creator() { // createComponentTheStructName let component_name = #name_str_lit; let tag_name = #tag; let js_init = String::from( r#" return class extends HTMLElement { constructor() { super(); let currentId = this.getAttribute("id"); if (currentId === null) { currentId = ""; } let resolvedId = __INIT_SELF_CALLBACK(currentId); if (!currentId) { this.setAttribute("id", resolvedId); } } connectedCallback() { let componentId = this.getAttribute("id"); __ATTACH_CALLBACK(componentId); } disconnectedCallback() { let componentId = this.getAttribute("id"); __DETACH_CALLBACK(componentId); } attributeChangedCallback(name, oldValue, newValue) { let componentId = this.getAttribute("id"); __ATTRIBUTE_CHANGED_CALLBACK(componentId, name, oldValue === null ? "" : String(oldValue), newValue === null ? "" : String(newValue)); } static get observedAttributes() { return __OBSERVED_CALLBACK(); } setAttribute(name, value) { let componentId = this.getAttribute("id"); if (name === "id" || !componentId) { return super.setAttribute(name, value); } let valAsStr = String(value); if (!__VALIDATE_ATTRIBUTE(componentId, name, valAsStr)) { throw new TypeError(`"${value}" is not a valid value for "${name}"`); } return super.setAttribute(name, value); } } "#, ); let js_init_with_state = js_init; let js_init_with_state = js_init_with_state.replace("__INIT_SELF_CALLBACK", #init_self_fn); let js_init_with_state = js_init_with_state.replace("__ATTACH_CALLBACK", #attach_state_fn); let js_init_with_state = js_init_with_state.replace("__DETACH_CALLBACK", #detach_state_fn); let js_init_with_state = js_init_with_state.replace("__ATTRIBUTE_CHANGED_CALLBACK", #change_fn); let js_init_with_state = js_init_with_state.replace("__OBSERVED_CALLBACK", #observed_fn); let js_init_with_state = js_init_with_state.replace("__VALIDATE_ATTRIBUTE", #validate_attribute_fn); let component = js_sys::Function::new_no_args(js_init_with_state.as_str()); let class = component.call0(&JsValue::NULL); if class.is_err() { error!("Failed to register class for {}", #tag); return; } let class_fn = class.unwrap(); let registry = web_sys::window().unwrap().custom_elements(); let result = registry.define(#tag, &class_fn.unchecked_into()); if result.is_err() { error!("Failed to register component", #tag); } } }; expanded } fn has_attribute_marker(attr: &Attribute) -> bool { attr.path().is_ident("attribute") } fn create_component(struct_item: &ItemStruct) -> proc_macro2::TokenStream { let struct_ident = &struct_item.ident; let struct_ref_container = Ident::new(&format!("{}RefContainer", struct_ident), Span::call_site()); let component_ident = Ident::new(&format!("{}Component", struct_ident), Span::call_site()); let store_name_str = store_name(struct_ident); let store_ident = Ident::new(&store_name_str, Span::call_site()); let observed_attributes_const = Ident::new( &format!( "{}_OBSERVED_ATTRIBUTES", struct_name_to_shouty_snake(struct_ident) ), Span::call_site(), ); let (setter_arms, getter_arms, validator_arms, observed_attributes) = match &struct_item.fields { Fields::Named(fields) => { let attribute_fields = fields .named .iter() .filter(|field| field.attrs.iter().any(has_attribute_marker)); let setter_arms = attribute_fields.clone().filter_map(|field| { let field_ident = field.ident.as_ref()?; let field_name = field_ident.to_string(); let field_ty = &field.ty; Some(quote! { #field_name => { self.#field_ident = value.parse::<#field_ty>().map_err(|_| ::kaja_web::Error)?; } }) }); let getter_arms = attribute_fields.clone().filter_map(|field| { let field_ident = field.ident.as_ref()?; let field_name = field_ident.to_string(); Some(quote! { #field_name => Some(self.#field_ident.to_string()) }) }); let validator_arms = attribute_fields.clone().filter_map(|field| { let field_ident = field.ident.as_ref()?; let field_name = field_ident.to_string(); let field_ty = &field.ty; Some(quote! { #field_name => { value.parse::<#field_ty>().map_err(|_| ::kaja_web::Error)?; } }) }); let observed_attributes = attribute_fields.filter_map(|field| { let field_ident = field.ident.as_ref()?; let field_name = LitStr::new(&field_ident.to_string(), Span::call_site()); Some(quote! { #field_name }) }); ( quote! { #(#setter_arms,)* }, quote! { #(#getter_arms,)* }, quote! { #(#validator_arms,)* }, quote! { #(#observed_attributes,)* }, ) } _ => (quote! {}, quote! {}, quote! {}, quote! {}), }; let struct_name_lit = LitStr::new(&struct_ident.to_string(), Span::call_site()); let expanded = quote! { thread_local! { static #store_ident: std::cell::RefCell<std::collections::HashMap<String, std::rc::Rc<std::cell::RefCell<#struct_ref_container>>>> = std::cell::RefCell::new(std::collections::HashMap::new()); } impl #struct_ident { fn set_test_attribute(&mut self, name: &str, value: &str) { let _ = self.try_set_test_attribute(name, value); } #[allow(unreachable_code)] fn try_set_test_attribute(&mut self, name: &str, value: &str) -> ::kaja_web::Result<()> { match name { #setter_arms _ => { error!("No field", name, "in", #struct_name_lit, "can't set it to", value); return Err(::kaja_web::Error); } } Ok(()) } #[allow(unreachable_code)] fn validate_test_attribute(&self, name: &str, value: &str) -> ::kaja_web::Result<()> { match name { #validator_arms _ => { // no validation for unknown attributes, assume it's valid } } Ok(()) } fn get_attribute(&self, name: &str) -> Option<String> { match name { #getter_arms _ => { error!("No field", name, "in", #struct_name_lit, "can't get it"); None } } } } #[derive(Default)] struct #struct_ref_container { id: String, attribute_values: std::rc::Rc<std::cell::RefCell<#struct_ident>>, } impl #struct_ref_container { fn create_component(id: &str) { let handle = std::rc::Rc::new(std::cell::RefCell::new(#struct_ref_container { id: id.to_string(), attribute_values: std::rc::Rc::new(std::cell::RefCell::new(#struct_ident::default())), })); Self::register_component(handle.clone()); let component = #component_ident::get_component(&id); if component.is_none() { error!("Failed to register component."); return; } let component_impl = component.unwrap(); component_impl.connected(); } fn register_component(component: std::rc::Rc<std::cell::RefCell<#struct_ref_container>>) { let id = component.borrow().id.clone(); #store_ident.with(|components| { components.borrow_mut().insert(id, component); }); } } static #observed_attributes_const: &[&str] = &[#observed_attributes]; #[derive(Default)] struct #component_ident { handle: std::rc::Rc<std::cell::RefCell<#struct_ref_container>>, attribute_values: std::rc::Rc<std::cell::RefCell<#struct_ident>>, } impl #component_ident { fn get_component(id: &str) -> Option<#component_ident> { let handle = #store_ident.with(|components| components.borrow().get(id).cloned()); if !handle.is_some() { return None; } let handle = handle.unwrap(); let attribute_values = handle.borrow().attribute_values.clone(); Some(#component_ident { handle, attribute_values, }) } fn observed_attributes() -> &'static [&'static str] { #observed_attributes_const } fn internal_attributes_update(&self, name: &str, _old_value: &str, new_value: &str) { let mut attr = self.attribute_values.borrow_mut(); attr.set_test_attribute(name, new_value); } fn set_attribute<T>(&self, name: &str, value: T) -> Result<()> where T: ToString, { <Self as ::kaja_web::ComponentHandle>::set_attribute(self, name, value) } fn set_attribute_string(&self, name: &str, value: &str) -> Result<()> { <Self as ::kaja_web::ComponentHandle>::set_attribute_string(self, name, value) } fn is_valid_attribute(&self, name: &str, value: &str) -> bool { let attr = self.attribute_values.borrow(); if attr.validate_test_attribute(name, value).is_err() { error!("Failed to parse attribute:", name, "with value:", value, "in set_attribute"); return false; } true } fn get_attribute_string(&self, name: &str) -> Option<String> { <Self as ::kaja_web::ComponentHandle>::get_attribute_string(self, name) } fn get_attribute<T>(&self, name: &str) -> Option<T> where T: std::str::FromStr, { <Self as ::kaja_web::ComponentHandle>::get_attribute(self, name) } fn get_attributes(&self) -> std::rc::Rc<std::cell::RefCell<#struct_ident>> { <Self as ::kaja_web::ComponentHandle>::get_attributes(self) } fn borrow(&self) -> std::cell::Ref<'_, #struct_ident> { <Self as ::kaja_web::ComponentHandle>::borrow(self) } fn borrow_mut(&self) -> std::cell::RefMut<'_, #struct_ident> { <Self as ::kaja_web::ComponentHandle>::borrow_mut(self) } fn with_mut<F>(&self, f: F) where F: FnOnce(&mut #struct_ident), { <Self as ::kaja_web::ComponentHandle>::with_mut(self, f) } fn with<F>(&self, f: F) where F: FnOnce(&#struct_ident), { <Self as ::kaja_web::ComponentHandle>::with(self, f) } fn get_id(&self) -> String { <Self as ::kaja_web::ComponentHandle>::get_id(self) } } impl ::kaja_web::ComponentHandle for #component_ident { type State = #struct_ident; fn set_attribute<T>(&self, name: &str, value: T) -> Result<()> where T: ToString, { let value = value.to_string(); self.set_attribute_string(name, &value) } fn set_attribute_string(&self, name: &str, value: &str) -> Result<()> { let id = self.get_id(); let element = get_element_by_id(&id); if element.is_none() { error!("Element not found for id:", id, "in set_attribute"); return Err(Error); } if !Self::observed_attributes().contains(&name) { error!("Attribute not observed:", name, "in set_attribute"); return Err(Error); } let valid = self.is_valid_attribute(name, value); if !valid { error!("Invalid type for attribute value:", value, "for name", name, "in set_attribute"); return Err(Error); } let res = element.unwrap().set_attribute(name, value); if res.is_err() { error!("Failed to set attribute:", name, "in set_attribute"); return Err(Error); } Ok(()) } fn get_attribute_string(&self, name: &str) -> Option<String> { let attr = self.attribute_values.borrow(); attr.get_attribute(name) } fn get_attribute<T>(&self, name: &str) -> Option<T> where T: std::str::FromStr, { let value = self.get_attribute_string(name)?; match value.parse::<T>() { Ok(value) => Some(value), Err(_) => { error!("Failed to parse attribute:", name, "in", #struct_name_lit); None } } } fn get_attributes(&self) -> std::rc::Rc<std::cell::RefCell<#struct_ident>> { self.attribute_values.clone() } fn borrow(&self) -> std::cell::Ref<'_, #struct_ident> { self.attribute_values.borrow() } fn borrow_mut(&self) -> std::cell::RefMut<'_, #struct_ident> { self.attribute_values.borrow_mut() } fn with_mut<F>(&self, f: F) where F: FnOnce(&mut #struct_ident), { let mut attr = self.attribute_values.borrow_mut(); f(&mut attr); } fn with<F>(&self, f: F) where F: FnOnce(&#struct_ident), { let attr = self.attribute_values.borrow(); f(&attr); } fn get_id(&self) -> String { self.handle.borrow().id.clone() } } }; expanded }

File history (46 commits)

ac7834f Johan Mattsson - johan.mattsson.m@gmail.com
Log less
Log less
64b7900 Johan Mattsson - johan.mattsson.m@gmail.com
Merge branch 'next-component'
Merge branch 'next-component'
d955433 Johan Mattsson - johan.mattsson.m@gmail.com
Documentation
Documentation
d30ae06 Johan Mattsson - johan.mattsson.m@gmail.com
New setter function
New setter function
a06f975 Johan Mattsson - johan.mattsson.m@gmail.com
Attribute update
Attribute update
834889e Johan Mattsson - johan.mattsson.m@gmail.com
Fix attribute annotation
Fix attribute annotation
6a72ca2 Johan Mattsson - johan.mattsson.m@gmail.com
Annotate attributes
Annotate attributes
a0aa003 Johan Mattsson - johan.mattsson.m@gmail.com
More attribute updates
More attribute updates
9311eac Johan Mattsson - johan.mattsson.m@gmail.com
Turn warnings back on
Turn warnings back on
41df0ed Johan Mattsson - johan.mattsson.m@gmail.com
Validate attributes set on web component in JS
Validate attributes set on web component in JS
5f8454f Johan Mattsson - johan.mattsson.m@gmail.com
Validate attributes before setting
Validate attributes before setting
363fc60 Johan Mattsson - johan.mattsson.m@gmail.com
Getters and setters for attributes
Getters and setters for attributes
2d79b82 Johan Mattsson - johan.mattsson.m@gmail.com
New component macro
New component macro
d117343 Johan Mattsson - johan.mattsson.m@gmail.com
Fix id and attribute
Fix id and attribute
0f9b3fa Johan Mattsson - johan.mattsson.m@gmail.com
Remove compoent threads with shared memory
Remove compoent threads with shared memory

I spent some time experimenting with threads and my conclusion
is that the restriction of not beeing able to lock on any
object in the main ui thread makes it more or less impossible.

Mainly because I don't know of a way to ensure that no wasm
code in the ui thread (deep down) calls Mutex::lock() and
ends up crashing the thread. Even things that seems to not
use any locking sometimes does.

Switching to web workers that posts messages instead.
f5ce5da Johan Mattsson - johan.mattsson.m@gmail.com
Use thread local storage for components
Use thread local storage for components
842f24e Johan Mattsson - johan.mattsson.m@gmail.com
Add component name to id
Add component name to id
469e64a Johan Mattsson - johan.mattsson.m@gmail.com
Fix component callbacks
Fix component callbacks
c981108 Johan Mattsson - johan.mattsson.m@gmail.com
New component impl
New component impl
4dcdd19 Johan Mattsson - johan.mattsson.m@gmail.com
Fix indentation
Fix indentation
f4af8ac Johan Mattsson - johan.mattsson.m@gmail.com
Log less
Log less
776c9f1 Johan Mattsson - johan.mattsson.m@gmail.com
Make attributes &str and use RwLock instead of Mutex
Make attributes &str and use RwLock instead of Mutex
55e2ad6 Johan Mattsson - johan.mattsson.m@gmail.com
Get components from static storage
Get components from static storage
43ba854 Johan Mattsson - johan.mattsson.m@gmail.com
Remove counter test code
Remove counter test code
6d0a194 Johan Mattsson - johan.mattsson.m@gmail.com
Fix rust init function
Fix rust init function
973b199 Johan Mattsson - johan.mattsson.m@gmail.com
Id update
Id update
e7bcccc Johan Mattsson - johan.mattsson.m@gmail.com
Update store
Update store
86d7d64 Johan Mattsson - johan.mattsson.m@gmail.com
Fix error messages
Fix error messages
c71203e Johan Mattsson - johan.mattsson.m@gmail.com
Global store
Global store
be0af7a Johan Mattsson - johan.mattsson.m@gmail.com
Update component id
Update component id
a49ec95 Johan Mattsson - johan.mattsson.m@gmail.com
More callbacks
More callbacks
a8a7119 Johan Mattsson - johan.mattsson.m@gmail.com
Update component id
Update component id
884c70d Johan Mattsson - johan.mattsson.m@gmail.com
Fix observer
Fix observer
18073d0 Johan Mattsson - johan.mattsson.m@gmail.com
Cleanup
Cleanup
161319c Johan Mattsson - johan.mattsson.m@gmail.com
Observed parameters
Observed parameters
a3ef81e Johan Mattsson - johan.mattsson.m@gmail.com
Detach web component
Detach web component
21e9bb6 Johan Mattsson - johan.mattsson.m@gmail.com
Fix incremented id
Fix incremented id
746de80 Johan Mattsson - johan.mattsson.m@gmail.com
Refacor
Refacor
8a5c4ed Johan Mattsson - johan.mattsson.m@gmail.com
Attach callback
Attach callback
cda64de Johan Mattsson - johan.mattsson.m@gmail.com
Shorter function names
Shorter function names
ba49729 Johan Mattsson - johan.mattsson.m@gmail.com
Fix and log init
Fix and log init
4a3a700 Johan Mattsson - johan.mattsson.m@gmail.com
Init functions
Init functions
36ffb93 Johan Mattsson - johan.mattsson.m@gmail.com
Rust side owner for the component
Rust side owner for the component
a9079b3 Johan Mattsson - johan.mattsson.m@gmail.com
Add global component store
Add global component store
86beb1c Johan Mattsson - johan.mattsson.m@gmail.com
Fix init state functions
Fix init state functions

The rust self for component.
f7d721a Johan Mattsson - johan.mattsson.m@gmail.com
Init state function
Init state function

Contact

Email: johan.mattsson.m@gmail.com
Mastodon: typo.social/@birdfont
GitHub: johanmattssonm
LinkedIn: johan-mattsson-2ab324112