active_element 0.0.12 → 0.0.14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/Gemfile.lock +17 -17
  4. data/app/assets/javascripts/active_element/highlight.js +311 -0
  5. data/app/assets/javascripts/active_element/json_field.js +51 -20
  6. data/app/assets/javascripts/active_element/popover.js +6 -4
  7. data/app/assets/stylesheets/active_element/_dark.scss +1 -1
  8. data/app/assets/stylesheets/active_element/application.scss +39 -1
  9. data/app/controllers/concerns/active_element/default_controller_actions.rb +7 -7
  10. data/app/views/active_element/_title.html.erb +1 -0
  11. data/app/views/active_element/components/fields/_json.html.erb +3 -2
  12. data/app/views/active_element/components/form/_field.html.erb +2 -1
  13. data/app/views/active_element/components/form/_json.html.erb +2 -0
  14. data/app/views/active_element/components/form/_label.html.erb +7 -0
  15. data/app/views/active_element/components/form.html.erb +2 -2
  16. data/app/views/layouts/active_element.html.erb +29 -6
  17. data/example_app/Gemfile.lock +1 -1
  18. data/lib/active_element/components/form.rb +1 -8
  19. data/lib/active_element/components/util/display_value_mapping.rb +0 -2
  20. data/lib/active_element/components/util/form_field_mapping.rb +2 -1
  21. data/lib/active_element/components/util.rb +7 -0
  22. data/lib/active_element/controller_interface.rb +2 -1
  23. data/lib/active_element/controller_state.rb +1 -1
  24. data/lib/active_element/default_controller/actions.rb +3 -0
  25. data/lib/active_element/default_controller/controller.rb +145 -0
  26. data/lib/active_element/default_controller/json_params.rb +48 -0
  27. data/lib/active_element/default_controller/params.rb +97 -0
  28. data/lib/active_element/default_controller/search.rb +112 -0
  29. data/lib/active_element/default_controller.rb +10 -132
  30. data/lib/active_element/version.rb +1 -1
  31. data/lib/active_element.rb +0 -2
  32. data/rspec-documentation/_head.html.erb +2 -0
  33. data/rspec-documentation/pages/000-Introduction.md +8 -5
  34. data/rspec-documentation/pages/005-Setup.md +21 -28
  35. data/rspec-documentation/pages/010-Components/Form Fields.md +35 -0
  36. data/rspec-documentation/pages/015-Custom Controllers.md +32 -0
  37. data/rspec-documentation/pages/016-Default Controller.md +132 -0
  38. data/rspec-documentation/pages/Themes.md +3 -0
  39. metadata +12 -4
  40. data/lib/active_element/default_record_params.rb +0 -62
  41. data/lib/active_element/default_search.rb +0 -110
@@ -1,7 +1,5 @@
1
1
  # Setup
2
2
 
3
- To integrate _ActiveElement_ into your _Rails_ application, follow the steps below:
4
-
5
3
  ## Installation
6
4
 
7
5
  Install the `active_element` gem by adding the following to your `Gemfile`:
@@ -20,7 +18,7 @@ $ bundle install
20
18
 
21
19
  Inherit from `ActiveElement::ApplicationController` in the controller you want to use with _ActiveElement_. In most cases this will either be your main `ApplicationController`, or a namespaced admin area controller, e.g. `Admin::ApplicationController`. This will apply the default _ActiveElement_ layout which includes a [Navbar](components/navbar.html), [Theme Switcher](components/theme-switcher.html), and all the required _CSS_ and _Javascript_.
22
20
 
23
- If you want to add custom content to the layout, see the [Hooks](hooks.html) documentation.
21
+ If you want to add custom content to the layout, see the [Hooks](hooks.html) documentation, or if you want to use a completely custom layout, simply specify `layout 'my_layout'` in your `ApplicationController`.
24
22
 
25
23
  ```ruby
26
24
  # app/controllers/application_controller.rb
@@ -30,23 +28,13 @@ class ApplicationController < ActiveElement::ApplicationController
30
28
  end
31
29
  ```
32
30
 
