Module: Glimmer::Web::Component

Defined Under Namespace

Modules: ClassMethods, GlimmerSupersedable

Constant Summary collapse

ADD_COMPONENT_KEYWORDS_UPON_INHERITANCE =
proc do
  class << self
    def inherited(subclass)
      Glimmer::Web::Component.add_component_keyword_to_classes_map_for(subclass)
      subclass.class_eval(&Glimmer::Web::Component::ADD_COMPONENT_KEYWORDS_UPON_INHERITANCE)
    end
  end
end
REGEX_LISTENER_OPTION_UPDATE =

<- end of class methods

/^on_(.)+_update$/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

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



520
521
522
523
524
525
526
527
528
# File 'lib/glimmer/web/component.rb', line 520

def method_missing(method_name, *args, &block)
  if can_handle_observation_request?(method_name)
    handle_observation_request(method_name, block)
  elsif markup_root.respond_to?(method_name, true)
    markup_root.send(method_name, *args, &block)
  else
    super(method_name, *args, &block)
  end
end

Instance Attribute Details

#argsObject (readonly)

Returns the value of attribute args.



270
271
272
# File 'lib/glimmer/web/component.rb', line 270

def args
  @args
end

#component_styleObject (readonly)

Returns the value of attribute component_style.



270
271
272
# File 'lib/glimmer/web/component.rb', line 270

def component_style
  @component_style
end

#default_slotObject (readonly)

Returns the value of attribute default_slot.



270
271
272
# File 'lib/glimmer/web/component.rb', line 270

def default_slot
  @default_slot
end

#eventsObject (readonly)

Returns the value of attribute events.



270
271
272
# File 'lib/glimmer/web/component.rb', line 270

def events
  @events
end

#markup_rootObject (readonly) Also known as: structure_root

Returns the value of attribute markup_root.



270
271
272
# File 'lib/glimmer/web/component.rb', line 270

def markup_root
  @markup_root
end

#optionsObject (readonly)

Returns the value of attribute options.



270
271
272
# File 'lib/glimmer/web/component.rb', line 270

def options
  @options
end

#parentObject (readonly) Also known as: parent_proxy

Returns the value of attribute parent.



270
271
272
# File 'lib/glimmer/web/component.rb', line 270

def parent
  @parent
end

#slot_elementsObject (readonly)

Returns the value of attribute slot_elements.



270
271
272
# File 'lib/glimmer/web/component.rb', line 270

def slot_elements
  @slot_elements
end

#style_blockObject (readonly)

Returns the value of attribute style_block.



270
271
272
# File 'lib/glimmer/web/component.rb', line 270

def style_block
  @style_block
end

Class Method Details

.add_component(component) ⇒ Object



203
204
205
206
# File 'lib/glimmer/web/component.rb', line 203

def add_component(component)
  component_class_to_components_map[component.class] ||= {}
  component_class_to_components_map[component.class][component.object_id] = component
end

.add_component_keyword_to_classes_map_for(component_class) ⇒ Object



179
180
181
182
183
184
# File 'lib/glimmer/web/component.rb', line 179

def add_component_keyword_to_classes_map_for(component_class)
  keywords_for_class(component_class).each do |keyword|
    Glimmer::Web::Component.component_keyword_to_classes_map[keyword] ||= []
    Glimmer::Web::Component.component_keyword_to_classes_map[keyword] << component_class
  end
end

.add_component_style(component) ⇒ Object



213
214
215
216
217
218
# File 'lib/glimmer/web/component.rb', line 213

def add_component_style(component)
  # We must not remove the head style element until all components are removed of a component class
  if Glimmer::Web::Component.component_count(component.class) == 1
    Glimmer::Web::Component.component_styles[component.class] = ComponentStyleContainer.render(parent: 'head', component: component, component_style_container_block: component.style_block)
  end
end

.any_component?(component_class) ⇒ Boolean

Returns:

  • (Boolean)


229
230
231
# File 'lib/glimmer/web/component.rb', line 229

def any_component?(component_class)
  component_class_to_components_map.has_key?(component_class)
end

.any_component_style?(component_class) ⇒ Boolean

Returns:

  • (Boolean)


233
234
235
# File 'lib/glimmer/web/component.rb', line 233

def any_component_style?(component_class)
  component_styles.has_key?(component_class)
end

.body_componentsObject



245
246
247
# File 'lib/glimmer/web/component.rb', line 245

def body_components
  components.reject {|component| component.is_a?(ComponentStyleContainer)}
end

.component_class_to_components_mapObject



