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.



22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/compony/component.rb', line 22

def initialize(parent_comp = nil, index: 0, **comp_opts)
  @parent_comp = parent_comp
  @sub_comps = []
  @index = index
  @comp_opts = comp_opts
  @before_render_block = nil
  @content_blocks = []
  @actions = []
  @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

#parent_compObject (readonly)



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

def parent_comp
  @parent_comp
end

Class Method Details

.setup(&block) ⇒ Object

DSL method



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

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.



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

def action(action_name, before: nil, &block)
  action_name = action_name.to_sym
  before_name = before&.to_sym
  action = MethodAccessibleHash.new(name: action_name, block:)

  existing_index = @actions.find_index { |el| el.name == action_name }
  if existing_index.present? && before_name.present?
    @actions.delete_at(existing_index) # Replacing an existing element with a before: directive - must delete before calculating indices
  end
  if before_name.present?
    before_index = @actions.find_index { |el| el.name == before_name } || fail("Action #{before_name} for :before not found in #{inspect}.")
  end

  if before_index.present?
    @actions.insert(before_index, action)
  elsif existing_index.present?
    @actions[existing_index] = action
  else
    @actions << action
  end
end

#add_content(index = -1,, &block) ⇒ Object

DSL method Adds a content block that will be executed after all previous ones. It is safe to use this method even if content has never been called You can use dyny here.



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

def add_content(index = -1, &block)
  fail("`content` expects a block in #{inspect}.") unless block_given?
  @content_blocks ||= []
  @content_blocks.insert(index, block)
end

#before_render(&block) ⇒ Object

DSL method



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

def before_render(&block)
  @before_render_block = block
end

#comp_class_forObject

TODO:

deprecate (check for usages beforehand)



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

def comp_class_for(...)
  Compony.comp_class_for(...)
end

#comp_class_for!Object

TODO:

deprecate (check for usages beforehand)



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

def comp_class_for!(...)
  Compony.comp_class_for!(...)
end

#comp_cstObject

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



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

def comp_cst
  self.class.name.demodulize.to_sym
end

#comp_nameObject

Returns the component name



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

def comp_name
  comp_cst.to_s.underscore
end

#content(&block) ⇒ Object

DSL method Overrides previous content (also from superclasses). Will be the first content block to run. You can use dyny here.



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

def content(&block)
  fail("`content` expects a block in #{inspect}.") unless block_given?
  @content_blocks = [block]
end

#family_cstObject

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



95
96
97
# File 'lib/compony/component.rb', line 95

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

#family_nameObject

Returns the family name



100
101
102
# File 'lib/compony/component.rb', line 100

def family_name
  family_cst.to_s.underscore
end

#idObject

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



61
62
63
# File 'lib/compony/component.rb', line 61

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

#inspectObject



42
43
44
# File 'lib/compony/component.rb', line 42

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.



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

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

#pathObject

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



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

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.



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

def path_hash
  Digest::SHA1.hexdigest(path)[..4]
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



150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/compony/component.rb', line 150

def render(controller, standalone: false, **locals)
  # Call before_render hook if any and backfire instance variables back to the component
  # TODO: Can .request_context be removed from the next line? Test well!
  RequestContext.new(self, controller, locals:).request_context.evaluate_with_backfire(&@before_render_block) if @before_render_block
  # 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.each do |block|
          # 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(&block)
        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



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/compony/component.rb', line 210

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.block.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)


228
229
230
# File 'lib/compony/component.rb', line 228

def resourceful?
  return false
end

#root_compObject

Returns the current root comp. Do not overwrite.



48
49
50
51
# File 'lib/compony/component.rb', line 48

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)


55
56
57
# File 'lib/compony/component.rb', line 55

def root_comp?
  parent_comp.nil?
end

#skip_action(action_name) ⇒ Object

DSL method Marks an action for skip



205
206
207
# File 'lib/compony/component.rb', line 205

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



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

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