Class: Compony::Intent

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

Overview

An Intent is a a gateway to a component, along with relevant context, such as the comp and family, perhaps a resource, standalone name, feasibility etc. The class provides tooling used by various Compony helpers used to point to other components in some way. Note: The arguments label and style are not part of the button: hash, because they are processed by the Intent before affecting the button.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(comp_name_or_cst_or_class = nil, model_or_family_name_or_cst = nil, standalone_name: nil, name: nil, label: nil, style: nil, button: {}, path: nil, method: nil, data: nil, data_class: nil, feasibility_target: nil, feasibility_action: nil) ⇒ Intent

Returns a new instance of Intent.

Parameters:

  • comp_name_or_cst_or_class (String, Symbol, Class) (defaults to: nil)

    The component that should be loaded, for instance ShowForAll, 'ShowForAll' or :show_for_all, or can also pass a component class (such as Components::Users::Show)

  • model_or_family_name_or_cst (String, Symbol, ApplicationRecord) (defaults to: nil)

    Either the family that contains the requested component, or an instance implementing model_name from which the family name is auto-generated. Examples: Users, 'Users', :users, User.first

  • standalone_name (Symbol) (defaults to: nil)

    If given, will override the standalone name for all path calls for this intent instance.

  • name (Symbol) (defaults to: nil)

    If given, will override the name of this intent. Defaults to component and family name joined by underscore.

  • label (String, Hash) (defaults to: nil)

    If given, will be used for generating the label. If Hash, is given as options to #label.

  • style (Symbol) (defaults to: nil)

    If given, sets the button style to use and uses the default one otherwise.

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

    Parameters that will be given as-is to the button component initializer.

  • path (String, Hash) (defaults to: nil)

    If given, will be used for generating the path. If Hash, is given as options to #path.

  • data (ApplicationRecord, Object) (defaults to: nil)

    If given, the target component will be instanciated with this argument. Omit if your second pos arg is a model.

  • data_class (Class) (defaults to: nil)

    If given, the target component will be instanciated with this argument.

  • feasibility_target (ApplicationRecord) (defaults to: nil)

    If given, will override the feasibility target (prevention framework)

  • feasibility_action (ApplicationRecord) (defaults to: nil)

    If given, will override the feasibility action (prevention framework)



26
27
28
29
30
31
32
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/compony/intent.rb', line 26

def initialize(comp_name_or_cst_or_class = nil,
               model_or_family_name_or_cst = nil,
               standalone_name: nil,
               name: nil,
               label: nil,
               style: nil,
               button: {},
               path: nil,
               method: nil,
               data: nil,
               data_class: nil,
               feasibility_target: nil,
               feasibility_action: nil)
  # Check for model / data
  @data = data
  @data ||= model_or_family_name_or_cst if model_or_family_name_or_cst.respond_to?(:model_name)
  @data_class = data_class

  # Figure out comp_class
  if comp_name_or_cst_or_class.nil?
    if name.blank? || !label.is_a?(String)
      fail('An intent created without positional arguments must be given the kwargs `name:`, `label` (String).')
    end
  elsif comp_name_or_cst_or_class.is_a?(Class) && (comp_name_or_cst_or_class <= Compony::Component)
    # A class was given as the first argument
    @comp_class = comp_name_or_cst_or_class
  else
    # Build the constant from the first two arguments
    family_underscore_str = if !(model_or_family_name_or_cst.is_a?(String) || model_or_family_name_or_cst.is_a?(Symbol)) && @data.respond_to?(:model_name)
                              # Determine the component family from the data
                              @data.model_name.plural
                            else
                              # Determine the component family from the given argument
                              model_or_family_name_or_cst.to_s.underscore
                            end
    constant_str = "::Components::#{family_underscore_str.camelize}::#{comp_name_or_cst_or_class.to_s.camelize}"
    @comp_class = constant_str.constantize
  end

  # Store further arguments
  @name = name&.to_sym
  @standalone_name = standalone_name
  @label = label.is_a?(String) ? label : nil
  @label_opts = label.is_a?(Hash) ? label : {}
  @style = style&.to_sym
  @button_opts = button
  @path = path.is_a?(String) ? path : nil
  @path_opts = path.is_a?(Hash) ? path : {}
  @method = method&.to_sym
  @feasibility_target = feasibility_target
  @feasibility_action = feasibility_action
end

Instance Attribute Details

#comp_classObject (readonly)



7
8
9
# File 'lib/compony/intent.rb', line 7

def comp_class
  @comp_class
end

#dataObject (readonly)



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

def data
  @data
end

Instance Method Details