258
259
260
# File 'lib/glimmer/web/component.rb', line 258

def component_class_to_components_map
  @component_class_to_components_map ||= {}
end

.component_count(component_class) ⇒ Object



237
238
239
# File 'lib/glimmer/web/component.rb', line 237

def component_count(component_class)
  component_class_to_components_map[component_class]&.size || 0
end

.component_keyword_to_classes_mapObject



191
192
193
# File 'lib/glimmer/web/component.rb', line 191

def component_keyword_to_classes_map
  @component_keyword_to_classes_map ||= reset_component_keyword_to_classes_map
end

.component_stylesObject



262
263
264
# File 'lib/glimmer/web/component.rb', line 262

def component_styles
  @component_styles ||= {}
end

.componentsObject



241
242
243
# File 'lib/glimmer/web/component.rb', line 241

def components
  component_class_to_components_map.values.map(&:values).flatten
end

.for(underscored_component_name) ⇒ Object



169
170
171
172
173
174
175
176
177
# File 'lib/glimmer/web/component.rb', line 169

def for(underscored_component_name)
  component_classes = Glimmer::Web::Component.component_keyword_to_classes_map[underscored_component_name]
  if component_classes.nil? || component_classes.empty?
    Glimmer::Config.logger.debug {"#{underscored_component_name} has no Glimmer web component class!" }
    nil
  else
    component_class = component_classes.first
  end
end

.head_componentsObject



249
250
251
# File 'lib/glimmer/web/component.rb', line 249

def head_components
  components.select {|component| component.is_a?(ComponentStyleContainer)}
end

.included(klass) ⇒ Object



154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/glimmer/web/component.rb', line 154

def included(klass)
  if !klass.ancestors.include?(GlimmerSupersedable)
    if !klass.to_s.include?('::') && Glimmer::Web::ElementProxy::ELEMENT_KEYWORDS.include?(klass.to_s.underscore)
      raise "Cannot define the Glimmer::Web::Component class \"#{klass.to_s}\" because it shadows the HTML element \"#{klass.to_s.underscore}\"! " +
            "Either rename the class (e.g. \"MyApp#{klass.to_s}\") to avoid conflicting with an existing HTML element name or " +
            "nest it within a namespace module/class (e.g. \"MyApp::#{klass.to_s}\")!"
    end
    klass.extend(ClassMethods)
    klass.include(Glimmer)
    klass.prepend(GlimmerSupersedable)
    Glimmer::Web::Component.add_component_keyword_to_classes_map_for(klass)
    klass.class_eval(&Glimmer::Web::Component::ADD_COMPONENT_KEYWORDS_UPON_INHERITANCE)
  end
end

.interpretation_stackObject



199
200
201
# File 'lib/glimmer/web/component.rb', line 199

def interpretation_stack
  @interpretation_stack ||= []
end

.keywords_for_class(component_class) ⇒ Object



186
187
188
189
# File 'lib/glimmer/web/component.rb', line 186

def keywords_for_class(component_class)
  namespaces = component_class.to_s.split(/::/).map(&:underscore).reverse
  namespaces.size.times.map { |n| namespaces[0..n].reverse.join('__') }
end

.remove_all_componentsObject



253
254
255
256
# File 'lib/glimmer/web/component.rb', line 253

def remove_all_components
  # removing body components automatically removes corresponding head components
  body_components.each(&:remove)
end

.remove_component(component) ⇒ Object



208
209
210
211
# File 'lib/glimmer/web/component.rb', line 208

def remove_component(component)
  component_class_to_components_map[component.class].delete(component.object_id)
  component_class_to_components_map.delete(component.class) if component_class_to_components_map[component.class].empty?
end

.remove_component_style(component) ⇒ Object



220
221
222
223
224
225
226
227
# File 'lib/glimmer/web/component.rb', line 220

def remove_component_style(component)
  # We must not remove the head style element until all components are removed of a component class
  if Glimmer::Web::Component.component_count(component.class) == 0 && Glimmer::Web::Component.any_component_style?(component.class)
    # TODO in the future, you would need to remove style using a jQuery call if you created head element in bulk
    Glimmer::Web::Component.component_styles[component.class].remove
    Glimmer::Web::Component.component_styles.delete(component.class)
  end
end

.reset_component_keyword_to_classes_mapObject



195
196
197
# File 'lib/glimmer/web/component.rb', line 195

def reset_component_keyword_to_classes_map
  @component_keyword_to_classes_map = {}
end

Instance Method Details

#add_custom_event_listener(observer, event) ⇒ Object



