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

#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(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_intents: [], 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)

    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
51
52
53
54
55
56
# 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_intents: [],
               skip_filters: [],
               default_sorting: 'id asc',
               **)
  @pagination = true
  @skip_pagination = !!skip_pagination
  @results_per_page = results_per_page
  @filtering = true
  @skip_filtering = !!skip_filtering
  @sorting = true
  @sorting_in_filter = true
  @sorting_links = true
  @skip_sorting_in_filter = !!skip_sorting || !!skip_sorting_in_filter
  @skip_sorting_links = !!skip_sorting || !!skip_sorting_links
  @columns = Compony::NaturalOrdering.new
  @row_intent_blocks = []
  @skipped_columns = skip_columns.map(&:to_sym)
  @skipped_row_intents = skip_row_intents.is_a?(Enumerable) ? skip_row_intents.map(&:to_sym) : []
  @skip_row_intents = skip_row_intents.is_a?(TrueClass)
  @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.



138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/compony/components/list.rb', line 138

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.



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

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



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

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.



187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/compony/components/list.rb', line 187

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



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

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



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

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



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

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



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

def filter_select_class(class_str)
  @filter_select_class = class_str
end

#filtering(new_filtering) ⇒ Object

DSL method Enables or disables filtering entirely (sorting is independent of this setting).



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

def filtering(new_filtering)
  @filtering = !!new_filtering
end

#filtering_enabled?Boolean (protected)

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

Returns:

  • (Boolean)


387
388
389
# File 'lib/compony/components/list.rb', line 387

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

#filters(*filter_names) ⇒ Object

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



212
213
214
# File 'lib/compony/components/list.rb', line 212

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

#pagination(new_pagination) ⇒ Object

DSL method Enables or disables pagination (caution: all records will be loaded).



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

def pagination(new_pagination)
  @pagination = !!new_pagination
end

#pagination_enabled?Boolean (protected)

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

Returns:

  • (Boolean)


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

def pagination_enabled?
  @pagination && !@skip_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.



233
234
235
236
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
# File 'lib/compony/components/list.rb', line 233

def process_data!(controller)
  fail('Data was already processed!') if @processed_data
  # Filtering
  if filtering_enabled? || sorting_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.



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

def results_per_page(new_results_per_page)
  @results_per_page = new_results_per_page
end

#row_intents(**intent_opts, &block) ⇒ Object

DSL method If a block is given: Enters the DSL where row intents can be added or removed (use from Component#setup within the component). If no block is given: Builds the declared intents for the given record and returns them (use in content or before_render, pass kwarg :data).



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

def row_intents(**intent_opts, &block)
  if block_given?
    # Enter DSL
    @row_intent_blocks << block
  else
    # Build the declared intents
    intents_ordering = NaturalOrdering.new
    @row_intent_blocks.each { |block| ManageIntentsDsl.new(intents_ordering, **intent_opts).evaluate(&block) } # this populates intents_ordering
    return intents_ordering.map!(&:payload)
  end
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.



161
162
163
# File 'lib/compony/components/list.rb', line 161

def skip_column(name)
  @skipped_columns << name.to_sym
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.



221
222
223
224
# File 'lib/compony/components/list.rb', line 221

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

#sorting(new_sorting) ⇒ Object

DSL method Enables or disables sorting entirely (both links and sorting input in filter).



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

def sorting(new_sorting)
  @sorting_in_filter = !!new_sorting
  @sorting_links = !!new_sorting
end

#sorting_enabled?Boolean (protected)

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

Returns:

  • (Boolean)


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

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

#sorting_in_filter(new_sorting_in_filter) ⇒ Object

DSL method Enables or disables sorting in filter.



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

def sorting_in_filter(new_sorting_in_filter)
  @sorting_in_filter = !!new_sorting_in_filter
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)


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

def sorting_in_filter_enabled?
  sorting_enabled? && @sorting_in_filter && !@skip_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.



412
413
414
415
416
417
418
419
420
# File 'lib/compony/components/list.rb', line 412

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

DSL method Enables or disables sorting links.



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

def sorting_links(new_sorting_links)
  @sorting_links = !!new_sorting_links
end

Returns whether generating sorting links 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_links_enabled?
  sorting_enabled? && @sorting_links && !@skip_sorting_links
end

#sorts(*names) ⇒ Object

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



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

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