#button_comp_optsObject

Returns the options that are given to the initializer when creating a button from this intent.



134
135
136
137
138
139
140
141
142
# File 'lib/compony/intent.rb', line 134

def button_comp_opts
  return {
    label:,
    href:   feasible? ? path : nil,
    method:,
    class:  feasible? ? nil : 'disabled',
    title:  feasible? ? nil : feasibility_target.full_feasibility_messages(feasibility_action).presence
  }.deep_merge(@button_opts)
end

#compObject

Instanciates the component and returns the instance. If data and/or data_class were specified when instantiating this intent, they are passed. All given arguments will be given to the component's initializer, also overriding data and data_class if present.



87
88
89
90
# File 'lib/compony/intent.rb', line 87

def comp(*, **)
  return nil if @comp_class.nil?
  return @comp ||= @comp_class.new(*, data: @data, data_class: @data_class, **)
end

#feasibility_actionObject



123
124
125
# File 'lib/compony/intent.rb', line 123

def feasibility_action
  @feasibility_action.presence || comp_class.comp_name.to_sym
end

#feasibility_targetObject



119
120
121
# File 'lib/compony/intent.rb', line 119

def feasibility_target
  @feasibility_target.presence || model? ? @data : nil
end

#feasible?Boolean

Returns whether this intent is feasible (no prevention)

Returns:

  • (Boolean)


128
129
130
131
# File 'lib/compony/intent.rb', line 128

def feasible?
  return true if feasibility_target.blank? || feasibility_action.blank?
  return feasibility_target.feasible?(feasibility_action)
end

#label(model = nil, **label_opt_overrides) ⇒ Object

Returns the label of buttons produced by this intent.



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

def label(model = nil, *, **label_opt_overrides)
  label_opts = @label_opts.deep_merge(label_opt_overrides)
  @label.presence || comp.label(model || (model? ? @data : nil), *, **label_opts)
end

#methodObject



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

def method
  @method || :get
end

#model?Boolean

Returns true for things like User.first, but false for things like :users or User

Returns:

  • (Boolean)


80
81
82
83
# File 'lib/compony/intent.rb', line 80

def model?
  @model = @data.respond_to?(:model_name) && !@data.is_a?(Class) if @model.nil?
  return @model
end

#nameObject

Returns a name for this intent, consisting of comp and family name. Can be overriden in the constructor. Example: :show_users, :destroy_sessions



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

def name
  @name.presence || :"#{comp_class.comp_name}_#{comp_class.family_name}"
end

#path(model = nil, standalone_name: nil, **path_opt_overrides) ⇒ Object

Returns the path to the component. Additional arguments are passed to the component's path block, which typically passes them to the Rails path helper.

Parameters:

  • model (ApplicationRecord) (defaults to: nil)

    If given and non-nil, will override the model passed to the component's path block

  • standalone_name (Symbol) (defaults to: nil)

    If given and non-nil, will override the standalone_name passed to the component's path block



96
97
98
99
100
101
# File 'lib/compony/intent.rb', line 96

def path(model = nil, *, standalone_name: nil, **path_opt_overrides)
  return @path if @path.present?
  return nil if @comp_class.nil?
  path_opts = @path_opts.deep_merge(path_opt_overrides)
  comp.path(model || (model? ? @data : nil), standalone_name: standalone_name || @standalone_name, **path_opts)
end

#render(controller, parent_comp = nil, style: nil, **button_arg_overrides) ⇒ Object

Renders this intent into a button defined by style.

Parameters:

  • controller (ApplicationController)

    The controller from the request context, needed to render the button.

  • parent_comp (Compony::Component) (defaults to: nil)

    If called from within a component, pass the component to inform the button that it is nested within.

  • style (Symbol) (defaults to: nil)

    If present, overrides the class of the generated button component, defaults to Compony#default_button_style.

  • button_arg_overrides (Hash)

    Any further kwargs are passed to the button component's initializer.



149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/compony/intent.rb', line 149

def render(controller, parent_comp = nil, style: nil, **button_arg_overrides)
  # Abort if not authorized
  return nil if comp && !comp.standalone_access_permitted_for?(controller, standalone_name: @standalone_name, verb: method)
  # Prepare opts
  button_comp_class ||= Compony.button_component_class(*[style || @style].compact)
  button_opts = (comp&.button_defaults || {}).merge(button_comp_opts).merge(button_arg_overrides) # overrides go right to left
  # Perform render
  if parent_comp
    return parent_comp.sub_comp(button_comp_class, **button_opts).render(controller)
  else
    button_comp_class.new(**button_opts).render(controller)
  end
end