bulma-phlex-rails 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 44794453fc2f23bad30359fbe4c2334a3f01f492e883d149752eb7855d22926e
4
- data.tar.gz: 63fafad11cdb41bb3c26e768bb9042faea483befb1802c0741b9c054cd9262a9
3
+ metadata.gz: 39faa4f9308c253fc3ee0c25e584fc0381ccce026a07cb3790467d0568a128fd
4
+ data.tar.gz: fc56dde51af79f608200b67d8efa1c2fb09f0079298bfb7ac3e8b366b5baf2fc
5
5
  SHA512:
6
- metadata.gz: 88e3734a644ae73ccde892ddbae655a6e3f5e18ae1d3ec5ed951822d4ec3a1c4bd903f0f315e8811860929d4e473894e4dfd536da4c39b12bad59230459036d4
7
- data.tar.gz: f36bcded467eacbc374f08b75a8033aa0e9089de882f2363c9d51f547803b7b9586478ddbdb2377ec4bb2f3091b967a6bd901f18a184b85cdeae3d170268b0d4
6
+ metadata.gz: 11f36f93ef95547edc84227583f99925a16c61209eb0af39722a0b1c5b77499d1be079536133a7a36115492d957ac3a55bb9bf214e9aeed8cb0bb231677479ac
7
+ data.tar.gz: 3bf6f35846e31edd272faecd6764640f5b18435e605649fb5775030006329d6af8f830b9631537f364be7f8215e88168814c6de49aa46d6c2d15eb76c9606914
@@ -0,0 +1,50 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ //
4
+ // The controller also supports mixins, which are additional behaviors that can be added to the
5
+ // controller. Mixins are defined in the application customControllerConfiguration and can be
6
+ // used to extend the functionality of the controller. Mixins are defined as objects with a
7
+ // beforeAdd method, which is called before the add action is executed. The mixin can modify the
8
+ // template before it is added to the container. The mixin can also return false to prevent the
9
+ // add action from being executed.
10
+ export default class extends Controller {
11
+ static values = {
12
+ templateId: String,
13
+ containerSelector: String,
14
+ position: { type: String, default: "beforeend" },
15
+ mixin: String,
16
+ };
17
+
18
+ initialize() {
19
+ if (this.hasMixinValue) {
20
+ const controllerMixins = this.application.customControllerConfiguration[this.identifier];
21
+
22
+ if (controllerMixins && controllerMixins[this.mixinValue]) {
23
+ Object.assign(this, controllerMixins[this.mixinValue]);
24
+ } else {
25
+ console.warn(`Mixin '${this.mixinValue}' not found for ${this.identifier} controller`);
26
+ }
27
+ }
28
+ }
29
+
30
+ add(event) {
31
+ const template = this.templateTarget.cloneNode(true);
32
+
33
+ if (this.beforeAdd(event, template.content)) {
34
+ const content = template.innerHTML.replace(/NEW_RECORD/g, new Date().getTime().toString());
35
+ this.containerTarget.insertAdjacentHTML(this.positionValue, content);
36
+ }
37
+ }
38
+
39
+ beforeAdd(_event, _node) {
40
+ return true;
41
+ }
42
+
43
+ get templateTarget() {
44
+ return document.getElementById(this.templateIdValue);
45
+ }
46
+
47
+ get containerTarget() {
48
+ return document.querySelector(this.containerSelectorValue);
49
+ }
50
+ }
@@ -0,0 +1,34 @@
1
+ import { Controller } from "@hotwired/stimulus";
2
+
3
+ export default class extends Controller {
4
+ static values = {
5
+ rowSelector: String,
6
+ };
7
+
8
+ remove(event) {
9
+ event.preventDefault();
10
+
11
+ const row = this.#surroundingRow(event);
12
+ const parent = row.parentElement;
13
+ if (row) {
14
+ row.remove();
15
+ this.dispatch("row-removed", { target: parent });
16
+ }
17
+ }
18
+
19
+ markForDestruction(event) {
20
+ event.preventDefault();
21
+ const row = this.#surroundingRow(event);
22
+ const destroyField = row.querySelector('input[name*="_destroy"]');
23
+
24
+ if (destroyField) {
25
+ destroyField.value = true;
26
+ row.classList.add("is-hidden");
27
+ this.dispatch("row-marked-for-destruction", { detail: { row: row } });
28
+ }
29
+ }
30
+
31
+ #surroundingRow(event) {
32
+ return event.target.closest(this.rowSelectorValue);
33
+ }
34
+ }
@@ -35,12 +35,10 @@ module BulmaPhlex
35
35
  end
36
36
 
37
37
  def view_template
38
- render FormField.new(**@form_field_options) do |f|
39
- f.control do
40
- @form_builder.label(@method, nil, class: "checkbox", skip_label_class: true) do |label_builder|
41
- raw @delivered.call(@options)
42
- render_label(label_builder)
43
- end
38
+ render FormField.new(**@form_field_options) do
39
+ @form_builder.label(@method, nil, class: "checkbox", skip_label_class: true) do |label_builder|
40
+ raw @delivered.call(@options)
41
+ render_label(label_builder)
44
42
  end
