Class: Glimmer::Web::ElementProxy

Inherits:
Object
  • Object
show all
Includes:
Glimmer
Defined in:
lib/glimmer/web/element_proxy.rb

Defined Under Namespace

Classes: Event

Constant Summary collapse

HTML_ELEMENT_KEYWORDS =
[
  "a", "abbr", "acronym", "address", "applet", "article", "aside", "audio",
  "basefont", "bdi", "bdo", "bgsound", "big", "blink", "blockquote", "body",
  "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "data",
  "datalist", "dd", "decorator", "details", "dfn", "dir", "div", "dl", "dt",
  "element", "embed", "fieldset", "figcaption", "figure", "font", "footer", "form", "frame",
  "frameset", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup",
  "hr", "html", "iframe", "img", "input", "isindex", "kbd", "keygen",
  "label", "legend", "li", "link", "listing", "main", "map", "marquee", "menu",
  "menuitem", "meta", "meter", "nav", "nobr", "noframes", "noscript", "object", "ol", "optgroup",
  "option", "output", "p", "param", "plaintext", "pre", "progress", "q", "rp", "rt",
  "ruby", "s", "samp", "script", "section", "select", "shadow", "source", "spacer",
  "span", "strike", "style", "summary", "table", "tbody", "td",
  "template", "textarea", "tfoot", "th", "thead", "time", "title", "tr", "track", "tt",
  "u", "ul", "var", "video", "wbr", "xmp",
]
SVG_ELEMENT_KEYWORDS =
[
  "animate", "animateMotion", "animateTransform", "circle", "clipPath", "defs", "desc", "ellipse",
  "feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix",
  "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA",
  "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode",
  "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile",
  "feTurbulence", "filter", "foreignObject", "g", "image", "line", "linearGradient", "marker",
  "mask", "metadata", "mpath", "path", "pattern", "polygon", "polyline", "radialGradient", "rect",
  "set", "stop", "svg", "switch", "symbol", "text", "textPath", "tspan", "use", "view",
].map(&:downcase)
ELEMENT_KEYWORDS =
HTML_ELEMENT_KEYWORDS + SVG_ELEMENT_KEYWORDS
HTML_VOID_ELEMENT_KEYWORDS =
%w[
  area base br col embed hr img input link meta param source track wbr
  keygen menuitem frame
]
HTML_ELEMENT_SPECIAL_ATTRIBUTES =

title is a special attribute because it matches an element tag name (needs special treatment)

['title']
GLIMMER_ATTRIBUTES =
[:parent]
PROPERTY_ALIASES =
{
  'inner_html' => 'innerHTML',
  'outer_html' => 'outerHTML',
}
CSS_CLASS_GLIMMER =
'glimmer'
FORMAT_DATETIME =

TODO ensure using when setting value on date/time fields without data-binding

'%Y-%m-%dT%H:%M'
FORMAT_DATE =

TODO ensure using when setting value on date/time fields without data-binding

'%Y-%m-%d'
FORMAT_TIME =

TODO ensure using when setting value on date/time fields without data-binding

'%H:%M'
FORMAT_MONTH =

TODO ensure using when setting value on date/time fields without data-binding

'%Y-%m'
REGEX_FORMAT_DATETIME =
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/
REGEX_FORMAT_DATE =
/^\d{4}-\d{2}-\d{2}$/
REGEX_FORMAT_TIME =
/^\d{2}:\d{2}$/
REGEX_FORMAT_MONTH =
/^\d{4}-\d{2}$/
REGEX_STYLE_SUB_PROPERTY =
/^(style)_(.*)$/
REGEX_CLASS_NAME_SUB_PROPERTY =
/^(class_name)_(.*)$/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(keyword, parent, args, block) ⇒ ElementProxy

Returns a new instance of ElementProxy.



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
199
200
# File 'lib/glimmer/web/element_proxy.rb', line 173

def initialize(keyword, parent, args, block)
  @keyword = keyword
  @parent = parent.is_a?(Glimmer::Web::Component) ? parent.markup_root : parent
  # @parent_component is the component that is the parent of an external element (not in markup {...} block) that is nested underneath (e.g. address_form { this_element })
  @parent_component = parent if parent.is_a?(Glimmer::Web::Component)
  if Component.interpretation_stack.last&.markup_root.nil?
    # @component is the one tied to this component-internal element as the markup root
    @component = Component.interpretation_stack.last
    @component&.instance_variable_set("@markup_root", self)
  else
    # @ancestor_component is the one tied to this component-internal element as an ancestor
    @ancestor_component = Component.interpretation_stack.last
  end
  @options = args.last.is_a?(Hash) ? args.last.symbolize_keys : {}
  if parent.nil?
    options[:parent] ||= Component.interpretation_stack.last&.options&.[](:parent)
    options[:render] ||= Component.interpretation_stack.last&.options&.[](:render)
    options[:bulk_render] ||= Component.interpretation_stack.last&.options&.[](:bulk_render)
  end
  @slot = options['slot'] || options[:slot]
  @slot = @slot.to_sym if @slot
  ancestor_component.slot_elements[@slot] = self if @slot && ancestor_component
  @args = args
  @block = block
  @children = []
  @parent&.post_initialize_child(self)
  render if !bulk_render? && !@rendered && render_after_create?
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args, &block) ⇒ Object



756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
# File 'lib/glimmer/web/element_proxy.rb', line 756

