turbo_turbo 0.2.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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.claude/settings.local.json +14 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +96 -0
  5. data/CHANGELOG.md +50 -0
  6. data/LICENSE +21 -0
  7. data/README.md +498 -0
  8. data/Rakefile +12 -0
  9. data/app/assets/stylesheets/turbo_turbo/alerts.css +7 -0
  10. data/app/assets/stylesheets/turbo_turbo/base.css +4 -0
  11. data/app/assets/stylesheets/turbo_turbo/button.css +24 -0
  12. data/app/assets/stylesheets/turbo_turbo/modal.css +81 -0
  13. data/app/components/turbo_turbo/alerts/alert_component.html.erb +36 -0
  14. data/app/components/turbo_turbo/alerts/alert_component.rb +12 -0
  15. data/app/components/turbo_turbo/alerts/error_component.html.erb +36 -0
  16. data/app/components/turbo_turbo/alerts/error_component.rb +12 -0
  17. data/app/components/turbo_turbo/alerts/info_component.html.erb +36 -0
  18. data/app/components/turbo_turbo/alerts/info_component.rb +12 -0
  19. data/app/components/turbo_turbo/alerts/success_component.html.erb +47 -0
  20. data/app/components/turbo_turbo/alerts/success_component.rb +12 -0
  21. data/app/components/turbo_turbo/alerts/warning_component.html.erb +36 -0
  22. data/app/components/turbo_turbo/alerts/warning_component.rb +12 -0
  23. data/app/components/turbo_turbo/modal_component.html.erb +20 -0
  24. data/app/components/turbo_turbo/modal_component.rb +9 -0
  25. data/app/components/turbo_turbo/modal_footer_component.html.erb +6 -0
  26. data/app/components/turbo_turbo/modal_footer_component.rb +10 -0
  27. data/config/locales/en.yml +15 -0
  28. data/config/routes/turbo_turbo_routes.rb +20 -0
  29. data/lib/generators/turbo_turbo/install_generator.rb +351 -0
  30. data/lib/generators/turbo_turbo/layout_generator.rb +221 -0
  31. data/lib/generators/turbo_turbo/templates/config/routes/turbo_turbo_routes.rb +20 -0
  32. data/lib/generators/turbo_turbo/templates/turbo_turbo/_error_message.html.erb +15 -0
  33. data/lib/generators/turbo_turbo/templates/turbo_turbo/_flashes.html.erb +8 -0
  34. data/lib/generators/turbo_turbo/templates/turbo_turbo/_modal_background.html.erb +2 -0
  35. data/lib/generators/turbo_turbo/templates/turbo_turbo/flash_controller.js +28 -0
  36. data/lib/generators/turbo_turbo/templates/turbo_turbo/modal_controller.js +114 -0
  37. data/lib/generators/turbo_turbo/views_generator.rb +57 -0
  38. data/lib/turbo_turbo/controller_helpers.rb +157 -0
  39. data/lib/turbo_turbo/engine.rb +19 -0
  40. data/lib/turbo_turbo/form_helper.rb +135 -0
  41. data/lib/turbo_turbo/parameter_sanitizer.rb +69 -0
  42. data/lib/turbo_turbo/standard_actions.rb +96 -0
  43. data/lib/turbo_turbo/test_helpers.rb +64 -0
  44. data/lib/turbo_turbo/version.rb +5 -0
  45. data/lib/turbo_turbo.rb +15 -0
  46. data/sig/turbo_turbo.rbs +4 -0
  47. data/turbo_turbo.gemspec +42 -0
  48. metadata +136 -0
