Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support of Stimulus change action for Tramway Multiselect #86

Merged
merged 20 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/rspec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ruby: ['3.2', '3.3']
ruby: ['3.2', '3.3', '3.4']
appraisal: ['rails-7.0', 'rails-7.1', 'rails-7.2', 'rails-8.0']

runs-on: ubuntu-latest
Expand Down
8 changes: 7 additions & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,19 @@ group :development do
end

group :test do
gem 'base64'
gem 'capybara'
gem 'database_cleaner-active_record'
gem 'debug'
gem 'drb'
gem 'factory_bot_rails'
gem 'faker'
gem 'launchy'
gem 'mutex_m'
gem 'rspec-rails'
gem 'selenium-webdriver'
gem 'webdrivers'
end

group :development, :test do
gem 'debug'
end
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@ install:
bundle
bundle exec appraisal install
lefthook install
bundle exec appraisal rspec
npm install eslint @eslint/js

eslint:
npx eslint . --ignore-pattern spec/dummy/vendor/

test:
bundle exec appraisal rspec
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,13 @@ eagerLoadControllersFrom("controllers", application)
application.register('multiselect', Multiselect) // register Multiselect controller class as `multiselect` stimulus controller
```

Use Stimulus `change` action with Tramway Multiselect

```ruby
= tramway_form_for @user do |f|
= f.multiselect :role, data: { action: 'change->user-form#updateForm' } # user-form is your Stimulus controller, updateForm is a method inside user-form Stimulus controller
```

### Tailwind-styled pagination for Kaminari

Tramway uses [Tailwind](https://tailwindcss.com/) by default. It has tailwind-styled pagination for [kaminari](https://github.com/kaminari/kaminari).
Expand Down
31 changes: 27 additions & 4 deletions app/assets/javascripts/tramway/multiselect_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export default class Multiselect extends Controller {
selectedItems: Array,
placeholder: String,
selectAsInput: String,
value: Array
value: Array,
onChange: String
}

connect() {
Expand All @@ -26,9 +27,14 @@ export default class Multiselect extends Controller {
}
});

const initialValues = this.element.dataset.value === undefined ? [] : this.element.dataset.value.split(',')
this.selectedItems = this.items.filter(item => initialValues.includes(item.value));
this.items = this.items.filter(item => !initialValues.includes(item.value));
const initialValues = this.element.dataset.value === undefined ? [] : JSON.parse(this.element.dataset.value);

initialValues.map((value) => {
const itemIndex = this.items.findIndex(x => x.value.toString() === value.toString());
this.items[itemIndex].selected = true;
})

this.selectedItems = this.items.filter(item => item.selected);

this.renderSelectedItems();
}
Expand Down Expand Up @@ -84,6 +90,23 @@ export default class Multiselect extends Controller {
if (this.dropdown()) {
this.dropdown().remove();
}

const onChange = this.element.dataset.onChange;

if (onChange) {
const [controllerName, actionName] = onChange.split('#');
const controller = this.application.controllers.find(controller => controller.identifier === controllerName)

if (controller) {
if (typeof controller[actionName] === 'function') {
controller[actionName]({ target: this.element });
} else {
alert(`Action not found: ${actionName}`); // eslint-disable-line no-undef
}
} else {
alert(`Controller not found: ${controllerName}`); // eslint-disable-line no-undef
}
}
}

get template() {
Expand Down
2 changes: 1 addition & 1 deletion app/components/tailwinds/form/builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def select(attribute, collection, **options, &)
def multiselect(attribute, collection, **options, &)
render(Tailwinds::Form::MultiselectComponent.new(
input: input(:text_field),
value: options[:value] || options[:selected] || object.public_send(attribute)&.first,
value: options[:value] || options[:selected] || object.public_send(attribute),
collection:,
**default_options(attribute, options)
), &)
Expand Down
21 changes: 19 additions & 2 deletions app/components/tailwinds/form/multiselect_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@ def before_render
def multiselect_hash
{
controller:, selected_item_template:, multiselect_selected_items_value:, dropdown_container:, item_container:,
items:, action:, select_as_input:, placeholder:, value:
items:, action:, select_as_input:, placeholder:, value:, on_change:
}.transform_keys { |key| key.to_s.gsub('_', '-') }
end

def controller
:multiselect
controllers = [:multiselect]
controllers << external_action.split('->').last.split('#').first if external_action
controllers += external_controllers
controllers.join(' ')
end

private
Expand All @@ -45,6 +48,20 @@ def select_as_input
render(Tailwinds::Form::Multiselect::SelectAsInput.new(options:, attribute:, input:))
end

def on_change
return unless external_action&.start_with?('change')

external_action.split('->').last
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have potential bugs with using .last? not so clear as for me

end

def external_controllers
options[:controller]&.split || []
end

def external_action
options.dig(:data, :action)
end

def method_missing(method_name, *, &)
component = component_name(method_name)

Expand Down
8 changes: 4 additions & 4 deletions app/components/tailwinds/navbar_component.html.haml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
%nav.py-4.px-4.sm:px-8.flex.justify-between.items-center.dark:bg-gray-800{ class: "bg-#{@color}" }
%nav.py-2.px-4.sm:px-8.flex.justify-between.items-center.dark:bg-gray-800{ class: "bg-#{@color}" }
.flex.justify-between.w-full
- if @title[:text].present? || @left_items.present?
.flex.items-center
Expand All @@ -7,16 +7,16 @@
.text-xl.text-white.font-bold
= @title[:text]
- if @left_items.present?
%ul.hidden.sm:flex.items-center.space-x-4.ml-4
%ul.block.flex.flex-row.items-center.space-x-4.ml-4
- @left_items.each do |item|
= item

.block.sm:hidden
.hidden.sm:block
%button#mobile-menu-button.text-white.focus:outline-none

- if @right_items.present?
%ul.hidden.sm:flex.items-center.space-x-4
%ul.block.sm:flex.items-center.space-x-4
- @right_items.each do |item|
= item

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
- unless current_page.first?
= link_to helpers.t('views.pagination.first').html_safe, url, remote:, class: pagination_classes(klass: 'first hidden sm:flex')
= link_to '⭰', url, remote:, class: pagination_classes(klass: 'first sm:hidden font-bold')
= link_to helpers.t('views.pagination.first').html_safe,
url,
remote:,
class: pagination_classes(klass: 'first sm:hidden flex')

= link_to '⭰',
url,
remote:,
class: pagination_classes(klass: 'first hidden sm:flex font-bold')
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
%span.page.gap.px-3.py-2.text-sm.font-medium.text-purple-700.dark:text-white.sm:flex.hidden
%span.page.gap.px-3.py-2.text-sm.font-medium.text-purple-700.dark:text-white.flex.sm:hidden
= helpers.t('views.pagination.truncate').html_safe
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
- unless current_page.last?
= link_to helpers.t('views.pagination.last').html_safe, url, remote:, class: pagination_classes(klass: 'last hidden sm:flex')
= link_to '⭲', url, remote:, class: pagination_classes(klass: 'last sm:hidden font-bold')
= link_to helpers.t('views.pagination.last').html_safe,
url,
remote:,
class: pagination_classes(klass: 'last sm:hidden flex')

= link_to '⭲',
url,
remote:,
class: pagination_classes(klass: 'last hidden sm:flex font-bold')
13 changes: 11 additions & 2 deletions app/components/tailwinds/pagination/next_page_component.html.haml
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
- unless current_page.last?
= link_to helpers.t('views.pagination.next').html_safe, url, rel: 'next', remote:, class: pagination_classes(klass: 'next hidden sm:flex')
= link_to '🠖', url, rel: 'next', remote:, class: pagination_classes(klass: 'next sm:hidden font-bold')
= link_to helpers.t('views.pagination.next').html_safe,
url,
rel: 'next',
remote:,
class: pagination_classes(klass: 'next sm:hidden flex')

= link_to '🠖',
url,
rel: 'next',
remote:,
class: pagination_classes(klass: 'next hidden sm:flex font-bold')
6 changes: 5 additions & 1 deletion app/components/tailwinds/pagination/page_component.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@
%span.px-3.py-2.font-medium.rounded-md.bg-purple-500.text-white.dark:text-gray-800.dark:bg-white
= page
- else
= link_to page, url, remote:, rel: page.rel, class: pagination_classes(klass: 'hidden sm:flex')
= link_to page,
url,
remote:,
rel: page.rel,
class: pagination_classes(klass: 'sm:hidden flex')
13 changes: 11 additions & 2 deletions app/components/tailwinds/pagination/prev_page_component.html.haml
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
- unless current_page.first?
= link_to helpers.t('views.pagination.previous').html_safe, url, rel: 'prev', remote:, class: pagination_classes(klass: 'prev hidden sm:flex')
= link_to '🠔', url, rel: 'prev', remote:, class: pagination_classes(klass: 'prev sm:hidden font-bold')
= link_to helpers.t('views.pagination.previous').html_safe,
url,
rel: 'prev',
remote:,
class: pagination_classes(klass: 'prev sm:hidden flex')

= link_to '🠔',
url,
rel: 'prev',
remote:,
class: pagination_classes(klass: 'prev hidden sm:flex font-bold')
2 changes: 1 addition & 1 deletion app/components/tailwinds/table/header_component.html.haml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
- cols = headers.map { |item| "1fr" }.join(",")

.div-table-row.hidden.md:grid.text-white.text-small.gap-4.bg-purple-700.dark:bg-gray-700.dark:text-gray-400{ class: "grid-cols-[#{cols}]" }
.div-table-row.block.grid.text-white.text-small.gap-4.bg-purple-700.dark:bg-gray-700.dark:text-gray-400{ class: "grid-cols-[#{cols}]" }
- headers.each do |header|
.div-table-cell.py-4.px-6
= header
8 changes: 5 additions & 3 deletions app/components/tailwinds/table/row_component.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
- if cells.any?
- cols = cells.count.times.map { |item| "1fr" }.join(",")

= row_tag class: "div-table-row hidden md:grid gap-4 bg-white border-b dark:bg-gray-800 dark:border-gray-700 grid-cols-[#{cols}]" do
-# desktop view
= row_tag class: "div-table-row block grid gap-4 bg-white border-b dark:bg-gray-800 dark:border-gray-700 grid-cols-[#{cols}]" do
- cells.each do |(_, value)|
.div-table-cell.px-6.py-4.font-medium.text-gray-900.whitespace-nowrap.dark:text-white.text-xs.sm:text-base
.div-table-cell.px-6.py-4.font-medium.text-gray-900.whitespace-nowrap.dark:text-white.sm:text-xs.text-base
= value

.div-table-row.border-b.dark:bg-gray-800.dark:border-gray-700.md:hidden.mb-2{ "data-action" => "click->preview#toggle", "data-controller" => "preview", "data-items" => cells.to_json }
-# mobile view
.div-table-row.xl:hidden.border-b.dark:bg-gray-800.dark:border-gray-700.mb-2{ "data-action" => "click->preview#toggle", "data-controller" => "preview", "data-items" => cells.to_json }
.w-full.p-4.bg-purple-100.text-gray-700.dark:bg-gray-700.dark:text-gray-400
= cells.values.first

Expand Down
2 changes: 1 addition & 1 deletion app/components/tailwinds/table/row_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class RowComponent < Tramway::Component::Base

def row_tag(**options, &)
if href.present?
klass = "#{options[:class] || ''} cursor-pointer hover:bg-gray-700"
klass = "#{options[:class] || ''} cursor-pointer dark:hover:bg-gray-700"

link_to(href, options.merge(class: klass)) do
yield if block_given?
Expand Down
9 changes: 6 additions & 3 deletions app/views/tramway/entities/index.html.haml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
- decorator = Tramway::Decorators::NameBuilder.default_decorator_class_name(@model_class)
- list_attributes = decorator.constantize.list_attributes

.w-full
- content_for :title, page_title

Expand All @@ -8,13 +11,13 @@
- if Tramway.config.pagination[:enabled]
= paginate @entities

- if decorator_class(@entities).list_attributes.empty?
- if list_attributes.empty?
%p.text-center.mt-10
You should fill class-level method `self.list_attributes` inside your
= decorator_class_name(@entities)
= decorator

= component 'tailwinds/table' do
= component 'tailwinds/table/header', headers: decorator_class(@entities).list_attributes.map { |attribute| @model_class.human_attribute_name(attribute) }
= component 'tailwinds/table/header', headers: list_attributes.map { |attribute| @model_class.human_attribute_name(attribute) }
- @entities.each do |item|
= render 'entity', entity: item

Expand Down
8 changes: 7 additions & 1 deletion gemfiles/rails_7.0.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,21 @@ group :development do
end

group :test do
gem 'base64'
gem 'capybara'
gem 'database_cleaner-active_record'
gem 'debug'
gem 'drb'
gem 'factory_bot_rails'
gem 'faker'
gem 'launchy'
gem 'mutex_m'
gem 'rspec-rails'
gem 'selenium-webdriver'
gem 'webdrivers'
end

group :development, :test do
gem 'debug'
end

gemspec path: '../'
8 changes: 7 additions & 1 deletion gemfiles/rails_7.1.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,21 @@ group :development do
end

group :test do
gem 'base64'
gem 'capybara'
gem 'database_cleaner-active_record'
gem 'debug'
gem 'drb'
gem 'factory_bot_rails'
gem 'faker'
gem 'launchy'
gem 'mutex_m'
gem 'rspec-rails'
gem 'selenium-webdriver'
gem 'webdrivers'
end

group :development, :test do
gem 'debug'
end

gemspec path: '../'
8 changes: 7 additions & 1 deletion gemfiles/rails_7.2.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,21 @@ group :development do
end

group :test do
gem 'base64'
gem 'capybara'
gem 'database_cleaner-active_record'
gem 'debug'
gem 'drb'
gem 'factory_bot_rails'
gem 'faker'
gem 'launchy'
gem 'mutex_m'
gem 'rspec-rails'
gem 'selenium-webdriver'
gem 'webdrivers'
end

group :development, :test do
gem 'debug'
end

gemspec path: '../'
Loading
Loading