45
43
  end
46
44
  end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaPhlex
4
+ module Rails
5
+ # # Base Display
6
+ #
7
+ # Base class for read-only display fields styled with Bulma.
8
+ #
9
+ # This allows the model to be passed via the `model:` option
10
+ # or as the first argument. When the model is passed as an option,
11
+ # the method is assumed to be the first argument.
12
+ class BaseDisplay < BulmaPhlex::Base
13
+ # Keyword arguments accepted by BulmaPhlex::FormField
14
+ FORM_FIELD_OPTIONS = BulmaPhlex::FormField
15
+ .instance_method(:initialize)
16
+ .parameters
17
+ .map { |_, name| name }
18
+ .freeze
19
+
20
+ def initialize(model, method = nil, **options)
21
+ @model = options.fetch(:model, model)
22
+ @method = method || model
23
+ @options = options
24
+ end
25
+
26
+ def view_template
27
+ form_field_options = @options.slice(*FORM_FIELD_OPTIONS)
28
+ BulmaPhlex::FormField(**form_field_options) do |field|
29
+ field.label { label(class: "label") { label_text } }
30
+ field.control { control_content }
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def label_text
37
+ model_name = @model.class.name.parameterize(separator: "_")
38
+ ActionView::Helpers::Tags::Translator.new(@model, model_name, @method, scope: "helpers.label").translate
39
+ end
40
+
41
+ def control_content
42
+ input type: "text", class: "input is-light", value: formatted_value, readonly: :readonly
43
+ end
44
+
45
+ def value
46
+ @model.public_send(@method)
47
+ end
48
+
49
+ def formatted_value
50
+ raise NotImplementedError, "Subclasses must implement the formatted_value method"
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaPhlex
4
+ module Rails
5
+ # # Currency Display
6
+ #
7
+ # A read-only display field styled with Bulma that formats a numeric value as
8
+ # currency. This component leverages Rails' `number_to_currency` helper
9
+ # for formatting.
10
+ #
11
+ # Currency options can be passed as a hash to customize the formatting.
12
+ #
13
+ # #### Arguments
14
+ #
15
+ # - `model`: ActiveRecord Model - The model containing the currency attribute. This can also be passed
16
+ # via the `options` (helpful when using `with_options`).
17
+ # - `method`: Symbol or String - The attribute method name for the currency field.
18
+ # - `options`: Hash - Additional options for the display field. This can include the
19
+ # `currency_options` key, which should be a hash of options passed to `number_to_currency`.
20
+ class CurrencyDisplay < BaseDisplay
21
+ include Phlex::Rails::Helpers::NumberToCurrency
22
+
23
+ def initialize(model, method = nil, **options)
24
+ super(model, method, **options.except(:currency_options))
25
+ @currency_options = options.fetch(:currency_options, {})
26
+ end
27
+
28
+ private
29
+
30
+ def formatted_value
31
+ number_to_currency(value, @currency_options)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaPhlex
4
+ module Rails
5
+ # # Formatted Display
6
+ #
7
+ # A read-only display field styled with Bulma. This requires a `format` option,
8
+ # which will be passed to `to_fs` for formatting the value.
9
+ #
10
+ # #### Arguments
11
+ #
12
+ # - `model`: ActiveRecord Model - The model containing the attribute to display.
13
+ # - `method`: Symbol or String - The attribute method name for the field.
14
+ # - `options`: Hash - Additional Bulma form field options can be passed, such as `:help`,
15
+ # `:icon_left`, `:icon_right`, `:column`, and `:grid`. Include a `format` key to specify
16
+ # the format to use with `to_fs`.
17
+ class FormattedDisplay < TextDisplay
18
+ def initialize(model, method = nil, **options)
19
+ super(model, method, **options.except(:format))
20
+ @format = options.fetch(:format, :default)
21
+ end
22
+
23
+ private
24
+
25
+ def formatted_value
26
+ value.to_fs(@format)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaPhlex
4
+ module Rails
5
+ # # Text Display
6
+ #
7
+ # A read-only text display field styled with Bulma.
8
+ #
9
+ # An optional formatter can be provided to customize the display of the text
10
+ # value, such as &:titleize or a custom lambda.
11
+ #
12
+ # #### Arguments
13
+ #
14
+ # - `model`: ActiveRecord Model - The model containing the text attribute.
15
+ # - `method`: Symbol or String - The attribute method name for the text field.
16
+ # - `options`: Hash - Additional Bulma form field options can be passed, such as `:help`,
17
+ # `:icon_left`, `:icon_right`, `:column`, and `:grid`. Use the `formatter` key to provide
18
+ # a block or lambda for custom text formatting.
19
+ class TextDisplay < BaseDisplay
20
+ def initialize(model, method, **options)
21
+ super(model, method, **options.except(:formatter))
22
+ @formatter = options[:formatter]
23
+ end
24
+
25
+ private
26
+
27
+ def formatted_value
28
+ text = value.to_s
29
+ @formatter ? @formatter.call(text) : text
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaPhlex
4
+ module Rails
5
+ # # Nested Form Add Button Component
6
+ #
7
+ # This button can be added to a form to allow users to dynamically add new nested form rows. It uses
8
+ # the Stimulus controller NestedFormsAddRow to handle the addition logic.
9
+ #
10
+ # #### Arguments
11
+ #
12
+ # - `template_id`: String - The ID of the `<template>` element that contains the nested form fields to be added.
13
+ # - `container_selector`: String - A CSS selector that identifies the container element where new rows should
14
+ # be added.
15
+ # - `label`: String (optional) - The text label to display on the button.
16
+ # - `icon_left`: String (optional) - The name of an icon to display on the left side of the button.
17
+ # - `icon_right`: String (optional) - The name of an icon to display on the right side of the button.
18
+ class NestedFormAddButton < BulmaPhlex::Base
19
+ def initialize(template_id:, # rubocop:disable Metrics/ParameterLists
20
+ container_selector:,
21
+ label: nil,
22
+ icon_left: nil,
23
+ icon_right: nil,
24
+ **html_attributes)
25
+ @template_id = template_id
26
+ @label = label
27
+ @icon_left = icon_left
28
+ @icon_right = icon_right
29
+ @container_selector = container_selector
30
+ @html_attributes = html_attributes
31
+ end
32
+
33
+ def view_template
34
+ render BulmaPhlex::FormField.new(icon_right: @icon) do
35
+ button(**mix({ type: "button", class: "button", data: stimulus_controller }, @html_attributes)) do
36
+ render BulmaPhlex::Icon(@icon_left) if @icon_left
37
+ span { @label } if @label
38
+ render BulmaPhlex::Icon(@icon_right) if @icon_right
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def stimulus_controller
46
+ { controller: "bulma-phlex--nested-forms-add-row",
47
+ bulma_phlex__nested_forms_add_row_container_selector_value: @container_selector,
48
+ bulma_phlex__nested_forms_add_row_template_id_value: @template_id,
49
+ action: "bulma-phlex--nested-forms-add-row#add" }
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaPhlex
4
+ module Rails
5
+ # # Nested Form Delete Button Component
6
+ #
7
+ # This button can be added to a nested form row to allow users to remove the row. It uses
8
+ # the Stimulus controller NestedFormsDeleteRow to handle the deletion logic.
9
+ #
10
+ # #### Arguments
11
+ #
12
+ # - `row_selector`: String - A CSS selector that identifies the row to be deleted. This is
13
+ # passed to the element's `closest` method to find the row element.
14
+ # - `action`: String - The action to perform when the button is clicked. This should correspond
15
+ # to a method in the NestedFormsDeleteRow Stimulus controller. Either "remove" to remove the row
16
+ # from the DOM, or "markForDestruction" to mark the row for destruction (for existing records).
17
+ # - `label`: String (optional) - The text label to display on the button.
18
+ # - `icon_left`: String (optional) - The name of an icon to display on the left side of the button.
19
+ # - `icon_right`: String (optional) - The name of an icon to display on the right side of the button.
20
+ class NestedFormDeleteButton < BulmaPhlex::Base
21
+ def initialize(row_selector:, # rubocop:disable Metrics/ParameterLists
22
+ action:,
23
+ label: nil,
24
+ icon_left: nil,
25
+ icon_right: nil,
26
+ **html_attributes)
27
+ @label = label
28
+ @icon_left = icon_left
29
+ @icon_right = icon_right
30
+ @row_selector = row_selector
31
+ @action = action
32
+ @html_attributes = html_attributes
33
+ end
34
+
35
+ def view_template
36
+ render BulmaPhlex::FormField.new(icon_right: @icon) do
37
+ button(**mix({ type: "button", class: "button", data: stimulus_controller }, @html_attributes)) do
38
+ render BulmaPhlex::Icon(@icon_left) if @icon_left
39
+ span { @label } if @label
40
+ render BulmaPhlex::Icon(@icon_right) if @icon_right
41
+ end
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def stimulus_controller
48
+ { controller: "bulma-phlex--nested-forms-delete-row",
49
+ bulma_phlex__nested_forms_delete_row_row_selector_value: @row_selector,
50
+ action: "bulma-phlex--nested-forms-delete-row##{@action}" }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaPhlex
4
+ module Rails
5
+ # # Radio Button Component
6
+ #
7
+ # This component renders a Bulma-styled radio button within a form. It integrates with Rails' form builder
8
+ # to ensure proper labeling and value handling.
9
+ #
10
+ # The label is looked up using I18n based on the method and tag value. The key is the combination of the
11
+ # method name and the tag value, formatted as `method_tagvalue`, under the scope `helpers.label.object_name`.
12
+ # If no translation is found, it defaults to a humanized version of the tag value.
13
+ #
14
+ # Example usage:
15
+ # ```ruby
16
+ # form_with model: @project do |f|
17
+ # f.radio_button :status, "active"
18
+ # f.radio_button :status, "archived"
19
+ # end
20
+ class RadioButton < BulmaPhlex::Base
21
+ def initialize(form_builder, method, tag_value, options, delivered)
22
+ @form_builder = form_builder
23
+ @method = method
24
+ @tag_value = tag_value
25
+ @options = options.dup
26
+ @delivered = delivered
27
+
28
+ @options[:class] = Array.wrap(@options[:class]) << "mr-2"
29
+ @form_field_options = @options.extract!(:column, :grid)
30
+ .with_defaults(column: @form_builder.columns_flag,
31
+ grid: @form_builder.grid_flag)
32
+ end
33
+
34
+ def view_template
35
+ render FormField.new(**@form_field_options) do
36
+ label(class: "radio") do
37
+ raw @delivered.call(@options)
38
+ plain label_from_tag_value
39
+ end
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def label_from_tag_value
46
+ key = [@method, @tag_value.to_s.downcase.gsub(/\s+/, "_")].join("_")
47
+ I18n.translate(key, scope: [:helpers, :label, @form_builder.object_name],
48
+ default: @tag_value.to_s.humanize)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaPhlex
4
+ module Rails
5
+ # # Displayable Block Options
6
+ #
7
+ # This concern provides methods to manage display options for Bulma Phlex components,
8
+ # specifically for displayable blocks such as read-only form fields. It allows options
9
+ # to be set for groups of fields, enabling consistent styling and layout.
10
+ #
11
+ # #### Included Methods
12
+ #
13
+ # - `with_options` - Wrap a block with specific options for displayable fields.
14
+ # - `in_columns` - Wrap a block to display fields in Bulma columns.
15
+ # - `in_grid` - Wrap a block to display fields in a Bulma grid.
16
+ module DisplayableBlockOptions
17
+ # Wrap a block with specific options for displayable fields. This allows redundant
18
+ # options to be specified once for multiple fields. These options are merged with
19
+ # any options passed directly to the individual field methods.
20
+ #
21
+ # If the `:columns` or `:grid` option is provided, the block will be wrapped
22
+ # in a Bulma `columns` or `grid` container, respectively.
23
+ #
24
+ # Blocks can be nested, with inner block options overriding outer block options.
25
+ #
26
+ # #### Example
27
+ #
28
+ # with_options(model: @user, column: true) do
29
+ # show_text(:username)
30
+ # show_date(birthdate, format: :short)
31
+ # end
32
+ def with_options(**block_options, &)
33
+ (@_block_display_options ||= BlockDisplayOptions.new).push(block_options)
34
+
35
+ if @_block_display_options.column?
36
+ render BulmaPhlex::Columns.new(**@_block_display_options.column_options, &)
37
+ elsif @_block_display_options.grid?
38
+ render BulmaPhlex::Grid.new(**@_block_display_options.grid_options, &)
39
+ else
40
+ yield
41
+ end
42
+
43
+ @_block_display_options.pop
44
+ end
45
+
46
+ # Retrieve the current block display options.
47
+ def block_display_options
48
+ @_block_display_options&.current || {}
49
+ end
50
+
51
+ # Wrap a block to display fields in Bulma columns. This adds a columns container
52
+ # and sets the `column: true` option for any `show_` fields within the block.
53
+ #
54
+ # This is a shorthand for `with_options(column: true) do`.
55
+ def in_columns(options = true, &) # rubocop:disable Style/OptionalBooleanParameter
56
+ with_options(columns: options, &)
57
+ end
58
+
59
+ # Wrap a block to display fields in a Bulma grid. This adds a grid container
60
+ # and sets the `grid: true` option for any `show_` fields within the block.
61
+ #
62
+ # You can optionally pass in any [Grid options](https://github.com/RockSolt/bulma-phlex#grid) as a hash.
63
+ def in_grid(options = true, &) # rubocop:disable Style/OptionalBooleanParameter
64
+ with_options(grid: options, &)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaPhlex
4
+ module Rails
5
+ # # Displayable Form Fields
6
+ #
7
+ # Phlex component mixin to provide displayable form fields to views. These read-only
8
+ # fields provide the same Bulma styling as standard form fields, but are intended for
9
+ # displaying data rather than accepting user input.
10
+ #
11
+ # #### Included Methods
12
+ #
13
+ # - `show_currency` - Display a currency field in a read-only Bulma-styled format.
14
+ # - `show_date` - Display a date field in a read-only Bulma-styled format.
15
+ # - `show_text` - Display a text field in a read
16
+ module DisplayableFormFields
17
+ include BulmaPhlex::Rails::DisplayableBlockOptions
18
+
19
+ # Display a currency field in a read-only Bulma-styled format. Include a `currency_format` option
20
+ # to customize the currency format, which will be passed to Rails' `number_to_currency` helper.
21
+ #
22
+ # #### Arguments
23
+ #
24
+ # - `model`: ActiveRecord Model - The model containing the currency attribute. This can also be passed
25
+ # via the `options` (helpful when using `with_options`).
26
+ # - `method`: Symbol or String - The attribute method name for the currency field.
27
+ # - `options`: Hash - Additional options for the display field. This can include the
28
+ # `currency_options` key, which should be a hash of options passed to `number_to_currency`.
29
+ #
30
+ # #### Example
31
+ #
32
+ # show_currency(@order, :total_amount)
33
+ # show_currency(@product, :price, currency_options: { unit: "€", precision: 2 })
34
+ def show_currency(model, method = nil, **options)
35
+ render BulmaPhlex::Rails::CurrencyDisplay.new(model, method,
36
+ **options.with_defaults(block_display_options))
37
+ end
38
+
39
+ # Display a date field in a read-only Bulma-styled format. Include a `format` option
40
+ # to customize the date format, which will be passed to `to_fs`.
41
+ # Defaults to `:long` format.
42
+ #
43
+ # #### Arguments
44
+ #
45
+ # - `model`: ActiveRecord Model - The model containing the date attribute. This can also be passed
46
+ # via the `options` (helpful when using `with_options`).
47
+ # - `method`: Symbol or String - The attribute method name for the date field.
48
+ # - `options`: Hash - Additional options for the display field. This can include the `format` key, which gets
49
+ # passed to `to_fs`.
50
+ #
51
+ # #### Example
52
+ #
53
+ # show_date(@user, :birthdate, format: :short)
54
+ # show_date(@event, :start_time)
55
+ # show_date(:scheduled_at, model: @appointment, format: "%B %d, %Y")
56
+ def show_date(model, method = nil, **options)
57
+ date_options = options.with_defaults(format: :long)
58
+ .with_defaults(block_display_options)
59
+ render BulmaPhlex::Rails::FormattedDisplay.new(model, method, **date_options)
60
+ end
61
+
62
+ # Display a text field in a read-only Bulma-styled format. A custom formatter
63
+ # block can be provided to modify the displayed text.
64
+ #
65
+ # #### Arguments
66
+ # - `model`: ActiveRecord Model - The model containing the text attribute. This can also be passed
67
+ # via the `options` (helpful when using `with_options`).
68
+ # - `method`: Symbol or String - The attribute method name for the text field.
69
+ # - Additional Bulma form field options can be passed, such as `:help`, `:icon_left`, `:icon_right`,
70
+ # `:column`, and `:grid`.
71
+ #
72
+ # #### Example
73
+ #
74
+ # show_text(@user, :username)
75
+ # show_text(@article, :title, &:titleize)
76
+ # show_text(:email, model: @contact, help: "Primary contact email")
77
+ def show_text(model, method = nil, **options, &formatter)
78
+ text_options = options.with_defaults(block_display_options)
79
+ render BulmaPhlex::Rails::TextDisplay.new(model, method, **text_options, formatter: formatter)
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaPhlex
4
+ module Rails
5
+ # # Nested Forms
6
+ #
7
+ # This modules provides support for nested forms. It overrides the `fields_for` method to
8
+ # capture the block for nested attributes then creates a template for adding new nested
9
+ # records dynamically when the `nested_form_add_button` method is called.
10
+ #
11
+ # The nested form can also include a delete button for each row using the
12
+ # `nested_form_delete_button` method.
13
+ module NestedForms
14
+ extend ActiveSupport::Concern
15
+
16
+ included do
17
+ attr_reader :nested_forms_add_buttons, :nested_forms_templates
18
+ end
19
+
20
+ def fields_for(record_name, record_object = nil, fields_options = {}, &block)
21
+ output = super
22
+
23
+ if (record_name.is_a?(Symbol) || record_name.is_a?(String)) && nested_attributes_association?(record_name)
24
+ if @nested_forms_add_buttons&.include?(record_name)
25
+ output += build_fields_for_template(record_name, fields_options, &block)
26
+ else
27
+ (@nested_forms_templates ||= {}).store(record_name, [fields_options, block])
28
+ end
29
+ end
30
+
31
+ output
32
+ end
33
+
34
+ # Add a button to add new nested form rows dynamically.
35
+ #
36
+ # #### Arguments
37
+ #
38
+ # - `record_name`: Symbol or String - The name of the nested association (e.g., :tasks).
39
+ # - `container`: String - A CSS selector that identifies the container element where new rows
40
+ # should be added.
41
+ # - `label`: String (optional) - The text label to display on the button.
42
+ # - `icon`: String (optional) - The name of an icon to display on the button (appears on the left by default).
43
+ # - `icon_left`: String (optional) - The name of an icon to display on the left side of the button.
44
+ # - `icon_right`: String (optional) - The name of an icon to display on the right side of the button.
45
+ # - Additional HTML attributes can be passed via `html_attributes`.
46
+ def nested_form_add_button(record_name, # rubocop:disable Metrics/ParameterLists
47
+ container:,
48
+ label: nil,
49
+ icon: nil,
50
+ icon_left: nil,
51
+ icon_right: nil,
52
+ **html_attributes)
53
+ button_html = build_nested_form_add_button(record_name, container, label, icon_left || icon, icon_right,
54
+ html_attributes)
55
+
56
+ if @nested_forms_templates&.key?(record_name)
57
+ field_options, block = @nested_forms_templates[record_name]
58
+ button_html + build_fields_for_template(record_name, field_options, &block)
59
+ else
60
+ (@nested_forms_add_buttons ||= []) << record_name
61
+ button_html
62
+ end
63
+ end
64
+
65
+ # Add a button to delete a nested form row.
66
+ #
67
+ # #### Arguments
68
+ #
69
+ # - `row_selector`: String - A CSS selector that identifies the row to be deleted. This is
70
+ # passed to the element's `closest` method to find the row element.
71
+ # - `label`: String (optional) - The text label to display on the button.
72
+ # - `icon`: String (optional) - The name of an icon to display on the button (appears on the left by default).
73
+ # - `icon_left`: String (optional) - The name of an icon to display on the left side of the button.
74
+ # - `icon_right`: String (optional) - The name of an icon to display on the right side of the button.
75
+ # - Additional HTML attributes can be passed via `html_attributes`.
76
+ def nested_form_delete_button(row_selector:, # rubocop:disable Metrics/ParameterLists
77
+ label: nil,
78
+ icon: nil,
79
+ icon_left: nil,
80
+ icon_right: nil,
81
+ **html_attributes)
82
+ action = object.persisted? ? "markForDestruction" : "remove"
83
+
84
+ build_nested_form_delete_button(label, icon_left || icon, icon_right, row_selector, action, html_attributes) +
85
+ hidden_field(:_destroy)
86
+ end
87
+
88
+ private
89
+
90
+ def build_fields_for_template(record_name, fields_options, &block)
91
+ reflection = @object.class.reflect_on_association(record_name.to_sym)
92
+ new_record = reflection.klass.new
93
+
94
+ name = "#{@object_name}[#{record_name}_attributes][NEW_RECORD]"
95
+ @template.content_tag(:template, id: "#{@object_name}_#{record_name}_fields_template") do
96
+ fields_for_nested_model(name, new_record, fields_options.merge(child_index: "NEW_RECORD"), block)
97
+ end
98
+ end
99
+
100
+ def build_nested_form_add_button(record_name, # rubocop:disable Metrics/ParameterLists
101
+ container,
102
+ label,
103
+ icon_left,
104
+ icon_right,
105
+ html_attributes)
106
+ BulmaPhlex::Rails::NestedFormAddButton.new(
107
+ template_id: "#{@object_name}_#{record_name}_fields_template",
108
+ label: label,
109
+ icon_left: icon_left,
110
+ icon_right: icon_right,
111
+ container_selector: container,
112
+ **html_attributes
113
+ ).render_in(@template)
114
+ end
115
+
116
+ def build_nested_form_delete_button(label, # rubocop:disable Metrics/ParameterLists
117
+ icon_left,
118
+ icon_right,
119
+ row_selector,
120
+ action,
121
+ html_attributes)
122
+ BulmaPhlex::Rails::NestedFormDeleteButton.new(
123
+ label: label,
124
+ icon_left: icon_left,
125
+ icon_right: icon_right,
126
+ row_selector: row_selector,
127
+ action: action,
128
+ **html_attributes
129
+ ).render_in(@template)
130
+ end
131
+ end
132
+ end
133
+ end
@@ -12,7 +12,15 @@ module BulmaPhlex
12
12
  # - `icon_right`: If set, the specified icon will be rendered on the right side of the input.
