brut 0.0.29 → 0.2.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 (517) 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 +48 -9
  9. data/brutrb.com/.vitepress/theme/style.css +14 -35
  10. data/brutrb.com/adrs.md +15 -0
  11. data/brutrb.com/ai.md +10 -15
  12. data/brutrb.com/assets.md +2 -9
  13. data/brutrb.com/brut-js.md +12 -2
  14. data/brutrb.com/cli.md +9 -13
  15. data/brutrb.com/components.md +118 -96
  16. data/brutrb.com/configuration.md +3 -4
  17. data/brutrb.com/css.md +2 -2
  18. data/brutrb.com/custom-element-tests.md +3 -4
  19. data/brutrb.com/database-access.md +1 -1
  20. data/brutrb.com/database-schema.md +29 -41
  21. data/brutrb.com/dev-environment.md +13 -8
  22. data/brutrb.com/dir-structure.md +120 -0
  23. data/brutrb.com/doc-conventions.md +17 -15
  24. data/brutrb.com/dx +1 -0
  25. data/brutrb.com/end-to-end-tests.md +12 -10
  26. data/brutrb.com/features.md +373 -0
  27. data/brutrb.com/flash-and-session.md +115 -131
  28. data/brutrb.com/form-constraints.md +266 -0
  29. data/brutrb.com/forms.md +140 -765
  30. data/brutrb.com/getting-started.md +10 -11
  31. data/brutrb.com/handlers.md +119 -95
  32. data/brutrb.com/hooks.md +18 -20
  33. data/brutrb.com/i18n.md +6 -4
  34. data/brutrb.com/images/DevEnvironment.graffle +0 -0
  35. data/brutrb.com/images/DevEnvironment.png +0 -0
  36. data/brutrb.com/images/LogoStop.png +0 -0
  37. data/brutrb.com/index.md +0 -3
  38. data/brutrb.com/instrumentation.md +7 -10
  39. data/brutrb.com/javascript.md +14 -14
  40. data/brutrb.com/keyword-injection.md +72 -114
  41. data/brutrb.com/layouts.md +20 -52
  42. data/brutrb.com/lsp.md +1 -1
  43. data/brutrb.com/overview.md +30 -372
  44. data/brutrb.com/pages.md +119 -207
  45. data/brutrb.com/public/SocialImage.png +0 -0
  46. data/brutrb.com/public/favicon.ico +0 -0
  47. data/brutrb.com/recipes/alternate-layouts.md +32 -0
  48. data/brutrb.com/recipes/authentication.md +315 -6
  49. data/brutrb.com/recipes/blank-layouts.md +22 -0
  50. data/brutrb.com/recipes/custom-flash.md +51 -0
  51. data/brutrb.com/recipes/indexed-forms.md +149 -0
  52. data/brutrb.com/recipes/text-field-component.md +182 -0
  53. data/brutrb.com/roadmap.md +57 -0
  54. data/brutrb.com/routes.md +56 -82
  55. data/brutrb.com/security.md +0 -3
  56. data/brutrb.com/space-time-continuum.md +8 -12
  57. data/brutrb.com/tutorial.md +5 -1
  58. data/brutrb.com/why.md +19 -0
  59. data/docs/404.html +8 -3
  60. data/docs/SocialImage.png +0 -0
  61. data/docs/adrs.html +29 -0
  62. data/docs/ai.html +11 -6
  63. data/docs/api/Brut/BackEnd/SeedData.html +1 -1
  64. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +1 -1
  65. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +1 -1
  66. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +1 -1
  67. data/docs/api/Brut/BackEnd/Sidekiq.html +1 -1
  68. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +1 -1
  69. data/docs/api/Brut/BackEnd/Validators.html +1 -1
  70. data/docs/api/Brut/BackEnd.html +1 -1
  71. data/docs/api/Brut/CLI/App.html +1 -1
  72. data/docs/api/Brut/CLI/AppRunner.html +1 -1
  73. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +1 -1
  74. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +1 -1
  75. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +1 -1
  76. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +1 -1
  77. data/docs/api/Brut/CLI/Apps/BuildAssets.html +1 -1
  78. data/docs/api/Brut/CLI/Apps/DB/Create.html +1 -1
  79. data/docs/api/Brut/CLI/Apps/DB/Drop.html +1 -1
  80. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +1 -1
  81. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +1 -1
  82. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +1 -1
  83. data/docs/api/Brut/CLI/Apps/DB/Seed.html +1 -1
  84. data/docs/api/Brut/CLI/Apps/DB/Status.html +1 -1
  85. data/docs/api/Brut/CLI/Apps/DB.html +1 -1
  86. data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +1 -1
  87. data/docs/api/Brut/CLI/Apps/DeployBase.html +1 -1
  88. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +1 -1
  89. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +1 -1
  90. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +1 -1
  91. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +1 -1
  92. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +3 -3
  93. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +1 -1
  94. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +1 -1
  95. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +1 -1
  96. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +1 -1
  97. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +2 -2
  98. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +1 -1
  99. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +1 -1
  100. data/docs/api/Brut/CLI/Apps/Scaffold.html +1 -1
  101. data/docs/api/Brut/CLI/Apps/Test/Audit.html +1 -1
  102. data/docs/api/Brut/CLI/Apps/Test/E2e.html +1 -1
  103. data/docs/api/Brut/CLI/Apps/Test/JS.html +6 -6
  104. data/docs/api/Brut/CLI/Apps/Test/Run.html +1 -1
  105. data/docs/api/Brut/CLI/Apps/Test.html +2 -2
  106. data/docs/api/Brut/CLI/Apps.html +1 -1
  107. data/docs/api/Brut/CLI/Command.html +3 -3
  108. data/docs/api/Brut/CLI/Error.html +1 -1
  109. data/docs/api/Brut/CLI/ExecutionResults/Result.html +1 -1
  110. data/docs/api/Brut/CLI/ExecutionResults.html +1 -1
  111. data/docs/api/Brut/CLI/Executor.html +1 -1
  112. data/docs/api/Brut/CLI/InvalidOption.html +1 -1
  113. data/docs/api/Brut/CLI/Options.html +1 -1
  114. data/docs/api/Brut/CLI/Output.html +1 -1
  115. data/docs/api/Brut/CLI/SystemExecError.html +1 -1
  116. data/docs/api/Brut/CLI.html +1 -1
  117. data/docs/api/Brut/FactoryBot.html +1 -1
  118. data/docs/api/Brut/Framework/App.html +1 -1
  119. data/docs/api/Brut/Framework/Config.html +1 -1
  120. data/docs/api/Brut/Framework/Container.html +1 -1
  121. data/docs/api/Brut/Framework/Error.html +1 -1
  122. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +1 -1
  123. data/docs/api/Brut/Framework/Errors/Bug.html +1 -1
  124. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +1 -1
  125. data/docs/api/Brut/Framework/Errors/MissingParameter.html +1 -1
  126. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +1 -1
  127. data/docs/api/Brut/Framework/Errors/NotFound.html +1 -1
  128. data/docs/api/Brut/Framework/Errors/NotImplemented.html +1 -1
  129. data/docs/api/Brut/Framework/Errors.html +1 -1
  130. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +1 -1
  131. data/docs/api/Brut/Framework/MCP.html +1 -1
  132. data/docs/api/Brut/Framework/ProjectEnvironment.html +1 -1
  133. data/docs/api/Brut/Framework.html +1 -1
  134. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +1 -1
  135. data/docs/api/Brut/FrontEnd/Component/Helpers.html +1 -1
  136. data/docs/api/Brut/FrontEnd/Component.html +1 -1
  137. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +1 -1
  138. data/docs/api/Brut/FrontEnd/Components/FormTag.html +1 -1
  139. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +1 -1
  140. data/docs/api/Brut/FrontEnd/Components/Input.html +1 -1
  141. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +1 -1
  142. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +1 -1
  143. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +1 -1
  144. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +3 -3
  145. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +1 -1
  146. data/docs/api/Brut/FrontEnd/Components/Inputs.html +1 -1
  147. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +1 -1
  148. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +1 -1
  149. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +1 -1
  150. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +1 -1
  151. data/docs/api/Brut/FrontEnd/Components.html +1 -1
  152. data/docs/api/Brut/FrontEnd/Download.html +1 -1
  153. data/docs/api/Brut/FrontEnd/Flash.html +1 -1
  154. data/docs/api/Brut/FrontEnd/Form.html +9 -11
  155. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +1 -1
  156. data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +1 -1
  157. data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +1 -1
  158. data/docs/api/Brut/FrontEnd/Forms/Input.html +1 -1
  159. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +1 -1
  160. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +1 -1
  161. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +135 -20
  162. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +1 -1
  163. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +135 -20
  164. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +1 -1
  165. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +1 -1
  166. data/docs/api/Brut/FrontEnd/Forms.html +1 -1
  167. data/docs/api/Brut/FrontEnd/GenericResponse.html +1 -1
  168. data/docs/api/Brut/FrontEnd/Handler.html +1 -1
  169. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +1 -1
  170. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +1 -1
  171. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +1 -1
  172. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +1 -1
  173. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +1 -1
  174. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +1 -1
  175. data/docs/api/Brut/FrontEnd/Handlers.html +1 -1
  176. data/docs/api/Brut/FrontEnd/HandlingResults.html +1 -1
  177. data/docs/api/Brut/FrontEnd/HttpMethod.html +1 -1
  178. data/docs/api/Brut/FrontEnd/HttpStatus.html +1 -1
  179. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +1 -1
  180. data/docs/api/Brut/FrontEnd/Layout.html +1 -1
  181. data/docs/api/Brut/FrontEnd/Middleware.html +1 -1
  182. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +1 -1
  183. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +1 -1
  184. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +1 -1
  185. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +1 -1
  186. data/docs/api/Brut/FrontEnd/Middlewares.html +1 -1
  187. data/docs/api/Brut/FrontEnd/Page.html +1 -1
  188. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +2 -2
  189. data/docs/api/Brut/FrontEnd/Pages.html +1 -1
  190. data/docs/api/Brut/FrontEnd/RequestContext.html +1 -1
  191. data/docs/api/Brut/FrontEnd/RouteHook.html +1 -1
  192. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +1 -1
  193. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +1 -1
  194. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +1 -1
  195. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +1 -1
  196. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +1 -1
  197. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +1 -1
  198. data/docs/api/Brut/FrontEnd/RouteHooks.html +1 -1
  199. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +1 -1
  200. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +1 -1
  201. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +1 -1
  202. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +1 -1
  203. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +1 -1
  204. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +1 -1
  205. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +1 -1
  206. data/docs/api/Brut/FrontEnd/Routing/Route.html +1 -1
  207. data/docs/api/Brut/FrontEnd/Routing.html +1 -1
  208. data/docs/api/Brut/FrontEnd/Session.html +1 -1
  209. data/docs/api/Brut/FrontEnd.html +1 -1
  210. data/docs/api/Brut/I18n/BaseMethods.html +1 -1
  211. data/docs/api/Brut/I18n/ForBackEnd.html +1 -1
  212. data/docs/api/Brut/I18n/ForCLI.html +1 -1
  213. data/docs/api/Brut/I18n/ForHTML.html +1 -1
  214. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +1 -1
  215. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +1 -1
  216. data/docs/api/Brut/I18n.html +1 -1
  217. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +1 -1
  218. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +1 -1
  219. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +1 -1
  220. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +1 -1
  221. data/docs/api/Brut/Instrumentation.html +1 -1
  222. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +1 -1
  223. data/docs/api/Brut/SinatraHelpers.html +1 -1
  224. data/docs/api/Brut/SpecSupport/ClockSupport.html +1 -1
  225. data/docs/api/Brut/SpecSupport/ComponentSupport.html +1 -1
  226. data/docs/api/Brut/SpecSupport/E2ETestServer.html +1 -1
  227. data/docs/api/Brut/SpecSupport/E2eSupport.html +1 -1
  228. data/docs/api/Brut/SpecSupport/EnhancedNode.html +1 -1
  229. data/docs/api/Brut/SpecSupport/FlashSupport.html +1 -1
  230. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +1 -1
  231. data/docs/api/Brut/SpecSupport/GeneralSupport.html +1 -1
  232. data/docs/api/Brut/SpecSupport/HandlerSupport.html +1 -1
  233. data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +1 -1
  234. data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +1 -1
  235. data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +1 -1
  236. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +1 -1
  237. data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +1 -1
  238. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +1 -1
  239. data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +1 -1
  240. data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +1 -1
  241. data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +1 -1
  242. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +1 -1
  243. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +1 -1
  244. data/docs/api/Brut/SpecSupport/Matchers.html +1 -1
  245. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +1 -1
  246. data/docs/api/Brut/SpecSupport/RSpecSetup.html +1 -1
  247. data/docs/api/Brut/SpecSupport/SessionSupport.html +1 -1
  248. data/docs/api/Brut/SpecSupport.html +1 -1
  249. data/docs/api/Brut.html +1 -1
  250. data/docs/api/Clock.html +1 -1
  251. data/docs/api/RichString.html +150 -343
  252. data/docs/api/SemanticLogger/Appender/Async.html +1 -1
  253. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +1 -1
  254. data/docs/api/Sequel/Extensions/BrutMigrations.html +1 -1
  255. data/docs/api/Sequel/Extensions.html +1 -1
  256. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +1 -1
  257. data/docs/api/Sequel/Plugins/CreatedAt.html +1 -1
  258. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +1 -1
  259. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +1 -1
  260. data/docs/api/Sequel/Plugins/ExternalId.html +1 -1
  261. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +1 -1
  262. data/docs/api/Sequel/Plugins/FindBang.html +1 -1
  263. data/docs/api/Sequel/Plugins.html +1 -1
  264. data/docs/api/Sequel.html +1 -1
  265. data/docs/api/_index.html +5 -5
  266. data/docs/api/class_list.html +1 -1
  267. data/docs/api/file.README.html +22 -3
  268. data/docs/api/index.html +22 -3
  269. data/docs/api/method_list.html +290 -306
  270. data/docs/api/top-level-namespace.html +1 -1
  271. data/docs/assets/DevEnvironment.DaFcVfwP.png +0 -0
  272. data/docs/assets/LogoStop.Gb3tDhL1.png +0 -0
  273. data/docs/assets/adrs.md.JRxZ5uYE.js +1 -0
  274. data/docs/assets/adrs.md.JRxZ5uYE.lean.js +1 -0
  275. data/docs/assets/{ai.md._6HCDL6d.js → ai.md.Cy9GWnER.js} +1 -1
  276. data/docs/assets/ai.md.Cy9GWnER.lean.js +1 -0
  277. data/docs/assets/{app.BhrfSt68.js → app.Dm3x-DQc.js} +1 -1
  278. data/docs/assets/{assets.md.D3wunzLx.js → assets.md.7C3HWkga.js} +3 -3
  279. data/docs/assets/{assets.md.D3wunzLx.lean.js → assets.md.7C3HWkga.lean.js} +1 -1
  280. data/docs/assets/{brut-js.md.o2DAO2s2.js → brut-js.md.B4GYxQVw.js} +1 -1
  281. data/docs/assets/{brut-js.md.o2DAO2s2.lean.js → brut-js.md.B4GYxQVw.lean.js} +1 -1
  282. data/docs/assets/chunks/@localSearchIndexroot.BqRrkR00.js +1 -0
  283. data/docs/assets/chunks/{VPLocalSearchBox.Dpot_2H4.js → VPLocalSearchBox.DL6bnqee.js} +1 -1
  284. data/docs/assets/chunks/{theme.N2SNVLgU.js → theme.BXdlf6e8.js} +2 -2
  285. data/docs/assets/{cli.md.RmeA2b0i.js → cli.md.CjsktgFz.js} +15 -20
  286. data/docs/assets/components.md.Pg_Lo35G.js +96 -0
  287. data/docs/assets/{components.md.CRUMdRoN.lean.js → components.md.Pg_Lo35G.lean.js} +1 -1
  288. data/docs/assets/{configuration.md.LG-zIBww.js → configuration.md.BfeGnEci.js} +3 -3
  289. data/docs/assets/{css.md.DJgj2clw.js → css.md.CltvJqAa.js} +3 -3
  290. data/docs/assets/{custom-element-tests.md.BrYJQEl3.js → custom-element-tests.md.B_rbta32.js} +3 -3
  291. data/docs/assets/{database-access.md.C7l-Vuvb.js → database-access.md.gnluu54N.js} +1 -1
  292. data/docs/assets/{database-schema.md.BUjR0VS1.js → database-schema.md.CSYk6E6v.js} +6 -6
  293. data/docs/assets/{database-schema.md.BUjR0VS1.lean.js → database-schema.md.CSYk6E6v.lean.js} +1 -1
  294. data/docs/assets/dev-environment.md.Dy6EldaM.js +16 -0
  295. data/docs/assets/dev-environment.md.Dy6EldaM.lean.js +1 -0
  296. data/docs/assets/dir-structure.md.CWir1pic.js +46 -0
  297. data/docs/assets/dir-structure.md.CWir1pic.lean.js +1 -0
  298. data/docs/assets/doc-conventions.md.DOkAuXlt.js +1 -0
  299. data/docs/assets/doc-conventions.md.DOkAuXlt.lean.js +1 -0
  300. data/docs/assets/{end-to-end-tests.md.yfQHC0b5.js → end-to-end-tests.md.DzqRpZ43.js} +5 -3
  301. data/docs/assets/end-to-end-tests.md.DzqRpZ43.lean.js +1 -0
  302. data/docs/assets/features.md.DPFXsy0z.js +154 -0
  303. data/docs/assets/features.md.DPFXsy0z.lean.js +1 -0
  304. data/docs/assets/flash-and-session.md.nPvUpnUx.js +79 -0
  305. data/docs/assets/{flash-and-session.md.BXY8RvT0.lean.js → flash-and-session.md.nPvUpnUx.lean.js} +1 -1
  306. data/docs/assets/form-constraints.md.x5tNpTTI.js +90 -0
  307. data/docs/assets/form-constraints.md.x5tNpTTI.lean.js +1 -0
  308. data/docs/assets/forms.md.BQZlCwvi.js +64 -0
  309. data/docs/assets/forms.md.BQZlCwvi.lean.js +1 -0
  310. data/docs/assets/{getting-started.md.Dj0qtZI2.js → getting-started.md.BcXnNuD6.js} +5 -5
  311. data/docs/assets/{getting-started.md.Dj0qtZI2.lean.js → getting-started.md.BcXnNuD6.lean.js} +1 -1
  312. data/docs/assets/handlers.md.Chyri6KA.js +54 -0
  313. data/docs/assets/handlers.md.Chyri6KA.lean.js +1 -0
  314. data/docs/assets/{hooks.md.C4-moMny.js → hooks.md.Jmb5VOLA.js} +4 -4
  315. data/docs/assets/{hooks.md.C4-moMny.lean.js → hooks.md.Jmb5VOLA.lean.js} +1 -1
  316. data/docs/assets/{i18n.md.Do9i1qWl.js → i18n.md.xQhiGo1G.js} +2 -2
  317. data/docs/assets/{i18n.md.Do9i1qWl.lean.js → i18n.md.xQhiGo1G.lean.js} +1 -1
  318. data/docs/assets/index.md.Bn9e0sRJ.js +1 -0
  319. data/docs/assets/index.md.Bn9e0sRJ.lean.js +1 -0
  320. data/docs/assets/{instrumentation.md.a9Pjps4P.js → instrumentation.md.BgcaGVYH.js} +2 -2
  321. data/docs/assets/{instrumentation.md.a9Pjps4P.lean.js → instrumentation.md.BgcaGVYH.lean.js} +1 -1
  322. data/docs/assets/{javascript.md.GWbhRS51.js → javascript.md.DzrMxUmI.js} +7 -7
  323. data/docs/assets/{javascript.md.GWbhRS51.lean.js → javascript.md.DzrMxUmI.lean.js} +1 -1
  324. data/docs/assets/keyword-injection.md.95Zgh2eN.js +21 -0
  325. data/docs/assets/{keyword-injection.md.Dt2tKREs.lean.js → keyword-injection.md.95Zgh2eN.lean.js} +1 -1
  326. data/docs/assets/{layouts.md.cPnh3NId.js → layouts.md.CJGDFY-m.js} +2 -15
  327. data/docs/assets/layouts.md.CJGDFY-m.lean.js +1 -0
  328. data/docs/assets/{lsp.md.Bsu-f6VU.js → lsp.md.Dn1rIiW0.js} +1 -1
  329. data/docs/assets/{lsp.md.Bsu-f6VU.lean.js → lsp.md.Dn1rIiW0.lean.js} +1 -1
  330. data/docs/assets/overview.md.iMnwLO4x.js +1 -0
  331. data/docs/assets/overview.md.iMnwLO4x.lean.js +1 -0
  332. data/docs/assets/pages.md.B7Hc-i6H.js +45 -0
  333. data/docs/assets/pages.md.B7Hc-i6H.lean.js +1 -0
  334. data/docs/assets/recipes_alternate-layouts.md.BwEytl59.js +22 -0
  335. data/docs/assets/recipes_alternate-layouts.md.BwEytl59.lean.js +1 -0
  336. data/docs/assets/recipes_authentication.md.Dzvi_g69.js +156 -0
  337. data/docs/assets/recipes_authentication.md.Dzvi_g69.lean.js +1 -0
  338. data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.js +15 -0
  339. data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.lean.js +1 -0
  340. data/docs/assets/recipes_custom-flash.md.CrQbI5eH.js +26 -0
  341. data/docs/assets/recipes_custom-flash.md.CrQbI5eH.lean.js +1 -0
  342. data/docs/assets/recipes_indexed-forms.md.CstYyOSo.js +74 -0
  343. data/docs/assets/recipes_indexed-forms.md.CstYyOSo.lean.js +1 -0
  344. data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.js +101 -0
  345. data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.lean.js +1 -0
  346. data/docs/assets/roadmap.md.C6PRi0DX.js +1 -0
  347. data/docs/assets/roadmap.md.C6PRi0DX.lean.js +1 -0
  348. data/docs/assets/routes.md.B8kfUPHU.js +21 -0
  349. data/docs/assets/{routes.md.BMM7peut.lean.js → routes.md.B8kfUPHU.lean.js} +1 -1
  350. data/docs/assets/{security.md.C668yXCi.js → security.md.C0G_AZR-.js} +1 -1
  351. data/docs/assets/{security.md.C668yXCi.lean.js → security.md.C0G_AZR-.lean.js} +1 -1
  352. data/docs/assets/space-time-continuum.md.xl44xDos.js +1 -0
  353. data/docs/assets/{space-time-continuum.md.KPUIKysQ.lean.js → space-time-continuum.md.xl44xDos.lean.js} +1 -1
  354. data/docs/assets/{style.B2o1L9eN.css → style.B1z60PPQ.css} +1 -1
  355. data/docs/assets/tutorial.md.BYXj4cOu.js +1 -0
  356. data/docs/assets/tutorial.md.BYXj4cOu.lean.js +1 -0
  357. data/docs/assets/why.md.C-hk5xgJ.js +1 -0
  358. data/docs/assets/why.md.C-hk5xgJ.lean.js +1 -0
  359. data/docs/assets.html +12 -7
  360. data/docs/brut-js/api/AjaxSubmit.html +1 -1
  361. data/docs/brut-js/api/AjaxSubmit.js.html +1 -1
  362. data/docs/brut-js/api/Autosubmit.html +1 -1
  363. data/docs/brut-js/api/Autosubmit.js.html +1 -1
  364. data/docs/brut-js/api/BaseCustomElement.html +1 -1
  365. data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
  366. data/docs/brut-js/api/BrutCustomElements.html +1 -1
  367. data/docs/brut-js/api/BufferedLogger.html +1 -1
  368. data/docs/brut-js/api/ConfirmSubmit.html +1 -1
  369. data/docs/brut-js/api/ConfirmSubmit.js.html +1 -1
  370. data/docs/brut-js/api/ConfirmationDialog.html +1 -1
  371. data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
  372. data/docs/brut-js/api/ConstraintViolationMessage.html +1 -1
  373. data/docs/brut-js/api/ConstraintViolationMessage.js.html +1 -1
  374. data/docs/brut-js/api/ConstraintViolationMessages.html +1 -1
  375. data/docs/brut-js/api/ConstraintViolationMessages.js.html +1 -1
  376. data/docs/brut-js/api/CopyToClipboard.html +1 -1
  377. data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
  378. data/docs/brut-js/api/Form.html +1 -1
  379. data/docs/brut-js/api/Form.js.html +1 -1
  380. data/docs/brut-js/api/I18nTranslation.html +1 -1
  381. data/docs/brut-js/api/I18nTranslation.js.html +1 -1
  382. data/docs/brut-js/api/LocaleDetection.html +1 -1
  383. data/docs/brut-js/api/LocaleDetection.js.html +1 -1
  384. data/docs/brut-js/api/Logger.html +1 -1
  385. data/docs/brut-js/api/Logger.js.html +1 -1
  386. data/docs/brut-js/api/Message.html +1 -1
  387. data/docs/brut-js/api/Message.js.html +1 -1
  388. data/docs/brut-js/api/PrefixedLogger.html +1 -1
  389. data/docs/brut-js/api/RichString.html +1 -1
  390. data/docs/brut-js/api/RichString.js.html +1 -1
  391. data/docs/brut-js/api/Tabs.html +1 -1
  392. data/docs/brut-js/api/Tabs.js.html +1 -1
  393. data/docs/brut-js/api/Tracing.html +1 -1
  394. data/docs/brut-js/api/Tracing.js.html +1 -1
  395. data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
  396. data/docs/brut-js/api/external-Performance.html +1 -1
  397. data/docs/brut-js/api/external-Promise.html +1 -1
  398. data/docs/brut-js/api/external-ValidityState.html +1 -1
  399. data/docs/brut-js/api/external-Window.html +1 -1
  400. data/docs/brut-js/api/external-fetch.html +1 -1
  401. data/docs/brut-js/api/global.html +1 -1
  402. data/docs/brut-js/api/index.html +1 -1
  403. data/docs/brut-js/api/index.js.html +1 -1
  404. data/docs/brut-js/api/module-testing.html +1 -1
  405. data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
  406. data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
  407. data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
  408. data/docs/brut-js/api/testing.DOMCreator.html +1 -1
  409. data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
  410. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
  411. data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
  412. data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
  413. data/docs/brut-js/api/testing_index.js.html +1 -1
  414. data/docs/brut-js.html +12 -7
  415. data/docs/business-logic.html +10 -5
  416. data/docs/cli.html +26 -26
  417. data/docs/components.html +61 -64
  418. data/docs/configuration.html +13 -8
  419. data/docs/css.html +14 -9
  420. data/docs/custom-element-tests.html +14 -9
  421. data/docs/database-access.html +12 -7
  422. data/docs/database-schema.html +15 -10
  423. data/docs/deployment.html +10 -5
  424. data/docs/dev-environment.html +17 -7
  425. data/docs/dir-structure.html +74 -0
  426. data/docs/doc-conventions.html +11 -6
  427. data/docs/end-to-end-tests.html +15 -8
  428. data/docs/favicon.ico +0 -0
  429. data/docs/features.html +182 -0
  430. data/docs/flash-and-session.html +73 -82
  431. data/docs/form-constraints.html +118 -0
  432. data/docs/forms.html +57 -367
  433. data/docs/getting-started.html +15 -10
  434. data/docs/handlers.html +51 -61
  435. data/docs/hashmap.json +1 -1
  436. data/docs/hooks.html +14 -9
  437. data/docs/i18n.html +12 -7
  438. data/docs/index.html +11 -6
  439. data/docs/instrumentation.html +12 -7
  440. data/docs/javascript.html +17 -12
  441. data/docs/jobs.html +10 -5
  442. data/docs/keyword-injection.html +22 -21
  443. data/docs/layouts.html +12 -20
  444. data/docs/lsp.html +11 -6
  445. data/docs/markdown-examples.html +10 -5
  446. data/docs/middleware.html +10 -5
  447. data/docs/overview.html +11 -138
  448. data/docs/pages.html +49 -121
  449. data/docs/recipes/alternate-layouts.html +50 -0
  450. data/docs/recipes/authentication.html +166 -6
  451. data/docs/recipes/blank-layouts.html +43 -0
  452. data/docs/recipes/custom-flash.html +54 -0
  453. data/docs/recipes/indexed-forms.html +102 -0
  454. data/docs/recipes/text-field-component.html +129 -0
  455. data/docs/roadmap.html +29 -0
  456. data/docs/routes.html +16 -19
  457. data/docs/security.html +11 -6
  458. data/docs/seed-data.html +10 -5
  459. data/docs/space-time-continuum.html +11 -6
  460. data/docs/tutorial.html +11 -6
  461. data/docs/unit-tests.html +10 -5
  462. data/docs/why.html +29 -0
  463. data/lib/brut/cli/apps/test.rb +1 -1
  464. data/lib/brut/front_end/components/inputs/select_tag_with_options.rb +2 -2
  465. data/lib/brut/front_end/form.rb +8 -8
  466. data/lib/brut/front_end/forms/radio_button_group_input.rb +8 -1
  467. data/lib/brut/front_end/forms/select_input.rb +8 -1
  468. data/lib/brut/junk_drawer.rb +48 -9
  469. data/lib/brut/version.rb +1 -1
  470. data/specs/brut/front_end/forms/radio_button_group_input.spec.rb +54 -0
  471. data/specs/brut/front_end/forms/select_input.spec.rb +54 -0
  472. data/specs/brut/junk_drawer.spec.rb +75 -0
  473. metadata +129 -82
  474. data/brutrb.com/images/logo-300.png +0 -0
  475. data/brutrb.com/images/logo.png +0 -0
  476. data/brutrb.com/not-released.md +0 -5
  477. data/brutrb.com/public/images/logo-300.png +0 -0
  478. data/brutrb.com/public/images/logo.png +0 -0
  479. data/docs/assets/LogoStop.X8x-4riz.png +0 -0
  480. data/docs/assets/ai.md._6HCDL6d.lean.js +0 -1
  481. data/docs/assets/chunks/@localSearchIndexroot.CeRAdP1K.js +0 -1
  482. data/docs/assets/components.md.CRUMdRoN.js +0 -104
  483. data/docs/assets/dev-env-overview.Gj7NWM8-.png +0 -0
  484. data/docs/assets/dev-environment.md.GZv6xvi9.js +0 -11
  485. data/docs/assets/dev-environment.md.GZv6xvi9.lean.js +0 -1
  486. data/docs/assets/doc-conventions.md.-kN3Xo5C.js +0 -1
  487. data/docs/assets/doc-conventions.md.-kN3Xo5C.lean.js +0 -1
  488. data/docs/assets/end-to-end-tests.md.yfQHC0b5.lean.js +0 -1
  489. data/docs/assets/flash-and-session.md.BXY8RvT0.js +0 -93
  490. data/docs/assets/forms.md.B-koVgyw.js +0 -379
  491. data/docs/assets/forms.md.B-koVgyw.lean.js +0 -1
  492. data/docs/assets/handlers.md.089DVD3v.js +0 -69
  493. data/docs/assets/handlers.md.089DVD3v.lean.js +0 -1
  494. data/docs/assets/index.md.CuBB-BdM.js +0 -1
  495. data/docs/assets/index.md.CuBB-BdM.lean.js +0 -1
  496. data/docs/assets/keyword-injection.md.Dt2tKREs.js +0 -25
  497. data/docs/assets/layouts.md.cPnh3NId.lean.js +0 -1
  498. data/docs/assets/not-released.md.BBy28McC.js +0 -1
  499. data/docs/assets/not-released.md.BBy28McC.lean.js +0 -1
  500. data/docs/assets/overview.md.DVKRM8zl.js +0 -133
  501. data/docs/assets/overview.md.DVKRM8zl.lean.js +0 -1
  502. data/docs/assets/pages.md.BE3kfOc5.js +0 -122
  503. data/docs/assets/pages.md.BE3kfOc5.lean.js +0 -1
  504. data/docs/assets/recipes_authentication.md.CAsXf7hk.js +0 -1
  505. data/docs/assets/recipes_authentication.md.CAsXf7hk.lean.js +0 -1
  506. data/docs/assets/routes.md.BMM7peut.js +0 -29
  507. data/docs/assets/space-time-continuum.md.KPUIKysQ.js +0 -1
  508. data/docs/assets/tutorial.md.BnoGjrdK.js +0 -1
  509. data/docs/assets/tutorial.md.BnoGjrdK.lean.js +0 -1
  510. data/docs/images/logo-300.png +0 -0
  511. data/docs/images/logo.png +0 -0
  512. data/docs/not-released.html +0 -24
  513. /data/docs/assets/{cli.md.RmeA2b0i.lean.js → cli.md.CjsktgFz.lean.js} +0 -0
  514. /data/docs/assets/{configuration.md.LG-zIBww.lean.js → configuration.md.BfeGnEci.lean.js} +0 -0
  515. /data/docs/assets/{css.md.DJgj2clw.lean.js → css.md.CltvJqAa.lean.js} +0 -0
  516. /data/docs/assets/{custom-element-tests.md.BrYJQEl3.lean.js → custom-element-tests.md.B_rbta32.lean.js} +0 -0
  517. /data/docs/assets/{database-access.md.C7l-Vuvb.lean.js → database-access.md.gnluu54N.lean.js} +0 -0
