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,133 @@
1
+ # Text Search
2
+
3
+ _ActiveElement_ provides a `text_search_field` capable of generating a full text search auto-suggest widget in a form.
4
+
5
+ The `text_search_field` accepts user input and presents a list of matching options, setting the field's `value` to a value specified by the field's configuration.
6
+
7
+ For example, you may want a `user_id` field that allows users to search by `name` and `email` on the `User` model, and returning the `id` for the selected result.
8
+
9
+ The field requires some extra configuration to prevent leaking unwanted data to the front end, and to allow you to select which columns should be searched and which column should be submitted to your controller `params` as the field's `value`.
10
+
11
+ ## Example
12
+
13
+ We'll make a form that creates a `Pet` record and associates it with a `User` by setting a `user_id` field.
14
+
15
+ Click the **Rendered Output** tab and type a few characters into the search field. In the real world this would be connected to your application, but for the purpose of this documentation we've stubbed the _Javascript_ `fetch` function to return some fake results based on the input.
16
+
17
+ ```rspec:html
18
+ subject do
19
+ active_element.component.form model: Pet.new,
20
+ fields: [
21
+ :name,
22
+ :animal,
23
+ [:user_id,
24
+ :text_search_field,
25
+ { search: { model: :user, with: [:name, :email], providing: :id } }]
26
+ ]
27
+ end
28
+
29
+ it { is_expected.to include 'Search...' }
30
+ ```
31
+
32
+ When you select an option from the suggestions, the full display value appears in the field, but the controller only receives the `id` attribute in `params[:pet][:user_id]` - the display value is a hidden field that gets overwritten by the actual value.
33
+
34
+ ## Routes
35
+
36
+ If you run `rails routes` in your project once you've set up _ActiveElement_ you'll notice an extra route added for each of your controllers:
37
+
38
+ ```console
39
+ $ rails routes
40
+
41
+ Routes for ActiveElement::Engine:
42
+ pets__active_element_text_search POST /pets/_active_element_text_search(.:format) pets#_active_element_text_search
43
+ ```
44
+
45
+ These routes receive text queries and generate results based on your configuration. They're required for the `text_search_field` to work but you can safely ignore them, they are protected by [permissions](#permissions) if you have [authorization](../../../access-control/authorization) configured, as well as some [model configuration](#model-configuration) to ensure that only fields you explicitly configure are searchable.
46
+
47
+ ## Configuration
48
+
49
+ ### Model Configuration
50
+
51
+ Allowing users to search arbitrary columns on arbitrary models would be a major security risk. To prevent unauthorized data access and _DoS_ vulnerabilities (e.g. allowing searching unindexed database columns), _ActiveElement_ requires that models define which columns are searchable and which columns can provide values. The example above requires the following model definition in order to work:
52
+
53
+ ```ruby
54
+ # app/models/user.rb
55
+
56
+ class User < ApplicationRecord
57
+ authorize_active_element_text_search with: [:name, :email], providing: :id
58
+ end
59
+ ```
60
+
61
+ Now even if an attacker sends a custom request to the text search endpoint of your application, only the `name` and `email` columns on the `users` table are searchable, while the `id` column can be returned in results but not searched.
62
+
63
+ **Important note:** Specifying columns as searchable using the `with` keyword implicitly permits their matched values to be returned in the result sent back to the front end. This is for two reasons:
64
+
65
+ 1. Displaying the full matched search parameters provides a more coherent user experience. If we only return the `id`, the user won't know if they're selecting the option they're looking for.
66
+ 1. Forcing the display of searched values is intended to reduce the risk of giving developers a false sense of security that only values listed in the `providing` keyword will be exposed. An attacker can use trivial techniques to gain the full value of a searchable column without being able to see it in the front end. By including the matched search values in the result, there is no ambiguity that these values are exposed to the front end application.
67
+
68
+ ### Inline Configuration
69
+
70
+ To allow re-use and to prevent cluttering your views, it is recommended to use [file-based configuration](#file-based), but inline configuration is also available.
71
+
72
+ We'll re-use the example from above, breaking down exactly what's happening:
73
+
74
+ ```rspec:html
75
+ subject do
76
+ active_element.component.form model: Pet.new,
77
+ fields: [
78
+ :name,
79
+ :animal,
80
+ [:user_id,
81
+ :text_search_field,
82
+ { search: { model: :user, with: [:name, :email], providing: :id } }]
83
+ ]
84
+ end
85
+
86
+ it { is_expected.to include 'Search...' }
87
+ ```
88
+
89
+ First we invoke the `form` component which renders our _HTML_ form, specifying `Pet.new` as the `model`, just like a regular _Rails_ form.
90
+
91
+ The `fields` array uses the usual format for the `name` and `animal` fields - the field type will be derived from the database type for each field.
92
+
93
+ The `user_id` field is specified as an array with three elements:
94
+
95
+ 1. The field name, `user_id`. This provides `params[:pet][:user_id]` when the form is submitted to the controller.
96
+ 1. The field type, _ActiveElement's_ `text_search_field`.
97
+ 1. An options hash, specifically including a `search` key that defines `model`, `with`, and `providing`.
98
+
99
+ It's important to clarify that specifying these fields in the view is not sufficient to provide security, since they simply provide metadata to help the front-end _Javascript_ component send the correct parameters. A user with access to the application could modify these fields and send arbitrary requests, which is why the [model configuration](#model-configuration) is required.
100
+
101
+ The `model` option translates `:user` into `User` to identify the _ActiveRecord_ model to use in the search, and the `with` and `providing` options specify the searchable fields (`with`) and the value field (`providing`). Only the `providing` field is included in the request `params`.
102
+
103
+ ### File-based Configuraton
104
+
105
+ Using inline configuration is fine for one-offs and examples, but it's easy to imagine a form definition becoming cluttered with multiple search fields defined, as well as requiring duplicating and maintaining the same search fields across different forms (e.g. `edit` and `new`).
106
+
107
+ To mitigate this, file-based configuration is recommended for text search fields.
108
+
109
+ The above configuration can be redefined by creating `config/forms/pet/user_id.yml`:
110
+
111
+ ```yaml
112
+ # config/forms/pet/user_id.yml
113
+
114
+ ---
115
+ type: text_search_field
116
+ options:
117
+ search:
118
+ model: user
119
+ with:
120
+ - name
121
+ - email
122
+ providing: id
123
+ ```
124
+
125
+ You can still use inline configuration to override these settings, but now the form can be defined as:
126
+
127
+ ```rspec:html
128
+ subject do
129
+ active_element.component.form model: Pet.new, fields: [:name, :animal, :user_id]
130
+ end
131
+
132
+ it { is_expected.to include 'Search...' }
133
+ ```
@@ -0,0 +1,46 @@
1
+ # Form Fields
2
+
3
+ _ActiveElement_ provides a selection of fields, most of which delegate to _Rails'_ own [form helpers](https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html).
4
+
5
+ Extra field types provided by _ActiveElement_ include:
6
+
7
+ * [`json_field`](form-fields/json.html): A rich _JSON_ schema-based form generator that recursively creates form elements for your _JSON_ objects.
8
+ * [`text_search_field`](form-fields/text-search.html): A full text search/auto-suggest component that provides a secure implementation with minimal configuration.
9
+ * [`check_boxes`](form-fields/check-boxes.html): A set of checkboxes that can optionally be provided as option groups.
10
+
11
+ Other field types are inferred from the data type and/or column name for each attribute passed to the `fields` array and leverage the existing [form helpers](https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html) provided by _Rails_. e.g. if you have an _ActiveRecord_ instance with an attribute whose database column type is `date`, a [date field](https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-date_field) will be automatically rendered. Similarly, a column named `email` will render an [`email_field`](https://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-email_field).
12
+
13
+ ```rspec:html
14
+ subject do
15
+ active_element.component.form model: User.new,
16
+ fields: [:email, :name, :date_of_birth]
17
+ end
18
+
19
+ it { is_expected.to include 'type="date"' }
20
+ ```
21
+
22
+ If you need to override these field types, use a two-element array of `[field_name, field_type]`:
23
+
24
+ ```rspec:html
25
+ subject do
26
+ active_element.component.form model: User.new,
27
+ fields: [:email, :name, [:date_of_birth, :text_field]]
28
+ end
29
+
30
+ it { is_expected.not_to include 'type="date"' }
31
+ ```
32
+
33
+ You can also pass a third element to the array to specify any options you want to pass to the field:
34
+
35
+ ```rspec:html
36
+ subject do
37
+ active_element.component.form model: User.new,
38
+ fields: [
39
+ :email,
40
+ :name,
41
+ [:date_of_birth, :text_field, { class: 'form-control my-class' }]
42
+ ]
43
+ end
44
+
45
+ it { is_expected.to include 'class="form-control my-class"' }
46
+ ```
@@ -0,0 +1,44 @@
1
+ # Forms
2
+
3
+ An interface for creating forms is provided by `active_element.component.form`, available in all views.
4
+
5
+ The `Form` component delegates to _Rails'_ `form_with` so any keyword arguments not processed by _ActiveElement_ can be passed as usual.
6
+
7
+ Forms in _ActiveElement_ provide a number of features designed to save you time and help you create powerful and flexible forms with minimal effort:
8
+
9
+ * Validation errors are automatically rendered for each form element when using _ActiveRecord_ objects or any object that implements `#errors`, `#valid?`, etc.
10
+ * Form inputs are inferred from the database column type of each field specified in the `fields` array.
11
+ * A powerful [_JSON_ editor](form-fields/json.html) is provided for editing complex _JSON_ objects with user-friendly form elements.
12
+ * A [full text search auto-suggest component](form-fields/text-search.html) with a quick and easy setup.
13
+ * Clean and consistent layout.
14
+
15
+ See the [full keyword argument specification](#options) for information on the various available options and the [Form Fields](form-fields.html) section for documentation on each individual field type.
16
+
17
+ ## Basic Example
18
+
19
+ ```rspec:html
20
+ subject do
21
+ active_element.component.form model: User.new,
22
+ fields: [:name, :email, :enabled]
23
+ end
24
+
25
+ it { is_expected.to include '<label for="user_name">' }
26
+ ```
27
+
28
+ ## Options
29
+
30
+ Forms receive the following keyword arguments. Any other arguments are passed to the underlying `form_with` call used to generate the form.
31
+
32
+ | Keyword | Description |
33
+ |-|-|
34
+ | `fields` | An `Array` of `Symbol`. Specifies the fields to render in the form. If a `model` keyword is used then this enables automatic type inference, otherwise the default field type is `text_field`. Pass an array of two-element arrays to specify a specific field type, e.g. `fields: [[:name, :text_field]]`. See [Form Fields](form-fields.html) documentation for available field types.
35
+ | `item` | Optionally pass a `Hash` instead of using `model`. This is useful for e.g. creating a form for generating search parameters.
36
+ | `submit` | Either a `String` specifying the text to display on the submit button, or a hash with the following optional keys: `[:label, :position]`. Set `position` to one of `[:top, :bottom, :both]`, e.g.: `submit: { label: 'Create', position: :top }`.
37
+ | `title` | A `String` specifying a title text to display above the form.
38
+ | `destroy` | A boolean specifying whether to display a "Delete" button above the form. This is only useful for forms editing an existing _ActiveRecord_ object, e.g. in an `edit.html.erb` view. Defaults to `false`.
39
+ | `action` | A `String` that overrides the default action (path the form is submitted to). If this value is not passed, the value is automatically inferred from the current controller path and action: `#new` and `#create` actions to e.g. `/users`, `#edit` and `#update` actions submit to e.g. `/users/:id/edit`.
40
+ | `method` | A `String` that overrides the default method (typically `PATCH` or `POST`). Like `action`, this is automatically inferred from the current controller path and action.
41
+ | `modal` | A boolean specifying whether to provide a button that opens the form as a modal. Defaults to `false`.
42
+ | `columns` | An `Integer` specifying how many columns to use when rending form inputs. Defaults to `1`.
43
+
44
+ For typical _Rails_ _RESTful_ routing patterns using _ActiveRecord_ objects you will usually only need to specify `model` and `fields`.
@@ -0,0 +1,23 @@
1
+ # JSON
2
+
3
+ The `json` component provides a mechanism for converting _Ruby_ objects and storing them as _JSON_ within your _HTML_ so that it can be later accessed by any _Javascript_ code running in your application.
4
+
5
+ The implementation is simple but is intended to avoid repetition of serialization and ensures that all _JSON_ data you output is stored consistently in one location.
6
+
7
+ _JSON_ data is available to _Javascript_ code as `ActiveElement.jsonData`. The component receives a key and an object to be serialized. The key is converted to camel case to provide conventionally-named _Javascript_ accessors.
8
+
9
+ Note that only the data key is converted to camel case. The data object itself is not transformed in any way aside from converting directly to _JSON_.
10
+
11
+ ```rspec:html
12
+ subject do
13
+ active_element.component.json :my_data, { some: { example: 'data' } }
14
+ end
15
+
16
+ it { is_expected.to include '"myData"' }
17
+ ```
18
+
19
+ The data is now available to _Javascript_ code - try running the following code in your browser's _Javascript_ console:
20
+
21
+ ```javascript
22
+ console.log(ActiveElement.jsonData.myData);
23
+ ```
@@ -0,0 +1,56 @@
1
+ # Navbar
2
+
3
+ A default _Navbar_ is provided automatically for any controllers that inherit from `ActiveElement::ApplicationController` and included in the default [Layout](../layout.html).
4
+
5
+ The _Navbar_ is generated from `#index` routes for all _ActiveElement_ controllers.
6
+
7
+ For example, if you have a `BookingsController` and `UsersController` that both inherit from `ActiveElement::Controller` (either directly or via common parent controller that inherits from `ActiveElement::ApplicationController`) and provide an `#index` action, your _Navbar_ will contain links for _Users_ and _Bookings_.
8
+
9
+ The _Navbar_ brand is calculated from the name of the application `module` as defined in `config/application.rb`.
10
+
11
+ If you wish to override the default _Navbar_ configuration, create an initializer and set your desired options:
12
+
13
+ ## Configuration
14
+
15
+ ```ruby
16
+ # config/initializers/active_element.rb
17
+
18
+ ActiveElement.application_name = 'My Bookings System'
19
+ ActiveElement.navbar_items = [
20
+ { label: 'Bookings', path: '/bookings' },
21
+ { label: 'Users', path: '/users' },
22
+ { label: 'New Booking', path: '/bookings/new' }
23
+ ]
24
+ ```
25
+
26
+ Note that overriding the default `navbar_items` means that all _Navbar_ items must now be manually configured and _ActiveElement_ will not infer any new items as new controllers/routes are added to your application.
27
+
28
+ ## Custom Usage
29
+
30
+ If you prefer to use a custom `layout` instead of the default provided by _ActiveElement_, the `navbar` component can be used like any other component. The default _Navbar_ uses `position: fixed` to keep the _Navbar_ visible when the user scrolls. This can be disabled by passing `fixed: false`.
31
+
32
+ ```rspec:html
33
+ subject do
34
+ active_element.component.navbar(fixed: false)
35
+ end
36
+
37
+ it { is_expected.to include 'Users' }
38
+ ```
39
+
40
+ The `navbar` component can also receive a custom set of options if you wish to have different menu items for certain pages.
41
+
42
+ ```rspec:html
43
+ subject do
44
+ active_element.component.navbar(fixed: false, items: [{ label: 'Custom Link', path: '/custom' }])
45
+ end
46
+
47
+ it { is_expected.to include 'Custom Link' }
48
+ ```
49
+
50
+ ## Options
51
+
52
+ | Keyword | Description |
53
+ |-|-|
54
+ | `fixed` | Enable or disable `position: fixed` on the _Navbar_ element to allow it to stay visible when the user scrolls. Defaults to `true`.
55
+ | `items` | An array of `{ label: ..., path: ...}` specifying all items that should appear in the _Navbar_.
56
+
@@ -0,0 +1,13 @@
1
+ # Page Section Title
2
+
3
+ Another very basic component, similar to [`page_title`](page-title.html) and [`page_subtitle`](page-subtitle.html).
4
+
5
+ Use this component to add a title to individual sections of your page.
6
+
7
+ ```rspec:html
8
+ subject do
9
+ active_element.component.page_section_title 'Features'
10
+ end
11
+
12
+ it { is_expected.to include 'Features' }
13
+ ```
@@ -0,0 +1,11 @@
1
+ # Page Subtitle
2
+
3
+ Like the [`page_title`](page-title.html) component, `page_subtitle` provides a simple heading tag.
4
+
5
+ ```rspec:html
6
+ subject do
7
+ active_element.component.page_subtitle 'Introduction'
8
+ end
9
+
10
+ it { is_expected.to include 'Introduction' }
11
+ ```
@@ -0,0 +1,11 @@
1
+ # Page Title
2
+
3
+ The `page_title` component provides a simple `<h2>` tag. It's very basic but it gives you a consistent way to generate page titles that can be amended later to include any classes that you want to apply to all instances of the component.
4
+
5
+ ```rspec:html
6
+ subject do
7
+ active_element.component.page_title 'Welcome to ActiveElement'
8
+ end
9
+
10
+ it { is_expected.to include 'Welcome to ActiveElement' }
11
+ ```
@@ -0,0 +1,29 @@
1
+ # Collection Table
2
+
3
+ The _Collection Table_ component provides a vertical table containing a collection of items. Use with _Active Record_ model instances, an array of objects that extend `ActiveModel::Naming`, or simple hash-like objects.
4
+
5
+ Field types are automatically inferred from their respective database columns and rendered using an appropriate formatter.
6
+
7
+ Pagination (provided by [Kaminari](https://github.com/kaminari/kaminari)) is enabled by default for larger collections.
8
+
9
+ See the [full keyword argument specification](options.html) for information on the various available options.
10
+
11
+ ```rspec:html
12
+ collection = [
13
+ User.new(name: 'John', email: 'john@example.com', enabled: true),
14
+ User.new(name: 'Jane', email: 'jane@example.org', enabled: false),
15
+ User.new(name: 'Peter', email: 'peter@example.org', enabled: false),
16
+ User.new(name: 'Sally', email: 'sally@example.org', enabled: true)
17
+ ]
18
+
19
+ subject do
20
+ active_element.component.table collection: collection,
21
+ fields: [:name, :email, :enabled],
22
+ show: true,
23
+ new: true,
24
+ edit: true,
25
+ destroy: true
26
+ end
27
+
28
+ it { is_expected.to include 'John' }
29
+ ```
@@ -0,0 +1,18 @@
1
+ # Item Table
2
+
3
+ The _Item Table_ component provides a horizontal table containing a single item and its attributes. Use with an _Active Record_ model instance, an object that extends `ActiveModel::Naming`, or a simple hash-like object.
4
+
5
+ See the [full keyword argument specification](options.html) for information on the various available options.
6
+
7
+ ```rspec:html
8
+ item = User.new(name: 'John', email: 'john@example.com', overview: 'Writes Ruby code for a living.')
9
+
10
+ subject do
11
+ active_element.component.table item: item,
12
+ fields: [:name, :email, :overview],
13
+ edit: true,
14
+ destroy: true
15
+ end
16
+
17
+ it { is_expected.to include 'John' }
18
+ ```
@@ -0,0 +1,19 @@
1
+ # Options
2
+
3
+ Tables receive the following keyword arguments:
4
+
5
+ | Keyword | Description |
6
+ |-|-|
7
+ | `collection` | A collection of objects. Must be iterable, e.g. the result of calling `where` on an _ActiveRecord_ model. Renders a [Collection Table](tables/collection-table.html)
8
+ | `item` | A single object. Renders an [Item Table](tables/item-table.html). Note that you must use either `collection` or `item` but not both.
9
+ | `fields` | _(Required)_ An array specifying the fields to display in the table. The keys specified in this array can be either hash keys or _ActiveRecord_ attribute names.
10
+ | `model_name` | Pass `model_name` if you are not using _ActiveRecord_ objects, e.g. if you are passing a hash or array of hashes, `model_name` should be provided to provide decorators, translations, CSS classes, etc. This argument should be omitted for _ActiveRecord_ objects (or any object that implements `ActiveModel::Naming`. |
11
+ | `class_name` | Provide an explicit class name to override the default derived from the received _ActiveRecord_ model class or the `model_name` parameter.
12
+ | `show` | A _boolean_ value specifying whether to display a _Show_ button in a `collection` table. The path for the link is derived from the received _ActiveRecord_ object, e.g. `user_path(record)`
13
+ | `destroy` | A _boolean_ value specifying whether to display a _Delete_ button for each record in a `collection` table or for the single record in an `item` table. Links to e.g. `user_path(record, method: :delete)`
14
+ | `edit` | A _boolean_ value specifying whether to display an _Edit_ button. Links to e.g. `edit_user_path(record)`
15
+ | `new` | A _boolean_ value specifying whether to display a "Create new ..." button above a `collection` table. Links to e.g. `new_user_path`
16
+ | `style` | Specify a _CSS_ string to be inserted into the `<table>` tag.
17
+ | `row_class` | Specify a class to be inserted into each `<tr>` tag in a `collection` table. Can be a `String` or a `Proc`. A `Proc` will receive the record as an argument, e.g.: `row_class: ->(record) { record.deleted_at.present? 'text-danger' : 'text-primary' }`
18
+ | `group` | Group rows in a `collection` by a given attribute, e.g.: `group: :country_code`.
19
+ | `paginate` | Enable or disable pagination for `collection` tables. Defaults to `true`.
@@ -0,0 +1,29 @@
1
+ # Tables
2
+
3
+ An interface for creating tables is provided by `active_element.component.table`, available in all views.
4
+
5
+ The two table types provided are [Collection Table](tables/collection-table.html) and [Item Table](tables/item-table.html).
6
+
7
+ See the [full keyword argument specification](tables/options.html) for information on the various available options.
8
+
9
+ ## Basic Example
10
+
11
+ ```rspec:html
12
+ collection = [
13
+ User.new(name: 'John', email: 'john@example.com', enabled: true),
14
+ User.new(name: 'Jane', email: 'jane@example.org', enabled: false),
15
+ User.new(name: 'Peter', email: 'peter@example.org', enabled: false),
16
+ User.new(name: 'Sally', email: 'sally@example.org', enabled: true)
17
+ ]
18
+
19
+ subject do
20
+ active_element.component.table collection: collection,
21
+ fields: [:name, :email, :enabled],
22
+ show: true,
23
+ new: true,
24
+ edit: true,
25
+ destroy: true
26
+ end
27
+
28
+ it { is_expected.to include 'John' }
29
+ ```
@@ -0,0 +1,15 @@
1
+ # Components
2
+
3
+ The core of _ActiveElement_ is its set of components. These components are intended to be used within _Rails_ views to reduce the amount of _HTML_ developers need to write, as well as standardizing the look and feel of your application. A typical admin application should need very little (if any) custom _HTML_. If custom _HTML_ is needed, the composable nature of the provided components allows for this.
4
+
5
+ See the individual documentation sections for each component, and visit the [Decorators](decorators.html) section to see how you can write composable partials to override the default data formatters as needed.
6
+
7
+ All examples demonstrate how to use a component in a _Rails_ view. Every view rendered by a controller that inherits from `ActiveElement::ApplicationController` will have the `active_element` method available to them.
8
+
9
+ To use a component in your view, call (e.g.) `active_element.component.form` to render a [Form](components/form.html) component:
10
+
11
+ ```erb
12
+ <%# app/views/users/new.html.erb %>
13
+
14
+ <%= active_element.component.form model: User.new, fields: [:email, :name] %>
15
+ ```
@@ -0,0 +1,20 @@
1
+ # Authentication
2
+
3
+ Authentication is enabled by calling `active_element.authenticate_with` from a controller, typically your `ApplicationController`, or some base controller used for a restricted namespace. `active_element.authenticate_with` receives a block which calls your chosen authentication framework's user authentication.
4
+
5
+
6
+ The recommended way to handle authentication with _ActiveElement_ is to use `prepend_before_action` which calls the authenticator:
7
+
8
+ ```ruby
9
+ class ApplicationController < ActionController::Base
10
+ prepend_before_action :configure_authentication
11
+
12
+ private
13
+
14
+ def configure_authentication
15
+ active_element.authenticate_with { authenticate_user! }
16
+ end
17
+ end
18
+ ```
19
+
20
+ Passing the authenticator to _ActiveElement_ is optional and you are free to handle your own authentication, but allowing _ActiveElement_ to call the authenticator is required if you wish to utilize the [authorization model](access-control/authorization.html).
@@ -0,0 +1,9 @@
1
+ # Environments
2
+
3
+ The documentation in this section covers behaviours as they are defined for deployment environments. Note that in a `development` environment all permissions checks are bypassed, with clear logging describing all permissions that would be expected in a deployment environment.
4
+
5
+ Since a developer is running an application locally and they are able to modify the application's code, applying permissions in this environment would not add any level of security. Instead, developers receive a clear indication of what permissions they must define in a deployment environment while not being hampered in their work by the authorization model.
6
+
7
+ To ensure that an application's permissions requirements are taken into account during development, permissions **are** required in a `test` environment. This allows developers to ensure that their application is capable of defining the permissions needed to access it and to verify access control correctness with fixture data.
8
+
9
+ To assist developers when writing tests, permissions failures are logged to `stderr` the `test` environment, as well as to the application's default log file. i.e. permissions failures will appear in-line with output from the application's test suite and are less likely to go unnoticed.
@@ -0,0 +1,41 @@
1
+ # Custom Routes
2
+
3
+ If you have any routes defined that do not fall under the routes automatically defined by _Rails_ with the `resources` routes helper (i.e. anything other than `index`, `show`, `new`, `create`, `edit`, `update`, `destroy`) you **must** have an explicit permission configuration defined. This design choice is intended to reduce the risk of accidental exposure of sensitive data by defaulting to having all routes protected. _ActiveElement_ will raise `ActiveEleement::UnprotectedRouteError` if an unprotected route is defined.
4
+
5
+ _ActiveElement_ provides `active_element.permit_action`, available in all controllers that inherit from `ActiveElement::ApplicationController`.
6
+
7
+ Use this helper to define a permission that a user must have in order to access a route or, if the route is intended to be available for any signed-in user, specify that it is always permitted.
8
+
9
+ ```ruby
10
+ # app/controllers/application_controller.rb
11
+
12
+ class ApplicationController < ActiveElement::ApplicationController
13
+ prepend_before_action :configure_authentication
14
+
15
+ private
16
+
17
+ def configure_authentication
18
+ active_element.authenticate_with { authenticate_user! }
19
+ active_element.authorize_with { current_user }
20
+ end
21
+ end
22
+ ```
23
+
24
+ ```ruby
25
+ # app/controllers/bookings_controller.rb
26
+
27
+ class BookingsController < ApplicationController
28
+ active_element.permit_action :export_csv, with: 'can_export_bookings_csv'
29
+ active_element.permit_action :remaining_tickets, always: true
30
+
31
+ def export_csv
32
+ # ...
33
+ end
34
+
35
+ def remaining_tickets
36
+ # ...
37
+ end
38
+ end
39
+ ```
40
+
41
+ In this example, `#export_csv` is only accessible to users with the `can_export_bookings_csv` permission, and `#remaining_tickets` is always available to any who has passed initial authentication.
@@ -0,0 +1,58 @@
1
+ # Permissions
2
+
3
+ In the [Setup](setup.html) example, `current_user` (typically returning an instance of an _ActiveRecord_ `User` model) must implement a method `#permissions` which returns an array of strings. You are free to implement this however you like, whether you store your permissions as a _JSON_ array in your database, in a join table, or via an external _API_ call.
4
+
5
+ _ActiveElement_ requires various inferred permissions to be present in order for a user to access a _Rails_ route. If a user does not have permissions, a splash screen appears informing the user what permissions they need to access the resource.
6
+
7
+ Permission names are based on the following templates:
8
+
9
+ |Permission Template|Relevant Controller Actions|
10
+ |`can_list_<application>_<namespace>_<controller>`|`#index`
11
+ |`can_view_<application>_<namespace>_<controller>`|`#show`
12
+ |`can_edit_<application>_<namespace>_<controller>`|`#edit`, `#update`
13
+ |`can_create_<application>_<namespace>_<controller>`|`#new`, `#create`
14
+ |`can_delete_<application>_<namespace>_<controller>`|`#destroy`
15
+
16
+ e.g. if your application is defined as:
17
+
18
+ ```ruby
19
+ # config/application.rb
20
+
21
+ module BookingSystem
22
+ class Application < Rails::Application
23
+ end
24
+ end
25
+ ```
26
+
27
+ and you have your controllers namespaced under `admin`:
28
+ ```ruby
29
+ # config/routes.rb
30
+
31
+ namespace :admin do
32
+ resources :bookings
33
+ end
34
+ ```
35
+
36
+ then the following permissions will be applied:
37
+
38
+ * `can_list_booking_system_admin_bookings`
39
+ * `can_view_booking_system_admin_bookings`
40
+ * `can_edit_booking_system_admin_bookings`
41
+ * `can_create_booking_system_admin_bookings`
42
+ * `can_delete_booking_system_admin_bookings`
43
+
44
+ Note that permissions are not mapped 1:1 to controller actions. This design choice is intended to reduce the risk of e.g. revoking a permission only for the `#edit` action and being misled into thinking that this would prevent a user from submitting a request directly to the `#update` action. `#index` and `#show` are implemented as separate permissions as it is expected that one view may provide more detailed information than another.
45
+
46
+ ## Listing Permissions
47
+
48
+ All permissions can be viewed at any time by running the provided _Rake_ task:
49
+
50
+ ```console
51
+ $ rake active_element:permissions
52
+
53
+ * can_list_booking_system_admin_bookings
54
+ * can_view_booking_system_admin_bookings
55
+ * can_edit_booking_system_admin_bookings
56
+ * can_create_booking_system_admin_bookings
57
+ * can_delete_booking_system_admin_bookings
58
+ ```
@@ -0,0 +1,27 @@
1
+ ## Setup
2
+
3
+ Enable authorization in your application by calling `active_element.authenticate_with` and `active_element.authorize_with` from a `prepend_before_action`. For example, if you are using [Devise](https://github.com/heartcombo/devise):
4
+
5
+ ```ruby
6
+ # app/controllers/application_controller.rb
7
+
8
+ class ApplicationController < ActionController::Base
9
+ prepend_before_action :configure_authentication
10
+
11
+ private
12
+
13
+ def configure_authentication
14
+ active_element.authenticate_with { authenticate_user! }
15
+ active_element.authorize_with { current_user }
16
+ end
17
+ end
18
+ ```
19
+
20
+ Adjust the provided example to suit your application's authentication framework.
21
+
22
+ As long as the following conditions are met then your application is ready to use _ActiveElement's_ authorization system:
23
+
24
+ * The method in the block sent to `active_element.authenticate_with` renders or redirects on authentication failure.
25
+ * The method in the block sent to `active_element.authorize_with` returns an object that implements a `#permissions` method which returns an array of strings.
26
+
27
+
@@ -0,0 +1,11 @@
1
+ # Authorization
2
+
3
+ _ActiveElement_ provides a comprehensive and robust authorization model to limit access to specific areas of your application to specific users. Any custom authorization not provided by _ActiveElement_ can be implemented using whatever setup you prefer, either by writing your own `before_action` methods that restrict access, e.g. by calling `head :unauthorized`, or by using a more advanced authorization framework like [Pundit](https://github.com/varvet/pundit) or [CanCanCan](https://github.com/CanCanCommunity/cancancan).
4
+
5
+ _ActiveElement_ does not attempt to replace the functionality of such frameworks and does not prevent their usage. Instead, the focus is on providing default permissions for accessing specific controller actions. Access control of particular data resources is left to the application and there are numerous high quality frameworks that provide this functionality.
6
+
7
+ If any controller inherits from `ActiveElement::ApplicationController` then it will automatically have permissions applied to all of its controller actions and, if authorization is enabled, users must have the required permissions to access. This provides an automated [least-privelege model](https://en.wikipedia.org/wiki/Principle_of_least_privilege) to all of your application's endpoints with minimal effort.
8
+
9
+ Note that actions that exist out of the standard set of _Rails_ _RESTful_ resources (i.e. `index`, `show`, `new`, `create`, `edit`, `update`, `destroy`) **must** have an explicit permission configuration defined using `active_element.permit_action`. See [Custom Routes](authorization/permissions/custom-routes.html) for more information.
10
+
11
+ See the [Setup](authorization/setup.html) and [Permissions](authorization/permissions.html) sections to get started.