13
13
  # - `column`: If true, the input will be wrapped in a Bulma column (only within a `columns` block).
14
14
  # - `grid`: If true, the input will be wrapped in a Bulma grid cell (only within a `grid` block).
15
+ #
16
+ # ## Nested Forms
17
+ #
18
+ # This form builder also includes support for nested forms. Invoke the `nested_form_add_button`
19
+ # method to add a button that allows users to dynamically add new nested form rows. Each
20
+ # nested form row can include a delete button using the `nested_form_delete_button` method.
15
21
  class FormBuilder < ActionView::Helpers::FormBuilder
22
+ include NestedForms
23
+
16
24
  attr_reader :columns_flag, :grid_flag
17
25
 
18
26
  def text_field(method, options = {}) = wrap_field(method, options) { |m, opts| super(m, opts) }
@@ -45,6 +53,11 @@ module BulmaPhlex
45
53
  end
46
54
  alias check_box checkbox
47
55
 
56
+ def radio_button(method, tag_value, options = {})
57
+ delivered = ->(opts) { super(method, tag_value, opts) }
58
+ RadioButton.new(self, method, tag_value, options, delivered).render_in(@template)
59
+ end
60
+
48
61
  # Override label to add Bulma's `label` class by default. Add `:skip_label_class` option
49
62
  # to skip adding the class.