@@ -1,205 +1,189 @@
1
1
  # Flash and Session
2
2
 
3
- Brut sessions are stored in cookies, encrypted to prevent tampering. The *flash*, which is a way to temporarily
4
- store small bits of information between page loads, is encoded in the session.
3
+ Brut sessions are stored in cookies, encrypted to prevent tampering. The *flash*, which is a way to temporarily store small bits of information between page loads, is encoded in the session.
5
4
 
6
5
  ## Overview
7
6
 
8
- Unlike Rails, the session and flash are presented to you as objects, not Hashes. By declaring the `session:`
9
- parameter on an initializer, you'll be given the current session for the request as an `AppSession`, which
10
- inherits from `Brut::FrontEnd::Session`. Similarly, declaring `flash:`, you'll get a `Brut::FrontEnd::Flash`.
7
+ Unlike Rails, the session and flash are presented to you as objects, not Hashes of Whatever. By declaring the `session:`
8
+ parameter on an initializer, you'll be given the current session for the request as an `AppSession`, which inherits from `Brut::FrontEnd::Session`. Similarly, declaring `flash:`, you'll get a `Brut::FrontEnd::Flash`.
11
9
 
12
10
  The idea is to use Ruby's type system to describe what data is in the session and flash.
13
11
 
14
12
  ### Session