392
393
394
# File 'lib/glimmer/web/component.rb', line 392

def add_custom_event_listener(observer, event)
  custom_event_listeners_for(event) << observer
end

#add_observer(observer, attribute_or_event) ⇒ Object



378
379
380
381
382
383
384
# File 'lib/glimmer/web/component.rb', line 378

def add_observer(observer, attribute_or_event)
  if can_add_attribute_observer?(attribute_or_event)
    super(observer, attribute_or_event)
  elsif can_add_custom_event_listener?(attribute_or_event)
    add_custom_event_listener(observer, attribute_or_event)
  end
end

#attribute_setter(attribute_name) ⇒ Object



458
459
460
# File 'lib/glimmer/web/component.rb', line 458

def attribute_setter(attribute_name)
  "#{attribute_name}="
end

#bind_content(*binding_args, &content_block) ⇒ Object



488
489
490
# File 'lib/glimmer/web/component.rb', line 488

def bind_content(*binding_args, &content_block)
  @markup_root&.bind_content(*binding_args, &content_block)
end

#can_add_attribute_observer?(attribute_name) ⇒ Boolean

Returns:

  • (Boolean)


362
363
364
365
366
367
# File 'lib/glimmer/web/component.rb', line 362

def can_add_attribute_observer?(attribute_name)
  has_option?(attribute_name) ||
    has_read_write_attribute?(attribute_name) ||
    has_instance_method?(attribute_name) ||
    has_instance_method?("#{attribute_name}?")
end

#can_add_custom_event_listener?(event) ⇒ Boolean

Returns:

  • (Boolean)


374
375
376
# File 'lib/glimmer/web/component.rb', line 374

def can_add_custom_event_listener?(event)
  events.include?(event.to_sym)
end

#can_add_observer?(attribute_or_event) ⇒ Boolean

Returns:

  • (Boolean)


357
358
359
360
# File 'lib/glimmer/web/component.rb', line 357

def can_add_observer?(attribute_or_event)
  can_add_attribute_observer?(attribute_or_event) ||
    can_add_custom_event_listener?(attribute_or_event)
end

#can_handle_observation_request?(observation_request) ⇒ Boolean

Returns:

  • (Boolean)


331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/glimmer/web/component.rb', line 331

def can_handle_observation_request?(observation_request)
  observation_request = observation_request.to_s
  result = false
  if observation_request.match(REGEX_LISTENER_OPTION_UPDATE)
    property = observation_request.sub(/^on_/, '').sub(/_update$/, '')
    result = can_add_observer?(property)
  elsif observation_request.start_with?('on_')
    event = observation_request.sub(/^on_/, '')
    result = can_add_observer?(event)
  end
  result || @markup_root&.can_handle_observation_request?(observation_request)
end

#content(*args, &block) ⇒ Object

Returns content block if used as an attribute reader (no args) Otherwise, if a block is passed, it adds it as content to this Glimmer web component



494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
# File 'lib/glimmer/web/component.rb', line 494

def content(*args, &block)
  if args.empty?
    if block_given?
      Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::Web::ComponentExpression.new, self.class.keyword, &block)
    else
      @content
    end
  else
    options = args.last.is_a?(Hash) ? args.last : {}
    slot = options[:slot] || options['slot']
    slot = slot.to_sym unless slot.nil?
    if slot
      Glimmer::DSL::Engine.add_content(self, Glimmer::DSL::Web::ComponentExpression.new, self.class.keyword, slot: slot, &block)
    else
      # delegate to GUI DSL ContentExpression
      super
    end
  end
end

#custom_event_listeners_for(event) ⇒ Object



386
387
388
389
390
# File 'lib/glimmer/web/component.rb', line 386

def custom_event_listeners_for(event)
  event = event.to_sym
  @custom_event_listeners ||= {}
  @custom_event_listeners[event] ||= []
end

#data_bind(property, model_binding) ⇒ Object



472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
# File 'lib/glimmer/web/component.rb', line 472

def data_bind(property, model_binding)
  if has_option?(property) || (has_read_write_attribute?(property) && !@markup_root.respond_to?(property) && !@markup_root.respond_to?("#{property}="))
    option_binding = Glimmer::DataBinding::ModelBinding.new(self, property)
    #TODO make this options observer dependent and all similar observers in element specific data binding handlers
    option_binding.observe(model_binding)
    option_binding.call(model_binding.evaluate_property)
    data_bindings[option_binding] = model_binding
    if !model_binding.binding_options[:read_only]
      model_binding.observe(option_binding)
      model_binding.call(option_binding.evaluate_property)
    end
  else
    @markup_root&.data_bind(property, model_binding)
  end