33
- ## Create a View
31
+ ## Default Controller Actions
34
32
 
35
- We'll use `UsersController` in this example, but you can replace this with whatever controller you want to use with _ActiveElement_.
33
+ _ActiveElement_ provides default controller actions for all controllers that inherit from `ActiveElement::ApplicationController` (directly or indirectly).
36
34
 
37
- Assuming your controller is defined something like this:
35
+ Each action provides boilerplate functionality to get your application off the ground as quickly as possible. See the [Default Controller](default-controller.html) for full details.
38
36
 
39
- ```ruby
40
- # app/controllers/users_controller.rb
41
-
42
- class UsersController < ApplicationController
43
- def index
44
- @users = User.all
45
- end
46
- end
47
- ```
48
-
49
- And your routes are defined something like this:
37
+ The below example creates a `/users` endpoint for your application with boilerplate to list, search, view, create, edit, and delete users:
50
38
 
51
39
  ```ruby
52
40
  # config/routes.rb
@@ -56,20 +44,25 @@ Rails.application.routes.draw do
56
44
  end
57
45
  ```
58
46
 
59
- Edit or create `app/views/users/index.html.erb`. Add a page title and a table component:
60
-
61
- ```erb
62
- <%# app/views/users/index.html.erb %>
63
-
64
- <%= active_element.component.page_title 'Users' %>
47
+ ```ruby
48
+ # app/controllers/users_controller.rb
65
49
 
66
- <%= active_element.component.table collection: @users, fields: [:id, :email, :name] %>
50
+ class UsersController < ApplicationController
51
+ active_element.listable_fields :name, :email, :created_at, order: :name
52
+ active_element.viewable_fields :name, :email, :created_at, :updated_at
53
+ active_element.editable_fields :name, :email
54
+ active_element.searchable_fields :name, :name, :created_at, :updated_at
55
+ active_element.deletable
56
+ end
67
57
  ```
68
58
 
69
- Adjust the `fields` to match whatever attributes you want to display for each `User` in your table.
59
+ ```ruby
60
+ # app/models/user.rb
70
61
 
71
- Start your _Rails_ application and browse to `/users` to see your new users index.
62
+ class User < ApplicationRecord
63
+ end
64
+ ```
72
65
 
73
- ## Next Steps
66
+ You can now browse to `/users` on your local development server and see all the default behaviour provided by _ActiveElement_.
74
67
 
75
- Now that you know how to render components, take a look at the [Components](components.html) section of this documentation to see what components are available and how to use them.
68
+ See the [Default Controller](default-controller.html) and [Custom Controllers](custom-controllers.html) sections for more details.
@@ -44,3 +44,38 @@ end
44
44
 
45
45
  it { is_expected.to include 'class="form-control my-class"' }
46
46
  ```