15
13
 
16
- Brut's session is somewhat richer than you might get from other frameworks. In particular, the session can
17
- provide you:
14
+ Brut's session is somewhat richer than you might get from other frameworks. In particular, the session can provide you:
18
15
 
19
16
  * The current `Brut::I18n::HTTPAcceptLanguage`, which is the visitor's locale. See [I18n](/i18n) for how this
20
17
  works and how to use this value.
21
18
  * The timezone as provided by the browser.
22
19
  * An explicitly-set timezone that may or may not be what the browser provided. See [Space-Time Continuum](/space-time-continuum) for more details.
23
20
 
24
- The session also handles serializing the flash to and from the browser's cookies and can store any arbitrary data
25
- you like via `[]`. You are encouraged to add methods to your app's `AppSession` to make it explicit what you are
26
- storing.
21
+ To access the session, declare it as a keyword argument to your page, handler, or
22
+ global component's intitializer:
27
23
 
28
- Let's see the [route hook](/hooks) from the [pages](/pages) section again.
24
+ ```ruby
25
+ class HomePage < AppPage
26
+ def initialize(session:)
27
+ @session = session
28
+ end
29
+ end
30
+ ```
31
+
32
+ When you create your Brut app, your `AppSession` won't have anything in it, although
33
+ it's a `Brut::FrontEnd::Session`, so you can certainly use `[]` and `[]=` on it.
34
+ However, you are encouraged to declare methods that describe precisely what is in
35
+ the session.
29
36
 
