Class: Compony::Components::Form

Inherits:
Compony::Component show all
Defined in:
lib/compony/components/form.rb

Overview

This component is used for the _form partial in the Rails paradigm.

Instance Attribute Summary

Attributes inherited from Compony::Component

#comp_opts, #content_blocks, #parent_comp

DSL collapse

Instance Method Summary collapse

Methods inherited from Compony::Component

#before_render, comp_name, #content, #exposed_intents, family_name, #id, #id_path, #id_path_hash, #inspect, #param_name, #path, #remove_content, #remove_content!, #render, #resourceful?, #root_comp, #root_comp?, setup, #sub_comp

Constructor Details

#initialize(*args, cancancan_action: :missing, disabled: false, **kwargs) ⇒ Form

Returns a new instance of Form.



6
7
8
9
10
11
# File 'lib/compony/components/form.rb', line 6

def initialize(*args, cancancan_action: :missing, disabled: false, **kwargs)
  @schema_lines_for_data = [] # Array of procs taking data returning a Schemacop proc
  @cancancan_action = cancancan_action
  @form_disabled = disabled
  super
end

Instance Method Details

#collectObject

Quick access for wrapping collections in Rails compatible format



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

def collect(...)
  Compony::ModelFields::Anchormodel.collect(...)
end

#disable!void

This method returns an undefined value.

DSL method, disables all inputs.



189
190
191
# File 'lib/compony/components/form.rb', line 189

def disable!
  @form_disabled = true
end

#fSimpleForm::FormBuilder

DSL method (inside form_fields). Returns the underlying simple_form builder, e.g. for f.rich_text_area or f.simple_fields_for (nested attributes).

Returns:

  • (SimpleForm::FormBuilder)

    The simple_form builder for the current form.



176
177
178
179
# File 'lib/compony/components/form.rb', line 176

def f
  fail("The `f` method may only be called inside `form_fields` for #{inspect}.") unless @simpleform
  return @simpleform
end

#field(name, multilang: false, **input_opts) ⇒ String+

DSL method (inside form_fields). Renders a simple_form input inferred from the model field name. Respects per-field CanCanCan authorization; skipped fields render nothing.

Parameters:

  • name (Symbol, String)

    The model field (use the association name, not the _id, for associations).

  • multilang (Boolean) (defaults to: false)

    If true, generates one suffixed input per available locale and returns the array (useful with the "mobility" gem).

  • input_opts (Hash)

    Passed to simple_form. Notable keys: as: (input type), hidden: true, autofocus:.

Returns:

  • (String, Array<String>)

    The input HTML (array when multilang).



113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# File 'lib/compony/components/form.rb', line 113

def field(name, multilang: false, **input_opts)
  fail("The `field` method may only be called inside `form_fields` for #{inspect}.") unless @simpleform

  if multilang
    I18n.available_locales.map { |locale| field("#{name}_#{locale}", **input_opts) }
  else
    name = name.to_sym

    input_opts.merge!(disabled: true) if @form_disabled

    # Check per-field authorization
    if @cancancan_action.present? && @controller.current_ability.permitted_attributes(@cancancan_action, @simpleform.object).exclude?(name)
      Rails.logger.debug do
        "Skipping form field #{name.inspect} because the current user is not allowed to perform #{@cancancan_action.inspect} on #{@simpleform.object}."
      end
      return
    end

    hidden = input_opts.delete(:hidden)
    model_field = @simpleform.object.fields[name]
    fail("Field #{name.inspect} is not defined on #{@simpleform.object.inspect} but was requested in #{inspect}.") unless model_field

    if hidden
      return model_field.simpleform_input_hidden(@simpleform, self, **input_opts)
    else
      unless @focus_given || @skip_autofocus
        input_opts[:autofocus] = true unless input_opts.key? :autofocus
        @focus_given = true
      end
      return model_field.simpleform_input(@simpleform, self, **input_opts)
    end
  end
end

#form_fields { ... } ⇒ Proc?

DSL method, use to set the form content (mandatory). The block holds the form inputs and is instance-exec'd in the form's request context where field, pw_field and f are available.

Yields:

  • Builds the form body using Dyny + the form field helpers.

Returns:

  • (Proc, nil)

    When called without a block, returns the stored block.



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

def form_fields(&block)
  return @form_fields unless block_given?
  @form_fields = block
end

#form_params(**new_form_params) ⇒ void

This method returns an undefined value.

DSL method, customizes the parameters given to simple_form_for.

Parameters:

  • new_form_params (Hash)

    Extra kwargs forwarded to simple_form_for.



197
198
199
# File 'lib/compony/components/form.rb', line 197

def form_params(**new_form_params)
  @form_params = new_form_params
end

#pw_field(name, **input_opts) ⇒ String?

DSL method (inside form_fields). Renders a password input; should be used for :password and :password_confirmation. Checks the :set_password CanCanCan ability; :hidden is intentionally unsupported here.

Parameters:

  • name (Symbol, String)

    The password field name.

  • input_opts (Hash)

    Passed to simple_form.

Returns:

  • (String, nil)

    The input HTML, or nil if not permitted.



153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/compony/components/form.rb', line 153

def pw_field(name, **input_opts)
  fail("The `pw_field` method may only be called inside `form_fields` for #{inspect}.") unless @simpleform
  name = name.to_sym

  # Check for authorization
  unless @cancancan_action.nil? || @controller.current_ability.can?(:set_password, @simpleform.object)
    Rails.logger.debug do
      "Skipping form pw_field #{name.inspect} because the current user is not allowed to perform :set_password on #{@simpleform.object}."
    end
    return
  end

  unless @focus_given || @skip_autofocus
    input_opts[:autofocus] = true unless input_opts.key? :autofocus
    @focus_given = true
  end
  return @simpleform.input name, **input_opts