def method_missing(method_name, *args, &block)
  # TODO consider doing more correct checking of availability of properties/methods using native ticks
  property_name = property_name_for(method_name)
  unnormalized_property_name = unnormalized_property_name_for(method_name)
  if method_name.end_with?('=') && HTML_ELEMENT_SPECIAL_ATTRIBUTES.include?(property_name)
    if rendered?
      dom_element.attr(property_name, *args)
    else
      enqueue_post_render_method_call(method_name, *args, &block)
    end
  elsif method_name.to_s.start_with?('class_name_')
    property, sub_property = method_name.to_s.match(REGEX_CLASS_NAME_SUB_PROPERTY).to_a.drop(1)
    if args.empty?
      class_name_included(sub_property)
    else
      class_name_included(sub_property, args.first)
    end
  elsif method_name.to_s.start_with?('style_')
    property, sub_property = method_name.to_s.match(REGEX_STYLE_SUB_PROPERTY).to_a.drop(1)
    sub_property = sub_property.gsub('_', '-')
    if args.empty?
      style_property(sub_property)
    else
      style_property(sub_property, args.first) # TODO in the future, support more sophisticated forms of CSS sub-property values, like [1.px, :solid, :black] for border
    end
  elsif method_name.to_s.start_with?('on_')
    handle_observation_request(method_name, block)
  elsif dom_element.respond_to?(method_name)
    if rendered?
      dom_element.send(method_name, *args, &block)
    else
      enqueue_post_render_method_call(method_name, *args, &block)
    end
  elsif !dom_element.prop(property_name).nil? && !dom_element.prop(property_name).is_a?(Proc)
    if rendered?
      if method_name.end_with?('=')
        dom_element.prop(property_name, *args)
      else
        dom_element.prop(property_name)
      end
    else
      enqueue_post_render_method_call(method_name, *args, &block)
    end
  elsif !dom_element.prop(unnormalized_property_name).nil? && !dom_element.prop(unnormalized_property_name).is_a?(Proc)
    if rendered?
      if method_name.end_with?('=')
        dom_element.prop(unnormalized_property_name, *args)
      else
        dom_element.prop(unnormalized_property_name)
      end
    else
      enqueue_post_render_method_call(method_name, *args, &block)
    end
  elsif dom_element && dom_element.length > 0
    if rendered?
      js_args = block.nil? ? args : (args + [block])
      begin
        Native.call(dom_element, '0').method_missing(method_name.to_s.camelcase, *js_args)
      rescue Exception => e
        begin
          Native.call(dom_element, '0').method_missing(method_name.to_s, *js_args)
        rescue Exception => e
          super(method_name, *args, &block)
        end
      end
    else
      enqueue_post_render_method_call(method_name, *args, &block)
    end
  else
    super(method_name, *args, &block)
  end
end

Instance Attribute Details

#ancestor_componentObject (readonly)

Returns the value of attribute ancestor_component.



169
170
171
# File 'lib/glimmer/web/element_proxy.rb', line 169

def ancestor_component
  @ancestor_component
end

#argsObject (readonly)

Returns the value of attribute args.



169
170
171
# File 'lib/glimmer/web/element_proxy.rb', line 169

def args
  @args
end

#backgroundObject

Returns the value of attribute background.



169
170
171
# File 'lib/glimmer/web/element_proxy.rb', line 169

def background
  @background
end

#childrenObject (readonly)

Returns the value of attribute children.



169
170
171
# File 'lib/glimmer/web/element_proxy.rb', line 169

def children
  @children
end

#componentObject (readonly)

Returns the value of attribute component.



169
170
171
# File 'lib/glimmer/web/element_proxy.rb', line 169

def component
  @component
end

#enabledObject

Returns the value of attribute enabled.



169
170
171
# File 'lib/glimmer/web/element_proxy.rb', line 169

def enabled
  @enabled
end

#foregroundObject

Returns the value of attribute foreground.



169
170
171
# File 'lib/glimmer/web/element_proxy.rb', line 169

def foreground
  @foreground
end

#keywordObject (readonly)

Returns the value of attribute keyword.



169
170
171
# File 'lib/glimmer/web/element_proxy.rb', line 169

def keyword
  @keyword
end

#optionsObject (readonly)

Returns the value of attribute options.



169
170
171
# File 'lib/glimmer/web/element_proxy.rb', line 169

def options
  @options
end

#parentObject (readonly)

Returns the value of attribute parent.



169
170
171
# File 'lib/glimmer/web/element_proxy.rb', line 169

def parent
  @parent
end

#parent_componentObject (readonly)

Returns the value of attribute parent_component.



169
170
171
# File 'lib/glimmer/web/element_proxy.rb', line 169

def parent_component
  @parent_component
end

#removedObject (readonly) Also known as: removed?

Returns the value of attribute removed.



169
170
171
# File 'lib/glimmer/web/element_proxy.rb', line 169

def removed
  @removed
end

#renderedObject (readonly) Also known as: rendered?

Returns the value of attribute rendered.



169
170
171
# File 'lib/glimmer/web/element_proxy.rb', line 169

def rendered
  @rendered
end

#slotObject (readonly)

Returns the value of attribute slot.



169
170
171
# File 'lib/glimmer/web/element_proxy.rb', line 169

def slot
  @slot
end

Class Method Details

.element_type(keyword) ⇒ Object

NOTE: Avoid using this method for now as it has slow performance returns Ruby proxy class (type) that would handle this keyword



60
61
62
63
64
65
# File 'lib/glimmer/web/element_proxy.rb', line 60

def element_type(keyword)
  class_name_main = "#{keyword.camelcase(:upper)}Proxy"
  Glimmer::Web::ElementProxy.const_get(class_name_main.to_sym)
rescue NameError => e
  Glimmer::Web::ElementProxy
end

.for(keyword, parent, args, block) ⇒ Object

NOTE: Avoid using this method until we start supporting ElementProxy subclasses in which case, we must cache them to avoid the slow performance of element_type Factory Method that translates a Glimmer DSL keyword into a ElementProxy object



54
55
56
# File 'lib/glimmer/web/element_proxy.rb', line 54

def for(keyword, parent, args, block)
  element_type(keyword).new(keyword, parent, args, block)
end

.html_keyword_supported?(keyword) ⇒ Boolean

Returns:

  • (Boolean)