50
63
  def label(method, text = nil, options = {}, &)
@@ -78,36 +91,55 @@ module BulmaPhlex
78
91
 
79
92
  # Fields declared in a column block will be wrapped in a Bulma column and carry
80
93
  # the `column` class by default (fields can use the `column` option to set sizes).
81
- def columns(min_breakpoint = nil, &)
94
+ #
95
+ # ## Arguments
96
+ #
97
+ # - `minimum_breakpoint`: (Symbol, optional) Sets the minimum breakpoint for the columns; default is `:tablet`.
98
+ # - `multiline`: (Boolean, optional) If true, allows the columns to wrap onto multiple lines.
99
+ # - `gap`: (optional) Use an integer (0-8) to set the gap size between columns; use a hash keyed by breakpoints
100
+ # to set responsive gap sizes.
101
+ # - `centered`: (Boolean, optional) If true, centers the columns.
102
+ # - `vcentered`: (Boolean, optional) If true, vertically centers the columns.
103
+ def columns(minimum_breakpoint: nil,
104
+ multiline: false,
105
+ gap: nil,
106
+ centered: false,
107
+ vcentered: false, &)
82
108
  @columns_flag = true
83
109
  columns = @template.capture(&)
84
110
  @columns_flag = false
85
111
 
86
- @template.content_tag(:div, class: [:columns, min_breakpoint]) do
112
+ BulmaPhlex::Columns.new(minimum_breakpoint:, multiline:, gap:, centered:, vcentered:) do
87
113
  @template.concat(columns)