30
- > [!CAUTION]
31
- > This hook is not production-ready. It lacks certain error-handling situations and
32
- > makes an assumption about how the session is managed. It's for demonstration only.
33
- > The [route hooks](/hooks) section has a more
34
- > appropriate example.
37
+ Let's say the currently logged-in visitor is available in the session. Your
38
+ `HomePage` could look like so:
35
39
 
36
40
  ```ruby
37
- class RequireAuthBeforeHook < Brut::FrontEnd::RouteHook
38
- def before(request_context:,session:)
39
- if session.current_user_id
40
- user = DB::User.find(id: session.current_user_id)
41
- if user
42
- request_context[:current_user] = user
41
+ class HomePage < AppPage
42
+ def initialize(session:)
43
+ @session = session
44
+ end
45
+
46
+ def view_template
47
+ h1 do
48
+ if @session.current_visitor
49
+ "Hello #{@session.current_visitor.name}"
50
+ else
51
+ "Hi!"
43
52
  end
44
53
  end
45
54
  end
46
55
  end
47
56
  ```
48
57
 
49
- When this hook executes, `session` will be an `AppSession`, serialized from the browser's cookies. Here's what
50
- that class might look like:
58
+ Let's suppose a `LoginHandler` exists, that can set a value for `current_visitor`:
51
59
 