36
37
38
# File 'lib/glimmer/web/element_proxy.rb', line 36

def html_keyword_supported?(keyword)
  HTML_ELEMENT_KEYWORDS.include?(keyword.to_s)
end

.html_void_keyword?(keyword) ⇒ Boolean Also known as: void_keyword?

an HTML element that doesn’t need a closing tag (officially, it’s wrong to call it self-closing; it’s called void instead)

Returns:

  • (Boolean)


46
47
48
# File 'lib/glimmer/web/element_proxy.rb', line 46

def html_void_keyword?(keyword)
  HTML_VOID_ELEMENT_KEYWORDS.include?(keyword.to_s)
end

.keyword_supported?(keyword) ⇒ Boolean

Returns:

  • (Boolean)


30
31
32
33
34
# File 'lib/glimmer/web/element_proxy.rb', line 30

def keyword_supported?(keyword)
  keyword = keyword.to_s
  ELEMENT_KEYWORDS.include?(keyword) ||
    FormattingElementProxy::FORMATTING_ELEMENT_KEYWORDS.include?(keyword)
end

.max_id_number_for(name) ⇒ Object



71
72
73
# File 'lib/glimmer/web/element_proxy.rb', line 71

def max_id_number_for(name)
  @max_id_numbers[name] = max_id_numbers[name] || 0
end

.max_id_numbersObject



75
76
77
# File 'lib/glimmer/web/element_proxy.rb', line 75

def max_id_numbers
  @max_id_numbers ||= reset_max_id_numbers!
end

.next_id_number_for(name) ⇒ Object



67
68
69
# File 'lib/glimmer/web/element_proxy.rb', line 67

def next_id_number_for(name)
  @max_id_numbers[name] = max_id_number_for(name) + 1
end

.render_html(element, attributes, content = nil) ⇒ Object



91
92
93
94
95
96
97
98
99
100
101
102
# File 'lib/glimmer/web/element_proxy.rb', line 91