88
- end
89
- end
90
-
91
- # Fields declared in a fixed_grid block will be wrapped in a Bulma fixed grid and
92
- # carry the `grid` class by default (fields can use the `grid` option to set sizes).
93
- def fixed_grid(&)
94
- # TODO: Use BulmaPhlex::FixedGrid for more options
95
- @template.content_tag(:div, class: "fixed-grid") do
96
- grid(&)
97
- end
114
+ end.render_in(@template)
98
115
  end
99
116
 
100
117
  # Fields declared in a grid block will be wrapped in a Bulma fixed grid and carry
101
118
  # the `grid` class by default (fields can use the `grid` option to set sizes).
102
- def grid(&)
119
+ #
120
+ # ## Arguments
121
+ #
122
+ # - `fixed_columns`: (Integer, optional) Specifies a fixed number of columns for the grid.
123
+ # - `auto_count`: (Boolean, optional) If true, the grid will automatically adjust the number
124
+ # of columns based on the content.
125
+ # - `minimum_column_width`: (Integer 1-32, optional) Sets a minimum width for the columns in the grid.
126
+ # - `gap`: (optional) Sets the gap size between grid items from 1-8 with 0.5 increments.
127
+ # - `column_gap`: (optional) Sets the column gap size between grid items from 1-8 with 0.5 increments.
128
+ # - `row_gap`: (optional) Sets the row gap size between grid items from 1-8 with 0.5 increments.
129
+ def grid(fixed_columns: nil, # rubocop:disable Metrics/ParameterLists
130
+ auto_count: false,
131
+ minimum_column_width: nil,
132
+ gap: nil,
133
+ column_gap: nil,
134
+ row_gap: nil,
135
+ &)
103
136
  @grid_flag = true
