Class: Compony::Component

Inherits:
Object
  • Object
show all
Defined in:
lib/compony/component.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(parent_comp = nil, index: 0, **comp_opts) ⇒ Component

Returns a new instance of Component.



43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# File 'lib/compony/component.rb', line 43

def initialize(parent_comp = nil, index: 0, **comp_opts)
  @parent_comp = parent_comp
  @sub_comps = []
  @index = index
  @comp_opts = comp_opts
  @before_render_blocks = NaturalOrdering.new
  @content_blocks = NaturalOrdering.new
  @actions = NaturalOrdering.new
  @skipped_actions = Set.new

  init_standalone
  init_labelling

  fail "#{inspect} is missing a call to `setup`." unless setup_blocks&.any?

  setup_blocks.each do |setup_block|
    instance_exec(&setup_block)
  end
end

Instance Attribute Details

#comp_optsObject (readonly)



9
10
11
# File 'lib/compony/component.rb', line 9

def comp_opts
  @comp_opts
end

#content_blocksObject (readonly)

needed in RequestContext for nesting



10
11
12
# File 'lib/compony/component.rb', line 10

def content_blocks
  @content_blocks
end

#parent_compObject (readonly)



8
9
10
# File 'lib/compony/component.rb', line 8

def parent_comp
  @parent_comp
end

Class Method Details

.comp_cstObject

Returns the name of the class constant of this component. Do not override.



34
35
36
# File 'lib/compony/component.rb', line 34

def self.comp_cst
  name.demodulize.to_sym
end

.comp_nameObject

Returns the component name



39
40
41
# File 'lib/compony/component.rb', line 39

def self.comp_name
  comp_cst.to_s.underscore
end

.family_cstObject

Returns the name of the module constant (=family) of this component. Do not override.



24
25
26
# File 'lib/compony/component.rb', line 24

def self.family_cst
  module_parent.to_s.demodulize.to_sym
end

.family_nameObject

Returns the family name



29
30
31
# File 'lib/compony/component.rb', line 29

def self.family_name
  family_cst.to_s.underscore
end

.setup(&block) ⇒ Object

DSL method



16
17
18
19
20
21
# File 'lib/compony/component.rb', line 16

def self.setup(&block)
  fail("`setup` expects a block in #{inspect}.") unless block_given?
  self.setup_blocks ||= []
  self.setup_blocks = setup_blocks.dup # This is required to prevent the parent class to see children's setup blocks.
  setup_blocks << block
end

Instance Method Details

#action(action_name, before: nil, &block) ⇒ Object

DSL method Adds or replaces an action (for action buttons) If before: is specified, will insert the action before the named action. When replacing, an element keeps its position unless before: is specified.



218
219
220
# File 'lib/compony/component.rb', line 218

def action(action_name, before: nil, &block)
  @actions.natural_push(action_name, block, before:)
end

#before_render(name = :main, before: nil, &block) ⇒ Object

DSL method Adds or overrides a before_render block. You can use controller.redirect_to to redirect away and halt the before_render/content chain

Parameters:

  • name (Symbol, String) (defaults to: :main)

    The name of the before_render block, defaults to :main

  • before (nil, Symbol, String) (defaults to: nil)

    If nil, the block will be added to the bottom of the before_render chain. Otherwise, pass the name of another block.

  • block (Proc)

    The block that should be run as part of the before_render pipeline. Will run in the component's context.



141
142
143
144
# File 'lib/compony/component.rb', line 141

def before_render(name = :main, before: nil, **, &block)
  fail("`before_render` expects a block in #{inspect}.") unless block_given?
  @before_render_blocks.natural_push(name, block, before:, **)
end

#comp_cstObject

Returns the name of the class constant of this component. Do not override.



126
127
128
# File 'lib/compony/component.rb', line 126

def comp_cst
  self.class.comp_cst
end

#comp_nameObject

Returns the component name



131
132
133
# File 'lib/compony/component.rb', line 131

def comp_name
  self.class.comp_name
end

#content(name = :main, before: nil, &block) ⇒ Object

DSL method Adds or overrides a content block.

Parameters:

  • name (Symbol, String) (defaults to: :main)

    The name of the content block, defaults to :main

  • before (nil, Symbol, String) (defaults to: nil)

    If nil, the block will be added to the bottom of the content chain. Otherwise, pass the name of another block.

  • kwargs (Hash)

    If hidden is true, the content will not be rendered by default, allowing you to nest it in another content block.

  • block (Proc)

    The block that should be run as part of the content pipeline. Will run in the component's context. You can use Dyny here.



152
153
154
155
156
157
158
# File 'lib/compony/component.rb', line 152

def content(name = :main, before: nil, **, &block)
  # A block is required here, but if this is an override (e.g. to hide another content block), we can tolerate the missing block.
  if !block_given? && @content_blocks.find { |b| b.name == name }.nil?
    fail("`content` expects a block in #{inspect}.")
  end
  @content_blocks.natural_push(name, block || :missing, before:, **)
end

#family_cstObject

Returns the name of the module constant (=family) of this component. Do not override.



116
117
118
# File 'lib/compony/component.rb', line 116

def family_cst
  self.class.family_cst
end

#family_nameObject

Returns the family name



121
122
123
# File 'lib/compony/component.rb', line 121

def family_name
  self.class.family_name
end

#idObject

Returns an identifier describing this component. Must be unique among simplings under the same parent_comp. Do not override.



82
83
84
# File 'lib/compony/component.rb', line 82

def id
  "#{family_name}_#{comp_name}_#{@index}"