52
60
  ```ruby
53
- # app/src/front_end/support/app_session.rb
54
- class AppSession < Brut::FrontEnd::Session
55
- def login!(current_user:)
56
- self[:current_user_id] = current_user.id
57
- end
58
-
59
- def logout!
60
- self[:current_user_id] = nil
61
+ class LoginHandler < AppHandler
62
+ def initialize(form:, session:)
63
+ @form = form
64
+ @session = session
61
65
  end
62
66
 
63
- def logged_in?
64
- !!self.current_user_id
67
+ def handle
68
+ visitor = Login.from_form(form:) # assume this exists
69
+ if visitor
70
+ @sesion.login!(visitor:)
71
+ else
72
+ # ...
73
+ end
65
74
  end
66
-
67
- def current_user_id = self[:current_user_id]
68
75
  end
69
76
  ```
70
77
 
71
- The session is a rich object and not just a thin wrapper over a Hash. You could even have the session perform
72
- the lookup in the database:
78
+ `AppSession` would need to look like so:
73
79
 
74
- ```ruby {11,14-16}
75
- # app/src/front_end/support/app_session.rb
80
+ ```ruby
76
81
  class AppSession < Brut::FrontEnd::Session
77
- def login!(current_user:)
78
- self[:current_user_id] = current_user.id
79
- end
80
- def logout!
81
- self[:current_user_id] = nil
82
+ def login!(visitor:)
83
+ self[:current_visitor_id] = visitor.id
82
84
  end