104
137
  cells = @template.capture(&)
105
138
  @grid_flag = false
106
139
 
107
- # TODO: Use BulmaPhlex::Grid for more options
108
- @template.content_tag(:div, class: "grid") do
140
+ BulmaPhlex::Grid.new(fixed_columns:, auto_count:, minimum_column_width:, gap:, column_gap:, row_gap:) do
109
141
  @template.concat(cells)
110
- end
142
+ end.render_in(@template)
111
143
  end
112
144
 
113
145
  private
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BulmaPhlex
4
+ module Rails
5
+ # # Block Display Options
6
+ #
7
+ # This model enables options to be collected in a nested fashion. It translates
8
+ # column and grid options into a flags for the form fields and stores the full
9
+ # options for the column and grid containers.
10
+ #
11
+ # One small difference between the two is that the column options are specified
12
+ # with a plural (`columms`) in `with_options` but the flag that gets passed to the
13
+ # form fields is singular (`column: true`). For grid options, both are singular
14
+ # (`grid`).
15
+ class BlockDisplayOptions
16
+ def initialize
17
+ @stack = [{}]
18
+ @grid_options = []
19
+ @column_options = []
20
+ end
21
+
22
+ def push(options)
23
+ options = options.dup
24
+
25
+ # store the columns option, then add a boolean flag
26
+ @column_options.push(options.delete(:columns))
27
+ options[:column] = !!@column_options.last # rubocop:disable Style/DoubleNegation
28
+
29
+ # store the grid option, then convert it to a boolean flag
30
+ @grid_options.push(options[:grid])
31
+ options[:grid] = !!options[:grid] # rubocop:disable Style/DoubleNegation
32
+
33
+ @stack.push(current.merge(options))
34
+ end
35
+
36
+ def pop
37
+ @grid_options.pop
38
+ @stack.pop
39
+ end
40
+
41
+ def current
42
+ @stack.last
43
+ end
44
+
45
+ def column?
46
+ current[:column]
47
+ end
48
+
49
+ def grid?
50
+ current[:grid]
51
+ end
52
+
53
+ def column_options
54
+ return {} unless column?
55
+
56
+ opts = @column_options.last
57
+ opts == true ? {} : opts
58
+ end
59
+
60
+ def grid_options
61
+ return {} unless grid?
62
+
63
+ opts = @grid_options.last
64
+ opts == true ? {} : opts
65
+ end
66
+ end
67
+ end
68
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module BulmaPhlex
4
4
  module Rails
