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
#![recursion_limit="512"]
#[macro_use]
extern crate stdweb;

pub trait CustomElement {
    fn get_observable_attributes() -> Vec<&'static str> {vec![]}
    fn get_observable_properties() -> Vec<&'static str> {vec![]}
    fn get_property(element:&stdweb::web::HtmlElement,prop_name:String) -> stdweb::Value {
        js!(
             return @{element.as_ref()}[(@{String::from(prop_name)})];
         )
    }
    fn get_attribute(element:&stdweb::web::HtmlElement,attr_name:&str) -> Option<String> {
        js!(
             return @{element.as_ref()}.getAttribute(@{String::from(attr_name)});
         ).into_string()
    }
    fn set_inner_html(element:&stdweb::web::HtmlElement,text:&str) {
        js!{
            @{element.as_ref()}.innerHTML = @{text};
        };
    }
    fn created(_id:String,_element:stdweb::web::HtmlElement){}
    fn connected(_id:String,_element:stdweb::web::HtmlElement){}
    fn disconnected(_id:String,_element:stdweb::web::HtmlElement){}
    fn attribute_changed(_id:String,_element:stdweb::web::HtmlElement,_attribute_name:String,_old_value:stdweb::Value,_new_value:stdweb::Value){}
    fn property_changed(_id:String,_element:stdweb::web::HtmlElement,_attribute_name:String,_old_value:stdweb::Value,_new_value:stdweb::Value){}

    // This function allows us to define a new html element hooked up to the static members of a type
    fn register(tag_name:&str) where Self: 'static {

        // we need to use the array of attribute names we should be observing
        // and pass them in as a joined string since giving arrays to stdweb
        // isn't possible or expensive
        let observed_attributes = Self::get_observable_attributes().join(":");
        let observed_properties = Self::get_observable_properties().join(":");

        js! {
            // use a global variable that allows us to give a context for what element
            // is currently handling an event
            window.currentElement = null;

            function guid() {
              function s4() {
                return Math.floor((1 + Math.random()) * 0x10000)
                  .toString(16)
                  .substring(1);
              }
              return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
                s4() + '-' + s4() + s4() + s4();
            }

            // create a generated custom element
            class GeneratedCustomElement extends HTMLElement {
              static get observedAttributes() {return (@{observed_attributes}).split(":"); }
              static get observedProperties() {return (@{observed_properties}).split(":"); }

              constructor() {
                  super();
                  this._id = guid();
                  this._props = {};
                  window.currentElement = this;
                  (@{Self::created})(this._id,this);
                  window.currentElement = null;
                  for(let i = 0 ; i < GeneratedCustomElement.observedProperties.length; i++) {
                      let name = GeneratedCustomElement.observedProperties[i];
                      Object.defineProperty(this, name, {
                          get() { return this._props[name]; },
                          set(newValue) {
                              let oldValue = this._props[name];
                              this._props[name] = newValue;
                              (@{Self::property_changed})(this._id,this,name,this[name],newValue);
                          },
                          enumerable: true,
                          configurable: true
                        });
                  }
              }

              connectedCallback() {
                window.currentElement = this;
                (@{Self::connected})(this._id,this);
                window.currentElement = null;
              }

              disconnectedCallback() {
                window.currentElement = this;
                (@{Self::disconnected})(this._id,this);
                window.currentElement = null;
              }

              attributeChangedCallback(attributeName, oldValue, newValue) {
                window.currentElement = this;
                (@{Self::attribute_changed})(this._id,this,attributeName,oldValue,newValue);
                window.currentElement = null;
              }
            }

            // tell the dom to associate it with an html tag name
            customElements.define(@{tag_name}, GeneratedCustomElement);
        }
    }
}