Skip to content

Commit

Permalink
Finalize the Custom Elements implementation.
Browse files Browse the repository at this point in the history
This fixes #82
  • Loading branch information
hmdne committed Jul 23, 2021
1 parent 504e017 commit b728839
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 32 deletions.
17 changes: 13 additions & 4 deletions examples/component/app/application.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
require "opal"
require "console"
require "browser"
require "browser/dom/mutation_observer"
require "browser/dom/element/custom"
require "browser/setup/full"

# Let's test some element before we have been initialized.
# This is so we can test the upgrading behavior.
$document.body << $document.create_element("app-mycounter")

d = DOM("<div>")
$document.body << d
d.inner_html = "<app-mycounter>"

class MyCounter < Browser::DOM::Element::Custom
# Custom Element interface:

self.observed_attributes = %w[value]
def_custom "app-mycounter"

def initialize
super
Expand Down Expand Up @@ -41,11 +46,15 @@ def increase!
def render
self.inner_html = "<h1>[#{self[:value]}]</h1>"
end

# This should come after the methods have been defined.
def_custom "app-mycounter"
end

$document.body << DOM("<app-mycounter></app-mycounter>")
$document.body << $document.create_element("app-mycounter")
$document.body << MyCounter.new
$document.body << DOM { e('app-mycounter') }

all = DOM("<h1>Increase all!</h1>")
all.on(:click) do
Expand Down
27 changes: 19 additions & 8 deletions opal/browser/dom/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,31 @@ def self.build(builder, item)

attr_reader :document, :element

NEW_PAGGIO = (Paggio::HTML.instance_method(:build!) rescue false)

def initialize(document, builder=nil, &block)
@document = document

@builder = Paggio::HTML.new(defer: true, &block)
# Compatibility issue due to an unreleased Paggio gem.
# Let's try to support both versions. When Paggio is released,
# we may remove it.

build = proc do
@builder.build!(force_call: !!builder)
@roots = @builder.each.map { |e| Builder.build(self, e) }
end
if NEW_PAGGIO
@builder = Paggio::HTML.new(defer: true, &block)

if builder
builder.extend!(@builder, &build)
build = proc do
@builder.build!(force_call: !!builder)
@roots = @builder.each.map { |e| Builder.build(self, e) }
end

if builder
builder.extend!(@builder, &build)
else
build.()
end
else
build.()
@builder = Paggio::HTML.new(&block)
@roots = @builder.each.map { |e| Builder.build(self, e) }
end
end

Expand Down
4 changes: 1 addition & 3 deletions opal/browser/dom/element.rb
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ def inspect
inspect += '.' + class_names.join('.')
end

"#<DOM::Element: #{inspect}>"
"#<#{self.class.name.gsub("Browser::","")}: #{inspect}>"
end

# @!attribute offset
Expand Down Expand Up @@ -614,5 +614,3 @@ def xpath(path)
require 'browser/dom/element/textarea'
require 'browser/dom/element/iframe'
require 'browser/dom/element/media'

require 'browser/dom/element/custom'
46 changes: 29 additions & 17 deletions opal/browser/dom/element/custom.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,12 @@ class Custom < Element

module ClassMethods
if Browser.supports? 'Custom Elements'
# Defines a new custom element
# Defines a new custom element. This should come as the last call
# in the class definition, because at this point the methods may
# be called!
#
# @opalopt uses:_dispatch_constructor,attached,detached,adopted,attribute_changed,observed_attributes
def def_custom(tag_name, base_class: nil, extends: nil)
$console.log(tag_name, base_class)
if `base_class !== nil`
elsif self.superclass == Custom
base_class = `HTMLElement`
Expand All @@ -74,19 +75,9 @@ def def_custom(tag_name, base_class: nil, extends: nil)
else customElements.define(#{tag_name}, #{@custom_class});
}
end

private def _dispatch_constructor(obj)
new(obj) if Element.native_is?(obj, self)
end
elsif Browser.supports? 'MutationObserver'
# Can we polyfill it?
def def_custom(tag_name, base_class: nil, extends: nil)
def_selector tag_name

$document.body.css(tag_name).each(&:attached)
end

MutationObserver.new do |obs|
Browser::DOM::MutationObserver.new do |obs|
obs.each do |e|
target = e.target

Expand All @@ -101,15 +92,30 @@ def def_custom(tag_name, base_class: nil, extends: nil)
end
end
end.observe($document.body, tree: true, children: true, attributes: :old)
else
# Well, we can't. Let's do what we can.
end

unless Browser.supports? 'Custom Elements'
# The polyfilled implementation. Define the selector and then
# try to upgrade the elements that are already in the document.
def def_custom(tag_name, base_class: nil, extends: nil)
def_selector tag_name