47
+
48
+ ## Custom Fields
49
+
50
+ If you're using the [Default Controller](../default-controller.html) or you simply want to separate your configuration from your views, you can customize each form field by creating a file named `config/forms/<model>/<field>.yml`.
51
+
52
+ The `User` `email` field can be configured by creating `config/forms/user/email.yml`:
53
+
54
+ ```yaml
55
+ # config/forms/user/email.yml
56
+
57
+ type: email_field
58
+ options:
59
+ class: 'form-control my-email-field-class'
60
+ description: 'We will use your email address to send your account details.'
61
+ placeholder: 'Enter your email address, e.g. user@example.com'
62
+ ```
63
+
64
+ ```rspec:html
65
+ subject do
66
+ active_element.component.form model: User.new,
67
+ fields: [:email, :name, :date_of_birth]
68
+ end
69
+
70
+ it { is_expected.to include 'class="form-control my-email-field-class"' }
71
+ ```
72
+
73
+ The `options` configuration receives a small number of options specific to _ActiveElement_ such as `description` and [Text Search](form-fields/text-search.html) configuration, otherwise they are passed directly to the underlying _Rails_ form helper.
74
+
75
+ The `type` configuration corresponds to either a _Rails_ form helper _ActiveElement_ extension field, i.e. `email_field` will call some variation of:
76
+
77
+ ```ruby
78
+ form_with do |form|
79
+ form.email_field :email
80
+ end
81
+ ```
@@ -0,0 +1,32 @@
1
+ # Custom Controllers
2
+
3
+ The [Default Controller](default-controller.html) provides out-of-the-box functionality to get you up and running quickly, but as your application progresses you will likely need to provide custom functionality in some areas.
4
+
5
+ A custom controller is just a regular _Rails_ controller. You can still benefit from default actions provided by _ActiveElement_ and only override the specific actions you need.
6
+
7
+ In the example below we'll implement a custom `#show` action on a `UsersController` and create a custom view.
8
+
9
+ ```ruby
10
+ # app/controllers/users_controller.rb
11
+
12
+ class UsersController < ApplicationController
13
+ active_element.listable_fields :name, :email, :created_at, order: :name
14
+ active_element.editable_fields :name, :email
15
+ active_element.searchable_fields :name, :name, :created_at, :updated_at
16
+ active_element.deletable
17
+
18
+ def show
19
+ @user = User.find(params[:id])
20
+ end
21
+ end
22
+ ```
23
+
24
+ ```erb
25
+ <%# app/views/users/index.html.erb %>
26
+
27
+ <%= active_element.component.page_title 'Users' %>
28
+
29
+ <%= active_element.component.table item: @user, fields: [:email, :name, :created_at, :updated_at] %>
30
+ ```
31
+
32
+ You can customize any action or view you like, simply by following standard _Rails_ patterns.
@@ -0,0 +1,132 @@
1
+ # Default Controller
2
+
3
+ The default controller in _ActiveElement_ provides all the standard _Rails_ actions:
4
+
5
+ * `#index`
6
+ * `#show`
7
+ * `#new`
8
+ * `#create`
9
+ * `#edit`
10
+ * `#update`
11
+ * `#destroy`
12
+
13
+ All controllers that inherit from `ActiveElement::ApplicationController` automatically receive these routes, but they must be explicitly enabled in order to serve content, otherwise a default `403 Forbidden` page will be rendered.
14
+
15
+ Each controller expects to find a model whose name corresponds to the controller, in line with typical _Rails_ conventions. For example, if you have a `RestaurantsController` then _ActiveElement_ will expect to find a `Restaurant` model. Each of the declarations covered below describe interactions with a corresponding model.
16
+
17
+ Depending on which declarations are defined in your controllers, you will see different _UI_ elements (e.g. a _Delete_ button for each record will only be present when `active_element.deletable` has been called).
18
+
19
+ ## Associations
20
+
21
+ Model associations are supported, so you can list database columns as well as relations defined on your model.
22
+
23
+ Here's a basic example:
24
+
25
+ ```ruby
26
+ # app/models/restaurant.rb
27
+
28
+ class Restaurant < ApplicationRecord
29
+ belongs_to :restaurateur
30
+ end
31
+ ```
32
+
33
+ ```ruby
34
+ # app/models/restaurateur.rb
35
+
36
+ class Restaurateur < ApplicationRecord
37
+ has_many :resaturants
38
+ end
39
+ ```
40
+
41
+ ```ruby
42
+ # app/controllers/restaurants_controller.rb
43
+
44
+ class RestaurantsController < ApplicationController
45
+ active_element.listable_fields :name, :restaurateur, :created_at
46
+ end
47
+ ```
48
+
49
+ Now when you browse to `/restaurants` you'll see a link to each restaurants owner in the rendered table.
50
+
51
+ ## Listable Fields
52
+
53
+ The `active_element.listable_fields` declaration provides a list of fields on your model that should be displayed in the default `#index` view.
54
+
55
+ A table of results will be rendered containing each record corresponding for the corresponding record, including _View_, _Edit_, and _Delete_ buttons, as well as a button above the table to create a new record.
56
+
57
+ The `order` keyword allows you to sort the results by a given field. This value is passed directly to the `ActiveRecord` `order` method, so you can use `:name` or `{ name: :desc }` or any other variation that `ActiveRecord` accepts.
58
+
59
+ ```ruby
60
+ # app/controllers/restaurants_controller.rb
61
+
62
+ class RestaurantsController < ApplicationController
63
+ active_element.listable_fields :name, :restaurateur, :created_at, order: :name
64
+ end
65
+ ```
66
+
67
+ As mentioned above, associations are supported, so the `restaurateur` association will automatically map to the associated record and you'll be provided with a link to that record in the results table.
68
+
69
+ Pagination is also provided by the default `#index` action.
70
+
71
+ ## Searchable Fields
72
+
73
+ The `active_element.searchable_fields` declaration provides a list of fields on your model that can be searched by a user. You can specify `string`, `integer`, and `datetime` fields.
74
+
75
+ A search form is generated and user input is processed according to column type.
76
+
77
+ * `string` fields generate an `ILIKE` (case-insensitive `LIKE`) query from user input: `"joh"` will match `"John Smith"`.
78
+ * `integer` fields require an exact match.
79
+ * `datetime` fields provide a range, allowing users to provide a start date/time, an end date/time, or both.
80
+ * Association fields will join on the relevant association and search all `string` and `integer` fields defined in the `searchable_fields` for the controller corresponding to the association model.
81
+
82
+ ```ruby
83
+ # app/controllers/restaurants_controller.rb
84
+
85
+ class RestaurantsController < ApplicationController
86
+ active_element.searchable_fields :name, :restaurateur, :created_at
87
+ end
88
+ ```
89
+
90
+ ```ruby
91
+ # app/controllers/restaurateurs_controller.rb
92
+
93
+ class RestaurateursController < ApplicationController
94
+ active_element.searchable_fields :name, :address
95
+ end
96
+ ```
97
+
98
+ ## Viewable Fields
99
+
100
+ The `active_element.viewable_fields` declaration provides a list of fields on your model that will be included when viewing an individual record via the `#show` action.
101
+
102
+ The results will be rendered in a horizontal table with one row for each item, including a _Delete_ and _Edit_ button above the table.
103
+
104
+ ```ruby
105
+ # app/controllers/restaurants_controller.rb
106
+
107
+ class RestaurantsController < ApplicationController
108
+ active_element.searchable_fields :name, :restaurateur, :created_at
109
+ end
110
+ ```
111
+
112
+ ## Editable Fields
113
+
114
+ The `active_element.editable_fields` declaration provides a list of fields on your model that can be modified by a user.
115
+
116
+ This declaration enables both the `#edit`, `#new`, `#update`, and `#create` actions as well as defining the permitted parameters for `#update` and `#create`. A form is automatically generated and each field is selected according to the column data type.
117
+
118
+ Note that each field type can be overridden and configured by defining `config/forms/<model>/<field.yml>`, allowing you to make many customizations to each field without having to work with views or override the default controller actions, as well as allowing each field configuration to be re-used in multiple places. See the [Form Fields](components/form-fields.html) documentation for more details.
119
+
120
+ By default, `json` and `jsonb` fields use the [JSON Field](form-fields/json.html) type, allowing you to edit complex _JSON_ data structures via user-friendly _HTML_ forms. A [schema file](form-fields/json/schema.html) **must** be defined for these fields. See the `json_field` documentation for more details.
121
+
122
+ ## Deletable
123
+
124
+ The `active_element.deletable` declaration does not receive any arguments but specifies that a record can be deleted by a user.
125
+
126
+ ```ruby
127
+ # app/controllers/restaurants_controller.rb
128
+
129
+ class RestaurantsController < ApplicationController
130
+ active_element.deletable
131
+ end
132
+ ```
@@ -0,0 +1,3 @@
1
+ # Themes
2
+
3
+ TODO
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_element
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.12
4
+ version: 0.0.14
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bob Farrell
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-06-20 00:00:00.000000000 Z
11
+ date: 2023-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bootstrap
@@ -116,6 +116,7 @@ files:
116
116
  - app/assets/javascripts/active_element/application.js
