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