83
85
 
84
- def logged_in?
85
- !!self.current_user
86
- end
87
-
88
- def current_user
89
- DB::User.find(id: self[:current_user_id])
86
+ def current_visitor
87
+ DB::Visitor.find(id: self[:current_visitor_id])
90
88
  end
91
89
  end
92
90
  ```
93
91
 
94
- Now, the hook could call `current_user`:
92
+ Brut encourages your session to be a rich object. You can declare any methods you
93
+ like:
95
94
 
96
- ```ruby {3-5}
97
- class RequireAuthBeforeHook < Brut::FrontEnd::RouteHook
98
- def before(request_context:,session:)
99
- if session.logged_in?
100
- request_context[:current_user] = session.current_user
101
- end
102
- end
95
+ ```ruby
96
+ class AppSession < Brut::FrontEnd::Session
97
+ def logged_in? = !!self.current_visitor
103
98
  end
104
99
  ```
105
100
 
106
- Let's see `LoginHandler` from the [handlers](/handlers) section, to see how to save the current user. Given what
107
- we've learned, the declaration of the `session:` parameter to the initializer means the relevant instance of
108
- `AppSession` will be passed in.
101
+ > [!NOTE]
102
+ > When dealing with auth, you can leverage
103
+ > keyword injection beyond injecting the session. This is
104
+ > discussed in [the auth recipe](/recipes/authentication.md)
109
105
 
110
- ```ruby {20}
111
- # app/src/front_end/handlers/login_handler.rb
112
- class LoginHandler < AppHandler
113
- def initialize(form:, session:)
114
- @form = form
115
- @session = session
116
- end
106
+ ### Flash
117
107
 
118
- def handle
119
- if !@form.constraint_violations?
120
- authorized_user = AuthorizedUser.login(
121
- email: form.email,
122
- password: form.password
123
- )
124
- if authorized_user.nil?
125
- @form.server_side_constraint_violation(
126
- input_name: :email,
127
- key: :login_not_found
128
- )
129
- else
130
- session.login!(current_user: authorized_user.user)
131
- end
132
- end
133
- if @form.constraint_violations?
134
- LoginPage.new(form: @form)
135
- else
136
- redirect_to(DashboardPage.routing)
137
- end
108
+ To access the flash, declare it as a keyword argument to your page, handler, or
109
+ global component's intitializer:
110
+
111
+ ```ruby {2,4}
112
+ class DeleteWidgetByIdHandler < AppHandler
113
+ def initialize(widget_id:, flash:)
114
+ @widget_id = widget_id
115
+ @flash = flash
138
116
  end
139
117
  end
