active_element 0.0.10 → 0.0.12
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +12 -2
- data/.strong_versions.yml +1 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +115 -75
- data/Makefile +10 -0
- data/active_element.gemspec +1 -1
- data/app/assets/javascripts/active_element/application.js +1 -0
- data/app/assets/javascripts/active_element/form.js +16 -32
- data/app/assets/javascripts/active_element/json_field.js +391 -135
- data/app/assets/javascripts/active_element/setup.js +13 -8
- data/app/assets/javascripts/active_element/text_search_field.js +38 -27
- data/app/assets/javascripts/active_element/theme.js +1 -1
- data/app/assets/javascripts/active_element/timezones.js +6 -0
- data/app/assets/stylesheets/active_element/_dark.scss +86 -0
- data/app/assets/stylesheets/active_element/_variables.scss +2 -1
- data/app/assets/stylesheets/active_element/application.scss +166 -33
- data/app/controllers/active_element/application_controller.rb +5 -0
- data/app/controllers/concerns/active_element/default_controller_actions.rb +38 -0
- data/app/views/active_element/_user.html.erb +20 -0
- data/app/views/active_element/components/fields/_json.html.erb +24 -0
- data/app/views/active_element/components/form/_check_box.html.erb +1 -0
- data/app/views/active_element/components/form/_check_boxes.html.erb +1 -1
- data/app/views/active_element/components/form/_datetime_range_field.html.erb +14 -0
- data/app/views/active_element/components/form/_field.html.erb +10 -7
- data/app/views/active_element/components/form/_generic_field.html.erb +1 -0
- data/app/views/active_element/components/form/_json.html.erb +10 -2
- data/app/views/active_element/components/form/_label.html.erb +12 -1
- data/app/views/active_element/components/form/_select.html.erb +4 -1
- data/app/views/active_element/components/form/_summary.html.erb +11 -1
- data/app/views/active_element/components/form/_templates.html.erb +42 -24
- data/app/views/active_element/components/form/_text_area.html.erb +2 -1
- data/app/views/active_element/components/form/_text_search.html.erb +8 -4
- data/app/views/active_element/components/form.html.erb +20 -17
- data/app/views/active_element/components/json.html.erb +1 -0
- data/app/views/active_element/components/navbar.html.erb +26 -0
- data/app/views/active_element/components/table/_collection_row.html.erb +2 -1
- data/app/views/active_element/components/table/_field.html.erb +8 -0
- data/app/views/active_element/components/table/_ungrouped_collection.html.erb +1 -0
- data/app/views/active_element/components/table/collection.html.erb +1 -1
- data/app/views/active_element/components/table/item.html.erb +6 -4
- data/app/views/active_element/default_views/edit.html.erb +5 -0
- data/app/views/active_element/default_views/forbidden.html.erb +7 -0
- data/app/views/active_element/default_views/index.html.erb +15 -0
- data/app/views/active_element/default_views/new.html.erb +4 -0
- data/app/views/active_element/default_views/show.html.erb +7 -0
- data/app/views/active_element/navbar/_menu.html.erb +1 -30
- data/app/views/active_element/theme/_select.html.erb +1 -1
- data/app/views/layouts/active_element.html.erb +16 -1
- data/config/brakeman.ignore +48 -0
- data/config/locales/en.yml +3 -0
- data/example_app/.gitattributes +7 -0
- data/example_app/.gitignore +35 -0
- data/example_app/.ruby-version +1 -0
- data/example_app/Gemfile +34 -0
- data/example_app/Gemfile.lock +296 -0
- data/example_app/README.md +24 -0
- data/example_app/Rakefile +6 -0
- data/example_app/app/assets/config/manifest.js +4 -0
- data/example_app/app/assets/images/.keep +0 -0
- data/example_app/app/assets/stylesheets/application.css +15 -0
- data/example_app/app/channels/application_cable/channel.rb +4 -0
- data/example_app/app/channels/application_cable/connection.rb +4 -0
- data/example_app/app/controllers/application_controller.rb +12 -0
- data/example_app/app/controllers/concerns/.keep +0 -0
- data/example_app/app/controllers/pets_controller.rb +7 -0
- data/example_app/app/controllers/users_controller.rb +7 -0
- data/example_app/app/helpers/application_helper.rb +2 -0
- data/example_app/app/javascript/application.js +3 -0
- data/example_app/app/javascript/controllers/application.js +9 -0
- data/example_app/app/javascript/controllers/hello_controller.js +7 -0
- data/example_app/app/javascript/controllers/index.js +11 -0
- data/example_app/app/jobs/application_job.rb +7 -0
- data/example_app/app/mailers/application_mailer.rb +4 -0
- data/example_app/app/models/application_record.rb +3 -0
- data/example_app/app/models/concerns/.keep +0 -0
- data/example_app/app/models/pet.rb +3 -0
- data/example_app/app/models/user.rb +8 -0
- data/example_app/app/views/layouts/application.html.erb +16 -0
- data/example_app/app/views/layouts/mailer.html.erb +13 -0
- data/example_app/app/views/layouts/mailer.text.erb +1 -0
- data/example_app/app/views/pets/index.html.erb +3 -0
- data/example_app/app/views/users/show.html.erb +3 -0
- data/example_app/bin/bundle +109 -0
- data/example_app/bin/importmap +4 -0
- data/example_app/bin/rails +4 -0
- data/example_app/bin/rake +4 -0
- data/example_app/bin/setup +33 -0
- data/example_app/config/application.rb +22 -0
- data/example_app/config/boot.rb +4 -0
- data/example_app/config/cable.yml +10 -0
- data/example_app/config/credentials.yml.enc +1 -0
- data/example_app/config/database.yml +25 -0
- data/example_app/config/environment.rb +5 -0
- data/example_app/config/environments/development.rb +70 -0
- data/example_app/config/environments/production.rb +93 -0
- data/example_app/config/environments/test.rb +60 -0
- data/example_app/config/importmap.rb +7 -0
- data/example_app/config/initializers/assets.rb +12 -0
- data/example_app/config/initializers/content_security_policy.rb +25 -0
- data/example_app/config/initializers/devise.rb +16 -0
- data/example_app/config/initializers/filter_parameter_logging.rb +8 -0
- data/example_app/config/initializers/inflections.rb +16 -0
- data/example_app/config/initializers/permissions_policy.rb +11 -0
- data/example_app/config/locales/devise.en.yml +65 -0
- data/example_app/config/locales/en.yml +33 -0
- data/example_app/config/puma.rb +43 -0
- data/example_app/config/routes.rb +8 -0
- data/example_app/config/storage.yml +34 -0
- data/example_app/config.ru +6 -0
- data/example_app/db/migrate/20230616210539_create_pet.rb +12 -0
- data/example_app/db/migrate/20230616211328_devise_create_users.rb +46 -0
- data/example_app/db/schema.rb +37 -0
- data/example_app/db/seeds.rb +33 -0
- data/example_app/lib/assets/.keep +0 -0
- data/example_app/lib/tasks/.keep +0 -0
- data/example_app/log/.keep +0 -0
- data/example_app/public/404.html +67 -0
- data/example_app/public/422.html +67 -0
- data/example_app/public/500.html +66 -0
- data/example_app/public/apple-touch-icon-precomposed.png +0 -0
- data/example_app/public/apple-touch-icon.png +0 -0
- data/example_app/public/favicon.ico +0 -0
- data/example_app/public/robots.txt +1 -0
- data/example_app/storage/.keep +0 -0
- data/example_app/test/application_system_test_case.rb +5 -0
- data/example_app/test/channels/application_cable/connection_test.rb +11 -0
- data/example_app/test/controllers/.keep +0 -0
- data/example_app/test/fixtures/files/.keep +0 -0
- data/example_app/test/fixtures/users.yml +11 -0
- data/example_app/test/helpers/.keep +0 -0
- data/example_app/test/integration/.keep +0 -0
- data/example_app/test/mailers/.keep +0 -0
- data/example_app/test/models/.keep +0 -0
- data/example_app/test/models/user_test.rb +7 -0
- data/example_app/test/system/.keep +0 -0
- data/example_app/test/test_helper.rb +13 -0
- data/example_app/tmp/.keep +0 -0
- data/example_app/tmp/pids/.keep +0 -0
- data/example_app/tmp/storage/.keep +0 -0
- data/example_app/vendor/.keep +0 -0
- data/example_app/vendor/javascript/.keep +0 -0
- data/lib/active_element/component.rb +9 -2
- data/lib/active_element/components/collection_table.rb +9 -2
- data/lib/active_element/components/email_fields.rb +14 -0
- data/lib/active_element/components/form.rb +48 -17
- data/lib/active_element/components/navbar.rb +64 -0
- data/lib/active_element/components/phone_fields.rb +14 -0
- data/lib/active_element/components/text_search/authorization.rb +9 -6
- data/lib/active_element/components/text_search/component.rb +4 -2
- data/lib/active_element/components/text_search.rb +13 -0
- data/lib/active_element/components/util/association_mapping.rb +74 -19
- data/lib/active_element/components/util/display_value_mapping.rb +13 -4
- data/lib/active_element/components/util/form_field_mapping.rb +139 -10
- data/lib/active_element/components/util/form_value_mapping.rb +3 -3
- data/lib/active_element/components/util/i18n.rb +1 -1
- data/lib/active_element/components/util/numeric_field.rb +73 -0
- data/lib/active_element/components/util/record_mapping.rb +43 -11
- data/lib/active_element/components/util/record_path.rb +21 -4
- data/lib/active_element/components/util.rb +13 -5
- data/lib/active_element/components.rb +3 -0
- data/lib/active_element/controller_action.rb +8 -2
- data/lib/active_element/controller_interface.rb +56 -18
- data/lib/active_element/controller_state.rb +44 -0
- data/lib/active_element/default_controller.rb +137 -0
- data/lib/active_element/default_record_params.rb +62 -0
- data/lib/active_element/default_search.rb +110 -0
- data/lib/active_element/json_field_schema.rb +59 -0
- data/lib/active_element/pre_render_processors/json.rb +98 -0
- data/lib/active_element/pre_render_processors.rb +11 -0
- data/lib/active_element/route.rb +12 -0
- data/lib/active_element/routes.rb +2 -1
- data/lib/active_element/version.rb +1 -1
- data/lib/active_element.rb +15 -32
- data/lib/tasks/active_element.rake +12 -1
- data/rspec-documentation/_head.html.erb +34 -0
- data/rspec-documentation/pages/000-Introduction.md +18 -0
- data/rspec-documentation/pages/005-Setup.md +75 -0
- data/rspec-documentation/pages/010-Components/Form Fields/Check Boxes.md +1 -0
- data/rspec-documentation/pages/010-Components/Form Fields/JSON/Controller Params.md +97 -0
- data/rspec-documentation/pages/010-Components/Form Fields/JSON/Schema.md +283 -0
- data/rspec-documentation/pages/010-Components/Form Fields/JSON/Types.md +36 -0
- data/rspec-documentation/pages/010-Components/Form Fields/JSON.md +70 -0
- data/rspec-documentation/pages/010-Components/Form Fields/Text Search.md +133 -0
- data/rspec-documentation/pages/010-Components/Form Fields.md +46 -0
- data/rspec-documentation/pages/010-Components/Forms.md +44 -0
- data/rspec-documentation/pages/010-Components/JSON Data.md +23 -0
- data/rspec-documentation/pages/010-Components/Navbar.md +56 -0
- data/rspec-documentation/pages/010-Components/Page Section Title.md +13 -0
- data/rspec-documentation/pages/010-Components/Page Subtitle.md +11 -0
- data/rspec-documentation/pages/010-Components/Page Title.md +11 -0
- data/rspec-documentation/pages/010-Components/Tables/Collection Table.md +29 -0
- data/rspec-documentation/pages/010-Components/Tables/Item Table.md +18 -0
- data/rspec-documentation/pages/010-Components/Tables/Options.md +19 -0
- data/rspec-documentation/pages/010-Components/Tables.md +29 -0
- data/rspec-documentation/pages/010-Components.md +15 -0
- data/rspec-documentation/pages/020-Access Control/010-Authentication.md +20 -0
- data/rspec-documentation/pages/020-Access Control/020-Authorization/Environments.md +9 -0
- data/rspec-documentation/pages/020-Access Control/020-Authorization/Permissions/Custom Routes.md +41 -0
- data/rspec-documentation/pages/020-Access Control/020-Authorization/Permissions.md +58 -0
- data/rspec-documentation/pages/020-Access Control/020-Authorization/Setup.md +27 -0
- data/rspec-documentation/pages/020-Access Control/020-Authorization.md +11 -0
- data/rspec-documentation/pages/020-Access Control.md +31 -0
- data/rspec-documentation/pages/040-Decorators/Inline Decorators.md +24 -0
- data/rspec-documentation/pages/040-Decorators/View Decorators.md +55 -0
- data/rspec-documentation/pages/040-Decorators.md +12 -0
- data/rspec-documentation/pages/300-Alternatives.md +21 -0
- data/rspec-documentation/pages/900-License.md +11 -0
- data/rspec-documentation/spec_helper.rb +53 -16
- data/rspec-documentation/support.rb +84 -0
- metadata +159 -14
- data/rspec-documentation/pages/Components/Forms.md +0 -1
- data/rspec-documentation/pages/Components/Tables.md +0 -47
- data/rspec-documentation/pages/Components.md +0 -1
- data/rspec-documentation/pages/Decorators/Inline Decorators.md +0 -1
- data/rspec-documentation/pages/Decorators/View Decorators.md +0 -1
- data/rspec-documentation/pages/Index.md +0 -3
- data/rspec-documentation/pages/Util/I18n.md +0 -1
- /data/rspec-documentation/pages/{Components → 010-Components}/Tabs.md +0 -0
@@ -0,0 +1,18 @@
|
|
1
|
+
# Introduction
|
2
|
+
|
3
|
+
_ActiveElement_ provides a range of rich [components](components.html) for fast, painless development of front end applications, primarily intended for (but not limited to) building administration areas.
|
4
|
+
|
5
|
+
An [authorization framework](access-control.html) is provided, intended to work alongside existing frameworks such as [Devise](https://github.com/heartcombo/devise), [Pundit](https://github.com/varvet/pundit), and [CanCanCan](https://github.com/CanCanCommunity/cancancan).
|
6
|
+
|
7
|
+
## Highlights
|
8
|
+
|
9
|
+
* Feature-rich [forms](components/forms.html) including a powerful [JSON form field component](components/form-fields/json.html).
|
10
|
+
* Simple and secure [auto-suggest text search](components/form-fields/text-search.html) widgets.
|
11
|
+
* [Tables](components/tables.html) with built-in pagination and action buttons for viewing/editing/deleting records.
|
12
|
+
* [Decorators](decorators.html) for overriding default display fields with simple _Rails_ view partials.
|
13
|
+
* Automated [permissions](access-control/authorization/permissions.html) that can be applied to all application endpoints with minimal effort.
|
14
|
+
* Sensible defaults to help you build your application quickly while also allowing you to customize when needed.
|
15
|
+
* _ActiveElement_ attempts to provide a framework of familiar patterns that work with you instead of against you. It does not attempt to do everything for you and avoids behind-the-scenes magic where possible.
|
16
|
+
* [Bootstrap](https://getbootstrap.com/) styling with [customizable themes](themes.html).
|
17
|
+
|
18
|
+
See the [Setup Guide](setup.html) and browse the rest of the documentation for full usage examples.
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# Setup
|
2
|
+
|
3
|
+
To integrate _ActiveElement_ into your _Rails_ application, follow the steps below:
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Install the `active_element` gem by adding the following to your `Gemfile`:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'active_element'
|
11
|
+
```
|
12
|
+
|
13
|
+
Then rebuild your bundle:
|
14
|
+
|
15
|
+
```console
|
16
|
+
$ bundle install
|
17
|
+
```
|
18
|
+
|
19
|
+
## Application Controller
|
20
|
+
|
21
|
+
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
|
+
|
23
|
+
If you want to add custom content to the layout, see the [Hooks](hooks.html) documentation.
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
# app/controllers/application_controller.rb
|
27
|
+
|
28
|
+
class ApplicationController < ActiveElement::ApplicationController
|
29
|
+
# ...
|
30
|
+
end
|
31
|
+
```
|
32
|
+
|
33
|
+
## Create a View
|
34
|
+
|
35
|
+
We'll use `UsersController` in this example, but you can replace this with whatever controller you want to use with _ActiveElement_.
|
36
|
+
|
37
|
+
Assuming your controller is defined something like this:
|
38
|
+
|
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:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
# config/routes.rb
|
53
|
+
|
54
|
+
Rails.application.routes.draw do
|
55
|
+
resources :users
|
56
|
+
end
|
57
|
+
```
|
58
|
+
|
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' %>
|
65
|
+
|
66
|
+
<%= active_element.component.table collection: @users, fields: [:id, :email, :name] %>
|
67
|
+
```
|
68
|
+
|
69
|
+
Adjust the `fields` to match whatever attributes you want to display for each `User` in your table.
|
70
|
+
|
71
|
+
Start your _Rails_ application and browse to `/users` to see your new users index.
|
72
|
+
|
73
|
+
## Next Steps
|
74
|
+
|
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.
|
@@ -0,0 +1 @@
|
|
1
|
+
# Check Boxes
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# Controller Params
|
2
|
+
|
3
|
+
_JSON_ fields are pre-processed by _ActiveElement_ before they arrive as controller `params`. Read the [Behind the Scenes](#behind-the-scenes) section if you want to know exactly what happens during this pre-processing.
|
4
|
+
|
5
|
+
Pre-processing _JSON_ parameters means that you get a regular _Rails_ controller `params` object (i.e. an instance of `ActionController::Parameters`) with all of the _JSON_ parameters available as though they were normal form parameters, so you can use them with `params.require(...).permit(...)`, pass them to `create` or `update` and let _ActiveRecord_ translate them back to _JSON_.
|
6
|
+
|
7
|
+
_ActiveElement_ does not automatically `permit` parameters but it does map types for you based on the defined [schema](schema.html). This means you don't have to manually parse dates, decimals, etc. if you need to work with them in your controller.
|
8
|
+
|
9
|
+
Each mapped type is specifically chosen to be safe to serialize back into _JSON_. This _JSON_ data can then be edited by _ActiveElement's_ `json_field` in your forms, allowing you to build complex forms with minimal effort and without having to work directly with the underlying _JSON_.
|
10
|
+
|
11
|
+
## Example Schema and Controller
|
12
|
+
|
13
|
+
### Schema
|
14
|
+
|
15
|
+
```yaml
|
16
|
+
# config/forms/user/pets.yml
|
17
|
+
---
|
18
|
+
type: array
|
19
|
+
shape:
|
20
|
+
type: object
|
21
|
+
shape:
|
22
|
+
fields:
|
23
|
+
- name: name
|
24
|
+
type: string
|
25
|
+
- name: age
|
26
|
+
type: integer
|
27
|
+
- name: animal
|
28
|
+
type: string
|
29
|
+
options:
|
30
|
+
- Cat
|
31
|
+
- Dog
|
32
|
+
- Polar Bear
|
33
|
+
- name: favorite_foods
|
34
|
+
type: array
|
35
|
+
shape:
|
36
|
+
type: string
|
37
|
+
options:
|
38
|
+
- Biscuits
|
39
|
+
- Plants
|
40
|
+
- Carpet
|
41
|
+
```
|
42
|
+
|
43
|
+
### Controller
|
44
|
+
|
45
|
+
Below you can see that the `email` param (a regular _Rails_ `email_field` or `text_field`) is used in conjunction with the `pets` param (an _ActiveElemen_ `json_field`).
|
46
|
+
|
47
|
+
See the official _Rails_ [ActionController::Parameters documentation](https://api.rubyonrails.org/classes/ActionController/Parameters.html) for more details.
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
class UsersController < ActiveElement::ApplicationController
|
51
|
+
def update
|
52
|
+
@user = User.find(params[:id])
|
53
|
+
if user.update(user_params)
|
54
|
+
flash.notice = 'User updated'
|
55
|
+
redirect_to user_path(@user)
|
56
|
+
else
|
57
|
+
flash.alert = 'User update failed'
|
58
|
+
render :edit
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def user_params
|
65
|
+
params.require(:user).permit(:email, pets: [:name, :age, :animal, favorite_foods: []])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
## Behind the Scenes
|
71
|
+
|
72
|
+
_ActiveElement_ tries to avoid behind-the-scenes magic where possible but, in this case, allowing fully transparent bi-directional _JSON_ parsing and type coercion is so convenient that we make an exception. Here's what happens when you generate a form with a _JSON_ field and then submit the form back to your _Rails_ application:
|
73
|
+
|
74
|
+
1. A `hidden` field `__json_fields[]` is created with its `value` set to the name of the field in dot notation, e.g. `user.pets`.
|
75
|
+
1. Another `hidden` field `__json_field_schemas[users][pets]` is created with its `value` set to an empty string. The front end _Javascript_ updates this when the form loads with the full schema. This ensures the data and schema are always consistent, even if the schema file changes between loading the form and submitting it.
|
76
|
+
1. Whenever the _JSON_ field is updated, the `value` for the main `input` field is set to the full state of the field's data structure, as a _JSON_ string.
|
77
|
+
1. When the form is submitted, a `before_action` in `ActiveElement::ApplicationController` intercepts the request and parses the _JSON_ data structure for any fields listed in the `__json_fields` array.
|
78
|
+
1. The resulting data structure (a _Ruby_ `Array` or `Hash`) is then traversed recursively, applying type coercion to any fields specified in the schema that require it, e.g. a `decimal` schema definition produces a `BigDecimal` for all applicable values in the data structure.
|
79
|
+
1. A new `ActionController::Parameters` object is created with all the regular fields (`text_field`, etc.) included, plus the transformed data structures for the _JSON_ fields.
|
80
|
+
1. The meta parameters `__json_fields` and `__json_field_schemas` are removed from the result. You'll see them in the logs but they won't get in the way in your controller.
|
81
|
+
1. The `request.params` object is re-assigned to the newly-constructed `ActionController::Parameters` object and the request continues as normal.
|
82
|
+
|
83
|
+
If you have worked with submitting _JSON_ to _Rails_ controllers before then you likely will have come across numerous edge cases and difficulties with handling different parameter types, deeply nested values, parser errors, etc. that required custom code to handle.
|
84
|
+
|
85
|
+
_ActiveElement_ aims to remove that effort completely and provide a `params` object that is familiar and consistent with _Rails_ conventions, so all you need to do is pass the params to your _ActiveRecord_ `create`/`update` methods and everything should work seamlessly (submit a [bug report](https://github.com/bobf/active_element/issues) if it doesn't!), while also giving you the benefit of being able to work with _Ruby_ objects.
|
86
|
+
|
87
|
+
For example, if you need to sort an array of objects by date before saving back to the database then it's as simple as:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
def sort_family_by_date_of_birth
|
91
|
+
user_params[:family].sort_by! { |family_member| family_member[:date_of_birth] }
|
92
|
+
end
|
93
|
+
|
94
|
+
def user_params
|
95
|
+
params.require(:user).permit(family: [:date_of_birth, :name, :relation])
|
96
|
+
end
|
97
|
+
```
|
@@ -0,0 +1,283 @@
|
|
1
|
+
# Schema
|
2
|
+
|
3
|
+
The `json_field` schema defines the shape of each _JSON_ object contained in your database.
|
4
|
+
|
5
|
+
Each model attribute has its own schema definition, each stored in a separate file for easy maintenance.
|
6
|
+
|
7
|
+
See the [Types](types.html) documentation for a list of all available types.
|
8
|
+
|
9
|
+
The schema definition requires two parameters to be present at the top level:
|
10
|
+
|
11
|
+
* `type`
|
12
|
+
* `shape`
|
13
|
+
|
14
|
+
The `type` at the top level should always be `array` or `object`.
|
15
|
+
|
16
|
+
The `shape` parameter defines the structure of your _JSON_, either each element within your _JSON_ `array` or the keys within your _JSON_ `object`.
|
17
|
+
|
18
|
+
`type` is required for all fields, and `shape` is required for all `array` and `object` fields, either at the top level or recursively at any point in your definition.
|
19
|
+
|
20
|
+
## Array of strings
|
21
|
+
|
22
|
+
An `array` of `string` objects can be defined with the following schema:
|
23
|
+
|
24
|
+
```yaml
|
25
|
+
# config/forms/user/nicknames.yml
|
26
|
+
|
27
|
+
---
|
28
|
+
type: array
|
29
|
+
shape:
|
30
|
+
type: string
|
31
|
+
```
|
32
|
+
|
33
|
+
You can see this schema in action in the below example:
|
34
|
+
|
35
|
+
```rspec:html
|
36
|
+
subject do
|
37
|
+
active_element.component.form model: User.new(email: 'user@example.com'),
|
38
|
+
fields: [:email, :nicknames]
|
39
|
+
end
|
40
|
+
|
41
|
+
it { is_expected.to include 'user[nicknames]' }
|
42
|
+
```
|
43
|
+
|
44
|
+
If you watch the _Javascript_ console in your browser, you can see the internal state updating as you edit the content (this only occurs when `ActiveElement.debug` is set to `true` in _Javascript_).
|
45
|
+
|
46
|
+
## Pre-defined options
|
47
|
+
|
48
|
+
The `string` type accepts a parameter `options` which is a list of pre-defined options that will be used to render a `select` element populated with the defined options:
|
49
|
+
|
50
|
+
```yaml
|
51
|
+
# config/forms/user/permissions.yml
|
52
|
+
---
|
53
|
+
type: array
|
54
|
+
shape:
|
55
|
+
type: string
|
56
|
+
options:
|
57
|
+
- can_make_coffee
|
58
|
+
- can_drink_coffee
|
59
|
+
- can_discuss_coffee
|
60
|
+
```
|
61
|
+
|
62
|
+
We'll use the schema from the previous example as well and create two separate `json_field` elements, and this time we'll pre-populate the `nicknames` field with some values:
|
63
|
+
|
64
|
+
```rspec:html
|
65
|
+
subject do
|
66
|
+
active_element.component.form model: User.new(nicknames: ['Buster', 'Coffee Guy']),
|
67
|
+
fields: [:email, :permissions, :nicknames]
|
68
|
+
end
|
69
|
+
|
70
|
+
it { is_expected.to include 'Coffee Guy' }
|
71
|
+
```
|
72
|
+
|
73
|
+
## Array of Objects
|
74
|
+
|
75
|
+
To define an array of objects, set the `type` parameter of the `array`'s `shape` to `object` and specify another `shape` with a list of `fields`, each with an associated `name` and `type`.
|
76
|
+
|
77
|
+
The `name` is used as the `object`'s key when the input is converted to _JSON_:
|
78
|
+
|
79
|
+
```yaml
|
80
|
+
# config/forms/user/family.yml
|
81
|
+
---
|
82
|
+
type: array
|
83
|
+
shape:
|
84
|
+
type: object
|
85
|
+
shape:
|
86
|
+
fields:
|
87
|
+
- name: relation
|
88
|
+
type: string
|
89
|
+
options:
|
90
|
+
- Parent
|
91
|
+
- Sibling
|
92
|
+
- Spouse
|
93
|
+
- name: name
|
94
|
+
type: string
|
95
|
+
- name: date_of_birth
|
96
|
+
type: date
|
97
|
+
```
|
98
|
+
|
99
|
+
Like the previous example, we'll keep the existing fields we've defined to generate a more complex form.
|
100
|
+
|
101
|
+
We've also introduced a `date` field here, which generates an _HTML5_ `date` input field (see the [Types](types.html) section for more information on each of the available types).
|
102
|
+
|
103
|
+
```rspec:html
|
104
|
+
let(:user) do
|
105
|
+
User.new(email: 'user@example.com',
|
106
|
+
nicknames: ['Buster', 'Coffee Guy'],
|
107
|
+
permissions: ['can_drink_coffee'])
|
108
|
+
end
|
109
|
+
|
110
|
+
subject do
|
111
|
+
active_element.component.form model: user,
|
112
|
+
fields: [:email, :nicknames, :permissions, :family]
|
113
|
+
end
|
114
|
+
|
115
|
+
it { is_expected.to include 'Spouse' }
|
116
|
+
```
|
117
|
+
|
118
|
+
## Focus
|
119
|
+
|
120
|
+
So far things are pretty easy to manage, but if we have a user with a large family then the view will quickly become very cluttered and it will be difficult for users to navigate the form.
|
121
|
+
|
122
|
+
To keep things manageable, the `array` type has an extra parameter `focus`. Use this parameter to specify a list of fields from each `object` found in the `array`. The first _truthy_ value (e.g. a non-empty string) found on each field is displayed as a placeholder. You can specify as many fields as you like.
|
123
|
+
|
124
|
+
This time we'll use another _JSON_ column on our `User` model: `extended_family`. We'll populate it with a few more family members and we'll specify `focus` on `name` and `estranged`. The new field `estranged` is a `boolean`, which we'll use for family members whose name we've forgotten.
|
125
|
+
|
126
|
+
We'll use `Faker` to generate some random data:
|
127
|
+
|
128
|
+
```yaml
|
129
|
+
# config/forms/user/extended_family.yml
|
130
|
+
---
|
131
|
+
type: array
|
132
|
+
focus:
|
133
|
+
- name
|
134
|
+
- estranged
|
135
|
+
shape:
|
136
|
+
type: object
|
137
|
+
shape:
|
138
|
+
fields:
|
139
|
+
- name: relation
|
140
|
+
type: string
|
141
|
+
options:
|
142
|
+
- Cousin
|
143
|
+
- Aunt
|
144
|
+
- Uncle
|
145
|
+
- name: name
|
146
|
+
type: string
|
147
|
+
- name: date_of_birth
|
148
|
+
type: date
|
149
|
+
- name: estranged
|
150
|
+
type: boolean
|
151
|
+
```
|
152
|
+
|
153
|
+
```rspec:html
|
154
|
+
let(:user) do
|
155
|
+
User.new(
|
156
|
+
email: 'user@example.com',
|
157
|
+
nicknames: ['Buster', 'Coffee Guy'],
|
158
|
+
permissions: ['can_make_coffee', 'can_drink_coffee', 'can_discuss_coffee'],
|
159
|
+
extended_family: extended_family
|
160
|
+
)
|
161
|
+
end
|
162
|
+
|
163
|
+
let(:extended_family) do
|
164
|
+
20.times.map do
|
165
|
+
estranged = (rand(3) % 3).zero?
|
166
|
+
{ name: estranged ? nil : Faker::Name.unique.name,
|
167
|
+
relation: ['Cousin', 'Aunt', 'Uncle'].sample,
|
168
|
+
date_of_birth: Faker::Date.birthday,
|
169
|
+
estranged: estranged }
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
subject do
|
174
|
+
active_element.component.form model: user,
|
175
|
+
fields: [:email, :nicknames, :permissions, :extended_family]
|
176
|
+
end
|
177
|
+
|
178
|
+
it { is_expected.to include 'Coffee Guy' }
|
179
|
+
```
|
180
|
+
|
181
|
+
## Wrapping Up
|
182
|
+
|
183
|
+
Aside from the handful of special parameters for certain [Types](types.html) we've covered everything you need to know about defining a _JSON_ object schema.
|
184
|
+
|
185
|
+
To wrap things up, we'll combine all of our schemas into one single `object` schema and render a form. We'll call our field `user_data` and merge all the schemas into a single file. The only thing different about this schema compared to the others is that the top-level `type` is `object` instead of `array`. Otherwise, we're re-using all of the same mechanisms described above. Each schema was copy & pasted into the new schema under the `fields` array of the top object and a `name` was assigned to each one, otherwise they're completely unchanged.
|
186
|
+
|
187
|
+
Since we're in debug mode for this documentation, the state is logged to the _Javascript_ console each time you modify a form value, so you can see what would be submitted if this form were connected to a real application.
|
188
|
+
|
189
|
+
Make sure you read the [Controller Parameters](controller-parameters.html) section to see how to use [Rails StrongParameters](https://api.rubyonrails.org/classes/ActionController/StrongParameters.html) in conjunction with _ActiveElement_ _JSON_ fields.
|
190
|
+
|
191
|
+
### Form
|
192
|
+
|
193
|
+
```rspec:html
|
194
|
+
let(:user) do
|
195
|
+
User.new(
|
196
|
+
email: 'user@example.com',
|
197
|
+
user_data: {
|
198
|
+
nicknames: ['Buster', 'Coffee Guy'],
|
199
|
+
permissions: ['can_make_coffee', 'can_drink_coffee', 'can_discuss_coffee'],
|
200
|
+
extended_family: extended_family
|
201
|
+
}
|
202
|
+
)
|
203
|
+
end
|
204
|
+
|
205
|
+
let(:extended_family) do
|
206
|
+
20.times.map do
|
207
|
+
estranged = (rand(3) % 3).zero?
|
208
|
+
{ name: estranged ? nil : Faker::Name.unique.name,
|
209
|
+
relation: ['Cousin', 'Aunt', 'Uncle'].sample,
|
210
|
+
date_of_birth: Faker::Date.birthday,
|
211
|
+
estranged: estranged }
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
subject do
|
216
|
+
active_element.component.form model: user, fields: [:email, :user_data]
|
217
|
+
end
|
218
|
+
|
219
|
+
it { is_expected.to include 'Coffee Guy' }
|
220
|
+
```
|
221
|
+
|
222
|
+
### Schema
|
223
|
+
|
224
|
+
```yaml
|
225
|
+
# config/forms/user/user_data.yml
|
226
|
+
---
|
227
|
+
type: object
|
228
|
+
shape:
|
229
|
+
fields:
|
230
|
+
- name: nicknames
|
231
|
+
type: array
|
232
|
+
shape:
|
233
|
+
type: string
|
234
|
+
|
235
|
+
- name: permissions
|
236
|
+
type: array
|
237
|
+
shape:
|
238
|
+
type: string
|
239
|
+
options:
|
240
|
+
- can_make_coffee
|
241
|
+
- can_drink_coffee
|
242
|
+
- can_discuss_coffee
|
243
|
+
|
244
|
+
- name: family
|
245
|
+
type: array
|
246
|
+
shape:
|
247
|
+
type: object
|
248
|
+
shape:
|
249
|
+
fields:
|
250
|
+
- name: relation
|
251
|
+
type: string
|
252
|
+
options:
|
253
|
+
- Parent
|
254
|
+
- Sibling
|
255
|
+
- Spouse
|
256
|
+
- name: name
|
257
|
+
type: string
|
258
|
+
- name: date_of_birth
|
259
|
+
type: date
|
260
|
+
|
261
|
+
- name: extended_family
|
262
|
+
type: array
|
263
|
+
focus:
|
264
|
+
- name
|
265
|
+
- estranged
|
266
|
+
shape:
|
267
|
+
type: object
|
268
|
+
shape:
|
269
|
+
fields:
|
270
|
+
- name: relation
|
271
|
+
type: string
|
272
|
+
options:
|
273
|
+
- Cousin
|
274
|
+
- Aunt
|
275
|
+
- Uncle
|
276
|
+
- name: name
|
277
|
+
type: string
|
278
|
+
- name: date_of_birth
|
279
|
+
type: date
|
280
|
+
- name: estranged
|
281
|
+
type: boolean
|
282
|
+
```
|
283
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# Types
|
2
|
+
|
3
|
+
The following _JSON_ primitives are supported in schema definitions. Note that `null` cannot be specified as a field type, but it is the default value for most types.
|
4
|
+
|
5
|
+
|Type|Description|Ruby Mapping
|
6
|
+
|-|-|
|
7
|
+
| `object` | A key-value data construct. | `Hash` (`{}`)
|
8
|
+
| `array` | An ordered sequence of objects of any type | `Array` (`[]`)
|
9
|
+
| `string` | A sequence of _Unicode_ characters | `String` (`""`)
|
10
|
+
| `boolean` | A `true` or `false` value | `TrueClass` or `FalseClass` (`true`, `false`)
|
11
|
+
| `float` | A floating-point number | `Float` (`0.1`)
|
12
|
+
| `null` | An empty value | `NilClass` (`nil`)
|
13
|
+
|
14
|
+
And the following extensions are provided:
|
15
|
+
|
16
|
+
|Type|Description|Ruby Mapping
|
17
|
+
|-|-|
|
18
|
+
| `date` | An [iso8601 date](https://en.wikipedia.org/wiki/ISO_8601#Dates) stored as `YYYY-MM-DD`. | `Date`
|
19
|
+
| `datetime` | An [iso8601-1:2019 combined date and time](https://en.wikipedia.org/wiki/ISO_8601#Combined_date_and_time_representations) stored as `YYYY-MM-DDThh:mm:ss` | `DateTime`
|
20
|
+
| `time` | An [iso8601-1:2019 time](https://en.wikipedia.org/wiki/ISO_8601#Times) stored as `hh:mm` | `String` ([*](#time-of-day))
|
21
|
+
| `decimal` | An infinite-precision decimal object stored as a string, e.g. `"3.141592653589793"` | `BigDecimal`
|
22
|
+
| `integer` | A whole number, stored as a _JSON_ `float`. | `Integer`
|
23
|
+
|
24
|
+
Defining types in your schema allows you to work directly with _Ruby_ objects when the form is submitted to a controller. The `params` arrive pre-parsed in formats that match the automatic serialization that _ActiveRecord_ performs. e.g. converting a `DateTime` object to _JSON_ in _Rails_ outputs the following:
|
25
|
+
|
26
|
+
```irb
|
27
|
+
irb(main):001:0> puts({ time: Time.now.utc }.to_json)
|
28
|
+
|
29
|
+
{"time":"2023-06-12T20:13:11.308Z"}
|
30
|
+
```
|
31
|
+
|
32
|
+
## Time of Day
|
33
|
+
|
34
|
+
Note that _Ruby_ has no native way to store a time of day without a date, so `time` fields are coerced to `String` (`hh:mm`) when processed into controller params.
|
35
|
+
|
36
|
+
You may find the [Tod](https://github.com/JackC/tod) gem useful when working with these values.
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# JSON
|
2
|
+
|
3
|
+
A custom form field `json_field` is provided for editing _JSON_ data.
|
4
|
+
|
5
|
+
The field is schema-based and expects to find a schema definition in `config/forms/<model>/<attribute>.yml`.
|
6
|
+
|
7
|
+
For example, to edit a _JSON_ attribute named `permissions` on a `User` model, _ActiveElement_ requires a file named `config/forms/user/permissions.yml`.
|
8
|
+
|
9
|
+
See the [Schema](json/schema.html) documentation for a detailed description of how this file should be generated.
|
10
|
+
|
11
|
+
The `json_field` type will be automatically selected for _ActiveRecord_ `json` and `jsonb` columns included in the `fields` array when used in conjunction with an _ActiveRecord_ model.
|
12
|
+
|
13
|
+
|
14
|
+
## Example Form
|
15
|
+
|
16
|
+
This example is powered by the [example schema](#example-schema) below.
|
17
|
+
|
18
|
+
The `pets` column on the `users` table is a `json` column so _ActiveElement_ loads the schema and generates a dynamic, interactive form component allowing users to edit the data structure without having to manually edit _JSON_.
|
19
|
+
|
20
|
+
Click the **Rendered Output** tab to see it in action:
|
21
|
+
|
22
|
+
```rspec:html
|
23
|
+
let(:user) do
|
24
|
+
User.new(
|
25
|
+
email: 'user@example.com',
|
26
|
+
pets: [
|
27
|
+
{ animal: 'Cat', name: 'Hercules', favorite_foods: ['Plants', 'Biscuits'] },
|
28
|
+
{ animal: 'Dog', name: 'Samson' }
|
29
|
+
]
|
30
|
+
)
|
31
|
+
end
|
32
|
+
|
33
|
+
subject do
|
34
|
+
active_element.component.form model: user, title: 'New User', fields: [:email, :pets]
|
35
|
+
end
|
36
|
+
|
37
|
+
it { is_expected.to include 'Hercules' }
|
38
|
+
```
|
39
|
+
|
40
|
+
## Example Schema
|
41
|
+
|
42
|
+
The example above is powered by this schema definition:
|
43
|
+
|
44
|
+
```yaml
|
45
|
+
# config/forms/user/pets.yml
|
46
|
+
---
|
47
|
+
type: array
|
48
|
+
shape:
|
49
|
+
type: object
|
50
|
+
shape:
|
51
|
+
fields:
|
52
|
+
- name: name
|
53
|
+
type: string
|
54
|
+
- name: age
|
55
|
+
type: integer
|
56
|
+
- name: animal
|
57
|
+
type: string
|
58
|
+
options:
|
59
|
+
- Cat
|
60
|
+
- Dog
|
61
|
+
- Polar Bear
|
62
|
+
- name: favorite_foods
|
63
|
+
type: array
|
64
|
+
shape:
|
65
|
+
type: string
|
66
|
+
options:
|
67
|
+
- Biscuits
|
68
|
+
- Plants
|
69
|
+
- Carpet
|
70
|
+
```
|