117
117
  - app/assets/javascripts/active_element/confirm.js
118
118
  - app/assets/javascripts/active_element/form.js
119
+ - app/assets/javascripts/active_element/highlight.js
119
120
  - app/assets/javascripts/active_element/json_field.js
120
121
  - app/assets/javascripts/active_element/pagination.js
121
122
  - app/assets/javascripts/active_element/popover.js
@@ -130,6 +131,7 @@ files:
130
131
  - app/assets/stylesheets/active_element/application.scss
131
132
  - app/controllers/active_element/application_controller.rb
132
133
  - app/controllers/concerns/active_element/default_controller_actions.rb
134
+ - app/views/active_element/_title.html.erb
133
135
  - app/views/active_element/_user.html.erb
134
136
  - app/views/active_element/components/_horizontal_tabs.html.erb
135
137
  - app/views/active_element/components/_vertical_tabs.html.erb
@@ -318,8 +320,11 @@ files:
318
320
  - lib/active_element/controller_interface.rb
319
321
  - lib/active_element/controller_state.rb
320
322
  - lib/active_element/default_controller.rb
321
- - lib/active_element/default_record_params.rb
322
- - lib/active_element/default_search.rb
323
+ - lib/active_element/default_controller/actions.rb
324
+ - lib/active_element/default_controller/controller.rb
325
+ - lib/active_element/default_controller/json_params.rb
326
+ - lib/active_element/default_controller/params.rb
327
+ - lib/active_element/default_controller/search.rb
323
328
  - lib/active_element/engine.rb
