brut 0.0.29 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (491) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/README.md +23 -2
  4. data/assets/LogoStop.pxd +0 -0
  5. data/assets/MetroLogo.graffle +0 -0
  6. data/assets/SocialImage.png +0 -0
  7. data/assets/SocialImage.pxd +0 -0
  8. data/brutrb.com/.vitepress/config.mjs +45 -8
  9. data/brutrb.com/.vitepress/theme/style.css +6 -5
  10. data/brutrb.com/ai.md +10 -15
  11. data/brutrb.com/assets.md +2 -9
  12. data/brutrb.com/brut-js.md +12 -2
  13. data/brutrb.com/cli.md +9 -13
  14. data/brutrb.com/components.md +118 -96
  15. data/brutrb.com/configuration.md +3 -4
  16. data/brutrb.com/css.md +2 -2
  17. data/brutrb.com/custom-element-tests.md +3 -4
  18. data/brutrb.com/database-access.md +1 -1
  19. data/brutrb.com/database-schema.md +29 -41
  20. data/brutrb.com/dev-environment.md +7 -7
  21. data/brutrb.com/dir-structure.md +120 -0
  22. data/brutrb.com/doc-conventions.md +18 -15
  23. data/brutrb.com/dx +1 -0
  24. data/brutrb.com/end-to-end-tests.md +12 -10
  25. data/brutrb.com/features.md +373 -0
  26. data/brutrb.com/flash-and-session.md +115 -131
  27. data/brutrb.com/form-constraints.md +266 -0
  28. data/brutrb.com/forms.md +140 -765
  29. data/brutrb.com/getting-started.md +10 -11
  30. data/brutrb.com/handlers.md +119 -95
  31. data/brutrb.com/hooks.md +18 -20
  32. data/brutrb.com/i18n.md +6 -4
  33. data/brutrb.com/images/LogoStop.png +0 -0
  34. data/brutrb.com/instrumentation.md +7 -10
  35. data/brutrb.com/javascript.md +14 -14
  36. data/brutrb.com/keyword-injection.md +72 -114
  37. data/brutrb.com/layouts.md +20 -52
  38. data/brutrb.com/lsp.md +1 -1
  39. data/brutrb.com/overview.md +30 -372
  40. data/brutrb.com/pages.md +119 -207
  41. data/brutrb.com/public/SocialImage.png +0 -0
  42. data/brutrb.com/public/favicon.ico +0 -0
  43. data/brutrb.com/recipes/alternate-layouts.md +32 -0
  44. data/brutrb.com/recipes/authentication.md +315 -6
  45. data/brutrb.com/recipes/blank-layouts.md +22 -0
  46. data/brutrb.com/recipes/custom-flash.md +51 -0
  47. data/brutrb.com/recipes/indexed-forms.md +149 -0
  48. data/brutrb.com/recipes/text-field-component.md +182 -0
  49. data/brutrb.com/routes.md +56 -82
  50. data/brutrb.com/security.md +0 -3
  51. data/brutrb.com/space-time-continuum.md +8 -12
  52. data/brutrb.com/tutorial.md +1 -1
  53. data/brutrb.com/why.md +19 -0
  54. data/docs/404.html +8 -3
  55. data/docs/SocialImage.png +0 -0
  56. data/docs/ai.html +11 -6
  57. data/docs/api/Brut/BackEnd/SeedData.html +1 -1
  58. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +1 -1
  59. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +1 -1
  60. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +1 -1
  61. data/docs/api/Brut/BackEnd/Sidekiq.html +1 -1
  62. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +1 -1
  63. data/docs/api/Brut/BackEnd/Validators.html +1 -1
  64. data/docs/api/Brut/BackEnd.html +1 -1
  65. data/docs/api/Brut/CLI/App.html +1 -1
  66. data/docs/api/Brut/CLI/AppRunner.html +1 -1
  67. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +1 -1
  68. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +1 -1
  69. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +1 -1
  70. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +1 -1
  71. data/docs/api/Brut/CLI/Apps/BuildAssets.html +1 -1
  72. data/docs/api/Brut/CLI/Apps/DB/Create.html +1 -1
  73. data/docs/api/Brut/CLI/Apps/DB/Drop.html +1 -1
  74. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +1 -1
  75. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +1 -1
  76. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +1 -1
  77. data/docs/api/Brut/CLI/Apps/DB/Seed.html +1 -1
  78. data/docs/api/Brut/CLI/Apps/DB/Status.html +1 -1
  79. data/docs/api/Brut/CLI/Apps/DB.html +1 -1
  80. data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +1 -1
  81. data/docs/api/Brut/CLI/Apps/DeployBase.html +1 -1
  82. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +1 -1
  83. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +1 -1
  84. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +1 -1
  85. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +1 -1
  86. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +1 -1
  87. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +1 -1
  88. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +1 -1
  89. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +1 -1
  90. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +1 -1
  91. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +1 -1
  92. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +1 -1
  93. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +1 -1
  94. data/docs/api/Brut/CLI/Apps/Scaffold.html +1 -1
  95. data/docs/api/Brut/CLI/Apps/Test/Audit.html +1 -1
  96. data/docs/api/Brut/CLI/Apps/Test/E2e.html +1 -1
  97. data/docs/api/Brut/CLI/Apps/Test/JS.html +1 -1
  98. data/docs/api/Brut/CLI/Apps/Test/Run.html +1 -1
  99. data/docs/api/Brut/CLI/Apps/Test.html +1 -1
  100. data/docs/api/Brut/CLI/Apps.html +1 -1
  101. data/docs/api/Brut/CLI/Command.html +1 -1
  102. data/docs/api/Brut/CLI/Error.html +1 -1
  103. data/docs/api/Brut/CLI/ExecutionResults/Result.html +1 -1
  104. data/docs/api/Brut/CLI/ExecutionResults.html +1 -1
  105. data/docs/api/Brut/CLI/Executor.html +1 -1
  106. data/docs/api/Brut/CLI/InvalidOption.html +1 -1
  107. data/docs/api/Brut/CLI/Options.html +1 -1
  108. data/docs/api/Brut/CLI/Output.html +1 -1
  109. data/docs/api/Brut/CLI/SystemExecError.html +1 -1
  110. data/docs/api/Brut/CLI.html +1 -1
  111. data/docs/api/Brut/FactoryBot.html +1 -1
  112. data/docs/api/Brut/Framework/App.html +1 -1
  113. data/docs/api/Brut/Framework/Config.html +1 -1
  114. data/docs/api/Brut/Framework/Container.html +1 -1
  115. data/docs/api/Brut/Framework/Error.html +1 -1
  116. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +1 -1
  117. data/docs/api/Brut/Framework/Errors/Bug.html +1 -1
  118. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +1 -1
  119. data/docs/api/Brut/Framework/Errors/MissingParameter.html +1 -1
  120. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +1 -1
  121. data/docs/api/Brut/Framework/Errors/NotFound.html +1 -1
  122. data/docs/api/Brut/Framework/Errors/NotImplemented.html +1 -1
  123. data/docs/api/Brut/Framework/Errors.html +1 -1
  124. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +1 -1
  125. data/docs/api/Brut/Framework/MCP.html +1 -1
  126. data/docs/api/Brut/Framework/ProjectEnvironment.html +1 -1
  127. data/docs/api/Brut/Framework.html +1 -1
  128. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +1 -1
  129. data/docs/api/Brut/FrontEnd/Component/Helpers.html +1 -1
  130. data/docs/api/Brut/FrontEnd/Component.html +1 -1
  131. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +1 -1
  132. data/docs/api/Brut/FrontEnd/Components/FormTag.html +1 -1
  133. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +1 -1
  134. data/docs/api/Brut/FrontEnd/Components/Input.html +1 -1
  135. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +1 -1
  136. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +1 -1
  137. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +1 -1
  138. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +1 -1
  139. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +1 -1
  140. data/docs/api/Brut/FrontEnd/Components/Inputs.html +1 -1
  141. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +1 -1
  142. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +1 -1
  143. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +1 -1
  144. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +1 -1
  145. data/docs/api/Brut/FrontEnd/Components.html +1 -1
  146. data/docs/api/Brut/FrontEnd/Download.html +1 -1
  147. data/docs/api/Brut/FrontEnd/Flash.html +1 -1
  148. data/docs/api/Brut/FrontEnd/Form.html +9 -11
  149. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +1 -1
  150. data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +1 -1
  151. data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +1 -1
  152. data/docs/api/Brut/FrontEnd/Forms/Input.html +1 -1
  153. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +1 -1
  154. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +1 -1
  155. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +135 -20
  156. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +1 -1
  157. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +135 -20
  158. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +1 -1
  159. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +1 -1
  160. data/docs/api/Brut/FrontEnd/Forms.html +1 -1
  161. data/docs/api/Brut/FrontEnd/GenericResponse.html +1 -1
  162. data/docs/api/Brut/FrontEnd/Handler.html +1 -1
  163. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +1 -1
  164. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +1 -1
  165. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +1 -1
  166. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +1 -1
  167. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +1 -1
  168. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +1 -1
  169. data/docs/api/Brut/FrontEnd/Handlers.html +1 -1
  170. data/docs/api/Brut/FrontEnd/HandlingResults.html +1 -1
  171. data/docs/api/Brut/FrontEnd/HttpMethod.html +1 -1
  172. data/docs/api/Brut/FrontEnd/HttpStatus.html +1 -1
  173. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +1 -1
  174. data/docs/api/Brut/FrontEnd/Layout.html +1 -1
  175. data/docs/api/Brut/FrontEnd/Middleware.html +1 -1
  176. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +1 -1
  177. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +1 -1
  178. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +1 -1
  179. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +1 -1
  180. data/docs/api/Brut/FrontEnd/Middlewares.html +1 -1
  181. data/docs/api/Brut/FrontEnd/Page.html +1 -1
  182. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +1 -1
  183. data/docs/api/Brut/FrontEnd/Pages.html +1 -1
  184. data/docs/api/Brut/FrontEnd/RequestContext.html +1 -1
  185. data/docs/api/Brut/FrontEnd/RouteHook.html +1 -1
  186. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +1 -1
  187. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +1 -1
  188. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +1 -1
  189. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +1 -1
  190. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +1 -1
  191. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +1 -1
  192. data/docs/api/Brut/FrontEnd/RouteHooks.html +1 -1
  193. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +1 -1
  194. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +1 -1
  195. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +1 -1
  196. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +1 -1
  197. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +1 -1
  198. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +1 -1
  199. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +1 -1
  200. data/docs/api/Brut/FrontEnd/Routing/Route.html +1 -1
  201. data/docs/api/Brut/FrontEnd/Routing.html +1 -1
  202. data/docs/api/Brut/FrontEnd/Session.html +1 -1
  203. data/docs/api/Brut/FrontEnd.html +1 -1
  204. data/docs/api/Brut/I18n/BaseMethods.html +1 -1
  205. data/docs/api/Brut/I18n/ForBackEnd.html +1 -1
  206. data/docs/api/Brut/I18n/ForCLI.html +1 -1
  207. data/docs/api/Brut/I18n/ForHTML.html +1 -1
  208. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +1 -1
  209. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +1 -1
  210. data/docs/api/Brut/I18n.html +1 -1
  211. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +1 -1
  212. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +1 -1
  213. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +1 -1
  214. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +1 -1
  215. data/docs/api/Brut/Instrumentation.html +1 -1
  216. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +1 -1
  217. data/docs/api/Brut/SinatraHelpers.html +1 -1
  218. data/docs/api/Brut/SpecSupport/ClockSupport.html +1 -1
  219. data/docs/api/Brut/SpecSupport/ComponentSupport.html +1 -1
  220. data/docs/api/Brut/SpecSupport/E2ETestServer.html +1 -1
  221. data/docs/api/Brut/SpecSupport/E2eSupport.html +1 -1
  222. data/docs/api/Brut/SpecSupport/EnhancedNode.html +1 -1
  223. data/docs/api/Brut/SpecSupport/FlashSupport.html +1 -1
  224. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +1 -1
  225. data/docs/api/Brut/SpecSupport/GeneralSupport.html +1 -1
  226. data/docs/api/Brut/SpecSupport/HandlerSupport.html +1 -1
  227. data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +1 -1
  228. data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +1 -1
  229. data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +1 -1
  230. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +1 -1
  231. data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +1 -1
  232. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +1 -1
  233. data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +1 -1
  234. data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +1 -1
  235. data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +1 -1
  236. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +1 -1
  237. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +1 -1
  238. data/docs/api/Brut/SpecSupport/Matchers.html +1 -1
  239. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +1 -1
  240. data/docs/api/Brut/SpecSupport/RSpecSetup.html +1 -1
  241. data/docs/api/Brut/SpecSupport/SessionSupport.html +1 -1
  242. data/docs/api/Brut/SpecSupport.html +1 -1
  243. data/docs/api/Brut.html +1 -1
  244. data/docs/api/Clock.html +1 -1
  245. data/docs/api/RichString.html +1 -1
  246. data/docs/api/SemanticLogger/Appender/Async.html +1 -1
  247. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +1 -1
  248. data/docs/api/Sequel/Extensions/BrutMigrations.html +1 -1
  249. data/docs/api/Sequel/Extensions.html +1 -1
  250. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +1 -1
  251. data/docs/api/Sequel/Plugins/CreatedAt.html +1 -1
  252. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +1 -1
  253. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +1 -1
  254. data/docs/api/Sequel/Plugins/ExternalId.html +1 -1
  255. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +1 -1
  256. data/docs/api/Sequel/Plugins/FindBang.html +1 -1
  257. data/docs/api/Sequel/Plugins.html +1 -1
  258. data/docs/api/Sequel.html +1 -1
  259. data/docs/api/_index.html +1 -1
  260. data/docs/api/file.README.html +22 -3
  261. data/docs/api/index.html +22 -3
  262. data/docs/api/method_list.html +16 -0
  263. data/docs/api/top-level-namespace.html +1 -1
  264. data/docs/assets/LogoStop.Gb3tDhL1.png +0 -0
  265. data/docs/assets/{ai.md._6HCDL6d.js → ai.md.Cy9GWnER.js} +1 -1
  266. data/docs/assets/ai.md.Cy9GWnER.lean.js +1 -0
  267. data/docs/assets/{app.BhrfSt68.js → app.ClaS47Ru.js} +1 -1
  268. data/docs/assets/{assets.md.D3wunzLx.js → assets.md.7C3HWkga.js} +3 -3
  269. data/docs/assets/{assets.md.D3wunzLx.lean.js → assets.md.7C3HWkga.lean.js} +1 -1
  270. data/docs/assets/{brut-js.md.o2DAO2s2.js → brut-js.md.B4GYxQVw.js} +1 -1
  271. data/docs/assets/{brut-js.md.o2DAO2s2.lean.js → brut-js.md.B4GYxQVw.lean.js} +1 -1
  272. data/docs/assets/chunks/@localSearchIndexroot.Biqy1A4t.js +1 -0
  273. data/docs/assets/chunks/{VPLocalSearchBox.Dpot_2H4.js → VPLocalSearchBox.DtgDfde2.js} +1 -1
  274. data/docs/assets/chunks/{theme.N2SNVLgU.js → theme.B45bvibT.js} +2 -2
  275. data/docs/assets/{cli.md.RmeA2b0i.js → cli.md.CjsktgFz.js} +15 -20
  276. data/docs/assets/components.md.DatoNgFo.js +96 -0
  277. data/docs/assets/{components.md.CRUMdRoN.lean.js → components.md.DatoNgFo.lean.js} +1 -1
  278. data/docs/assets/{configuration.md.LG-zIBww.js → configuration.md.DeyhpqEx.js} +3 -3
  279. data/docs/assets/{css.md.DJgj2clw.js → css.md.CltvJqAa.js} +3 -3
  280. data/docs/assets/{custom-element-tests.md.BrYJQEl3.js → custom-element-tests.md.B_rbta32.js} +3 -3
  281. data/docs/assets/{database-access.md.C7l-Vuvb.js → database-access.md.gnluu54N.js} +1 -1
  282. data/docs/assets/{database-schema.md.BUjR0VS1.js → database-schema.md.CSYk6E6v.js} +6 -6
  283. data/docs/assets/{database-schema.md.BUjR0VS1.lean.js → database-schema.md.CSYk6E6v.lean.js} +1 -1
  284. data/docs/assets/dev-environment.md.BroAOLhF.js +11 -0
  285. data/docs/assets/dir-structure.md.CWir1pic.js +46 -0
  286. data/docs/assets/dir-structure.md.CWir1pic.lean.js +1 -0
  287. data/docs/assets/doc-conventions.md.BzmSrTEW.js +1 -0
  288. data/docs/assets/doc-conventions.md.BzmSrTEW.lean.js +1 -0
  289. data/docs/assets/{end-to-end-tests.md.yfQHC0b5.js → end-to-end-tests.md.DzqRpZ43.js} +5 -3
  290. data/docs/assets/end-to-end-tests.md.DzqRpZ43.lean.js +1 -0
  291. data/docs/assets/features.md.DPFXsy0z.js +154 -0
  292. data/docs/assets/features.md.DPFXsy0z.lean.js +1 -0
  293. data/docs/assets/flash-and-session.md.nPvUpnUx.js +79 -0
  294. data/docs/assets/{flash-and-session.md.BXY8RvT0.lean.js → flash-and-session.md.nPvUpnUx.lean.js} +1 -1
  295. data/docs/assets/form-constraints.md.x5tNpTTI.js +90 -0
  296. data/docs/assets/form-constraints.md.x5tNpTTI.lean.js +1 -0
  297. data/docs/assets/forms.md.C2Dizvzq.js +64 -0
  298. data/docs/assets/forms.md.C2Dizvzq.lean.js +1 -0
  299. data/docs/assets/{getting-started.md.Dj0qtZI2.js → getting-started.md.C93e0odB.js} +5 -5
  300. data/docs/assets/{getting-started.md.Dj0qtZI2.lean.js → getting-started.md.C93e0odB.lean.js} +1 -1
  301. data/docs/assets/handlers.md.Chyri6KA.js +54 -0
  302. data/docs/assets/handlers.md.Chyri6KA.lean.js +1 -0
  303. data/docs/assets/{hooks.md.C4-moMny.js → hooks.md.Jmb5VOLA.js} +4 -4
  304. data/docs/assets/{hooks.md.C4-moMny.lean.js → hooks.md.Jmb5VOLA.lean.js} +1 -1
  305. data/docs/assets/{i18n.md.Do9i1qWl.js → i18n.md.xQhiGo1G.js} +2 -2
  306. data/docs/assets/{i18n.md.Do9i1qWl.lean.js → i18n.md.xQhiGo1G.lean.js} +1 -1
  307. data/docs/assets/{index.md.CuBB-BdM.js → index.md.CAMqGBJE.js} +1 -1
  308. data/docs/assets/{index.md.CuBB-BdM.lean.js → index.md.CAMqGBJE.lean.js} +1 -1
  309. data/docs/assets/{instrumentation.md.a9Pjps4P.js → instrumentation.md.BgcaGVYH.js} +2 -2
  310. data/docs/assets/{instrumentation.md.a9Pjps4P.lean.js → instrumentation.md.BgcaGVYH.lean.js} +1 -1
  311. data/docs/assets/{javascript.md.GWbhRS51.js → javascript.md.DzrMxUmI.js} +7 -7
  312. data/docs/assets/{javascript.md.GWbhRS51.lean.js → javascript.md.DzrMxUmI.lean.js} +1 -1
  313. data/docs/assets/keyword-injection.md.95Zgh2eN.js +21 -0
  314. data/docs/assets/{keyword-injection.md.Dt2tKREs.lean.js → keyword-injection.md.95Zgh2eN.lean.js} +1 -1
  315. data/docs/assets/{layouts.md.cPnh3NId.js → layouts.md.CJGDFY-m.js} +2 -15
  316. data/docs/assets/layouts.md.CJGDFY-m.lean.js +1 -0
  317. data/docs/assets/{lsp.md.Bsu-f6VU.js → lsp.md.Dn1rIiW0.js} +1 -1
  318. data/docs/assets/{lsp.md.Bsu-f6VU.lean.js → lsp.md.Dn1rIiW0.lean.js} +1 -1
  319. data/docs/assets/overview.md.Bdq4qt3L.js +1 -0
  320. data/docs/assets/overview.md.Bdq4qt3L.lean.js +1 -0
  321. data/docs/assets/pages.md.B7Hc-i6H.js +45 -0
  322. data/docs/assets/pages.md.B7Hc-i6H.lean.js +1 -0
  323. data/docs/assets/recipes_alternate-layouts.md.BwEytl59.js +22 -0
  324. data/docs/assets/recipes_alternate-layouts.md.BwEytl59.lean.js +1 -0
  325. data/docs/assets/recipes_authentication.md.Dzvi_g69.js +156 -0
  326. data/docs/assets/recipes_authentication.md.Dzvi_g69.lean.js +1 -0
  327. data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.js +15 -0
  328. data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.lean.js +1 -0
  329. data/docs/assets/recipes_custom-flash.md.CrQbI5eH.js +26 -0
  330. data/docs/assets/recipes_custom-flash.md.CrQbI5eH.lean.js +1 -0
  331. data/docs/assets/recipes_indexed-forms.md.CstYyOSo.js +74 -0
  332. data/docs/assets/recipes_indexed-forms.md.CstYyOSo.lean.js +1 -0
  333. data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.js +101 -0
  334. data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.lean.js +1 -0
  335. data/docs/assets/routes.md.B8kfUPHU.js +21 -0
  336. data/docs/assets/{routes.md.BMM7peut.lean.js → routes.md.B8kfUPHU.lean.js} +1 -1
  337. data/docs/assets/{security.md.C668yXCi.js → security.md.C0G_AZR-.js} +1 -1
  338. data/docs/assets/{security.md.C668yXCi.lean.js → security.md.C0G_AZR-.lean.js} +1 -1
  339. data/docs/assets/space-time-continuum.md.xl44xDos.js +1 -0
  340. data/docs/assets/{space-time-continuum.md.KPUIKysQ.lean.js → space-time-continuum.md.xl44xDos.lean.js} +1 -1
  341. data/docs/assets/{style.B2o1L9eN.css → style.prAgp4yQ.css} +1 -1
  342. data/docs/assets/tutorial.md.a4a0eVOy.js +1 -0
  343. data/docs/assets/tutorial.md.a4a0eVOy.lean.js +1 -0
  344. data/docs/assets/why.md.C-hk5xgJ.js +1 -0
  345. data/docs/assets/why.md.C-hk5xgJ.lean.js +1 -0
  346. data/docs/assets.html +12 -7
  347. data/docs/brut-js/api/AjaxSubmit.html +1 -1
  348. data/docs/brut-js/api/AjaxSubmit.js.html +1 -1
  349. data/docs/brut-js/api/Autosubmit.html +1 -1
  350. data/docs/brut-js/api/Autosubmit.js.html +1 -1
  351. data/docs/brut-js/api/BaseCustomElement.html +1 -1
  352. data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
  353. data/docs/brut-js/api/BrutCustomElements.html +1 -1
  354. data/docs/brut-js/api/BufferedLogger.html +1 -1
  355. data/docs/brut-js/api/ConfirmSubmit.html +1 -1
  356. data/docs/brut-js/api/ConfirmSubmit.js.html +1 -1
  357. data/docs/brut-js/api/ConfirmationDialog.html +1 -1
  358. data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
  359. data/docs/brut-js/api/ConstraintViolationMessage.html +1 -1
  360. data/docs/brut-js/api/ConstraintViolationMessage.js.html +1 -1
  361. data/docs/brut-js/api/ConstraintViolationMessages.html +1 -1
  362. data/docs/brut-js/api/ConstraintViolationMessages.js.html +1 -1
  363. data/docs/brut-js/api/CopyToClipboard.html +1 -1
  364. data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
  365. data/docs/brut-js/api/Form.html +1 -1
  366. data/docs/brut-js/api/Form.js.html +1 -1
  367. data/docs/brut-js/api/I18nTranslation.html +1 -1
  368. data/docs/brut-js/api/I18nTranslation.js.html +1 -1
  369. data/docs/brut-js/api/LocaleDetection.html +1 -1
  370. data/docs/brut-js/api/LocaleDetection.js.html +1 -1
  371. data/docs/brut-js/api/Logger.html +1 -1
  372. data/docs/brut-js/api/Logger.js.html +1 -1
  373. data/docs/brut-js/api/Message.html +1 -1
  374. data/docs/brut-js/api/Message.js.html +1 -1
  375. data/docs/brut-js/api/PrefixedLogger.html +1 -1
  376. data/docs/brut-js/api/RichString.html +1 -1
  377. data/docs/brut-js/api/RichString.js.html +1 -1
  378. data/docs/brut-js/api/Tabs.html +1 -1
  379. data/docs/brut-js/api/Tabs.js.html +1 -1
  380. data/docs/brut-js/api/Tracing.html +1 -1
  381. data/docs/brut-js/api/Tracing.js.html +1 -1
  382. data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
  383. data/docs/brut-js/api/external-Performance.html +1 -1
  384. data/docs/brut-js/api/external-Promise.html +1 -1
  385. data/docs/brut-js/api/external-ValidityState.html +1 -1
  386. data/docs/brut-js/api/external-Window.html +1 -1
  387. data/docs/brut-js/api/external-fetch.html +1 -1
  388. data/docs/brut-js/api/global.html +1 -1
  389. data/docs/brut-js/api/index.html +1 -1
  390. data/docs/brut-js/api/index.js.html +1 -1
  391. data/docs/brut-js/api/module-testing.html +1 -1
  392. data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
  393. data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
  394. data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
  395. data/docs/brut-js/api/testing.DOMCreator.html +1 -1
  396. data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
  397. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
  398. data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
  399. data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
  400. data/docs/brut-js/api/testing_index.js.html +1 -1
  401. data/docs/brut-js.html +12 -7
  402. data/docs/business-logic.html +10 -5
  403. data/docs/cli.html +26 -26
  404. data/docs/components.html +61 -64
  405. data/docs/configuration.html +13 -8
  406. data/docs/css.html +14 -9
  407. data/docs/custom-element-tests.html +14 -9
  408. data/docs/database-access.html +12 -7
  409. data/docs/database-schema.html +15 -10
  410. data/docs/deployment.html +10 -5
  411. data/docs/dev-environment.html +12 -7
  412. data/docs/dir-structure.html +74 -0
  413. data/docs/doc-conventions.html +11 -6
  414. data/docs/end-to-end-tests.html +15 -8
  415. data/docs/favicon.ico +0 -0
  416. data/docs/features.html +182 -0
  417. data/docs/flash-and-session.html +73 -82
  418. data/docs/form-constraints.html +118 -0
  419. data/docs/forms.html +57 -367
  420. data/docs/getting-started.html +15 -10
  421. data/docs/handlers.html +51 -61
  422. data/docs/hashmap.json +1 -1
  423. data/docs/hooks.html +14 -9
  424. data/docs/i18n.html +12 -7
  425. data/docs/index.html +11 -6
  426. data/docs/instrumentation.html +12 -7
  427. data/docs/javascript.html +17 -12
  428. data/docs/jobs.html +10 -5
  429. data/docs/keyword-injection.html +22 -21
  430. data/docs/layouts.html +12 -20
  431. data/docs/lsp.html +11 -6
  432. data/docs/markdown-examples.html +10 -5
  433. data/docs/middleware.html +10 -5
  434. data/docs/not-released.html +10 -5
  435. data/docs/overview.html +11 -138
  436. data/docs/pages.html +49 -121
  437. data/docs/recipes/alternate-layouts.html +50 -0
  438. data/docs/recipes/authentication.html +166 -6
  439. data/docs/recipes/blank-layouts.html +43 -0
  440. data/docs/recipes/custom-flash.html +54 -0
  441. data/docs/recipes/indexed-forms.html +102 -0
  442. data/docs/recipes/text-field-component.html +129 -0
  443. data/docs/routes.html +16 -19
  444. data/docs/security.html +11 -6
  445. data/docs/seed-data.html +10 -5
  446. data/docs/space-time-continuum.html +11 -6
  447. data/docs/tutorial.html +11 -6
  448. data/docs/unit-tests.html +10 -5
  449. data/docs/why.html +29 -0
  450. data/lib/brut/front_end/form.rb +8 -8
  451. data/lib/brut/front_end/forms/radio_button_group_input.rb +8 -1
  452. data/lib/brut/front_end/forms/select_input.rb +8 -1
  453. data/lib/brut/version.rb +1 -1
  454. data/specs/brut/front_end/forms/radio_button_group_input.spec.rb +54 -0
  455. data/specs/brut/front_end/forms/select_input.spec.rb +54 -0
  456. metadata +117 -75
  457. data/brutrb.com/public/images/logo-300.png +0 -0
  458. data/brutrb.com/public/images/logo.png +0 -0
  459. data/docs/assets/LogoStop.X8x-4riz.png +0 -0
  460. data/docs/assets/ai.md._6HCDL6d.lean.js +0 -1
  461. data/docs/assets/chunks/@localSearchIndexroot.CeRAdP1K.js +0 -1
  462. data/docs/assets/components.md.CRUMdRoN.js +0 -104
  463. data/docs/assets/dev-environment.md.GZv6xvi9.js +0 -11
  464. data/docs/assets/doc-conventions.md.-kN3Xo5C.js +0 -1
  465. data/docs/assets/doc-conventions.md.-kN3Xo5C.lean.js +0 -1
  466. data/docs/assets/end-to-end-tests.md.yfQHC0b5.lean.js +0 -1
  467. data/docs/assets/flash-and-session.md.BXY8RvT0.js +0 -93
  468. data/docs/assets/forms.md.B-koVgyw.js +0 -379
  469. data/docs/assets/forms.md.B-koVgyw.lean.js +0 -1
  470. data/docs/assets/handlers.md.089DVD3v.js +0 -69
  471. data/docs/assets/handlers.md.089DVD3v.lean.js +0 -1
  472. data/docs/assets/keyword-injection.md.Dt2tKREs.js +0 -25
  473. data/docs/assets/layouts.md.cPnh3NId.lean.js +0 -1
  474. data/docs/assets/overview.md.DVKRM8zl.js +0 -133
  475. data/docs/assets/overview.md.DVKRM8zl.lean.js +0 -1
  476. data/docs/assets/pages.md.BE3kfOc5.js +0 -122
  477. data/docs/assets/pages.md.BE3kfOc5.lean.js +0 -1
  478. data/docs/assets/recipes_authentication.md.CAsXf7hk.js +0 -1
  479. data/docs/assets/recipes_authentication.md.CAsXf7hk.lean.js +0 -1
  480. data/docs/assets/routes.md.BMM7peut.js +0 -29
  481. data/docs/assets/space-time-continuum.md.KPUIKysQ.js +0 -1
  482. data/docs/assets/tutorial.md.BnoGjrdK.js +0 -1
  483. data/docs/assets/tutorial.md.BnoGjrdK.lean.js +0 -1
  484. data/docs/images/logo-300.png +0 -0
  485. data/docs/images/logo.png +0 -0
  486. /data/docs/assets/{cli.md.RmeA2b0i.lean.js → cli.md.CjsktgFz.lean.js} +0 -0
  487. /data/docs/assets/{configuration.md.LG-zIBww.lean.js → configuration.md.DeyhpqEx.lean.js} +0 -0
  488. /data/docs/assets/{css.md.DJgj2clw.lean.js → css.md.CltvJqAa.lean.js} +0 -0
  489. /data/docs/assets/{custom-element-tests.md.BrYJQEl3.lean.js → custom-element-tests.md.B_rbta32.lean.js} +0 -0
  490. /data/docs/assets/{database-access.md.C7l-Vuvb.lean.js → database-access.md.gnluu54N.lean.js} +0 -0
  491. /data/docs/assets/{dev-environment.md.GZv6xvi9.lean.js → dev-environment.md.BroAOLhF.lean.js} +0 -0