5
- VERSION = "0.4.0"
5
+ VERSION = "0.5.0"
6
6
  end
7
7
  end
@@ -13,6 +13,8 @@ end
13
13
 
14
14
  loader = Zeitwerk::Loader.for_gem_extension(BulmaPhlex)
15
15
  loader.collapse("#{__dir__}/rails/components")
16
+ loader.collapse("#{__dir__}/rails/components/displays")
17
+ loader.collapse("#{__dir__}/rails/concerns")
16
18
  loader.collapse("#{__dir__}/rails/helpers")
17
19
  loader.collapse("#{__dir__}/rails/models")
18
20
  loader.setup
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bulma-phlex-rails
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Todd Kummer
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2026-01-31 00:00:00.000000000 Z
10
+ date: 2026-02-07 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: actionpack
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 0.9.0
32
+ version: 0.10.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: 0.9.0
39
+ version: 0.10.0
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: phlex-rails
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -65,8 +65,6 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '7.2'
68
- description: Create forms with Bulma CSS framework styles using Phlex components in
69
- your Rails applications.
70
68
  email:
71
69
  - todd@rockridgesolutions.com
72
70
  executables: []
@@ -76,15 +74,28 @@ files:
76
74
  - app/javascript/controllers/bulma_phlex/dropdown_controller.js
