Class: Bridgetown::Component

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Includes:
Streamlined
Defined in:
lib/bridgetown-core/component.rb

Direct Known Subclasses

Shared::Navbar

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Streamlined

#helper

Methods included from ERBCapture

#capture

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method) ⇒ Object



235
236
237
238
239
240
241
# File 'lib/bridgetown-core/component.rb', line 235

def method_missing(method, ...)
  if helpers.respond_to?(method.to_sym)
    helpers.send(method.to_sym, ...)
  else
    super
  end
end

Class Attribute Details

.source_locationObject

Returns the value of attribute source_location.



18
19
20
# File 'lib/bridgetown-core/component.rb', line 18

def source_location
  @source_location
end

Instance Attribute Details

#siteBridgetown::Site (readonly)

Returns:



12
13
14
# File 'lib/bridgetown-core/component.rb', line 12

def site
  @site
end

#view_contextBridgetown::RubyTemplateView, Bridgetown::Component (readonly)



15
16
17
# File 'lib/bridgetown-core/component.rb', line 15

def view_context
  @view_context
end

Class Method Details

.component_template_contentString

Read the template file.

Returns:

  • (String)


83
84
85
# File 'lib/bridgetown-core/component.rb', line 83

def component_template_content
  @_tmpl_content ||= File.read(component_template_path)
end

.component_template_pathString

Find the first matching template path based on source location and extension.

Returns:

  • (String)


58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/bridgetown-core/component.rb', line 58

def component_template_path
  @_tmpl_path ||= begin
    stripped_path = File.join(
      File.dirname(source_location),
      File.basename(source_location, ".*")
    )
    supported_template_extensions.each do |ext|
      test_path = "#{stripped_path}.#{ext}"
      break test_path if File.exist?(test_path)

      test_path = "#{stripped_path}.html.#{ext}"
      break test_path if File.exist?(test_path)
    end
  end

  unless @_tmpl_path.is_a?(String)
    raise "#{name}: no matching template could be found in #{File.dirname(source_location)}"
  end

  @_tmpl_path
end

.inherited(child) ⇒ Object



20
21
22
23
24
25
26
27
28
# File 'lib/bridgetown-core/component.rb', line 20

def inherited(child)
  # Code cribbed from ViewComponent by GitHub:
  # Derive the source location of the component Ruby file from the call stack
  child.source_location = caller_locations(1, 10).reject do |l|
    l.label == "inherited"
  end[0].absolute_path

  super
end

.path_for_errorsObject



95
96
97
98
99
# File 'lib/bridgetown-core/component.rb', line 95

def path_for_errors
  File.basename(component_template_path)
rescue RuntimeError
  source_location
end

.renderer_for_ext(ext) ⇒ Object

Return the appropriate template renderer for a given extension. TODO: make this extensible

Parameters:

  • ext (String)

    erb, slim, etc.



34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/bridgetown-core/component.rb', line 34

def renderer_for_ext(ext, &)
  @_tmpl ||= case ext.to_s
             when "erb"
               Tilt::ErubiTemplate.new(component_template_path,
                                       outvar: "@_erbout",
                                       bufval: "Bridgetown::OutputBuffer.new",
                                       engine_class: Bridgetown::ERBEngine,
                                       &)
             when "serb"
               Tilt::SerbeaTemplate.new(component_template_path, &)
             when "slim" # requires bridgetown-slim
               Slim::Template.new(component_template_path, &)
             when "haml" # requires bridgetown-haml
               Tilt::HamlTemplate.new(component_template_path, &)
             else
               raise NameError
             end
rescue NameError, LoadError
  raise "No component rendering engine could be found for .#{ext} templates"
end

.supported_template_extensionsArray<String>

A list of extensions supported by the renderer TODO: make this extensible

Returns:

  • (Array<String>)


91
92
93
# File 'lib/bridgetown-core/component.rb', line 91

def supported_template_extensions
  %w(erb serb slim haml)
end

Instance Method Details

#_rendererObject



222
223
224
225
226
227
228
229
# File 'lib/bridgetown-core/component.rb', line 222

def _renderer
  @_renderer ||= begin
    ext = File.extname(self.class.component_template_path).delete_prefix(".")
    self.class.renderer_for_ext(ext) { self.class.component_template_content }.tap do |rn|
      self.class.include(rn.is_a?(Tilt::SerbeaTemplate) ? Serbea::Helpers : ERBCapture)
    end
  end
end

#before_renderObject

Subclasses can override this method to perform tasks before a render.



214
# File 'lib/bridgetown-core/component.rb', line 214

def before_render; end