data/brutrb.com/forms.md CHANGED
@@ -1,844 +1,224 @@
1
1
  # Forms
2
2
 
3
- The most common way for a web site visitor to submit data to the server is to submit a form. The Web Platform's forms API is much like an uncle you may have: old and rich.
4
-
5
- Brut's forms module solves three problems:
6
-
7
- * Descrbing the data being collected and submitted
8
- * Providing access to the submitted form data on the server
9
- * Support for creating a good client-side experience regarding constraint violations, both client-side and
10
- server-side.
3
+ In HTML, forms are the way data is submit to the server. Forms attract complexity
4
+ since they interact with user experience, data validation, and interaction with a
5
+ back-end database.
11
6
 
12
7
  ## Overview
13
8
 
14
- The forms module has a lot of moving parts, but the general process of using forms is:
15
-
16
- 1. Create a *form class* to describe the data in your form.
17
- 2. Use an instance of that form class to generate HTML for the elements of the form.
18
- 3. Implement a *handler class* that will receive an instance of your form class, populated with the data provided when the form was submitted. (**Note**: forms generally cannot contain data submitted by the browser that was not described by the form class, thus obviating the need for something like Rails' strong parameters).
19
- 4. Add server-side constraint violations to the form and re-render your HTML or use the form data as input to a
20
- back-end process.
21
-
22
- ### Forms Are Submitted to Routes
23
-
24
- When you have a form to process, create a `form` route (remember, all routes must follow the [rules of routing](/routes)):
25
-
26
- ```ruby{6}
27
- class App < Brut::Framework::App
28
-
29
- # ...
30
-
31
- routes do
32
- form "/login"
33
-
34
- # ...
35
- end
36
- end
37
- ```
38
-
39
- Because a form is not a url, the `form` method will set up two expectations of your code:
9
+ Forms in Brut accomplish three things:
40
10
 
41
- * There should be a form class, in this example named `LoginForm`
42
- * There should be a handler class, in this example named `LoginHandler`.
11
+ * Forms model the data elements of a `<form>`, including client-side constraints (which Brut can check server-side as well).
12
+ * Forms assist in HTML generation, to ensure the HTML elements are consistent and
13
+ correct.
14
+ * Forms hold data submitted to the server. No need for strong parameters or digging
15
+ into a Hash of Whatever.
43
16
 
44
- When a browser issues an HTTP `POST` to `/login`, the forms contents will populate an instance of
45
- `LoginForm`, which will be given to an instance of a `LoginHandler` to process the form submission.
17
+ Since forms can lead to a lot of complexity, this module will stick to the very
18
+ basics. There are several recipes we'll link to that explain more complex
19
+ interactions with forms.
46
20
 
47
- First, let's go through the bare minimum of form processing.
21
+ ### Declaring Form Data/Elements
48
22
 
49
- ### Simplest Case of Form Processing
50
-
51
- #### Creating a Form Class
52
-
53
- All form classes must be subclasses of `Brut::FrontEnd::Form`, though practically speaking, yours will subclass `AppForm`, which subclasses `Brut::FrontEnd::Form`. The form class allows you to use various class methods to declare your form's inputs.
54
-
55
- Let's take a simple example of a login form that has an email and a password. We'll use `input` to declare two
56
- `<input>` elements. `input`, like `<input>` accepts a type. Brut recognizes the same list of types as modern
57
- browers. In this case, we want `type="email"` and a `type="password"` inputs:
58
-
59
- ```ruby
60
- # app/src/front_end/forms/login_form.rb
61
- class LoginForm < AppForm
62
- input :email, type: :email
63
- input :password, type: :password
64
- end
65
- ```
66
-
67
- #### Generating HTML with a Form Object
68
-
69
- Let's suppose we have a `LoginPage` that will include this form. Below is a sketch of the implementation. Note
70
- that `form_tag` is a Brut-provided method that will create an HTML `<form>` but also include a hidden field used
71
- for CSRF protection.
23
+ When you [create a form route](/routes), this imlplies a form class exists to
24
+ specify the data:
72
25
 
73
26
  ```ruby
74
- # app/src/front_end/pages/login_page.rb
75
- class LoginPage < AppPage
76
- def initialize
77
- @form = LoginForm.new
78
- end
79
-
80
- def page_template
81
- form_tag(method: :post,
82
- action: LoginHandler.routing) do
83
- # ...
84
- button { "Login" }
85
- end
86
- end
27
+ # app/src/app.rb
28
+ routes do
29
+ form "new_widget"
87
30
  end
88
- ```
89
-
90
- Brut can generate the HTML for the needed inputs via `Brut::FrontEnd::Components::Inputs::TextField`, which is a very long class name. Hold that thought for now. This method will generate an `<input>` element for you, based on how you've set up the field in your form class. The HTML element will have a value set based on the form, if there is a value.
91
-
92
- ```ruby {11,12}
93
- # app/src/front_end/pages/login_page.rb
94
- class LoginPage < AppPage
95
- def initialize
96
- @form = LoginForm.new
97
- end
98
-
99
- def page_template
100
- form_tag(method: :post,
101
- action: LoginHandler.routing) do
102
- # We promise you don't have to type this every time!
103
- Brut::FrontEnd::Components::Inputs::TextField.new(form: @form, input_name: :email)
104
- Brut::FrontEnd::Components::Inputs::TextField.new(form: @form, input_name: :password)
105
- button { "Login" }
106
- end
107
- end
108
- end
109
- ```
110
-
111
- This produces the following HTML (formatted here for clarity):
112
-
113
- ```html
114
- <form method="post" action="/login">
115
- <input type="email" name="email" required>
116
- <input type="password" name="password" required>
117
- <button>Login</button>
118
- </form>
119
- ```
120
-
121
- Note that each fields type and name match what was used in `LoginForm`. Also note that both fields have the `required` attribute. We'll discuss why in a moment.
122
-
123
- #### Expedient Access to Brut Components
124
-
125
- `Brut::FrontEnd::Components` is the root namespace of, among other things, [components](/components) provided by Brut. Components are Phlex components that generate HTML and can be accessed as a Phlex *kit*, by including the namespace in your class:
126
-
127
-
128
- ```ruby {2}
129
- # app/src/front_end/pages/login_page.rb
130
- class LoginPage < AppPage
131
- include Brut::FrontEnd::Components
132
31
 
133
- # ...
32
+ # app/src/front_end/forms/new_widget_form.rb
33
+ class NewWidgetForm < AppForm
134
34
  end
135
35
  ```
136
36
 
137
- This allows you to call `Inputs::TextField` like a method:
138
-
139
- ```ruby {12,13}
140
- # app/src/front_end/pages/login_page.rb
141
- class LoginPage < AppPage
142
- include Brut::FrontEnd::Components
143
-
144
- def initialize
145
- @form = LoginForm.new
146
- end
147
-
148
- def page_template
149
- form_tag(method: :post,
150
- action: LoginHandler.routing) do
151
- Inputs::TextField(form: @form, input_name: :email)
152
- Inputs::TextField(form: @form, input_name: :password)
153
- button { "Login" }
154
- end
155
- end
156
- end
157
- ```
158
-
159
- Brut prefers this style, instead of a bag of random helpers, to make it more clear where the logic being
160
- called is actually coming from. In practice, you would create your own re-usable components for input
161
- fields that use `Brut::FrontEnd::Components::Inputs::TextField` and friends, so even if you find the kit
162
- version too long, it's not something you should be typing a lot.
163
-
164
- #### Receive the Submission
165
-
166
- When the website visitor clicks the "Login" button, the form's data is submitted to `/login` via an HTTP Post.
167
- Brut expects the class `LoginHandler` to exist and will call its `handle!` method. A *handler* must extend
168
- `Brut::FrontEnd::Handler`, though practically speaking it will extend your app's `AppHandler`, which extends `Brut::FrontEnd::Handler`.
169
-
170
- The handler's initializer's signature indicates what data should be passed-in by Brut. Since this is processing a
171
- form submission, the `form:` parameter should be included. If it is, an instance of `LoginForm`, populated with the data provided by the website visitor, will be passed to the initializer. It can accept other parameters as well, but we'll discuss that later.
172
-
173
- Note that *you* must implement `handle`, which `handle!` calls:
37
+ `AppForm` extends `Brut::FrontEnd::Form`, which provides class methods you can use
38
+ to declare your form's elements. Let's say our form has a name (that must be at least 3 characters), a quantity (integer greater than 0), and an optional description.
174
39
 
175
40
  ```ruby
176
- # app/src/front_end/handlers/login_handler.rb
177
- class LoginHandler < AppHandler
178
- def initializer(form:)
179
- @form = form
180
- end
181
- def handle
182
- # ...
183
- end
41
+ class NewWidgetForm < AppForm
42
+ input :name, minlength: 3
43
+ input :quantity, type: :number, min: 0, step: 1
44
+ input :description, required: true
184
45
  end
185
46
  ```
186
47
 
187
- Typically, `handle` will implement a common pattern: checking the validity of the form submission and, if it's
188
- not valid, re-render the previous page with errors, whereas if it is valid, execute some back-end logic.
48
+ `input` declares a form element that will ultimately be handled by an `<input>` or
49
+ `<textarea>` tag. `select` and `radio_button_group` are also avaiable, and are
50
+ discussed in recipes.
189
51
 
190
- If you'll remember, both email and password were set as required in the HTML. We'll talk about how to control
191
- that behavior later, but it does mean that the browser would not submit form data without those values provided.
192
- That said, JavaScript could be circumvented, so our handler could be called without either of those fields.
52
+ `input` accepts an input name used for `<input>`'s `name` attribute. It also
53
+ accepts keyword arguments that match the initializer of
54
+ `Brut::FrontEnd::Forms::InputDefinition`. You'll notice those values mirror the
55
+ various attributes related to client-side constraint validations, for example
56
+ `minlength:` and `pattern:`.
193
57
 
194
- Because `LoginForm` describes the inputs *and* we used an instance of it to generate HTML, that instance can re-evaulate the client-side constraints at any time. The handler does this by calling `#constraint_violations?`.
58
+ Form elements have some defaults, as described below:
195
59
 
196
- ```ruby {7}
197
- # app/src/front_end/handlers/login_handler.rb
198
- class LoginHandler < AppHandler
199
- def initialize(form:)
200
- @form = form
201
- end
202
- def handle
203
- if @form.constraint_violations?
204
- # ...
205
- else
206
- # ...
207
- end
208
- end
209
- end
210
- ```
60
+ | Declaration | Default Behavior |
61
+ |---|---|
62
+ | `input :email` | `type: :email` |
63
+ | `input :password` | `type: :password` |
64
+ | `input :password_confirmation` | `type: :password` |
65
+ | `input «any other name»` | `type: :text` |
66
+ | `input «name», type: :checkbox` | `required: false` |
67
+ | `input «name» type: «not checkbox»` | `required: true` |
211
68
 
212
- Of course, some constraints can't be validated
213
- client-side and require some back-end logic. In this case, we want to check that there is an authorized user
214
- with that email and password. Let's assume the existence of the class `AuthorizedUser` that has a class method
215
- `login` that returns `nil` if there is no user with that email/password combination.
69
+ ### Using Forms to Generate HTML
216
70
 
217
- If that returns `nil`, we want to re-render the `LoginPage`, exposing some sort of constraint violation message
218
- so it can be rendered. We also want the form fields to be pre-filled with the values the visitor provided.
71
+ One reason Brut models forms as classes with declared inputs is that you can then
72
+ use an instance of that class to generate HTML. Brut will generate appropriate
73
+ HTML, optionally configured to show a pre-existing value from the form.
219
74
 
220
- `Brut::FrontEnd::Components` can handle this, so we need to pass our form object into `LoginPage` instead of allowing `LoginPage` to create an empty one. We can do that by adding a `form:` keyword argument that defaults to `nil`:
75
+ The classes that do this are in `Brut::FrontEnd::Components`
221
76
 
222
- ```ruby {3,4}
223
- # app/src/front_end/pages/login_page.rb
224
- class LoginPage < AppPage
225
- def initialize(form: nil)
226
- @form = form || LoginForm.new
227
- end
77
+ | Class | Purpose |
78
+ |---|---|
79
+ |`Brut::FrontEnd::Components::FormTag` | Creates a `<form>` tag that submits to the form's configured route and includes [CSRF protection](/security). |
80
+ |`Brut::FrontEnd::Components::InputTag` | Creates an `<input>` tag |
81
+ |`Brut::FrontEnd::Components::RadioButton` | Creates an `<input type="radio">` tag
82
+ for use in a radio button group. |
83
+ |`Brut::FrontEnd::Components::SelectTagWithOptions` | Creates a `<select>` tag with
84
+ `<option>` tags inside. |
85
+ |`Brut::FrontEnd::Components::TextareaTag` | Creates a `<textarea>` tag. |
228
86
 
229
- def page_template
230
- form_tag(method: :post,
231
- action: LoginHandler.routing) do
232
- Inputs::TextField(form: @form, input_name: :email)
233
- Inputs::TextField(form: @form, input_name: :password)
234
- button { "Login" }
235
- end
236
- end
237
- end
238
- ```
87
+ All of these classes have an initializer that accepts:
239
88
 
240
- To trigger this behavior, the handler will:
241
-
242
- * Call `server_side_constraint_violation` on the form instance.
243
- * Pass it to `LoginPage.new`, which it will return, thus re-rendering the page (when a handler's `handle!` method returns an instance of a page, that page's HTML is generated as the response).
244
-
245
- ```ruby {10-13,17}
246
- # app/src/front_end/handlers/login_handler.rb
247
- class LoginHandler < AppHandler
248
- def handle(form:)
249
- if !form.constraint_violations?
250
- authorized_user = AuthorizedUser.login(
251
- email: form.email,
252
- password: form.password
253
- )
254
- if authorized_user.nil?
255
- form.server_side_constraint_violation(
256
- input_name: :email,
257
- key: :login_not_found
258
- )
259
- end
260
- end
261
- if form.constraint_violations?
262
- LoginPage.new(form: @form)
263
- else
264
- # ...
265
- end
266
- end
267
- end
268
- ```
269
-
270
- When `LoginPage` generates HTML, different HTML is generated, since the form being passed to the components contains constraint violations.
271
-
272
- #### Showing Constraint Violations in HTML
89
+ * `form:` the form object, used to figure out the HTML attributes and current value of the element.
90
+ * `input_name:` to know which input is being generated.
91
+ * `index:` for [indexed form elements](/recipes/indexed-forms.md).
92
+ * `**html_attributes` any other HTML attributesyou'd like to include.
273
93
 
274
- When `Brut::FrontEnd::Components::Inputs::TextField` is created with an existing form that has constraint violations, different HTML is generated. This is what would be produced by our existing `LoginPage` (again, formatted her for clarity):
275
-
276
- ```html {3}
277
- <form method="post" action="/login">
278
- <input type="email" name="email" required
279
- data-invalid data-login_not_found>
280
- <input type="password" name="password" required>
281
- <button>Login</button>
282
- </form>
283
- ```
284
-
285
- These `data-` attributes allow you to target these fields with CSS.
286
-
287
- Actual error messages aren't shown since we didn't put in any HTML that might hold them. The form object
288
- is capable of exposing the constraint violations as keys, intended to be used by the [I18n system](/i18n).
289
-
290
- In general, you don't want to do this directy, but the API looks like so:
94
+ These class names are quite long, but since these are Phlex components, you can
95
+ `include` `Brut::FrontEnd::Components` and access their initializers as a [Phlex
96
+ kit](https://phlex.fun):
291
97
 
292
98
  ```ruby
293
- form.input(:email).validity_state.each do |constraint|
294
- # use constraint.key to construct a message
295
- end
296
- ```
297
-
298
- The reason to avoid this is that a) Brut provides a built-in component to generate HTML and b) if you use
299
- Brut's component, you can achieve parity between client-side constraint violations detected by the browser
300
- and server-side violations identified by your app.
301
-
302
- ### Forms and Constraint Violations
303
-
304
- There are two common issues around constraint violations in HTML forms:
305
-
306
- * Handling the case where JavaScript is circumvented and invalid data is submitted to the server.
307
- * Unifying how client- and server-side constraint violations are shown the user.
308
-
309
- We saw that the use of form classes handles the first issue: a form created with submitted data can self-validate its configured client-side constraints. In this section, we'll see how to unify the violations from both client- and server-side, which will include actually showing error messages.
310
-
311
- Above, we mentioned that each constrait violation is represented by a key to be used with the [I18n
312
- system](/i18n). For client-side violations, these keys are limited to those that are part of the web platform's [`ValidityState`](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState) class. For example, `patternMismatch` is the key used when an input field's value doesn't match the regular expression set in the `pattern` attribute.
313
-
314
- Brut provides default translations for these in `app/config/i18n/en/1_defaults.rb` under the prefix
315
- `cv.fe` (`cv` being short of "constraint violation" and `fe` being short for "front end"). Note that these
316
- keys match `ValidityState` so are in camel-case, not Ruby's idiomatic snake-case.
317
-
318
- Back-end constraint violations are expected to have keys under `cv.be` (`be` for "back-end"), and these
319
- keys *should* conform to Ruby's idioms.
320
-
321
- Let's look at showing server-side constraints first, since those are more like what you may be familiar
322
- with coming from Rails.
323
-
324
- #### Showing Server-Side Violations
325
-
326
- Brut provides the component `Brut::FrontEnd::Components::ConstraintViolations`, which will render all the markup you need for both server- and client-side violations. When there are server-side violations, this component will handle generating the actual error messages.
327
-
328
- Because we've included `Brut::FrontEnd::Components`, the Phlex kit allows
329
- `ConstraintViolations` to be called directly, like so:
330
-
331
- ```ruby {7,10}
332
- # Inside app/src/front_end/pages/login_page.rb
333
- def page_template
334
- form_tag(method: :post,
335
- action: LoginHandler.routing) do
336
-
337
- Inputs::TextField(form: @form, input_name: :email)
338
- ConstraintViolations(form: @form, input_name: :email)
339
-
340
- Inputs::TextField(form: @form, input_name: :password)
341
- ConstraintViolations(form: @form, input_name: :password)
99
+ class NewWidgetPage < AppPage
100
+ include Brut::FrontEnd::Components
342
101
 
343
- button { "Login" }
102
+ def initialize(form: nil)
103
+ @form = form || NewWidgetForm.new
344
104
  end
345
- end
346
- ```
347
-
348
- In the case where we've set the server-side constraint violation for the email field, and assuming that the i18n
349
- key "cv.be.login\_not\_found" maps to the string "No login with that email/password", here is the HTML that
350
- will be rendered:
351
-
352
- ```html {4-8,11-12}
353
- <form method="post" action="/login">
354
- <input type="email" name="email" required
355
- data-invalid data-login_not_found>
356
- <brut-cv-messages input-name="email">
357
- <brut-cv server-side>
358
- No login with that email/password.
359
- </brut-cv>
360
- </brut-cv-messages>
361
-
362
- <input type="password" name="password" required>
363
- <brut-cv-messages input-name="password">
364
- </brut-cv-messages>
365
-
366
- <button>Login</button>
367
- </form>
368
- ```
369
-
370
- `brut-cv-messages` and `brut-cv` are [autonomous custom
371
- elements](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements). If you aren't
372
- familiar with this part of the web platform, there are two things to know:
373
-
374
- * These elements can be targeted with CSS without any JavaScript executing at all (they are treated and rendered by the browser as if they are `display: inline` elements).
375
- * It's possible to attach behavior to these with JavaScript to add progressively-enhanced behavior.
376
-
377
- Without any JavaScript, you now have the basis for styling your error messages *and* the server-side messages are
378
- now rendered using internationalization. As a very basic demonstration, you could place this in
379
- `app/src/front_end/css/index.css`:
380
-
381
- ```css
382
- input[data-invalid] {
383
- color: red;
384
- background-color: mistyrose; /* yes, that's a CSS color :) */
385
- }
386
- brut-cv-messages {
387
- color: red;
388
- display: block;
389
-
390
- brut-cv {
391
- display: block;
392
- }
393
- }
394
- ```
395
-
396
- #### Dynamically Showing Client-Side Violations
397
-
398
- It would be nice if, when the browser detects client-side violations before the user submits the form, the
399
- same UI could be used to show *those* error messages. Brut achieves this via the aforementioned
400
- autonomous custom elements.
401
105
 
