brut 0.0.27 β†’ 0.0.29

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 (411) 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/assets/Logo-Square.pxd +0 -0
  8. data/assets/LogoPylon.pxd +0 -0
  9. data/assets/LogoStop.pxd +0 -0
  10. data/assets/LogoTall.pxd +0 -0
  11. data/bin/docs +24 -2
  12. data/bin/rspec +27 -0
  13. data/bin/setup +3 -3
  14. data/brutrb.com/.vitepress/config.mjs +1 -0
  15. data/brutrb.com/.vitepress/theme/custom.css +7 -0
  16. data/brutrb.com/.vitepress/theme/style.css +26 -15
  17. data/brutrb.com/deployment.md +123 -45
  18. data/brutrb.com/dev-environment.md +6 -5
  19. data/brutrb.com/doc-conventions.md +1 -1
  20. data/brutrb.com/getting-started.md +64 -28
  21. data/brutrb.com/images/LogoPylon.png +0 -0
  22. data/brutrb.com/images/LogoSquare.png +0 -0
  23. data/brutrb.com/images/LogoStop.png +0 -0
  24. data/brutrb.com/images/LogoTall.png +0 -0
  25. data/brutrb.com/images/OverviewMetro.graffle +0 -0
  26. data/brutrb.com/images/OverviewMetro.png +0 -0
  27. data/brutrb.com/index.md +4 -3
  28. data/brutrb.com/instrumentation.md +12 -0
  29. data/brutrb.com/layouts.md +130 -0
  30. data/brutrb.com/overview.md +6 -6
  31. data/docker-compose.dx.yml +5 -2
  32. data/docs/404.html +3 -3
  33. data/docs/ai.html +6 -6
  34. data/docs/api/Brut/BackEnd/SeedData.html +1 -1
  35. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +1 -1
  36. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +1 -1
  37. data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +1 -1
  38. data/docs/api/Brut/BackEnd/Sidekiq.html +1 -1
  39. data/docs/api/Brut/BackEnd/Validators/FormValidator.html +1 -1
  40. data/docs/api/Brut/BackEnd/Validators.html +1 -1
  41. data/docs/api/Brut/BackEnd.html +1 -1
  42. data/docs/api/Brut/CLI/App.html +38 -13
  43. data/docs/api/Brut/CLI/AppRunner.html +1 -1
  44. data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +2 -2
  45. data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +2 -2
  46. data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +2 -2
  47. data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +2 -2
  48. data/docs/api/Brut/CLI/Apps/BuildAssets.html +1 -1
  49. data/docs/api/Brut/CLI/Apps/DB/Create.html +2 -2
  50. data/docs/api/Brut/CLI/Apps/DB/Drop.html +2 -2
  51. data/docs/api/Brut/CLI/Apps/DB/Migrate.html +9 -3
  52. data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +11 -11
  53. data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +2 -2
  54. data/docs/api/Brut/CLI/Apps/DB/Seed.html +2 -2
  55. data/docs/api/Brut/CLI/Apps/DB/Status.html +12 -12
  56. data/docs/api/Brut/CLI/Apps/DB.html +1 -1
  57. data/docs/api/Brut/CLI/Apps/DeployBase/GitChecks.html +270 -0
  58. data/docs/api/Brut/CLI/Apps/DeployBase.html +257 -0
  59. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy/Deploy.html +585 -0
  60. data/docs/api/Brut/CLI/Apps/HerokuContainerBasedDeploy.html +196 -0
  61. data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +1 -1
  62. data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +2 -2
  63. data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +2 -2
  64. data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +2 -2
  65. data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +2 -2
  66. data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +2 -2
  67. data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +1 -1
  68. data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +2 -2
  69. data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +1 -1
  70. data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +2 -2
  71. data/docs/api/Brut/CLI/Apps/Scaffold.html +1 -1
  72. data/docs/api/Brut/CLI/Apps/Test/Audit.html +12 -10
  73. data/docs/api/Brut/CLI/Apps/Test/E2e.html +8 -8
  74. data/docs/api/Brut/CLI/Apps/Test/JS.html +9 -9
  75. data/docs/api/Brut/CLI/Apps/Test/Run.html +18 -18
  76. data/docs/api/Brut/CLI/Apps/Test.html +1 -1
  77. data/docs/api/Brut/CLI/Apps.html +2 -2
  78. data/docs/api/Brut/CLI/Command.html +113 -28
  79. data/docs/api/Brut/CLI/Error.html +1 -1
  80. data/docs/api/Brut/CLI/ExecutionResults/Result.html +1 -1
  81. data/docs/api/Brut/CLI/ExecutionResults.html +1 -1
  82. data/docs/api/Brut/CLI/Executor.html +169 -38
  83. data/docs/api/Brut/CLI/InvalidOption.html +1 -1
  84. data/docs/api/Brut/CLI/Options.html +68 -19
  85. data/docs/api/Brut/CLI/Output.html +1 -1
  86. data/docs/api/Brut/CLI/SystemExecError.html +1 -1
  87. data/docs/api/Brut/CLI.html +1 -1
  88. data/docs/api/Brut/FactoryBot.html +1 -1
  89. data/docs/api/Brut/Framework/App.html +1 -1
  90. data/docs/api/Brut/Framework/Config.html +1 -1
  91. data/docs/api/Brut/Framework/Container.html +110 -29
  92. data/docs/api/Brut/Framework/Error.html +1 -1
  93. data/docs/api/Brut/Framework/Errors/AbstractMethod.html +89 -1
  94. data/docs/api/Brut/Framework/Errors/Bug.html +1 -1
  95. data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +1 -1
  96. data/docs/api/Brut/Framework/Errors/MissingParameter.html +1 -1
  97. data/docs/api/Brut/Framework/Errors/NoClassForPath.html +1 -1
  98. data/docs/api/Brut/Framework/Errors/NotFound.html +1 -1
  99. data/docs/api/Brut/Framework/Errors/NotImplemented.html +1 -1
  100. data/docs/api/Brut/Framework/Errors.html +31 -8
  101. data/docs/api/Brut/Framework/FussyTypeEnforcement.html +1 -1
  102. data/docs/api/Brut/Framework/MCP.html +1 -1
  103. data/docs/api/Brut/Framework/ProjectEnvironment.html +1 -1
  104. data/docs/api/Brut/Framework.html +1 -1
  105. data/docs/api/Brut/FrontEnd/AssetPathResolver.html +1 -1
  106. data/docs/api/Brut/FrontEnd/Component/Helpers.html +36 -26
  107. data/docs/api/Brut/FrontEnd/Component.html +7 -7
  108. data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +1 -1
  109. data/docs/api/Brut/FrontEnd/Components/FormTag.html +37 -29
  110. data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +1 -1
  111. data/docs/api/Brut/FrontEnd/Components/Input.html +1 -1
  112. data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +1 -1
  113. data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +20 -117
  114. data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +25 -23
  115. data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +73 -380
  116. data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +22 -138
  117. data/docs/api/Brut/FrontEnd/Components/Inputs.html +1 -1
  118. data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +1 -1
  119. data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +1 -1
  120. data/docs/api/Brut/FrontEnd/Components/TimeTag.html +1 -1
  121. data/docs/api/Brut/FrontEnd/Components/Traceparent.html +1 -1
  122. data/docs/api/Brut/FrontEnd/Components.html +23 -2
  123. data/docs/api/Brut/FrontEnd/Download.html +1 -1
  124. data/docs/api/Brut/FrontEnd/Flash.html +1 -1
  125. data/docs/api/Brut/FrontEnd/Form.html +1 -1
  126. data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +24 -68
  127. data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +201 -0
  128. data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +535 -0
  129. data/docs/api/Brut/FrontEnd/Forms/Input.html +983 -35
  130. data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +4 -4
  131. data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +29 -19
  132. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +9 -3
  133. data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +1 -1
  134. data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +9 -3
  135. data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +1 -1
  136. data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +72 -22
  137. data/docs/api/Brut/FrontEnd/Forms.html +1 -1
  138. data/docs/api/Brut/FrontEnd/GenericResponse.html +1 -1
  139. data/docs/api/Brut/FrontEnd/Handler.html +1 -1
  140. data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +1 -1
  141. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +1 -1
  142. data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +1 -1
  143. data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +1 -1
  144. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +1 -1
  145. data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +1 -1
  146. data/docs/api/Brut/FrontEnd/Handlers.html +1 -1
  147. data/docs/api/Brut/FrontEnd/HandlingResults.html +1 -1
  148. data/docs/api/Brut/FrontEnd/HttpMethod.html +1 -1
  149. data/docs/api/Brut/FrontEnd/HttpStatus.html +1 -1
  150. data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +1 -1
  151. data/docs/api/Brut/FrontEnd/Layout.html +1 -1
  152. data/docs/api/Brut/FrontEnd/Middleware.html +1 -1
  153. data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +1 -1
  154. data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +1 -1
  155. data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +1 -1
  156. data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +6 -2
  157. data/docs/api/Brut/FrontEnd/Middlewares.html +1 -1
  158. data/docs/api/Brut/FrontEnd/Page.html +1 -1
  159. data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +1 -1
  160. data/docs/api/Brut/FrontEnd/Pages.html +1 -1
  161. data/docs/api/Brut/FrontEnd/RequestContext.html +1 -1
  162. data/docs/api/Brut/FrontEnd/RouteHook.html +1 -1
  163. data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +1 -1
  164. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +1 -1
  165. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +1 -1
  166. data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +1 -1
  167. data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +1 -1
  168. data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +1 -1
  169. data/docs/api/Brut/FrontEnd/RouteHooks.html +1 -1
  170. data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +1 -1
  171. data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +1 -1
  172. data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +1 -1
  173. data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +1 -1
  174. data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +1 -1
  175. data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +1 -1
  176. data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +1 -1
  177. data/docs/api/Brut/FrontEnd/Routing/Route.html +1 -1
  178. data/docs/api/Brut/FrontEnd/Routing.html +1 -1
  179. data/docs/api/Brut/FrontEnd/Session.html +1 -1
  180. data/docs/api/Brut/FrontEnd.html +1 -1
  181. data/docs/api/Brut/I18n/BaseMethods.html +1 -1
  182. data/docs/api/Brut/I18n/ForBackEnd.html +1 -1
  183. data/docs/api/Brut/I18n/ForCLI.html +1 -1
  184. data/docs/api/Brut/I18n/ForHTML.html +1 -1
  185. data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +1 -1
  186. data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +1 -1
  187. data/docs/api/Brut/I18n.html +1 -1
  188. data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +1 -1
  189. data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +1 -1
  190. data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +1 -1
  191. data/docs/api/Brut/Instrumentation/OpenTelemetry.html +1 -1
  192. data/docs/api/Brut/Instrumentation.html +1 -1
  193. data/docs/api/Brut/SinatraHelpers/ClassMethods.html +1 -1
  194. data/docs/api/Brut/SinatraHelpers.html +1 -1
  195. data/docs/api/Brut/SpecSupport/ClockSupport.html +1 -1
  196. data/docs/api/Brut/SpecSupport/ComponentSupport.html +1 -1
  197. data/docs/api/Brut/SpecSupport/E2ETestServer.html +20 -20
  198. data/docs/api/Brut/SpecSupport/E2eSupport.html +1 -1
  199. data/docs/api/Brut/SpecSupport/EnhancedNode.html +1 -1
  200. data/docs/api/Brut/SpecSupport/FlashSupport.html +1 -1
  201. data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +1 -1
  202. data/docs/api/Brut/SpecSupport/GeneralSupport.html +1 -1
  203. data/docs/api/Brut/SpecSupport/HandlerSupport.html +2 -2
  204. data/docs/api/Brut/SpecSupport/Matchers/BeABug.html +142 -0
  205. data/docs/api/Brut/SpecSupport/Matchers/BePageFor.html +142 -0
  206. data/docs/api/Brut/SpecSupport/Matchers/BeRoutingFor.html +155 -0
  207. data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +55 -25
  208. data/docs/api/Brut/SpecSupport/Matchers/HaveGenerated.html +149 -0
  209. data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +46 -19
  210. data/docs/api/Brut/SpecSupport/Matchers/HaveI18nString.html +149 -0
  211. data/docs/api/Brut/SpecSupport/Matchers/HaveLinkTo.html +149 -0
  212. data/docs/api/Brut/SpecSupport/Matchers/HaveRedirectedTo.html +165 -0
  213. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedHttpStatus.html +158 -0
  214. data/docs/api/Brut/SpecSupport/Matchers/HaveReturnedRackResponse.html +156 -0
  215. data/docs/api/Brut/SpecSupport/Matchers.html +2 -2
  216. data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +24 -24
  217. data/docs/api/Brut/SpecSupport/RSpecSetup.html +55 -20
  218. data/docs/api/Brut/SpecSupport/SessionSupport.html +1 -1
  219. data/docs/api/Brut/SpecSupport.html +1 -1
  220. data/docs/api/Brut.html +1 -1
  221. data/docs/api/Clock.html +1 -1
  222. data/docs/api/RichString.html +1 -1
  223. data/docs/api/SemanticLogger/Appender/Async.html +1 -1
  224. data/docs/api/Sequel/Extensions/BrutInstrumentation.html +5 -1
  225. data/docs/api/Sequel/Extensions/BrutMigrations.html +36 -28
  226. data/docs/api/Sequel/Extensions.html +1 -1
  227. data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +1 -1
  228. data/docs/api/Sequel/Plugins/CreatedAt.html +1 -1
  229. data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +1 -1
  230. data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +1 -1
  231. data/docs/api/Sequel/Plugins/ExternalId.html +1 -1
  232. data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +1 -1
  233. data/docs/api/Sequel/Plugins/FindBang.html +1 -1
  234. data/docs/api/Sequel/Plugins.html +1 -1
  235. data/docs/api/Sequel.html +1 -1
  236. data/docs/api/SpecSupport/Matchers/BeABug.html +143 -0
  237. data/docs/api/_index.html +106 -1
  238. data/docs/api/class_list.html +1 -1
  239. data/docs/api/css/full_list.css +2 -1
  240. data/docs/api/css/style.css +14 -13
  241. data/docs/api/file.README.html +1 -1
  242. data/docs/api/index.html +1 -1
  243. data/docs/api/method_list.html +530 -330
  244. data/docs/api/top-level-namespace.html +1 -1
  245. data/docs/assets/LogoStop.X8x-4riz.png +0 -0
  246. data/docs/assets/OverviewMetro.DUS-5fUZ.png +0 -0
  247. data/docs/assets/{ai.md.tZrjP9im.js β†’ ai.md._6HCDL6d.js} +1 -1
  248. data/docs/assets/ai.md._6HCDL6d.lean.js +1 -0
  249. data/docs/assets/{app.D_yaTITQ.js β†’ app.BhrfSt68.js} +1 -1
  250. data/docs/assets/chunks/@localSearchIndexroot.CeRAdP1K.js +1 -0
  251. data/docs/assets/chunks/{VPLocalSearchBox.B2-ZzyTY.js β†’ VPLocalSearchBox.Dpot_2H4.js} +1 -1
  252. data/docs/assets/chunks/{theme.CfGFVRvE.js β†’ theme.N2SNVLgU.js} +2 -2
  253. data/docs/assets/{components.md.eCttGlN-.js β†’ components.md.CRUMdRoN.js} +1 -1
  254. data/docs/assets/{configuration.md.BRriU0cL.js β†’ configuration.md.LG-zIBww.js} +1 -1
  255. data/docs/assets/deployment.md.BLseERGV.js +48 -0
  256. data/docs/assets/deployment.md.BLseERGV.lean.js +1 -0
  257. data/docs/assets/{dev-environment.md.BNc8AYiK.js β†’ dev-environment.md.GZv6xvi9.js} +1 -1
  258. data/docs/assets/doc-conventions.md.-kN3Xo5C.js +1 -0
  259. data/docs/assets/{doc-conventions.md.DCfRXXi-.lean.js β†’ doc-conventions.md.-kN3Xo5C.lean.js} +1 -1
  260. data/docs/assets/{forms.md.CBTYQ_Cz.js β†’ forms.md.B-koVgyw.js} +23 -23
  261. data/docs/assets/{forms.md.CBTYQ_Cz.lean.js β†’ forms.md.B-koVgyw.lean.js} +1 -1
  262. data/docs/assets/getting-started.md.Dj0qtZI2.js +25 -0
  263. data/docs/assets/getting-started.md.Dj0qtZI2.lean.js +1 -0
  264. data/docs/assets/index.md.CuBB-BdM.js +1 -0
  265. data/docs/assets/index.md.CuBB-BdM.lean.js +1 -0
  266. data/docs/assets/{instrumentation.md.CL6ax7nT.js β†’ instrumentation.md.a9Pjps4P.js} +2 -2
  267. data/docs/assets/{instrumentation.md.CL6ax7nT.lean.js β†’ instrumentation.md.a9Pjps4P.lean.js} +1 -1
  268. data/docs/assets/layouts.md.cPnh3NId.js +51 -0
  269. data/docs/assets/layouts.md.cPnh3NId.lean.js +1 -0
  270. data/docs/assets/lsp.md.Bsu-f6VU.js +1 -0
  271. data/docs/assets/lsp.md.Bsu-f6VU.lean.js +1 -0
  272. data/docs/assets/{overview.md.CDalkuxV.js β†’ overview.md.DVKRM8zl.js} +4 -4
  273. data/docs/assets/overview.md.DVKRM8zl.lean.js +1 -0
  274. data/docs/assets/recipes_authentication.md.CAsXf7hk.js +1 -0
  275. data/docs/assets/recipes_authentication.md.CAsXf7hk.lean.js +1 -0
  276. data/docs/assets/{style.D73IYGCX.css β†’ style.B2o1L9eN.css} +1 -1
  277. data/docs/assets.html +5 -5
  278. data/docs/brut-js/api/AjaxSubmit.html +1 -1
  279. data/docs/brut-js/api/AjaxSubmit.js.html +1 -1
  280. data/docs/brut-js/api/Autosubmit.html +1 -1
  281. data/docs/brut-js/api/Autosubmit.js.html +1 -1
  282. data/docs/brut-js/api/BaseCustomElement.html +1 -1
  283. data/docs/brut-js/api/BaseCustomElement.js.html +1 -1
  284. data/docs/brut-js/api/BrutCustomElements.html +1 -1
  285. data/docs/brut-js/api/BufferedLogger.html +1 -1
  286. data/docs/brut-js/api/ConfirmSubmit.html +1 -1
  287. data/docs/brut-js/api/ConfirmSubmit.js.html +1 -1
  288. data/docs/brut-js/api/ConfirmationDialog.html +1 -1
  289. data/docs/brut-js/api/ConfirmationDialog.js.html +1 -1
  290. data/docs/brut-js/api/ConstraintViolationMessage.html +1 -1
  291. data/docs/brut-js/api/ConstraintViolationMessage.js.html +1 -1
  292. data/docs/brut-js/api/ConstraintViolationMessages.html +1 -1
  293. data/docs/brut-js/api/ConstraintViolationMessages.js.html +1 -1
  294. data/docs/brut-js/api/CopyToClipboard.html +1 -1
  295. data/docs/brut-js/api/CopyToClipboard.js.html +1 -1
  296. data/docs/brut-js/api/Form.html +1 -1
  297. data/docs/brut-js/api/Form.js.html +1 -1
  298. data/docs/brut-js/api/I18nTranslation.html +1 -1
  299. data/docs/brut-js/api/I18nTranslation.js.html +1 -1
  300. data/docs/brut-js/api/LocaleDetection.html +1 -1
  301. data/docs/brut-js/api/LocaleDetection.js.html +1 -1
  302. data/docs/brut-js/api/Logger.html +1 -1
  303. data/docs/brut-js/api/Logger.js.html +1 -1
  304. data/docs/brut-js/api/Message.html +1 -1
  305. data/docs/brut-js/api/Message.js.html +1 -1
  306. data/docs/brut-js/api/PrefixedLogger.html +1 -1
  307. data/docs/brut-js/api/RichString.html +1 -1
  308. data/docs/brut-js/api/RichString.js.html +1 -1
  309. data/docs/brut-js/api/Tabs.html +1 -1
  310. data/docs/brut-js/api/Tabs.js.html +1 -1
  311. data/docs/brut-js/api/Tracing.html +1 -1
  312. data/docs/brut-js/api/Tracing.js.html +1 -1
  313. data/docs/brut-js/api/external-CustomElementRegistry.html +1 -1
  314. data/docs/brut-js/api/external-Performance.html +1 -1
  315. data/docs/brut-js/api/external-Promise.html +1 -1
  316. data/docs/brut-js/api/external-ValidityState.html +1 -1
  317. data/docs/brut-js/api/external-Window.html +1 -1
  318. data/docs/brut-js/api/external-fetch.html +1 -1
  319. data/docs/brut-js/api/global.html +1 -1
  320. data/docs/brut-js/api/index.html +1 -1
  321. data/docs/brut-js/api/index.js.html +1 -1
  322. data/docs/brut-js/api/module-testing.html +1 -1
  323. data/docs/brut-js/api/testing.AssetMetadata.html +1 -1
  324. data/docs/brut-js/api/testing.AssetMetadataLoader.html +1 -1
  325. data/docs/brut-js/api/testing.CustomElementTest.html +1 -1
  326. data/docs/brut-js/api/testing.DOMCreator.html +1 -1
  327. data/docs/brut-js/api/testing_AssetMetadata.js.html +1 -1
  328. data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +1 -1
  329. data/docs/brut-js/api/testing_CustomElementTest.js.html +1 -1
  330. data/docs/brut-js/api/testing_DOMCreator.js.html +1 -1
  331. data/docs/brut-js/api/testing_index.js.html +1 -1
  332. data/docs/brut-js.html +5 -5
  333. data/docs/business-logic.html +5 -5
  334. data/docs/cli.html +5 -5
  335. data/docs/components.html +7 -7
  336. data/docs/configuration.html +6 -6
  337. data/docs/css.html +5 -5
  338. data/docs/custom-element-tests.html +5 -5
  339. data/docs/database-access.html +5 -5
  340. data/docs/database-schema.html +5 -5
  341. data/docs/deployment.html +53 -6
  342. data/docs/dev-environment.html +6 -6
  343. data/docs/doc-conventions.html +6 -6
  344. data/docs/end-to-end-tests.html +5 -5
  345. data/docs/flash-and-session.html +5 -5
  346. data/docs/forms.html +28 -28
  347. data/docs/getting-started.html +30 -7
  348. data/docs/handlers.html +5 -5
  349. data/docs/hashmap.json +1 -1
  350. data/docs/hooks.html +5 -5
  351. data/docs/i18n.html +5 -5
  352. data/docs/index.html +6 -6
  353. data/docs/instrumentation.html +6 -6
  354. data/docs/javascript.html +5 -5
  355. data/docs/jobs.html +5 -5
  356. data/docs/keyword-injection.html +5 -5
  357. data/docs/layouts.html +74 -0
  358. data/docs/lsp.html +24 -0
  359. data/docs/markdown-examples.html +5 -5
  360. data/docs/middleware.html +5 -5
  361. data/docs/not-released.html +5 -5
  362. data/docs/overview.html +9 -9
  363. data/docs/pages.html +6 -6
  364. data/docs/recipes/authentication.html +24 -0
  365. data/docs/routes.html +5 -5
  366. data/docs/security.html +5 -5
  367. data/docs/seed-data.html +5 -5
  368. data/docs/space-time-continuum.html +5 -5
  369. data/docs/tutorial.html +5 -5
  370. data/docs/unit-tests.html +5 -5
  371. data/dx/bash_customizations +7 -0
  372. data/dx/build +13 -2
  373. data/dx/docker-compose.env +1 -1
  374. data/dx/exec +25 -8
  375. data/lib/brut/cli/app.rb +7 -2
  376. data/lib/brut/cli/apps/deploy_base.rb +86 -0
  377. data/lib/brut/cli/apps/heroku_container_based_deploy.rb +194 -0
  378. data/lib/brut/cli/command.rb +7 -13
  379. data/lib/brut/cli/executor.rb +31 -5
  380. data/lib/brut/cli/options.rb +4 -0
  381. data/lib/brut/cli.rb +4 -3
  382. data/lib/brut/framework/container.rb +25 -7
  383. data/lib/brut/framework/errors/abstract_method.rb +7 -0
  384. data/lib/brut/framework/errors.rb +4 -2
  385. data/lib/brut/front_end/forms/input.rb +253 -20
  386. data/lib/brut/front_end/forms/input_definition.rb +15 -12
  387. data/lib/brut/front_end/middlewares/reload_app.rb +2 -0
  388. data/lib/brut/front_end.rb +1 -0
  389. data/lib/brut/spec_support/rspec_setup.rb +42 -2
  390. data/lib/brut/version.rb +1 -1
  391. data/lib/sequel/extensions/brut_instrumentation.rb +4 -0
  392. data/specs/brut/front_end/forms/input.spec.rb +978 -0
  393. data/specs/spec_helper.rb +27 -0
  394. data/specs/support/matchers/have_constraint_violation.rb +23 -0
  395. data/specs/support/matchers.rb +5 -0
  396. data/specs/support.rb +3 -0
  397. metadata +77 -29
  398. data/docs/assets/ai.md.tZrjP9im.lean.js +0 -1
  399. data/docs/assets/chunks/@localSearchIndexroot.BsN5i0Fi.js +0 -1
  400. data/docs/assets/deployment.md.Dbka4OTr.js +0 -1
  401. data/docs/assets/deployment.md.Dbka4OTr.lean.js +0 -1
  402. data/docs/assets/doc-conventions.md.DCfRXXi-.js +0 -1
  403. data/docs/assets/getting-started.md.Bz2s1Vjb.js +0 -2
  404. data/docs/assets/getting-started.md.Bz2s1Vjb.lean.js +0 -1
  405. data/docs/assets/index.md.B28EwVpq.js +0 -1
  406. data/docs/assets/index.md.B28EwVpq.lean.js +0 -1
  407. data/docs/assets/overview.Da81cB9R.png +0 -0
  408. data/docs/assets/overview.md.CDalkuxV.lean.js +0 -1
  409. /data/docs/assets/{components.md.eCttGlN-.lean.js β†’ components.md.CRUMdRoN.lean.js} +0 -0
  410. /data/docs/assets/{configuration.md.BRriU0cL.lean.js β†’ configuration.md.LG-zIBww.lean.js} +0 -0
  411. /data/docs/assets/{dev-environment.md.BNc8AYiK.lean.js β†’ dev-environment.md.GZv6xvi9.lean.js} +0 -0