77
75
  - app/javascript/controllers/bulma_phlex/file_input_display_controller.js
78
76
  - app/javascript/controllers/bulma_phlex/navigation_bar_controller.js
77
+ - app/javascript/controllers/bulma_phlex/nested_forms_add_row_controller.js
78
+ - app/javascript/controllers/bulma_phlex/nested_forms_delete_row_controller.js
79
79
  - app/javascript/controllers/bulma_phlex/tabs_controller.js
80
80
  - config/importmap.rb
81
81
  - lib/bulma-phlex-rails.rb
82
82
  - lib/bulma_phlex/rails.rb
83
83
  - lib/bulma_phlex/rails/components/checkbox.rb
84
+ - lib/bulma_phlex/rails/components/displays/base_display.rb
85
+ - lib/bulma_phlex/rails/components/displays/currency_display.rb
86
+ - lib/bulma_phlex/rails/components/displays/formatted_display.rb
87
+ - lib/bulma_phlex/rails/components/displays/text_display.rb
88
+ - lib/bulma_phlex/rails/components/nested_form_add_button.rb
89
+ - lib/bulma_phlex/rails/components/nested_form_delete_button.rb
90
+ - lib/bulma_phlex/rails/components/radio_button.rb
91
+ - lib/bulma_phlex/rails/concerns/displayable_block_options.rb
92
+ - lib/bulma_phlex/rails/concerns/displayable_form_fields.rb
93
+ - lib/bulma_phlex/rails/concerns/nested_forms.rb
84
94
  - lib/bulma_phlex/rails/engine.rb
85
95
  - lib/bulma_phlex/rails/form_builder.rb
86
96
  - lib/bulma_phlex/rails/helpers/card_helper.rb
87
97
  - lib/bulma_phlex/rails/helpers/table_helper.rb
98
+ - lib/bulma_phlex/rails/models/block_display_options.rb
88
99
  - lib/bulma_phlex/rails/version.rb
89
100
  homepage: https://github.com/RockSolt/bulma-phlex-rails
90
101
  licenses:
@@ -108,5 +119,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
108
119
  requirements: []
109
120
  rubygems_version: 3.6.9
110
121
  specification_version: 4
111
- summary: Bulma-friendly form builder built with Phlex for Rails applications.
122
+ summary: Simplify the view layer with a component library built on Phlex and styled
123
+ with Bulma CSS framework. The code is simple and the UI is clean.
112
124
  test_files: []