Class: Bridgetown::Component

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
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

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method, *args, **kwargs, &block) ⇒ Object



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

def method_missing(method, *args, **kwargs, &block)
  if helpers.respond_to?(method.to_sym)
    helpers.send method.to_sym, *args, **kwargs, &block
  else
    super
  end
end

Class Attribute Details

.source_locationObject

Returns the value of attribute source_location.



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

def source_location
  @source_location
end

Instance Attribute Details

#siteBridgetown::Site (readonly)

Returns:



10
11
12
# File 'lib/bridgetown-core/component.rb', line 10

def site
  @site
end

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



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

def view_context
  @view_context
end

Class Method Details

.component_template_contentString

Read the template file.

Returns:

  • (String)


81
82
83
# File 'lib/bridgetown-core/component.rb', line 81

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)


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

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



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

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



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

def path_for_errors
  component_template_path
rescue RuntimeError
  source_location
end

.renderer_for_ext(ext, &block) ⇒ Object

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

Parameters:

  • ext (String)

    erb, slim, etc.



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

def renderer_for_ext(ext, &block)
  @_tmpl ||= case ext.to_s
             when "erb"
               Tilt::ErubiTemplate.new(component_template_path,
                                       outvar: "@_erbout",
                                       bufval: "Bridgetown::OutputBuffer.new",
                                       engine_class: Bridgetown::ERBEngine,
                                       &block)
             when "serb"
               Tilt::SerbeaTemplate.new(component_template_path, &block)
             when "slim" # requires bridgetown-slim
               Slim::Template.new(component_template_path, &block)
             when "haml" # requires bridgetown-haml
               Tilt::HamlTemplate.new(component_template_path, &block)
             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>)


89
90
91
# File 'lib/bridgetown-core/component.rb', line 89

def supported_template_extensions
  %w(erb serb slim haml)
end

Instance Method Details

#_rendererObject



214
215
216
217
218
219
220
221
# File 'lib/bridgetown-core/component.rb', line 214

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.



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

def before_render; end

#callObject

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



201
202
203
# File 'lib/bridgetown-core/component.rb', line 201

def call
  nil
end

#contentString

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

Returns:

  • (String)

    or nil



103
104
105
# File 'lib/bridgetown-core/component.rb', line 103

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

#helpersObject



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

def helpers
  @helpers ||= Bridgetown::RubyTemplateView::Helpers.new(
    self, view_context&.site || Bridgetown::Current.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)


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

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)


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

def render?
  true
end

#render_in(view_context, &block) ⇒ Object

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

Parameters:



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/bridgetown-core/component.rb', line 177

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

  if render?
    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)


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

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



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

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: name, content: content, context: self, transform: false)

  nil
end

#slotsArray<Bridgetown::Slot>

Returns:



108
109
110
# File 'lib/bridgetown-core/component.rb', line 108

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)


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

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)


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

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.



196
197
198
# File 'lib/bridgetown-core/component.rb', line 196

def template
  call || _renderer.render(self)
end