end

#get_attribute(attribute_name) ⇒ Object



450
451
452
453
454
455
456
# File 'lib/glimmer/web/component.rb', line 450

def get_attribute(attribute_name)
  if has_instance_method?(attribute_name)
    send(attribute_name)
  else
    @markup_root.get_attribute(attribute_name)
  end
end

#handle_observation_request(observation_request, block) ⇒ Object



344
345
346
347
348
349
350
351
352
353
354
355
# File 'lib/glimmer/web/component.rb', line 344

def handle_observation_request(observation_request, block)
  observation_request = observation_request.to_s
  if observation_request.match(REGEX_LISTENER_OPTION_UPDATE)
    property = observation_request.sub(/^on_/, '').sub(/_update$/, '')
    add_observer(DataBinding::Observer.proc(&block), property) if can_add_observer?(property)
  elsif observation_request.start_with?('on_')
    event = observation_request.sub(/^on_/, '') # TODO look into eliminating duplication from above
    add_observer(DataBinding::Observer.proc(&block), event) if can_add_observer?(event)
  else
    @markup_root.handle_observation_request(observation_request, block)
  end
end

#has_attribute?(attribute_name, *args) ⇒ Boolean

Returns:

  • (Boolean)


424
425
426
427
# File 'lib/glimmer/web/component.rb', line 424

def has_attribute?(attribute_name, *args)
  has_instance_method?(attribute_setter(attribute_name)) ||
    @markup_root.has_attribute?(attribute_name, *args)
end

#has_instance_method?(method_name) ⇒ Boolean

This method ensures it has an instance method not coming from Glimmer DSL

Returns:

  • (Boolean)


442
443
444
445
446
447
448
# File 'lib/glimmer/web/component.rb', line 442

def has_instance_method?(method_name)
  # TODO this carryover code from other Glimmer DSLs doesn't seem to be needed in this DSL (and probably doesn't work in Opal anyways)
  respond_to?(method_name) &&
    !markup_root&.respond_to?(method_name) &&
    !method(method_name)&.source_location&.first&.include?('glimmer/dsl/engine.rb') &&
    !method(method_name)&.source_location&.first&.include?('glimmer/web/element_proxy.rb')
end

#has_option?(option_name) ⇒ Boolean

Returns:

  • (Boolean)


369
370
371
372
# File 'lib/glimmer/web/component.rb', line 369

def has_option?(option_name)
  normalized_option_name = option_name.to_s.to_sym
  options.keys.include?(normalized_option_name)
end

#has_read_write_attribute?(attribute) ⇒ Boolean

Returns:

  • (Boolean)


437
438
439
# File 'lib/glimmer/web/component.rb', line 437

def has_read_write_attribute?(attribute)
  respond_to?(attribute) && respond_to?("#{attribute}=")
end

#initialize(parent, args, options, &content) ⇒ Object

Raises:

  • (Glimmer::Error)


274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
# File 'lib/glimmer/web/component.rb', line 274

def initialize(parent, args, options, &content)
  Glimmer::Web::Component.add_component(self)
  Component.interpretation_stack.push(self)
  @parent = parent
  options = args.delete_at(-1) if args.is_a?(Array) && args.last.is_a?(Hash)
  if args.is_a?(Hash)
    options = args
    args = []
  end
  @slot_elements = {}
  @args = args
  options ||= {}
  @options = self.class.options.merge(options)
  @events = self.class.instance_variable_get("@events") || []
  @default_slot = self.class.instance_variable_get("@default_slot")
  @content = Util::ProcTracker.new(content) if content
#         @style_blocks = {} # TODO enable when doing bulk head rendering in the future
  execute_hooks('before_render')
  markup_block = self.class.instance_variable_get("@markup_block")
#         add_style_block
  raise Glimmer::Error, 'Invalid Glimmer web component for having no markup! Please define markup block!' if markup_block.nil?
  @markup_root = instance_exec(&markup_block)
  add_style_block
#         add_style_to_markup_root
  @markup_root.options[:parent] = options[:parent] if !options[:parent].nil?
  @parent ||= @markup_root.parent
  raise Glimmer::Error, 'Invalid Glimmer web component for having an empty markup! Please fill markup block!' if @markup_root.nil?
  if options[:render] != false
    execute_hooks('after_render')
  else
    on_render_listener = proc { execute_hooks('after_render') }
    @markup_root.handle_observation_request('on_render', on_render_listener)
  end
  
  # TODO adapt for web
  observer_registration_cleanup_listener = proc do
    observer_registrations.compact.each(&:deregister)
    observer_registrations.clear
  end
  @markup_root.handle_observation_request('on_remove', observer_registration_cleanup_listener)
  post_add_content if content.nil?