end

#inspectObject



63
64
65
# File 'lib/compony/component.rb', line 63

def inspect
  "#<#{self.class.name}:#{hash}>"
end

#param_name(unprefixed_param_name) ⇒ Object

Given an unprefixed name of a param, adds the path hash Do not overwrite.



104
105
106
# File 'lib/compony/component.rb', line 104

def param_name(unprefixed_param_name)
  "#{path_hash}_#{unprefixed_param_name}"
end

#pathObject

Returns the id path from the root_comp. Do not overwrite.



88
89
90
91
92
93
94
# File 'lib/compony/component.rb', line 88

def path
  if root_comp?
    id
  else
    "#{parent_comp.path}/#{id}"
  end
end

#path_hashObject

Returns a hash for the path. Used for params prefixing. Do not overwrite.



98
99
100
# File 'lib/compony/component.rb', line 98

def path_hash
  Digest::SHA1.hexdigest(path)[..4]
end

#remove_content(name) ⇒ Object

DSL method Removes a content block. Use this in subclasses if a content block defined in the parent should be removed from the child.

Parameters:

  • name (Symbol, String)

    Name of the content block that should be removed



163
164
165
166
167
168
169
170
171
# File 'lib/compony/component.rb', line 163

def remove_content(name)
  existing_index = @content_blocks.find_index { |el| el.name == name.to_sym }
  if existing_index.nil?
    return false
  else
    @content_blocks.delete_at(existing_index)
    return true
  end
end

#remove_content!(name) ⇒ Object

DSL method Removes a content block and fails if the content block was not found.

Parameters:

  • name (Symbol, String)

    Name of the content block that should be removed



176
177
178
# File 'lib/compony/component.rb', line 176

def remove_content!(name)
  remove_content(name) || fail("Content block #{name.inspect} not found for removal in #{inspect}.")
end

#render(controller, standalone: false, **locals) ⇒ Object

Renders the component using the controller passsed to it and returns it as a string. Do not overwrite.

Parameters:

  • standalone (Boolean) (defaults to: false)

    pass true iff render is called from render_standalone



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/compony/component.rb', line 183

def render(controller, standalone: false, **locals)
  # Call before_render hooks (if any) and backfire instance variables back to the component
  @before_render_blocks.each do |element|
    RequestContext.new(self, controller, locals:).evaluate_with_backfire(&element.payload)
    # Stop if a `before_render` block issued a body (e.g. through redirecting)
    break unless controller.response_body.nil?
  end

  # Render, unless before_render has already issued a body (e.g. through redirecting).
  if controller.response_body.nil?
    fail "#{self.class.inspect} must define `content` or set a response body in `before_render`" if @content_blocks.none?
    return controller.render_to_string(
      type:   :dyny,
      locals: { content_blocks: @content_blocks, standalone:, component: self, render_locals: locals },
      inline: <<~RUBY
        if Compony.content_before_root_comp_block && standalone
          Compony::RequestContext.new(component, controller, helpers: self, locals: render_locals).evaluate(&Compony.content_before_root_comp_block)
        end
        content_blocks.reject{ |el| el.hidden }.each do |element|
          # Instanciate and evaluate a fresh RequestContext in order to use the buffer allocated by the ActionView (needed for `concat` calls)
          Compony::RequestContext.new(component, controller, helpers: self, locals: render_locals).evaluate(&element.payload)
        end
        if Compony.content_after_root_comp_block && standalone
          Compony::RequestContext.new(component, controller, helpers: self, locals: render_locals).evaluate(&Compony.content_after_root_comp_block)
        end
      RUBY
    )
  else
    return nil # Prevent double render errors
  end
end

#render_actions(controller, wrapper_class: '', action_class: '') ⇒ Object

Used to render all actions of this component, each button wrapped in a div with the specified class



229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
# File 'lib/compony/component.rb', line 229

def render_actions(controller, wrapper_class: '', action_class: '')
  h = controller.helpers
  h.(:div, class: wrapper_class) do
    button_htmls = @actions.map do |action|
      next if @skipped_actions.include?(action.name)
      Compony.with_button_defaults(feasibility_action: action.name.to_sym) do
        action_button = action.payload.call(controller)
        next unless action_button
        button_html = action_button.render(controller)
        next if button_html.blank?
        h.(:div, button_html, class: action_class)
      end
    end
    next h.safe_join button_htmls
  end
end

#resourceful?Boolean

Is true for resourceful components

Returns:

  • (Boolean)


247
248
249
# File 'lib/compony/component.rb', line 247

def resourceful?
  return false
end

#root_compObject

Returns the current root comp. Do not overwrite.



69
70
71
72
# File 'lib/compony/component.rb', line 69

def root_comp
  return self unless parent_comp
  return parent_comp.root_comp
end

#root_comp?Boolean

Returns whether or not this is the root comp. Do not overwrite.

Returns:

  • (Boolean)


76
77
78
# File 'lib/compony/component.rb', line 76

def root_comp?
  parent_comp.nil?
end

#skip_action(action_name) ⇒ Object

DSL method Marks an action for skip



224
225
226
# File 'lib/compony/component.rb', line 224

def skip_action(action_name)
  @skipped_actions << action_name.to_sym
end

#sub_comp(component_class, **comp_opts) ⇒ Object

Instanciate a component with self as a parent



109
110
111
112
113
# File 'lib/compony/component.rb', line 109

def sub_comp(component_class, **comp_opts)
  sub = component_class.new(self, index: @sub_comps.count, **comp_opts)
  @sub_comps << sub
  return sub
end