brut 0.0.20 → 0.0.22
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 +24 -3
- data/.nvim.lua +1 -0
- data/Dockerfile.dx +12 -3
- data/Gemfile.lock +9 -7
- data/README.md +0 -7
- data/Rakefile +6 -4
- data/bin/dev +20 -0
- data/bin/docs +27 -0
- data/bin/setup +47 -1
- data/brut-css/.nvim.lua +1 -0
- data/brut-css/README.md +28 -0
- data/brut-css/bin/build +31 -0
- data/brut-css/bin/dev +1 -0
- data/brut-css/bin/docs +15 -0
- data/brut-css/bin/setup +5 -0
- data/brut-css/config/media-queries-all.css +15 -0
- data/brut-css/config/media-queries-minimal.css +5 -0
- data/brut-css/config/postcss.config.cjs +7 -0
- data/brut-css/config/pseudo-classes-all.css +9 -0
- data/brut-css/dx +1 -0
- data/brut-css/package-lock.json +3217 -0
- data/brut-css/package.json +36 -0
- data/brut-css/src/css/appearance.css +145 -0
- data/brut-css/src/css/border.css +522 -0
- data/brut-css/src/css/colors.css +3502 -0
- data/brut-css/src/css/dimensions.css +548 -0
- data/brut-css/src/css/flex.css +179 -0
- data/brut-css/src/css/index.css +13 -0
- data/brut-css/src/css/layout.css +120 -0
- data/brut-css/src/css/list.css +41 -0
- data/brut-css/src/css/positioning.css +354 -0
- data/brut-css/src/css/properties/colors.css +455 -0
- data/brut-css/src/css/properties/index.css +3 -0
- data/brut-css/src/css/properties/spacing.css +140 -0
- data/brut-css/src/css/properties/typography.css +224 -0
- data/brut-css/src/css/reset.css +107 -0
- data/brut-css/src/css/spacing.css +585 -0
- data/brut-css/src/css/typography.css +519 -0
- data/brut-css/src/css/utils.css +104 -0
- data/brut-css/src/docs/1_getting-started/1_overview.md +46 -0
- data/brut-css/src/docs/1_getting-started/2_installation.md +25 -0
- data/brut-css/src/docs/1_getting-started/3_core-concepts.md +75 -0
- data/brut-css/src/docs/1_getting-started/4_simple-example.md +132 -0
- data/brut-css/src/docs/1_getting-started/page.html.ejs +10 -0
- data/brut-css/src/docs/2_properties/page.html.ejs +71 -0
- data/brut-css/src/docs/3_classes/color-demo.html.ejs +31 -0
- data/brut-css/src/docs/3_classes/page.html.ejs +87 -0
- data/brut-css/src/docs/4_customization/1_design-system.md +36 -0
- data/brut-css/src/docs/4_customization/2_breakpoints.md +75 -0
- data/brut-css/src/docs/4_customization/3_pseudo-classes.md +74 -0
- data/brut-css/src/docs/4_customization/4_advanced-configuration.md +40 -0
- data/brut-css/src/docs/4_customization/page.html.ejs +10 -0
- data/brut-css/src/docs/docs.css +98 -0
- data/brut-css/src/docs/includes/body-and-header.html.ejs +30 -0
- data/brut-css/src/docs/includes/footer-and-rest.html.ejs +9 -0
- data/brut-css/src/docs/includes/head.html.ejs +5 -0
- data/brut-css/src/docs/includes/nav.html.ejs +10 -0
- data/brut-css/src/docs/index.html.ejs +32 -0
- data/brut-css/src/docs/prism-twilight.min.css +1 -0
- data/brut-css/src/js/Logger.js +71 -0
- data/brut-css/src/js/build.js +111 -0
- data/brut-css/src/js/cli/CLIArgError.js +7 -0
- data/brut-css/src/js/cli/Debug.js +27 -0
- data/brut-css/src/js/cli/DocsDir.js +16 -0
- data/brut-css/src/js/cli/DocsTemplateSourceDir.js +16 -0
- data/brut-css/src/js/cli/InputFile.js +31 -0
- data/brut-css/src/js/cli/MediaQueryConfigFile.js +10 -0
- data/brut-css/src/js/cli/OutputFile.js +22 -0
- data/brut-css/src/js/cli/ParsedArg.js +17 -0
- data/brut-css/src/js/cli/PathToBrutCSSRoot.js +19 -0
- data/brut-css/src/js/cli/PseudoClassConfigFile.js +11 -0
- data/brut-css/src/js/cli.js +108 -0
- data/brut-css/src/js/docGenerator.js +467 -0
- data/brut-css/src/js/mediaQueryConfigParser.js +98 -0
- data/brut-css/src/js/post-css-plugins/addMediaQueriesPlugin.js +49 -0
- data/brut-css/src/js/post-css-plugins/addPseudoClassesPlugin.js +42 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Category.js +9 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/DocState.js +185 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Documentable.js +8 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Group.js +7 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/ParsedComment.js +73 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Property.js +9 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/PropertyCategory.js +4 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/PropertyGroup.js +8 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/Rule.js +12 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/RuleCategory.js +4 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/RuleGroup.js +8 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/SeeRef.js +5 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin/SeeURL.js +9 -0
- data/brut-css/src/js/post-css-plugins/generateDocumentationPlugin.js +49 -0
- data/brut-css/src/js/post-css-plugins/generateRootCustomPropertiesPlugin.js +45 -0
- data/brut-css/src/js/pseudoClassConfigParser.js +145 -0
- data/brut-js/.projections.json +10 -0
- data/brut-js/README.md +118 -0
- data/brut-js/bin/build +10 -0
- data/brut-js/bin/ci +5 -0
- data/brut-js/bin/setup +5 -0
- data/brut-js/docs/README.md +8 -0
- data/brut-js/docs/jsdoc-plugins/customElementTag.js +8 -0
- data/brut-js/docs/jsdoc-theme/publish.js +692 -0
- data/brut-js/docs/jsdoc-theme/static/scripts/linenumber.js +25 -0
- data/brut-js/docs/jsdoc-theme/static/scripts/prettify/Apache-License-2.0.txt +202 -0
- data/brut-js/docs/jsdoc-theme/static/scripts/prettify/lang-css.js +2 -0
- data/brut-js/docs/jsdoc-theme/static/scripts/prettify/prettify.js +28 -0
- data/brut-js/docs/jsdoc-theme/static/styles/jsdoc-default.css +327 -0
- data/brut-js/docs/jsdoc-theme/static/styles/prettify-jsdoc.css +111 -0
- data/brut-js/docs/jsdoc-theme/static/styles/prettify-tomorrow.css +132 -0
- data/brut-js/docs/jsdoc-theme/tmpl/augments.tmpl +10 -0
- data/brut-js/docs/jsdoc-theme/tmpl/container.tmpl +199 -0
- data/brut-js/docs/jsdoc-theme/tmpl/details.tmpl +143 -0
- data/brut-js/docs/jsdoc-theme/tmpl/example.tmpl +2 -0
- data/brut-js/docs/jsdoc-theme/tmpl/examples.tmpl +13 -0
- data/brut-js/docs/jsdoc-theme/tmpl/exceptions.tmpl +32 -0
- data/brut-js/docs/jsdoc-theme/tmpl/layout.tmpl +38 -0
- data/brut-js/docs/jsdoc-theme/tmpl/mainpage.tmpl +14 -0
- data/brut-js/docs/jsdoc-theme/tmpl/members.tmpl +38 -0
- data/brut-js/docs/jsdoc-theme/tmpl/method.tmpl +131 -0
- data/brut-js/docs/jsdoc-theme/tmpl/modifies.tmpl +14 -0
- data/brut-js/docs/jsdoc-theme/tmpl/params.tmpl +131 -0
- data/brut-js/docs/jsdoc-theme/tmpl/properties.tmpl +108 -0
- data/brut-js/docs/jsdoc-theme/tmpl/returns.tmpl +19 -0
- data/brut-js/docs/jsdoc-theme/tmpl/source.tmpl +8 -0
- data/brut-js/docs/jsdoc-theme/tmpl/tutorial.tmpl +19 -0
- data/brut-js/docs/jsdoc-theme/tmpl/type.tmpl +7 -0
- data/brut-js/docs/jsdoc.config.json +23 -0
- data/brut-js/docs/package-lock.json +343 -0
- data/brut-js/docs/package.json +7 -0
- data/brut-js/package-lock.json +2171 -0
- data/brut-js/package.json +32 -0
- data/brut-js/specs/AjaxSubmit.spec.js +256 -0
- data/brut-js/specs/Autosubmit.spec.js +127 -0
- data/brut-js/specs/ConfirmSubmit.spec.js +193 -0
- data/brut-js/specs/ConstraintViolationMessage.spec.js +33 -0
- data/brut-js/specs/ConstraintViolationMessages.spec.js +29 -0
- data/brut-js/specs/CopyToClipboard.spec.js +35 -0
- data/brut-js/specs/Form.spec.js +181 -0
- data/brut-js/specs/I18nTranslation.spec.js +19 -0
- data/brut-js/specs/LocaleDetection.spec.js +22 -0
- data/brut-js/specs/Message.spec.js +15 -0
- data/brut-js/specs/SpecHelper.js +23 -0
- data/brut-js/specs/Tabs.spec.js +41 -0
- data/brut-js/specs/config/asset_metadata.json +7 -0
- data/brut-js/src/AjaxSubmit.js +384 -0
- data/brut-js/src/Autosubmit.js +63 -0
- data/brut-js/src/BaseCustomElement.js +261 -0
- data/brut-js/src/ConfirmSubmit.js +116 -0
- data/brut-js/src/ConfirmationDialog.js +143 -0
- data/brut-js/src/ConstraintViolationMessage.js +125 -0
- data/brut-js/src/ConstraintViolationMessages.js +98 -0
- data/brut-js/src/CopyToClipboard.js +96 -0
- data/brut-js/src/Form.js +151 -0
- data/brut-js/src/I18nTranslation.js +61 -0
- data/brut-js/src/LocaleDetection.js +117 -0
- data/brut-js/src/Logger.js +90 -0
- data/brut-js/src/Message.js +56 -0
- data/brut-js/src/RichString.js +113 -0
- data/brut-js/src/Tabs.js +168 -0
- data/brut-js/src/Tracing.js +247 -0
- data/brut-js/src/appForTestingOnly.js +15 -0
- data/brut-js/src/index.js +130 -0
- data/brut-js/src/testing/AssetMetadata.js +35 -0
- data/brut-js/src/testing/AssetMetadataLoader.js +25 -0
- data/brut-js/src/testing/CustomElementTest.js +235 -0
- data/brut-js/src/testing/DOMCreator.js +45 -0
- data/brut-js/src/testing/index.js +48 -0
- data/brutrb.com/.vitepress/config.mjs +106 -0
- data/brutrb.com/.vitepress/plugins/jsdocLinker.js +34 -0
- data/brutrb.com/.vitepress/plugins/rdocLinker.js +18 -0
- data/brutrb.com/.vitepress/theme/custom.css +7 -0
- data/brutrb.com/.vitepress/theme/index.js +18 -0
- data/brutrb.com/.vitepress/theme/style.css +149 -0
- data/brutrb.com/ai.md +68 -0
- data/brutrb.com/assets.md +138 -0
- data/brutrb.com/bin/build +5 -0
- data/brutrb.com/bin/deploy +7 -0
- data/brutrb.com/bin/dev +5 -0
- data/brutrb.com/bin/setup +5 -0
- data/brutrb.com/brut-js.md +117 -0
- data/brutrb.com/business-logic.md +55 -0
- data/brutrb.com/cli.md +278 -0
- data/brutrb.com/components.md +243 -0
- data/brutrb.com/configuration.md +257 -0
- data/brutrb.com/css.md +103 -0
- data/brutrb.com/custom-element-tests.md +149 -0
- data/brutrb.com/database-access.md +201 -0
- data/brutrb.com/database-schema.md +312 -0
- data/brutrb.com/deployment.md +66 -0
- data/brutrb.com/dev-environment.md +179 -0
- data/brutrb.com/doc-conventions.md +39 -0
- data/brutrb.com/end-to-end-tests.md +174 -0
- data/brutrb.com/flash-and-session.md +224 -0
- data/brutrb.com/forms.md +866 -0
- data/brutrb.com/getting-started.md +66 -0
- data/brutrb.com/handlers.md +153 -0
- data/brutrb.com/hooks.md +178 -0
- data/brutrb.com/i18n.md +188 -0
- data/brutrb.com/images/Makefile +10 -0
- data/brutrb.com/images/dev-env-overview.dot +54 -0
- data/brutrb.com/images/dev-env-overview.png +0 -0
- data/brutrb.com/images/dev-env-protocol.dot +37 -0
- data/brutrb.com/images/dev-env-protocol.png +0 -0
- data/brutrb.com/images/logo-300.png +0 -0
- data/brutrb.com/images/logo.png +0 -0
- data/brutrb.com/images/overview.graffle +0 -0
- data/brutrb.com/images/overview.png +0 -0
- data/brutrb.com/images/spa.dot +19 -0
- data/brutrb.com/images/spa.png +0 -0
- data/brutrb.com/images/workspace-protocol.dot +44 -0
- data/brutrb.com/images/workspace-protocol.png +0 -0
- data/brutrb.com/index.md +36 -0
- data/brutrb.com/instrumentation.md +183 -0
- data/brutrb.com/javascript.md +122 -0
- data/brutrb.com/jobs.md +14 -0
- data/{doc-src → brutrb.com}/keyword-injection.md +122 -68
- data/brutrb.com/markdown-examples.md +85 -0
- data/brutrb.com/middleware.md +80 -0
- data/brutrb.com/not-released.md +5 -0
- data/brutrb.com/overview.md +404 -0
- data/brutrb.com/package-lock.json +2404 -0
- data/brutrb.com/package.json +11 -0
- data/brutrb.com/pages.md +378 -0
- data/brutrb.com/public/images/logo-300.png +0 -0
- data/brutrb.com/public/images/logo.png +0 -0
- data/brutrb.com/routes.md +215 -0
- data/brutrb.com/security.md +105 -0
- data/brutrb.com/seed-data.md +63 -0
- data/brutrb.com/space-time-continuum.md +85 -0
- data/brutrb.com/tutorial.md +3 -0
- data/brutrb.com/unit-tests.md +148 -0
- data/docker-compose.dx.yml +6 -3
- data/docs/404.html +21 -0
- data/docs/CNAME +1 -0
- data/docs/ai.html +24 -0
- data/docs/api/Brut/BackEnd/SeedData.html +493 -0
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server/FlushSpans.html +214 -0
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares/Server.html +125 -0
- data/docs/api/Brut/BackEnd/Sidekiq/Middlewares.html +125 -0
- data/docs/api/Brut/BackEnd/Sidekiq.html +125 -0
- data/docs/api/Brut/BackEnd/Validators/FormValidator.html +414 -0
- data/docs/api/Brut/BackEnd/Validators.html +128 -0
- data/docs/api/Brut/BackEnd.html +132 -0
- data/docs/api/Brut/CLI/App.html +1576 -0
- data/docs/api/Brut/CLI/AppRunner.html +491 -0
- data/docs/api/Brut/CLI/Apps/BuildAssets/All.html +264 -0
- data/docs/api/Brut/CLI/Apps/BuildAssets/CSS.html +306 -0
- data/docs/api/Brut/CLI/Apps/BuildAssets/Images.html +262 -0
- data/docs/api/Brut/CLI/Apps/BuildAssets/JS.html +314 -0
- data/docs/api/Brut/CLI/Apps/BuildAssets.html +183 -0
- data/docs/api/Brut/CLI/Apps/DB/Create.html +365 -0
- data/docs/api/Brut/CLI/Apps/DB/Drop.html +357 -0
- data/docs/api/Brut/CLI/Apps/DB/Migrate.html +383 -0
- data/docs/api/Brut/CLI/Apps/DB/NewMigration.html +335 -0
- data/docs/api/Brut/CLI/Apps/DB/Rebuild.html +329 -0
- data/docs/api/Brut/CLI/Apps/DB/Seed.html +347 -0
- data/docs/api/Brut/CLI/Apps/DB/Status.html +383 -0
- data/docs/api/Brut/CLI/Apps/DB.html +183 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Action/Route.html +303 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Action.html +512 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Component.html +398 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/CustomElementTest.html +374 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/E2ETest.html +410 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Form.html +262 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Page/Route.html +303 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Page.html +480 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/RoutesEditor.html +450 -0
- data/docs/api/Brut/CLI/Apps/Scaffold/Test.html +380 -0
- data/docs/api/Brut/CLI/Apps/Scaffold.html +253 -0
- data/docs/api/Brut/CLI/Apps/Test/Audit.html +464 -0
- data/docs/api/Brut/CLI/Apps/Test/E2e.html +407 -0
- data/docs/api/Brut/CLI/Apps/Test/JS.html +262 -0
- data/docs/api/Brut/CLI/Apps/Test/Run.html +578 -0
- data/docs/api/Brut/CLI/Apps/Test.html +253 -0
- data/docs/api/Brut/CLI/Apps.html +125 -0
- data/docs/api/Brut/CLI/Command.html +2342 -0
- data/docs/api/Brut/CLI/Error.html +139 -0
- data/docs/api/Brut/CLI/ExecutionResults/Result.html +664 -0
- data/docs/api/Brut/CLI/ExecutionResults.html +675 -0
- data/docs/api/Brut/CLI/Executor.html +430 -0
- data/docs/api/Brut/CLI/InvalidOption.html +245 -0
- data/docs/api/Brut/CLI/Options.html +753 -0
- data/docs/api/Brut/CLI/Output.html +699 -0
- data/docs/api/Brut/CLI/SystemExecError.html +451 -0
- data/docs/api/Brut/CLI.html +263 -0
- data/docs/api/Brut/FactoryBot.html +225 -0
- data/docs/api/Brut/Framework/App.html +1097 -0
- data/docs/api/Brut/Framework/Config.html +1045 -0
- data/docs/api/Brut/Framework/Container.html +1379 -0
- data/docs/api/Brut/Framework/Error.html +140 -0
- data/docs/api/Brut/Framework/Errors/AbstractMethod.html +144 -0
- data/docs/api/Brut/Framework/Errors/Bug.html +234 -0
- data/docs/api/Brut/Framework/Errors/MissingConfiguration.html +257 -0
- data/docs/api/Brut/Framework/Errors/MissingParameter.html +273 -0
- data/docs/api/Brut/Framework/Errors/NoClassForPath.html +471 -0
- data/docs/api/Brut/Framework/Errors/NotFound.html +308 -0
- data/docs/api/Brut/Framework/Errors/NotImplemented.html +234 -0
- data/docs/api/Brut/Framework/Errors.html +328 -0
- data/docs/api/Brut/Framework/FussyTypeEnforcement.html +392 -0
- data/docs/api/Brut/Framework/MCP.html +861 -0
- data/docs/api/Brut/Framework/ProjectEnvironment.html +648 -0
- data/docs/api/Brut/Framework.html +129 -0
- data/docs/api/Brut/FrontEnd/AssetPathResolver.html +317 -0
- data/docs/api/Brut/FrontEnd/Component/Helpers.html +326 -0
- data/docs/api/Brut/FrontEnd/Component.html +365 -0
- data/docs/api/Brut/FrontEnd/Components/ConstraintViolations.html +470 -0
- data/docs/api/Brut/FrontEnd/Components/FormTag.html +518 -0
- data/docs/api/Brut/FrontEnd/Components/I18nTranslations.html +317 -0
- data/docs/api/Brut/FrontEnd/Components/Input.html +195 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs/CsrfToken.html +339 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs/InputTag.html +660 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs/RadioButton.html +417 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs/SelectTagWithOptions.html +918 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs/TextareaTag.html +651 -0
- data/docs/api/Brut/FrontEnd/Components/Inputs.html +125 -0
- data/docs/api/Brut/FrontEnd/Components/LocaleDetection.html +367 -0
- data/docs/api/Brut/FrontEnd/Components/PageIdentifier.html +336 -0
- data/docs/api/Brut/FrontEnd/Components/TimeTag.html +655 -0
- data/docs/api/Brut/FrontEnd/Components/Traceparent.html +352 -0
- data/docs/api/Brut/FrontEnd/Components.html +135 -0
- data/docs/api/Brut/FrontEnd/Download.html +467 -0
- data/docs/api/Brut/FrontEnd/Flash.html +1150 -0
- data/docs/api/Brut/FrontEnd/Form.html +1157 -0
- data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +634 -0
- data/docs/api/Brut/FrontEnd/Forms/Input.html +615 -0
- data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +547 -0
- data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +1318 -0
- data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +609 -0
- data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +587 -0
- data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +613 -0
- data/docs/api/Brut/FrontEnd/Forms/SelectInputDefinition.html +582 -0
- data/docs/api/Brut/FrontEnd/Forms/ValidityState.html +609 -0
- data/docs/api/Brut/FrontEnd/Forms.html +127 -0
- data/docs/api/Brut/FrontEnd/GenericResponse.html +377 -0
- data/docs/api/Brut/FrontEnd/Handler.html +442 -0
- data/docs/api/Brut/FrontEnd/Handlers/CspReportingHandler.html +318 -0
- data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler/TraceParent.html +336 -0
- data/docs/api/Brut/FrontEnd/Handlers/InstrumentationHandler.html +399 -0
- data/docs/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html +354 -0
- data/docs/api/Brut/FrontEnd/Handlers/MissingHandler/Form.html +151 -0
- data/docs/api/Brut/FrontEnd/Handlers/MissingHandler.html +315 -0
- data/docs/api/Brut/FrontEnd/Handlers.html +125 -0
- data/docs/api/Brut/FrontEnd/HandlingResults.html +339 -0
- data/docs/api/Brut/FrontEnd/HttpMethod.html +661 -0
- data/docs/api/Brut/FrontEnd/HttpStatus.html +496 -0
- data/docs/api/Brut/FrontEnd/InlineSvgLocator.html +284 -0
- data/docs/api/Brut/FrontEnd/Layout.html +318 -0
- data/docs/api/Brut/FrontEnd/Middleware.html +135 -0
- data/docs/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html +288 -0
- data/docs/api/Brut/FrontEnd/Middlewares/Favicon.html +292 -0
- data/docs/api/Brut/FrontEnd/Middlewares/OpenTelemetrySpan.html +324 -0
- data/docs/api/Brut/FrontEnd/Middlewares/ReloadApp.html +372 -0
- data/docs/api/Brut/FrontEnd/Middlewares.html +125 -0
- data/docs/api/Brut/FrontEnd/Page.html +773 -0
- data/docs/api/Brut/FrontEnd/Pages/MissingPage.html +797 -0
- data/docs/api/Brut/FrontEnd/Pages.html +125 -0
- data/docs/api/Brut/FrontEnd/RequestContext.html +1312 -0
- data/docs/api/Brut/FrontEnd/RouteHook.html +424 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/AgeFlash.html +242 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineScripts.html +249 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts/ReportOnly.html +264 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html +261 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/LocaleDetection.html +284 -0
- data/docs/api/Brut/FrontEnd/RouteHooks/SetupRequestContext.html +252 -0
- data/docs/api/Brut/FrontEnd/RouteHooks.html +115 -0
- data/docs/api/Brut/FrontEnd/Routing/FormHandlerRoute.html +227 -0
- data/docs/api/Brut/FrontEnd/Routing/FormRoute.html +305 -0
- data/docs/api/Brut/FrontEnd/Routing/MissingForm.html +324 -0
- data/docs/api/Brut/FrontEnd/Routing/MissingHandler.html +319 -0
- data/docs/api/Brut/FrontEnd/Routing/MissingPage.html +315 -0
- data/docs/api/Brut/FrontEnd/Routing/MissingPath.html +315 -0
- data/docs/api/Brut/FrontEnd/Routing/PageRoute.html +327 -0
- data/docs/api/Brut/FrontEnd/Routing/Route.html +761 -0
- data/docs/api/Brut/FrontEnd/Routing.html +927 -0
- data/docs/api/Brut/FrontEnd/Session.html +1195 -0
- data/docs/api/Brut/FrontEnd.html +134 -0
- data/docs/api/Brut/I18n/BaseMethods.html +931 -0
- data/docs/api/Brut/I18n/ForBackEnd.html +302 -0
- data/docs/api/Brut/I18n/ForCLI.html +302 -0
- data/docs/api/Brut/I18n/ForHTML.html +296 -0
- data/docs/api/Brut/I18n/HTTPAcceptLanguage/AlwaysEnglish.html +316 -0
- data/docs/api/Brut/I18n/HTTPAcceptLanguage.html +930 -0
- data/docs/api/Brut/I18n.html +127 -0
- data/docs/api/Brut/Instrumentation/LoggerSpanExporter.html +435 -0
- data/docs/api/Brut/Instrumentation/OpenTelemetry/NormalizedAttributes.html +286 -0
- data/docs/api/Brut/Instrumentation/OpenTelemetry/Span.html +302 -0
- data/docs/api/Brut/Instrumentation/OpenTelemetry.html +864 -0
- data/docs/api/Brut/Instrumentation.html +126 -0
- data/docs/api/Brut/SinatraHelpers/ClassMethods.html +532 -0
- data/docs/api/Brut/SinatraHelpers.html +281 -0
- data/docs/api/Brut/SpecSupport/ClockSupport.html +383 -0
- data/docs/api/Brut/SpecSupport/ComponentSupport.html +502 -0
- data/docs/api/Brut/SpecSupport/E2ETestServer.html +503 -0
- data/docs/api/Brut/SpecSupport/E2eSupport.html +142 -0
- data/docs/api/Brut/SpecSupport/EnhancedNode.html +403 -0
- data/docs/api/Brut/SpecSupport/FlashSupport.html +278 -0
- data/docs/api/Brut/SpecSupport/GeneralSupport/ClassMethods.html +401 -0
- data/docs/api/Brut/SpecSupport/GeneralSupport.html +195 -0
- data/docs/api/Brut/SpecSupport/HandlerSupport.html +160 -0
- data/docs/api/Brut/SpecSupport/Matchers/HaveConstraintViolation.html +553 -0
- data/docs/api/Brut/SpecSupport/Matchers/HaveHTMLAttribute.html +439 -0
- data/docs/api/Brut/SpecSupport/Matchers.html +125 -0
- data/docs/api/Brut/SpecSupport/RSpecSetup/OptionalSidekiqSupport.html +335 -0
- data/docs/api/Brut/SpecSupport/RSpecSetup.html +602 -0
- data/docs/api/Brut/SpecSupport/SessionSupport.html +196 -0
- data/docs/api/Brut/SpecSupport.html +129 -0
- data/docs/api/Brut.html +225 -0
- data/docs/api/Clock.html +603 -0
- data/docs/api/RichString.html +968 -0
- data/docs/api/SemanticLogger/Appender/Async.html +219 -0
- data/docs/api/Sequel/Extensions/BrutInstrumentation.html +115 -0
- data/docs/api/Sequel/Extensions/BrutMigrations.html +533 -0
- data/docs/api/Sequel/Extensions.html +117 -0
- data/docs/api/Sequel/Plugins/CreatedAt/InstanceMethods.html +105 -0
- data/docs/api/Sequel/Plugins/CreatedAt.html +125 -0
- data/docs/api/Sequel/Plugins/ExternalId/ClassMethods.html +207 -0
- data/docs/api/Sequel/Plugins/ExternalId/InstanceMethods.html +186 -0
- data/docs/api/Sequel/Plugins/ExternalId.html +218 -0
- data/docs/api/Sequel/Plugins/FindBang/ClassMethods.html +202 -0
- data/docs/api/Sequel/Plugins/FindBang.html +125 -0
- data/docs/api/Sequel/Plugins.html +117 -0
- data/docs/api/Sequel.html +117 -0
- data/docs/api/_index.html +1553 -0
- data/docs/api/class_list.html +54 -0
- data/docs/api/css/common.css +1 -0
- data/docs/api/css/full_list.css +58 -0
- data/docs/api/css/style.css +503 -0
- data/docs/api/file.README.html +127 -0
- data/docs/api/file_list.html +59 -0
- data/docs/api/frames.html +22 -0
- data/docs/api/index.html +127 -0
- data/docs/api/js/app.js +344 -0
- data/docs/api/js/full_list.js +242 -0
- data/docs/api/js/jquery.js +4 -0
- data/docs/api/method_list.html +3998 -0
- data/docs/api/top-level-namespace.html +112 -0
- data/docs/assets/ai.md.tZrjP9im.js +1 -0
- data/docs/assets/ai.md.tZrjP9im.lean.js +1 -0
- data/docs/assets/app.D_yaTITQ.js +1 -0
- data/docs/assets/assets.md.D3wunzLx.js +19 -0
- data/docs/assets/assets.md.D3wunzLx.lean.js +1 -0
- data/docs/assets/brut-js.md.o2DAO2s2.js +12 -0
- data/docs/assets/brut-js.md.o2DAO2s2.lean.js +1 -0
- data/docs/assets/business-logic.md.BY4hGy0m.js +1 -0
- data/docs/assets/business-logic.md.BY4hGy0m.lean.js +1 -0
- data/docs/assets/chunks/@localSearchIndexroot.BsN5i0Fi.js +1 -0
- data/docs/assets/chunks/VPLocalSearchBox.B2-ZzyTY.js +8 -0
- data/docs/assets/chunks/framework.1L-BeKqY.js +18 -0
- data/docs/assets/chunks/theme.CfGFVRvE.js +2 -0
- data/docs/assets/cli.md.RmeA2b0i.js +127 -0
- data/docs/assets/cli.md.RmeA2b0i.lean.js +1 -0
- data/docs/assets/components.md.eCttGlN-.js +104 -0
- data/docs/assets/components.md.eCttGlN-.lean.js +1 -0
- data/docs/assets/configuration.md.BRriU0cL.js +78 -0
- data/docs/assets/configuration.md.BRriU0cL.lean.js +1 -0
- data/docs/assets/css.md.DJgj2clw.js +21 -0
- data/docs/assets/css.md.DJgj2clw.lean.js +1 -0
- data/docs/assets/custom-element-tests.md.BrYJQEl3.js +69 -0
- data/docs/assets/custom-element-tests.md.BrYJQEl3.lean.js +1 -0
- data/docs/assets/database-access.md.C7l-Vuvb.js +63 -0
- data/docs/assets/database-access.md.C7l-Vuvb.lean.js +1 -0
- data/docs/assets/database-schema.md.BUjR0VS1.js +63 -0
- data/docs/assets/database-schema.md.BUjR0VS1.lean.js +1 -0
- data/docs/assets/deployment.md.Dbka4OTr.js +1 -0
- data/docs/assets/deployment.md.Dbka4OTr.lean.js +1 -0
- data/docs/assets/dev-env-overview.Gj7NWM8-.png +0 -0
- data/docs/assets/dev-env-protocol.DysDAtnz.png +0 -0
- data/docs/assets/dev-environment.md.BNc8AYiK.js +11 -0
- data/docs/assets/dev-environment.md.BNc8AYiK.lean.js +1 -0
- data/docs/assets/doc-conventions.md.DCfRXXi-.js +1 -0
- data/docs/assets/doc-conventions.md.DCfRXXi-.lean.js +1 -0
- data/docs/assets/end-to-end-tests.md.yfQHC0b5.js +26 -0
- data/docs/assets/end-to-end-tests.md.yfQHC0b5.lean.js +1 -0
- data/docs/assets/flash-and-session.md.BXY8RvT0.js +93 -0
- data/docs/assets/flash-and-session.md.BXY8RvT0.lean.js +1 -0
- data/docs/assets/forms.md.CBTYQ_Cz.js +379 -0
- data/docs/assets/forms.md.CBTYQ_Cz.lean.js +1 -0
- data/docs/assets/getting-started.md.Bz2s1Vjb.js +2 -0
- data/docs/assets/getting-started.md.Bz2s1Vjb.lean.js +1 -0
- data/docs/assets/handlers.md.089DVD3v.js +69 -0
- data/docs/assets/handlers.md.089DVD3v.lean.js +1 -0
- data/docs/assets/hooks.md.C4-moMny.js +80 -0
- data/docs/assets/hooks.md.C4-moMny.lean.js +1 -0
- data/docs/assets/i18n.md.Do9i1qWl.js +23 -0
- data/docs/assets/i18n.md.Do9i1qWl.lean.js +1 -0
- data/docs/assets/index.md.B28EwVpq.js +1 -0
- data/docs/assets/index.md.B28EwVpq.lean.js +1 -0
- data/docs/assets/instrumentation.md.CL6ax7nT.js +35 -0
- data/docs/assets/instrumentation.md.CL6ax7nT.lean.js +1 -0
- data/docs/assets/javascript.md.GWbhRS51.js +31 -0
- data/docs/assets/javascript.md.GWbhRS51.lean.js +1 -0
- data/docs/assets/jobs.md.S-2amAYp.js +1 -0
- data/docs/assets/jobs.md.S-2amAYp.lean.js +1 -0
- data/docs/assets/keyword-injection.md.Dt2tKREs.js +25 -0
- data/docs/assets/keyword-injection.md.Dt2tKREs.lean.js +1 -0
- data/docs/assets/markdown-examples.md.CCFEQO44.js +33 -0
- data/docs/assets/markdown-examples.md.CCFEQO44.lean.js +1 -0
- data/docs/assets/middleware.md.Czz_UlJN.js +20 -0
- data/docs/assets/middleware.md.Czz_UlJN.lean.js +1 -0
- data/docs/assets/not-released.md.BBy28McC.js +1 -0
- data/docs/assets/not-released.md.BBy28McC.lean.js +1 -0
- data/docs/assets/overview.Da81cB9R.png +0 -0
- data/docs/assets/overview.md.CDalkuxV.js +133 -0
- data/docs/assets/overview.md.CDalkuxV.lean.js +1 -0
- data/docs/assets/pages.md.BE3kfOc5.js +122 -0
- data/docs/assets/pages.md.BE3kfOc5.lean.js +1 -0
- data/docs/assets/routes.md.BMM7peut.js +29 -0
- data/docs/assets/routes.md.BMM7peut.lean.js +1 -0
- data/docs/assets/security.md.C668yXCi.js +1 -0
- data/docs/assets/security.md.C668yXCi.lean.js +1 -0
- data/docs/assets/seed-data.md.BvFZlqIk.js +14 -0
- data/docs/assets/seed-data.md.BvFZlqIk.lean.js +1 -0
- data/docs/assets/spa.qejUdp-5.png +0 -0
- data/docs/assets/space-time-continuum.md.KPUIKysQ.js +1 -0
- data/docs/assets/space-time-continuum.md.KPUIKysQ.lean.js +1 -0
- data/docs/assets/style.D73IYGCX.css +1 -0
- data/docs/assets/tutorial.md.BnoGjrdK.js +1 -0
- data/docs/assets/tutorial.md.BnoGjrdK.lean.js +1 -0
- data/docs/assets/unit-tests.md.DUGrnLj5.js +13 -0
- data/docs/assets/unit-tests.md.DUGrnLj5.lean.js +1 -0
- data/docs/assets/workspace-protocol.C0gXsoDb.png +0 -0
- data/docs/assets.html +42 -0
- data/docs/brut-css/brut.css +1 -0
- data/docs/brut-css/brut.max.css +22372 -0
- data/docs/brut-css/classes/appearances.html +783 -0
- data/docs/brut-css/classes/background-colors.html +3529 -0
- data/docs/brut-css/classes/border-colors.html +3529 -0
- data/docs/brut-css/classes/borders.html +2293 -0
- data/docs/brut-css/classes/dimensions.html +2581 -0
- data/docs/brut-css/classes/flex.html +917 -0
- data/docs/brut-css/classes/foreground-colors.html +3261 -0
- data/docs/brut-css/classes/junk-drawer.html +431 -0
- data/docs/brut-css/classes/layout.html +668 -0
- data/docs/brut-css/classes/lists.html +331 -0
- data/docs/brut-css/classes/positioning.html +1751 -0
- data/docs/brut-css/classes/spacings.html +2633 -0
- data/docs/brut-css/classes/typography.html +2206 -0
- data/docs/brut-css/customization/advanced-configuration.html +204 -0
- data/docs/brut-css/customization/breakpoints.html +227 -0
- data/docs/brut-css/customization/design-system.html +197 -0
- data/docs/brut-css/customization/pseudo-classes.html +228 -0
- data/docs/brut-css/docs.css +98 -0
- data/docs/brut-css/getting-started/core-concepts.html +234 -0
- data/docs/brut-css/getting-started/installation.html +190 -0
- data/docs/brut-css/getting-started/overview.html +210 -0
- data/docs/brut-css/getting-started/simple-example.html +285 -0
- data/docs/brut-css/index.html +193 -0
- data/docs/brut-css/prism-twilight.min.css +1 -0
- data/docs/brut-css/properties/colors.html +1548 -0
- data/docs/brut-css/properties/spacings.html +614 -0
- data/docs/brut-css/properties/typography.html +777 -0
- data/docs/brut-js/api/AjaxSubmit.html +374 -0
- data/docs/brut-js/api/AjaxSubmit.js.html +435 -0
- data/docs/brut-js/api/Autosubmit.html +192 -0
- data/docs/brut-js/api/Autosubmit.js.html +114 -0
- data/docs/brut-js/api/BaseCustomElement.html +1091 -0
- data/docs/brut-js/api/BaseCustomElement.js.html +312 -0
- data/docs/brut-js/api/BrutCustomElements.html +172 -0
- data/docs/brut-js/api/BufferedLogger.html +173 -0
- data/docs/brut-js/api/ConfirmSubmit.html +278 -0
- data/docs/brut-js/api/ConfirmSubmit.js.html +167 -0
- data/docs/brut-js/api/ConfirmationDialog.html +425 -0
- data/docs/brut-js/api/ConfirmationDialog.js.html +194 -0
- data/docs/brut-js/api/ConstraintViolationMessage.html +448 -0
- data/docs/brut-js/api/ConstraintViolationMessage.js.html +176 -0
- data/docs/brut-js/api/ConstraintViolationMessages.html +590 -0
- data/docs/brut-js/api/ConstraintViolationMessages.js.html +149 -0
- data/docs/brut-js/api/CopyToClipboard.html +345 -0
- data/docs/brut-js/api/CopyToClipboard.js.html +147 -0
- data/docs/brut-js/api/Form.html +294 -0
- data/docs/brut-js/api/Form.js.html +202 -0
- data/docs/brut-js/api/I18nTranslation.html +409 -0
- data/docs/brut-js/api/I18nTranslation.js.html +112 -0
- data/docs/brut-js/api/LocaleDetection.html +312 -0
- data/docs/brut-js/api/LocaleDetection.js.html +168 -0
- data/docs/brut-js/api/Logger.html +702 -0
- data/docs/brut-js/api/Logger.js.html +141 -0
- data/docs/brut-js/api/Message.html +238 -0
- data/docs/brut-js/api/Message.js.html +107 -0
- data/docs/brut-js/api/PrefixedLogger.html +369 -0
- data/docs/brut-js/api/RichString.html +1049 -0
- data/docs/brut-js/api/RichString.js.html +164 -0
- data/docs/brut-js/api/Tabs.html +295 -0
- data/docs/brut-js/api/Tabs.js.html +219 -0
- data/docs/brut-js/api/Tracing.html +277 -0
- data/docs/brut-js/api/Tracing.js.html +298 -0
- data/docs/brut-js/api/external-CustomElementRegistry.html +140 -0
- data/docs/brut-js/api/external-Performance.html +138 -0
- data/docs/brut-js/api/external-Promise.html +138 -0
- data/docs/brut-js/api/external-ValidityState.html +138 -0
- data/docs/brut-js/api/external-Window.html +233 -0
- data/docs/brut-js/api/external-fetch.html +138 -0
- data/docs/brut-js/api/global.html +400 -0
- data/docs/brut-js/api/index.html +168 -0
- data/docs/brut-js/api/index.js.html +181 -0
- data/docs/brut-js/api/module-testing.html +383 -0
- data/docs/brut-js/api/scripts/linenumber.js +25 -0
- data/docs/brut-js/api/scripts/prettify/Apache-License-2.0.txt +202 -0
- data/docs/brut-js/api/scripts/prettify/lang-css.js +2 -0
- data/docs/brut-js/api/scripts/prettify/prettify.js +28 -0
- data/docs/brut-js/api/styles/jsdoc-default.css +327 -0
- data/docs/brut-js/api/styles/prettify-jsdoc.css +111 -0
- data/docs/brut-js/api/styles/prettify-tomorrow.css +132 -0
- data/docs/brut-js/api/testing.AssetMetadata.html +172 -0
- data/docs/brut-js/api/testing.AssetMetadataLoader.html +171 -0
- data/docs/brut-js/api/testing.CustomElementTest.html +679 -0
- data/docs/brut-js/api/testing.DOMCreator.html +171 -0
- data/docs/brut-js/api/testing_AssetMetadata.js.html +86 -0
- data/docs/brut-js/api/testing_AssetMetadataLoader.js.html +76 -0
- data/docs/brut-js/api/testing_CustomElementTest.js.html +286 -0
- data/docs/brut-js/api/testing_DOMCreator.js.html +96 -0
- data/docs/brut-js/api/testing_index.js.html +99 -0
- data/docs/brut-js.html +35 -0
- data/docs/business-logic.html +24 -0
- data/docs/cli.html +150 -0
- data/docs/components.html +127 -0
- data/docs/configuration.html +101 -0
- data/docs/css.html +44 -0
- data/docs/custom-element-tests.html +92 -0
- data/docs/database-access.html +86 -0
- data/docs/database-schema.html +86 -0
- data/docs/deployment.html +24 -0
- data/docs/dev-environment.html +34 -0
- data/docs/doc-conventions.html +24 -0
- data/docs/end-to-end-tests.html +49 -0
- data/docs/flash-and-session.html +116 -0
- data/docs/forms.html +402 -0
- data/docs/getting-started.html +25 -0
- data/docs/handlers.html +92 -0
- data/docs/hashmap.json +1 -0
- data/docs/hooks.html +103 -0
- data/docs/i18n.html +46 -0
- data/docs/images/logo-300.png +0 -0
- data/docs/images/logo.png +0 -0
- data/docs/index.html +24 -0
- data/docs/instrumentation.html +58 -0
- data/docs/javascript.html +54 -0
- data/docs/jobs.html +24 -0
- data/docs/keyword-injection.html +48 -0
- data/docs/markdown-examples.html +56 -0
- data/docs/middleware.html +43 -0
- data/docs/not-released.html +24 -0
- data/docs/overview.html +156 -0
- data/docs/pages.html +145 -0
- data/docs/routes.html +52 -0
- data/docs/security.html +24 -0
- data/docs/seed-data.html +37 -0
- data/docs/space-time-continuum.html +24 -0
- data/docs/tutorial.html +24 -0
- data/docs/unit-tests.html +36 -0
- data/docs/vp-icons.css +1 -0
- data/lib/brut/back_end/seed_data.rb +19 -2
- data/lib/brut/back_end/sidekiq/middlewares/server.rb +2 -1
- data/lib/brut/back_end/sidekiq/middlewares.rb +2 -1
- data/lib/brut/back_end/sidekiq.rb +2 -1
- data/lib/brut/back_end/validator.rb +5 -1
- data/lib/brut/back_end.rb +4 -2
- data/lib/brut/cli/app_runner.rb +1 -1
- data/lib/brut/cli/apps/test.rb +5 -0
- data/lib/brut/cli.rb +4 -3
- data/lib/brut/factory_bot.rb +0 -5
- data/lib/brut/framework/app.rb +70 -5
- data/lib/brut/framework/config.rb +5 -3
- data/lib/brut/framework/container.rb +3 -2
- data/lib/brut/framework/errors.rb +12 -4
- data/lib/brut/framework/mcp.rb +58 -1
- data/lib/brut/framework/project_environment.rb +6 -2
- data/lib/brut/framework.rb +1 -1
- data/lib/brut/front_end/component.rb +69 -71
- data/lib/brut/front_end/components/constraint_violations.rb +1 -4
- data/lib/brut/front_end/components/form_tag.rb +1 -1
- data/lib/brut/front_end/components/input.rb +3 -3
- data/lib/brut/front_end/components/inputs/csrf_token.rb +1 -1
- data/lib/brut/front_end/components/inputs/{text_field.rb → input_tag.rb} +7 -9
- data/lib/brut/front_end/components/inputs/radio_button.rb +1 -1
- data/lib/brut/front_end/components/inputs/select_tag_with_options.rb +187 -0
- data/lib/brut/front_end/components/inputs/{textarea.rb → textarea_tag.rb} +2 -2
- data/lib/brut/front_end/components/time_tag.rb +2 -1
- data/lib/brut/front_end/form.rb +4 -4
- data/lib/brut/front_end/forms/input.rb +2 -1
- data/lib/brut/front_end/forms/input_definition.rb +5 -2
- data/lib/brut/front_end/forms/radio_button_group_input.rb +2 -1
- data/lib/brut/front_end/forms/radio_button_group_input_definition.rb +2 -2
- data/lib/brut/front_end/forms/select_input.rb +2 -4
- data/lib/brut/front_end/forms/select_input_definition.rb +2 -2
- data/lib/brut/front_end/handler.rb +28 -26
- data/lib/brut/front_end/handlers/csp_reporting_handler.rb +5 -2
- data/lib/brut/front_end/handlers/instrumentation_handler.rb +8 -4
- data/lib/brut/front_end/handlers/locale_detection_handler.rb +9 -5
- data/lib/brut/front_end/handlers/missing_handler.rb +5 -2
- data/lib/brut/front_end/layout.rb +16 -0
- data/lib/brut/front_end/page.rb +52 -29
- data/lib/brut/front_end/request_context.rb +3 -2
- data/lib/brut/front_end/routing.rb +5 -1
- data/lib/brut/front_end.rb +4 -13
- data/lib/brut/i18n/base_methods.rb +167 -79
- data/lib/brut/i18n/for_back_end.rb +4 -0
- data/lib/brut/i18n/for_cli.rb +4 -0
- data/lib/brut/i18n/for_html.rb +32 -4
- data/lib/brut/i18n/http_accept_language.rb +47 -0
- data/lib/brut/instrumentation/open_telemetry.rb +36 -1
- data/lib/brut/instrumentation.rb +3 -5
- data/lib/brut/sinatra_helpers.rb +11 -3
- data/lib/brut/spec_support/component_support.rb +30 -16
- data/lib/brut/spec_support/e2e_support.rb +1 -1
- data/lib/brut/spec_support/e2e_test_server.rb +3 -0
- data/lib/brut/spec_support/general_support.rb +3 -0
- data/lib/brut/spec_support/handler_support.rb +6 -1
- data/lib/brut/spec_support/matcher.rb +1 -0
- data/lib/brut/spec_support/matchers/be_page_for.rb +1 -0
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +1 -0
- data/lib/brut/spec_support/matchers/have_i18n_string.rb +2 -5
- data/lib/brut/spec_support/matchers/have_link_to.rb +1 -0
- data/lib/brut/spec_support/matchers/have_redirected_to.rb +1 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +1 -0
- data/lib/brut/spec_support/matchers/have_returned_rack_response.rb +44 -0
- data/lib/brut/spec_support.rb +1 -1
- data/lib/brut/version.rb +1 -1
- data/lib/brut.rb +5 -4
- data/lib/sequel/extensions/brut_migrations.rb +1 -1
- metadata +648 -13
- data/doc-src/architecture.md +0 -102
- data/doc-src/assets.md +0 -98
- data/doc-src/forms.md +0 -214
- data/doc-src/handlers.md +0 -83
- data/doc-src/javascript.md +0 -265
- data/doc-src/pages.md +0 -210
- data/doc-src/route-hooks.md +0 -59
- data/lib/brut/front_end/components/inputs/select.rb +0 -117
data/brutrb.com/forms.md
ADDED
@@ -0,0 +1,866 @@
|
|
1
|
+
# Forms
|
2
|
+
|
3
|
+
The most common way for a web site visitor to submit data to the server is to submit a form. The Web Platform's forms API is much like an uncle you may have: old and rich.
|
4
|
+
|
5
|
+
Brut's forms module solves three problems:
|
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.
|
11
|
+
|
12
|
+
## Overview
|
13
|
+
|
14
|
+
The forms module has a lot of moving parts, but the general process of using forms is:
|
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:
|
40
|
+
|
41
|
+
* There should be a form class, in this example named `LoginForm`
|
42
|
+
* There should be a handler class, in this example named `LoginHandler`.
|
43
|
+
|
44
|
+
When a browser issues an HTTP `POST` to `/login`, the forms contents will populate an instance of
|
45
|
+
`LoginForm`, which will be given to an instance of a `LoginHandler` to process the form submission.
|
46
|
+
|
47
|
+
First, let's go through the bare minimum of form processing.
|
48
|
+
|
49
|
+
### Simplest Case of Form Processing
|
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.
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
# app/src/front_end/pages/login_page.rb
|
75
|
+
class LoginPage < AppPage
|
76
|
+
def initialize
|
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
|
87
|
+
end
|
88
|
+
```
|
89
|
+
|
90
|
+
Brut can generate the HTML for the needed inputs via `Brut::FrontEnd::Components::Inputs::TextField.for_form_input`, which is a very long 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.for_form_input(form: @form, input_name: :email)
|
104
|
+
Brut::FrontEnd::Components::Inputs::TextField.for_form_input(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
|
+
|
133
|
+
# ...
|
134
|
+
end
|
135
|
+
```
|
136
|
+
|
137
|
+
This allows you to call `Inputs::TextField.for_form_input`:
|
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.for_form_input(form: @form, input_name: :email)
|
152
|
+
Inputs::TextField.for_form_input(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:
|
174
|
+
|
175
|
+
```ruby
|
176
|
+
# app/src/front_end/handlers/login_handler.rb
|
177
|
+
class LoginHandler < AppHandler
|
178
|
+
def initializer(form:)
|
179
|
+
@form = form
|
180
|
+
end
|
181
|
+
def handle
|
182
|
+
# ...
|
183
|
+
end
|
184
|
+
end
|
185
|
+
```
|
186
|
+
|
187
|
+
Typically, `handle` will implement a common pattern: checking the validity of the form submission and, if it's
|
188
|
+
not valid, re-render the previous page with errors, whereas if it is valid, execute some back-end logic.
|
189
|
+
|
190
|
+
If you'll remember, both email and password were set as required in the HTML. We'll talk about how to control
|
191
|
+
that behavior later, but it does mean that the browser would not submit form data without those values provided.
|
192
|
+
That said, JavaScript could be circumvented, so our handler could be called without either of those fields.
|
193
|
+
|
194
|
+
Because `LoginForm` describes the inputs *and* we used an instance of it to generate HTML, that instance can re-evaulate the client-side constraints at any time. The handler does this by calling `#constraint_violations?`.
|
195
|
+
|
196
|
+
```ruby {7}
|
197
|
+
# app/src/front_end/handlers/login_handler.rb
|
198
|
+
class LoginHandler < AppHandler
|
199
|
+
def initialize(form:)
|
200
|
+
@form = form
|
201
|
+
end
|
202
|
+
def handle
|
203
|
+
if @form.constraint_violations?
|
204
|
+
# ...
|
205
|
+
else
|
206
|
+
# ...
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
```
|
211
|
+
|
212
|
+
Of course, some constraints can't be validated
|
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.
|
216
|
+
|
217
|
+
If that returns `nil`, we want to re-render the `LoginPage`, exposing some sort of constraint violation message
|
218
|
+
so it can be rendered. We also want the form fields to be pre-filled with the values the visitor provided.
|
219
|
+
|
220
|
+
`for_form_input` can handle this, so we need to pass our form object into `LoginPage` instead of allowing `LoginPage` to create an empty one. We can do that by adding a `form:` keyword argument that defaults to `nil`:
|
221
|
+
|
222
|
+
```ruby {3,4}
|
223
|
+
# app/src/front_end/pages/login_page.rb
|
224
|
+
class LoginPage < AppPage
|
225
|
+
def initialize(form: nil)
|
226
|
+
@form = form || LoginForm.new
|
227
|
+
end
|
228
|
+
|
229
|
+
def page_template
|
230
|
+
form_tag(method: :post,
|
231
|
+
action: LoginHandler.routing) do
|
232
|
+
Inputs::TextField.for_form_input(form: @form, input_name: :email)
|
233
|
+
Inputs::TextField.for_form_input(form: @form, input_name: :password)
|
234
|
+
button { "Login" }
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
```
|
239
|
+
|
240
|
+
To trigger this behavior, the handler will:
|
241
|
+
|
242
|
+
* Call `server_side_constraint_violation` on the form instance.
|
243
|
+
* Pass it to `LoginPage.new`, which it will return, thus re-rendering the page (when a handler's `handle!` method returns an instance of a page, that page's HTML is generated as the response).
|
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
|
271
|
+
`for_form_input` contains constraint violations.
|
272
|
+
|
273
|
+
#### Showing Constraint Violations in HTML
|
274
|
+
|
275
|
+
When `Inputs::TextField.for_form_input` is called with an existing form that has constraint violations, different HTML is generated. This is what would be produced by our existing `LoginPage` (again, formatted her for clarity):
|
276
|
+
|
277
|
+
```html {3}
|
278
|
+
<form method="post" action="/login">
|
279
|
+
<input type="email" name="email" required
|
280
|
+
data-invalid data-login_not_found>
|
281
|
+
<input type="password" name="password" required>
|
282
|
+
<button>Login</button>
|
283
|
+
</form>
|
284
|
+
```
|
285
|
+
|
286
|
+
These `data-` attributes allow you to target these fields with CSS.
|
287
|
+
|
288
|
+
Actual error messages aren't shown since we didn't put in any HTML that might hold them. The form object
|
289
|
+
is capable of exposing the constraint violations as keys, intended to be used by the [I18n system](/i18n).
|
290
|
+
|
291
|
+
In general, you don't want to do this directy, but the API looks like so:
|
292
|
+
|
293
|
+
```ruby
|
294
|
+
form.input(:email).validity_state.each do |constraint|
|
295
|
+
# use constraint.key to construct a message
|
296
|
+
end
|
297
|
+
```
|
298
|
+
|
299
|
+
The reason to avoid this is that a) Brut provides a built-in component to generate HTML and b) if you use
|
300
|
+
Brut's component, you can achieve parity between client-side constraint violations detected by the browser
|
301
|
+
and server-side violations identified by your app.
|
302
|
+
|
303
|
+
### Forms and Constraint Violations
|
304
|
+
|
305
|
+
There are two common issues around constraint violations in HTML forms:
|
306
|
+
|
307
|
+
* Handling the case where JavaScript is circumvented and invalid data is submitted to the server.
|
308
|
+
* Unifying how client- and server-side constraint violations are shown the user.
|
309
|
+
|
310
|
+
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.
|
311
|
+
|
312
|
+
Above, we mentioned that each constrait violation is represented by a key to be used with the [I18n
|
313
|
+
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.
|
314
|
+
|
315
|
+
Brut provides default translations for these in `app/config/i18n/en/1_defaults.rb` under the prefix
|
316
|
+
`cv.fe` (`cv` being short of "constraint violation" and `fe` being short for "front end"). Note that these
|
317
|
+
keys match `ValidityState` so are in camel-case, not Ruby's idiomatic snake-case.
|
318
|
+
|
319
|
+
Back-end constraint violations are expected to have keys under `cv.be` (`be` for "back-end"), and these
|
320
|
+
keys *should* conform to Ruby's idioms.
|
321
|
+
|
322
|
+
Let's look at showing server-side constraints first, since those are more like what you may be familiar
|
323
|
+
with coming from Rails.
|
324
|
+
|
325
|
+
#### Showing Server-Side Violations
|
326
|
+
|
327
|
+
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.
|
328
|
+
|
329
|
+
Because we've included `Brut::FrontEnd::Components`, the Phlex kit allows
|
330
|
+
`ConstraintViolations` to be called directly, like so:
|
331
|
+
|
332
|
+
```ruby {7,10}
|
333
|
+
# Inside app/src/front_end/pages/login_page.rb
|
334
|
+
def page_template
|
335
|
+
form_tag(method: :post,
|
336
|
+
action: LoginHandler.routing) do
|
337
|
+
|
338
|
+
Inputs::TextField.for_form_input(form: @form, input_name: :email)
|
339
|
+
ConstraintViolations(form: @form, input_name: :email)
|
340
|
+
|
341
|
+
Inputs::TextField.for_form_input(form: @form, input_name: :password)
|
342
|
+
ConstraintViolations(form: @form, input_name: :password)
|
343
|
+
|
344
|
+
button { "Login" }
|
345
|
+
end
|
346
|
+
end
|
347
|
+
```
|
348
|
+
|
349
|
+
In the case where we've set the server-side constraint violation for the email field, and assuming that the i18n
|
350
|
+
key "cv.be.login\_not\_found" maps to the string "No login with that email/password", here is the HTML that
|
351
|
+
will be rendered:
|
352
|
+
|
353
|
+
```html {4-8,11-12}
|
354
|
+
<form method="post" action="/login">
|
355
|
+
<input type="email" name="email" required
|
356
|
+
data-invalid data-login_not_found>
|
357
|
+
<brut-cv-messages input-name="email">
|
358
|
+
<brut-cv server-side>
|
359
|
+
No login with that email/password.
|
360
|
+
</brut-cv>
|
361
|
+
</brut-cv-messages>
|
362
|
+
|
363
|
+
<input type="password" name="password" required>
|
364
|
+
<brut-cv-messages input-name="password">
|
365
|
+
</brut-cv-messages>
|
366
|
+
|
367
|
+
<button>Login</button>
|
368
|
+
</form>
|
369
|
+
```
|
370
|
+
|
371
|
+
`brut-cv-messages` and `brut-cv` are [autonomous custom
|
372
|
+
elements](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements). If you aren't
|
373
|
+
familiar with this part of the web platform, there are two things to know:
|
374
|
+
|
375
|
+
* 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).
|
376
|
+
* It's possible to attach behavior to these with JavaScript to add progressively-enhanced behavior.
|
377
|
+
|
378
|
+
Without any JavaScript, you now have the basis for styling your error messages *and* the server-side messages are
|
379
|
+
now rendered using internationalization. As a very basic demonstration, you could place this in
|
380
|
+
`app/src/front_end/css/index.css`:
|
381
|
+
|
382
|
+
```css
|
383
|
+
input[data-invalid] {
|
384
|
+
color: red;
|
385
|
+
background-color: mistyrose; /* yes, that's a CSS color :) */
|
386
|
+
}
|
387
|
+
brut-cv-messages {
|
388
|
+
color: red;
|
389
|
+
display: block;
|
390
|
+
|
391
|
+
brut-cv {
|
392
|
+
display: block;
|
393
|
+
}
|
394
|
+
}
|
395
|
+
```
|
396
|
+
|
397
|
+
#### Dynamically Showing Client-Side Violations
|
398
|
+
|
399
|
+
It would be nice if, when the browser detects client-side violations before the user submits the form, the
|
400
|
+
same UI could be used to show *those* error messages. Brut achieves this via the aforementioned
|
401
|
+
autonomous custom elements.
|
402
|
+
|
403
|
+
You'll note that even though the password field had no constraint violations, `<brut-cv-messages input-name="password">` was still generated for it. This element, working in conjuction with a few other elements, will provide localized messaging for client-side constraint violations using the same markup and CSS as your server-side constraint violations.
|
404
|
+
|
405
|
+
* `<brut-form>` will manage the `<form>` it contains to listen for any violations
|
406
|
+
* `<brut-cv-messages>` identifies where error messages should go, per form element.
|
407
|
+
* `<brut-cv>` contains a specific message or key.
|
408
|
+
* `<brut-i18n-translation>` maps keys from `<brut-cv>` elements to actual translated strings.
|
409
|
+
|
410
|
+
Together, these elements will show the visitor localized error messages exactly the same way as
|
411
|
+
server-side error messages are shown.
|
412
|
+
|
413
|
+
First, we need to wrap our form with `brut-form`:
|
414
|
+
|
415
|
+
```ruby {3,15}
|
416
|
+
# Inside app/src/front_end/pages/login_page.rb
|
417
|
+
def page_template
|
418
|
+
brut_form do
|
419
|
+
form_tag(method: :post,
|
420
|
+
action: LoginHandler.routing) do
|
421
|
+
|
422
|
+
Inputs::TextField.for_form_input(form: @form, input_name: :email)
|
423
|
+
ConstraintViolations(form: @form, input_name: :email)
|
424
|
+
|
425
|
+
Inputs::TextField.for_form_input(form: @form, input_name: :password)
|
426
|
+
ConstraintViolations(form: @form, input_name: :password)
|
427
|
+
|
428
|
+
button { "Login" }
|
429
|
+
end
|
430
|
+
end
|
431
|
+
end
|
432
|
+
```
|
433
|
+
|
434
|
+
Second, we need `<brut-i18-translation>` elements on the page somewhere. These *should* be in your
|
435
|
+
default layout and look like so:
|
436
|
+
|
437
|
+
```ruby {7,8}
|
438
|
+
# app/src/front_end/layouts/default_layout.rb
|
439
|
+
def view_template
|
440
|
+
doctype
|
441
|
+
html(lang: "en") do
|
442
|
+
head do
|
443
|
+
# ...
|
444
|
+
I18nTranslations("cv.fe")
|
445
|
+
I18nTranslations("cv.this_field")
|
446
|
+
# ...
|
447
|
+
end
|
448
|
+
body do
|
449
|
+
yield
|
450
|
+
end
|
451
|
+
end
|
452
|
+
end
|
453
|
+
```
|
454
|
+
|
455
|
+
`I18nTranslations` is a shortcut to `Brut::FrontEnd::Components::I18nTranslations`, which is a component to
|
456
|
+
render one `<brut-i18n-translation>` element per transalation found under the given prefix. Thus, it
|
457
|
+
would generate HTML like so:
|
458
|
+
|
459
|
+
```html
|
460
|
+
<brut-i18n-translation
|
461
|
+
key="cv.fe.badInput"
|
462
|
+
value="%{field} is the wrong type of data">
|
463
|
+
</brut-i18n-translation>
|
464
|
+
<brut-i18n-translation
|
465
|
+
key="cv.fe.patternMismatch"
|
466
|
+
value="%{field} isn't in the right format">
|
467
|
+
</brut-i18n-translation>
|
468
|
+
<!-- etc. -->
|
469
|
+
```
|
470
|
+
|
471
|
+
With this in place, here is how this works:
|
472
|
+
|
473
|
+
1. The `<brut-form>` listens for constraint violations on the `<form>` elements.
|
474
|
+
2. When one is detected, it then locates the `<brut-cv-messages>` element for that element's name (based on the `input-name` attribute).
|
475
|
+
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>`.
|
476
|
+
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.
|
477
|
+
|
478
|
+
This may seem convoluted, however it separates concerns reasonably well and allows localization of the messaging.
|
479
|
+
|
480
|
+
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.
|
481
|
+
|
482
|
+
You can now style these client-side messages with a slight change to your CSS:
|
483
|
+
|
484
|
+
```css {2}
|
485
|
+
input[data-invalid],
|
486
|
+
input:invalid {
|
487
|
+
color: red;
|
488
|
+
background-color: mistyrose;
|
489
|
+
}
|
490
|
+
brut-cv-messages {
|
491
|
+
color: red;
|
492
|
+
display: block;
|
493
|
+
|
494
|
+
brut-cv {
|
495
|
+
display: block;
|
496
|
+
}
|
497
|
+
}
|
498
|
+
```
|
499
|
+
|
500
|
+
Note that a) this didn't require a lot of code on your part, b) the server is still re-evaluating the
|
501
|
+
client-side constraints, so the visitor will see them, even if JavaScript is off or fails, and c) it
|
502
|
+
sticks as closely to the web platform as possible.
|
503
|
+
|
504
|
+
That all said, this implementation falls vicitim to an annoyance of client-side constraint violations, which is prematurely showing error messages.
|
505
|
+
|
506
|
+
#### Managing Errors Shown Before Submission
|
507
|
+
|
508
|
+
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.
|
509
|
+
|
510
|
+
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
|
511
|
+
[Baseline](https://developer.mozilla.org/en-US/docs/Glossary/Baseline/Compatibility).
|
512
|
+
|
513
|
+
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:
|
514
|
+
|
515
|
+
```css
|
516
|
+
/* First, hide client-side messaging by default.
|
517
|
+
Server-side messages will always appear */
|
518
|
+
form {
|
519
|
+
brut-cv {
|
520
|
+
display:none;
|
521
|
+
}
|
522
|
+
brut-cv[server-side] {
|
523
|
+
display:block;
|
524
|
+
}
|
525
|
+
}
|
526
|
+
|
527
|
+
/* Now, show constraint violations only if
|
528
|
+
submitted-invalid was set */
|
529
|
+
brut-form[submitted-invalid] {
|
530
|
+
brut-cv {
|
531
|
+
display:block;
|
532
|
+
}
|
533
|
+
}
|
534
|
+
|
535
|
+
/* Always show elements with data-invalid since that
|
536
|
+
is server-generated, but only style the elements
|
537
|
+
as invalid if the form has submitted-invalid on it */
|
538
|
+
input[data-invalid],
|
539
|
+
brut-form[submitted-invalid] input:invalid {
|
540
|
+
color: red;
|
541
|
+
background-color: mistyrose;
|
542
|
+
}
|
543
|
+
|
544
|
+
brut-cv-messages {
|
545
|
+
color: red;
|
546
|
+
display: block;
|
547
|
+
}
|
548
|
+
```
|
549
|
+
|
550
|
+
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.
|
551
|
+
|
552
|
+
### Checkboxes
|
553
|
+
|
554
|
+
Checkboxes are implemented in HTML by `<input type="checkbox">`, so in your form, you would use `type:
|
555
|
+
:checkbox`:
|
556
|
+
|
557
|
+
```ruby {5,6}
|
558
|
+
# app/src/front_end/forms/login_form.rb
|
559
|
+
class LoginForm < AppForm
|
560
|
+
input :email, type: :email
|
561
|
+
input :password, type: :password
|
562
|
+
input :remember, type: :checkbox
|
563
|
+
input :not_robot, type: :checkbox
|
564
|
+
end
|
565
|
+
```
|
566
|
+
|
567
|
+
Checkboxes can be rendered by `Inputs::TextField.for_form_input`, 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:
|
568
|
+
|
569
|
+
```html
|
570
|
+
<!-- Form.new(params: { remember: "true" }) -->
|
571
|
+
<input type="checkbox" name="remember" value="true" checked>
|
572
|
+
<input type="checkbox" name="not_robot" value="true">
|
573
|
+
```
|
574
|
+
|
575
|
+
### Radio Buttons
|
576
|
+
|
577
|
+
Radio buttons are implemented in HTML by `<input type="radio">`, with an expectation of more than one such input
|
578
|
+
having the same value for the `name` attribute, but different values for the `value` attributes, one of which may
|
579
|
+
be `checked`.
|
580
|
+
|
581
|
+
Brut implements this via `Brut::FrontEnd::Components::Inputs::RadioButton`, which has the class method
|
582
|
+
`for_form_input`. To create radio buttons in a form, use `radio_button_group`:
|
583
|
+
|
584
|
+
```ruby {5}
|
585
|
+
# app/src/front_end/forms/login_form.rb
|
586
|
+
class LoginForm < AppForm
|
587
|
+
input :email, type: :email
|
588
|
+
input :password, type: :password
|
589
|
+
radio_button_group :remember
|
590
|
+
end
|
591
|
+
```
|
592
|
+
|
593
|
+
The form would not need to be configured with the possible values - that will happen when you generate HTML:
|
594
|
+
|
595
|
+
```ruby
|
596
|
+
def view_template
|
597
|
+
form do
|
598
|
+
[ :never, :one_week, :one_month ].each do |remember|
|
599
|
+
label do
|
600
|
+
render(
|
601
|
+
Inputs::RadioButton.for_form_input(
|
602
|
+
form:,
|
603
|
+
input_name: :remember,
|
604
|
+
value: remember
|
605
|
+
)
|
606
|
+
)
|
607
|
+
plain { remember.to_s }
|
608
|
+
end
|
609
|
+
end
|
610
|
+
end
|
611
|
+
end
|
612
|
+
```
|
613
|
+
|
614
|
+
When generating HTML, Brut will examine the value of `form.remember` to know which radio button to check. To set
|
615
|
+
a default, set that value when creating the form:
|
616
|
+
|
617
|
+
```ruby {4-6}
|
618
|
+
# app/src/front_end/pages/login_page.rb
|
619
|
+
class LoginPage < AppPage
|
620
|
+
def initialize(form: nil)
|
621
|
+
@form = form || LoginForm.new(params: {
|
622
|
+
remember: "never"
|
623
|
+
})
|
624
|
+
end
|
625
|
+
```
|
626
|
+
|
627
|
+
### Selects
|
628
|
+
|
629
|
+
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.
|
630
|
+
|
631
|
+
> [!WARNING]
|
632
|
+
> Brut does not support multi-selects, yet.
|
633
|
+
|
634
|
+
|
635
|
+
You can set up a select via `select`
|
636
|
+
|
637
|
+
```ruby {5}
|
638
|
+
# app/src/front_end/forms/login_form.rb
|
639
|
+
class LoginForm < AppForm
|
640
|
+
input :email, type: :email
|
641
|
+
input :password, type: :password
|
642
|
+
select :remember
|
643
|
+
end
|
644
|
+
```
|
645
|
+
|
646
|
+
Creating the HTML can be done with `Brut::FrontEnd::Components::Inputs::Select`. It's `for_form_input` is more complex, since it provides a way to show visitor-friendly values instead of the innate `value` for each option,
|
647
|
+
as well as to allow for a "blank" entry.
|
648
|
+
|
649
|
+
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.
|
650
|
+
|
651
|
+
```ruby
|
652
|
+
class LoginRememberOption
|
653
|
+
include Brut::I18n::ForBackend
|
654
|
+
def initialize(value)
|
655
|
+
@value = value
|
656
|
+
end
|
657
|
+
|
658
|
+
def to_s = @value
|
659
|
+
|
660
|
+
def name
|
661
|
+
t("login.remember_options.#{@value}")
|
662
|
+
end
|
663
|
+
|
664
|
+
def self.all
|
665
|
+
[
|
666
|
+
LoginRememberOption.new("never"),
|
667
|
+
LoginRememberOption.new("one_week"),
|
668
|
+
LoginRememberOption.new("one_month"),
|
669
|
+
]
|
670
|
+
end
|
671
|
+
end
|
672
|
+
```
|
673
|
+
|
674
|
+
To show these options in a `<select>`, we might do this:
|
675
|
+
|
676
|
+
```ruby
|
677
|
+
def view_template
|
678
|
+
form do
|
679
|
+
render(
|
680
|
+
Inputs::Select.for_form_input(
|
681
|
+
form:,
|
682
|
+
input_name: :remember,
|
683
|
+
options: LoginRememberOption.all,
|
684
|
+
value_attribute: :to_s,
|
685
|
+
option_text_attribute: :name,
|
686
|
+
include_blank: {
|
687
|
+
value: :blank,
|
688
|
+
text_content: "-- Choose --",
|
689
|
+
}
|
690
|
+
)
|
691
|
+
)
|
692
|
+
end
|
693
|
+
end
|
694
|
+
```
|
695
|
+
|
696
|
+
This will create this HTML (making some assumptions about the translations):
|
697
|
+
|
698
|
+
```html
|
699
|
+
<select name="remember">
|
700
|
+
<option value="blank">-- Choose --</option>
|
701
|
+
<option value="never">Never</option>
|
702
|
+
<option value="one_week">One Week</option>
|
703
|
+
<option value="one_month">One Month</option>
|
704
|
+
</select>
|
705
|
+
```
|
706
|
+
|
707
|
+
### Arrays of Values
|
708
|
+
|
709
|
+
Some complex forms involve a potentially arbitrary number of inputs for a given field. For example, you might allow the visitor to edit widgets in bulk, 10 at a time.
|
710
|
+
|
711
|
+
Brut can handle this, with help from Rack. First, you'll use `array: true` when declaring an input:
|
712
|
+
|
713
|
+
```ruby
|
714
|
+
class BulkWidgetForm < AppForm
|
715
|
+
input :name, array: true, required: false
|
716
|
+
end
|
717
|
+
```
|
718
|
+
|
719
|
+
In this case, we need `required: false` or every single field we generate will be required.
|
720
|
+
|
721
|
+
To generate the HTML, use the optional `index:` parameter to `for_form_input` as well as for
|
722
|
+
`ConstraintViolations`:
|
723
|
+
|
724
|
+
```ruby {11,16}
|
725
|
+
# Inside e.g. app/src/front_end/pages/create_bulk_widget_page.rb
|
726
|
+
def page_template
|
727
|
+
brut_form do
|
728
|
+
form_tag(method: :post,
|
729
|
+
action: BulkWidgetForm.routing) do
|
730
|
+
|
731
|
+
10.times do |i|
|
732
|
+
Inputs::TextField.for_form_input(
|
733
|
+
form: @form,
|
734
|
+
input_name: :name,
|
735
|
+
index: i
|
736
|
+
)
|
737
|
+
ConstraintViolations(
|
738
|
+
form: @form,
|
739
|
+
input_name: :email,
|
740
|
+
index: i
|
741
|
+
)
|
742
|
+
end
|
743
|
+
|
744
|
+
button { "Save" }
|
745
|
+
end
|
746
|
+
end
|
747
|
+
end
|
748
|
+
```
|
749
|
+
|
750
|
+
This will generate HTML like so:
|
751
|
+
|
752
|
+
```html
|
753
|
+
<!-- ... -->
|
754
|
+
<input name="name[]">
|
755
|
+
<!-- ... -->
|
756
|
+
<input name="name[]">
|
757
|
+
<!-- ... -->
|
758
|
+
```
|
759
|
+
|
760
|
+
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.
|
761
|
+
|
762
|
+
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.
|
763
|
+
|
764
|
+
In the handler, values can be accessed by index:
|
765
|
+
|
766
|
+
```ruby {4}
|
767
|
+
# app/src/front_end/handlers/bulk_widget_handler.rb
|
768
|
+
class BulkWidgetHandler < AppHandler
|
769
|
+
def handle
|
770
|
+
@form.name(2) # name with index 2 i.e. the 3rd value
|
771
|
+
end
|
772
|
+
end
|
773
|
+
```
|
774
|
+
|
775
|
+
To set the values, you must provide an array to `params:`:
|
776
|
+
|
777
|
+
```ruby {3}
|
778
|
+
widgets = DB::Widget.order(:created_at).limit(10)
|
779
|
+
BulkWidgetForm.new(params: {
|
780
|
+
name: widgets.to_a.map(&:name),
|
781
|
+
})
|
782
|
+
|
783
|
+
# OR
|
784
|
+
BulkWidgetForm.new(params: {
|
785
|
+
name: [
|
786
|
+
"",
|
787
|
+
"",
|
788
|
+
"Third Widget",
|
789
|
+
"",
|
790
|
+
""
|
791
|
+
],
|
792
|
+
})
|
793
|
+
```
|
794
|
+
|
795
|
+
To set server-side constraint violations, `index:` can be used:
|
796
|
+
|
797
|
+
```ruby {4}
|
798
|
+
form.server_side_constraint_violation(
|
799
|
+
input_name: :name,
|
800
|
+
key: :must_be_two_words,
|
801
|
+
index: 3
|
802
|
+
)
|
803
|
+
```
|
804
|
+
|
805
|
+
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.
|
806
|
+
|
807
|
+
## Testing
|
808
|
+
|
809
|
+
Form classes don't need any logic on them, but they can be given helper methods or other logic if it makes sense.
|
810
|
+
To test them, test them like any other class - instantiate an object and examine the behavior of its methods.
|
811
|
+
|
812
|
+
Note that Brut provides the constructor for all form classes, and it expects a single keyword parameter named `params:`
|
813
|
+
that is a hash mapping strings to strings representing the submitted form data. The keys can be symbols and Brut will
|
814
|
+
map them to strings.
|
815
|
+
|
816
|
+
Testing handlers is covered in [Handlers](/handlers)
|
817
|
+
|
818
|
+
When testing the UX around constraint violations, you should use an end-to-end test, as this will allow you to
|
819
|
+
assert behavior around client-side constraint violations. This is discussed in [End-to-end
|
820
|
+
Tests](/end-to-end-tests).
|
821
|
+
|
822
|
+
## Recommended Practices
|
823
|
+
|
824
|
+
### Make Use of Components
|
825
|
+
|
826
|
+
The example we saw above creates only minimal markup, yet required a fair bit of code. You are encouraged to
|
827
|
+
create your own components that generate the markup you need for your app's inputs. For example, you are likely
|
828
|
+
going to want `app/src/front_end/components/text_field_component.rb` to generate whatever markup is needed fo
|
829
|
+
your text fields to look how they are supposed to, with and without constraint violation messages.
|
830
|
+
|
831
|
+
### Functional or Utility CSS Is Difficult Here
|
832
|
+
|
833
|
+
The way constraint violations are implemented leverages the web platform, which naturally includes conventional use of CSS. This creates an "impedance mismatch" with functional or utility CSS like Tailwind.
|
834
|
+
|
835
|
+
While it may be possible to write a bunch of single-purpose classes to target the markup and attributes Brut generates, it may be easier to write conventional CSS for constraint violations.
|
836
|
+
|
837
|
+
To avoid duplication, you should leverage the custom properties of your CSS framework. For example:
|
838
|
+
|
839
|
+
```css
|
840
|
+
input[data-invalid] {
|
841
|
+
color: var(--color-red);
|
842
|
+
}
|
843
|
+
```
|
844
|
+
|
845
|
+
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.
|
846
|
+
|
847
|
+
## Technical Notes
|
848
|
+
|
849
|
+
> [!IMPORTANT]
|
850
|
+
> Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's
|
851
|
+
> internals, the source code is always more correct.
|
852
|
+
|
853
|
+
_Last Updated May 13, 2025_
|
854
|
+
|
855
|
+
Form internals try to coerce types to strings, since the web and HTTP is all strings all the time. Empty strings
|
856
|
+
are coerced to `nil`. If the form's `params:` value contains any type Brut cannot deal with, you'll get an
|
857
|
+
exception during tests and a notice/event in production.
|
858
|
+
|
859
|
+
For HTML generation, there are few classes that work together:
|
860
|
+
|
861
|
+
* *input definitions* define an input and tend to provide an API similar to HTML's. See `Brut::FrontEnd::Forms::InputDefinition`.
|
862
|
+
* *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`.
|
863
|
+
* `Brut::FrontEnd::Forms::InputDeclarations` is a module that allows creating input definitions inside your form
|
864
|
+
class. It implements the class methods like `input`.
|
865
|
+
* `Brut::FrontEnd::Components::Inputs` contains components used to generate `<input>` fields. These classes will
|
866
|
+
coerce the value of the `input` they are given to generate the correct HTML.
|