140
118
  ```
141
119
 
142
- Brut will handle saving the updated values in the response so when, in this case, the `DashboardPage` is
143
- rendered, it can see which user is logged in.
144
-
145
- ### Flash
146
-
147
- By default, your app will use Brut's flash class, `Brut::FrontEnd::Flash`. This is because you typically don't
148
- need to enhance the flash. Brut's flash has an "alert" and "notice", and you can use them however you see fit. You can also set arbitrary messages in the flash via `[]`.
120
+ By default, the flash will be a `Brut::FrontEnd::Flash`. While you can set your own
121
+ class, this is less commonly needed, so Brut doesn't provide one by default. Like
122
+ the session, you can use `[]`, but are discouraged from this to avoid Hashes of
123
+ Whatever littering your code.
149
124
 
150
- The contents of the flash only survive one request, so anything you set will be available in that session's next
151
- request, but not after that.
125
+ The default flash provides a `notice` attribute and an `alert` attribute. Their
126
+ values only survive one request, so anything you set will be available in that session's next request, but not after that.
152
127
 
153
- Note that the flash's alert and notice are intended to be I18n keys. You don't have to use them this way, but it
154
- is encouraged. If you pass an array into `alert=` or `notice=`, the elements will be joined to form an I18n key.
128
+ The values are expected to be I18n keys:
155
129
 
156
- You can create your own subclass if you need a richer flash class than the one Brut provides.
157
-
158
- First, create your class. It can be anywhere, but we recommend `app/src/front_end/support/app_flash.rb`:
159
-
160
- ```ruby
161
- # app/src/front_end/support/app_flash.rb
162
- class AppFlash < Brut::FrontEnd::Flash
163
- # For example
164
- def debug=(debug)
165
- self[:debug] = debug
130
+ ```ruby {5,8}
131
+ def handle
132
+ widget = DB::Widget.find!(id: @widget_id)
133
+ if widget.can_delete?
134
+ widget.delete
135
+ @flash.notice = :widget_deleted
136
+ redirect_to(WidgetsPage)
137
+ else
138
+ @flash.alert = :widget_cannot_be_deleted
139
+ WidgetsPage.new
140
+ end
166
141
  end
167
- def debug = self[:debug]
168
- def debug? = !!self.debug
169
142
  end
170
143
  ```
171
144
 
172
- Then, in your `App`, located in `app/src/app.rb`, use `Brut.container.override` to change the class used for the
173
- flash:
174
-
175
- ```ruby
176
- class App < Brut::Framework::App
177
- # ...
178
- def initialize
179
- Brut.container.override(
180
- "flash_class",
181
- AppFlash
182
- )
145
+ This is only enforced by convention, but you should stick to one convention since
146
+ you will likely create a [global component](/components) for the flash:
147
+
148
+ ```ruby {17}
149
+ class FlashComponent < AppComponent
150
+ def initialize(flash:)
151
+ if flash.notice?
152
+ @message_key = flash.notice
153
+ @role = :info
154
+ elsif flash.alert?
155
+ @message_key = flash.alert
156
+ @role = :alert
157
+ end
183
158
  end
184
159
 
185
- # ...
160
+ def any_message? = !@message_key.nil?
161
+
162
+ def view_template
163
+ if any_message?
164
+ div(role: @role) do
165
+ t([ :flash, @message_key ])
166
+ end
167
+ end
168
+ end
186
169
  end
