Class: Compony::Components::List

Inherits:
Compony::Component show all
Includes:
Compony::ComponentMixins::Resourceful
Defined in:
lib/compony/components/list.rb

Overview

This component is used for the Rails list paradigm. It is meant to be nested inside the same family's ::Index or an owner's ::Show component.

Instance Attribute Summary

Attributes included from Compony::ComponentMixins::Resourceful

#data, #global_after_assign_attributes_block, #global_after_load_data_block, #global_assign_attributes_block, #global_load_data_block, #global_store_data_block

Attributes inherited from Compony::Component

#comp_opts, #content_blocks, #parent_comp

Instance Method Summary collapse

Methods included from Compony::ComponentMixins::Resourceful

#after_assign_attributes, #after_load_data, #assign_attributes, #data_class, #load_data, #resourceful?, #resourceful_sub_comp, #store_data

Methods inherited from Compony::Component

#action, #before_render, comp_cst, comp_name, #content, family_cst, family_name, #id, #id_path, #id_path_hash, #inspect, #param_name, #path, #remove_content, #remove_content!, #render, #render_actions, #resourceful?, #root_comp, #root_comp?, setup, #skip_action, #sub_comp

Constructor Details

#initialize(skip_pagination: false, results_per_page: 20, skip_filtering: false, skip_sorting: false, skip_sorting_in_filter: false, skip_sorting_links: false, skip_columns: [], skip_row_actions: [], skip_filters: [], default_sorting: 'id asc') ⇒ List

The following parameters are meant for the case where this component is nested and therefore instanciated by a parent comp. If the component is to configure itself, use the DSL calls below instead.

Parameters:

  • skip_pagination (Boolean) (defaults to: false)

    Disables pagination (caution: all records will be loaded)

  • results_per_page (Integer) (defaults to: 20)

    In case pagination is active, defines the amount of records to display per page.

  • skip_filtering (Boolean) (defaults to: false)

    Disables filtering entirely (sorting is independent of this setting).

  • skip_sorting (Boolean) (defaults to: false)

    Disables sorting entirely (both links and sorting input in filter).

  • skip_sorting_in_filter (Boolean) (defaults to: false)

    Disables sorting in filter.

  • skip_sorting_links (Boolean) (defaults to: false)

    Disables sorting links.

  • skip_columns (Array) (defaults to: [])

    Column names to be skipped.

  • skip_row_actions (Array) (defaults to: [])

    Row action names to be skipped.

  • skip_filters (Array) (defaults to: [])

    Filter names to be skipped.

  • default_sorting (String) (defaults to: 'id asc')

    Default sorting (only relevant for ransack based sorting)



20
21
22
23
24
25
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
# File 'lib/compony/components/list.rb', line 20

def initialize(*,
               skip_pagination: false,
               results_per_page: 20,
               skip_filtering: false,
               skip_sorting: false,
               skip_sorting_in_filter: false,
               skip_sorting_links: false,
               skip_columns: [],
               skip_row_actions: [],
               skip_filters: [],
               default_sorting: 'id asc',
               **)
  @pagination = !skip_pagination
  @results_per_page = results_per_page
  @filtering = !skip_filtering
  @sorting_in_filter = !skip_sorting && !skip_sorting_in_filter
  @sorting_links = !skip_sorting && !skip_sorting_links
  @columns = Compony::NaturalOrdering.new
  @row_actions = Compony::NaturalOrdering.new
  @skipped_columns = skip_columns.map(&:to_sym)
  @skipped_row_actions = skip_row_actions.map(&:to_sym)
  @filters = Compony::NaturalOrdering.new
  @sorts = Compony::NaturalOrdering.new
  @skipped_filters = skip_filters.map(&:to_sym)
  @default_sorting = default_sorting
  @filter_label_class = 'list-filter-label'
  @filter_input_class = 'list-filter-input'
  @filter_select_class = 'list-filter-select'
  @filter_item_wrapper_class = nil
  super(*, **)
end

Instance Method Details

#column(name, label: nil, class: nil, link_opts: {}, &block) ⇒ Object

