Class: Compony::Component

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

Overview

API:

  • description

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.

API:

  • description



33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
# File 'lib/compony/component.rb', line 33

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
  @exposed_intent_blocks = []
  @skipped_actions = Set.new
  @path_block = proc do |model = nil, *args_for_path_helper, standalone_name: nil, **kwargs_for_path_helper|
    kwargs_for_path_helper.merge!(id: model.id) if model
    next Rails.application.routes.url_helpers.send(
      "#{path_helper_name(standalone_name)}_path",
      *args_for_path_helper,
      **kwargs_for_path_helper
    )
  end

  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)

API:

  • description



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

def comp_opts
  @comp_opts
end

#content_blocksObject (readonly)

needed in RequestContext for nesting

API:

  • description



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

def content_blocks
  @content_blocks
end

#parent_compObject (readonly)

API:

  • description



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

def parent_comp
  @parent_comp
end

Class Method Details

.comp_nameObject

Returns the component name

API:

  • description



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

def self.comp_name
  name.demodulize.underscore
end

.family_nameObject

Returns the family name

API:

  • description



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

def self.family_name
  module_parent.to_s.demodulize.underscore
end

.setup(&block) ⇒ Object

DSL method

API:

  • description



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

#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:

  • (defaults to: :main)

    The name of the before_render block, defaults to :main

  • (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.

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

API:

  • description



147
148
149
150
# File 'lib/compony/component.rb', line 147

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

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

DSL method Adds or overrides a content block.

Parameters:

  • (defaults to: :main)

    The name of the content block, defaults to :main

  • (defaults to: nil)

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

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

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

API:

  • description



158
159
160
161
162
163
164
# File 'lib/compony/component.rb', line 158

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

#exposed_intents(&block) ⇒ Object

DSL method If a block is given: Enters the DSL where exposed intents can be added or removed (use from setup within the component). If no block is given: Builds the declared intents and returns them (use from a RequestContext outside the component).

API:

  • description



224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/compony/component.rb', line 224

def exposed_intents(&block)
  if block_given?
    # Enter DSL
    @exposed_intent_blocks << block
  else
    # Build the declared intents
    return @exposed_intents if @exposed_intents
    @exposed_intents = NaturalOrdering.new
    @exposed_intent_blocks.each { |block| ManageIntentsDsl.new(@exposed_intents).evaluate(&block) } # alters @exposed_intents
    return @exposed_intents.map!(&:payload)
  end
end

#idObject

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

API:

  • description



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

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

#id_pathObject

Returns the id_path from the root_comp. Do not overwrite.

API:

  • description



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

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

#id_path_hashObject

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

API:

  • description



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

def id_path_hash
  Digest::SHA1.hexdigest(id_path)[..4]
end

#inspectObject

API:

  • description



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

def inspect
  standalone_paths = standalone_configs.values.to_h { |c| [c[:path], c.verbs.keys] }
  return "#<#{self.class.name} id_path: #{id_path.inspect}, standalone_paths: #{standalone_paths.inspect}, data: #{@data.inspect}>"
end

#param_name(unprefixed_param_name) ⇒ Object

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

API:

  • description



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

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

#path(&block) ⇒ void

This method returns an undefined value.

DSL method Overrides how the path to this component should be generated. The block will be given the following args: a model (optional), pos. args for the path helper, the kwarg standalone_name and kwargs for the path helper. The block is expected to return a Rails path. It is not given controller or helpers, instead use: Rails.application.routes.url_helpers. For the default block, refer to the initializer of this class. Useful when callers should pass a higher-level argument that is translated into path params here, e.g. minting a signed token into the URL so an unauthenticated link can authorize itself. Worked examples: see doc/guide/standalone.md ("Customizing path generation") and doc/guide/patterns.md §18 (signed-token capability links).

API:

  • public



132
133
134
135
136
137
138
139
# File 'lib/compony/component.rb', line 132

def path(*, **, &block)
  if block_given?
    # Assignment via DSL
    @path_block = block
  else
    @path_block.call(*, **)
  end
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 of the content block that should be removed

API:

  • description



169
170
171
172
173
174
175
176
177
# File 'lib/compony/component.rb', line 169

def remove_content(name) # rubocop:disable Naming/PredicateMethod
  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 of the content block that should be removed

API:

  • description



182
183
184
# File 'lib/compony/component.rb', line 182

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:

  • (defaults to: false)

    pass true iff render is called from render_standalone

API:

  • description



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
214
215
216
217
218
219
# File 'lib/compony/component.rb', line 189

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: "        if Compony.content_before_root_comp_block && standalone\n          Compony::RequestContext.new(component, controller, helpers: self, locals: render_locals).evaluate(&Compony.content_before_root_comp_block)\n        end\n        content_blocks.reject{ |el| el.hidden }.each do |element|\n          # Instanciate and evaluate a fresh RequestContext in order to use the buffer allocated by the ActionView (needed for `concat` calls)\n          Compony::RequestContext.new(component, controller, helpers: self, locals: render_locals).evaluate(&element.payload)\n        end\n        if Compony.content_after_root_comp_block && standalone\n          Compony::RequestContext.new(component, controller, helpers: self, locals: render_locals).evaluate(&Compony.content_after_root_comp_block)\n        end\n      RUBY\n    )\n  else\n    return nil # Prevent double render errors\n  end\nend\n"

#resourceful?Boolean

Is true for resourceful components

Returns:

API:

  • description



238
239
240
# File 'lib/compony/component.rb', line 238

def resourceful?
  return false
end

#root_compObject

Returns the current root comp. Do not overwrite.

API:

  • description



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:

API:

  • description



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

def root_comp?
  parent_comp.nil?
end

#sub_comp(**comp_opts) ⇒ Object

Instanciate a component with self as a parent

API:

  • description



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

def sub_comp(*, **comp_opts)
  intent = Compony.intent(*)
  sub = intent.comp(self, index: @sub_comps.count, **comp_opts)
  @sub_comps << sub
  return sub
end