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