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



532
533
534
535
536
537
538
539
540
# File 'lib/glimmer/web/component.rb', line 532

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.



283
284
285
# File 'lib/glimmer/web/component.rb', line 283

def args
  @args
end

#component_styleObject (readonly)

Returns the value of attribute component_style.



283
284
285
# File 'lib/glimmer/web/component.rb', line 283

def component_style
  @component_style
end

#default_slotObject (readonly)

Returns the value of attribute default_slot.



283
284
285
# File 'lib/glimmer/web/component.rb', line 283

def default_slot
  @default_slot
end

#eventsObject (readonly)

Returns the value of attribute events.



283
284
285
# File 'lib/glimmer/web/component.rb', line 283

def events
  @events
end

#markup_rootObject (readonly)

Returns the value of attribute markup_root.



283
284
285
# File 'lib/glimmer/web/component.rb', line 283

def markup_root
  @markup_root
end

#optionsObject (readonly)

Returns the value of attribute options.



283
284
285
# File 'lib/glimmer/web/component.rb', line 283

def options
  @options
end

#parentObject (readonly) Also known as: parent_proxy

Returns the value of attribute parent.



283
284
285
# File 'lib/glimmer/web/component.rb', line 283

def parent
  @parent
end

#slot_elementsObject (readonly)

Returns the value of attribute slot_elements.



283
284
285
# File 'lib/glimmer/web/component.rb', line 283

def slot_elements
  @slot_elements
end

#style_blockObject (readonly)

Returns the value of attribute style_block.



283
284
285
# File 'lib/glimmer/web/component.rb', line 283

def style_block
  @style_block
end

Class Method Details

.add_component(component) ⇒ Object



216
217
218
219
# File 'lib/glimmer/web/component.rb', line 216

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



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

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



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

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)


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

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

.any_component_style?(component_class) ⇒ Boolean

Returns:

  • (Boolean)


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

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

.body_componentsObject



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

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

.component_class_to_components_mapObject



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

def component_class_to_components_map
  @component_class_to_components_map ||= {}
end

.component_count(component_class) ⇒ Object



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

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

.component_keyword_to_classes_mapObject



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

def component_keyword_to_classes_map
  @component_keyword_to_classes_map ||= reset_component_keyword_to_classes_map
end

.component_stylesObject



275
276
277
# File 'lib/glimmer/web/component.rb', line 275

def component_styles
  @component_styles ||= {}
end

.componentsObject



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

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

.for(underscored_component_name) ⇒ Object



182
183
184
185
186
187
188
189
190
# File 'lib/glimmer/web/component.rb', line 182

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



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

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

.included(klass) ⇒ Object



172
173
174
175
176
177
178
179
180
# File 'lib/glimmer/web/component.rb', line 172

def included(klass)
  if !klass.ancestors.include?(GlimmerSupersedable)
    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



212
213
214
# File 'lib/glimmer/web/component.rb', line 212

def interpretation_stack
  @interpretation_stack ||= []
end

.keywords_for_class(component_class) ⇒ Object



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

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



266
267
268
269
# File 'lib/glimmer/web/component.rb', line 266

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

.remove_component(component) ⇒ Object



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

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



233
234
235
236
237
238
239
240
# File 'lib/glimmer/web/component.rb', line 233

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



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

def reset_component_keyword_to_classes_map
  @component_keyword_to_classes_map = {}
end

Instance Method Details

#add_custom_event_listener(observer, event) ⇒ Object



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

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

#add_observer(observer, attribute_or_event) ⇒ Object



390
391
392
393
394
395
396
# File 'lib/glimmer/web/component.rb', line 390

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



470
471
472
# File 'lib/glimmer/web/component.rb', line 470

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

#bind_content(*binding_args, &content_block) ⇒ Object



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

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)


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

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)


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

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

#can_add_observer?(attribute_or_event) ⇒ Boolean

Returns:

  • (Boolean)


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

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)


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

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



506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
# File 'lib/glimmer/web/component.rb', line 506

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



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

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

#data_bind(property, model_binding) ⇒ Object



484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
# File 'lib/glimmer/web/component.rb', line 484

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



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

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



356
357
358
359
360
361
362
363
364
365
366
367
# File 'lib/glimmer/web/component.rb', line 356

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)


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

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)


454
455
456
457
458
459
460
# File 'lib/glimmer/web/component.rb', line 454

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)


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

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)


449
450
451
# File 'lib/glimmer/web/component.rb', line 449

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

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

Raises:

  • (Glimmer::Error)


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
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/glimmer/web/component.rb', line 286

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



549
550
551
552
553
554
555
556
557
558
# File 'lib/glimmer/web/component.rb', line 549

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



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

alias local_respond_to? respond_to_missing?

#notify_custom_event_listeners(event, *args) ⇒ Object



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

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



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

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



339
340
341
# File 'lib/glimmer/web/component.rb', line 339

def observer_registrations
  @observer_registrations ||= []
end

#post_add_contentObject



334
335
336
# File 'lib/glimmer/web/component.rb', line 334

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



330
331
332
# File 'lib/glimmer/web/component.rb', line 330

def post_initialize_child(child)
  # No Op by default
end

#removeObject



479
480
481
482
# File 'lib/glimmer/web/component.rb', line 479

def remove
  remove_all_listeners
  @markup_root&.remove
end

#remove_all_listenersObject



526
527
528
529
530
# File 'lib/glimmer/web/component.rb', line 526

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



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

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



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

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



474
475
476
477
# File 'lib/glimmer/web/component.rb', line 474

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)


543
544
545
546
547
# File 'lib/glimmer/web/component.rb', line 543

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

#set_attribute(attribute_name, *args) ⇒ Object



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

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