187
170
  ```
188
171
 
189
- Brut's configuration system is discussed in more detail in [Configuration](/configuration).
172
+ See [using your own Flash class](/recipes/custom-flash) to see how to enhance Brut's
173
+ flash with your own logic.
190
174
 
191
175
  ## Testing
192
176
 
193
177
  Testing your session or flash classes may not be super valuable, however they are normal Ruby objects so you can
194
- test them in a conventional way. Both classes treat their internals as a Hash, so you can implement and assert
195
- via the `[]` and `[]=` methods.
178
+ test them in a conventional way. Although you are discouraged from using `[]` and
179
+ `[]=` as the public API of your session or flash, they can be useful for assertions
180
+ or test setup.
196
181
 
197
182
  ## Recommended Practices
198
183
 
199
- While you can use both the flash and the session has a hash of whatever, your are encouraged to avoid this in
200
- your production code. Create well-defined attributes or methods to manipulate these objects using the language
201
- of your domain.
202
-
184
+ Do not treat the session or flash as a Hash of Whatever. Isolate all magic keys to
185
+ the class and provide a rich API. It doesn't take that much effort and will make
186
+ your app way easier to manage.
203
187
 
204
188
  ## Technical Notes
205
189
 
@@ -0,0 +1,266 @@
1
+ # Form Constraint Validations
2
+
3
+ Aside from simply collecting data and submitting it to the server, form data has
4
+ *constraints* that must be validated before data is accepted. Brut provides support
5
+ for both client-side and server-side constraints.
6
+
7
+ ## Overview
8
+
9
+ When validating form data against its constraints, Brut provides assistance in two
10
+ ways:
11
+
12
+ * Specifying constraint violations that only the server can evaluate.
13
+ * Unifying the user experience for both client-side and server-side constraint violations.
14
+
15
+ ### Specifying Constraints
16
+
17
+ For both client and server-side constraint violations, Brut uses the
18
+ `Brut::FrontEnd::Forms::ConstraintViolation` class to represent a specific error on
19
+ a specific field. This class is a wrapper around an i18n key, context to generate
20
+ that key's messaging, and a flag indicating if the violation is server or client
21
+ side.
22
+
23
+ To specify a server-side constraint violation on a form, call
24
+ `server_side_constraint_violation`:
25
+
26
+ ```ruby
27
+ form.server_side_constraint_violation(
28
+ input_name: :name,
29
+ key: :name_is_taken
30
+ )
31
+ ```
32
+
33
+ The `input_name` is the same value you used when creating your form class, and `key`
34
+ is an [I18n](/i18n) key that will have `cv.be` prepended to it (for **c*onstratin **v**iolation, **b**ack **e**nd). Thus, the key in the above example is `"cv.be.name_is_taken"`.
35
+
36
+ Brut forms will automatically add client-side constraints based on the value
37
+ assigned to the input. For example, since `name` must be 3 or more characters, this
38
+ code would implicitly set `:rangeOverflow` as a client-side constraint violation:
39
+
40
+ ```ruby
41
+ form.input(:name).value = "xx"
42
+ ```
43
+
44
+ ### Accessing Constraints when Generating HTML
45
+
46
+ `Brut::FrontEnd::Form` provides the method `constraint_violations` to access the
47
+ constraints, however we recommend using the
48
+ `Brut::FrontEnd::Components::ConstraintViolations` component instead. This component
49
+ generates particular markup useful for unifying the UX around constraint violations,
50
+ which we'll discuss in a moment.
51
+
52
+ ```ruby {13,16,19}
53
+ class NewWidgetPage < AppPage
54
+ include Brut::FrontEnd::Components
55
+
56
+ def initialize(form: nil)
57
+ @form = form || NewWidgetForm.new
58
+ end
59
+
60
+ private attr_reader :form
61
+
62
+ def page_template
63
+ FormTag(for: form) do
64
+ Components::InputTag(form:, input_name: :name)
65
+ Components::ConstraintViolations(form: input_name: :name)
66
+
67
+ Components::InputTag(form:, input_name: :quantity)
68
+ Components::ConstraintViolations(form: input_name: :quantity)
69
+
70
+ Components::TextareaTag(form:, input_name: :description)
71
+ Components::ConstraintViolations(form: input_name: :description)
72
+ end
73
+ end
74
+ end
75
+ ```
76
+
77
+ Among other things, `ConstraintViolations` will translate all server-side constraint
78
+ violations into the currently selected locale, if there are any.
79
+
80
+ ### Styling Server and Client-Side Constraint Violations
81
+
82
+ Without any server-side constraint violations, this is the HTML that would be
83
+ generated for the "name" input tag:
84
+
85
+ ```html
86
+ <input type="text" name="name" required minlength="3">
87
+ <brut-cv-messages input-name="name"></brut-cv-messages>
88
+ ```
89
+
90
+ `<brut-cv-messages>` is an autonomous custom element that serves two purposes:
91
+
92
+ * It is part of how client-side constraint violations are shown to the visitor.
93
+ * It can be used to target CSS for styling, without the need for `<div>` and `data-` elements. It's more explicitly for constraint violation messaging.
94
+
95
+ To make `<brut-cv-messages>` work with client-side constraint violations, the
96
+ `<form>` must be contained by a `<brut-form>`:
97
+
98
+ ```ruby {2,13}
99
+ def page_template
100
+ brut_form do
101
+ FormTag(for: form) do
102
+ Components::InputTag(form:, input_name: :name)
103
+ Components::ConstraintViolations(form: input_name: :name)
104
+
105
+ Components::InputTag(form:, input_name: :quantity)
106
+ Components::ConstraintViolations(form: input_name: :quantity)
107
+
108
+ Components::TextareaTag(form:, input_name: :description)
109
+ Components::ConstraintViolations(form: input_name: :description)
110
+ end
111
+ end
112
+ end
113
+ ```
114
+
115
+ `<brut-form>` listens for events from the `<form>` it contains. For an "invalid"
116
+ events, it will locate the element relevant to the event, locate its
117
+ `<brut-cv-messages>` tag, and insert one `<brut-cv>` tag for each error from the
118
+ inputs `ValidityState`. That may look like so:
119
+
120
+ ```html {3}
121
+ <input type="text" name="name" required minlength="3">
122
+ <brut-cv-messages input-name="name">
123
+ <brut-cv input-name="name" key="rangeUnderflow"></brut-cv>
124
+ </brut-cv-messages>
125
+ ```
126
+
127
+ They `key` attribute is for an I18n key that is expected to be on the page inside a
128
+ `<brut-i18n-translation>` element. These are typically included in the [layout](/layouts), and generate HTML like so:
129
+
130
+ ```html
131
+ <brut-i18n-translation key="cv.fe.rangeUnderflow"
132
+ value="%{field} is too short"></brut-i18n-translation>
133
+ ```
134
+
135
+ `<brut-cv>` will, whenever its `key` attribute is set or changed, locate the
136
+ corrsponding `<brut-i18n-translation>` element, and perform substitution, result in
137
+ this HTML:
138
+
139
+ ```html {4}
140
+ <input type="text" name="name" required minlength="3">
141
+ <brut-cv-messages input-name="name">
142
+ <brut-cv input-name="name" key="rangeUnderflow">
143
+ This field is too short
144
+ </brut-cv>
145
+ </brut-cv-messages>
146
+ ```
147
+
148
+ Presumably, your layout rendered `<brut-i18n-translation>` tags with the visitor's
149
+ chosen locale (which would be the default behavior of the layout included with a new app).
150
+
151
+ Coming back to the use of `ConstraintViolations`, if there were a server-side
152
+ violation, the same general markup is generated:
153
+
154
+ ```html {3,4}
155
+ <input type="text" name="name" required minlength="3">
156
+ <brut-cv-messages input-name="name">
157
+ <brut-cv server-side>
158
+ This name has already been taken.
159
+ </brut-cv>
160
+ </brut-cv-messages>
161
+ ```
162
+
163
+ The `server-side` attribute is set, which can help with CSS targeting.
164
+
165
+ The *last* piece of this puzzle is a solution for the issue where forms that have
166
+ not yet been submitted are considered to have invalid values by the browser.
167
+ `<brut-form>` will add the `submitted-invalid` attribute to itself whenever form
168
+ submission has been prevented by invalid attributes.
169
+
170
+ This might lead to HTML like so:
171
+
172
+ ```html {1}
173
+ <brut-form submitted-invalid>
174
+ <form ...>
175
+
176
+ <!-- .. -->
177
+
178
+ <input type="text" name="name" required minlength="3">
179
+ <brut-cv-messages input-name="name">
180
+ <brut-cv input-name="name" key="rangeUnderflow">
181
+ This field is too short
182
+ </brut-cv>
183
+ </brut-cv-messages>
184
+
185
+ <!-- ... -->
186
+
187
+ </form>
188
+ </brut-form>
189
+ ```
190
+
191
+ This is everything you need to style all constraint violations the same:
192
+
193
+ ```css
194
+ /* By default, brut-cv is hidden */
195
+ brut-cv {
196
+ display: none;
197
+ }
198
+
199
+ /* brut-cv inside a submitted-invalid
200
+ OR brut-cv from the server ARE shown */
201
+ brut-form[submitted-invalid] brut-cv,
202
+ brut-cv[server-side] {
203
+ display: block;
204
+ color: red; /* e.g. */
205
+ }
206
+ ```
207
+
208
+ If JavaScript is not enabled, everything degrades properly, as long as your handler
209
+ re-checks the client-side validations (we'll discuss in the next module):
210
+
211
+ ```ruby
212
+ def handle
213
+ # This will be true by virtue of the form's
214
+ # values having been set to values that violate
215
+ # one or more client-side violations.
216
+ if @form.constraint_violations?
217
+ # ...
218
+ end
219
+ end
220
+ ```
221
+
222
+ ## Testing
223
+
224
+ Testing client-side validations must be done with end-to-end tests. Writing code
225
+ like so will work just fine:
226
+
227
+ ```ruby
228
+ button = page.locator("brut-form button")
229
+ button.click
230
+
231
+ brut_cv = page.locator("brut-cv-messages[input-name='name'] brut-cv")
232
+ expect(brut_cv).to have_text("too short")
233
+ ```
234
+
235
+ Playwright will wait for the `brut-cv` containing the text "too short" to appear on
236
+ the page, so you should not have any race conditions.
237
+
238
+ ## Recommended Practices
239
+
240
+ ### Utility CSS is Tricky Here
241
+
242
+ Utility CSS like BrutCSS or TailwindCSS isn't well-suited to targeting elements
243
+ based on custom elements or attributes. You will need to write CSS or need to
244
+ create your own utility CSS for these situations.
245
+
246
+ In our opinion, writing CSS for something like this isn't a big deal as it can
247
+ reduce duplcation via the use of custom properties from your CSS library/design
248
+ system and it tends to be stable once created.
249
+
250
+ ### Learn to Be OK with the Browser's UX
251
+
252
+ One complain about client-side constraint violations is that the browser often
253
+ provides UX that you cannot control. This isn't ideal, but it does have the virtue
254
+ of being accessible and obvious. Visitors also really don't care about how ugly it
255
+ is as much as you might think. The utility and accessibility offset is as
256
+ worthwhile tradeoff.
257
+
258
+ ## Technical Notes
259
+
260
+ > [!IMPORTANT]
261
+ > Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's
262
+ > internals, the source code is always more correct.
263
+
264
+ _Last Updated July 6, 2025_
265
+
266
+ Nothing at this time.