@@ -1,4 +1,4 @@
1
- import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E=JSON.parse('{"title":"Forms","description":"","frontmatter":{},"headers":[],"relativePath":"forms.md","filePath":"forms.md"}'),e={name:"forms.md"};function l(h,s,p,k,r,d){return n(),a("div",null,s[0]||(s[0]=[t(`<h1 id="forms" tabindex="-1">Forms <a class="header-anchor" href="#forms" aria-label="Permalink to &quot;Forms&quot;">​</a></h1><p>The most common way for a web site visitor to submit data to the server is to submit a form. The Web Platform&#39;s forms API is much like an uncle you may have: old and rich.</p><p>Brut&#39;s forms module solves three problems:</p><ul><li>Descrbing the data being collected and submitted</li><li>Providing access to the submitted form data on the server</li><li>Support for creating a good client-side experience regarding constraint violations, both client-side and server-side.</li></ul><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>The forms module has a lot of moving parts, but the general process of using forms is:</p><ol><li>Create a <em>form class</em> to describe the data in your form.</li><li>Use an instance of that form class to generate HTML for the elements of the form.</li><li>Implement a <em>handler class</em> that will receive an instance of your form class, populated with the data provided when the form was submitted. (<strong>Note</strong>: forms generally cannot contain data submitted by the browser that was not described by the form class, thus obviating the need for something like Rails&#39; strong parameters).</li><li>Add server-side constraint violations to the form and re-render your HTML or use the form data as input to a back-end process.</li></ol><h3 id="forms-are-submitted-to-routes" tabindex="-1">Forms Are Submitted to Routes <a class="header-anchor" href="#forms-are-submitted-to-routes" aria-label="Permalink to &quot;Forms Are Submitted to Routes&quot;">​</a></h3><p>When you have a form to process, create a <code>form</code> route (remember, all routes must follow the <a href="/routes.html">rules of routing</a>):</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> App</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Framework</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">App</span></span>
1
+ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const g=JSON.parse('{"title":"Forms","description":"","frontmatter":{},"headers":[],"relativePath":"forms.md","filePath":"forms.md"}'),e={name:"forms.md"};function l(h,s,p,k,r,d){return n(),a("div",null,s[0]||(s[0]=[t(`<h1 id="forms" tabindex="-1">Forms <a class="header-anchor" href="#forms" aria-label="Permalink to &quot;Forms&quot;">​</a></h1><p>The most common way for a web site visitor to submit data to the server is to submit a form. The Web Platform&#39;s forms API is much like an uncle you may have: old and rich.</p><p>Brut&#39;s forms module solves three problems:</p><ul><li>Descrbing the data being collected and submitted</li><li>Providing access to the submitted form data on the server</li><li>Support for creating a good client-side experience regarding constraint violations, both client-side and server-side.</li></ul><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><p>The forms module has a lot of moving parts, but the general process of using forms is:</p><ol><li>Create a <em>form class</em> to describe the data in your form.</li><li>Use an instance of that form class to generate HTML for the elements of the form.</li><li>Implement a <em>handler class</em> that will receive an instance of your form class, populated with the data provided when the form was submitted. (<strong>Note</strong>: forms generally cannot contain data submitted by the browser that was not described by the form class, thus obviating the need for something like Rails&#39; strong parameters).</li><li>Add server-side constraint violations to the form and re-render your HTML or use the form data as input to a back-end process.</li></ol><h3 id="forms-are-submitted-to-routes" tabindex="-1">Forms Are Submitted to Routes <a class="header-anchor" href="#forms-are-submitted-to-routes" aria-label="Permalink to &quot;Forms Are Submitted to Routes&quot;">​</a></h3><p>When you have a form to process, create a <code>form</code> route (remember, all routes must follow the <a href="/routes.html">rules of routing</a>):</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> App</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Framework</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">App</span></span>
2
2
  <span class="line"></span>
3
3
  <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
4
4
  <span class="line"></span>
@@ -24,7 +24,7 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E
24
24
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> button { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Login&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
25
25
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
26
26
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
27
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Brut can generate the HTML for the needed inputs via <code>Brut::FrontEnd::Components::Inputs::TextField.for_form_input</code>, which is a very long name. Hold that thought for now. This method will generate an <code>&lt;input&gt;</code> element for you, based on how you&#39;ve set up the field in your form class. The HTML element will have a value set based on the form, if there is a value.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/front_end/pages/login_page.rb</span></span>
27
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Brut can generate the HTML for the needed inputs via <a href="/api/Brut/FrontEnd/Components/Inputs/TextField.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Inputs::TextField</code></a>, which is a very long class name. Hold that thought for now. This method will generate an <code>&lt;input&gt;</code> element for you, based on how you&#39;ve set up the field in your form class. The HTML element will have a value set based on the form, if there is a value.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/front_end/pages/login_page.rb</span></span>
28
28
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
29
29
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span></span>
30
30
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginForm</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span></span>
@@ -34,8 +34,8 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E
34
34
  <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> form_tag</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">method:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
35
35
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> action:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginHandler</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">routing</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
36
36
  <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # We promise you don&#39;t have to type this every time!</span></span>
37
- <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Components</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">for_form_input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
38
- <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Components</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">for_form_input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :password</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
37
+ <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Components</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
38
+ <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Components</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :password</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
39
39
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> button { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Login&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
40
40
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
41
41
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
@@ -48,7 +48,7 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E
48
48
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> include</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Components</span></span>
49
49
  <span class="line"></span>
50
50
  <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
51
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>This allows you to call <code>Inputs::TextField.for_form_input</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/front_end/pages/login_page.rb</span></span>
51
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>This allows you to call <code>Inputs::TextField</code> like a method:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/front_end/pages/login_page.rb</span></span>
52
52
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
53
53
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> include</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Components</span></span>
54
54
  <span class="line"></span>
@@ -59,8 +59,8 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E
59
59
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> page_template</span></span>
60
60
  <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> form_tag</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">method:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
61
61
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> action:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginHandler</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">routing</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
62
- <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">for_form_input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
63
- <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">for_form_input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :password</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
62
+ <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
63
+ <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :password</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
64
64
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> button { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Login&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
65
65
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
66
66
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
@@ -84,7 +84,7 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E
84
84
  <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
85
85
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
86
86
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
87
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Of course, some constraints can&#39;t be validated client-side and require some back-end logic. In this case, we want to check that there is an authorized user with that email and password. Let&#39;s assume the existence of the class <code>AuthorizedUser</code> that has a class method <code>login</code> that returns <code>nil</code> if there is no user with that email/password combination.</p><p>If that returns <code>nil</code>, we want to re-render the <code>LoginPage</code>, exposing some sort of constraint violation message so it can be rendered. We also want the form fields to be pre-filled with the values the visitor provided.</p><p><code>for_form_input</code> can handle this, so we need to pass our form object into <code>LoginPage</code> instead of allowing <code>LoginPage</code> to create an empty one. We can do that by adding a <code>form:</code> keyword argument that defaults to <code>nil</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/front_end/pages/login_page.rb</span></span>
87
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Of course, some constraints can&#39;t be validated client-side and require some back-end logic. In this case, we want to check that there is an authorized user with that email and password. Let&#39;s assume the existence of the class <code>AuthorizedUser</code> that has a class method <code>login</code> that returns <code>nil</code> if there is no user with that email/password combination.</p><p>If that returns <code>nil</code>, we want to re-render the <code>LoginPage</code>, exposing some sort of constraint violation message so it can be rendered. We also want the form fields to be pre-filled with the values the visitor provided.</p><p><a href="/api/Brut/FrontEnd/Components.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components</code></a> can handle this, so we need to pass our form object into <code>LoginPage</code> instead of allowing <code>LoginPage</code> to create an empty one. We can do that by adding a <code>form:</code> keyword argument that defaults to <code>nil</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/front_end/pages/login_page.rb</span></span>
88
88
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginPage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
89
89
  <span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> nil</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
90
90
  <span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">||</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginForm</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">new</span></span>
@@ -93,8 +93,8 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E
93
93
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> page_template</span></span>
94
94
  <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> form_tag</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">method:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
95
95
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> action:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginHandler</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">routing</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
96
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">for_form_input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
97
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">for_form_input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :password</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
96
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
97
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :password</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
98
98
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> button { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Login&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
99
99
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
100
100
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
@@ -119,7 +119,7 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E
119
119
  <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span>
120
120
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
121
121
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
122
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>When <code>LoginPage</code> generates HTML, different HTML is generated, since the form being passed to <code>for_form_input</code> contains constraint violations.</p><h4 id="showing-constraint-violations-in-html" tabindex="-1">Showing Constraint Violations in HTML <a class="header-anchor" href="#showing-constraint-violations-in-html" aria-label="Permalink to &quot;Showing Constraint Violations in HTML&quot;">​</a></h4><p>When <code>Inputs::TextField.for_form_input</code> is called with an existing form that has constraint violations, different HTML is generated. This is what would be produced by our existing <code>LoginPage</code> (again, formatted her for clarity):</p><div class="language-html vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">form</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> method</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;post&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> action</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;/login&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
122
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>When <code>LoginPage</code> generates HTML, different HTML is generated, since the form being passed to the components contains constraint violations.</p><h4 id="showing-constraint-violations-in-html" tabindex="-1">Showing Constraint Violations in HTML <a class="header-anchor" href="#showing-constraint-violations-in-html" aria-label="Permalink to &quot;Showing Constraint Violations in HTML&quot;">​</a></h4><p>When <a href="/api/Brut/FrontEnd/Components/Inputs/TextField.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Inputs::TextField</code></a> is created with an existing form that has constraint violations, different HTML is generated. This is what would be produced by our existing <code>LoginPage</code> (again, formatted her for clarity):</p><div class="language-html vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">form</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> method</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;post&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> action</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;/login&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
123
123
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">input</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;email&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;email&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> required</span></span>
124
124
  <span class="line highlighted"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> data-invalid</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> data-login_not_found</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
125
125
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">input</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;password&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;password&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> required</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
@@ -131,10 +131,10 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E
131
131
  <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> form_tag</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">method:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
132
132
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> action:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginHandler</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">routing</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
133
133
  <span class="line"></span>
134
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">for_form_input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
134
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
135
135
  <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> ConstraintViolations</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
136
136
  <span class="line"></span>
137
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">for_form_input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :password</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
137
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :password</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
138
138
  <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> ConstraintViolations</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :password</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
139
139
  <span class="line"></span>
140
140
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> button { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Login&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
@@ -170,10 +170,10 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E
170
170
  <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> form_tag</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">method:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
171
171
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> action:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginHandler</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">routing</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
172
172
  <span class="line"></span>
173
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">for_form_input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
173
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
174
174
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> ConstraintViolations</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
175
175
  <span class="line"></span>
176
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">for_form_input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :password</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
176
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :password</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
177
177
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> ConstraintViolations</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :password</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
178
178
  <span class="line"></span>
179
179
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> button { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;Login&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
@@ -250,9 +250,9 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E
250
250
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:password</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">type:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :password</span></span>
251
251
  <span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:remember</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">type:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :checkbox</span></span>
252
252
  <span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:not_robot</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">type:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :checkbox</span></span>
253
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Checkboxes can be rendered by <code>Inputs::TextField.for_form_input</code>, and their <code>value</code> attribute would always be the string <code>&quot;true&quot;</code>. If the form&#39;s value for the input is the string <code>&quot;true&quot;</code>, the checkbox would have the <code>checked</code> attribute:</p><div class="language-html vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;">&lt;!-- Form.new(params: { remember: &quot;true&quot; }) --&gt;</span></span>
253
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Checkboxes can be rendered by a <a href="/api/Brut/FrontEnd/Components/Inputs/TextField.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Inputs::TextField</code></a>, and their <code>value</code> attribute would always be the string <code>&quot;true&quot;</code>. If the form&#39;s value for the input is the string <code>&quot;true&quot;</code>, the checkbox would have the <code>checked</code> attribute:</p><div class="language-html vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">html</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;">&lt;!-- Form.new(params: { remember: &quot;true&quot; }) --&gt;</span></span>
254
254
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">input</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;checkbox&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;remember&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> value</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;true&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> checked</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
255
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">input</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;checkbox&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;not_robot&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> value</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;true&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span></code></pre></div><h3 id="radio-buttons" tabindex="-1">Radio Buttons <a class="header-anchor" href="#radio-buttons" aria-label="Permalink to &quot;Radio Buttons&quot;">​</a></h3><p>Radio buttons are implemented in HTML by <code>&lt;input type=&quot;radio&quot;&gt;</code>, with an expectation of more than one such input having the same value for the <code>name</code> attribute, but different values for the <code>value</code> attributes, one of which may be <code>checked</code>.</p><p>Brut implements this via <a href="/api/Brut/FrontEnd/Components/Inputs/RadioButton.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Inputs::RadioButton</code></a>, which has the class method <code>for_form_input</code>. To create radio buttons in a form, use <code>radio_button_group</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/front_end/forms/login_form.rb</span></span>
255
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">input</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> type</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;checkbox&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;not_robot&quot;</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> value</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;true&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span></code></pre></div><h3 id="radio-buttons" tabindex="-1">Radio Buttons <a class="header-anchor" href="#radio-buttons" aria-label="Permalink to &quot;Radio Buttons&quot;">​</a></h3><p>Radio buttons are implemented in HTML by <code>&lt;input type=&quot;radio&quot;&gt;</code>, with an expectation of more than one such input having the same value for the <code>name</code> attribute, but different values for the <code>value</code> attributes, one of which may be <code>checked</code>.</p><p>Brut implements this via <a href="/api/Brut/FrontEnd/Components/Inputs/RadioButton.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Inputs::RadioButton</code></a>, whose initializer behaves like the other form input components. To create radio buttons in a form, use <code>radio_button_group</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/front_end/forms/login_form.rb</span></span>
256
256
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginForm</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppForm</span></span>
257
257
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">type:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :email</span></span>
258
258
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:password</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">type:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :password</span></span>
@@ -262,7 +262,7 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E
262
262
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> [ </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:never</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:one_week</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:one_month</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> ].</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">each</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> |remember|</span></span>
263
263
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> label </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
264
264
  <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> render</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
265
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">RadioButton</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">for_form_input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
265
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">RadioButton</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
266
266
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
267
267
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :remember</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
268
268
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> value:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> remember</span></span>
@@ -283,7 +283,7 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E
283
283
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:email</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">type:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :email</span></span>
284
284
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:password</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">type:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :password</span></span>
285
285
  <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> select</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :remember</span></span>
286
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Creating the HTML can be done with <a href="/api/Brut/FrontEnd/Components/Inputs/Select.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Inputs::Select</code></a>. It&#39;s <code>for_form_input</code> is more complex, since it provides a way to show visitor-friendly values instead of the innate <code>value</code> for each option, as well as to allow for a &quot;blank&quot; entry.</p><p>Let&#39;s suppose we have a class named <code>LoginRememberOption</code>. It&#39;s a simple wrapper around a value we might store in the database and use to lookup an I18n key.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginRememberOption</span></span>
286
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Creating the HTML can be done with <a href="/api/Brut/FrontEnd/Components/Inputs/Select.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Inputs::Select</code></a>. It&#39;s initializer is more complex, since it provides a way to show visitor-friendly values instead of the innate <code>value</code> for each option, as well as to allow for a &quot;blank&quot; entry.</p><p>Let&#39;s suppose we have a class named <code>LoginRememberOption</code>. It&#39;s a simple wrapper around a value we might store in the database and use to lookup an I18n key.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginRememberOption</span></span>
287
287
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> include</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">I18n</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">ForBackend</span></span>
288
288
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(value)</span></span>
289
289
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @value </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">=</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> value</span></span>
@@ -305,7 +305,7 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E
305
305
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>To show these options in a <code>&lt;select&gt;</code>, we might do this:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> view_template</span></span>
306
306
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
307
307
  <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> render</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
308
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">for_form_input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
308
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
309
309
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
310
310
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :remember</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
311
311
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> options:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> LoginRememberOption</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">all</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
@@ -325,14 +325,14 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E
325
325
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> &lt;</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">option</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> value</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">&quot;one_month&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;One Month&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">option</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span>
326
326
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&lt;/</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">select</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">&gt;</span></span></code></pre></div><h3 id="arrays-of-values" tabindex="-1">Arrays of Values <a class="header-anchor" href="#arrays-of-values" aria-label="Permalink to &quot;Arrays of Values&quot;">​</a></h3><p>Some complex forms involve a potentially arbitrary number of inputs for a given field. For example, you might allow the visitor to edit widgets in bulk, 10 at a time.</p><p>Brut can handle this, with help from Rack. First, you&#39;ll use <code>array: true</code> when declaring an input:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> BulkWidgetForm</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppForm</span></span>
327
327
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">array:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">required:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> false</span></span>
328
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>In this case, we need <code>required: false</code> or every single field we generate will be required.</p><p>To generate the HTML, use the optional <code>index:</code> parameter to <code>for_form_input</code> as well as for <code>ConstraintViolations</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># Inside e.g. app/src/front_end/pages/create_bulk_widget_page.rb</span></span>
328
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>In this case, we need <code>required: false</code> or every single field we generate will be required.</p><p>To generate the HTML, use the optional <code>index:</code> parameter to the initializer as well as for <code>ConstraintViolations</code>:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># Inside e.g. app/src/front_end/pages/create_bulk_widget_page.rb</span></span>
329
329
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> page_template</span></span>
330
330
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> brut_form </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
331
331
  <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> form_tag</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">method:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
332
332
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> action:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> BulkWidgetForm</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">routing</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
333
333
  <span class="line"></span>
334
334
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 10</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">times</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> |i|</span></span>
335
- <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">for_form_input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
335
+ <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Inputs</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">TextField</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span></span>
336
336
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> @form,</span></span>
337
337
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> input_name:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
338
338
  <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> index:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> i</span></span>
@@ -376,4 +376,4 @@ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E
376
376
  <span class="line highlighted"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> index:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 3</span></span>
377
377
  <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span></code></pre></div><p>This can be quite complicated when editing sparse data. Brut should do a decent job raising an error if you try to treat a non-array value as an array or vice-versa.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h2><p>Form classes don&#39;t need any logic on them, but they can be given helper methods or other logic if it makes sense. To test them, test them like any other class - instantiate an object and examine the behavior of its methods.</p><p>Note that Brut provides the constructor for all form classes, and it expects a single keyword parameter named <code>params:</code> that is a hash mapping strings to strings representing the submitted form data. The keys can be symbols and Brut will map them to strings.</p><p>Testing handlers is covered in <a href="/handlers.html">Handlers</a></p><p>When testing the UX around constraint violations, you should use an end-to-end test, as this will allow you to assert behavior around client-side constraint violations. This is discussed in <a href="/end-to-end-tests.html">End-to-end Tests</a>.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><h3 id="make-use-of-components" tabindex="-1">Make Use of Components <a class="header-anchor" href="#make-use-of-components" aria-label="Permalink to &quot;Make Use of Components&quot;">​</a></h3><p>The example we saw above creates only minimal markup, yet required a fair bit of code. You are encouraged to create your own components that generate the markup you need for your app&#39;s inputs. For example, you are likely going to want <code>app/src/front_end/components/text_field_component.rb</code> to generate whatever markup is needed fo your text fields to look how they are supposed to, with and without constraint violation messages.</p><h3 id="functional-or-utility-css-is-difficult-here" tabindex="-1">Functional or Utility CSS Is Difficult Here <a class="header-anchor" href="#functional-or-utility-css-is-difficult-here" aria-label="Permalink to &quot;Functional or Utility CSS Is Difficult Here&quot;">​</a></h3><p>The way constraint violations are implemented leverages the web platform, which naturally includes conventional use of CSS. This creates an &quot;impedance mismatch&quot; with functional or utility CSS like Tailwind.</p><p>While it may be possible to write a bunch of single-purpose classes to target the markup and attributes Brut generates, it may be easier to write conventional CSS for constraint violations.</p><p>To avoid duplication, you should leverage the custom properties of your CSS framework. For example:</p><div class="language-css vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">css</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">input</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">[</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">data-invalid</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">] {</span></span>
378
378
  <span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">var</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;">--color-red</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">);</span></span>
379
- <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">}</span></span></code></pre></div><p>In theory, your design for form inputs will be done once and be relatively stable. But, this is the downside of using CSS frameworks that eschew using CSS directly. You will need to manage this.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated May 13, 2025</em></p><p>Form internals try to coerce types to strings, since the web and HTTP is all strings all the time. Empty strings are coerced to <code>nil</code>. If the form&#39;s <code>params:</code> value contains any type Brut cannot deal with, you&#39;ll get an exception during tests and a notice/event in production.</p><p>For HTML generation, there are few classes that work together:</p><ul><li><em>input definitions</em> define an input and tend to provide an API similar to HTML&#39;s. See <a href="/api/Brut/FrontEnd/Forms/InputDefinition.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Forms::InputDefinition</code></a>.</li><li><em>inputs</em> represent the runtime state of an input from the browser. Whereas an input definition has no state, the input does. It delegates much of its behavior to the underlying input definition. It&#39;s <code>value=</code> method performs client-side constraint validations by creating a <code>ValidityState</code> internally. See <a href="/api/Brut/FrontEnd/Forms/Input.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Forms::Input</code></a>.</li><li><a href="/api/Brut/FrontEnd/Forms/InputDeclarations.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Forms::InputDeclarations</code></a> is a module that allows creating input definitions inside your form class. It implements the class methods like <code>input</code>.</li><li><a href="/api/Brut/FrontEnd/Components/Inputs.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Inputs</code></a> contains components used to generate <code>&lt;input&gt;</code> fields. These classes will coerce the value of the <code>input</code> they are given to generate the correct HTML.</li></ul>`,164)]))}const g=i(e,[["render",l]]);export{E as __pageData,g as default};
379
+ <span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">}</span></span></code></pre></div><p>In theory, your design for form inputs will be done once and be relatively stable. But, this is the downside of using CSS frameworks that eschew using CSS directly. You will need to manage this.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated May 13, 2025</em></p><p>Form internals try to coerce types to strings, since the web and HTTP is all strings all the time. Empty strings are coerced to <code>nil</code>. If the form&#39;s <code>params:</code> value contains any type Brut cannot deal with, you&#39;ll get an exception during tests and a notice/event in production.</p><p>For HTML generation, there are few classes that work together:</p><ul><li><em>input definitions</em> define an input and tend to provide an API similar to HTML&#39;s. See <a href="/api/Brut/FrontEnd/Forms/InputDefinition.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Forms::InputDefinition</code></a>.</li><li><em>inputs</em> represent the runtime state of an input from the browser. Whereas an input definition has no state, the input does. It delegates much of its behavior to the underlying input definition. It&#39;s <code>value=</code> method performs client-side constraint validations by creating a <code>ValidityState</code> internally. See <a href="/api/Brut/FrontEnd/Forms/Input.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Forms::Input</code></a>.</li><li><a href="/api/Brut/FrontEnd/Forms/InputDeclarations.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Forms::InputDeclarations</code></a> is a module that allows creating input definitions inside your form class. It implements the class methods like <code>input</code>.</li><li><a href="/api/Brut/FrontEnd/Components/Inputs.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Inputs</code></a> contains components used to generate <code>&lt;input&gt;</code> fields. These classes will coerce the value of the <code>input</code> they are given to generate the correct HTML.</li></ul>`,164)]))}const E=i(e,[["render",l]]);export{g as __pageData,E as default};
@@ -1 +1 @@
1
- import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const E=JSON.parse('{"title":"Forms","description":"","frontmatter":{},"headers":[],"relativePath":"forms.md","filePath":"forms.md"}'),e={name:"forms.md"};function l(h,s,p,k,r,d){return n(),a("div",null,s[0]||(s[0]=[t("",164)]))}const g=i(e,[["render",l]]);export{E as __pageData,g as default};
1
+ import{_ as i,c as a,o as n,ag as t}from"./chunks/framework.1L-BeKqY.js";const g=JSON.parse('{"title":"Forms","description":"","frontmatter":{},"headers":[],"relativePath":"forms.md","filePath":"forms.md"}'),e={name:"forms.md"};function l(h,s,p,k,r,d){return n(),a("div",null,s[0]||(s[0]=[t("",164)]))}const E=i(e,[["render",l]]);export{g as __pageData,E as default};
@@ -0,0 +1,25 @@
1
+ import{_ as s,c as e,o as t,ag as n}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"Getting Started","description":"","frontmatter":{},"headers":[],"relativePath":"getting-started.md","filePath":"getting-started.md"}'),i={name:"getting-started.md"};function p(l,a,o,r,d,h){return t(),e("div",null,a[0]||(a[0]=[n(`<h1 id="getting-started" tabindex="-1">Getting Started <a class="header-anchor" href="#getting-started" aria-label="Permalink to &quot;Getting Started&quot;">​</a></h1><p>Brut is developed alongside a separate gem called <code>mkbrut</code>, which allows you to create a new Brut app. It will set up your dev environment as well.</p><h2 id="get-mkbrut" tabindex="-1">Get <code>mkbrut</code> <a class="header-anchor" href="#get-mkbrut" aria-label="Permalink to &quot;Get \`mkbrut\`&quot;">​</a></h2><p>The simplest way to use <code>mkbrut</code> is to use an existing <a href="https://hub.docker.com/repository/docker/thirdtank/mkbrut/general" target="_blank" rel="noreferrer">Docker image</a>. You don&#39;t have to install or configure Ruby:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>docker run \\</span></span>
2
+ <span class="line"><span> -v &quot;$PWD&quot;:&quot;$PWD&quot; \\</span></span>
3
+ <span class="line"><span> -w &quot;$PWD&quot; \\</span></span>
4
+ <span class="line"><span> -it \\</span></span>
5
+ <span class="line"><span> thirdtank/mkbrut \\</span></span>
6
+ <span class="line"><span> mkbrut my-new-app</span></span></code></pre></div><p>If you already have Ruby 3.4 installed, you can install <code>mkbrut</code> directly:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>&gt; gem install mkbrut</span></span>
7
+ <span class="line"><span>&gt; mkbrut my-new-app</span></span></code></pre></div><h2 id="init-your-app" tabindex="-1">Init Your App <a class="header-anchor" href="#init-your-app" aria-label="Permalink to &quot;Init Your App&quot;">​</a></h2><p>A Brut app just needs a name, which will be used to derive a few more useful values. For now:</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-GxBbJ" id="tab-z5MzsdZ" checked><label data-title="Docker-based" for="tab-z5MzsdZ">Docker-based</label><input type="radio" name="group-GxBbJ" id="tab-YOC99E9"><label data-title="RubyGems-based" for="tab-YOC99E9">RubyGems-based</label></div><div class="blocks"><div class="language- vp-adaptive-theme active"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>docker run \\</span></span>
8
+ <span class="line"><span> -v &quot;$PWD&quot;:&quot;$PWD&quot; \\</span></span>
9
+ <span class="line"><span> -w &quot;$PWD&quot; \\</span></span>
10
+ <span class="line"><span> -it \\</span></span>
11
+ <span class="line"><span> thirdtank/mkbrut \\</span></span>
12
+ <span class="line"><span> mkbrut my-new-app</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>mkbrut my-new-app</span></span></code></pre></div></div></div><p>This will create your new app, along with some demo routes, components, handlers, and tests. If this is your first time using Brut, we recommend you examine these demo components.</p><p>To create your app without the demo components:</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-9f-sX" id="tab-TJslFtx" checked><label data-title="Docker-based" for="tab-TJslFtx">Docker-based</label><input type="radio" name="group-9f-sX" id="tab-YVrabuP"><label data-title="RubyGems-based" for="tab-YVrabuP">RubyGems-based</label></div><div class="blocks"><div class="language- vp-adaptive-theme active"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>docker run \\</span></span>
13
+ <span class="line"><span> -v &quot;$PWD&quot;:&quot;$PWD&quot; \\</span></span>
14
+ <span class="line"><span> -w &quot;$PWD&quot; \\</span></span>
15
+ <span class="line"><span> -it \\</span></span>
16
+ <span class="line"><span> thirdtank/mkbrut \\</span></span>
17
+ <span class="line"><span> mkbrut my-new-app --no-demo</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>mkbrut my-new-app --no-demo</span></span></code></pre></div></div></div><h2 id="start-your-dev-environment" tabindex="-1">Start Your Dev Environment <a class="header-anchor" href="#start-your-dev-environment" aria-label="Permalink to &quot;Start Your Dev Environment&quot;">​</a></h2><p>Brut includes a dev environment based on Docker. It uses Docker compose to run a Docker container where your app will run, a Docker container for Postgres, and a Docker container for local observability via OpenTelemetry.</p><ol><li><p><a href="https://docs.docker.com/get-started/get-docker/" target="_blank" rel="noreferrer">Install Docker</a></p></li><li><p>Build the image used to create you app&#39;s container:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>&gt; dx/build</span></span></code></pre></div></li><li><p>Start up all the containers:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>&gt; dx/start</span></span></code></pre></div></li><li><p>Now, &quot;log in&quot; to the container where your app and its tests will run:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>&gt; dx/exec login</span></span></code></pre></div></li><li><p>Set everything up:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>inside-container&gt; bin/setup</span></span></code></pre></div></li></ol><p>Now, you&#39;re ready to go. See <a href="/dev-environment.html">Dev Environemnt</a> for details on how this all works.</p><h2 id="run-the-app" tabindex="-1">Run the App <a class="header-anchor" href="#run-the-app" aria-label="Permalink to &quot;Run the App&quot;">​</a></h2><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>inside-container&gt; bin/dev</span></span></code></pre></div><p>You can now visit your app at <code>localhost:6502</code></p><p>You can make changes and see them when you reload. Open up <code>app/src/front_end/pages/home_page.rb</code> <em>in your editor running on your computer</em> and change the <code>h1</code> to look like so:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> HomePage</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppPage</span></span>
18
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> page_template</span></span>
19
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> div</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">class:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;flex flex-column items-center justify-center h-80vh&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
20
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> img</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">src:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;/static/images/icon.png&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">class:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;h-50&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
21
+ <span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> h1</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">class:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;ff-sans ma-0 lh-title f-5&quot;</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">) </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
22
+ <span class="line highlighted"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> &quot;Welcome to My New App!&quot;</span></span>
23
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
24
+ <span class="line"></span>
25
+ <span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"> # ...</span></span></code></pre></div><p>When you reload your browser, you&#39;ll see your change</p><h2 id="run-the-tests" tabindex="-1">Run the Tests <a class="header-anchor" href="#run-the-tests" aria-label="Permalink to &quot;Run the Tests&quot;">​</a></h2><p>There are a few tests you can run, as well as some checks that you aren&#39;t using RubyGems with security vulnerabilities. Run it all now with <code>bin/ci</code>:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>inside-container&gt; bin/dev</span></span></code></pre></div><h2 id="now-build-the-rest-of-your-app-πŸ¦‰" tabindex="-1">Now Build The Rest of Your App πŸ¦‰ <a class="header-anchor" href="#now-build-the-rest-of-your-app-πŸ¦‰" aria-label="Permalink to &quot;Now Build The Rest of Your App πŸ¦‰&quot;">​</a></h2><p>You can <a href="/tutorial.html">follow the tutorial</a>, check out the <a href="/overview.html">conceptual overview</a>, or dive straight into the API docs. You might also want to check out the docs for <a href="/lsp.html">LSP Support</a>.</p>`,28)]))}const k=s(i,[["render",p]]);export{u as __pageData,k as default};
@@ -0,0 +1 @@
1
+ import{_ as s,c as e,o as t,ag as n}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"Getting Started","description":"","frontmatter":{},"headers":[],"relativePath":"getting-started.md","filePath":"getting-started.md"}'),i={name:"getting-started.md"};function p(l,a,o,r,d,h){return t(),e("div",null,a[0]||(a[0]=[n("",28)]))}const k=s(i,[["render",p]]);export{u as __pageData,k as default};
@@ -0,0 +1 @@
1
+ import{_ as a,c as o,o as n,j as t}from"./chunks/framework.1L-BeKqY.js";const s="/assets/LogoStop.X8x-4riz.png",g=JSON.parse(`{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"Brut RB","text":"Raw Ruby Web Apps","tagline":"Standards-based, No-nonsense, HTML-first, Low Ceremony","ximage":{"src":"/images/LogoTall.png","alt":"A Ruby gemstone embedded into a concrete, brutalist building"},"actions":[{"theme":"brand","text":"Brut is Not Yet Released","link":"/not-released"},{"theme":"brand","text":"Getting Started","link":"/getting-started"},{"theme":"alt","text":"Conceptual Overview","link":"/overview"}]},"xfeatures":[{"title":"Standards-Based","icon":"πŸ“„","details":"Brut leverages HTML, HTTP, SQL, and the Ruby standard library to let you write apps using standards you already know…or could quickly learn"},{"title":"Convention-Oriented","icon":"🎚️","details":"In a Brut app, there's usually just one way to do something. Learn things once, and you won't forget how your app works."},{"title":"Objects and Methods","icon":"πŸ’»","details":"Author classes to create objects on which to call methods. Nothing fancy."},{"title":"Builds on Community Libraries","icon":"🏘️","details":"Sequel, Phlex, I18n, RSpec. They do it best"}]},"headers":[],"relativePath":"index.md","filePath":"index.md"}`),i={name:"index.md"};function r(d,e,l,c,u,p){return n(),o("div",null,e[0]||(e[0]=[t("p",null,[t("img",{src:s,alt:"logo"})],-1)]))}const h=a(i,[["render",r]]);export{g as __pageData,h as default};
@@ -0,0 +1 @@
1
+ import{_ as a,c as o,o as n,j as t}from"./chunks/framework.1L-BeKqY.js";const s="/assets/LogoStop.X8x-4riz.png",g=JSON.parse(`{"title":"","description":"","frontmatter":{"layout":"home","hero":{"name":"Brut RB","text":"Raw Ruby Web Apps","tagline":"Standards-based, No-nonsense, HTML-first, Low Ceremony","ximage":{"src":"/images/LogoTall.png","alt":"A Ruby gemstone embedded into a concrete, brutalist building"},"actions":[{"theme":"brand","text":"Brut is Not Yet Released","link":"/not-released"},{"theme":"brand","text":"Getting Started","link":"/getting-started"},{"theme":"alt","text":"Conceptual Overview","link":"/overview"}]},"xfeatures":[{"title":"Standards-Based","icon":"πŸ“„","details":"Brut leverages HTML, HTTP, SQL, and the Ruby standard library to let you write apps using standards you already know…or could quickly learn"},{"title":"Convention-Oriented","icon":"🎚️","details":"In a Brut app, there's usually just one way to do something. Learn things once, and you won't forget how your app works."},{"title":"Objects and Methods","icon":"πŸ’»","details":"Author classes to create objects on which to call methods. Nothing fancy."},{"title":"Builds on Community Libraries","icon":"🏘️","details":"Sequel, Phlex, I18n, RSpec. They do it best"}]},"headers":[],"relativePath":"index.md","filePath":"index.md"}`),i={name:"index.md"};function r(d,e,l,c,u,p){return n(),o("div",null,e[0]||(e[0]=[t("p",null,[t("img",{src:s,alt:"logo"})],-1)]))}const h=a(i,[["render",r]]);export{g as __pageData,h as default};
@@ -1,4 +1,4 @@
1
- import{_ as s,c as t,o as e,ag as a}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"Instrumentation and Observability","description":"","frontmatter":{},"headers":[],"relativePath":"instrumentation.md","filePath":"instrumentation.md"}'),n={name:"instrumentation.md"};function h(r,i,o,l,p,d){return e(),t("div",null,i[0]||(i[0]=[a(`<h1 id="instrumentation-and-observability" tabindex="-1">Instrumentation and Observability <a class="header-anchor" href="#instrumentation-and-observability" aria-label="Permalink to &quot;Instrumentation and Observability&quot;">​</a></h1><p>Brut has built-in support for OpenTelemetry, which is an open standard used by many observability vendors to allow you to understand the behavior of your app in production. Brut also includes a configuration for the <a href="https://github.com/CtrlSpice/otel-desktop-viewer/" target="_blank" rel="noreferrer">otel-desktop-viewer</a>, which allows you to see instrumentation in development.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><h3 id="why-instrument" tabindex="-1">Why Instrument? <a class="header-anchor" href="#why-instrument" aria-label="Permalink to &quot;Why Instrument?&quot;">​</a></h3><p>In production, you&#39;ll need to know what your app is doing and how well it&#39;s working. Historically, logs can provide this information in a roundabout way. Over the last many years, Application Performance Monitoring (APM) vendors like New Relic and Data Dog allowed developers to see much richer detail about how an app is working.</p><p>You could see, for example, the 95th percentil of your dashboard controller&#39;s performance, or the top 10 slowest SQL statements your app is executing. OpenTelemetry attempts to unify the API used to communicate this information from your app to your chosen vendor, and most vendors support it.</p><p>Instrumentation, then, is a way to record what your app is doing, how long its taking, and perhaps even why it&#39;s doing what it&#39;s doing, down to a very specific level. If properly configured, you could examine the performance of the app for a particular user on a particular day.</p><h3 id="setting-up-instrumentation" tabindex="-1">Setting up Instrumentation <a class="header-anchor" href="#setting-up-instrumentation" aria-label="Permalink to &quot;Setting up Instrumentation&quot;">​</a></h3><p>Brut automatically sets up OpenTelemetry (OTel) tracing. The primary interface you will use is <a href="/api/Brut/Instrumentation/OpenTelemetry.html" target="_self" rel="noopener" data-no-router><code>Brut::Instrumentation::OpenTelemetry</code></a>, which is available via <code>Brut.container.instrumentation</code>. We&#39;ll discuss that in a moment.</p><p>To configure the specifics of where the traces wil go, the OTel gem uses environment variables:</p><table tabindex="0"><thead><tr><th>Variable</th><th>Value</th><th>Purpose</th></tr></thead><tbody><tr><td><code>OTEL_EXPORTER_OTLP_ENDPOINT</code></td><td>Depends on environment</td><td>Where to send the tracers. This is provided by your vendor, but is <code>http://otel-desktop-viewer:4318</code> in development</td></tr><tr><td><code>OTEL_EXPORTER_OTLP_HEADERS</code></td><td>Depends on vendor</td><td>Your vendor may ask you to set this. It often contains identifying information or API keys</td></tr><tr><td><code>OTEL_EXPORTER_OTLP_PROTOCOL</code></td><td>http/protobuf</td><td>Your vendor may request a different protocol, but protobuf is common and supported by otel-desktop-viewer</td></tr><tr><td><code>OTEL_LOG_LEVEL</code></td><td>debug</td><td>Useful when setting everything up to understand why things aren&#39;t working if they aren&#39;t working</td></tr><tr><td><code>OTEL_RUBY_BSP_START_THREAD_ON_BOOT</code></td><td>false</td><td>Deals with esoteric issues with Puma. See <a href="https://github.com/open-telemetry/opentelemetry-ruby/issues/462" target="_blank" rel="noreferrer">this GitHub issue</a> for the details.</td></tr><tr><td><code>OTEL_SERVICE_NAME</code></td><td>Your app&#39;s <code>id</code> from <code>App</code></td><td>Identifiers your app&#39;s name to the vendor</td></tr><tr><td><code>OTEL_TRACES_EXPORTER</code></td><td>otlp</td><td>Configures the class inside the OTel gem that will export the instrumentation to the vendor. If you omit this, Brut will log the instrumentation to the console</td></tr></tbody></table><p>When you created your Brut app, your <code>.env.development</code> and <code>.env.test</code> should have values for all these environment variables that will send instrumentation to the otel-desktop-viewer that was also configured.</p><p>If you run your app using <code>bin/dev</code> and use the app for a bit, then go to <code>http://localhost:8000</code>, you will see the otel-desktop-viewer UI and can browser the spans and traces sent by Brut.</p><h3 id="what-is-instrumented-by-default" tabindex="-1">What is Instrumented By Default <a class="header-anchor" href="#what-is-instrumented-by-default" aria-label="Permalink to &quot;What is Instrumented By Default&quot;">​</a></h3><p>Brut attempts to automatically instrument useful things so you don&#39;t have to do anything to start getting data. Brut will attempt to conform to standard semantics for HTTP requests and SQL statements.</p><p>Here is a non-exhaustive list of what Brut automatically instruments:</p><ul><li>How long each page or handler request takes</li><li>CLI execution time</li><li>Time to rebuild the schema for tests</li><li>Time to run tests</li><li>Time to apply migrations</li><li>Time spent inside a route hook</li><li>The locale detected from the browser</li><li>The layout class used when rendering a page</li><li>If a requested path is owned by Brut or not</li><li>Ignored parameters on all form submissions</li><li>How long reloading takes in development</li><li>CSP reporting results</li></ul><h3 id="adding-your-own-instrumentation" tabindex="-1">Adding Your Own Instrumentation <a class="header-anchor" href="#adding-your-own-instrumentation" aria-label="Permalink to &quot;Adding Your Own Instrumentation&quot;">​</a></h3><p>You can add instrumentation in a few ways:</p><ul><li><em>Spans</em> record a block of code. They are shown as a sub-span if one is already in effect. When you create a span, that means it will be shown in the context of the HTTP request.</li><li><em>Attributes</em> can be added to the current span to provide more context about what is happening. For example, the HTTP request method is an attribute of the span used for the HTTP request. These attributes allow for sophisticated querying in the vendor&#39;s UI.</li><li><em>Events</em> record things that happen and metadata about that thing. These are like log statements. They are associated with the span you are in when you add the event.</li></ul><p>These can all be added via <code>Brut.container.instrumentation</code>, which is a <a href="/api/Brut/Instrumentation/OpenTelemetry.html" target="_self" rel="noopener" data-no-router><code>Brut::Instrumentation::OpenTelemetry</code></a> instance.</p><p>These methods are available:</p><ul><li><code>span(name,**attributes,&amp;block)</code> - Create a new span around the block yielded.</li><li><code>add_attributes(attributes)</code> - Add attributes to the current span. These will be prefixed with your app&#39;s prefix so it&#39;s clear in the observability UI that they are for your app and not standard.</li><li><code>add_event(name,**attributes)</code> - Add an event with optional attributes for the current span.</li><li><code>record_exception(ex,attributes=nil)</code> - Record an exception that was caught.</li><li><code>record_and_reraise_exception!(ex,attributes=nil)</code> - Record an exception and raise it.</li></ul><p>Suppose you want to instrument <code>RequireAuthBeforeHook</code> from the <a href="/hooks.html">hooks</a> documentation. Although the hook&#39;s <code>before</code> method is instrumented by Brut already, let&#39;s add some metadata to that span, and also add a span around the login check.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/front_end/route_hooks/require_auth_before_hook.rb</span></span>
1
+ import{_ as s,c as e,o as t,ag as a}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"Instrumentation and Observability","description":"","frontmatter":{},"headers":[],"relativePath":"instrumentation.md","filePath":"instrumentation.md"}'),n={name:"instrumentation.md"};function h(r,i,o,l,p,d){return t(),e("div",null,i[0]||(i[0]=[a(`<h1 id="instrumentation-and-observability" tabindex="-1">Instrumentation and Observability <a class="header-anchor" href="#instrumentation-and-observability" aria-label="Permalink to &quot;Instrumentation and Observability&quot;">​</a></h1><p>Brut has built-in support for OpenTelemetry, which is an open standard used by many observability vendors to allow you to understand the behavior of your app in production. Brut also includes a configuration for the <a href="https://github.com/CtrlSpice/otel-desktop-viewer/" target="_blank" rel="noreferrer">otel-desktop-viewer</a>, which allows you to see instrumentation in development.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to &quot;Overview&quot;">​</a></h2><h3 id="why-instrument" tabindex="-1">Why Instrument? <a class="header-anchor" href="#why-instrument" aria-label="Permalink to &quot;Why Instrument?&quot;">​</a></h3><p>In production, you&#39;ll need to know what your app is doing and how well it&#39;s working. Historically, logs can provide this information in a roundabout way. Over the last many years, Application Performance Monitoring (APM) vendors like New Relic and Data Dog allowed developers to see much richer detail about how an app is working.</p><p>You could see, for example, the 95th percentil of your dashboard controller&#39;s performance, or the top 10 slowest SQL statements your app is executing. OpenTelemetry attempts to unify the API used to communicate this information from your app to your chosen vendor, and most vendors support it.</p><p>Instrumentation, then, is a way to record what your app is doing, how long its taking, and perhaps even why it&#39;s doing what it&#39;s doing, down to a very specific level. If properly configured, you could examine the performance of the app for a particular user on a particular day.</p><h3 id="setting-up-instrumentation" tabindex="-1">Setting up Instrumentation <a class="header-anchor" href="#setting-up-instrumentation" aria-label="Permalink to &quot;Setting up Instrumentation&quot;">​</a></h3><p>Brut automatically sets up OpenTelemetry (OTel) tracing. The primary interface you will use is <a href="/api/Brut/Instrumentation/OpenTelemetry.html" target="_self" rel="noopener" data-no-router><code>Brut::Instrumentation::OpenTelemetry</code></a>, which is available via <code>Brut.container.instrumentation</code>. We&#39;ll discuss that in a moment.</p><p>To configure the specifics of where the traces wil go, the OTel gem uses environment variables:</p><table tabindex="0"><thead><tr><th>Variable</th><th>Value</th><th>Purpose</th></tr></thead><tbody><tr><td><code>OTEL_EXPORTER_OTLP_ENDPOINT</code></td><td>Depends on environment</td><td>Where to send the tracers. This is provided by your vendor, but is <code>http://otel-desktop-viewer:4318</code> in development</td></tr><tr><td><code>OTEL_EXPORTER_OTLP_HEADERS</code></td><td>Depends on vendor</td><td>Your vendor may ask you to set this. It often contains identifying information or API keys</td></tr><tr><td><code>OTEL_EXPORTER_OTLP_PROTOCOL</code></td><td>http/protobuf</td><td>Your vendor may request a different protocol, but protobuf is common and supported by otel-desktop-viewer</td></tr><tr><td><code>OTEL_LOG_LEVEL</code></td><td>debug</td><td>Useful when setting everything up to understand why things aren&#39;t working if they aren&#39;t working</td></tr><tr><td><code>OTEL_RUBY_BSP_START_THREAD_ON_BOOT</code></td><td>false</td><td>Deals with esoteric issues with Puma. See <a href="https://github.com/open-telemetry/opentelemetry-ruby/issues/462" target="_blank" rel="noreferrer">this GitHub issue</a> for the details.</td></tr><tr><td><code>OTEL_SERVICE_NAME</code></td><td>Your app&#39;s <code>id</code> from <code>App</code></td><td>Identifiers your app&#39;s name to the vendor</td></tr><tr><td><code>OTEL_TRACES_EXPORTER</code></td><td>otlp</td><td>Configures the class inside the OTel gem that will export the instrumentation to the vendor. If you omit this, Brut will log the instrumentation to the console</td></tr></tbody></table><p>When you created your Brut app, your <code>.env.development</code> and <code>.env.test</code> should have values for all these environment variables that will send instrumentation to the otel-desktop-viewer that was also configured.</p><p>If you run your app using <code>bin/dev</code> and use the app for a bit, then go to <code>http://localhost:8000</code>, you will see the otel-desktop-viewer UI and can browser the spans and traces sent by Brut.</p><h3 id="what-is-instrumented-by-default" tabindex="-1">What is Instrumented By Default <a class="header-anchor" href="#what-is-instrumented-by-default" aria-label="Permalink to &quot;What is Instrumented By Default&quot;">​</a></h3><p>Brut attempts to automatically instrument useful things so you don&#39;t have to do anything to start getting data. Brut will attempt to conform to standard semantics for HTTP requests and SQL statements.</p><p>Here is a non-exhaustive list of what Brut automatically instruments:</p><ul><li>How long each page or handler request takes</li><li>CLI execution time</li><li>Time to rebuild the schema for tests</li><li>Time to run tests</li><li>Time to apply migrations</li><li>Time spent inside a route hook</li><li>The locale detected from the browser</li><li>The layout class used when rendering a page</li><li>If a requested path is owned by Brut or not</li><li>Ignored parameters on all form submissions</li><li>How long reloading takes in development</li><li>CSP reporting results</li><li>SQL Statements</li></ul><div class="warning custom-block github-alert"><p class="custom-block-title">WARNING</p><p><code>Sequel::Extensions::BrutInstrumentation</code> sets up telemetry for Sequel, and it does it in a relatively simplistic way. The result is that <em>all</em> SQL statements are part of the telemetry, including the actual values inserted or used in <code>WHERE</code> clauses. While you should not be putting sensitive data into your database, be warned that this is happening. There are plans to improve this to be more flexible and reduce the Schance of sensitive data being sent in traces.</p></div><h3 id="adding-your-own-instrumentation" tabindex="-1">Adding Your Own Instrumentation <a class="header-anchor" href="#adding-your-own-instrumentation" aria-label="Permalink to &quot;Adding Your Own Instrumentation&quot;">​</a></h3><p>You can add instrumentation in a few ways:</p><ul><li><em>Spans</em> record a block of code. They are shown as a sub-span if one is already in effect. When you create a span, that means it will be shown in the context of the HTTP request.</li><li><em>Attributes</em> can be added to the current span to provide more context about what is happening. For example, the HTTP request method is an attribute of the span used for the HTTP request. These attributes allow for sophisticated querying in the vendor&#39;s UI.</li><li><em>Events</em> record things that happen and metadata about that thing. These are like log statements. They are associated with the span you are in when you add the event.</li></ul><p>These can all be added via <code>Brut.container.instrumentation</code>, which is a <a href="/api/Brut/Instrumentation/OpenTelemetry.html" target="_self" rel="noopener" data-no-router><code>Brut::Instrumentation::OpenTelemetry</code></a> instance.</p><p>These methods are available:</p><ul><li><code>span(name,**attributes,&amp;block)</code> - Create a new span around the block yielded.</li><li><code>add_attributes(attributes)</code> - Add attributes to the current span. These will be prefixed with your app&#39;s prefix so it&#39;s clear in the observability UI that they are for your app and not standard.</li><li><code>add_event(name,**attributes)</code> - Add an event with optional attributes for the current span.</li><li><code>record_exception(ex,attributes=nil)</code> - Record an exception that was caught.</li><li><code>record_and_reraise_exception!(ex,attributes=nil)</code> - Record an exception and raise it.</li></ul><p>Suppose you want to instrument <code>RequireAuthBeforeHook</code> from the <a href="/hooks.html">hooks</a> documentation. Although the hook&#39;s <code>before</code> method is instrumented by Brut already, let&#39;s add some metadata to that span, and also add a span around the login check.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/front_end/route_hooks/require_auth_before_hook.rb</span></span>
2
2
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> RequireAuthBeforeHook</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> &lt;</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">FrontEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">RouteHook</span></span>
3
3
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> before</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">request_context:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">session:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">request:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">env:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
4
4
  <span class="line"><span style="--shiki-light:#E36209;--shiki-dark:#FFAB70;"> is_home_page</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> = request.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">path_info</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">match</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">/^</span><span style="--shiki-light:#22863A;--shiki-light-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold;">\\/</span><span style="--shiki-light:#032F62;--shiki-dark:#DBEDFF;">?$/</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
@@ -32,4 +32,4 @@ import{_ as s,c as t,o as e,ag as a}from"./chunks/framework.1L-BeKqY.js";const u
32
32
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
33
33
  <span class="line"></span>
34
34
  <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
35
- <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Now, for every request someone makes to our app, we will see a span for the <code>RequireAuthBeforeHook</code>, and inside that span, we&#39;ll see the attributes we added as well as a sub-span representing the login check (which itself will have an attribute about the user&#39;s logged-in status).</p><h3 id="client-side-observability" tabindex="-1">Client-Side Observability <a class="header-anchor" href="#client-side-observability" aria-label="Permalink to &quot;Client-Side Observability&quot;">​</a></h3><p>The class <a href="/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Handlers::InstrumentationHandler</code></a> is set up to receive information from the client-side to provide insights about client-side behavior as part of a server-side request. Brut attempts to join up any client-side instrumentation to the request that served it.</p><p>It does this <a href="/api/Brut/FrontEnd/Components/Traceparent.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Traceparent</code></a> component, which is included in your default layout when you created your Brut app. This creates a <code>&lt;meta&gt;</code> tag containing standardized information used to connect the client-side behavior to the server-side request.</p><p>The Brut custom element <a href="/brut-js/api/Tracing.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-tracing&gt;</code></a> uses this information, along with statistics from the browser, to send a custom payload back to Brut at the route <code>/__brut/instrumentation</code>, which is handled by the aforementioned <code>InstrumentationHandler</code>.</p><p>You should then see client-side tracing information as a sub-span of your HTTP request. The information available depends on the browser, and some browsers don&#39;t send much.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h2><p>Generally you don&#39;t want to test instrumentation unless it&#39;s highly complex and critical to the app&#39;s ability to be maintained. Ideally, your end-to-end tests will cover all the instrumentation code you write so you can be sure that none of that causes a problem.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><p>Entire books and conferences exist on how to properly instrument your app. Our suggestion is to take what you have by default and add additional instrumentation only to solve specific problems or identify specific issues.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated June 12, 2025</em></p><p>Brut does not have plans to support non-OTel instrumentation, nor does it have plans to provide hooks to use proprietary formats. This could change of course.</p><p>The client-side portion of this is highly customized. The Otel open source code for the client side is massive and hugely complex, so Brut decided to try to produce something simple and straightforward as a start. This can and will evolve over time.</p>`,40)]))}const c=s(n,[["render",h]]);export{u as __pageData,c as default};
35
+ <span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Now, for every request someone makes to our app, we will see a span for the <code>RequireAuthBeforeHook</code>, and inside that span, we&#39;ll see the attributes we added as well as a sub-span representing the login check (which itself will have an attribute about the user&#39;s logged-in status).</p><h3 id="client-side-observability" tabindex="-1">Client-Side Observability <a class="header-anchor" href="#client-side-observability" aria-label="Permalink to &quot;Client-Side Observability&quot;">​</a></h3><p>The class <a href="/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Handlers::InstrumentationHandler</code></a> is set up to receive information from the client-side to provide insights about client-side behavior as part of a server-side request. Brut attempts to join up any client-side instrumentation to the request that served it.</p><p>It does this <a href="/api/Brut/FrontEnd/Components/Traceparent.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Traceparent</code></a> component, which is included in your default layout when you created your Brut app. This creates a <code>&lt;meta&gt;</code> tag containing standardized information used to connect the client-side behavior to the server-side request.</p><p>The Brut custom element <a href="/brut-js/api/Tracing.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;">&lt;brut-tracing&gt;</code></a> uses this information, along with statistics from the browser, to send a custom payload back to Brut at the route <code>/__brut/instrumentation</code>, which is handled by the aforementioned <code>InstrumentationHandler</code>.</p><p>You should then see client-side tracing information as a sub-span of your HTTP request. The information available depends on the browser, and some browsers don&#39;t send much.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to &quot;Testing&quot;">​</a></h2><p>Generally you don&#39;t want to test instrumentation unless it&#39;s highly complex and critical to the app&#39;s ability to be maintained. Ideally, your end-to-end tests will cover all the instrumentation code you write so you can be sure that none of that causes a problem.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to &quot;Recommended Practices&quot;">​</a></h2><p>Entire books and conferences exist on how to properly instrument your app. Our suggestion is to take what you have by default and add additional instrumentation only to solve specific problems or identify specific issues.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to &quot;Technical Notes&quot;">​</a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut&#39;s internals, the source code is always more correct.</p></div><p><em>Last Updated June 12, 2025</em></p><p>Brut does not have plans to support non-OTel instrumentation, nor does it have plans to provide hooks to use proprietary formats. This could change of course.</p><p>The client-side portion of this is highly customized. The Otel open source code for the client side is massive and hugely complex, so Brut decided to try to produce something simple and straightforward as a start. This can and will evolve over time.</p>`,41)]))}const c=s(n,[["render",h]]);export{u as __pageData,c as default};
@@ -1 +1 @@
1
- import{_ as s,c as t,o as e,ag as a}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"Instrumentation and Observability","description":"","frontmatter":{},"headers":[],"relativePath":"instrumentation.md","filePath":"instrumentation.md"}'),n={name:"instrumentation.md"};function h(r,i,o,l,p,d){return e(),t("div",null,i[0]||(i[0]=[a("",40)]))}const c=s(n,[["render",h]]);export{u as __pageData,c as default};
1
+ import{_ as s,c as e,o as t,ag as a}from"./chunks/framework.1L-BeKqY.js";const u=JSON.parse('{"title":"Instrumentation and Observability","description":"","frontmatter":{},"headers":[],"relativePath":"instrumentation.md","filePath":"instrumentation.md"}'),n={name:"instrumentation.md"};function h(r,i,o,l,p,d){return t(),e("div",null,i[0]||(i[0]=[a("",41)]))}const c=s(n,[["render",h]]);export{u as __pageData,c as default};