def render_html(element, attributes, content = nil)
  attributes = attributes.reduce('') do |output, option_pair|
    attribute, value = option_pair
    value = value.to_s.sub('"', '"').sub("'", ''')
    output += " #{attribute}=\"#{value}\""
  end
  if html_void_keyword?(element)
    "<#{element}#{attributes}>"
  else
    "<#{element}#{attributes}>#{content}</#{element}>"
  end
end

.reset_max_id_numbers!Object



79
80
81
# File 'lib/glimmer/web/element_proxy.rb', line 79

def reset_max_id_numbers!
  @max_id_numbers = {}
end

.svg_keyword_supported?(keyword) ⇒ Boolean

Returns:

  • (Boolean)


40
41
42
# File 'lib/glimmer/web/element_proxy.rb', line 40

def svg_keyword_supported?(keyword)
  SVG_ELEMENT_KEYWORDS.include?(keyword.to_s)
end

.underscored_widget_name(widget_proxy) ⇒ Object



83
84
85
# File 'lib/glimmer/web/element_proxy.rb', line 83

def underscored_widget_name(widget_proxy)
  widget_proxy.class.name.split(/::|\./).last.sub(/Proxy$/, '').underscore
end

.unrendered_dom_element(keyword) ⇒ Object



104
105
106
107
# File 'lib/glimmer/web/element_proxy.rb', line 104

def unrendered_dom_element(keyword)
  @unrendered_dom_elements ||= {}
  @unrendered_dom_elements[keyword] ||= Element["<#{keyword} />"]
end

.widget_handling_listenerObject



87
88
89
# File 'lib/glimmer/web/element_proxy.rb', line 87

def widget_handling_listener
  @@widget_handling_listener
end

Instance Method Details

#add_content_on_render(&content_block) ⇒ Object



475
476
477
478
479
480
481
# File 'lib/glimmer/web/element_proxy.rb', line 475

def add_content_on_render(&content_block)
  if rendered?
    content_block.call
  else
    content_on_render_blocks << content_block
  end
end

#add_contents_for_render_blocksObject



855
856
857
858
859
860
861
862
# File 'lib/glimmer/web/element_proxy.rb', line 855

def add_contents_for_render_blocks
  unless skip_content_on_render_blocks?
    content_on_render_blocks.each do |content_block|
      content(&content_block)
    end
  end
  children.each(&:add_contents_for_render_blocks) if bulk_render?
end

#add_css_class(css_class) ⇒ Object



554
555
556
557
558
559
560
# File 'lib/glimmer/web/element_proxy.rb', line 554

def add_css_class(css_class)
  if rendered?
    dom_element.add_class(css_class)
  else
    enqueue_post_render_method_call('add_css_class', css_class)
  end
end

#add_css_classes(css_classes_to_add) ⇒ Object



562
563
564
# File 'lib/glimmer/web/element_proxy.rb', line 562

def add_css_classes(css_classes_to_add)
  css_classes_to_add.each {|css_class| add_css_class(css_class)}
end

#add_text_content(text, on_empty: false) ⇒ Object



459
460
461
462
463
464
465
# File 'lib/glimmer/web/element_proxy.rb', line 459

def add_text_content(text, on_empty: false)
  if rendered?
    dom_element.append(text.to_s) if !on_empty || dom_element.text.to_s.empty?
  else
    enqueue_post_render_method_call('add_text_content', text, on_empty:)
  end
end

#attach(the_parent_dom_element) ⇒ Object



446
447
448
# File 'lib/glimmer/web/element_proxy.rb', line 446

def attach(the_parent_dom_element)
  the_parent_dom_element.append(@dom)
end

#bind_content(*binding_args, &content_block) ⇒ Object

Data-binds the generation of nested content to a model/property (in binding args) consider providing an option to avoid initial rendering without any changes happening



715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
# File 'lib/glimmer/web/element_proxy.rb', line 715

def bind_content(*binding_args, &content_block)
  content_binding_work = proc do |*values|
    # TODO in the future, consider optimizing code by diffing content if that makes sense (e.g. using opal-virtual-dom)
    # To do so, we must avoid generating new content with new unique IDs/Classes and only append the new IDs classes after mounting
    # TODO consider optimizing remove performance by doing clear instead and removing listeners separately
    children.dup.each { |child| child.remove }
    content(bulk_render: true, &content_block)
    if bulk_render? && rendered?
      self.inner_html = children_dom_content
      children.each(&:mark_rendered)
      children.each(&:invoke_post_render_method_calls)
      children.each(&:handle_observation_requests)
      children.each(&:add_contents_for_render_blocks)
      children.each(&:notify_on_render_listeners)
    end
  end
  model_binding_observer = Glimmer::DataBinding::ModelBinding.new(*binding_args)
  content_binding_observer = Glimmer::DataBinding::Observer.proc(&content_binding_work)
  content_binding_observer_registrations << content_binding_observer.observe(model_binding_observer)
  content_binding_work.call # TODO inspect if we need to pass args here (from observed attributes) [but it's simpler not to pass anything at first]
end

#build_dom(layout: true) ⇒ Object



483
484
485
486
# File 'lib/glimmer/web/element_proxy.rb', line 483

def build_dom(layout: true)
  # TODO consider passing parent element instead and having table item include a table cell widget only for opal
  @dom = dom # TODO unify how to build dom for most widgets based on element, id, and name (class)
end

#bulk_render?Boolean

Returns:

  • (Boolean)


206
207
208
# File 'lib/glimmer/web/element_proxy.rb', line 206

def bulk_render?
  options[:bulk_render] != false && (@parent.nil? || @parent.bulk_render?)
end

#can_handle_observation_request?(keyword) ⇒ Boolean

Returns:

  • (Boolean)


622
623
624
625
626
627
628
629
630
631
632
633
634
# File 'lib/glimmer/web/element_proxy.rb', line 622

def can_handle_observation_request?(keyword)
  # TODO sort this out for Opal
  keyword = keyword.to_s
  keyword.start_with?('on')
#         if keyword.start_with?('on_swt_')
#           constant_name = keyword.sub(/^on_swt_/, '')
#           SWTProxy.has_constant?(constant_name)
#         elsif keyword.start_with?('on_')
#           # event = keyword.sub(/^on_/, '')
#           # can_add_listener?(event) || can_handle_drag_observation_request?(keyword) || can_handle_drop_observation_request?(keyword)
#           true # TODO filter by valid listeners only in the future
#         end
end

#children_dom_contentObject



500
501
502
# File 'lib/glimmer/web/element_proxy.rb', line 500

def children_dom_content
  children.map(&:dom).join
end

#class_name=(*values) ⇒ Object Also known as: classes=



543
544
545
546
547
548
549
550
551
# File 'lib/glimmer/web/element_proxy.rb', line 543

def class_name=(*values)
  if rendered?
    values = normalize_class_name(values).split(' ')
    new_class_name = (base_css_classes + values).uniq.compact.join(' ')
    dom_element.prop('className', new_class_name)
  else
    enqueue_post_render_method_call('class_name=', *values)
  end
end

#class_name_included(one_class_name, value = nil) ⇒ Object



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
# File 'lib/glimmer/web/element_proxy.rb', line 354

def class_name_included(one_class_name, value = nil)
  if rendered?
    if value.nil?
      class_name.include?(one_class_name)
    else
      if value
        add_css_class(one_class_name)
      else
        remove_css_class(one_class_name)
      end
    end
  else
    enqueue_args = ['class_name_included', one_class_name]
    enqueue_args << value unless value.nil?
    enqueue_post_render_method_call(*enqueue_args)
  end
end

#clear_css_classesObject



578
579
580
# File 'lib/glimmer/web/element_proxy.rb', line 578

def clear_css_classes
  css_classes.each {|css_class| remove_css_class(css_class)}
end

#content(bulk_render: false, &block) ⇒ Object



519
520
521
522
523
524
525
# File 'lib/glimmer/web/element_proxy.rb', line 519

def content(bulk_render: false, &block)
  original_bulk_render = options[:bulk_render]
  options[:bulk_render] = bulk_render if rendered?
  return_value = Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::Web::ElementExpression.new, keyword, &block)
  options[:bulk_render] = original_bulk_render if rendered?
  return_value
end

#content_binding_observer_registrationsObject



737
738
739
# File 'lib/glimmer/web/element_proxy.rb', line 737

def content_binding_observer_registrations
  @content_binding_observer_registrations ||= []
end

#content_on_render_blocksObject



467
468
469
# File 'lib/glimmer/web/element_proxy.rb', line 467

def content_on_render_blocks
  @content_on_render_blocks ||= []
end

#css_classesObject



230
231
232
# File 'lib/glimmer/web/element_proxy.rb', line 230

def css_classes
  dom_element.attr('class').to_s.split if rendered?
end

#data_bind(property, model_binding) ⇒ Object



687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
# File 'lib/glimmer/web/element_proxy.rb', line 687

def data_bind(property, model_binding)
  element_binding_read_translator = value_converters_for_input_type(type)&.[](:model_to_view)
  element_binding = DataBinding::ElementBinding.new(self, property, translator: element_binding_read_translator)
  #TODO make this options observer dependent and all similar observers in element specific data binding handlers
  element_binding.observe(model_binding)
  element_binding.call(model_binding.evaluate_property)
  data_bindings[element_binding] = model_binding
  if !model_binding.binding_options[:read_only]
    # TODO add guards against nil cases for hash below
    listener_keyword = data_binding_listener_for_element_and_property(keyword, property)
    if listener_keyword
      data_binding_read_listener = lambda do |event|
        view_property_value = send(property)
        element_binding_write_translator = value_converters_for_input_type(type)&.[](:view_to_model)
        is_date_time = ['date', 'time', 'datetime-local', 'month'].include?(options['type'])
        converted_view_property_value = element_binding_write_translator&.call(view_property_value, model_binding.evaluate_property)
        if !is_date_time
          converted_view_property_value ||= view_property_value
        end
        model_binding.call(converted_view_property_value)
      end
      handle_observation_request(listener_keyword, data_binding_read_listener)
    end
  end
end

#data_binding_element_keyword_to_property_listener_mapObject



886
887
888
889
890
891
892
893
894
895
896
897
898
899
# File 'lib/glimmer/web/element_proxy.rb', line 886

def data_binding_element_keyword_to_property_listener_map
  @data_binding_element_keyword_to_property_listener_map ||= {
    'input' => {
      'value' => 'oninput',
      'checked' => 'oninput',
    },
    'select' => {
      'value' => 'onchange',
    },
    'textarea' => {
      'value' => 'oninput',
    },
  }
end

#data_binding_listener_for_element_and_property(element_keyword, property) ⇒ Object



878
879
880
# File 'lib/glimmer/web/element_proxy.rb', line 878

def data_binding_listener_for_element_and_property(element_keyword, property)
  data_binding_property_listener_map_for_element(element_keyword)[property]
end

#data_binding_property_listener_map_for_element(element_keyword) ⇒ Object



882
883
884
# File 'lib/glimmer/web/element_proxy.rb', line 882

def data_binding_property_listener_map_for_element(element_keyword)
  data_binding_element_keyword_to_property_listener_map[element_keyword] || {}
end

#data_bindingsObject



675
676
677
# File 'lib/glimmer/web/element_proxy.rb', line 675

def data_bindings
  @data_bindings ||= {}
end

#domObject Also known as: to_s



488
489
490
491
492
493
494
495
496
# File 'lib/glimmer/web/element_proxy.rb', line 488

def dom
  # TODO auto-convert known glimmer attributes like parent to data attributes like data-parent
  # TODO check if we need to avoid rendering content block if no content is available
  @dom ||= begin
    content = args.first.is_a?(String) ? args.first : ''
    content += children_dom_content if bulk_render?
    ElementProxy.render_html(keyword, html_options, content)
  end
end

#dom_elementObject



582
583
584
585
586
587
588
589
590
# File 'lib/glimmer/web/element_proxy.rb', line 582

def dom_element
  if rendered?
    # TODO consider making this pick an element in relation to its parent, allowing unhooked dom elements to be built if needed (unhooked to the visible page dom)
    Document.find(selector)
  else
    # Using a fill-in dom element until self is rendered
    ElementProxy.unrendered_dom_element(keyword)
  end
end

#elementObject

Root element representing widget. Must be overridden by subclasses if different from div



289
290
291
# File 'lib/glimmer/web/element_proxy.rb', line 289

def element
  keyword
end

#element_idObject

element ID is used as a css class to identify the element. It is intentionally not set as the actual HTML element ID to let software engineers specify their own IDs if they wanted



539
540
541
# File 'lib/glimmer/web/element_proxy.rb', line 539

def element_id
  @element_id ||= "#{CSS_CLASS_GLIMMER}-#{name}-#{ElementProxy.next_id_number_for(self.class.to_s)}"
end

#enqueue_post_render_method_call(method_name, *args, &block) ⇒ Object



833
834
835
836
# File 'lib/glimmer/web/element_proxy.rb', line 833

def enqueue_post_render_method_call(method_name, *args, &block)
  post_render_method_calls << [method_name, args, block]
  nil
end

#event_handling_suspended?Boolean

Returns:

  • (Boolean)


610
611
612
# File 'lib/glimmer/web/element_proxy.rb', line 610

def event_handling_suspended?
  @event_handling_suspended
end

#event_listener_proxiesObject



598
599
600
# File 'lib/glimmer/web/element_proxy.rb', line 598

def event_listener_proxies
  @event_listener_proxies ||= []
end

#find_ancestor(include_self: false, &condition) ⇒ Object



310
311
312
313
314
315
316
317
318
# File 'lib/glimmer/web/element_proxy.rb', line 310

def find_ancestor(include_self: false, &condition)
  current_element_proxy = self
  return current_element_proxy if include_self && condition.call(current_element_proxy)
  until current_element_proxy.parent.nil?
    current_element_proxy = current_element_proxy.parent
    return current_element_proxy if condition.call(current_element_proxy)
  end
  nil
end

#handle_observation_request(keyword, original_event_listener) ⇒ Object



636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
# File 'lib/glimmer/web/element_proxy.rb', line 636

def handle_observation_request(keyword, original_event_listener)
  if rendered?
    if keyword.start_with?('on_') && !['on_render', 'on_remove'].include?(keyword.to_s)
      (ancestor_component || component)&.handle_observation_request(keyword, original_event_listener)
    else
      listener = ListenerProxy.new(
        element: self,
        selector: selector,
        dom_element: dom_element,
        event_attribute: keyword,
        original_event_listener: original_event_listener,
      )
      listener.register
      listeners_for(keyword) << listener
      listener
    end
  else
    enqueue_post_render_method_call('handle_observation_request', keyword, original_event_listener)
  end
end

#handle_observation_requestsObject



846
847
848
849
850
851
852
853
# File 'lib/glimmer/web/element_proxy.rb', line 846

def handle_observation_requests
  observation_requests&.each do |keyword, event_listener_set|
    event_listener_set.each do |event_listener|
      handle_observation_request(keyword, event_listener)
    end
  end
  children.each(&:handle_observation_requests) if bulk_render?
end

#html?Boolean

Returns:

  • (Boolean)


234
235
236
# File 'lib/glimmer/web/element_proxy.rb', line 234

def html?
  ElementProxy.html_keyword_supported?(keyword)
end

#html_optionsObject



504
505
506
507
508
509
510
511
512
513
514
515
516
517
# File 'lib/glimmer/web/element_proxy.rb', line 504

def html_options
  body_class = (base_css_classes + css_classes.to_a).uniq.compact.join(' ')
  html_options = options.dup
  GLIMMER_ATTRIBUTES.each do |attribute|
    next unless html_options.include?(attribute)
    data_normalized_attribute = attribute.split('_').join('-')
    html_options["data-#{data_normalized_attribute}"] = html_options.delete(attribute)
  end
  html_options[:class] ||= ''
  html_options[:class] = "#{normalize_class_name(html_options.delete('class') || html_options.delete(:class))} #{body_class}".strip
  html_options[:style] = normalize_style(html_options.delete('style') || html_options.delete(:style))
  html_options['data-turbo'] = 'false' if parent.nil?
  html_options
end

#html_void?Boolean Also known as: void?

Returns:

  • (Boolean)


242
243
244
# File 'lib/glimmer/web/element_proxy.rb', line 242

def html_void?
  ElementProxy.html_void_keyword?(keyword)
end

#input_value_convertersObject



905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
# File 'lib/glimmer/web/element_proxy.rb', line 905

def input_value_converters
  @input_value_converters ||= {
    'number' => {
      model_to_view: -> (value, old_value) { value.to_s },
      view_to_model: -> (value, old_value) {
        value.include?('.') ? value.to_f : value.to_i
      },
    },
    'range' => {
      model_to_view: -> (value, old_value) { value.to_s },
      view_to_model: -> (value, old_value) {
        value.include?('.') ? value.to_f : value.to_i
      },
    },
    'datetime-local' => {
      model_to_view: -> (value, old_value) {
        if value.respond_to?(:strftime)
          value.strftime(FORMAT_DATETIME)
        elsif value.is_a?(String) && valid_js_date_string?(value)
          value
        elsif value.to_s.empty?
          ''
        else # keep old value if a bad model attribute value was set
          old_value&.strftime(FORMAT_DATETIME)
        end
      },
      view_to_model: -> (value, old_value) {
        if value.to_s.empty?
          nil
        else
          date = Native(`new Date(Date.parse(#{value}))`)
          year = Native.call(date, 'getFullYear')
          month = Native.call(date, 'getMonth') + 1
          day = Native.call(date, 'getDate')
          hour = Native.call(date, 'getHours')
          minute = Native.call(date, 'getMinutes')
          Time.new(year, month, day, hour, minute)
        end
      },
    },
    'date' => {
      model_to_view: -> (value, old_value) {
        if value.respond_to?(:strftime)
          value.strftime(FORMAT_DATE)
        elsif value.is_a?(String) && valid_js_date_string?(value)
          value
        elsif value.to_s.empty?
          ''
        else # keep old value if a bad model attribute value was set
          old_value&.strftime(FORMAT_DATE)
        end
      },
      view_to_model: -> (value, old_value) {
        if value.to_s.empty?
          nil
        else
          year, month, day = value.split('-')
          if !old_value.to_s.empty?
            Time.new(year, month, day, old_value.hour, old_value.min)
          else
            Time.new(year, month, day)
          end
        end
      },
    },
    'time' => {
      model_to_view: -> (value, old_value) {
        if value.respond_to?(:strftime)
          value.strftime(FORMAT_TIME)
        elsif value.is_a?(String) && valid_js_date_string?(value)
          value
        elsif value.to_s.empty?
          ''
        else # keep old value if a bad model attribute value was set
          old_value&.strftime(FORMAT_TIME)
        end
      },
      view_to_model: -> (value, old_value) {
        if value.to_s.empty?
          nil
        else
          hour, minute = value.split(':')
          if !old_value.to_s.empty?
            Time.new(old_value.year, old_value.month, old_value.day, hour, minute)
          else
            now = Time.now
            Time.new(now.year, now.month, now.day, hour, minute)
          end
        end
      },
    },
    'month' => {
      model_to_view: -> (value, old_value) {
        if value.respond_to?(:strftime)
          value.strftime(FORMAT_MONTH)
        elsif value.is_a?(String) && valid_js_date_string?(value)
          value
        elsif value.to_s.empty?
          ''
        else # keep old value if a bad model attribute value was set
          old_value&.strftime(FORMAT_MONTH)
        end
      },
      view_to_model: -> (value, old_value) {
        if value.to_s.empty?
          nil
        else
          year, month = value.split('-')
          if !old_value.to_s.empty?
            Time.new(year, month, old_value.day, old_value.hour, old_value.min)
          else
            Time.new(year, month)
          end
        end
      },
    },
  }
end

#inspect(basic: false) ⇒ Object



1024
1025
1026
1027
1028
1029
1030
1031
1032
# File 'lib/glimmer/web/element_proxy.rb', line 1024

def inspect(basic: false)
  if basic
    attributes = {keyword:, args:}
  else
    parent_inspect = parent.is_a?(Glimmer::Web::ElementProxy) || parent.is_a?(Glimmer::Web::Component) ? parent.inspect(basic: true) : parent.inspect
    attributes = {keyword:, args:, parent: parent_inspect}
  end
  "#<#{self.class}:0x#{object_id.to_s(16)} #{keyword}##{element_id} #{attributes}>"
end

#invoke_post_render_method_callsObject



838
839
840
841
842
843
844
# File 'lib/glimmer/web/element_proxy.rb', line 838

def invoke_post_render_method_calls
  return unless rendered?
  post_render_method_calls.each do |method_name, args, block|
    send(method_name, *args, &block)
  end
  children.each(&:invoke_post_render_method_calls) if bulk_render?
end

#listenersObject



614
615
616
# File 'lib/glimmer/web/element_proxy.rb', line 614

def listeners
  @listeners ||= {}
end

#listeners_for(listener_event) ⇒ Object



618
619
620
# File 'lib/glimmer/web/element_proxy.rb', line 618

def listeners_for(listener_event)
  listeners[listener_event.to_s] ||= []
end

#mark_renderedObject



454
455
456
457
# File 'lib/glimmer/web/element_proxy.rb', line 454

def mark_rendered
  @rendered = true
  children.each(&:mark_rendered) if bulk_render?
end

#nameObject



532
533
534
# File 'lib/glimmer/web/element_proxy.rb', line 532

def name
  self.class.name.split('::').last.underscore.sub(/_proxy$/, '').gsub('_', '-')
end

#notify_listeners(event) ⇒ Object



664
665
666
667
668
# File 'lib/glimmer/web/element_proxy.rb', line 664

def notify_listeners(event)
  listeners_for(event).each do |listener|
    listener.original_event_listener.call(EventProxy.new(listener: listener))
  end
end

#notify_on_render_listenersObject



670
671
672
673
# File 'lib/glimmer/web/element_proxy.rb', line 670

def notify_on_render_listeners
  notify_listeners('on_render')
  children.each(&:notify_on_render_listeners) if bulk_render?
end

#observation_request_to_event_mappingObject

Subclasses must override with their own mappings



528
529
530
# File 'lib/glimmer/web/element_proxy.rb', line 528

def observation_request_to_event_mapping
  {}
end

#observation_requestsObject

TODO consider adding a default #dom method implementation for the common case, automatically relying on #element and other methods to build the dom html



594
595
596
# File 'lib/glimmer/web/element_proxy.rb', line 594

def observation_requests
  @observation_requests ||= {}
end

#parent_dom_elementObject



407
408
409
410
411
412
413
414
415
416
417
418
419
# File 'lib/glimmer/web/element_proxy.rb', line 407

def parent_dom_element
  if parent
    parent.dom_element
  else
    options[:parent] ||= 'body'
    the_element = Document.find(options[:parent])
    if the_element.length == 0
      options[:parent] = 'body'
      the_element = Document.find('body')
    end
    the_element
  end
end

#parent_selectorObject



403
404
405
# File 'lib/glimmer/web/element_proxy.rb', line 403

def parent_selector
  @parent&.selector
end

#parentsObject Also known as: ancestors



299
300
301
302
303
304
305
306
307
# File 'lib/glimmer/web/element_proxy.rb', line 299

def parents
  parents_array = []
  current_widget = self
  until current_widget.parent.nil?
    current_widget = current_widget.parent
    parents_array << current_widget
  end
  parents_array
end

#post_add_contentObject

Executes at the closing of a parent widget curly braces after all children/properties have been added/set



226
227
228
# File 'lib/glimmer/web/element_proxy.rb', line 226

def post_add_content
  render if bulk_render? && @parent.nil? && !rendered?
end

#post_initialize_child(child) ⇒ Object

Executes for the parent of a child that just got added



215
216
217
218
# File 'lib/glimmer/web/element_proxy.rb', line 215

def post_initialize_child(child)
  @children << child
  child.render if !bulk_render? && !render_after_create?
end

#post_remove_child(child) ⇒ Object

Executes for the parent of a child that just got removed



221
222
223
# File 'lib/glimmer/web/element_proxy.rb', line 221

def post_remove_child(child)
  @children.delete(child)
end

#post_render_method_callsObject



829
830
831
# File 'lib/glimmer/web/element_proxy.rb', line 829

def post_render_method_calls
  @post_render_method_calls ||= []
end


320
321
322
323
# File 'lib/glimmer/web/element_proxy.rb', line 320

def print
  `window.print()`
  true
end

#property_name_for(method_name) ⇒ Object



864
865
866
867
# File 'lib/glimmer/web/element_proxy.rb', line 864

def property_name_for(method_name)
  attribute_name = method_name.end_with?('=') ? method_name.to_s[0...-1] : method_name.to_s
  PROPERTY_ALIASES[attribute_name] || attribute_name.camelcase
end

#reattach(old_element) ⇒ Object



450
451
452
# File 'lib/glimmer/web/element_proxy.rb', line 450

def reattach(old_element)
  old_element.replace_with(@dom)
end

#removeObject



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
# File 'lib/glimmer/web/element_proxy.rb', line 247

def remove
  return if @removed
  on_remove_listeners = listeners_for('on_remove').dup
  if rendered?
    @children.dup.each do |child|
      child.remove
    end
    remove_all_listeners
    dom_element.remove
  end
  parent&.post_remove_child(self)
  if component
    Glimmer::Web::Component.remove_component(component)
    component.remove_style_block
  end
  @removed = true
  on_remove_listeners.each do |listener|
    listener.original_event_listener.call(EventProxy.new(listener: listener))
  end
end

#remove_all_listenersObject



268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/glimmer/web/element_proxy.rb', line 268

def remove_all_listeners
  listeners.each do |event, event_listeners|
    event_listeners.dup.each(&:unregister)
  end
  listeners.clear
  data_bindings.each do |element_binding, model_binding|
    element_binding.unregister_all_observables
  end
  data_bindings.clear
  content_binding_observer_registrations.each do |observer_registration|
    observer_registration.unregister
  end
  content_binding_observer_registrations.clear
end

#remove_css_class(css_class) ⇒ Object



566
567
568
569
570
571
572
# File 'lib/glimmer/web/element_proxy.rb', line 566

def remove_css_class(css_class)
  if rendered?
    dom_element.remove_class(css_class)
  else
    enqueue_post_render_method_call('remove_css_class', css_class)
  end
end

#remove_css_classes(css_classes_to_remove) ⇒ Object



574
575
576
# File 'lib/glimmer/web/element_proxy.rb', line 574

def remove_css_classes(css_classes_to_remove)
  css_classes_to_remove.each {|css_class| remove_css_class(css_class)}
end

#remove_event_listener_proxiesObject



657
658
659
660
661
662
# File 'lib/glimmer/web/element_proxy.rb', line 657

def remove_event_listener_proxies
  event_listener_proxies.each do |event_listener_proxy|
    event_listener_proxy.unregister
  end
  event_listener_proxies.clear
end

#render(parent: nil, custom_parent_dom_element: nil, brand_new: false) ⇒ Object Also known as: rerender



421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
# File 'lib/glimmer/web/element_proxy.rb', line 421

def render(parent: nil, custom_parent_dom_element: nil, brand_new: false)
  parent_selector = parent
  options[:parent] = parent_selector if !parent_selector.to_s.empty?
  if !options[:parent].to_s.empty?
    # ensure element is orphaned as it is becoming a top-level root element
    @parent&.post_remove_child(self)
    @parent = nil
  end
  the_parent_dom_element = custom_parent_dom_element || parent_dom_element
  brand_new ||= @dom.nil? || !options[:parent].to_s.empty? || (old_element = dom_element).empty?
  build_dom(layout: !custom_parent_dom_element) # TODO handle custom parent layout by passing parent instead of parent dom element
  if brand_new
    attach(the_parent_dom_element)
  else
    reattach(old_element)
  end
  mark_rendered
  invoke_post_render_method_calls if bulk_render?
  handle_observation_requests
  children.each(&:render) if !bulk_render? && !render_after_create?
  add_contents_for_render_blocks
  notify_on_render_listeners
end

#render_after_create?Boolean

Returns:

  • (Boolean)


210
211
212
# File 'lib/glimmer/web/element_proxy.rb', line 210

def render_after_create?
  options[:render] != false && (@parent.nil? || @parent.render_after_create?)
end

#respond_to_missing?(method_name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


741
742
743
744
745
746
747
748
749
750
751
752
753
754
# File 'lib/glimmer/web/element_proxy.rb', line 741

def respond_to_missing?(method_name, include_private = false)
  # TODO consider doing more correct checking of availability of properties/methods using native ticks
  property_name = property_name_for(method_name)
  unnormalized_property_name = unnormalized_property_name_for(method_name)
  super(method_name, include_private) ||
    (dom_element && dom_element.length > 0 && Native.call(dom_element, '0').respond_to?(method_name.to_s.camelcase, include_private)) ||
    (dom_element && dom_element.length > 0 && Native.call(dom_element, '0').respond_to?(method_name.to_s, include_private)) ||
    dom_element.respond_to?(method_name, include_private) ||
    (!dom_element.prop(property_name).nil? && !dom_element.prop(property_name).is_a?(Proc)) ||
    (!dom_element.prop(unnormalized_property_name).nil? && !dom_element.prop(unnormalized_property_name).is_a?(Proc)) ||
    method_name.to_s.start_with?('on_') ||
    method_name.to_s.start_with?('style_') ||
    method_name.to_s.start_with?('class_name_')
end

#resume_event_handlingObject



606
607
608
# File 'lib/glimmer/web/element_proxy.rb', line 606

def resume_event_handling
  @event_handling_suspended = false
end

#selectorObject

Subclasses can override with their own selector



284
285
286
# File 'lib/glimmer/web/element_proxy.rb', line 284

def selector
  ".#{element_id}"
end

#shellObject



293
294
295
296
297
# File 'lib/glimmer/web/element_proxy.rb', line 293

def shell
  current_widget = self
  current_widget = current_widget.parent until current_widget.parent.nil?
  current_widget
end

#skip_content_on_render_blocks?Boolean

Returns:

  • (Boolean)


471
472
473
# File 'lib/glimmer/web/element_proxy.rb', line 471

def skip_content_on_render_blocks?
  false
end

#style(value = null) ⇒ Object Also known as: style=



372
373
374
375
376
377
378
379
380
381
382
383
384
385
# File 'lib/glimmer/web/element_proxy.rb', line 372

def style(value = null)
  if rendered?
    if value.nil?
      dom_element.attr('style')
    else
      value = normalize_style(value)
      dom_element.attr('style', value)
    end
  else
    enqueue_args = ['style']
    enqueue_args << value unless value.nil?
    enqueue_post_render_method_call(*enqueue_args)
  end
end

#style_property(property, value = nil) ⇒ Object



388
389
390
391
392
393
394
395
396
397
398
399
400
401
# File 'lib/glimmer/web/element_proxy.rb', line 388

def style_property(property, value = nil)
  if rendered?
    property = property.to_s.gsub('_', '-')
    if value.nil?
      dom_element.css(property)
    else
      dom_element.css(property, value)
    end
  else
    enqueue_args = ['style_property', property]
    enqueue_args << value unless value.nil?
    enqueue_post_render_method_call(*enqueue_args)
  end
end

#suspend_event_handlingObject



602
603
604
# File 'lib/glimmer/web/element_proxy.rb', line 602

def suspend_event_handling
  @event_handling_suspended = true
end

#svg?Boolean

Returns:

  • (Boolean)


238
239
240
# File 'lib/glimmer/web/element_proxy.rb', line 238

def svg?
  ElementProxy.svg_keyword_supported?(keyword)
end

#swt_widgetObject



873
874
875
876
# File 'lib/glimmer/web/element_proxy.rb', line 873

def swt_widget
  # only added for compatibility/adaptibility with Glimmer DSL for SWT
  self
end

#typeObject



679
680
681
682
683
684
685
# File 'lib/glimmer/web/element_proxy.rb', line 679

def type
  if rendered?
    super
  else
    options[:type] || 'text'
  end
end

#unnormalized_property_name_for(method_name) ⇒ Object



869
870
871
# File 'lib/glimmer/web/element_proxy.rb', line 869

def unnormalized_property_name_for(method_name)
  method_name.end_with?('=') ? method_name.to_s[0...-1] : method_name.to_s
end

#value_converters_for_input_type(input_type) ⇒ Object



901
902
903
# File 'lib/glimmer/web/element_proxy.rb', line 901

def value_converters_for_input_type(input_type)
  input_value_converters[input_type]
end