@@ -0,0 +1,114 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ // Connects to data-controller="turbo-turbo--modal"
4
+ export default class extends Controller {
5
+ static targets = [ "background", "modalBackdrop", "title", "subtitle", "body", "modal", "footer", "form", "submitButton", "cancelButton" ]
6
+
7
+ connect() {
8
+ this.modalTarget.addEventListener('click', this.stopClickPropagation);
9
+ }
10
+
11
+ appendSubmitAndCancelButtons() {
12
+ if (this.hasSubmitButtonTarget) {
13
+ return;
14
+ }
15
+ this.footerTarget.appendChild(this.generateSubmitButton(this.dataset?.submitText || "Submit"));
16
+ this.footerTarget.prepend(this.generateCancelButton(this.dataset?.cancelText || "Cancel"));
17
+ }
18
+
19
+ submitForm() {
20
+ this.formTarget.requestSubmit();
21
+ }
22
+
23
+ disconnect() {
24
+ this.modalTarget.removeEventListener('click', this.stopClickPropagation);
25
+ }
26
+
27
+ stopClickPropagation(event) {
28
+ event.stopPropagation();
29
+ }
30
+
31
+ closeOnSuccess(){
32
+ if (event.detail.success)
33
+ this.closeModal()
34
+ }
35
+
36
+ async setRemoteSource() {
37
+ event.preventDefault()
38
+ const url = event.currentTarget.dataset.url;
39
+ const title = event.currentTarget.dataset.title
40
+ const subtitle = event.currentTarget.dataset.subtitle || ""
41
+ const response = await fetch(url, {
42
+ headers: { "Accept": "text/vnd.turbo-stream.html" }
43
+ });
44
+
45
+ const text = await response.text();
46
+ const parser = new DOMParser();
47
+ const doc = parser.parseFromString(text, "text/html");
48
+ const stream = doc.querySelector("turbo-stream");
49
+ if (title) {
50
+ this.titleTarget.innerText = title;
51
+ } else {
52
+ document.getElementById("modal_title_bar_turbo_turbo").classList.add("hidden")
53
+ }
54
+ this.subtitleTarget.innerText = subtitle;
55
+ this.bodyTarget.innerHTML = stream.querySelector("template").innerHTML;
56
+ if (this.hasFormTarget) {
57
+ this.appendSubmitAndCancelButtons();
58
+ }
59
+ }
60
+
61
+ open() {
62
+ this.backgroundTarget.classList.add("show");
63
+ document.getElementById("modal_title_bar_turbo_turbo").classList.remove("hidden")
64
+ this.backgroundTarget.addEventListener('transitionend', () => {
65
+ this.modalBackdropTarget.classList.add("show");
66
+ }, { once: true });
67
+ document.body.classList.add("overflow-hidden")
68
+ }
69
+
70
+ close() {
71
+ if (!event.target.closest('.Modal-turbo-turbo')) {
72
+ this.closeModal()
73
+ }
74
+ }
75
+
76
+ closeModal(){
77
+ event.preventDefault()
78
+ document.body.classList.remove("overflow-hidden")
79
+ this.modalBackdropTarget.classList.remove("show");
80
+ this.modalBackdropTarget.addEventListener('transitionend', () => {
81
+ this.backgroundTarget.classList.remove("show");
82
+ this.titleTarget.innerText = "";
83
+ this.subtitleTarget.innerText = "";
84
+ this.bodyTarget.innerHTML = "";
85
+ if (this.hasSubmitButtonTarget) {
86
+ this.submitButtonTarget.remove();
87
+ }
88
+ if (this.hasCancelButtonTarget) {
89
+ this.cancelButtonTarget.remove();
90
+ }
91
+ }, { once: true });
92
+ }
93
+
94
+ generateCancelButton(cancelText){
95
+ const button = document.createElement("button");
96
+ button.classList.add("font-semibold", "leading-6", "text-gray-900");
97
+ button.setAttribute("data-action", "turbo-turbo--modal#closeModal");
98
+ button.setAttribute("data-turbo-turbo--modal-target", "cancelButton");
99
+ button.textContent = cancelText
100
+
101
+ return button
102
+ }
103
+
104
+ generateSubmitButton(submitText){
105
+ const button = document.createElement("button");
106
+ button.classList.add("rounded-md", "bg-primary-600", "px-3", "py-2", "text-sm", "font-semibold", "text-white", "shadow-sm", "hover:bg-primary-500", "focus-visible:outline", "focus-visible:outline-2", "focus-visible:outline-offset-2", "focus-visible:outline-primary-600");
107
+ button.setAttribute("data-action", "turbo-turbo--modal#submitForm");
108
+ button.setAttribute("data-turbo-turbo--modal-target", "submitButton");
109
+ button.textContent = submitText
110
+
111
+ return button
112
+ }
113
+ }
114
+
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module TurboTurbo
6
+ module Generators
7
+ class ViewsGenerator < Rails::Generators::Base
8
+ desc "Copy TurboTurbo views for customization"
9
+ source_root File.expand_path("../../..", __dir__)
10
+
11
+ def copy_view_components
12
+ empty_directory "app/components/turbo_turbo"
13
+ empty_directory "app/components/turbo_turbo/alerts"
14
+ copy_file "app/components/turbo_turbo/alerts/alert_component.rb",
15
+ "app/components/turbo_turbo/alerts/alert_component.rb"
16
+ copy_file "app/components/turbo_turbo/alerts/success_component.rb",
17
+ "app/components/turbo_turbo/alerts/success_component.rb"
18
+ copy_file "app/components/turbo_turbo/alerts/error_component.rb",
19
+ "app/components/turbo_turbo/alerts/error_component.rb"
20
+ copy_file "app/components/turbo_turbo/alerts/info_component.rb",
21
+ "app/components/turbo_turbo/alerts/info_component.rb"
22
+ copy_file "app/components/turbo_turbo/alerts/warning_component.rb",
23
+ "app/components/turbo_turbo/alerts/warning_component.rb"
24
+ copy_file "app/components/turbo_turbo/modal_component.rb", "app/components/turbo_turbo/modal_component.rb"
25
+ copy_file "app/components/turbo_turbo/modal_footer_component.rb",
26
+ "app/components/turbo_turbo/modal_footer_component.rb"
27
+ end
28
+
29
+ def copy_view_templates
30
+ copy_file "app/components/turbo_turbo/alerts/alert_component.html.erb",
31
+ "app/components/turbo_turbo/alerts/alert_component.html.erb"
32
+ copy_file "app/components/turbo_turbo/alerts/success_component.html.erb",
33
+ "app/components/turbo_turbo/alerts/success_component.html.erb"
34
+ copy_file "app/components/turbo_turbo/alerts/error_component.html.erb",
35
+ "app/components/turbo_turbo/alerts/error_component.html.erb"
36
+ copy_file "app/components/turbo_turbo/alerts/info_component.html.erb",
37
+ "app/components/turbo_turbo/alerts/info_component.html.erb"
38
+ copy_file "app/components/turbo_turbo/alerts/warning_component.html.erb",
39
+ "app/components/turbo_turbo/alerts/warning_component.html.erb"
40
+ copy_file "app/components/turbo_turbo/modal_component.html.erb",
41
+ "app/components/turbo_turbo/modal_component.html.erb"
42
+ copy_file "app/components/turbo_turbo/modal_footer_component.html.erb",
43
+ "app/components/turbo_turbo/modal_footer_component.html.erb"
44
+ end
45
+
46
+ def display_instructions
47
+ say "\n\nTurboTurbo views have been copied for customization!", :green
48
+ say "\nThe following files have been copied to your application:"
49
+ say "• Alert ViewComponents (turbo_turbo/alerts/)"
50
+ say "• Modal ViewComponents (turbo_turbo/)"
51
+ say "• All component templates (.html.erb)"
52
+ say "\nYou can now customize these components as needed."
53
+ say "Note: Components in your app/ directory will override the gem versions."
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,157 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboTurbo
4
+ module ControllerHelpers
5
+ extend ActiveSupport::Concern
6
+ include StandardActions
7
+ include FormHelper
8
+
9
+ included do
10
+ # Configuration for turbo response action groups
11
+ mattr_accessor :turbo_response_actions, default: {
12
+ prepend: %w[create],
13
+ replace: %w[update email_modal],
14
+ remove: %w[destroy archive restore toggle_resolved]
15
+ }
16
+
17
+ mattr_accessor :error_response_actions, default: {
18
+ validation_errors: %w[create update upload],
19
+ flash_errors: %w[destroy archive restore]
20
+ }
21
+ end
22
+
23
+ # ========================================
24
+ # Main Response Methods
25
+ # ========================================
26
+
27
+ def process_turbo_response(object, event, flash_naming_attribute: nil)
28
+ if event
29
+ turbo_success_response(object, flash_naming_attribute)
30
+ else
31
+ turbo_error_response(object, flash_naming_attribute)
32
+ end
33
+ end
34
+
35
+ def turbo_success_response(object, flash_naming_attribute)
36
+ render turbo_stream: [
37
+ turbo_behavior_for_success(object),
38
+ replace_turbo_flashes(flash_reference(object, flash_naming_attribute))
39
+ ].compact_blank
40
+ end
41
+
42
+ def turbo_error_response(object, flash_naming_attribute = nil)
43
+ if validation_error_action?
44
+ render turbo_stream: turbo_stream.update(
45
+ :error_message_turbo_turbo,
46
+ partial: "turbo_turbo/error_message",
47
+ locals: { message: validation_error_messages(object) }
48
+ ), status: :unprocessable_entity
49
+ elsif flash_error_action?
50
+ render turbo_stream: turbo_stream.replace(
51
+ :flashes_turbo_turbo,
52
+ partial: "turbo_turbo/flashes",
53
+ locals: { flash: turbo_flash_error_message(flash_reference(object, flash_naming_attribute)) }
54
+ )
55
+ end
56
+ end
57
+
58
+ def render_turbo_modal(locals = default_locals, partial = default_partial)
59
+ render turbo_stream: turbo_stream.update(:modal_body, partial:, locals:)
60
+ end
61
+
62
+ # ========================================
63
+ # Turbo Stream Behavior Methods
64
+ # ========================================
65
+
66
+ def turbo_behavior_for_success(object, partial: default_partial)
67
+ if prepend_action?
68
+ turbo_stream.prepend(:search_results, partial:, locals: { "#{default_object_key}": object })
69
+ elsif replace_action?
70
+ turbo_stream.replace(object, partial:, locals: { "#{default_object_key}": object })
71
+ elsif remove_action?
72
+ turbo_stream.remove(object)
73
+ end
74
+ end
75
+
76
+ # ========================================
77
+ # Flash Message Methods (I18n-ready)
78
+ # ========================================
79
+
80
+ def replace_turbo_flashes(flash_reference)
81
+ turbo_stream.replace(:flashes_turbo_turbo, partial: "turbo_turbo/flashes",
82
+ locals: { flash: turbo_flash_success_message(flash_reference) })
83
+ end
84
+
85
+ def turbo_flash_error_message(flash_reference)
86
+ action_verb = action_name == "destroy" ? "delete" : action_name
87
+
88
+ { error: I18n.t("turbo_turbo.flash.error.default",
89
+ action: action_verb,
90
+ resource: flash_reference,
91
+ default: "We encountered an error trying to %<action>s %<resource>s.") }
92
+ end
93
+
94
+ def turbo_flash_success_message(flash_reference)
95
+ message = I18n.t("turbo_turbo.flash.success.#{action_name}",
96
+ resource: flash_reference,
97
+ action: action_name,
98
+ default: I18n.t("turbo_turbo.flash.success.default",
99
+ resource: flash_reference,
100
+ action: action_name,
101
+ default: "#{flash_reference} #{action_name}d!"))
102
+
103
+ { success: message }
104
+ end
105
+
106
+ def validation_error_messages(object)
107
+ bulleted_errors = object.errors.full_messages.map { |e| "<li class='text-red-700'>#{e}</li>" }.join
108
+ "<ul>#{bulleted_errors}</ul>"
109
+ end
110
+
111
+ # ========================================
112
+ # Action Group Helper Methods
113
+ # ========================================
114
+
115
+ def prepend_action?
116
+ turbo_response_actions[:prepend].include?(action_name)
117
+ end
118
+
119
+ def replace_action?
120
+ turbo_response_actions[:replace].include?(action_name)
121
+ end
122
+
123
+ def remove_action?
124
+ turbo_response_actions[:remove].include?(action_name)
125
+ end
126
+
127
+ def validation_error_action?
128
+ error_response_actions[:validation_errors].include?(action_name)
129
+ end
130
+
131
+ def flash_error_action?
132
+ error_response_actions[:flash_errors].include?(action_name)
133
+ end
134
+
135
+ # ========================================
136
+ # Utility/Helper Methods
137
+ # ========================================
138
+
139
+ def flash_reference(object, flash_naming_attribute)
140
+ flash_naming_attribute ? object.send(flash_naming_attribute).titleize : object.class.name.titleize
141
+ end
142
+
143
+ def default_partial
144
+ action = %w[create update].include?(action_name) ? controller_name.singularize : action_name
145
+ "#{controller_path}/#{action}"
146
+ end
147
+
148
+ def default_locals
149
+ key = default_object_key
150
+ { "#{key}": send(key) }
151
+ end
152
+
153
+ def default_object_key
154
+ controller_name.singularize.to_sym
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/engine"
4
+
5
+ module TurboTurbo
6
+ class Engine < ::Rails::Engine
7
+ isolate_namespace TurboTurbo
8
+
9
+ initializer "turbo_turbo.setup" do |_app|
10
+ ActiveSupport.on_load(:action_controller_base) do
11
+ include TurboTurbo::ControllerHelpers
12
+ end
13
+
14
+ ActiveSupport.on_load(:action_view) do
15
+ include TurboTurbo::FormHelper
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboTurbo
4
+ # FormHelper provides a standardized way to create turbo-enabled modal forms
5
+ # that integrate seamlessly with the TurboTurbo modal system.
6
+ #
7
+ # == Basic Usage
8
+ #
9
+ # Instead of writing:
10
+ # = simple_form_for service_provider, url: [:admin, service_provider],
11
+ # data: {
12
+ # turbo_turbo__modal_target: "form",
13
+ # action: "turbo:submit-end->turbo-turbo--modal#closeOnSuccess"
14
+ # } do |form|
15
+ # .ModalContent-turbo-turbo
16
+ # #error_message_turbo_turbo
17
+ # [user-specific fields here]
18
+ #
19
+ # You can now write:
20
+ # <%= turbo_form_for(service_provider, url: [:admin, service_provider]) do |form| %>
21
+ # <!-- just the form fields -->
22
+ # <% end %>
23
+ #
24
+ # == Form Builder Support
25
+ #
26
+ # The helper supports multiple form builders:
27
+ # - SimpleForm (default): turbo_form_for(object, builder: :simple_form)
28
+ # - Rails form_with: turbo_form_for(object, builder: :form_with)
29
+ # - Rails form_for: turbo_form_for(object, builder: :form_for)
30
+ #
31
+ # == Customization Options
32
+ #
33
+ # turbo_form_for(object,
34
+ # url: custom_path,
35
+ # builder: :form_with, # Form builder to use
36
+ # data: { custom_attr: "value" } # Additional data attributes
37
+ # )
38
+ #
39
+ module FormHelper
40
+ extend ActiveSupport::Concern
41
+
42
+ # Main method for creating turbo-enabled modal forms
43
+ #
44
+ # @param object [ActiveRecord::Base] The object for the form
45
+ # @param options [Hash] Form options including url, method, etc.
46
+ # @option options [Symbol] :builder (:simple_form) Form builder to use (:simple_form, :form_with, :form_for)
47
+ # @option options [String] :url The form submission URL
48
+ # @option options [Symbol] :method HTTP method for the form
49
+ # @option options [Hash] :data Additional data attributes to merge
50
+ # @option options [Hash] :html Additional HTML attributes for the form
51
+ # @yield [form] Form builder object
52
+ def turbo_form_for(object, options = {}, &)
53
+ # Extract and set defaults
54
+ builder_type = options.delete(:builder) || :simple_form
55
+
56
+ # Build default data attributes for turbo modal integration
57
+ default_data = {
58
+ turbo_turbo__modal_target: "form"
59
+ }
60
+
61
+ # Add auto-close action if enabled
62
+ default_data[:action] = "turbo:submit-end->turbo-turbo--modal#closeOnSuccess"
63
+
64
+ # Merge user-provided data attributes
65
+ user_data = options.delete(:data) || {}
66
+ merged_data = default_data.merge(user_data)
67
+
68
+ # Set up form options
69
+ form_options = options.merge(data: merged_data)
70
+
71
+ # Ensure URL is set if not provided
72
+ unless form_options[:url]
73
+ if object.respond_to?(:persisted?) && object.persisted?
74
+ form_options[:url] = object
75
+ else
76
+ # Try to infer URL from object class
77
+ object.class.model_name
78
+ form_options[:url] = [:admin, object] if defined?(controller) && controller.class.name.include?("Admin")
79
+ end
80
+ end
81
+
82
+ # Generate the form based on builder type
83
+ form_html = case builder_type
84
+ when :simple_form
85
+ build_simple_form(object, form_options, &)
86
+ when :form_with
87
+ build_form_with(object, form_options, &)
88
+ when :form_for
89
+ build_form_for(object, form_options, &)
90
+ else
91
+ raise ArgumentError,
92
+ "Unknown form builder: #{builder_type}. Supported builders: :simple_form, :form_with, :form_for"
93
+ end
94
+
95
+ # Wrap the form in modal content structure
96
+ content_tag(:div, class: "ModalContent-turbo-turbo") do
97
+ concat content_tag(:div, "", id: "error_message_turbo_turbo")
98
+ concat form_html
99
+ end
100
+ end
101
+
102
+ # Convenience method for SimpleForm (most common case)
103
+ def turbo_simple_form_for(object, options = {}, &)
104
+ turbo_form_for(object, options.merge(builder: :simple_form), &)
105
+ end
106
+
107
+ # Convenience method for Rails form_with
108
+ def turbo_form_with(model:, **options, &)
109
+ turbo_form_for(model, options.merge(builder: :form_with), &)
110
+ end
111
+
112
+ private
113
+
114
+ def build_simple_form(object, options, &)
115
+ unless defined?(SimpleForm) && respond_to?(:simple_form_for)
116
+ raise "SimpleForm is not available. Install the simple_form gem or use a different builder."
117
+ end
118
+
119
+ simple_form_for(object, options, &)
120
+ end
121
+
122
+ def build_form_with(object, options, &)
123
+ # Convert object-based options to form_with format
124
+ form_with_options = options.dup
125
+ form_with_options[:model] = object
126
+ form_with_options.delete(:url) if form_with_options[:url] == object
127
+
128
+ form_with(**form_with_options, &)
129
+ end
130
+
131
+ def build_form_for(object, options, &)
132
+ form_for(object, options, &)
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboTurbo
4
+ module ParameterSanitizer
5
+ extend ActiveSupport::Concern
6
+
7
+ # Sanitizes parameters based on the model schema
8
+ # Converts boolean fields and trims string inputs
9
+ #
10
+ # Usage:
11
+ # params.require(:user).permit(:name, :active).sanitize_for_model(:user)
12
+ #
13
+ # @param model_key [Symbol] The key to deduce the model from (e.g., :user for User model)
14
+ # @return [ActionController::Parameters] The sanitized parameters
15
+ def sanitize_for_model(model_key)
16
+ model = model_from_key(model_key)
17
+ convert_boolean_params(model)
18
+ trim_input_strings
19
+ self
20
+ end
21
+
22
+ private
23
+
24
+ # Converts string boolean values to actual booleans based on model schema
25
+ def convert_boolean_params(model)
26
+ boolean_columns = columns_for(model, [:boolean])
27
+ each { |k, v| self[k] = convert_to_boolean(v) if boolean_columns.include?(k.to_s) }
28
+ end
29
+
30
+ # Trims whitespace from strings and cleans arrays
31
+ def trim_input_strings
32
+ each do |k, v|
33
+ if v.is_a?(Array)
34
+ self[k] = v.map { |item| item.is_a?(String) ? item.strip : item }.select(&:present?)
35
+ elsif v.is_a?(String)
36
+ self[k] = v.strip
37
+ end
38
+ end
39
+ end
40
+
41
+ # Gets columns of specified types from the model
42
+ def columns_for(model, types)
43
+ model.columns_hash.select { |_, column| types.include?(column.type) }.keys
44
+ end
45
+
46
+ # Deduces the model class from a symbol key
47
+ def model_from_key(key)
48
+ model_name = key.to_s.classify.safe_constantize
49
+ raise ArgumentError, "Model could not be deduced from symbol (#{key})" unless model_name
50
+
51
+ model_name
52
+ end
53
+
54
+ # Converts string boolean values to actual booleans
55
+ def convert_to_boolean(field)
56
+ case field
57
+ when "false", "0"
58
+ false
59
+ when "true", "1"
60
+ true
61
+ else
62
+ field
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ # Extend ActionController::Parameters with the sanitization methods
69
+ ActionController::Parameters.include(TurboTurbo::ParameterSanitizer)
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TurboTurbo
4
+ module StandardActions
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ # DSL method to automatically generate standard CRUD actions
9
+ #
10
+ # Usage:
11
+ # class MessagesController < ApplicationController
12
+ # turbo_actions :create, :update, :destroy
13
+ #
14
+ # private
15
+ #
16
+ # def message_params
17
+ # params.require(:message).permit(:content, :title)
18
+ # end
19
+ # end
20
+ def turbo_actions(*actions)
21
+ actions.each do |action|
22
+ unless %i[
23
+ create update destroy show new edit
24
+ ].include?(action)
25
+ raise ArgumentError,
26
+ "Unknown action: #{action}. Supported actions: :create, :update, :destroy, :show, :new, :edit"
27
+ end
28
+
29
+ send(:"define_turbo_#{action}_action")
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def define_turbo_create_action
36
+ define_method :create do
37
+ model_instance = model_class.new(create_params)
38
+ process_turbo_response(model_instance, model_instance.save)
39
+ end
40
+ end
41
+
42
+ def define_turbo_update_action
43
+ define_method :update do
44
+ process_turbo_response(model_instance, model_instance.update(send(model_params_method)))
45
+ end
46
+ end
47
+
48
+ def define_turbo_destroy_action
49
+ define_method :destroy do
50
+ process_turbo_response(model_instance, model_instance.destroy)
51
+ end
52
+ end
53
+
54
+ %i[show new edit].each do |action|
55
+ define_method :"define_turbo_#{action}_action" do
56
+ define_method action do
57
+ render_turbo_modal({ model_name: send("model_instance_for_#{action}") })
58
+ end
59
+ end
60
+ end
61
+ end
62
+
63
+ # Instance methods - can be overridden in controllers
64
+ def create_params
65
+ send(model_params_method)
66
+ end
67
+
68
+ def model_params_method
69
+ "#{model_name}_params"
70
+ end
71
+
72
+ def model_instance_for_show
73
+ model_instance
74
+ end
75
+
76
+ def model_instance_for_edit
77
+ model_instance
78
+ end
79
+
80
+ def model_instance_for_new
81
+ model_class.new
82
+ end
83
+
84
+ def model_instance
85
+ model_class.find(params[:id])
86
+ end
87
+
88
+ def model_class
89
+ self.class.controller_name.classify.constantize
90
+ end
91
+
92
+ def model_name
93
+ self.class.controller_name.singularize
94
+ end
95
+ end
96
+ end