DSL method Adds a new column to the list. If name corresponds to that of a field, everything is auto-inferred. Custom columns can be added by providing at least label and a block that will be given a record and instance-execed for every row. Please note that the column is only shown if the current user has permission to index the attribute.

Parameters:

  • name (String)

    Name of the field that is supposed to be displayed. If custom name, make sure the user has the permission to index the attribute.

  • label (String) (defaults to: nil)

    Title of the column to be displayed in the table header.

  • class (String) (defaults to: nil)

    Space-separated list of CSS classes for each cell

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

    Only used in the case of a model field, this is used to pass options to the field's value_for.

  • block (Block)

    Custom block, given the record and instance-execed in the context of the cell for every row.



132
133
134
135
136
137
138
139
140
141
142
143
144
# File 'lib/compony/components/list.rb', line 132

def column(name, label: nil, class: nil, link_opts: {}, **, &block)
  name = name.to_sym
  unless block_given?
    # Assume field column
    field = data_class.fields[name] || fail("Field #{name.inspect} was not found for data class #{data_class}")
    block = proc do |record|
      if controller.current_ability.permitted_attributes(:index, record).include?(field.name.to_sym)
        next field.value_for(record, link_to_component: :show, controller:, link_opts:).to_s
      end
    end
  end
  @columns.natural_push(name, block, label: label || field.label, class:, **)
end

#columns(*col_names) ⇒ Object

DSL method Adds multiple columns at once, sharing the same kwargs.



148
149
150
# File 'lib/compony/components/list.rb', line 148

def columns(*col_names, **)
  col_names.each { |col_name| column(col_name, **) }
end

#default_sorting(new_default_sorting) ⇒ Object

DSL method Overrides the default sorting



91
92
93
# File 'lib/compony/components/list.rb', line 91

def default_sorting(new_default_sorting)
  @default_sorting = new_default_sorting
end

#filter(name, label: nil, &block) ⇒ Object

DSL method Adds a ransack filter. If name is the name of an existing model field, the filter is auto-generated. If name is a valid Ransack search string (e.g. id_eq), all you need to pass is name and label. To create a fully custom filter, pass name and block. The block will be given the Ransack search form and should return HTML.

Parameters:

  • name (String)

    The name of the filter. Can either be the name of a field, a ransack search string or a custom name (see above).

  • label (String) (defaults to: nil)

    The text to use in the input's label.

  • block (Block)

    Custom block that will be given the Ransack search form and should produce a label and a search input.



191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# File 'lib/compony/components/list.rb', line 191

def filter(name, label: nil, **, &block)
  name = name.to_sym
  unless block_given?
    field = data_class.fields[name]
    block ||= proc do |f|
      label ||= field.label if field
      fail("You must provide a label to filter #{name.inspect}.") unless label

      if field
        filter_name = field.ransack_filter_name
        filter_input_html = capture { field.ransack_filter_input(f, filter_input_class: @filter_input_class, filter_select_class: @filter_select_class) }
      else
        filter_name = name
        filter_input_html = capture { f.search_field(filter_name, class: @filter_input_class) }
      end
      div tag.label(label, for: filter_name, class: @filter_label_class), class: @filter_item_wrapper_class
      div filter_input_html, class: @filter_item_wrapper_class
    end
  end

  @filters.natural_push(name, block, **)
end

#filter_input_class(class_str) ⇒ Object

DSL method Sets the CSS class attribute for string form inputs in filters.

Parameters:

  • class_str (String)

    Space-separated list of CSS classes



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

def filter_input_class(class_str)
  @filter_input_class = class_str
end

#filter_item_wrapper_class(class_str) ⇒ Object

DSL method Sets the CSS class attribute for the div that wraps input-related elements in filters (inputs, selects, labels).

Parameters:

  • class_str (String)

    Space-separated list of CSS classes



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

def filter_item_wrapper_class(class_str)
  @filter_item_wrapper_class = class_str
end

#filter_label_class(class_str) ⇒ Object

DSL method Sets the CSS class attribute for form label elements in filters.

Parameters:

  • class_str (String)

    Space-separated list of CSS classes



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

def filter_label_class(class_str)
  @filter_label_class = class_str
