active_element 0.0.9 → 0.0.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (214) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -2
  3. data/.strong_versions.yml +1 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +111 -78
  6. data/Makefile +10 -0
  7. data/active_element.gemspec +1 -1
  8. data/app/assets/javascripts/active_element/application.js +1 -0
  9. data/app/assets/javascripts/active_element/form.js +16 -32
  10. data/app/assets/javascripts/active_element/json_field.js +391 -135
  11. data/app/assets/javascripts/active_element/setup.js +13 -8
  12. data/app/assets/javascripts/active_element/text_search_field.js +27 -28
  13. data/app/assets/javascripts/active_element/theme.js +1 -1
  14. data/app/assets/javascripts/active_element/timezones.js +6 -0
  15. data/app/assets/stylesheets/active_element/_dark.scss +86 -0
  16. data/app/assets/stylesheets/active_element/_variables.scss +2 -1
  17. data/app/assets/stylesheets/active_element/application.scss +170 -31
  18. data/app/controllers/active_element/application_controller.rb +5 -0
  19. data/app/controllers/concerns/active_element/default_controller_actions.rb +38 -0
  20. data/app/views/active_element/_user.html.erb +20 -0
  21. data/app/views/active_element/components/fields/_json.html.erb +24 -0
  22. data/app/views/active_element/components/form/_check_box.html.erb +1 -0
  23. data/app/views/active_element/components/form/_check_boxes.html.erb +1 -1
  24. data/app/views/active_element/components/form/_datetime_range_field.html.erb +14 -0
  25. data/app/views/active_element/components/form/_field.html.erb +10 -7
  26. data/app/views/active_element/components/form/_generic_field.html.erb +1 -0
  27. data/app/views/active_element/components/form/_json.html.erb +10 -2
  28. data/app/views/active_element/components/form/_label.html.erb +12 -1
  29. data/app/views/active_element/components/form/_select.html.erb +4 -1
  30. data/app/views/active_element/components/form/_summary.html.erb +11 -1
  31. data/app/views/active_element/components/form/_templates.html.erb +37 -22
  32. data/app/views/active_element/components/form/_text_area.html.erb +2 -1
  33. data/app/views/active_element/components/form/_text_search.html.erb +7 -3
  34. data/app/views/active_element/components/form.html.erb +20 -17
  35. data/app/views/active_element/components/json.html.erb +1 -0
  36. data/app/views/active_element/components/navbar.html.erb +26 -0
  37. data/app/views/active_element/components/table/_collection_row.html.erb +2 -1
  38. data/app/views/active_element/components/table/_field.html.erb +8 -0
  39. data/app/views/active_element/components/table/collection.html.erb +1 -1
  40. data/app/views/active_element/components/table/item.html.erb +5 -4
  41. data/app/views/active_element/default_views/edit.html.erb +5 -0
  42. data/app/views/active_element/default_views/index.html.erb +15 -0
  43. data/app/views/active_element/default_views/new.html.erb +4 -0
  44. data/app/views/active_element/default_views/show.html.erb +7 -0
  45. data/app/views/active_element/navbar/_menu.html.erb +1 -30
  46. data/app/views/active_element/theme/_select.html.erb +1 -1
  47. data/app/views/layouts/active_element.html.erb +17 -2
  48. data/config/brakeman.ignore +48 -0
  49. data/example_app/.gitattributes +7 -0
  50. data/example_app/.gitignore +35 -0
  51. data/example_app/.ruby-version +1 -0
  52. data/example_app/Gemfile +34 -0
  53. data/example_app/Gemfile.lock +296 -0
  54. data/example_app/README.md +24 -0
  55. data/example_app/Rakefile +6 -0
  56. data/example_app/app/assets/config/manifest.js +4 -0
  57. data/example_app/app/assets/images/.keep +0 -0
  58. data/example_app/app/assets/stylesheets/application.css +15 -0
  59. data/example_app/app/channels/application_cable/channel.rb +4 -0
  60. data/example_app/app/channels/application_cable/connection.rb +4 -0
  61. data/example_app/app/controllers/application_controller.rb +12 -0
  62. data/example_app/app/controllers/concerns/.keep +0 -0
  63. data/example_app/app/controllers/pets_controller.rb +6 -0
  64. data/example_app/app/controllers/users_controller.rb +6 -0
  65. data/example_app/app/helpers/application_helper.rb +2 -0
  66. data/example_app/app/javascript/application.js +3 -0
  67. data/example_app/app/javascript/controllers/application.js +9 -0
  68. data/example_app/app/javascript/controllers/hello_controller.js +7 -0
  69. data/example_app/app/javascript/controllers/index.js +11 -0
  70. data/example_app/app/jobs/application_job.rb +7 -0
  71. data/example_app/app/mailers/application_mailer.rb +4 -0
  72. data/example_app/app/models/application_record.rb +3 -0
  73. data/example_app/app/models/concerns/.keep +0 -0
  74. data/example_app/app/models/pet.rb +3 -0
  75. data/example_app/app/models/user.rb +8 -0
  76. data/example_app/app/views/layouts/application.html.erb +16 -0
  77. data/example_app/app/views/layouts/mailer.html.erb +13 -0
  78. data/example_app/app/views/layouts/mailer.text.erb +1 -0
  79. data/example_app/app/views/pets/index.html.erb +3 -0
  80. data/example_app/app/views/users/show.html.erb +3 -0
  81. data/example_app/bin/bundle +109 -0
  82. data/example_app/bin/importmap +4 -0
  83. data/example_app/bin/rails +4 -0
  84. data/example_app/bin/rake +4 -0
  85. data/example_app/bin/setup +33 -0
  86. data/example_app/config/application.rb +22 -0
  87. data/example_app/config/boot.rb +4 -0
  88. data/example_app/config/cable.yml +10 -0
  89. data/example_app/config/credentials.yml.enc +1 -0
  90. data/example_app/config/database.yml +25 -0
  91. data/example_app/config/environment.rb +5 -0
  92. data/example_app/config/environments/development.rb +70 -0
  93. data/example_app/config/environments/production.rb +93 -0
  94. data/example_app/config/environments/test.rb +60 -0
  95. data/example_app/config/importmap.rb +7 -0
  96. data/example_app/config/initializers/assets.rb +12 -0
  97. data/example_app/config/initializers/content_security_policy.rb +25 -0
  98. data/example_app/config/initializers/devise.rb +16 -0
  99. data/example_app/config/initializers/filter_parameter_logging.rb +8 -0
  100. data/example_app/config/initializers/inflections.rb +16 -0
  101. data/example_app/config/initializers/permissions_policy.rb +11 -0
  102. data/example_app/config/locales/devise.en.yml +65 -0
  103. data/example_app/config/locales/en.yml +33 -0
  104. data/example_app/config/puma.rb +43 -0
  105. data/example_app/config/routes.rb +8 -0
  106. data/example_app/config/storage.yml +34 -0
  107. data/example_app/config.ru +6 -0
  108. data/example_app/db/migrate/20230616210539_create_pet.rb +12 -0
  109. data/example_app/db/migrate/20230616211328_devise_create_users.rb +46 -0
  110. data/example_app/db/schema.rb +37 -0
  111. data/example_app/db/seeds.rb +33 -0
  112. data/example_app/lib/assets/.keep +0 -0
  113. data/example_app/lib/tasks/.keep +0 -0
  114. data/example_app/log/.keep +0 -0
  115. data/example_app/public/404.html +67 -0
  116. data/example_app/public/422.html +67 -0
  117. data/example_app/public/500.html +66 -0
  118. data/example_app/public/apple-touch-icon-precomposed.png +0 -0
  119. data/example_app/public/apple-touch-icon.png +0 -0
  120. data/example_app/public/favicon.ico +0 -0
  121. data/example_app/public/robots.txt +1 -0
  122. data/example_app/storage/.keep +0 -0
  123. data/example_app/test/application_system_test_case.rb +5 -0
  124. data/example_app/test/channels/application_cable/connection_test.rb +11 -0
  125. data/example_app/test/controllers/.keep +0 -0
  126. data/example_app/test/fixtures/files/.keep +0 -0
  127. data/example_app/test/fixtures/users.yml +11 -0
  128. data/example_app/test/helpers/.keep +0 -0
  129. data/example_app/test/integration/.keep +0 -0
  130. data/example_app/test/mailers/.keep +0 -0
  131. data/example_app/test/models/.keep +0 -0
  132. data/example_app/test/models/user_test.rb +7 -0
  133. data/example_app/test/system/.keep +0 -0
  134. data/example_app/test/test_helper.rb +13 -0
  135. data/example_app/tmp/.keep +0 -0
  136. data/example_app/tmp/pids/.keep +0 -0
  137. data/example_app/tmp/storage/.keep +0 -0
  138. data/example_app/vendor/.keep +0 -0
  139. data/example_app/vendor/javascript/.keep +0 -0
  140. data/lib/active_element/component.rb +9 -2
  141. data/lib/active_element/components/collection_table.rb +9 -2
  142. data/lib/active_element/components/email_fields.rb +14 -0
  143. data/lib/active_element/components/form.rb +48 -17
  144. data/lib/active_element/components/navbar.rb +64 -0
  145. data/lib/active_element/components/phone_fields.rb +14 -0
  146. data/lib/active_element/components/text_search/authorization.rb +9 -6
  147. data/lib/active_element/components/text_search/component.rb +4 -2
  148. data/lib/active_element/components/text_search.rb +4 -0
  149. data/lib/active_element/components/util/association_mapping.rb +74 -19
  150. data/lib/active_element/components/util/display_value_mapping.rb +13 -4
  151. data/lib/active_element/components/util/form_field_mapping.rb +127 -10
  152. data/lib/active_element/components/util/form_value_mapping.rb +3 -7
  153. data/lib/active_element/components/util/i18n.rb +1 -1
  154. data/lib/active_element/components/util/record_mapping.rb +43 -11
  155. data/lib/active_element/components/util/record_path.rb +21 -4
  156. data/lib/active_element/components/util.rb +12 -5
  157. data/lib/active_element/components.rb +3 -0
  158. data/lib/active_element/controller_action.rb +8 -2
  159. data/lib/active_element/controller_interface.rb +47 -5
  160. data/lib/active_element/default_controller.rb +93 -0
  161. data/lib/active_element/default_record_params.rb +62 -0
  162. data/lib/active_element/default_text_search.rb +110 -0
  163. data/lib/active_element/json_field_schema.rb +59 -0
  164. data/lib/active_element/pre_render_processors/json.rb +98 -0
  165. data/lib/active_element/pre_render_processors.rb +11 -0
  166. data/lib/active_element/route.rb +12 -0
  167. data/lib/active_element/routes.rb +2 -1
  168. data/lib/active_element/version.rb +1 -1
  169. data/lib/active_element.rb +14 -32
  170. data/lib/tasks/active_element.rake +12 -1
  171. data/rspec-documentation/_head.html.erb +34 -0
  172. data/rspec-documentation/pages/000-Introduction.md +18 -0
  173. data/rspec-documentation/pages/005-Setup.md +75 -0
  174. data/rspec-documentation/pages/010-Components/Form Fields/Check Boxes.md +1 -0
  175. data/rspec-documentation/pages/010-Components/Form Fields/JSON/Controller Params.md +97 -0
  176. data/rspec-documentation/pages/010-Components/Form Fields/JSON/Schema.md +283 -0
  177. data/rspec-documentation/pages/010-Components/Form Fields/JSON/Types.md +36 -0
  178. data/rspec-documentation/pages/010-Components/Form Fields/JSON.md +70 -0
  179. data/rspec-documentation/pages/010-Components/Form Fields/Text Search.md +133 -0
  180. data/rspec-documentation/pages/010-Components/Form Fields.md +46 -0
  181. data/rspec-documentation/pages/010-Components/Forms.md +44 -0
  182. data/rspec-documentation/pages/010-Components/JSON Data.md +23 -0
  183. data/rspec-documentation/pages/010-Components/Navbar.md +56 -0
  184. data/rspec-documentation/pages/010-Components/Page Section Title.md +13 -0
  185. data/rspec-documentation/pages/010-Components/Page Subtitle.md +11 -0
  186. data/rspec-documentation/pages/010-Components/Page Title.md +11 -0
  187. data/rspec-documentation/pages/010-Components/Tables/Collection Table.md +29 -0
  188. data/rspec-documentation/pages/010-Components/Tables/Item Table.md +18 -0
  189. data/rspec-documentation/pages/010-Components/Tables/Options.md +19 -0
  190. data/rspec-documentation/pages/010-Components/Tables.md +29 -0
  191. data/rspec-documentation/pages/010-Components.md +15 -0
  192. data/rspec-documentation/pages/020-Access Control/010-Authentication.md +20 -0
  193. data/rspec-documentation/pages/020-Access Control/020-Authorization/Environments.md +9 -0
  194. data/rspec-documentation/pages/020-Access Control/020-Authorization/Permissions/Custom Routes.md +41 -0
  195. data/rspec-documentation/pages/020-Access Control/020-Authorization/Permissions.md +58 -0
  196. data/rspec-documentation/pages/020-Access Control/020-Authorization/Setup.md +27 -0
  197. data/rspec-documentation/pages/020-Access Control/020-Authorization.md +11 -0
  198. data/rspec-documentation/pages/020-Access Control.md +31 -0
  199. data/rspec-documentation/pages/040-Decorators/Inline Decorators.md +24 -0
  200. data/rspec-documentation/pages/040-Decorators/View Decorators.md +55 -0
  201. data/rspec-documentation/pages/040-Decorators.md +12 -0
  202. data/rspec-documentation/pages/300-Alternatives.md +21 -0
  203. data/rspec-documentation/pages/900-License.md +11 -0
  204. data/rspec-documentation/spec_helper.rb +53 -16
  205. data/rspec-documentation/support.rb +84 -0
  206. metadata +155 -14
  207. data/rspec-documentation/pages/Components/Forms.md +0 -1
  208. data/rspec-documentation/pages/Components/Tables.md +0 -47
  209. data/rspec-documentation/pages/Components.md +0 -1
  210. data/rspec-documentation/pages/Decorators/Inline Decorators.md +0 -1
  211. data/rspec-documentation/pages/Decorators/View Decorators.md +0 -1
  212. data/rspec-documentation/pages/Index.md +0 -3
  213. data/rspec-documentation/pages/Util/I18n.md +0 -1
  214. /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,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
+ ```