end

#schema(wrapper_key) { ... } ⇒ void (protected)

This method returns an undefined value.

DSL method, replaces the form's schema and wrapper key with a completely manual Schemacop3 schema.

Parameters:

  • wrapper_key (Symbol, String)

    The top-level params wrapper key (e.g. the model's singular name).

Yields:

  • Runs in a Schemacop3 context defining the wrapped params.



273
274
275
276
277
278
279
280
# File 'lib/compony/components/form.rb', line 273

def schema(wrapper_key, &block)
  if block_given?
    @schema_wrapper_key = wrapper_key
    @schema_block = block
  else
    fail 'schema requires a block to be given'
  end
end

#schema_block_for(data, controller) ⇒ Object

Attr reader for @schema_block with auto-calculated default



78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/compony/components/form.rb', line 78

def schema_block_for(data, controller)
  if @schema_block
    return @schema_block
  else
    # If schema was not called, auto-infer a default
    local_schema_lines_for_data = @schema_lines_for_data
    return proc do
      local_schema_lines_for_data.each do |schema_line|
        schema_line_proc = schema_line.call(data, controller) # This may return nil, e.g. is the user is not authorized to set a field
        instance_exec(&schema_line_proc) unless schema_line_proc.nil?
      end
    end
  end
end

#schema_field(field_name, multilang: false) ⇒ void (protected)

This method returns an undefined value.

DSL method, whitelists a single field of data_class in the param schema, auto-generating the correct schema line. Respects per-field CanCanCan authorization.

Parameters:

  • field_name (Symbol, String)

    The model field (association name, not _id, for associations).

  • multilang (Boolean) (defaults to: false)

    If true, whitelists one suffixed field per available locale (useful with the "mobility" gem).



221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# File 'lib/compony/components/form.rb', line 221

def schema_field(field_name, multilang: false)
  if multilang
    I18n.available_locales.each { |locale| schema_field("#{field_name}_#{locale}") }
  else
    # This runs upon component setup.
    @schema_lines_for_data << proc do |data, controller|
      # This runs within a request context.
      field = data.class.fields[field_name.to_sym] || fail("No field #{field_name.to_sym.inspect} found for #{data.inspect} in #{inspect}.")
      # Check per-field authorization
      if @cancancan_action.present? && controller.current_ability.permitted_attributes(@cancancan_action.to_sym, data).exclude?(field.name.to_sym)
        Rails.logger.debug do
          "Skipping form schema_field #{field_name.inspect} because the current user is not allowed to perform #{@cancancan_action.inspect} on #{data}."
        end
        next nil
      end
      next field.schema_line
    end
  end
end

#schema_fields(*field_names) ⇒ void (protected)

This method returns an undefined value.

DSL method, whitelists several fields at once (see #schema_field).

Parameters:

  • field_names (Array<Symbol,String>)

    The model fields to whitelist.



264
265
266
# File 'lib/compony/components/form.rb', line 264

def schema_fields(*field_names)
  field_names.each { |field_name| schema_field(field_name) }
end

#schema_line { ... } ⇒ void (protected)

This method returns an undefined value.

DSL method, adds a Schemacop3 line whitelisting param(s) inside the schema's wrapper.

Yields:

  • Runs in a Schemacop3 context, e.g. str? :foo.



211
212
213
# File 'lib/compony/components/form.rb', line 211

def schema_line(&block)
  @schema_lines_for_data << proc { |_data, _controller| block }
end

#schema_pw_field(field_name) ⇒ void (protected)

This method returns an undefined value.

DSL method, whitelists a password param in the schema (checks the :set_password permission).

Parameters:

  • field_name (Symbol, String)

    The password field name.



245
246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/compony/components/form.rb', line 245

def schema_pw_field(field_name)
  # This runs upon component setup.
  @schema_lines_for_data << proc do |data, controller|
    # This runs within a request context.
    # Check per-field authorization
    unless @cancancan_action.nil? || controller.current_ability.can?(:set_password, data)
      Rails.logger.debug do
        "Skipping form schema_pw_field #{field_name.inspect} because the current user is not allowed to perform :set_password on #{data}."
      end
      next nil
    end
    next proc { obj? field_name.to_sym }
  end
end

#schema_wrapper_key_for(data) ⇒ Object

Attr reader for @schema_wrapper_key with auto-calculated default



68
69
70
71
72
73
74
75
# File 'lib/compony/components/form.rb', line 68

def schema_wrapper_key_for(data)
  if @schema_wrapper_key.present?
    return @schema_wrapper_key
  else
    # If schema was not called, auto-infer a default
    data.model_name.singular
  end
end

#skip_autofocusvoid (protected)

This method returns an undefined value.

DSL method, skips adding autofocus to the first field.



285
286
287
# File 'lib/compony/components/form.rb', line 285

def skip_autofocus
  @skip_autofocus = true
end

#with_simpleform(simpleform, controller) ⇒ Object

TODO:

Refactor? Could this be greatly simplified by having form_field to |f| ?

This method is used by render to store the simpleform instance inside the component such that we can call methods from inside form_fields. This is a workaround required because the form does not exist when the RequestContext is being built, and we want the method field to be available inside the form_fields block.



97
98
99
100
101
102
103
104
# File 'lib/compony/components/form.rb', line 97

def with_simpleform(simpleform, controller)
  @simpleform = simpleform
  @controller = controller
  @focus_given = false
  yield
  @simpleform = nil
  @controller = nil
end