$document.body.css(tag_name).each(&:attached)
$document.body.css(tag_name).each do |elem|
elem = _dispatch_constructor(elem.to_n)
elem.attached
end
end
end

private def _dispatch_constructor(obj)
%x{
if (typeof obj.$$opal_native_cached !== 'undefined') {
delete obj.$$opal_native_cached;
}
}
new(obj)
end

# This must be defined before def_custom is called!
attr_accessor :observed_attributes

Expand All @@ -121,16 +127,22 @@ def self.included(klass)
klass.extend ClassMethods
end

# @abstract
def attached
end

# @abstract
def detached
end

# @abstract
def adopted
end

# Note: for this method to fire, you will need to
# Note: for this method to fire, you will need to define
# the observed attributes.
#
# @abstract
def attribute_changed(attr, from, to)
end

Expand Down
4 changes: 4 additions & 0 deletions opal/browser/dom/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ def ancestors(expression = nil)
NodeSet.new(parents)
end

def attached?
`#@native.isConnected`
end

alias before add_previous_sibling

# Remove the node from its parent.
Expand Down
3 changes: 3 additions & 0 deletions opal/browser/setup/full.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
require 'browser/setup/large'

require 'browser/event/all'

require 'browser/dom/builder'
require 'browser/dom/mutation_observer'
require 'browser/dom/element/custom'

require 'browser/canvas'
106 changes: 106 additions & 0 deletions spec/dom/element/custom_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
require 'spec_helper'
require 'browser/dom/element/custom'

describe Browser::DOM::Element::Custom do
before(:each) do
$scratchpad = Hash.new { false }
end

def create_custom_class(name, observed_attrs = [])
Class.new(Browser::DOM::Element::Custom) do
def initialize
super
$scratchpad[:initialized] = true
end

def attached
$scratchpad[:attached] = true
end

def detached
$scratchpad[:detached] = true
end

def adopted
$scratchpad[:adopted] = true
end

def attribute_changed(attr, from, to)
$scratchpad[:attribute_changed] = [attr, from, to]
end

self.observed_attributes = observed_attrs

def_custom name
end
end

describe "upgrades" do
html <<-HTML
<app-ex1></app-ex1>
<app-ex2></app-ex2>
<app-ex7 prop="true"></app-ex7>
HTML

it "existing elements when they have been initialized before" do
expect($document.at_css("app-ex1").class).to eq(Browser::DOM::Element)
klass = create_custom_class("app-ex1")
expect($document.at_css("app-ex1").class).to eq(klass)
expect($scratchpad[:initialized]).to be(true)
expect($scratchpad[:attached]).to be(true)
end

it "existing elements when they have not been initialized before" do
klass = create_custom_class("app-ex2")
expect($scratchpad[:initialized]).to be(true)
expect($scratchpad[:attached]).to be(true)
expect($document.at_css("app-ex2").class).to eq(klass)
end

it "and fires property update events when upgraded" do
klass = create_custom_class("app-ex7", ["prop"])
expect($scratchpad[:attribute_changed]).to eq([:prop, nil, "true"])
end
end

it "creates and handles new elements correctly" do
klass = create_custom_class("app-ex3")
elem = klass.new
expect($scratchpad[:initialized]).to be(true)
expect($scratchpad[:attached]).to be(false)
$document.body << elem
expect($scratchpad[:detached]).to be(false)
expect($scratchpad[:attached]).to be(true)
elem.remove
expect($scratchpad[:detached]).to be(true)
end

it "correctly tracks updated properties" do
klass = create_custom_class("app-ex4")
elem = klass.new
expect($scratchpad[:attribute_changed]).to be(false)
elem[:untracked] = "test"
expect($scratchpad[:attribute_changed]).to be(false)

klass = create_custom_class("app-ex5", ["tracked"])
elem = klass.new
expect($scratchpad[:attribute_changed]).to be(false)
elem[:untracked] = "test"
expect($scratchpad[:attribute_changed]).to be(false)
elem[:tracked] = "test"
expect($scratchpad[:attribute_changed]).to eq([:tracked, nil, "test"])
end

it "allows creation of custom elements in various ways" do
klass = create_custom_class("app-ex6")

elem = klass.new
expect(elem).to be_a(klass)
elem = $document.create_element("app-ex6")
expect(elem).to be_a(klass)
elem = DOM("<app-ex6>")
expect(elem).to be_a(klass)
elem = DOM { e("app-ex6") }
expect(elem).to be_a(klass)
end
end

0 comments on commit b728839

Please sign in to comment.