end

#filter_select_class(class_str) ⇒ Object

DSL method Sets the CSS class attribute for form select inputs in filters.

Parameters:

  • class_str (String)

    Space-separated list of CSS classes



112
113
114
# File 'lib/compony/components/list.rb', line 112

def filter_select_class(class_str)
  @filter_select_class = class_str
end

#filtering_enabled?Boolean (protected)

Returns whether filtering is possible and wanted in general (regardless of whether there are any filters defined)

Returns:

  • (Boolean)


392
393
394
# File 'lib/compony/components/list.rb', line 392

def filtering_enabled?
  @filtering && defined?(Ransack)
end

#filters(*filter_names) ⇒ Object

DSL method Adds multiple filters at once, sharing the same kwargs.



216
217
218
# File 'lib/compony/components/list.rb', line 216

def filters(*filter_names, **)
  filter_names.each { |filter_name| filter(filter_name, **) }
end

#pagination_enabled?Boolean (protected)

Returns whether pagination is enabled (regardless of whether there is more than one page)

Returns:

  • (Boolean)


412
413
414
# File 'lib/compony/components/list.rb', line 412

def pagination_enabled?
  @pagination
end

#process_data!(controller) ⇒ Object

This method must be called before the data is read for the first time. It makes the data fit for display. Only call it once.



237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/compony/components/list.rb', line 237

def process_data!(controller)
  fail('Data was already processed!') if @processed_data
  # Filtering
  if filtering_enabled?
    @q = @data.ransack(controller.params[param_name(:q)], auth_object: controller.current_ability, search_key: param_name(:q))
    @q.sorts = @default_sorting if @q.sorts.empty?
    filtered_data = @q.result.accessible_by(controller.current_ability)
  else
    filtered_data = @data
  end
  # Pagination
  if pagination_enabled?
    @page = controller.params[param_name('page')].presence&.to_i || 1
    @pagination_offset = (@page - 1) * @results_per_page
    @total_pages = (filtered_data.count.to_f / @results_per_page).ceil
    if @pagination_offset < 0 || @pagination_offset >= filtered_data.count # out of bounds check
      @page = 1
      @pagination_offset = 0
    end
    @processed_data = filtered_data.offset(@pagination_offset).limit(@results_per_page)
  else
    @processed_data = filtered_data
  end
  # Apply skips to configs
  # Exclude columns that are skipped or the user is not allowed to display
  @columns.select! do |col, _|
    @skipped_columns.exclude?(col[:name]) && controller.current_ability.permitted_attributes(:index, data_class).include?(col[:name])
  end
  # Exclude skipped filters
  @filters.select! { |filter, _| @skipped_filters.exclude?(filter[:name]) }
end

#results_per_page(new_results_per_page) ⇒ Object

DSL method In case pagination is active, defines the amount of records to display per page.



60
61
62
# File 'lib/compony/components/list.rb', line 60

def results_per_page(new_results_per_page)
  @results_per_page = new_results_per_page
end

#row_action(name, button_opts: {}, &block) ⇒ Object

DSL method Adds a row action. The very last col provides actions such as :show, :edit or :destroy. Use this method to add your own. In case the action exists as a component in the family of data_class, it is enough to pass the action's name, and the rest is auto-generated. In order to create a custom row action, pass a block that will be given the current record and instance-execed once per row, for every record.

Parameters:

  • name (Symbol, String)

    The name of the action (e.g. :edit).

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

    Only relevant in case of an auto-generated row action, this allows to configure the generated button.

  • block (Block)

    To create a custom row action; block will be given the current record and instance-execed once per row, for every record.



166
167
168
169
170
171
172
173
174
175
# File 'lib/compony/components/list.rb', line 166

def row_action(name, button_opts: {}, **, &block)
  name = name.to_sym
  unless block_given?
    block = proc do |record|
      next if Compony.comp_class_for(name, record).nil?
      compony_button(name, record, **button_opts)
    end
  end
  @row_actions.natural_push(name, block, **)
end

#skip_column(name) ⇒ Object

DSL method Marks a single column as skipped. It will not be displayed, even if it is defined.