#callObject

Typically not used but here as a compatibility nod toward ViewComponent.



209
210
211
# File 'lib/bridgetown-core/component.rb', line 209

def call
  nil
end

#contentString

If a content block was originally passed into via render, capture its output.

Returns:

  • (String)

    or nil



105
106
107
# File 'lib/bridgetown-core/component.rb', line 105

def content
  @_content ||= (view_context.capture(self, &@_content_block) if @_content_block)
end

#helpersObject



231
232
233
# File 'lib/bridgetown-core/component.rb', line 231

def helpers
  @helpers ||= Bridgetown::RubyTemplateView::Helpers.new(self, view_context&.site)
end

#render(item, options = {}, &block) ⇒ String

Provide a render helper for evaluation within the component context.

Parameters:

  • item (Object)

    a component supporting render_in or a partial name

  • options (Hash) (defaults to: {})

    passed to the partial helper if needed

Returns:

  • (String)


164
165
166
167
168
169
170
171
172
173
174
# File 'lib/bridgetown-core/component.rb', line 164

def render(item, options = {}, &block)
  if item.respond_to?(:render_in)
    result = ""
    capture do # this ensures no leaky interactions between BT<=>VC blocks
      result = item.render_in(self, &block)
    end
    result&.html_safe
  else
    partial(item, options, &block)&.html_safe
  end
end

#render?Boolean

Subclasses can override this method to determine if the component should be rendered based on initialized data or other logic.

Returns:

  • (Boolean)


218
219
220
# File 'lib/bridgetown-core/component.rb', line 218

def render?
  true
end

#render_in(view_context, &block) ⇒ Object

This is where the magic happens. Render the component within a view context.

Parameters:



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/bridgetown-core/component.rb', line 179

def render_in(view_context, &block)
  @view_context = view_context
  @_content_block = block

  if render?
    if helpers.site.config.fast_refresh
      signal = helpers.site.tmp_cache["comp-signal:#{self.class.source_location}"] ||=
        Signalize.signal(1)
      # subscribe so resources are attached to this component within effect
      signal.value
    end
    before_render
    template
  else
    ""
  end
rescue StandardError => e
  Bridgetown.logger.error "Component error:",
                          "#{self.class} encountered an error while " \
                          "rendering `#{self.class.path_for_errors}'"
  raise e
end

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

Returns:

  • (Boolean)


243
244
245
# File 'lib/bridgetown-core/component.rb', line 243

def respond_to_missing?(method, include_private = false)
  helpers.respond_to?(method.to_sym, include_private) || super
end

#slot(name, input = nil, replace: false, &block) ⇒ void

This method returns an undefined value.

Define a new component slot

Parameters:

  • name (String, Symbol)

    name of the slot

  • input (String) (defaults to: nil)

    content if not supplying a block

  • replace (Boolean) (defaults to: false)

    set to true to replace any previously defined slot with same name



120
121
122
123
124
125
126
127
128
129
# File 'lib/bridgetown-core/component.rb', line 120

def slot(name, input = nil, replace: false, &block)
  content = block.nil? ? input.to_s : view_context.capture(&block)

  name = name.to_s
  slots.reject! { _1.name == name } if replace

  slots << Slot.new(name:, content:, context: self, transform: false)

  nil
end

#slotsArray<Bridgetown::Slot>

Returns:



110
111
112
# File 'lib/bridgetown-core/component.rb', line 110

def slots
  @slots ||= []
end

#slotted(name, default_input = nil, &default_block) ⇒ String

Render out a component slot

Parameters:

  • name (String, Symbol)

    name of the slot

  • input (String)

    default content if slot isn't defined and no block provided

Returns:

  • (String)


136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/bridgetown-core/component.rb', line 136

def slotted(name, default_input = nil, &default_block)
  content # ensure content block is processed

  name = name.to_s
  filtered_slots = slots.select do |slot|
    slot.name == name
  end

  return filtered_slots.map(&:content).join.html_safe if filtered_slots.length.positive?

  default_block.nil? ? default_input.to_s : capture(&default_block)
end

#slotted?(name) ⇒ Boolean

Check if a component slot has been defined

Returns:

  • (Boolean)


152
153
154
155
156
157
# File 'lib/bridgetown-core/component.rb', line 152

def slotted?(name)
  name = name.to_s
  slots.any? do |slot|
    slot.name == name
  end
end

#templateObject

Subclasses can override this method to return a string from their own template handling.



204
205
206
# File 'lib/bridgetown-core/component.rb', line 204

def template
  (method(:call).arity.zero? ? call : nil) || _renderer.render(self)
end