end

#inspect(basic: false) ⇒ Object



537
538
539
540
541
542
543
544
545
546
# File 'lib/glimmer/web/component.rb', line 537

def inspect(basic: false)
  keyword = self.class.keyword
  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)} #{markup_root&.keyword}##{markup_root&.element_id} #{attributes}>"
end

#local_respond_to?Object



530
# File 'lib/glimmer/web/component.rb', line 530

alias local_respond_to? respond_to_missing?

#notify_custom_event_listeners(event, *args) ⇒ Object



418
419
420
421
422
# File 'lib/glimmer/web/component.rb', line 418

def notify_custom_event_listeners(event, *args)
  custom_event_listeners_for(event).each do |listener|
    listener.call(*args)
  end
end

#notify_listeners(event, *args) ⇒ Object



410
411
412
413
414
415
416
# File 'lib/glimmer/web/component.rb', line 410

def notify_listeners(event, *args)
  if can_add_custom_event_listener?(event)
    notify_custom_event_listeners(event, *args)
  else
    @markup_root&.notify_listeners(event)
  end
end

#observer_registrationsObject

This stores observe keyword registrations of model/attribute observers



327
328
329
# File 'lib/glimmer/web/component.rb', line 327

def observer_registrations
  @observer_registrations ||= []
end

#post_add_contentObject



322
323
324
# File 'lib/glimmer/web/component.rb', line 322

def post_add_content
  Component.interpretation_stack.pop
end

#post_initialize_child(child) ⇒ Object

Subclasses may override to perform post initialization work on an added child



318
319
320
# File 'lib/glimmer/web/component.rb', line 318

def post_initialize_child(child)
  # No Op by default
end

#removeObject



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

def remove
  remove_all_listeners
  @markup_root&.remove
end

#remove_all_listenersObject



514
515
516
517
518
# File 'lib/glimmer/web/component.rb', line 514

def remove_all_listeners
  data_bindings.each do |option_binding, model_binding|
    option_binding.unregister_all_observables
  end
end

#remove_custom_event_listener(observer, event) ⇒ Object



405
406
407
408
# File 'lib/glimmer/web/component.rb', line 405

def remove_custom_event_listener(observer, event)
  event = event.to_sym
  custom_event_listeners_for(event).delete(observer) if custom_event_listeners_for(event).include?(observer)
end

#remove_observer(observer, attribute_or_event, options = {}) ⇒ Object



396
397
398
399
400
401
402
403
# File 'lib/glimmer/web/component.rb', line 396

def remove_observer(observer, attribute_or_event, options = {})
  # TODO should we removing attribute observers? when removing component?
  if can_add_attribute_observer?(attribute_or_event)
    super(observer, attribute_or_event)
  elsif can_add_custom_event_listener?(attribute_or_event)
    remove_custom_event_listener(observer, attribute_or_event)
  end
end

#render(parent: nil, custom_parent_dom_element: nil, brand_new: false) ⇒ Object



462
463
464
465
# File 'lib/glimmer/web/component.rb', line 462

def render(parent: nil, custom_parent_dom_element: nil, brand_new: false)
  # this method is defined to prevent displaying a harmless Glimmer no keyword error as an annoying useless warning
  @markup_root&.render(parent: parent, custom_parent_dom_element: custom_parent_dom_element, brand_new: brand_new)
end

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

Returns:

  • (Boolean)


531
532
533
534
535
# File 'lib/glimmer/web/component.rb', line 531

def respond_to_missing?(method_name, include_private = false)
  super(method_name, include_private) or
    can_handle_observation_request?(method_name) or
    markup_root.respond_to?(method_name, include_private)
end

#root_componentObject



548
549
550
# File 'lib/glimmer/web/component.rb', line 548

def root_component
  (markup_root&.root_parent || markup_root)&.component
end

#set_attribute(attribute_name, *args) ⇒ Object



429
430
431
432
433
434
435
# File 'lib/glimmer/web/component.rb', line 429

def set_attribute(attribute_name, *args)
  if has_instance_method?(attribute_setter(attribute_name))
    send(attribute_setter(attribute_name), *args)
  else
    @markup_root.set_attribute(attribute_name, *args)
  end
end