324
329
  - lib/active_element/json_field_schema.rb
325
330
  - lib/active_element/permissions_check.rb
@@ -354,6 +359,8 @@ files:
354
359
  - rspec-documentation/pages/010-Components/Tables/Item Table.md
355
360
  - rspec-documentation/pages/010-Components/Tables/Options.md
356
361
  - rspec-documentation/pages/010-Components/Tabs.md
362
+ - rspec-documentation/pages/015-Custom Controllers.md
363
+ - rspec-documentation/pages/016-Default Controller.md
357
364
  - rspec-documentation/pages/020-Access Control.md
358
365
  - rspec-documentation/pages/020-Access Control/010-Authentication.md
359
366
  - rspec-documentation/pages/020-Access Control/020-Authorization.md
@@ -367,6 +374,7 @@ files:
367
374
  - rspec-documentation/pages/040-Decorators/View Decorators.md
368
375
  - rspec-documentation/pages/300-Alternatives.md
369
376
  - rspec-documentation/pages/900-License.md
377
+ - rspec-documentation/pages/Themes.md
370
378
  - rspec-documentation/spec_helper.rb
371
379
  - rspec-documentation/support.rb
372
380
  - sig/active_element.rbs
@@ -1,62 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveElement
4
- # Provides params for ActiveRecord models when using the default boilerplate controller
5
- # actions. Navigates input parameters and maps them to appropriate relations as needed.
6
- class DefaultRecordParams
7
- def initialize(controller:, model:)
8
- @controller = controller
9
- @model = model
10
- end
11
-
12
- def params
13
- with_transformed_relations(
14
- controller.params.require(controller.controller_name.singularize)
15
- .permit(controller.active_element.state.editable_fields)
16
- )
17
- end
18
-
19
- private
20
-
21
- attr_reader :controller, :model
22
-
23
- def with_transformed_relations(params)
24
- params.to_h.to_h do |key, value|
25
- next [key, value] unless relation?(key)
26
-
27
- relation_param(key, value)
28
- end
29
- end
30
-
31
- def relation_param(key, value)
32
- case relation(key).macro
33
- when :belongs_to
34
- belongs_to_param(key, value)
35
- when :has_one
36
- has_one_param(key, value)
37
- when :has_many
38
- has_many_param(key, value)
39
- end
40
- end
41
-
42
- def belongs_to_param(key, value)
43
- [relation(key).foreign_key, value]
44
- end
45
-
46
- def has_one_param(key, value) # rubocop:disable Naming/PredicateName
47
- [relation(key).name, relation(key).klass.find_by(relation(key).klass.primary_key => value)]
48
- end
49
-
50
- def has_many_param(key, _value) # rubocop:disable Naming/PredicateName
51
- [relation(key).name, relation(key).klass.where(relation(key).klass.primary_key => relation(key).value)]
52
- end
53
-
54
- def relation?(attribute)
55
- relation(attribute.to_sym).present?
56
- end
57
-
58
- def relation(attribute)
59
- model.reflect_on_association(attribute.to_sym)
60
- end
61
- end
62
- end
@@ -1,110 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveElement
4
- # Full text search and datetime querying for DefaultController, provides full text search
5
- # filters for all controllers with configured searchable fields. Includes support for querying
6
- # across relations.
7
- class DefaultSearch
8
- def initialize(controller:, model:)
9
- @controller = controller
10
- @model = model
11
- end
12
-
13
- def search_filters
14
- @search_filters ||= controller.params.permit(*searchable_fields).transform_values do |value|
15
- value.try(:compact_blank) || value
16
- end.compact_blank
17
- end
18
-
19
- def text_search?
20
- search_filters.present?
21
- end
22
-
23
- def text_search
24
- conditions = search_filters.to_h.map do |key, value|
25
- next relation_matches(key, value) if relation?(key)
26
- next datetime_between(key, value) if datetime?(key)
27
-
28
- model.arel_table[key].matches("#{value}%")
29
- end
30
- conditions[1..].reduce(conditions.first) do |accumulated, condition|
31
- accumulated.and(condition)
32
- end
33
- end
34
-
35
- def search_relations
36
- search_filters.to_h.keys.map { |key| relation?(key) ? key.to_sym : nil }.compact
37
- end
38
-
39
- private
40
-
41
- attr_reader :controller, :model
42
-
43
- def searchable_fields
44
- controller.active_element.state.searchable_fields.map do |field|
45
- next field unless field.to_s.end_with?('_at')
46
-
47
- { field => %i[from to] }
48
- end
49
- end
50
-
51
- def noop
52
- Arel::Nodes::True.new.eq(Arel::Nodes::True.new)
53
- end
54
-
55
- def datetime?(key)
56
- model.columns.find { |column| column.name.to_s == key.to_s }&.type == :datetime
57
- end
58
-
59
- def datetime_between(key, value)
60
- return noop if value[:from].blank? && value[:to].blank?
61
-
62
- model.arel_table[key].between(range_begin(value)...range_end(value))
63
- end
64
-
65
- def range_begin(value)
66
- value[:from].present? ? Time.zone.parse(value[:from]) + timezone_offset : -Float::INFINITY
67
- end
68
-
69
- def range_end(value)
70
- value[:to].present? ? Time.zone.parse(value[:to]) + timezone_offset : Float::INFINITY
71
- end
72
-
73
- def timezone_offset
74
- controller.request.cookies['timezone_offset'].to_i.minutes
75
- end
76
-
77
- def relation_matches(key, value)
78
- fields = searchable_relation_fields(key)
79
- relation_model = relation(key).klass
80
- fields.select! do |field|
81
- relation_model.columns.find { |column| column.name.to_s == field.to_s }&.type == :string
82
- end
83
-
84
- return noop if fields.empty?
85
-
86
- relation_conditions(fields, value, relation_model)
87
- end
88
-
89
- def relation_conditions(fields, value, relation_model)
90
- fields[1..].reduce(relation_model.arel_table[fields.first].matches("#{value}%")) do |condition, field|
91
- condition.or(relation_model.arel_table[field].matches("#{value}%"))
92
- end
93
- end
94
-
95
- def searchable_relation_fields(key)
96
- Components::Util.relation_controller(model, controller, key)
97
- &.active_element
98
- &.state
99
- &.fetch(:searchable_fields, []) || []
100
- end
101
-
102
- def relation?(attribute)
103
- relation(attribute.to_sym).present?
104
- end
105
-
106
- def relation(attribute)
107
- model.reflect_on_association(attribute.to_sym)
108
- end
109
- end
110
- end