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

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



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

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)

<- end of class methods



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

def args
  @args
end

#component_styleObject (readonly)

<- end of class methods



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

def component_style
  @component_style
end

#markup_rootObject (readonly)

<- end of class methods



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

def markup_root
  @markup_root
end

#optionsObject (readonly)

<- end of class methods



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

def options
  @options
end

#parentObject (readonly) Also known as: parent_proxy

<- end of class methods



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

def parent
  @parent
end

#slot_elementsObject (readonly)

<- end of class methods



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

def slot_elements
  @slot_elements
end

#style_blockObject (readonly)

<- end of class methods



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

def style_block
  @style_block
end

Class Method Details

.add_component(component) ⇒ Object



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

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



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

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



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

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)


219
220
221
# File 'lib/glimmer/web/component.rb', line 219

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

.any_component_style?(component_class) ⇒ Boolean

Returns:

  • (Boolean)


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

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

.body_componentsObject



235
236
237
# File 'lib/glimmer/web/component.rb', line 235

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

.component_class_to_components_mapObject



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

def component_class_to_components_map
  @component_class_to_components_map ||= {}
end

.component_count(component_class) ⇒ Object



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

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

.component_keyword_to_classes_mapObject



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

def component_keyword_to_classes_map
  @component_keyword_to_classes_map ||= reset_component_keyword_to_classes_map
end

.component_stylesObject



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

def component_styles
  @component_styles ||= {}
end

.componentsObject



231
232
233
# File 'lib/glimmer/web/component.rb', line 231

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

.for(underscored_component_name) ⇒ Object



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

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



239
240
241
# File 'lib/glimmer/web/component.rb', line 239

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

.included(klass) ⇒ Object



149
150
151
152
153
154
155
156
157
# File 'lib/glimmer/web/component.rb', line 149

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



189
190
191
# File 'lib/glimmer/web/component.rb', line 189

def interpretation_stack
  @interpretation_stack ||= []
end

.keywords_for_class(component_class) ⇒ Object



176
177
178
179
# File 'lib/glimmer/web/component.rb', line 176

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



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

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

.remove_component(component) ⇒ Object



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

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



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

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



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

def reset_component_keyword_to_classes_map
  @component_keyword_to_classes_map = {}
end

Instance Method Details

#add_observer(observer, attribute_name) ⇒ Object



341
342
343
344
345
346
347
# File 'lib/glimmer/web/component.rb', line 341

def add_observer(observer, attribute_name)
  if has_instance_method?(attribute_name)
    super(observer, attribute_name)
  else
    @markup_root.add_observer(observer, attribute_name)
  end
end

#attribute_setter(attribute_name) ⇒ Object



378
379
380
# File 'lib/glimmer/web/component.rb', line 378

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

#bind_content(*binding_args, &content_block) ⇒ Object



395
396
397
# File 'lib/glimmer/web/component.rb', line 395

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

#can_add_observer?(attribute_name) ⇒ Boolean

Returns:

  • (Boolean)


337
338
339
# File 'lib/glimmer/web/component.rb', line 337

def can_add_observer?(attribute_name)
  has_instance_method?(attribute_name) || has_instance_method?("#{attribute_name}?") || @markup_root.can_add_observer?(attribute_name)
end

#can_handle_observation_request?(observation_request) ⇒ Boolean

Returns:

  • (Boolean)


317
318
319
320
321
322
323
324
325
# File 'lib/glimmer/web/component.rb', line 317

def can_handle_observation_request?(observation_request)
  observation_request = observation_request.to_s
  result = false
  if observation_request.start_with?('on_updated_')
    property = observation_request.sub(/^on_updated_/, '')
    result = can_add_observer?(property)
  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



401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/glimmer/web/component.rb', line 401

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
    # delegate to GUI DSL ContentExpression
    super
  end
end

#data_bind(property, model_binding) ⇒ Object



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

def data_bind(property, model_binding)
  @markup_root&.data_bind(property, model_binding)
end

#get_attribute(attribute_name) ⇒ Object



370
371
372
373
374
375
376
# File 'lib/glimmer/web/component.rb', line 370

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



327
328
329
330
331
332
333
334
335
# File 'lib/glimmer/web/component.rb', line 327

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

#has_attribute?(attribute_name, *args) ⇒ Boolean

Returns:

  • (Boolean)


349
350
351
352
# File 'lib/glimmer/web/component.rb', line 349

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)


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

def has_instance_method?(method_name)
  respond_to?(method_name) and
    !markup_root&.respond_to?(method_name) and
    !method(method_name)&.source_location&.first&.include?('glimmer/dsl/engine.rb') and
    !method(method_name)&.source_location&.first&.include?('glimmer/web/element_proxy.rb')
end

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

Raises:

  • (Glimmer::Error)


261
262
263
264
265
266
267
268
269
270
271
272
273
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
# File 'lib/glimmer/web/component.rb', line 261

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
  options ||= {}
  @slot_elements = {}
  @args = args
  options ||= {}
  @options = self.class.options.merge(options)
  @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

#local_respond_to?Object



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

alias local_respond_to? respond_to_missing?

#observer_registrationsObject

This stores observe keyword registrations of model/attribute observers



313
314
315
# File 'lib/glimmer/web/component.rb', line 313

def observer_registrations
  @observer_registrations ||= []
end

#post_add_contentObject



308
309
310
# File 'lib/glimmer/web/component.rb', line 308

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



304
305
306
# File 'lib/glimmer/web/component.rb', line 304

def post_initialize_child(child)
  # No Op by default
end

#removeObject



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

def remove
  @markup_root&.remove
end

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



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

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)


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

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



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

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