402
- You'll note that even though the password field had no constraint violations, `<brut-cv-messages input-name="password">` was still generated for it. This element, working in conjuction with a few other elements, will provide localized messaging for client-side constraint violations using the same markup and CSS as your server-side constraint violations.
106
+ private attr_reader :form
403
107
 
404
- * `<brut-form>` will manage the `<form>` it contains to listen for any violations
405
- * `<brut-cv-messages>` identifies where error messages should go, per form element.
406
- * `<brut-cv>` contains a specific message or key.
407
- * `<brut-i18n-translation>` maps keys from `<brut-cv>` elements to actual translated strings.
408
-
409
- Together, these elements will show the visitor localized error messages exactly the same way as
410
- server-side error messages are shown.
411
-
412
- First, we need to wrap our form with `brut-form`:
413
-
414
- ```ruby {3,15}
415
- # Inside app/src/front_end/pages/login_page.rb
416
- def page_template
417
- brut_form do
418
- form_tag(method: :post,
419
- action: LoginHandler.routing) do
420
-
421
- Inputs::TextField(form: @form, input_name: :email)
422
- ConstraintViolations(form: @form, input_name: :email)
423
-
424
- Inputs::TextField(form: @form, input_name: :password)
425
- ConstraintViolations(form: @form, input_name: :password)
426
-
427
- button { "Login" }
428
- end
429
- end
430
- end
431
- ```
432
-
433
- Second, we need `<brut-i18-translation>` elements on the page somewhere. These *should* be in your
434
- default layout and look like so:
435
-
436
- ```ruby {7,8}
437
- # app/src/front_end/layouts/default_layout.rb
438
- def view_template
439
- doctype
440
- html(lang: "en") do
441
- head do
442
- # ...
443
- I18nTranslations("cv.fe")
444
- I18nTranslations("cv.this_field")
445
- # ...
446
- end
447
- body do
448
- yield
108
+ def page_template
109
+ FormTag(for: form) do
110
+ Components::InputTag(form:, input_name: :name)
111
+ Components::InputTag(form:, input_name: :quantity)
112
+ Components::TextareaTag(form:, input_name: :description)
449
113
  end
450
114
  end
451
115
  end
452
116
  ```
