Class: Dommy::CustomElementRegistry

Inherits:
Object
  • Object
show all
Includes:
Bridge::Methods
Defined in:
lib/dommy/custom_elements.rb

Overview

‘window.customElements` — registry mapping custom element tag names to Ruby classes that extend `HTMLElement`. Lifecycle callbacks (`connected_callback` / `disconnected_callback` / `attribute_changed_callback` / `adopted_callback`) are invoked by the document’s mutation pipeline when registered elements are added, removed, or have observed attributes mutated.

Names must contain a hyphen per the HTML spec (e.g., ‘my-button`).

Constant Summary collapse

PCEN =

html.spec.whatwg.org/#valid-custom-element-name PCENChar — the characters allowed after the first (ASCII-lower) char: a superset of [-._0-9a-z] plus wide Unicode ranges. A valid name is ‘[a-z] PCENChar* - PCENChar*` (i.e. lower-alpha start + at least one “-”).

"\\-._0-9a-z\\u00B7\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u037D" \
"\\u037F-\\u1FFF\\u200C-\\u200D\\u203F-\\u2040\\u2070-\\u218F" \
"\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD\\u{10000}-\\u{EFFFF}"
NAME_RE =
Regexp.new("\\A[a-z][#{PCEN}]*-[#{PCEN}]*\\z")
RESERVED_NAMES =

Hyphenated names that the HTML spec reserves (SVG / MathML elements), so they are NOT valid custom element names even though they match NAME_RE.

%w[
  annotation-xml color-profile font-face font-face-src font-face-uri
  font-face-format font-face-name missing-glyph
].to_set.freeze

Instance Method Summary collapse

Methods included from Bridge::Methods

included

Constructor Details

#initialize(window) ⇒ CustomElementRegistry

Returns a new instance of CustomElementRegistry.



29
30
31
32
33
34
35
# File 'lib/dommy/custom_elements.rb', line 29

def initialize(window)
  @window = window
  # name → klass
  @definitions = {}
  # name → Array<{ resolve, reject }>
  @pending_promises = {}
end

Instance Method Details

#__js_call__(method, args) ⇒ Object



105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/dommy/custom_elements.rb', line 105

def __js_call__(method, args)
  case method
  when "define"
    define(args[0], args[1], args[2])
  when "get"
    get(args[0])
  when "whenDefined"
    when_defined(args[0])
  when "upgrade"
    upgrade(args[0])
  end
end

#__js_get__(_key) ⇒ Object



99
100
101
# File 'lib/dommy/custom_elements.rb', line 99

def __js_get__(_key)
  nil
end

#define(name, klass, _options = nil) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'lib/dommy/custom_elements.rb', line 37

def define(name, klass, _options = nil)
  key = name.to_s
  unless key.match?(NAME_RE)
    raise DOMException::SyntaxError, "#{name.inspect} is not a valid custom element name"
  end
  if RESERVED_NAMES.include?(key)
    raise DOMException::SyntaxError, "#{name.inspect} is a reserved element name"
  end

  raise DOMException::NotSupportedError, "#{key} already defined" if @definitions.key?(key)

  @definitions[key] = klass
  # Resolve any pending whenDefined() promises and re-wrap
  # already-existing nodes (upgrade).
  resolve_pending(key, klass)
  upgrade_existing(key)
  nil
end

#get(name) ⇒ Object



56
57
58
# File 'lib/dommy/custom_elements.rb', line 56

def get(name)
  @definitions[name.to_s]
end

#get_name(klass) ⇒ Object



60
61
62
63
# File 'lib/dommy/custom_elements.rb', line 60

def get_name(klass)
  @definitions.each { |k, v| return k if v == klass }
  nil
end

#upgrade(root) ⇒ Object

Walk ‘root`’s subtree and re-wrap any nodes whose tag is now registered; fires ‘connectedCallback` for each upgraded node that’s currently attached to a document tree.



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/dommy/custom_elements.rb', line 83

def upgrade(root)
  return nil unless root.respond_to?(:__dommy_backend_node__)

  walk_descendants(root.__dommy_backend_node__) do |nk|
    next unless nk.element?
    next unless @definitions.key?(nk.name)

    # Force re-wrap by clearing the document's cached wrapper.
    @window.document.__internal_reset_wrapper__(nk)
    wrapped = @window.document.wrap_node(nk)
    @window.document.__internal_notify_connected__(wrapped) if wrapped
  end

  nil
end

#when_defined(name) ⇒ Object

Returns a Dommy::PromiseValue that resolves with the registered constructor when ‘name` is defined (immediately if already so).



67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/dommy/custom_elements.rb', line 67

def when_defined(name)
  key = name.to_s
  promise = PromiseValue.new(@window)
  if (klass = @definitions[key])
    promise.fulfill(klass)
  else
    @pending_promises[key] ||= []
    @pending_promises[key] << promise
  end

  promise
end