Parameters:

  • name (Symbol, String)

    Name of the column to be skipped.



155
156
157
# File 'lib/compony/components/list.rb', line 155

def skip_column(name)
  @skipped_columns << name.to_sym
end

#skip_filtering!Object

DSL method Disables filtering entirely (sorting is independent of this setting).



66
67
68
# File 'lib/compony/components/list.rb', line 66

def skip_filtering!
  @filtering = false
end

#skip_pagination!Object

DSL method Disables pagination (caution: all records will be loaded).



54
55
56
# File 'lib/compony/components/list.rb', line 54

def skip_pagination!
  @pagination = false
end

#skip_row_action(name) ⇒ Object

DSL method Marks a single row action as skipped. It will not be displayed, even if it is defined.

Parameters:

  • name (Symbol, String)

    Name of the row action to be skipped.



180
181
182
# File 'lib/compony/components/list.rb', line 180

def skip_row_action(name)
  @skipped_row_actions << name.to_sym
end

#skip_sorting!Object

DSL method Disables sorting entirely (both links and sorting input in filter).



72
73
74
75
# File 'lib/compony/components/list.rb', line 72

def skip_sorting!
  @sorting_in_filter = false
  @sorting_links = false
end

#skip_sorting_in_filter!Object

DSL method Disables sorting in filter.



79
80
81
# File 'lib/compony/components/list.rb', line 79

def skip_sorting_in_filter!
  @sorting_in_filter = false
end

#skip_sorting_links!Object

DSL method Disables sorting links.



85
86
87
# File 'lib/compony/components/list.rb', line 85

def skip_sorting_links!
  @sorting_links = false
end

#sort(name, label: nil) ⇒ Object

DSL method Adds a sorting criterion that will be processed by ransack. data_class must be sortable by this criterion. See Ransack's sorting for constraints. For every call of this method, one sorting link and two entries (asc, desc) in the sorting-in-filter feature will be generated, if enabled.

Parameters:

  • name (Symbol, String)

    Sorting criteria, e.g. :id or :label.

  • label (String) (defaults to: nil)

    Label of the sorting link / entries.



225
226
227
228
# File 'lib/compony/components/list.rb', line 225

def sort(name, label: nil)
  label ||= data_class.fields[name].label
  @sorts.natural_push(name.to_sym, nil, label:)
end

#sorting_enabled?Boolean (protected)

Returns whether sorting is possible and wanted in general (regardless of whether there are any sorts defined)

Returns:

  • (Boolean)


397
398
399
# File 'lib/compony/components/list.rb', line 397

def sorting_enabled?
  (@sorting_in_filter || @sorting_links) && defined?(Ransack)
end

#sorting_in_filter_enabled?Boolean (protected)

Returns whether sorting in filter is possible and wanted in general (regardless of whether there are any sorts defined)

Returns:

  • (Boolean)


402
403
404
# File 'lib/compony/components/list.rb', line 402

def sorting_in_filter_enabled?
  sorting_enabled? && @sorting_in_filter
end

#sorting_in_filter_select_optsObject (protected)

Returns the select options for sorting suitable for passing in a f.select. Used in sorting-in-filter feature. Useful for custom subclasses of List.



417
418
419
420
421
422
423
424
425
# File 'lib/compony/components/list.rb', line 417

def sorting_in_filter_select_opts
  @sorts.flat_map do |sort|
    %w[asc desc].map do |order|
      label = "#{sort[:label]} #{order == 'asc' ? '' : ''}"
      value = "#{sort[:name]} #{order}"
      [label, value]
    end
  end
end

Returns whether generating sorting links is possible and wanted in general (regardless of whether there are any sorts defined)

Returns:

  • (Boolean)


407
408
409
# File 'lib/compony/components/list.rb', line 407

def sorting_links_enabled?
  sorting_enabled? && @sorting_links
end

#sorts(*names) ⇒ Object

DSL method Adds multiple sorts at once, sharing the same kwargs.



232
233
234
# File 'lib/compony/components/list.rb', line 232

def sorts(*names, **)
  names.each { |name| sort(name, **) }
end