453
117
 
454
- `I18nTranslations` is a shortcut to `Brut::FrontEnd::Components::I18nTranslations`, which is a component to
455
- render one `<brut-i18n-translation>` element per transalation found under the given prefix. Thus, it
456
- would generate HTML like so:
457
-
458
- ```html
459
- <brut-i18n-translation
460
- key="cv.fe.badInput"
461
- value="%{field} is the wrong type of data">
462
- </brut-i18n-translation>
463
- <brut-i18n-translation
464
- key="cv.fe.patternMismatch"
465
- value="%{field} isn't in the right format">
466
- </brut-i18n-translation>
467
- <!-- etc. -->
468
- ```
469
-
470
- With this in place, here is how this works:
471
-
472
- 1. The `<brut-form>` listens for constraint violations on the `<form>` elements.
473
- 2. When one is detected, it then locates the `<brut-cv-messages>` element for that element's name (based on the `input-name` attribute).
474
- 3. `<brut-form>` will insert one `<brut-cv>` for each constraint that element's value violates, based on `ValidityState`. The value from `ValidityState` is used to create an I18n key, for example `<brut-cv key="cv.fe.patternMismatch"></brut-cv>`.
475
- 4. `brut-cv` itself is a custom element that will use its `key` attribute to locate the actual message to show. That message is expected to be in a `<brut-i18n-translation>` element with a matching key, somewhere on the page.
476
-
477
- This may seem convoluted, however it separates concerns reasonably well and allows localization of the messaging.
478
-
479
- If your visitor's locale is not `en`, the layout would render different values for each `<brut-i18n-transation>` elements, thus allowing client-side constraint violations to be shown in the visitor's language.
480
-
481
- You can now style these client-side messages with a slight change to your CSS:
482
-
483
- ```css {2}
484
- input[data-invalid],
485
- input:invalid {
486
- color: red;
487
- background-color: mistyrose;
488
- }
489
- brut-cv-messages {
490
- color: red;
491
- display: block;
492
-
493
- brut-cv {
494
- display: block;
495
- }
496
- }
497
- ```
498
-
499
- Note that a) this didn't require a lot of code on your part, b) the server is still re-evaluating the
500
- client-side constraints, so the visitor will see them, even if JavaScript is off or fails, and c) it
501
- sticks as closely to the web platform as possible.
502
-
503
- That all said, this implementation falls vicitim to an annoyance of client-side constraint violations, which is prematurely showing error messages.
504
-
505
- #### Managing Errors Shown Before Submission
506
-
507
- I'm sure we've all experienced over-zealous forms where typing a single character into an email field reveals a blaring red message that our email is not valid. Brut's custom elements can help.
508
-
509
- You can certainly use [`user-invalid`](https://developer.mozilla.org/en-US/docs/Web/CSS/:user-invalid) to help address this problem, but it doesn't always work how you'd think, and is only recently available in
510
- [Baseline](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility).
511
-
512
- To help, `brut-form` will set the attribute `submitted-invalid` on itself if the user has attempted to submit the form with data that violates the client-side constraints. A slight change to CSS will cause your error messages to only show up when submission has been attempted:
513
-
514
- ```css
515
- /* First, hide client-side messaging by default.
516
- Server-side messages will always appear */
517
- form {
518
- brut-cv {
519
- display:none;
520
- }
521
- brut-cv[server-side] {
522
- display:block;
523
- }
524
- }
525
-
526
- /* Now, show constraint violations only if
527
- submitted-invalid was set */
528
- brut-form[submitted-invalid] {
529
- brut-cv {
530
- display:block;
531
- }
532
- }
533
-
534
- /* Always show elements with data-invalid since that
535
- is server-generated, but only style the elements
536
- as invalid if the form has submitted-invalid on it */
537
- input[data-invalid],
538
- brut-form[submitted-invalid] input:invalid {
539
- color: red;
540
- background-color: mistyrose;
541
- }
542
-
543
- brut-cv-messages {
544
- color: red;
545
- display: block;
546
- }
547
- ```
548
-
549
- Now, client-side constraint violations will only be shown to the user when they attempt to submit the form. Note that you have complete control, since this is all impelmented using standard CSS. Brut and its custom elements give you the tools and hooks to style as you see fit.
550
-
551
- ### Checkboxes
118
+ Phlex kits provides a methods named for the class that call that class' constructor.
552
119
 
553
- Checkboxes are implemented in HTML by `<input type="checkbox">`, so in your form, you would use `type:
554
- :checkbox`:
555
-
556
- ```ruby {5,6}
557
- # app/src/front_end/forms/login_form.rb
558
- class LoginForm < AppForm
559
- input :email, type: :email
560
- input :password, type: :password
561
- input :remember, type: :checkbox
562
- input :not_robot, type: :checkbox
563
- end
564
- ```
565
-
566
- Checkboxes can be rendered by a `Brut::FrontEnd::Components::Inputs::TextField`, and their `value` attribute would always be the string `"true"`. If the form's value for the input is the string `"true"`, the checkbox would have the `checked` attribute:
120
+ The code above will generate this HTML
567
121
 
568
122
  ```html
569
- <!-- Form.new(params: { remember: "true" }) -->
570
- <input type="checkbox" name="remember" value="true" checked>
571
- <input type="checkbox" name="not_robot" value="true">
123
+ <form action="/new_widgets" method="post">
124
+ <input type="hidden" name="authenticity_token" value=«value»>
125
+ <input type="text" name="name" required minlength="3">
126
+ <input type="number" name="quantity" required min="0" step="1">
127
+ <textarea name="description">
128
+ </textarea>
129
+ </form>
572
130
  ```
573
131
 
574
- ### Radio Buttons
575
-
576
- Radio buttons are implemented in HTML by `<input type="radio">`, with an expectation of more than one such input
577
- having the same value for the `name` attribute, but different values for the `value` attributes, one of which may
578
- be `checked`.
579
-
580
- Brut implements this via `Brut::FrontEnd::Components::Inputs::RadioButton`, whose initializer behaves like the other form input components. To create radio buttons in a form, use `radio_button_group`:
132
+ Forms accept a single initializer parameter, `params` that is a `Hash`.
133
+ `Brut::FrontEnd::Form` implements this initializer, and will pluck values from the
134
+ hash to initialize the inputs:
581
135
 
582
- ```ruby {5}
583
- # app/src/front_end/forms/login_form.rb
584
- class LoginForm < AppForm
585
- input :email, type: :email
586
- input :password, type: :password
587
- radio_button_group :remember
588
- end
589
- ```
136
+ ::: code-group
590
137
 
591
- The form would not need to be configured with the possible values - that will happen when you generate HTML:
592
-
593
- ```ruby
594
- def view_template
595
- form do
596
- [ :never, :one_week, :one_month ].each do |remember|
597
- label do
598
- render(
599
- Inputs::RadioButton(
600
- form:,
601
- input_name: :remember,
602
- value: remember
603
- )
604
- )
605
- plain { remember.to_s }
606
- end
607
- end
608
- end
609
- end
610
- ```
611
-
612
- When generating HTML, Brut will examine the value of `form.remember` to know which radio button to check. To set
613
- a default, set that value when creating the form:
138
+ ```ruby [Form Class] {5-8}
139
+ class NewWidgetPage < AppPage
140
+ include Brut::FrontEnd::Components
614
141
 
615
- ```ruby {4-6}
616
- # app/src/front_end/pages/login_page.rb
617
- class LoginPage < AppPage
618
142
  def initialize(form: nil)
619
- @form = form || LoginForm.new(params: {
620
- remember: "never"
621
- })
622
- end
623
- ```
624
-
625
- ### Selects
626
-
627
- Selects are implemented in HTML by a `<select>` that has a `name` attribute, and contains several `<option>` elements, each having a `value` attribute. They work like radio buttons in Brut, in that you would not specify the possible values in the form class.
628
-
629
- > [!WARNING]
630
- > Brut does not support multi-selects, yet.
631
-
632
-
633
- You can set up a select via `select`
634
-
635
- ```ruby {5}
636
- # app/src/front_end/forms/login_form.rb
637
- class LoginForm < AppForm
638
- input :email, type: :email
639
- input :password, type: :password
640
- select :remember
641
- end
642
- ```
643
-
644
- Creating the HTML can be done with `Brut::FrontEnd::Components::Inputs::Select`. It's initializer is more complex, since it provides a way to show visitor-friendly values instead of the innate `value` for each option, as well as to allow for a "blank" entry.
645
-
646
- Let's suppose we have a class named `LoginRememberOption`. It's a simple wrapper around a value we might store in the database and use to lookup an I18n key.
647
-
648
- ```ruby
649
- class LoginRememberOption
650
- include Brut::I18n::ForBackend
651
- def initialize(value)
652
- @value = value
143
+ @form = form || NewWidgetForm.new( params: {
144
+ name: "My New Widget",
145
+ quantity: 10,
146
+ })
653
147
  end
654
148
 
655
- def to_s = @value
656
-
657
- def name
658
- t("login.remember_options.#{@value}")
659
- end
149
+ # ...
660
150
 
661
- def self.all
662
- [
663
- LoginRememberOption.new("never"),
664
- LoginRememberOption.new("one_week"),
665
- LoginRememberOption.new("one_month"),
666
- ]
667
- end
668
151
  end
669
152
  ```
670
153
 
671
- To show these options in a `<select>`, we might do this:
672
-
673
- ```ruby
674
- def view_template
675
- form do
676
- render(
677
- Inputs::Select(
678
- form:,
679
- input_name: :remember,
680
- options: LoginRememberOption.all,
681
- value_attribute: :to_s,
682
- option_text_attribute: :name,
683
- include_blank: {
684
- value: :blank,
685
- text_content: "-- Choose --",
686
- }
687
- )
688
- )
689
- end
690
- end
154
+ ```html [HTML Generated] {3,5}
155
+ <form action="/new_widgets" method="post">
156
+ <input type="hidden" name="authenticity_token" value=«value»>
157
+ <input type="text" value="My New Widget"
158
+ name="name" required minlength="3">
159
+ <input type="number" value="10"
160
+ name="quantity" required min="0" step="1">
161
+ <textarea name="description">
162
+ </textarea>
163
+ </form>
691
164
  ```
692
165
 
693
- This will create this HTML (making some assumptions about the translations):
166
+ :::
694
167
 
695
- ```html
696
- <select name="remember">
697
- <option value="blank">-- Choose --</option>
698
- <option value="never">Never</option>
699
- <option value="one_week">One Week</option>
700
- <option value="one_month">One Month</option>
701
- </select>
702
- ```
168
+ ### Accessing Data in a Submitted Form
703
169
 
704
- ### Arrays of Values
170
+ As mentioned in [routes](/routes), a `form` route implies not just a form class, but
171
+ a [handler](/handlers) class to receive the submitted data.
705
172
 
706
- Some complex forms involve a potentially arbitrary number of inputs for a given field. For example, you might allow the visitor to edit widgets in bulk, 10 at a time.
707
-
708
- Brut can handle this, with help from Rack. First, you'll use `array: true` when declaring an input:
173
+ We'll discuss handlers in the next section, but they demonstrate how you can access
174
+ a form's data:
709
175
 
710
176
  ```ruby
711
- class BulkWidgetForm < AppForm
712
- input :name, array: true, required: false
713
- end
714
- ```
715
-
716
- In this case, we need `required: false` or every single field we generate will be required.
717
-
718
- To generate the HTML, use the optional `index:` parameter to the initializer as well as for `ConstraintViolations`:
719
-
720
- ```ruby {11,16}
721
- # Inside e.g. app/src/front_end/pages/create_bulk_widget_page.rb
722
- def page_template
723
- brut_form do
724
- form_tag(method: :post,
725
- action: BulkWidgetForm.routing) do
726
-
727
- 10.times do |i|
728
- Inputs::TextField(
729
- form: @form,
730
- input_name: :name,
731
- index: i
732
- )
733
- ConstraintViolations(
734
- form: @form,
735
- input_name: :email,
736
- index: i
737
- )
738
- end
739
-
740
- button { "Save" }
741
- end
177
+ class NewWidgetHandler < AppHandler
178
+ def initialize(form:)
179
+ @form = form
742
180
  end
743
- end
744
- ```
745
-
746
- This will generate HTML like so:
747
-
748
- ```html
749
- <!-- ... -->
750
- <input name="name[]">
751
- <!-- ... -->
752
- <input name="name[]">
753
- <!-- ... -->
754
- ```
755
-
756
- The `[]` is a Rack-specific format that will provide the values to the server as an array. While this is not supported nor required of the web platform, Rack does not provide all values for a given input name, unless that name has the `[]` suffix.
757
181
 
758
- Also note that you do not have to specify a max length of the array. You can use as many as you like, just be sure that the index values are monotonically increasing with no gaps.
759
-
760
- In the handler, values can be accessed by index:
761
-
762
- ```ruby {4}
763
- # app/src/front_end/handlers/bulk_widget_handler.rb
764
- class BulkWidgetHandler < AppHandler
765
182
  def handle
766
- @form.name(2) # name with index 2 i.e. the 3rd value
183
+ form.name # => whatever name was submitted
184
+ form.quantity # => whatever quantity was submitted
185
+ form.description # => description provided
767
186
  end
768
187
  end
769
188
  ```
770
189
 
771
- To set the values, you must provide an array to `params:`:
772
-
773
- ```ruby {3}
774
- widgets = DB::Widget.order(:created_at).limit(10)
775
- BulkWidgetForm.new(params: {
776
- name: widgets.to_a.map(&:name),
777
- })
778
-
779
- # OR
780
- BulkWidgetForm.new(params: {
781
- name: [
782
- "",
783
- "",
784
- "Third Widget",
785
- "",
786
- ""
787
- ],
788
- })
789
- ```
190
+ A few things to note about how this works:
790
191
 
791
- To set server-side constraint violations, `index:` can be used:
192
+ * Only those inputs declared in the form class can be accessed. All other values are
193
+ discarded. No need for "strong parameters".
194
+ * All values are strings, because this is what HTML provides.
195
+ * Blank values are coerced to `nil`.
792
196
 
793
- ```ruby {4}
794
- form.server_side_constraint_violation(
795
- input_name: :name,
796
- key: :must_be_two_words,
797
- index: 3
798
- )
799
- ```
800
-
801
- This can be quite complicated when editing sparse data. Brut should do a decent job raising an error if you try to treat a non-array value as an array or vice-versa.
197
+ The next module will deal with form constraints and validations, in particular how
198
+ to manage the user experience around client-side constraint violations, how to
199
+ re-check them server side, and how to perform server-side checks.
802
200
 
803
201
  ## Testing
804
202
 
805
- Form classes don't need any logic on them, but they can be given helper methods or other logic if it makes sense.
806
- To test them, test them like any other class - instantiate an object and examine the behavior of its methods.
807
-
808
- Note that Brut provides the constructor for all form classes, and it expects a single keyword parameter named `params:`
809
- that is a hash mapping strings to strings representing the submitted form data. The keys can be symbols and Brut will
810
- map them to strings.
811
-
812
- Testing handlers is covered in [Handlers](/handlers)
813
-
814
- When testing the UX around constraint violations, you should use an end-to-end test, as this will allow you to
815
- assert behavior around client-side constraint violations. This is discussed in [End-to-end
816
- Tests](/end-to-end-tests).
203
+ Form classes don't need any logic on them, but they can be given helper methods or other logic if it makes sense. To test them, test them like any other class - instantiate an object and examine the behavior of its methods.
817
204
 
818
205
  ## Recommended Practices
819
206
 
820
- ### Make Use of Components
821
-
822
- The example we saw above creates only minimal markup, yet required a fair bit of code. You are encouraged to
823
- create your own components that generate the markup you need for your app's inputs. For example, you are likely
824
- going to want `app/src/front_end/components/text_field_component.rb` to generate whatever markup is needed fo
825
- your text fields to look how they are supposed to, with and without constraint violation messages.
826
-
827
- ### Functional or Utility CSS Is Difficult Here
207
+ ### Create Components to Generate Form Controls
828
208
 
829
- The way constraint violations are implemented leverages the web platform, which naturally includes conventional use of CSS. This creates an "impedance mismatch" with functional or utility CSS like Tailwind.
209
+ `Brut::FrontEnd::Components::Inputs` will generate the basic tags like `<input>` or
210
+ `<select>`. Everything else like `<label>` is up to you. We recommend that you
211
+ create [components](/components) to generate the markup required for *your* inputs
212
+ and controls.
830
213
 
831
- While it may be possible to write a bunch of single-purpose classes to target the markup and attributes Brut generates, it may be easier to write conventional CSS for constraint violations.
214
+ The recipe ["Creating a Text Field"](/recipes/text-field-component) will walk you
215
+ through the steps and considerations.
832
216
 
833
- To avoid duplication, you should leverage the custom properties of your CSS framework. For example:
217
+ ### Take Advantage of Client Side Constraints
834
218
 
835
- ```css
836
- input[data-invalid] {
837
- color: var(--color-red);
838
- }
839
- ```
840
-
841
- In theory, your design for form inputs will be done once and be relatively stable. But, this is the downside of using CSS frameworks that eschew using CSS directly. You will need to manage this.
219
+ Even though client-side constraints can sometimes be awkward in certain browsers,
220
+ they are going to be eminently usable and accessible, and you can easily
221
+ re-validate them on the server side.
842
222
 
843
223
  ## Technical Notes
844
224
 
@@ -848,15 +228,10 @@ In theory, your design for form inputs will be done once and be relatively stabl
848
228
 
849
229
  _Last Updated May 13, 2025_
850
230
 
851
- Form internals try to coerce types to strings, since the web and HTTP is all strings all the time. Empty strings
852
- are coerced to `nil`. If the form's `params:` value contains any type Brut cannot deal with, you'll get an
853
- exception during tests and a notice/event in production.
854
-
855
231
  For HTML generation, there are few classes that work together:
856
232
 
857
233
  * *input definitions* define an input and tend to provide an API similar to HTML's. See `Brut::FrontEnd::Forms::InputDefinition`.
858
- * *inputs* represent the runtime state of an input from the browser. Whereas an input definition has no state, the input does. It delegates much of its behavior to the underlying input definition. It's `value=` method performs client-side constraint validations by creating a `ValidityState` internally. See `Brut::FrontEnd::Forms::Input`.
234
+ * *inputs* represent the runtime state of an input from the browser. Whereas an input definition has no state, the input does. It delegates much of its behavior to the underlying input definition. It's `value=` method performs client-side constraint validations by creating a `Brut::FrontEnd::Forms::ValidityState` internally. See `Brut::FrontEnd::Forms::Input`.
859
235
  * `Brut::FrontEnd::Forms::InputDeclarations` is a module that allows creating input definitions inside your form
860
236
  class. It implements the class methods like `input`.
861
- * `Brut::FrontEnd::Components::Inputs` contains components used to generate `<input>` fields. These classes will
862
- coerce the value of the `input` they are given to generate the correct HTML.
237
+ * `Brut::FrontEnd::Components::Inputs` contains components used to generate `<input>` fields. These classes will coerce the value of the `input` they are given to generate the correct HTML.