brut 0.0.28 → 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 (532) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +6 -0
  3. data/.projections.json +10 -0
  4. data/.rspec +3 -0
  5. data/Dockerfile.dx +32 -14
  6. data/Gemfile.lock +1 -1
  7. data/README.md +23 -2
  8. data/assets/Logo-Square.pxd +0 -0
  9. data/assets/LogoPylon.pxd +0 -0
  10. data/assets/LogoStop.pxd +0 -0
  11. data/assets/LogoTall.pxd +0 -0
  12. data/assets/MetroLogo.graffle +0 -0
  13. data/assets/SocialImage.png +0 -0
  14. data/assets/SocialImage.pxd +0 -0
  15. data/bin/docs +24 -2
  16. data/bin/rspec +27 -0
  17. data/bin/setup +3 -3
  18. data/brutrb.com/.vitepress/config.mjs +45 -8
  19. data/brutrb.com/.vitepress/theme/custom.css +7 -0
  20. data/brutrb.com/.vitepress/theme/style.css +29 -17
  21. data/brutrb.com/ai.md +10 -15
  22. data/brutrb.com/assets.md +2 -9
  23. data/brutrb.com/brut-js.md +12 -2
  24. data/brutrb.com/cli.md +9 -13
  25. data/brutrb.com/components.md +118 -96
  26. data/brutrb.com/configuration.md +3 -4
  27. data/brutrb.com/css.md +2 -2
  28. data/brutrb.com/custom-element-tests.md +3 -4
  29. data/brutrb.com/database-access.md +1 -1
  30. data/brutrb.com/database-schema.md +29 -41
  31. data/brutrb.com/deployment.md +123 -45
  32. data/brutrb.com/dev-environment.md +7 -7
  33. data/brutrb.com/dir-structure.md +120 -0
  34. data/brutrb.com/doc-conventions.md +18 -15
  35. data/brutrb.com/dx +1 -0
  36. data/brutrb.com/end-to-end-tests.md +12 -10
  37. data/brutrb.com/features.md +373 -0
  38. data/brutrb.com/flash-and-session.md +115 -131
  39. data/brutrb.com/form-constraints.md +266 -0
  40. data/brutrb.com/forms.md +140 -765
  41. data/brutrb.com/getting-started.md +10 -11
  42. data/brutrb.com/handlers.md +119 -95
  43. data/brutrb.com/hooks.md +18 -20
  44. data/brutrb.com/i18n.md +6 -4
  45. data/brutrb.com/images/LogoPylon.png +0 -0
  46. data/brutrb.com/images/LogoSquare.png +0 -0
  47. data/brutrb.com/images/LogoStop.png +0 -0
  48. data/brutrb.com/images/LogoTall.png +0 -0
  49. data/brutrb.com/images/OverviewMetro.graffle +0 -0
  50. data/brutrb.com/images/OverviewMetro.png +0 -0
  51. data/brutrb.com/index.md +4 -3
  52. data/brutrb.com/instrumentation.md +7 -10
  53. data/brutrb.com/javascript.md +14 -14
  54. data/brutrb.com/keyword-injection.md +72 -114
  55. data/brutrb.com/layouts.md +20 -52
  56. data/brutrb.com/lsp.md +1 -1
  57. data/brutrb.com/overview.md +35 -377
  58. data/brutrb.com/pages.md +119 -207
  59. data/brutrb.com/public/SocialImage.png +0 -0
  60. data/brutrb.com/public/favicon.ico +0 -0
  61. data/brutrb.com/recipes/alternate-layouts.md +32 -0
  62. data/brutrb.com/recipes/authentication.md +315 -6
  63. data/brutrb.com/recipes/blank-layouts.md +22 -0
  64. data/brutrb.com/recipes/custom-flash.md +51 -0
  65. data/brutrb.com/recipes/indexed-forms.md +149 -0
  66. data/brutrb.com/recipes/text-field-component.md +182 -0
  67. data/brutrb.com/routes.md +56 -82
  68. data/brutrb.com/security.md +0 -3
  69. data/brutrb.com/space-time-continuum.md +8 -12
  70. data/brutrb.com/tutorial.md +1 -1
  71. data/brutrb.com/why.md +19 -0
  72. data/docker-compose.dx.yml +5 -2
  73. data/docs/404.html +8 -3
  74. data/docs/SocialImage.png +0 -0
  75. data/docs/ai.html +11 -6
  76. data/docs/api/Brut/BackEnd/SeedData.html +1 -1
  77. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +1 -1
  78. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +1 -1
  79. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +1 -1
  80. data/docs/api/Brut/BackEnd/Sidekiq.html +1 -1
  81. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +1 -1
  82. data/docs/api/Brut/BackEnd/Validators.html +1 -1
  83. data/docs/api/Brut/BackEnd.html +1 -1
  84. data/docs/api/Brut/CLI/App.html +1 -1
  85. data/docs/api/Brut/CLI/AppRunner.html +1 -1
  86. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +1 -1
  87. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +1 -1
  88. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +1 -1
  89. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +1 -1
  90. data/docs/api/Brut/CLI/Apps/BuildAssets.html +1 -1
  91. data/docs/api/Brut/CLI/Apps/DB/Create.html +1 -1
  92. data/docs/api/Brut/CLI/Apps/DB/Drop.html +1 -1
  93. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +1 -1
  94. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +1 -1
  95. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +1 -1
  96. data/docs/api/Brut/CLI/Apps/DB/Seed.html +1 -1
  97. data/docs/api/Brut/CLI/Apps/DB/Status.html +1 -1
  98. data/docs/api/Brut/CLI/Apps/DB.html +1 -1
  99. data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +1 -1
  100. data/docs/api/Brut/CLI/Apps/DeployBase.html +1 -1
  101. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +1 -1
  102. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +1 -1
  103. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +1 -1
  104. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +1 -1
  105. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +1 -1
  106. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +1 -1
  107. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +1 -1
  108. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +1 -1
  109. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +1 -1
  110. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +1 -1
  111. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +1 -1
  112. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +1 -1
  113. data/docs/api/Brut/CLI/Apps/Scaffold.html +1 -1
  114. data/docs/api/Brut/CLI/Apps/Test/Audit.html +1 -1
  115. data/docs/api/Brut/CLI/Apps/Test/E2e.html +1 -1
  116. data/docs/api/Brut/CLI/Apps/Test/JS.html +1 -1
  117. data/docs/api/Brut/CLI/Apps/Test/Run.html +1 -1
  118. data/docs/api/Brut/CLI/Apps/Test.html +1 -1
  119. data/docs/api/Brut/CLI/Apps.html +1 -1
  120. data/docs/api/Brut/CLI/Command.html +1 -1
  121. data/docs/api/Brut/CLI/Error.html +1 -1
  122. data/docs/api/Brut/CLI/ExecutionResults/Result.html +1 -1
  123. data/docs/api/Brut/CLI/ExecutionResults.html +1 -1
  124. data/docs/api/Brut/CLI/Executor.html +1 -1
  125. data/docs/api/Brut/CLI/InvalidOption.html +1 -1
  126. data/docs/api/Brut/CLI/Options.html +1 -1
  127. data/docs/api/Brut/CLI/Output.html +1 -1
  128. data/docs/api/Brut/CLI/SystemExecError.html +1 -1
  129. data/docs/api/Brut/CLI.html +1 -1
  130. data/docs/api/Brut/FactoryBot.html +1 -1
  131. data/docs/api/Brut/Framework/App.html +1 -1
  132. data/docs/api/Brut/Framework/Config.html +1 -1
  133. data/docs/api/Brut/Framework/Container.html +1 -1
  134. data/docs/api/Brut/Framework/Error.html +1 -1
  135. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +1 -1
  136. data/docs/api/Brut/Framework/Errors/Bug.html +1 -1
  137. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +1 -1
  138. data/docs/api/Brut/Framework/Errors/MissingParameter.html +1 -1
  139. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +1 -1
  140. data/docs/api/Brut/Framework/Errors/NotFound.html +1 -1
  141. data/docs/api/Brut/Framework/Errors/NotImplemented.html +1 -1
  142. data/docs/api/Brut/Framework/Errors.html +1 -1
  143. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +1 -1
  144. data/docs/api/Brut/Framework/MCP.html +1 -1
  145. data/docs/api/Brut/Framework/ProjectEnvironment.html +1 -1
  146. data/docs/api/Brut/Framework.html +1 -1
  147. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +1 -1
  148. data/docs/api/Brut/FrontEnd/Component/Helpers.html +1 -1
  149. data/docs/api/Brut/FrontEnd/Component.html +1 -1
  150. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +1 -1
  151. data/docs/api/Brut/FrontEnd/Components/FormTag.html +1 -1
  152. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +1 -1
  153. data/docs/api/Brut/FrontEnd/Components/Input.html +1 -1
  154. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +1 -1
  155. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +1 -1
  156. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +1 -1
  157. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +1 -1
  158. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +1 -1
  159. data/docs/api/Brut/FrontEnd/Components/Inputs.html +1 -1
  160. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +1 -1
  161. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +1 -1
  162. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +1 -1
  163. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +1 -1
  164. data/docs/api/Brut/FrontEnd/Components.html +1 -1
  165. data/docs/api/Brut/FrontEnd/Download.html +1 -1
  166. data/docs/api/Brut/FrontEnd/Flash.html +1 -1
  167. data/docs/api/Brut/FrontEnd/Form.html +9 -11
  168. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +1 -1
  169. data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +201 -0
  170. data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +535 -0
  171. data/docs/api/Brut/FrontEnd/Forms/Input.html +983 -35
  172. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +1 -1
  173. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +29 -19
  174. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +141 -20
  175. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +1 -1
  176. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +141 -20
  177. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +1 -1
  178. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +1 -1
  179. data/docs/api/Brut/FrontEnd/Forms.html +1 -1
  180. data/docs/api/Brut/FrontEnd/GenericResponse.html +1 -1
  181. data/docs/api/Brut/FrontEnd/Handler.html +1 -1
  182. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +1 -1
  183. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +1 -1
  184. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +1 -1
  185. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +1 -1
  186. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +1 -1
  187. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +1 -1
  188. data/docs/api/Brut/FrontEnd/Handlers.html +1 -1
  189. data/docs/api/Brut/FrontEnd/HandlingResults.html +1 -1
  190. data/docs/api/Brut/FrontEnd/HttpMethod.html +1 -1
  191. data/docs/api/Brut/FrontEnd/HttpStatus.html +1 -1
  192. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +1 -1
  193. data/docs/api/Brut/FrontEnd/Layout.html +1 -1
  194. data/docs/api/Brut/FrontEnd/Middleware.html +1 -1
  195. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +1 -1
  196. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +1 -1
  197. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +1 -1
  198. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +1 -1
  199. data/docs/api/Brut/FrontEnd/Middlewares.html +1 -1
  200. data/docs/api/Brut/FrontEnd/Page.html +1 -1
  201. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +1 -1
  202. data/docs/api/Brut/FrontEnd/Pages.html +1 -1
  203. data/docs/api/Brut/FrontEnd/RequestContext.html +1 -1
  204. data/docs/api/Brut/FrontEnd/RouteHook.html +1 -1
  205. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +1 -1
  206. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +1 -1
  207. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +1 -1
  208. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +1 -1
  209. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +1 -1
  210. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +1 -1
  211. data/docs/api/Brut/FrontEnd/RouteHooks.html +1 -1
  212. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +1 -1
  213. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +1 -1
  214. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +1 -1
  215. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +1 -1
  216. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +1 -1
  217. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +1 -1
  218. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +1 -1
  219. data/docs/api/Brut/FrontEnd/Routing/Route.html +1 -1
  220. data/docs/api/Brut/FrontEnd/Routing.html +1 -1
  221. data/docs/api/Brut/FrontEnd/Session.html +1 -1
  222. data/docs/api/Brut/FrontEnd.html +1 -1
  223. data/docs/api/Brut/I18n/BaseMethods.html +1 -1
  224. data/docs/api/Brut/I18n/ForBackEnd.html +1 -1
  225. data/docs/api/Brut/I18n/ForCLI.html +1 -1
  226. data/docs/api/Brut/I18n/ForHTML.html +1 -1
  227. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +1 -1
  228. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +1 -1
  229. data/docs/api/Brut/I18n.html +1 -1
  230. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +1 -1
  231. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +1 -1
  232. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +1 -1
  233. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +1 -1
  234. data/docs/api/Brut/Instrumentation.html +1 -1
  235. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +1 -1
  236. data/docs/api/Brut/SinatraHelpers.html +1 -1
  237. data/docs/api/Brut/SpecSupport/ClockSupport.html +1 -1
  238. data/docs/api/Brut/SpecSupport/ComponentSupport.html +1 -1
  239. data/docs/api/Brut/SpecSupport/E2ETestServer.html +1 -1
  240. data/docs/api/Brut/SpecSupport/E2eSupport.html +1 -1
  241. data/docs/api/Brut/SpecSupport/EnhancedNode.html +1 -1
  242. data/docs/api/Brut/SpecSupport/FlashSupport.html +1 -1
  243. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +1 -1
  244. data/docs/api/Brut/SpecSupport/GeneralSupport.html +1 -1
  245. data/docs/api/Brut/SpecSupport/HandlerSupport.html +1 -1
  246. data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +1 -1
  247. data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +1 -1
  248. data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +1 -1
  249. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +1 -1
  250. data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +1 -1
  251. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +1 -1
  252. data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +1 -1
  253. data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +1 -1
  254. data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +1 -1
  255. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +1 -1
  256. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +1 -1
  257. data/docs/api/Brut/SpecSupport/Matchers.html +1 -1
  258. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +1 -1
  259. data/docs/api/Brut/SpecSupport/RSpecSetup.html +1 -1
  260. data/docs/api/Brut/SpecSupport/SessionSupport.html +1 -1
  261. data/docs/api/Brut/SpecSupport.html +1 -1
  262. data/docs/api/Brut.html +1 -1
  263. data/docs/api/Clock.html +1 -1
  264. data/docs/api/RichString.html +1 -1
  265. data/docs/api/SemanticLogger/Appender/Async.html +1 -1
  266. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +1 -1
  267. data/docs/api/Sequel/Extensions/BrutMigrations.html +1 -1
  268. data/docs/api/Sequel/Extensions.html +1 -1
  269. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +1 -1
  270. data/docs/api/Sequel/Plugins/CreatedAt.html +1 -1
  271. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +1 -1
  272. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +1 -1
  273. data/docs/api/Sequel/Plugins/ExternalId.html +1 -1
  274. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +1 -1
  275. data/docs/api/Sequel/Plugins/FindBang.html +1 -1
  276. data/docs/api/Sequel/Plugins.html +1 -1
  277. data/docs/api/Sequel.html +1 -1
  278. data/docs/api/_index.html +15 -1
  279. data/docs/api/class_list.html +1 -1
  280. data/docs/api/css/full_list.css +2 -1
  281. data/docs/api/css/style.css +14 -13
  282. data/docs/api/file.README.html +22 -3
  283. data/docs/api/index.html +22 -3
  284. data/docs/api/method_list.html +435 -275
  285. data/docs/api/top-level-namespace.html +1 -1
  286. data/docs/assets/LogoStop.Gb3tDhL1.png +0 -0
  287. data/docs/assets/OverviewMetro.DUS-5fUZ.png +0 -0
  288. data/docs/assets/{ai.md._6HCDL6d.js → ai.md.Cy9GWnER.js} +1 -1
  289. data/docs/assets/ai.md.Cy9GWnER.lean.js +1 -0
  290. data/docs/assets/{app.BX81XO4N.js → app.ClaS47Ru.js} +1 -1
  291. data/docs/assets/{assets.md.D3wunzLx.js → assets.md.7C3HWkga.js} +3 -3
  292. data/docs/assets/{assets.md.D3wunzLx.lean.js → assets.md.7C3HWkga.lean.js} +1 -1
  293. data/docs/assets/{brut-js.md.o2DAO2s2.js → brut-js.md.B4GYxQVw.js} +1 -1
  294. data/docs/assets/{brut-js.md.o2DAO2s2.lean.js → brut-js.md.B4GYxQVw.lean.js} +1 -1
  295. data/docs/assets/chunks/@localSearchIndexroot.Biqy1A4t.js +1 -0
  296. data/docs/assets/chunks/{VPLocalSearchBox.gABXcTWp.js → VPLocalSearchBox.DtgDfde2.js} +1 -1
  297. data/docs/assets/chunks/{theme.DwUXXAL3.js → theme.B45bvibT.js} +2 -2
  298. data/docs/assets/{cli.md.RmeA2b0i.js → cli.md.CjsktgFz.js} +15 -20
  299. data/docs/assets/components.md.DatoNgFo.js +96 -0
  300. data/docs/assets/{components.md.CRUMdRoN.lean.js → components.md.DatoNgFo.lean.js} +1 -1
  301. data/docs/assets/{configuration.md.BGHl8oRC.js → configuration.md.DeyhpqEx.js} +3 -3
  302. data/docs/assets/{css.md.DJgj2clw.js → css.md.CltvJqAa.js} +3 -3
  303. data/docs/assets/{custom-element-tests.md.BrYJQEl3.js → custom-element-tests.md.B_rbta32.js} +3 -3
  304. data/docs/assets/{database-access.md.C7l-Vuvb.js → database-access.md.gnluu54N.js} +1 -1
  305. data/docs/assets/{database-schema.md.BUjR0VS1.js → database-schema.md.CSYk6E6v.js} +6 -6
  306. data/docs/assets/{database-schema.md.BUjR0VS1.lean.js → database-schema.md.CSYk6E6v.lean.js} +1 -1
  307. data/docs/assets/deployment.md.BLseERGV.js +48 -0
  308. data/docs/assets/deployment.md.BLseERGV.lean.js +1 -0
  309. data/docs/assets/dev-environment.md.BroAOLhF.js +11 -0
  310. data/docs/assets/dir-structure.md.CWir1pic.js +46 -0
  311. data/docs/assets/dir-structure.md.CWir1pic.lean.js +1 -0
  312. data/docs/assets/doc-conventions.md.BzmSrTEW.js +1 -0
  313. data/docs/assets/doc-conventions.md.BzmSrTEW.lean.js +1 -0
  314. data/docs/assets/{end-to-end-tests.md.yfQHC0b5.js → end-to-end-tests.md.DzqRpZ43.js} +5 -3
  315. data/docs/assets/end-to-end-tests.md.DzqRpZ43.lean.js +1 -0
  316. data/docs/assets/features.md.DPFXsy0z.js +154 -0
  317. data/docs/assets/features.md.DPFXsy0z.lean.js +1 -0
  318. data/docs/assets/flash-and-session.md.nPvUpnUx.js +79 -0
  319. data/docs/assets/{flash-and-session.md.BXY8RvT0.lean.js → flash-and-session.md.nPvUpnUx.lean.js} +1 -1
  320. data/docs/assets/form-constraints.md.x5tNpTTI.js +90 -0
  321. data/docs/assets/form-constraints.md.x5tNpTTI.lean.js +1 -0
  322. data/docs/assets/forms.md.C2Dizvzq.js +64 -0
  323. data/docs/assets/forms.md.C2Dizvzq.lean.js +1 -0
  324. data/docs/assets/{getting-started.md.Ciz82L0m.js → getting-started.md.C93e0odB.js} +5 -5
  325. data/docs/assets/{getting-started.md.Ciz82L0m.lean.js → getting-started.md.C93e0odB.lean.js} +1 -1
  326. data/docs/assets/handlers.md.Chyri6KA.js +54 -0
  327. data/docs/assets/handlers.md.Chyri6KA.lean.js +1 -0
  328. data/docs/assets/{hooks.md.C4-moMny.js → hooks.md.Jmb5VOLA.js} +4 -4
  329. data/docs/assets/{hooks.md.C4-moMny.lean.js → hooks.md.Jmb5VOLA.lean.js} +1 -1
  330. data/docs/assets/{i18n.md.Do9i1qWl.js → i18n.md.xQhiGo1G.js} +2 -2
  331. data/docs/assets/{i18n.md.Do9i1qWl.lean.js → i18n.md.xQhiGo1G.lean.js} +1 -1
  332. data/docs/assets/index.md.CAMqGBJE.js +1 -0
  333. data/docs/assets/index.md.CAMqGBJE.lean.js +1 -0
  334. data/docs/assets/{instrumentation.md.a9Pjps4P.js → instrumentation.md.BgcaGVYH.js} +2 -2
  335. data/docs/assets/{instrumentation.md.a9Pjps4P.lean.js → instrumentation.md.BgcaGVYH.lean.js} +1 -1
  336. data/docs/assets/{javascript.md.GWbhRS51.js → javascript.md.DzrMxUmI.js} +7 -7
  337. data/docs/assets/{javascript.md.GWbhRS51.lean.js → javascript.md.DzrMxUmI.lean.js} +1 -1
  338. data/docs/assets/keyword-injection.md.95Zgh2eN.js +21 -0
  339. data/docs/assets/{keyword-injection.md.Dt2tKREs.lean.js → keyword-injection.md.95Zgh2eN.lean.js} +1 -1
  340. data/docs/assets/{layouts.md.cPnh3NId.js → layouts.md.CJGDFY-m.js} +2 -15
  341. data/docs/assets/layouts.md.CJGDFY-m.lean.js +1 -0
  342. data/docs/assets/{lsp.md.Bsu-f6VU.js → lsp.md.Dn1rIiW0.js} +1 -1
  343. data/docs/assets/{lsp.md.Bsu-f6VU.lean.js → lsp.md.Dn1rIiW0.lean.js} +1 -1
  344. data/docs/assets/overview.md.Bdq4qt3L.js +1 -0
  345. data/docs/assets/overview.md.Bdq4qt3L.lean.js +1 -0
  346. data/docs/assets/pages.md.B7Hc-i6H.js +45 -0
  347. data/docs/assets/pages.md.B7Hc-i6H.lean.js +1 -0
  348. data/docs/assets/recipes_alternate-layouts.md.BwEytl59.js +22 -0
  349. data/docs/assets/recipes_alternate-layouts.md.BwEytl59.lean.js +1 -0
  350. data/docs/assets/recipes_authentication.md.Dzvi_g69.js +156 -0
  351. data/docs/assets/recipes_authentication.md.Dzvi_g69.lean.js +1 -0
  352. data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.js +15 -0
  353. data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.lean.js +1 -0
  354. data/docs/assets/recipes_custom-flash.md.CrQbI5eH.js +26 -0
  355. data/docs/assets/recipes_custom-flash.md.CrQbI5eH.lean.js +1 -0
  356. data/docs/assets/recipes_indexed-forms.md.CstYyOSo.js +74 -0
  357. data/docs/assets/recipes_indexed-forms.md.CstYyOSo.lean.js +1 -0
  358. data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.js +101 -0
  359. data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.lean.js +1 -0
  360. data/docs/assets/routes.md.B8kfUPHU.js +21 -0
  361. data/docs/assets/{routes.md.BMM7peut.lean.js → routes.md.B8kfUPHU.lean.js} +1 -1
  362. data/docs/assets/{security.md.C668yXCi.js → security.md.C0G_AZR-.js} +1 -1
  363. data/docs/assets/{security.md.C668yXCi.lean.js → security.md.C0G_AZR-.lean.js} +1 -1
  364. data/docs/assets/space-time-continuum.md.xl44xDos.js +1 -0
  365. data/docs/assets/{space-time-continuum.md.KPUIKysQ.lean.js → space-time-continuum.md.xl44xDos.lean.js} +1 -1
  366. data/docs/assets/{style.D73IYGCX.css → style.prAgp4yQ.css} +1 -1
  367. data/docs/assets/tutorial.md.a4a0eVOy.js +1 -0
  368. data/docs/assets/tutorial.md.a4a0eVOy.lean.js +1 -0
  369. data/docs/assets/why.md.C-hk5xgJ.js +1 -0
  370. data/docs/assets/why.md.C-hk5xgJ.lean.js +1 -0
  371. data/docs/assets.html +12 -7
  372. data/docs/brut-js/api/AjaxSubmit.html +1 -1
  373. data/docs/brut-js/api/AjaxSubmit.js.html +1 -1
  374. data/docs/brut-js/api/Autosubmit.html +1 -1
  375. data/docs/brut-js/api/Autosubmit.js.html +1 -1
  376. data/docs/brut-js/api/BaseCustomElement.html +1 -1
  377. data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
  378. data/docs/brut-js/api/BrutCustomElements.html +1 -1
  379. data/docs/brut-js/api/BufferedLogger.html +1 -1
  380. data/docs/brut-js/api/ConfirmSubmit.html +1 -1
  381. data/docs/brut-js/api/ConfirmSubmit.js.html +1 -1
  382. data/docs/brut-js/api/ConfirmationDialog.html +1 -1
  383. data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
  384. data/docs/brut-js/api/ConstraintViolationMessage.html +1 -1
  385. data/docs/brut-js/api/ConstraintViolationMessage.js.html +1 -1
  386. data/docs/brut-js/api/ConstraintViolationMessages.html +1 -1
  387. data/docs/brut-js/api/ConstraintViolationMessages.js.html +1 -1
  388. data/docs/brut-js/api/CopyToClipboard.html +1 -1
  389. data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
  390. data/docs/brut-js/api/Form.html +1 -1
  391. data/docs/brut-js/api/Form.js.html +1 -1
  392. data/docs/brut-js/api/I18nTranslation.html +1 -1
  393. data/docs/brut-js/api/I18nTranslation.js.html +1 -1
  394. data/docs/brut-js/api/LocaleDetection.html +1 -1
  395. data/docs/brut-js/api/LocaleDetection.js.html +1 -1
  396. data/docs/brut-js/api/Logger.html +1 -1
  397. data/docs/brut-js/api/Logger.js.html +1 -1
  398. data/docs/brut-js/api/Message.html +1 -1
  399. data/docs/brut-js/api/Message.js.html +1 -1
  400. data/docs/brut-js/api/PrefixedLogger.html +1 -1
  401. data/docs/brut-js/api/RichString.html +1 -1
  402. data/docs/brut-js/api/RichString.js.html +1 -1
  403. data/docs/brut-js/api/Tabs.html +1 -1
  404. data/docs/brut-js/api/Tabs.js.html +1 -1
  405. data/docs/brut-js/api/Tracing.html +1 -1
  406. data/docs/brut-js/api/Tracing.js.html +1 -1
  407. data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
  408. data/docs/brut-js/api/external-Performance.html +1 -1
  409. data/docs/brut-js/api/external-Promise.html +1 -1
  410. data/docs/brut-js/api/external-ValidityState.html +1 -1
  411. data/docs/brut-js/api/external-Window.html +1 -1
  412. data/docs/brut-js/api/external-fetch.html +1 -1
  413. data/docs/brut-js/api/global.html +1 -1
  414. data/docs/brut-js/api/index.html +1 -1
  415. data/docs/brut-js/api/index.js.html +1 -1
  416. data/docs/brut-js/api/module-testing.html +1 -1
  417. data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
  418. data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
  419. data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
  420. data/docs/brut-js/api/testing.DOMCreator.html +1 -1
  421. data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
  422. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
  423. data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
  424. data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
  425. data/docs/brut-js/api/testing_index.js.html +1 -1
  426. data/docs/brut-js.html +12 -7
  427. data/docs/business-logic.html +10 -5
  428. data/docs/cli.html +26 -26
  429. data/docs/components.html +61 -64
  430. data/docs/configuration.html +13 -8
  431. data/docs/css.html +14 -9
  432. data/docs/custom-element-tests.html +14 -9
  433. data/docs/database-access.html +12 -7
  434. data/docs/database-schema.html +15 -10
  435. data/docs/deployment.html +58 -6
  436. data/docs/dev-environment.html +12 -7
  437. data/docs/dir-structure.html +74 -0
  438. data/docs/doc-conventions.html +11 -6
  439. data/docs/end-to-end-tests.html +15 -8
  440. data/docs/favicon.ico +0 -0
  441. data/docs/features.html +182 -0
  442. data/docs/flash-and-session.html +73 -82
  443. data/docs/form-constraints.html +118 -0
  444. data/docs/forms.html +57 -367
  445. data/docs/getting-started.html +15 -10
  446. data/docs/handlers.html +51 -61
  447. data/docs/hashmap.json +1 -1
  448. data/docs/hooks.html +14 -9
  449. data/docs/i18n.html +12 -7
  450. data/docs/index.html +11 -6
  451. data/docs/instrumentation.html +12 -7
  452. data/docs/javascript.html +17 -12
  453. data/docs/jobs.html +10 -5
  454. data/docs/keyword-injection.html +22 -21
  455. data/docs/layouts.html +12 -20
  456. data/docs/lsp.html +11 -6
  457. data/docs/markdown-examples.html +10 -5
  458. data/docs/middleware.html +10 -5
  459. data/docs/not-released.html +10 -5
  460. data/docs/overview.html +11 -138
  461. data/docs/pages.html +49 -121
  462. data/docs/recipes/alternate-layouts.html +50 -0
  463. data/docs/recipes/authentication.html +166 -6
  464. data/docs/recipes/blank-layouts.html +43 -0
  465. data/docs/recipes/custom-flash.html +54 -0
  466. data/docs/recipes/indexed-forms.html +102 -0
  467. data/docs/recipes/text-field-component.html +129 -0
  468. data/docs/routes.html +16 -19
  469. data/docs/security.html +11 -6
  470. data/docs/seed-data.html +10 -5
  471. data/docs/space-time-continuum.html +11 -6
  472. data/docs/tutorial.html +11 -6
  473. data/docs/unit-tests.html +10 -5
  474. data/docs/why.html +29 -0
  475. data/dx/bash_customizations +7 -0
  476. data/dx/build +13 -2
  477. data/dx/docker-compose.env +1 -1
  478. data/dx/exec +25 -8
  479. data/lib/brut/front_end/form.rb +8 -8
  480. data/lib/brut/front_end/forms/input.rb +253 -20
  481. data/lib/brut/front_end/forms/input_definition.rb +15 -12
  482. data/lib/brut/front_end/forms/radio_button_group_input.rb +8 -1
  483. data/lib/brut/front_end/forms/select_input.rb +8 -1
  484. data/lib/brut/front_end.rb +1 -0
  485. data/lib/brut/version.rb +1 -1
  486. data/specs/brut/front_end/forms/input.spec.rb +978 -0
  487. data/specs/brut/front_end/forms/radio_button_group_input.spec.rb +54 -0
  488. data/specs/brut/front_end/forms/select_input.spec.rb +54 -0
  489. data/specs/spec_helper.rb +27 -0
  490. data/specs/support/matchers/have_constraint_violation.rb +23 -0
  491. data/specs/support/matchers.rb +5 -0
  492. data/specs/support.rb +3 -0
  493. metadata +141 -77
  494. data/brutrb.com/public/images/logo-300.png +0 -0
  495. data/brutrb.com/public/images/logo.png +0 -0
  496. data/docs/assets/ai.md._6HCDL6d.lean.js +0 -1
  497. data/docs/assets/chunks/@localSearchIndexroot.CoYzciVi.js +0 -1
  498. data/docs/assets/components.md.CRUMdRoN.js +0 -104
  499. data/docs/assets/deployment.md.Dbka4OTr.js +0 -1
  500. data/docs/assets/deployment.md.Dbka4OTr.lean.js +0 -1
  501. data/docs/assets/dev-environment.md.GZv6xvi9.js +0 -11
  502. data/docs/assets/doc-conventions.md.-kN3Xo5C.js +0 -1
  503. data/docs/assets/doc-conventions.md.-kN3Xo5C.lean.js +0 -1
  504. data/docs/assets/end-to-end-tests.md.yfQHC0b5.lean.js +0 -1
  505. data/docs/assets/flash-and-session.md.BXY8RvT0.js +0 -93
  506. data/docs/assets/forms.md.B-koVgyw.js +0 -379
  507. data/docs/assets/forms.md.B-koVgyw.lean.js +0 -1
  508. data/docs/assets/handlers.md.089DVD3v.js +0 -69
  509. data/docs/assets/handlers.md.089DVD3v.lean.js +0 -1
  510. data/docs/assets/index.md.B28EwVpq.js +0 -1
  511. data/docs/assets/index.md.B28EwVpq.lean.js +0 -1
  512. data/docs/assets/keyword-injection.md.Dt2tKREs.js +0 -25
  513. data/docs/assets/layouts.md.cPnh3NId.lean.js +0 -1
  514. data/docs/assets/overview.Da81cB9R.png +0 -0
  515. data/docs/assets/overview.md.C5wlBcR5.js +0 -133
  516. data/docs/assets/overview.md.C5wlBcR5.lean.js +0 -1
  517. data/docs/assets/pages.md.BE3kfOc5.js +0 -122
  518. data/docs/assets/pages.md.BE3kfOc5.lean.js +0 -1
  519. data/docs/assets/recipes_authentication.md.CAsXf7hk.js +0 -1
  520. data/docs/assets/recipes_authentication.md.CAsXf7hk.lean.js +0 -1
  521. data/docs/assets/routes.md.BMM7peut.js +0 -29
  522. data/docs/assets/space-time-continuum.md.KPUIKysQ.js +0 -1
  523. data/docs/assets/tutorial.md.BnoGjrdK.js +0 -1
  524. data/docs/assets/tutorial.md.BnoGjrdK.lean.js +0 -1
  525. data/docs/images/logo-300.png +0 -0
  526. data/docs/images/logo.png +0 -0
  527. /data/docs/assets/{cli.md.RmeA2b0i.lean.js → cli.md.CjsktgFz.lean.js} +0 -0
  528. /data/docs/assets/{configuration.md.BGHl8oRC.lean.js → configuration.md.DeyhpqEx.lean.js} +0 -0
  529. /data/docs/assets/{css.md.DJgj2clw.lean.js → css.md.CltvJqAa.lean.js} +0 -0
  530. /data/docs/assets/{custom-element-tests.md.BrYJQEl3.lean.js → custom-element-tests.md.B_rbta32.lean.js} +0 -0
  531. /data/docs/assets/{database-access.md.C7l-Vuvb.lean.js → database-access.md.gnluu54N.lean.js} +0 -0
  532. /data/docs/assets/{dev-environment.md.GZv6xvi9.lean.js → dev-environment.md.BroAOLhF.lean.js} +0 -0
@@ -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.