brut 0.14.0 → 0.15.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/CHANGELOG.md +5 -0
- data/Gemfile.lock +1 -1
- data/brut-css/package-lock.json +2 -2
- data/brut-css/package.json +1 -1
- data/brut-js/package-lock.json +2 -2
- data/brut-js/package.json +1 -1
- data/brut-js/specs/Toast.spec.js +34 -0
- data/brut-js/src/I18nTranslation.js +3 -0
- data/brut-js/src/Message.js +9 -3
- data/brut-js/src/RichString.js +4 -1
- data/brut-js/src/Toast.js +102 -0
- data/brut-js/src/index.js +3 -0
- data/brutrb.com/brut-js.md +1 -0
- data/docs/404.html +3 -3
- data/docs/adrs.html +7 -7
- data/docs/ai.html +7 -7
- 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/DbModel.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/ButtonTag.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 +37 -18
- 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/CsrfProtector.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 +1 -1
- data/docs/api/Brut/FrontEnd/Forms/Button.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/ButtonInputDefinition.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/ConstraintViolation.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/Input/Color.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/Input/TimeOfDay.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/Input.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/InputDeclarations.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/InputDefinition.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInput.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/RadioButtonGroupInputDefinition.html +1 -1
- data/docs/api/Brut/FrontEnd/Forms/SelectInput.html +1 -1
- 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 +171 -3
- 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/Methods/ClassMethods.html +1 -1
- data/docs/api/Brut/Instrumentation/Methods.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/RubocopConfig.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/ModuleName.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 +1 -1
- data/docs/api/file.README.html +1 -1
- data/docs/api/index.html +1 -1
- data/docs/api/method_list.html +157 -141
- data/docs/api/top-level-namespace.html +1 -1
- data/docs/assets/adrs.md.YglbWtQe.js +1 -0
- data/docs/assets/adrs.md.YglbWtQe.lean.js +1 -0
- data/docs/assets/ai.md.ChLnvDAX.js +1 -0
- data/docs/assets/ai.md.ChLnvDAX.lean.js +1 -0
- data/docs/assets/{app.BDtsVxyd.js → app.B0X8upRm.js} +1 -1
- data/docs/assets/{assets.md.7C3HWkga.js → assets.md.BEF6Oz6K.js} +2 -2
- data/docs/assets/assets.md.BEF6Oz6K.lean.js +1 -0
- data/docs/assets/{brut-js.md.B4GYxQVw.js → brut-js.md.CbJAe2Ky.js} +2 -2
- data/docs/assets/brut-js.md.CbJAe2Ky.lean.js +1 -0
- data/docs/assets/business-logic.md.DbuaOYGU.js +1 -0
- data/docs/assets/business-logic.md.DbuaOYGU.lean.js +1 -0
- data/docs/assets/chunks/@localSearchIndexroot.C0s1k0UQ.js +1 -0
- data/docs/assets/chunks/VPLocalSearchBox.jLmhant1.js +8 -0
- data/docs/assets/chunks/framework.C4nOkCZI.js +18 -0
- data/docs/assets/chunks/{theme.DZKmijwi.js → theme.CtVUdCdt.js} +2 -2
- data/docs/assets/{cli.md.CjsktgFz.js → cli.md.DDMar_51.js} +2 -2
- data/docs/assets/cli.md.DDMar_51.lean.js +1 -0
- data/docs/assets/{components.md.rMhQ0WdZ.js → components.md.C6nWgDP0.js} +5 -5
- data/docs/assets/components.md.C6nWgDP0.lean.js +1 -0
- data/docs/assets/{configuration.md.BK42Yjp_.js → configuration.md.CpbYHWPb.js} +2 -2
- data/docs/assets/configuration.md.CpbYHWPb.lean.js +1 -0
- data/docs/assets/{css.md.CltvJqAa.js → css.md.K5rOCOQY.js} +2 -2
- data/docs/assets/css.md.K5rOCOQY.lean.js +1 -0
- data/docs/assets/{custom-element-tests.md.B_rbta32.js → custom-element-tests.md.DiLe-eFw.js} +2 -2
- data/docs/assets/custom-element-tests.md.DiLe-eFw.lean.js +1 -0
- data/docs/assets/{database-access.md.gnluu54N.js → database-access.md.Dc8l2Plf.js} +2 -2
- data/docs/assets/database-access.md.Dc8l2Plf.lean.js +1 -0
- data/docs/assets/{database-schema.md.LpmBPVEU.js → database-schema.md.BJ_JhXmO.js} +2 -2
- data/docs/assets/database-schema.md.BJ_JhXmO.lean.js +1 -0
- data/docs/assets/{deployment.md.BLseERGV.js → deployment.md.C1u5ep0g.js} +2 -2
- data/docs/assets/deployment.md.C1u5ep0g.lean.js +1 -0
- data/docs/assets/{dev-environment.md.DRH2D2-O.js → dev-environment.md.B1S9p5ZK.js} +2 -2
- data/docs/assets/{dev-environment.md.DRH2D2-O.lean.js → dev-environment.md.B1S9p5ZK.lean.js} +1 -1
- data/docs/assets/{dir-structure.md.CWir1pic.js → dir-structure.md.D1T2kGwj.js} +2 -2
- data/docs/assets/dir-structure.md.D1T2kGwj.lean.js +1 -0
- data/docs/assets/doc-conventions.md.CDnWaEFg.js +1 -0
- data/docs/assets/doc-conventions.md.CDnWaEFg.lean.js +1 -0
- data/docs/assets/{end-to-end-tests.md.DzqRpZ43.js → end-to-end-tests.md.BJJdNDYL.js} +2 -2
- data/docs/assets/end-to-end-tests.md.BJJdNDYL.lean.js +1 -0
- data/docs/assets/{features.md.DPFXsy0z.js → features.md.BDWxnyNO.js} +2 -2
- data/docs/assets/features.md.BDWxnyNO.lean.js +1 -0
- data/docs/assets/{flash-and-session.md.nPvUpnUx.js → flash-and-session.md.CUsMxoNl.js} +2 -2
- data/docs/assets/flash-and-session.md.CUsMxoNl.lean.js +1 -0
- data/docs/assets/{form-constraints.md.KTv5cdR4.js → form-constraints.md.KlfXSKm2.js} +2 -2
- data/docs/assets/form-constraints.md.KlfXSKm2.lean.js +1 -0
- data/docs/assets/{forms.md.v9qIbmUM.js → forms.md.Bii91k3E.js} +3 -3
- data/docs/assets/forms.md.Bii91k3E.lean.js +1 -0
- data/docs/assets/{getting-started.md.DTOl4c2g.js → getting-started.md.ChAvueK7.js} +4 -4
- data/docs/assets/getting-started.md.ChAvueK7.lean.js +1 -0
- data/docs/assets/{handlers.md.h84MMB1R.js → handlers.md.C5tUwmmo.js} +2 -2
- data/docs/assets/handlers.md.C5tUwmmo.lean.js +1 -0
- data/docs/assets/{hooks.md.Jmb5VOLA.js → hooks.md.CoiYCKRc.js} +2 -2
- data/docs/assets/hooks.md.CoiYCKRc.lean.js +1 -0
- data/docs/assets/{i18n.md.BAm9t9JJ.js → i18n.md.DxkCKhUw.js} +2 -2
- data/docs/assets/i18n.md.DxkCKhUw.lean.js +1 -0
- data/docs/assets/{index.md.Bn9e0sRJ.js → index.md.DnphWyQd.js} +1 -1
- data/docs/assets/{index.md.Bn9e0sRJ.lean.js → index.md.DnphWyQd.lean.js} +1 -1
- data/docs/assets/{instrumentation.md._lNSriEZ.js → instrumentation.md.BcxjC4jd.js} +2 -2
- data/docs/assets/instrumentation.md.BcxjC4jd.lean.js +1 -0
- data/docs/assets/{javascript.md.DzrMxUmI.js → javascript.md.D6fxhaQb.js} +2 -2
- data/docs/assets/javascript.md.D6fxhaQb.lean.js +1 -0
- data/docs/assets/jobs.md.Bc7Y1YpK.js +1 -0
- data/docs/assets/jobs.md.Bc7Y1YpK.lean.js +1 -0
- data/docs/assets/{keyword-injection.md.95Zgh2eN.js → keyword-injection.md.CqLnnzIz.js} +2 -2
- data/docs/assets/keyword-injection.md.CqLnnzIz.lean.js +1 -0
- data/docs/assets/layouts.md.HEbeK7Jr.js +68 -0
- data/docs/assets/layouts.md.HEbeK7Jr.lean.js +1 -0
- data/docs/assets/lsp.md.bE9dW8n9.js +1 -0
- data/docs/assets/lsp.md.bE9dW8n9.lean.js +1 -0
- data/docs/assets/{markdown-examples.md.CCFEQO44.js → markdown-examples.md.BPmtHlc-.js} +2 -2
- data/docs/assets/markdown-examples.md.BPmtHlc-.lean.js +1 -0
- data/docs/assets/{middleware.md.Czz_UlJN.js → middleware.md.BhOIsg59.js} +2 -2
- data/docs/assets/middleware.md.BhOIsg59.lean.js +1 -0
- data/docs/assets/overview.md.BpWAgPFH.js +1 -0
- data/docs/assets/overview.md.BpWAgPFH.lean.js +1 -0
- data/docs/assets/{pages.md.B7Hc-i6H.js → pages.md.B3sQXpEd.js} +2 -2
- data/docs/assets/pages.md.B3sQXpEd.lean.js +1 -0
- data/docs/assets/{recipes_alternate-layouts.md.BwEytl59.js → recipes_alternate-layouts.md.C1QzVkA7.js} +2 -2
- data/docs/assets/recipes_alternate-layouts.md.C1QzVkA7.lean.js +1 -0
- data/docs/assets/{recipes_authentication.md.nwO6F7Ou.js → recipes_authentication.md.CyvoIW82.js} +2 -2
- data/docs/assets/recipes_authentication.md.CyvoIW82.lean.js +1 -0
- data/docs/assets/{recipes_custom-flash.md.CrQbI5eH.js → recipes_custom-flash.md.6gFqf2uL.js} +2 -2
- data/docs/assets/recipes_custom-flash.md.6gFqf2uL.lean.js +1 -0
- data/docs/assets/{recipes_form-errors.md.Bv5RCKqH.js → recipes_form-errors.md.B5ptSzMO.js} +2 -2
- data/docs/assets/recipes_form-errors.md.B5ptSzMO.lean.js +1 -0
- data/docs/assets/{recipes_indexed-forms.md.CstYyOSo.js → recipes_indexed-forms.md.BYYQGW2C.js} +2 -2
- data/docs/assets/recipes_indexed-forms.md.BYYQGW2C.lean.js +1 -0
- data/docs/assets/{recipes_migrations.md.CTcnWDJF.js → recipes_migrations.md.Cid7-3cu.js} +2 -2
- data/docs/assets/recipes_migrations.md.Cid7-3cu.lean.js +1 -0
- data/docs/assets/{recipes_text-field-component.md.H4wLAK0Z.js → recipes_text-field-component.md.VhOsCtKI.js} +2 -2
- data/docs/assets/recipes_text-field-component.md.VhOsCtKI.lean.js +1 -0
- data/docs/assets/roadmap.md.CJsbUmK_.js +1 -0
- data/docs/assets/roadmap.md.CJsbUmK_.lean.js +1 -0
- data/docs/assets/{routes.md.BD6y2i-f.js → routes.md.C1dgIBtD.js} +2 -2
- data/docs/assets/routes.md.C1dgIBtD.lean.js +1 -0
- data/docs/assets/security.md.Jn4SY1uK.js +1 -0
- data/docs/assets/security.md.Jn4SY1uK.lean.js +1 -0
- data/docs/assets/{seed-data.md.BvFZlqIk.js → seed-data.md.UZW0WxYN.js} +2 -2
- data/docs/assets/seed-data.md.UZW0WxYN.lean.js +1 -0
- data/docs/assets/space-time-continuum.md.D9rYGDFH.js +1 -0
- data/docs/assets/space-time-continuum.md.D9rYGDFH.lean.js +1 -0
- data/docs/assets/{tutorial.md.BM40jnoq.js → tutorial.md.BX6f6l00.js} +2 -2
- data/docs/assets/tutorial.md.BX6f6l00.lean.js +1 -0
- data/docs/assets/{tutorials_01-intro.md.B4sUBY3X.js → tutorials_01-intro.md.CzZ3kpF_.js} +2 -2
- data/docs/assets/{tutorials_01-intro.md.B4sUBY3X.lean.js → tutorials_01-intro.md.CzZ3kpF_.lean.js} +1 -1
- data/docs/assets/{tutorials_02-dialog.md.CPNK1SC_.js → tutorials_02-dialog.md.De6iTsWX.js} +2 -2
- data/docs/assets/{tutorials_02-dialog.md.CPNK1SC_.lean.js → tutorials_02-dialog.md.De6iTsWX.lean.js} +1 -1
- data/docs/assets/{unit-tests.md.DUGrnLj5.js → unit-tests.md.vDsdBbO_.js} +2 -2
- data/docs/assets/unit-tests.md.vDsdBbO_.lean.js +1 -0
- data/docs/assets/why.md.4WpxdrQ2.js +1 -0
- data/docs/assets/why.md.4WpxdrQ2.lean.js +1 -0
- data/docs/assets.html +7 -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 +7 -7
- data/docs/business-logic.html +7 -7
- data/docs/cli.html +7 -7
- data/docs/components.html +10 -10
- data/docs/configuration.html +7 -7
- data/docs/css.html +7 -7
- data/docs/custom-element-tests.html +7 -7
- data/docs/database-access.html +7 -7
- data/docs/database-schema.html +7 -7
- data/docs/deployment.html +7 -7
- data/docs/dev-environment.html +7 -7
- data/docs/dir-structure.html +7 -7
- data/docs/doc-conventions.html +7 -7
- data/docs/end-to-end-tests.html +7 -7
- data/docs/features.html +7 -7
- data/docs/flash-and-session.html +7 -7
- data/docs/form-constraints.html +7 -7
- data/docs/forms.html +8 -8
- data/docs/getting-started.html +9 -9
- data/docs/handlers.html +7 -7
- data/docs/hashmap.json +1 -1
- data/docs/hooks.html +7 -7
- data/docs/i18n.html +7 -7
- data/docs/index.html +6 -6
- data/docs/instrumentation.html +7 -7
- data/docs/javascript.html +7 -7
- data/docs/jobs.html +7 -7
- data/docs/keyword-injection.html +7 -7
- data/docs/layouts.html +42 -12
- data/docs/lsp.html +7 -7
- data/docs/markdown-examples.html +7 -7
- data/docs/middleware.html +7 -7
- data/docs/overview.html +7 -7
- data/docs/pages.html +7 -7
- data/docs/recipes/alternate-layouts.html +8 -8
- data/docs/recipes/authentication.html +7 -7
- data/docs/recipes/custom-flash.html +8 -8
- data/docs/recipes/form-errors.html +7 -7
- data/docs/recipes/indexed-forms.html +7 -7
- data/docs/recipes/migrations.html +7 -7
- data/docs/recipes/text-field-component.html +7 -7
- data/docs/roadmap.html +7 -7
- data/docs/routes.html +7 -7
- data/docs/security.html +7 -7
- data/docs/seed-data.html +7 -7
- data/docs/space-time-continuum.html +7 -7
- data/docs/tutorial.html +7 -7
- data/docs/tutorials/01-intro.html +7 -7
- data/docs/tutorials/02-dialog.html +7 -7
- data/docs/unit-tests.html +7 -7
- data/docs/why.html +7 -7
- data/lib/brut/version.rb +1 -1
- data/mkbrut/Gemfile.lock +1 -1
- data/mkbrut/lib/mkbrut/version.rb +1 -1
- metadata +114 -115
- data/docs/assets/adrs.md.BxjHi9-8.js +0 -1
- data/docs/assets/adrs.md.BxjHi9-8.lean.js +0 -1
- data/docs/assets/ai.md.Cy9GWnER.js +0 -1
- data/docs/assets/ai.md.Cy9GWnER.lean.js +0 -1
- data/docs/assets/assets.md.7C3HWkga.lean.js +0 -1
- data/docs/assets/brut-js.md.B4GYxQVw.lean.js +0 -1
- data/docs/assets/business-logic.md.BY4hGy0m.js +0 -1
- data/docs/assets/business-logic.md.BY4hGy0m.lean.js +0 -1
- data/docs/assets/chunks/@localSearchIndexroot.BWVzhs5N.js +0 -1
- data/docs/assets/chunks/VPLocalSearchBox.DCJk5nAW.js +0 -8
- data/docs/assets/chunks/framework.1L-BeKqY.js +0 -18
- data/docs/assets/cli.md.CjsktgFz.lean.js +0 -1
- data/docs/assets/components.md.rMhQ0WdZ.lean.js +0 -1
- data/docs/assets/configuration.md.BK42Yjp_.lean.js +0 -1
- data/docs/assets/css.md.CltvJqAa.lean.js +0 -1
- data/docs/assets/custom-element-tests.md.B_rbta32.lean.js +0 -1
- data/docs/assets/database-access.md.gnluu54N.lean.js +0 -1
- data/docs/assets/database-schema.md.LpmBPVEU.lean.js +0 -1
- data/docs/assets/deployment.md.BLseERGV.lean.js +0 -1
- data/docs/assets/dir-structure.md.CWir1pic.lean.js +0 -1
- data/docs/assets/doc-conventions.md.DOkAuXlt.js +0 -1
- data/docs/assets/doc-conventions.md.DOkAuXlt.lean.js +0 -1
- data/docs/assets/end-to-end-tests.md.DzqRpZ43.lean.js +0 -1
- data/docs/assets/features.md.DPFXsy0z.lean.js +0 -1
- data/docs/assets/flash-and-session.md.nPvUpnUx.lean.js +0 -1
- data/docs/assets/form-constraints.md.KTv5cdR4.lean.js +0 -1
- data/docs/assets/forms.md.v9qIbmUM.lean.js +0 -1
- data/docs/assets/getting-started.md.DTOl4c2g.lean.js +0 -1
- data/docs/assets/handlers.md.h84MMB1R.lean.js +0 -1
- data/docs/assets/hooks.md.Jmb5VOLA.lean.js +0 -1
- data/docs/assets/i18n.md.BAm9t9JJ.lean.js +0 -1
- data/docs/assets/instrumentation.md._lNSriEZ.lean.js +0 -1
- data/docs/assets/javascript.md.DzrMxUmI.lean.js +0 -1
- data/docs/assets/jobs.md.S-2amAYp.js +0 -1
- data/docs/assets/jobs.md.S-2amAYp.lean.js +0 -1
- data/docs/assets/keyword-injection.md.95Zgh2eN.lean.js +0 -1
- data/docs/assets/layouts.md.CVGl9xIO.js +0 -38
- data/docs/assets/layouts.md.CVGl9xIO.lean.js +0 -1
- data/docs/assets/lsp.md.Dn1rIiW0.js +0 -1
- data/docs/assets/lsp.md.Dn1rIiW0.lean.js +0 -1
- data/docs/assets/markdown-examples.md.CCFEQO44.lean.js +0 -1
- data/docs/assets/middleware.md.Czz_UlJN.lean.js +0 -1
- data/docs/assets/overview.md.DlKiRRG_.js +0 -1
- data/docs/assets/overview.md.DlKiRRG_.lean.js +0 -1
- data/docs/assets/pages.md.B7Hc-i6H.lean.js +0 -1
- data/docs/assets/recipes_alternate-layouts.md.BwEytl59.lean.js +0 -1
- data/docs/assets/recipes_authentication.md.nwO6F7Ou.lean.js +0 -1
- data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.js +0 -15
- data/docs/assets/recipes_blank-layouts.md.fyAUJyJR.lean.js +0 -1
- data/docs/assets/recipes_custom-flash.md.CrQbI5eH.lean.js +0 -1
- data/docs/assets/recipes_form-errors.md.Bv5RCKqH.lean.js +0 -1
- data/docs/assets/recipes_indexed-forms.md.CstYyOSo.lean.js +0 -1
- data/docs/assets/recipes_migrations.md.CTcnWDJF.lean.js +0 -1
- data/docs/assets/recipes_text-field-component.md.H4wLAK0Z.lean.js +0 -1
- data/docs/assets/roadmap.md.C6PRi0DX.js +0 -1
- data/docs/assets/roadmap.md.C6PRi0DX.lean.js +0 -1
- data/docs/assets/routes.md.BD6y2i-f.lean.js +0 -1
- data/docs/assets/security.md.C0G_AZR-.js +0 -1
- data/docs/assets/security.md.C0G_AZR-.lean.js +0 -1
- data/docs/assets/seed-data.md.BvFZlqIk.lean.js +0 -1
- data/docs/assets/space-time-continuum.md.xl44xDos.js +0 -1
- data/docs/assets/space-time-continuum.md.xl44xDos.lean.js +0 -1
- data/docs/assets/tutorial.md.BM40jnoq.lean.js +0 -1
- data/docs/assets/unit-tests.md.DUGrnLj5.lean.js +0 -1
- data/docs/assets/why.md.C-hk5xgJ.js +0 -1
- data/docs/assets/why.md.C-hk5xgJ.lean.js +0 -1
- data/docs/recipes/blank-layouts.html +0 -43
@@ -1,4 +1,4 @@
|
|
1
|
-
import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.
|
1
|
+
import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.C4nOkCZI.js";const E=JSON.parse('{"title":"Styling Form Errors","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/form-errors.md","filePath":"recipes/form-errors.md"}'),t={name:"recipes/form-errors.md"};function h(l,s,p,k,r,d){return n(),a("div",null,[...s[0]||(s[0]=[e(`<h1 id="styling-form-errors" tabindex="-1">Styling Form Errors <a class="header-anchor" href="#styling-form-errors" aria-label="Permalink to "Styling Form Errors""></a></h1><p>Brut makes it as easy as possible to unify client-side and server-side constraint violation handling, including how you style those messages.</p><h2 id="requirements" tabindex="-1">Requirements <a class="header-anchor" href="#requirements" aria-label="Permalink to "Requirements""></a></h2><p>What you want:</p><ul><li>When a form is rendered for the first time, there should be errors shown.</li><li>When a visitor interacts with a form before submissions, no errors are shown.</li><li>When the form is submitted, client-side constraint violations should be shown.</li><li>When JavaScript is circumvented and the form is submitted with client-side constraint violations, the form should be re-generated, showing those violations the same is if JavaScripts was <em>not</em> circumvented</li><li>When there are no client-side constraint violations, but there <em>are</em> server-side violations, the form should be re-generated, showing those violations the same is if JavaScripts was <em>not</em> circumvented</li></ul><p>This can be achieved through CSS.</p><h2 id="recipe" tabindex="-1">Recipe <a class="header-anchor" href="#recipe" aria-label="Permalink to "Recipe""></a></h2><h3 id="create-pages-and-html" tabindex="-1">Create Pages and HTML <a class="header-anchor" href="#create-pages-and-html" aria-label="Permalink to "Create Pages and HTML""></a></h3><p>First, create a form and handler:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>bin/scaffold form /new_widget</span></span></code></pre></div><p>Edit <code>app/src/front_end/forms/new_widget_form.rb</code></p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetForm</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> <</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppForm</span></span>
|
2
2
|
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">minlength:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 3</span></span>
|
3
3
|
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:description</span></span>
|
4
4
|
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>Now, implement the handler in <code>app/src/front_end/handlers/new_widget_handler.rb</code> to check for client-side violations <em>and</em> require that the description have at least 5 words in it.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> NewWidgetHandler</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> <</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppHandler</span></span>
|
@@ -63,4 +63,4 @@ import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.1L-BeKqY.js";const E
|
|
63
63
|
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> background-color</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">pink</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
|
64
64
|
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> border</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">solid</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> thin</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> red</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
|
65
65
|
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> border-radius</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">: </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">1</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">rem</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">;</span></span>
|
66
|
-
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">}</span></span></code></pre></div>`,29)]))}const g=i(t,[["render",h]]);export{E as __pageData,g as default};
|
66
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">}</span></span></code></pre></div>`,29)])])}const g=i(t,[["render",h]]);export{E as __pageData,g as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.C4nOkCZI.js";const E=JSON.parse('{"title":"Styling Form Errors","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/form-errors.md","filePath":"recipes/form-errors.md"}'),t={name:"recipes/form-errors.md"};function h(l,s,p,k,r,d){return n(),a("div",null,[...s[0]||(s[0]=[e("",29)])])}const g=i(t,[["render",h]]);export{E as __pageData,g as default};
|
data/docs/assets/{recipes_indexed-forms.md.CstYyOSo.js → recipes_indexed-forms.md.BYYQGW2C.js}
RENAMED
@@ -1,4 +1,4 @@
|
|
1
|
-
import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.
|
1
|
+
import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.C4nOkCZI.js";const g=JSON.parse('{"title":"Indexed Forms","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/indexed-forms.md","filePath":"recipes/indexed-forms.md"}'),h={name:"recipes/indexed-forms.md"};function t(l,s,p,k,r,d){return n(),a("div",null,[...s[0]||(s[0]=[e(`<h1 id="indexed-forms" tabindex="-1">Indexed Forms <a class="header-anchor" href="#indexed-forms" aria-label="Permalink to "Indexed Forms""></a></h1><p>HTTP allows a form to have any number of elements with the same name. HTTP will make all values available to you. Rack supports this, too, but not in a standard way.</p><p>This recipe will show a form that has more than one set of text fields for the same conceptual field.</p><h2 id="feature" tabindex="-1">Feature <a class="header-anchor" href="#feature" aria-label="Permalink to "Feature""></a></h2><p>Allow editing a single form that has 10 sets of name/quantity fields, in order to bulk create up to 10 widgets at a time.</p><h2 id="recipe" tabindex="-1">Recipe <a class="header-anchor" href="#recipe" aria-label="Permalink to "Recipe""></a></h2><p>We'll create a form, a handler, and a page to do this.</p><h3 id="creating-a-form-with-indexes" tabindex="-1">Creating a Form with Indexes <a class="header-anchor" href="#creating-a-form-with-indexes" aria-label="Permalink to "Creating a Form with Indexes""></a></h3><p>First, we'll scaffold the form and handler:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>bin/scaffold form /bulk_create_widgets</span></span></code></pre></div><p>Next, we'll create the form in <code>app/src/front_end/forms/bulk_create_widgets_form.rb</code> This will look like a normal form except each field will have <code>array: true</code>, to indicate there will be an arbitrary number of these fields. Since they are not all going to be required, we'll set <code>required: false</code>.</p><p>When you specify <code>array:true</code>, the method created by <code>input</code> accepts an index as an argument. For example, <code>form.name(3)</code> would retrieve the fourth name submitted.</p><p>We'll also implement the method <code>each_widget</code> that will yield each name/quantity pair. The reason Brut doesn't provide this is that your form could have non-array values as well, so there is no obvious implementation.</p><p>Brut <em>does</em> provide an <code>_each</code> method for every array field. We can use that to iterate over however many values were submitted.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> BulkCreateWidgetsForm</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> <</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppForm</span></span>
|
2
2
|
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:name</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">array:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">required:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> false</span></span>
|
3
3
|
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> input </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:quantity</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">type:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> :number</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">array:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> true</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">required:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> false</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
|
4
4
|
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> min:</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> 1</span></span>
|
@@ -71,4 +71,4 @@ import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.1L-BeKqY.js";const g
|
|
71
71
|
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> end</span></span>
|
72
72
|
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> end</span></span>
|
73
73
|
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> end</span></span>
|
74
|
-
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">end</span></span></code></pre></div><p>Even if the form doesn't have 10 entries, the code above will create 10 pairs of fields. If there are server-side constraint violations, they will be shown for the appropriate index. Lastly, Brut's components (like <code>TextField</code>) will use the Rack non-standard HTML for arrays of values. Instead of <code>name="quantity"</code>, Brut will render <code>name="quantity[]"</code>.</p>`,23)]))}const o=i(h,[["render",t]]);export{g as __pageData,o as default};
|
74
|
+
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">end</span></span></code></pre></div><p>Even if the form doesn't have 10 entries, the code above will create 10 pairs of fields. If there are server-side constraint violations, they will be shown for the appropriate index. Lastly, Brut's components (like <code>TextField</code>) will use the Rack non-standard HTML for arrays of values. Instead of <code>name="quantity"</code>, Brut will render <code>name="quantity[]"</code>.</p>`,23)])])}const o=i(h,[["render",t]]);export{g as __pageData,o as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.C4nOkCZI.js";const g=JSON.parse('{"title":"Indexed Forms","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/indexed-forms.md","filePath":"recipes/indexed-forms.md"}'),h={name:"recipes/indexed-forms.md"};function t(l,s,p,k,r,d){return n(),a("div",null,[...s[0]||(s[0]=[e("",23)])])}const o=i(h,[["render",t]]);export{g as __pageData,o as default};
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import{_ as a,c as i,o as n,ag as e}from"./chunks/framework.
|
1
|
+
import{_ as a,c as i,o as n,ag as e}from"./chunks/framework.C4nOkCZI.js";const c=JSON.parse('{"title":"Migration Example","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/migrations.md","filePath":"recipes/migrations.md"}'),t={name:"recipes/migrations.md"};function l(p,s,h,k,d,o){return n(),i("div",null,[...s[0]||(s[0]=[e(`<h1 id="migration-example" tabindex="-1">Migration Example <a class="header-anchor" href="#migration-example" aria-label="Permalink to "Migration Example""></a></h1><p>If you've not used <a href="https://sequel.jeremyevans.net/" target="_blank" rel="noreferrer">Sequel</a> before, this recipe will show you the basics of creating <a href="https://sequel.jeremyevans.net/rdoc/files/doc/migration_rdoc.html" target="_blank" rel="noreferrer">migrations</a>, which are how the database schema is managed in Brut.</p><h2 id="feature" tabindex="-1">Feature <a class="header-anchor" href="#feature" aria-label="Permalink to "Feature""></a></h2><ul><li>An accounts table will store an email and a deactivated date</li><li>A blog posts table will store a title and content, and be attributed to an account.</li></ul><h2 id="recipe" tabindex="-1">Recipe <a class="header-anchor" href="#recipe" aria-label="Permalink to "Recipe""></a></h2><p>We'll create the migration, create data models, create and lint factories, then create seed data.</p><h3 id="create-the-migration" tabindex="-1">Create the Migration <a class="header-anchor" href="#create-the-migration" aria-label="Permalink to "Create the Migration""></a></h3><p>Create the migration file with <code>bin/db new_migration</code>:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>> bin/db new_migration Accounts and Blog Posts</span></span>
|
2
2
|
<span class="line"><span>[ bin/db ] Migration created:</span></span>
|
3
3
|
<span class="line"><span> app/src/back_end/data_models/migrations/20250711215310_Accounts-and-Blog-Posts.rb</span></span></code></pre></div><div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p><p>Your filename will be different, since it embeds a timestamp for when <code>bin/db new_migration</code> was run.</p></div><p>Now, use Sequel's migrations API, keeping in mind <a href="/database-schema.html">Brut's augmentations</a>, to create our tables.</p><p>Our tables will use <a href="/database-schema.html#external-ids">an external ids</a>. Note that Brut will ensure both tables have primary keys and have <code>created_at</code> fields.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/back_end/data_models/migrations/20250711215310_Accounts-and-Blog-Posts.rb</span></span>
|
4
4
|
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Sequel</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">migration</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> do</span></span>
|
@@ -94,4 +94,4 @@ import{_ as a,c as i,o as n,ag as e}from"./chunks/framework.1L-BeKqY.js";const c
|
|
94
94
|
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> create</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">:blog_post</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">, </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">account:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> chris)</span></span>
|
95
95
|
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
96
96
|
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
97
|
-
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>We can apply this with <code>bin/db seed</code>:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>bin/db seed</span></span></code></pre></div><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p><code>bin/db rebuild</code> will <em>not</em> apply seed data, however <code>bin/setup</code> should. For now, if you want to totally reset your database, you will need to do <code>bin/db rebuild && bin/db seed && bin/db rebuild -e test</code></p></div>`,37)]))}const g=a(t,[["render",l]]);export{c as __pageData,g as default};
|
97
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>We can apply this with <code>bin/db seed</code>:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>bin/db seed</span></span></code></pre></div><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p><code>bin/db rebuild</code> will <em>not</em> apply seed data, however <code>bin/setup</code> should. For now, if you want to totally reset your database, you will need to do <code>bin/db rebuild && bin/db seed && bin/db rebuild -e test</code></p></div>`,37)])])}const g=a(t,[["render",l]]);export{c as __pageData,g as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as a,c as i,o as n,ag as e}from"./chunks/framework.C4nOkCZI.js";const c=JSON.parse('{"title":"Migration Example","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/migrations.md","filePath":"recipes/migrations.md"}'),t={name:"recipes/migrations.md"};function l(p,s,h,k,d,o){return n(),i("div",null,[...s[0]||(s[0]=[e("",37)])])}const g=a(t,[["render",l]]);export{c as __pageData,g as default};
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import{_ as i,c as a,o as n,ag as l}from"./chunks/framework.
|
1
|
+
import{_ as i,c as a,o as n,ag as l}from"./chunks/framework.C4nOkCZI.js";const g=JSON.parse('{"title":"Creating your Own Text Field Component","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/text-field-component.md","filePath":"recipes/text-field-component.md"}'),h={name:"recipes/text-field-component.md"};function t(p,s,k,e,E,r){return n(),a("div",null,[...s[0]||(s[0]=[l(`<h1 id="creating-your-own-text-field-component" tabindex="-1">Creating your Own Text Field Component <a class="header-anchor" href="#creating-your-own-text-field-component" aria-label="Permalink to "Creating your Own Text Field Component""></a></h1><p>Brut's <a href="/api/Brut/FrontEnd/Components/Input/InputTag.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::Input::InputTag</code></a> creates only the <code><input></code> HTML element. You will likely want something more sophsticated. You can achieve this by creating your own component.</p><h2 id="feature" tabindex="-1">Feature <a class="header-anchor" href="#feature" aria-label="Permalink to "Feature""></a></h2><p>We'll make a text field that has a label, error messages, and styling. It will support three sizes: small, normal, and large.</p><p>It will require a form and an input name, and optional index as well.</p><h2 id="recipe" tabindex="-1">Recipe <a class="header-anchor" href="#recipe" aria-label="Permalink to "Recipe""></a></h2><h3 id="create-the-initializer" tabindex="-1">Create the Initializer <a class="header-anchor" href="#create-the-initializer" aria-label="Permalink to "Create the Initializer""></a></h3><p>First, we'll create the component:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>bin/scaffold component text_field</span></span></code></pre></div><p>Now, edit the initializer to accept the parameters we need:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/front_end/components/text_field_component.rb</span></span>
|
2
2
|
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> TextFieldComponent</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> <</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AppComponent</span></span>
|
3
3
|
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> def</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> initialize</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">form:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
|
4
4
|
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> input_name:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
|
@@ -98,4 +98,4 @@ import{_ as i,c as a,o as n,ag as l}from"./chunks/framework.1L-BeKqY.js";const g
|
|
98
98
|
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> button { </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"Save"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> }</span></span>
|
99
99
|
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
100
100
|
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
101
|
-
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div>`,27)]))}const y=i(h,[["render",t]]);export{g as __pageData,y as default};
|
101
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div>`,27)])])}const y=i(h,[["render",t]]);export{g as __pageData,y as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as i,c as a,o as n,ag as l}from"./chunks/framework.C4nOkCZI.js";const g=JSON.parse('{"title":"Creating your Own Text Field Component","description":"","frontmatter":{},"headers":[],"relativePath":"recipes/text-field-component.md","filePath":"recipes/text-field-component.md"}'),h={name:"recipes/text-field-component.md"};function t(p,s,k,e,E,r){return n(),a("div",null,[...s[0]||(s[0]=[l("",27)])])}const y=i(h,[["render",t]]);export{g as __pageData,y as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as t,c as o,o as a,ag as i}from"./chunks/framework.C4nOkCZI.js";const h=JSON.parse('{"title":"Roadmap to 1.0","description":"","frontmatter":{},"headers":[],"relativePath":"roadmap.md","filePath":"roadmap.md"}'),l={name:"roadmap.md"};function r(s,e,n,d,c,m){return a(),o("div",null,[...e[0]||(e[0]=[i('<h1 id="roadmap-to-1-0" tabindex="-1">Roadmap to 1.0 <a class="header-anchor" href="#roadmap-to-1-0" aria-label="Permalink to "Roadmap to 1.0""></a></h1><p>A lot of Brut is solid, but there's several things missing from what I would call a 1.0 release. Here are some ideas of what I think is needed:</p><h2 id="better-dev-experience" tabindex="-1">Better Dev Experience <a class="header-anchor" href="#better-dev-experience" aria-label="Permalink to "Better Dev Experience""></a></h2><ul><li>The output of <code>bin/dev</code> isn't great.</li><li>otel-desktop-viewer is cool, but not the easiest to figure out issues as compred to good 'ole logging.</li><li>Error pages in the app are <em>really</em> bad.</li><li>CLI apps are OK, but could be fancier.</li></ul><h2 id="more-tests" tabindex="-1">More Tests <a class="header-anchor" href="#more-tests" aria-label="Permalink to "More Tests""></a></h2><ul><li>Unit tests for all/most classes are needed. There's only a few now.</li><li>Integration test of <code>mkbrut</code>, all automated.</li><li>Web component/custom element tests need to be re-thought.</li><li>Test output is a wall of text stack trace and this sucks.</li><li>Improvements in access to Playwright features.</li><li>Playright is the worst E2E testing tool except all the rest. Would love a better option here.</li></ul><h2 id="more-complete-web-features" tabindex="-1">More Complete Web Features <a class="header-anchor" href="#more-complete-web-features" aria-label="Permalink to "More Complete Web Features""></a></h2><ul><li>Content security policy doens't allow for hashes, which can be limiting in some situations. I want everyone to be running with a CSP, so it has to be configurable to some degree.</li><li>Websockets, server-push, etc. should be possible or at least have a recipe.</li><li>Learn more about importmaps.</li></ul><h2 id="client-side-improvements" tabindex="-1">Client-Side Improvements <a class="header-anchor" href="#client-side-improvements" aria-label="Permalink to "Client-Side Improvements""></a></h2><p>BrutJS is woefully incomplete. I'd like developers to be able to accomplishe certain tasks without needing a framework:</p><ul><li>Hooks into asset building to e.g. enable TailwindCSS or other tools.</li><li>Better use of <code>fetch</code> in more situations</li><li>Server-generated HTML replacement</li><li>Better support for "API" style back-end when a framework <em>is</em> going to be used.</li></ul><h2 id="deployment" tabindex="-1">Deployment <a class="header-anchor" href="#deployment" aria-label="Permalink to "Deployment""></a></h2><p>Out of the box support for more deployment mechanism, at least:</p><ul><li>Normal Heroku/<code>Procfile</code>-based deploy</li><li>Digital Ocean-style hosting</li><li>VPS?</li></ul><h2 id="documentation" tabindex="-1">Documentation <a class="header-anchor" href="#documentation" aria-label="Permalink to "Documentation""></a></h2><ul><li>More recipes for how to do things</li><li>More complete API docs with examples</li><li>A unified look and feel across the board</li><li>Get rid of VitePress for something less client-heavy, but still great</li><li>Dash-accessible API docs</li></ul><h2 id="misc" tabindex="-1">Misc <a class="header-anchor" href="#misc" aria-label="Permalink to "Misc""></a></h2><ul><li>More direct Sidekiq support</li></ul>',18)])])}const p=t(l,[["render",r]]);export{h as __pageData,p as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as t,c as o,o as a,ag as i}from"./chunks/framework.C4nOkCZI.js";const h=JSON.parse('{"title":"Roadmap to 1.0","description":"","frontmatter":{},"headers":[],"relativePath":"roadmap.md","filePath":"roadmap.md"}'),l={name:"roadmap.md"};function r(s,e,n,d,c,m){return a(),o("div",null,[...e[0]||(e[0]=[i("",18)])])}const p=t(l,[["render",r]]);export{h as __pageData,p as default};
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import{_ as a,c as t,o as s,ag as i}from"./chunks/framework.
|
1
|
+
import{_ as a,c as t,o as s,ag as i}from"./chunks/framework.C4nOkCZI.js";const u=JSON.parse('{"title":"Routes","description":"","frontmatter":{},"headers":[],"relativePath":"routes.md","filePath":"routes.md"}'),o={name:"routes.md"};function n(r,e,l,h,d,p){return s(),t("div",null,[...e[0]||(e[0]=[i(`<h1 id="routes" tabindex="-1">Routes <a class="header-anchor" href="#routes" aria-label="Permalink to "Routes""></a></h1><p>The primary function of a web framework like Brut is to map URLs requested by the browser or an HTTP client and invoke code based on them.</p><p>Brut has a fairly simple routing system that's not designed for flexibility.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to "Overview""></a></h2><p>Your app has a subclass of <a href="/api/Brut/Framework/App.html" target="_self" rel="noopener" data-no-router><code>Brut::Framework::App</code></a>, called <code>App</code>. It includes a call to the <code>routes</code> class method. In there, you declare your routes by using one of four methods:</p><table tabindex="0"><thead><tr><th>Method</th><th>HTTP Method</th><th>Purpose</th></tr></thead><tbody><tr><td><code>page «route»</code></td><td>GET</td><td>Declare a page</td></tr><tr><td><code>form «route»</code></td><td>POST</td><td>Declare a form to be submitted to a handler</td></tr><tr><td><code>action «route»</code></td><td>POST</td><td>Declare an element-less form to be submitted to a handler (akin to Rails' <code>button_to</code> helper)</td></tr><tr><td><code>path «route», method: «method»</code></td><td><code>«method»</code></td><td>Declare an arbitrary path to a handler</td></tr></tbody></table><p>The value for <code>«route»</code>, along with the method called, is used to determine what class(es) will be used to handle the route.</p><h3 id="«route»-syntax" tabindex="-1">«route» Syntax <a class="header-anchor" href="#«route»-syntax" aria-label="Permalink to "«route» Syntax""></a></h3><p>A route is a string that contains the <em>path part</em> of a <a href="https://developer.mozilla.org/en-US/docs/Web/API/URL" target="_blank" rel="noreferrer">URL</a>. <em>Segments</em> of the path (i.e. the stuff between each forward slash <code>/</code>) can be either <em>static</em> or a <em>placeholder</em>.</p><p>As such:</p><ul><li>Only the <a href="https://developer.mozilla.org/en-US/docs/Web/API/URL/pathname" target="_blank" rel="noreferrer">pathname</a> of a request may be specified.</li><li>All routes must start with a slash</li><li>A placeholder segment must be a valid Ruby identifier preceded by a colon, e.g. <code>:company_id</code> is allowed, but <code>:company-id</code> is not.</li><li>Routes may not start with a placeholder.</li></ul><p>Some examples:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span>"/dash_board"</span></span>
|
2
2
|
<span class="line"><span>"/widgets/:id"</span></span>
|
3
3
|
<span class="line"><span>"/company/:company_id/locations/:location_id"</span></span>
|
4
4
|
<span class="line"><span>"/"</span></span></code></pre></div><h3 id="class-naming-conventions" tabindex="-1">Class Naming Conventions <a class="header-anchor" href="#class-naming-conventions" aria-label="Permalink to "Class Naming Conventions""></a></h3><p>Brut is convention-based, so you are not able to specify the name of the classes used to handle routes. Brut will use the method you called (e.g. <code>page</code>) and the route your provided to determine the class name.</p><p>Some examples:</p><table tabindex="0"><thead><tr><th>Route invocation</th><th>Expected Class Name(s)</th></tr></thead><tbody><tr><td><code>page "/dashboard"</code></td><td><code>DashboardPage</code></td></tr><tr><td><code>page "/widgets/:id"</code></td><td><code>WidgetsByIdPage</code></td></tr><tr><td><code>form "/login"</code></td><td><code>LoginForm</code> and <code>LoginHandler</code></td></tr><tr><td><code>action "/delete_widget/:id"</code></td><td><code>DeleteWidgetWithIdHandler</code></td></tr><tr><td><code>path "/tokens/personal/:token, method :put"</code></td><td><code>Tokens::PersonalWithTokenHandler</code></td></tr></tbody></table><p>Specifically, the name of the class(es) is/are determined as follows:</p><ul><li>Static segments of the pathname are mapped to namespaces or a class based on converting the path segment to camel-case. For example <code>new_widget</code> becomes <code>NewWidget</code>.</li><li>The final static segment in the path represents a class name. All other static segments represent modules in which the final class is namespaced <ul><li>If the route is for a page, <code>Page</code> is appended to the class name.</li><li>If the route is for a form, there are two classes in play, one appended with <code>Form</code> and one with <code>Handler</code>.</li><li>If the route has no form and is just a handler, <code>Handler</code> is appended to the class name.</li></ul></li><li>Placeholder segments are attached to the previous static segment, augmenting its name: <ul><li>The placeholder is camel-cased</li><li>The placeholder is prefixed with <code>By</code> for <code>page</code> routes and <code>With</code> for all other routes</li><li>the prefixed-placeholder is appended to the previous module or class name, e.g. <code>WidgetsById</code></li></ul></li><li>These are now connected to form a valid Ruby class name.</li><li>The route <code>/</code> is special and always maps to <code>HomePage</code>.</li></ul><p>Note that deeply nested routes that contain several placeholders will work, and create complicated classnames.</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">page </span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"/company/:company_id/location/:location_id"</span></span>
|
@@ -18,4 +18,4 @@ import{_ as a,c as t,o as s,ag as i}from"./chunks/framework.1L-BeKqY.js";const u
|
|
18
18
|
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># :id was used as a path parameter for</span></span>
|
19
19
|
<span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># WidgetsByIdPage (path '/widgets/:id')</span></span></code></pre></div><p><code>routing</code> is how you create links to other pages:</p><div class="language-erb vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">erb</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"><</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">a</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> href</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">=</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"><%= </span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">DashBoardPage</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">.</span><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">routing</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> %></span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">></span></span>
|
20
20
|
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> Go to Dashboard</span></span>
|
21
|
-
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"></</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">></span></span></code></pre></div><div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p><p>You can use <code>routing</code> to create <code><form></code> actions, but <a href="/api/Brut/FrontEnd/Components/FormTag.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::FormTag</code></a>, which we'll discuss in <a href="/forms.html">Forms</a>, can do this for you.</p></div><p>The <code>routing</code> method isn't an abstraction around routes. It's more of a strongly-typed translation. This means when you change something, your app won't route to non-existent routes—it'll blow up with a helpful error.</p><p>For example, if you decided that <code>/dash_board/</code> should've been called <code>/account_home</code>, you would change the value in <code>app.rb</code>, then rename the class. At this point, any code that routes to <code>DashboardPage.routing</code> will raise a <code>NameError</code>. With sufficient test coverage, you can address everywhere you see the <code>NameError</code> and be confident you have changed the name and route successfully.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to "Testing""></a></h2><p>Routes are configuration, so you do not need to test them. In fact, you can't test them directly. Your end-to-end tests should adequately cover the correct usage of your routes. If you always using <code>.routing</code> to generate routes, Ruby's runtime checks will also ensure you have not used a non-existent or invalid route.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to "Recommended Practices""></a></h2><p>Brut does not provide flexibility with routes, nor is logic intended to exist where you are declaring them.</p><h3 id="routes-should-be-named-for-concepts-anyone-can-understand" tabindex="-1">Routes Should be Named for Concepts Anyone Can Understand <a class="header-anchor" href="#routes-should-be-named-for-concepts-anyone-can-understand" aria-label="Permalink to "Routes Should be Named for Concepts Anyone Can Understand""></a></h3><p>If you have an account management page that allows modifying data in a table called <code>user_preferences</code>, but everyone just calls it "the account management page", the route should be <code>/account_management</code>.</p><p>Although routes are primarily for programmers, there's no reason not to name them using the terms everyone involved in your app uses. This is part of the reason Brut inserts <code>By</code> or <code>With</code> when there is a placeholder. It allows you to have a page for all widgets—the "widgets page"—and a page for a specific widget by id—the "widgets by id page".</p><h3 id="prefer-shallow-routes-with-a-single-placeholder" tabindex="-1">Prefer Shallow Routes with a Single Placeholder <a class="header-anchor" href="#prefer-shallow-routes-with-a-single-placeholder" aria-label="Permalink to "Prefer Shallow Routes with a Single Placeholder""></a></h3><p>The more path segments your route has, and the more placeholders it is, the longer your class name will be and the more you lose the connection to reality. The "company by company id location by location id page" doesn't exactly roll off the tongue.</p><p>Life will be easier if you can choose names and routes that have a single placeholder. Multiple path segments can be useful for namespacing.</p><h3 id="placeholders-identify-things-query-strings-search-for-things" tabindex="-1">Placeholders Identify Things, Query Strings Search for Things <a class="header-anchor" href="#placeholders-identify-things-query-strings-search-for-things" aria-label="Permalink to "Placeholders Identify Things, Query Strings Search for Things""></a></h3><p>A query string is for just that: querying. The query string is not for identifying things. That's what URIs are for.</p><p>As such, for routes where a specific <em>thing</em> is being identified, use route placeholders like <code>/widgets/:id</code>. When a route is used for searching or locating <em>things</em>, a query string is better: <code>/widgets?type=«type»</code>.</p><p>Remember that the query string is <em>not</em> part of the class name. The values for the query string will be made available to your page or handler.</p><h3 id="pluralization-is-up-to-you" tabindex="-1">Pluralization Is Up to You <a class="header-anchor" href="#pluralization-is-up-to-you" aria-label="Permalink to "Pluralization Is Up to You""></a></h3><p>The rules Brut uses to determine the class names to handle routes do not rely on pluralization. You can have a <code>/widget</code> route and a <code>/widgets</code> route, if that makes sense to your domain and team. They are both handled by the same set of underlying rules.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to "Technical Notes""></a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's internals, the source code is always more correct.</p></div><p><em>Last Updated Feb 23, 2025</em></p><p>Brut stores all configured routes in a <a href="/api/Brut/FrontEnd/Routing.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Routing</code></a> object. This means that all metadata about a route is available. You are not intended to interact with this class, but you will note that in certain circumstances, the <a href="/api/Brut/FrontEnd/Routing/Route.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Routing::Route</code></a> can be injected into your class.</p><p>Brut uses this metadata to create route handlers with Sinatra. While Brut may not always use Sinatra under the covers, it does as of the writing, so when you call <code>page "/widgets"</code>, Brut will call <code>get "/widgets" do</code> and pass a block to Sinatra to find the class to handle the reqest, create an instance of it, call a method on it, and return the response.</p>`,54)]))}const g=a(o,[["render",n]]);export{u as __pageData,g as default};
|
21
|
+
<span class="line"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"></</span><span style="--shiki-light:#22863A;--shiki-dark:#85E89D;">a</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">></span></span></code></pre></div><div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p><p>You can use <code>routing</code> to create <code><form></code> actions, but <a href="/api/Brut/FrontEnd/Components/FormTag.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::FormTag</code></a>, which we'll discuss in <a href="/forms.html">Forms</a>, can do this for you.</p></div><p>The <code>routing</code> method isn't an abstraction around routes. It's more of a strongly-typed translation. This means when you change something, your app won't route to non-existent routes—it'll blow up with a helpful error.</p><p>For example, if you decided that <code>/dash_board/</code> should've been called <code>/account_home</code>, you would change the value in <code>app.rb</code>, then rename the class. At this point, any code that routes to <code>DashboardPage.routing</code> will raise a <code>NameError</code>. With sufficient test coverage, you can address everywhere you see the <code>NameError</code> and be confident you have changed the name and route successfully.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to "Testing""></a></h2><p>Routes are configuration, so you do not need to test them. In fact, you can't test them directly. Your end-to-end tests should adequately cover the correct usage of your routes. If you always using <code>.routing</code> to generate routes, Ruby's runtime checks will also ensure you have not used a non-existent or invalid route.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to "Recommended Practices""></a></h2><p>Brut does not provide flexibility with routes, nor is logic intended to exist where you are declaring them.</p><h3 id="routes-should-be-named-for-concepts-anyone-can-understand" tabindex="-1">Routes Should be Named for Concepts Anyone Can Understand <a class="header-anchor" href="#routes-should-be-named-for-concepts-anyone-can-understand" aria-label="Permalink to "Routes Should be Named for Concepts Anyone Can Understand""></a></h3><p>If you have an account management page that allows modifying data in a table called <code>user_preferences</code>, but everyone just calls it "the account management page", the route should be <code>/account_management</code>.</p><p>Although routes are primarily for programmers, there's no reason not to name them using the terms everyone involved in your app uses. This is part of the reason Brut inserts <code>By</code> or <code>With</code> when there is a placeholder. It allows you to have a page for all widgets—the "widgets page"—and a page for a specific widget by id—the "widgets by id page".</p><h3 id="prefer-shallow-routes-with-a-single-placeholder" tabindex="-1">Prefer Shallow Routes with a Single Placeholder <a class="header-anchor" href="#prefer-shallow-routes-with-a-single-placeholder" aria-label="Permalink to "Prefer Shallow Routes with a Single Placeholder""></a></h3><p>The more path segments your route has, and the more placeholders it is, the longer your class name will be and the more you lose the connection to reality. The "company by company id location by location id page" doesn't exactly roll off the tongue.</p><p>Life will be easier if you can choose names and routes that have a single placeholder. Multiple path segments can be useful for namespacing.</p><h3 id="placeholders-identify-things-query-strings-search-for-things" tabindex="-1">Placeholders Identify Things, Query Strings Search for Things <a class="header-anchor" href="#placeholders-identify-things-query-strings-search-for-things" aria-label="Permalink to "Placeholders Identify Things, Query Strings Search for Things""></a></h3><p>A query string is for just that: querying. The query string is not for identifying things. That's what URIs are for.</p><p>As such, for routes where a specific <em>thing</em> is being identified, use route placeholders like <code>/widgets/:id</code>. When a route is used for searching or locating <em>things</em>, a query string is better: <code>/widgets?type=«type»</code>.</p><p>Remember that the query string is <em>not</em> part of the class name. The values for the query string will be made available to your page or handler.</p><h3 id="pluralization-is-up-to-you" tabindex="-1">Pluralization Is Up to You <a class="header-anchor" href="#pluralization-is-up-to-you" aria-label="Permalink to "Pluralization Is Up to You""></a></h3><p>The rules Brut uses to determine the class names to handle routes do not rely on pluralization. You can have a <code>/widget</code> route and a <code>/widgets</code> route, if that makes sense to your domain and team. They are both handled by the same set of underlying rules.</p><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to "Technical Notes""></a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's internals, the source code is always more correct.</p></div><p><em>Last Updated Feb 23, 2025</em></p><p>Brut stores all configured routes in a <a href="/api/Brut/FrontEnd/Routing.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Routing</code></a> object. This means that all metadata about a route is available. You are not intended to interact with this class, but you will note that in certain circumstances, the <a href="/api/Brut/FrontEnd/Routing/Route.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Routing::Route</code></a> can be injected into your class.</p><p>Brut uses this metadata to create route handlers with Sinatra. While Brut may not always use Sinatra under the covers, it does as of the writing, so when you call <code>page "/widgets"</code>, Brut will call <code>get "/widgets" do</code> and pass a block to Sinatra to find the class to handle the reqest, create an instance of it, call a method on it, and return the response.</p>`,54)])])}const g=a(o,[["render",n]]);export{u as __pageData,g as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as a,c as t,o as s,ag as i}from"./chunks/framework.C4nOkCZI.js";const u=JSON.parse('{"title":"Routes","description":"","frontmatter":{},"headers":[],"relativePath":"routes.md","filePath":"routes.md"}'),o={name:"routes.md"};function n(r,e,l,h,d,p){return s(),t("div",null,[...e[0]||(e[0]=[i("",54)])])}const g=a(o,[["render",n]]);export{u as __pageData,g as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as t,c as o,o as a,ag as r}from"./chunks/framework.C4nOkCZI.js";const p=JSON.parse('{"title":"Security","description":"","frontmatter":{},"headers":[],"relativePath":"security.md","filePath":"security.md"}'),i={name:"security.md"};function s(n,e,c,l,d,u){return a(),o("div",null,[...e[0]||(e[0]=[r('<h1 id="security" tabindex="-1">Security <a class="header-anchor" href="#security" aria-label="Permalink to "Security""></a></h1><p>As a new framework, Brut has not been battle-tested against potential securitiy exploits. That said, Brut does configure several features to help manage security issues.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to "Overview""></a></h2><p>Web application security is a large topic. Brut provides a few features out of the box to manage security of your app:</p><ul><li>Encrypted sessions</li><li>Cross-Site Request Forgery (CSRF) protection</li><li>Content Security Policy headers and tools</li><li><code>bundle audit</code></li></ul><h3 id="encrypted-sessions" tabindex="-1">Encrypted Sessions <a class="header-anchor" href="#encrypted-sessions" aria-label="Permalink to "Encrypted Sessions""></a></h3><p>Brut uses the <code>Rack::Session::Cookie</code> middleware to manage cookies. They are encrypted with the value of the environment variable <code>SESSION_SECRET</code>. This prevents both the website visitor and other websites from examining the contents of the cookie.</p><p>This means that if Brut places data in there, it can rely on it being safe when fetched later.</p><p>Cookie behavior and configuration is currently not configurable.</p><h3 id="csrf-protection" tabindex="-1">CSRF Protection <a class="header-anchor" href="#csrf-protection" aria-label="Permalink to "CSRF Protection""></a></h3><p>A <a href="https://owasp.org/www-community/attacks/csrf" target="_blank" rel="noreferrer">cross-site request forgery</a> happens when a malicious site submits information from their site to a Brut-powered site, assuming it will allow and respect your credentials, potentially initiating an action on the Brut-powered site that you did not intend.</p><p>The common way to mitigate this is to require a CSRF token to be included with each form submission. This value is generated by the server and included int he user's secure session. If the form submission's CSRF token matches, the request is assumed to be valid.</p><p>Brut configures <a href="https://sinatrarb.com/rack-protection/" target="_blank" rel="noreferrer">Rack::Protection::AuthenticityToken</a> as a middleware. Its configuration requires that <em>all</em> form submissions include a valid CSRF token, with a few exceptions noted in the <a href="#technical-notes">Technical Notes</a> section below.</p><p>To include a valid CSRF token in your <a href="/forms.html">form</a>, use <a href="/api/Brut/FrontEnd/Components/FormTag.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Components::FormTag</code></a> to generate your <code><form></code>. That's it.</p><p>Brut's approach to Ajax requests is to model them as form submissions, so by creating a form using <code>FormTag</code> and using its <a href="https://developer.mozilla.org/en-US/docs/Web/API/FormData" target="_blank" rel="noreferrer"><code>FormData</code></a> to submit that form with e.g. <code>fetch</code>, the CSRF token will be supplied.</p><div class="note custom-block github-alert"><p class="custom-block-title">NOTE</p><p>AJax and CSRF is currently somewhat immature in Brut, so there may be areas where things don't work as you'd expect.</p></div><h3 id="content-security-policy-headers-and-tools" tabindex="-1">Content Security Policy headers and tools <a class="header-anchor" href="#content-security-policy-headers-and-tools" aria-label="Permalink to "Content Security Policy headers and tools""></a></h3><p><a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP" target="_blank" rel="noreferrer">Content Security Policy (CSP)</a> is a way for a website to tell the browser what CSS and JavaScript is allowed to execute on the page. Most web frameworks default to a lax or no policy, which makes applying one later difficult.</p><p>Brut takes the opposite approach. By default, Brut configures <a href="/api/Brut/FrontEnd/RouteHooks/CSPNoInlineStylesOrScripts.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::RouteHooks::CSPNoInlineStylesOrScripts</code></a> as an after route hook. This sets the content security poilcy header to disallow all inline stylings (the <code>style=</code> attribute) and all inline script tags (e.g. <code>onclick=</code>). In development, inline styles are allowed, since they are convienient when prototyping.</p><p>Brut also configures CSP reporting, which is sent into the <a href="/instrumentation.html">instrumentation</a> system.</p><p>You can control this behavior by changing the <code>csp_class</code> and/or <code>csp_reporting_class</code> using <code>Brut.container.override</code> in your <code>App</code>. You can either create your own route hook and use that, or set either to <code>nil</code> to disable CSP entirely.</p><h3 id="bundle-audit" tabindex="-1"><code>bundle audit</code> <a class="header-anchor" href="#bundle-audit" aria-label="Permalink to "`bundle audit`""></a></h3><p>The <code>bin/ci</code> script provided when you created your Burt app includes a call to <code>bundle exec bundle audit check --update</code>, which will audit your RubyGems for versions that have known vulnerabilities. If any are found, <code>bin/ci</code> exits nonzero, thus failing your build.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to "Testing""></a></h2><p>In general, you don't want to test any of this unless you've set up something custom or complex. Testing security is done more effectively by a third party evaluating your app as a so-called "black box" or "opaque box" where its probed for issues without knowing how the app is implemented.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to "Recommended Practices""></a></h2><ul><li>Do not disable CSRF protection.</li><li>Do not disable your content security policy. Use Brut's built-in and design your app to not require inline styles or scripts. It's much easier to not do this in the first place than to try to unwind it later.</li><li>Do not ship your app if <code>bundle audit</code> finds vulnerabilities.</li></ul><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to "Technical Notes""></a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's internals, the source code is always more correct.</p></div><p><em>Last Updated June 13, 2025</em></p><h3 id="csrf-protection-1" tabindex="-1">CSRF Protection <a class="header-anchor" href="#csrf-protection-1" aria-label="Permalink to "CSRF Protection""></a></h3><p>CSRF protection is not required for <em>Brut-owned paths</em>, as defined by <a href="/api/Brut/FrontEnd/Middlewares/AnnotateBrutOwnedPaths.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Middlewares::AnnotateBrutOwnedPaths</code></a>. The reason for this is that these paths semantically should not be HTTP GETs, but are also not form submissions. They are currently used for locale detection and instrumentation, which we believe are OK to allow without CSRF protection.</p><p>This may change in the future.</p><h3 id="encrypted-sessions-1" tabindex="-1">Encrypted Sessions <a class="header-anchor" href="#encrypted-sessions-1" aria-label="Permalink to "Encrypted Sessions""></a></h3><p>Session cookies are set to expire after 1 year and use the value <code>Lax</code> for <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies#controlling_third-party_cookies_with_samesite" target="_blank" rel="noreferrer"><code>SameSite</code></a>. "Lax" allows other sites to submit the Brut-powered site's cookies, but only if the user navigates to the site. They are not sent if another site submits an Ajax request. We feel this is the right tradeoff betweeen usability and security.</p>',35)])])}const m=t(i,[["render",s]]);export{p as __pageData,m as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as t,c as o,o as a,ag as r}from"./chunks/framework.C4nOkCZI.js";const p=JSON.parse('{"title":"Security","description":"","frontmatter":{},"headers":[],"relativePath":"security.md","filePath":"security.md"}'),i={name:"security.md"};function s(n,e,c,l,d,u){return a(),o("div",null,[...e[0]||(e[0]=[r("",35)])])}const m=t(i,[["render",s]]);export{p as __pageData,m as default};
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import{_ as e,c as s,o as i,ag as t}from"./chunks/framework.
|
1
|
+
import{_ as e,c as s,o as i,ag as t}from"./chunks/framework.C4nOkCZI.js";const k=JSON.parse('{"title":"See Data for Development","description":"","frontmatter":{},"headers":[],"relativePath":"seed-data.md","filePath":"seed-data.md"}'),n={name:"seed-data.md"};function l(o,a,h,d,r,p){return i(),s("div",null,[...a[0]||(a[0]=[t(`<h1 id="see-data-for-development" tabindex="-1">See Data for Development <a class="header-anchor" href="#see-data-for-development" aria-label="Permalink to "See Data for Development""></a></h1><p>In Brut, <em>seed data</em> is data you set up for the purposes of doing local development. It is <em>not</em> for managing production data and is not used for tests.</p><h2 id="overview" tabindex="-1">Overview <a class="header-anchor" href="#overview" aria-label="Permalink to "Overview""></a></h2><p>Seed data lives in <code>app/src/back_end/data_models/seed</code>. You can create as many files in here as you like. Each should contain a class that extends <a href="/api/Brut/BackEnd/SeedData.html" target="_self" rel="noopener" data-no-router><code>Brut::BackEnd::SeedData</code></a> and implements <code>seed!</code>. By doing this, the class is regsitered with Brut and when you run <code>bin/db seed</code> all the classes are created and <code>seed!</code> is called.</p><p>FactoryBot will be set up for you, so you can call <code>FactoryBot.create</code> to create the data.</p><p>For example, here is how we might create two accounts, one deactivated, in the same organization:</p><div class="language-ruby vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">ruby</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6A737D;--shiki-dark:#6A737D;"># app/src/back_end/data_models/seed/all_seed_data.rb</span></span>
|
2
2
|
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">class</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> AllSeedData</span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> <</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> Brut</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">BackEnd</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">SeedData</span></span>
|
3
3
|
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> include</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> FactoryBot</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Syntax</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">::</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">Methods</span></span>
|
4
4
|
<span class="line"></span>
|
@@ -11,4 +11,4 @@ import{_ as e,c as s,o as i,ag as t}from"./chunks/framework.1L-BeKqY.js";const k
|
|
11
11
|
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> organization:</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">,</span></span>
|
12
12
|
<span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> email:</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> "chris@example.com"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
13
13
|
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
14
|
-
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>You can store other data here, such as a CSV, if you want to load data organized in another way. Seed data will only ever be loaded in development, so you can organize it however you like.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to "Testing""></a></h2><p>You do not need to test your seed data. Presumably, you will be using your app in development, and this will be sufficient to determine if your seed data is fit for purpose.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to "Recommended Practices""></a></h2><ul><li>Use FactoryBot as this is consistent with your tests</li><li>Use literal values for anything relevant to local development. In the example above, the two email addresses are important, presumably because you'd be using those to login. The organization name, however, is irrelevant, so we allow Faker to come up with a name.</li><li>Use local variables as documentation. In the example above, there's no need to set <code>active_account</code> or <code>inactive_account</code>, but doing so makes it clear what those objects are for.</li><li>Ensure all seed data classes are independent from one another, as the order of their exeuction cannot be guaranteed.</li></ul><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to "Technical Notes""></a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's internals, the source code is always more correct.</p></div><p><em>Last Updated May 8, 2025</em></p><p>All seed data is loaded in one transaction. This means that if any class' seed data fails, no data will be written.</p><p>Seed data also is not assumed to be idempotent. If you run it twice, you will likely get an error. Because <a href="/database-schema.html#ephemeral-dev-database">your dev database is ephemeral</a>, you can always recreate your dev database via <code>bin/db rebuild && bin/db seed</code>.</p>`,17)]))}const u=e(n,[["render",l]]);export{k as __pageData,u as default};
|
14
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">end</span></span></code></pre></div><p>You can store other data here, such as a CSV, if you want to load data organized in another way. Seed data will only ever be loaded in development, so you can organize it however you like.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to "Testing""></a></h2><p>You do not need to test your seed data. Presumably, you will be using your app in development, and this will be sufficient to determine if your seed data is fit for purpose.</p><h2 id="recommended-practices" tabindex="-1">Recommended Practices <a class="header-anchor" href="#recommended-practices" aria-label="Permalink to "Recommended Practices""></a></h2><ul><li>Use FactoryBot as this is consistent with your tests</li><li>Use literal values for anything relevant to local development. In the example above, the two email addresses are important, presumably because you'd be using those to login. The organization name, however, is irrelevant, so we allow Faker to come up with a name.</li><li>Use local variables as documentation. In the example above, there's no need to set <code>active_account</code> or <code>inactive_account</code>, but doing so makes it clear what those objects are for.</li><li>Ensure all seed data classes are independent from one another, as the order of their exeuction cannot be guaranteed.</li></ul><h2 id="technical-notes" tabindex="-1">Technical Notes <a class="header-anchor" href="#technical-notes" aria-label="Permalink to "Technical Notes""></a></h2><div class="important custom-block github-alert"><p class="custom-block-title">IMPORTANT</p><p>Technical Notes are for deeper understanding and debugging. While we will try to keep them up-to-date with changes to Brut's internals, the source code is always more correct.</p></div><p><em>Last Updated May 8, 2025</em></p><p>All seed data is loaded in one transaction. This means that if any class' seed data fails, no data will be written.</p><p>Seed data also is not assumed to be idempotent. If you run it twice, you will likely get an error. Because <a href="/database-schema.html#ephemeral-dev-database">your dev database is ephemeral</a>, you can always recreate your dev database via <code>bin/db rebuild && bin/db seed</code>.</p>`,17)])])}const u=e(n,[["render",l]]);export{k as __pageData,u as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as e,c as s,o as i,ag as t}from"./chunks/framework.C4nOkCZI.js";const k=JSON.parse('{"title":"See Data for Development","description":"","frontmatter":{},"headers":[],"relativePath":"seed-data.md","filePath":"seed-data.md"}'),n={name:"seed-data.md"};function l(o,a,h,d,r,p){return i(),s("div",null,[...a[0]||(a[0]=[t("",17)])])}const u=e(n,[["render",l]]);export{k as __pageData,u as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as t,c as o,o as s,ag as i}from"./chunks/framework.C4nOkCZI.js";const l=JSON.parse('{"title":"Space/Time Continuum - Making Sense of Times and Time Zones","description":"","frontmatter":{},"headers":[],"relativePath":"space-time-continuum.md","filePath":"space-time-continuum.md"}'),a={name:"space-time-continuum.md"};function n(r,e,d,c,h,m){return s(),o("div",null,[...e[0]||(e[0]=[i('<h1 id="space-time-continuum-making-sense-of-times-and-time-zones" tabindex="-1">Space/Time Continuum - Making Sense of Times and Time Zones <a class="header-anchor" href="#space-time-continuum-making-sense-of-times-and-time-zones" aria-label="Permalink to "Space/Time Continuum - Making Sense of Times and Time Zones""></a></h1><p>Time zones are the worst. But they are fact of life. This means that answer a question like "what is the date?" or "is it Monday?" are not that easy to answer. Brut tries to help.</p><h2 id="timezones-outside-of-web-requests" tabindex="-1">Timezones Outside of Web Requests <a class="header-anchor" href="#timezones-outside-of-web-requests" aria-label="Permalink to "Timezones Outside of Web Requests""></a></h2><p>For back-end code, storing dates to the database, etc., Brut falls back to the normal Ruby app mechanisms for determining the "current time zone", which is to say, it super-duper depends. The system, the database, and Ruby can all configure a time zone that is in effect when dates are parsed or stored.</p><p>The main way to deal with this is in how <a href="/database-schema.html">Brut manages your database schema</a>, which is to say that it defaults to using <code>timestamp with time zone</code> and encourages you to do the same.</p><p>What this data type means is if your system is set to UTC, stores a timestamp, then restarts with the time zone set to America/Los_Angeles, that timestamp will be read back without ambiguity. If you use <code>timestamp without time zone</code> (or SQL's standard <code>timestamp</code>), this will not be the case.</p><p>All this is to say, if you use <code>timestamp with time zone</code>, you should generally not have to worry about this. Just be careful when serializing these values.</p><h2 id="timezones-for-sessions" tabindex="-1">Timezones for Sessions <a class="header-anchor" href="#timezones-for-sessions" aria-label="Permalink to "Timezones for Sessions""></a></h2><p>Depending on what your app does, you may need to show dates or times to the site visitor. And you'll probably want to show those in their time zone. Brut can help with this.</p><p>As mentioned in <a href="/flash-and-session.html">Flash and Session</a>, the session provides access to timezone information. Brut also provides a <code>Clock</code> class that represents the current date and time in the current session's time zone.</p><p>There are two ways to determine a visitor's time zone: you can ask them, or you can ask their browser.</p><h3 id="getting-timezone-from-the-browser" tabindex="-1">Getting Timezone from the Browser <a class="header-anchor" href="#getting-timezone-from-the-browser" aria-label="Permalink to "Getting Timezone from the Browser""></a></h3><p>The default <code><head></code> section of your app's <code>DefaultLayout</code> will include the Brut-provided custom element <a href="/brut-js/api/LocaleDetection.html" target="_self" rel="noopener" data-no-router><code style="white-space:nowrap;"><brut-locale-detection></code></a>. This HTML custom element is configured to communicate the browser's locale and timezone back to Brut at the URL <code>/__brut/local_detection</code>, which is handled by <a href="/api/Brut/FrontEnd/Handlers/LocaleDetectionHandler.html" target="_self" rel="noopener" data-no-router><code>Brut::FrontEnd::Handlers::LocaleDetectionHandler</code></a>.</p><p>The custom element uses <code>Intl.DateTimeFormat().resolvedOptions().timeZone</code> to determine the browser's timezone and sends this back to Brut. Whatever this value is, it will be set in the session as <code>timezone_from_browser</code>. When you ask the session for the <code>timezone_from_browser</code>, Brut will attempt to locate a <code>TZInfo::Timezone</code> with that name. If it finds one, that is returned. Otherwise, <code>nil</code> is returned.</p><p>This is only part of how Brut determines the session's time zone.</p><h3 id="getting-the-session-s-timezone" tabindex="-1">Getting the Session's Timezone <a class="header-anchor" href="#getting-the-session-s-timezone" aria-label="Permalink to "Getting the Session's Timezone""></a></h3><p>If you ask the session instead for <code>timezone</code>, and it has not been set explicitly, Brut will first check <code>timezone_from_browser</code>. If it's not <code>nil</code>, it's returned. If it <em>is</em> <code>nil</code>, the timezone whose name is in <code>ENV['TZ']</code> is returned, unless that is missing or invalid, in which case UTC is returned.</p><p>If you have a way to ask the user what their timezone is, you can set it via <code>session.timezone=</code>. If you have done this, <em>that</em> value is returned instead of the above logic.</p><p>Note that in all cases, the timezone that is serialized into the session is the name. This means it's technically possible for the name to be valid when stored and invalid when read, if you have updated the <code>tzinfo</code> gem and something changed (this should be exceedingly rare).</p><p>Therefore, it's recommended that if you have asked the visitor their preferred time zone, you store that somewhere in the database, so you can detect when the value from the session has drifted.</p><h3 id="using-the-timezone" tabindex="-1">Using the Timezone <a class="header-anchor" href="#using-the-timezone" aria-label="Permalink to "Using the Timezone""></a></h3><p>With a <code>TZInfo::Timezone</code> object, you can certainly create a Ruby <code>Time</code> object via <code>timezone.to_local(Time.new)</code>. However, you don't have to do this. By <a href="/keyword-injection.html">keyword injecting</a><code>clock:</code>, you will get an instance of <code>Clock</code>, primed with the value of <code>session.timezone</code> as its timezone.</p><p>The <code>Clock</code> responds to <code>now</code> and <code>today</code>, and will return the current timestamp and current date, respectively, in the visitor's time zone. This means that if your view code does something like <code>l(clock.now, format: :date)</code>, it will show the current time in the visitor's time zone.</p><p>This, coupled with the use of <code>timestamp with time zone</code>, means these values can be safely sent to the back-end to be stored in the database, and conversion is not necessary.</p><h2 id="testing" tabindex="-1">Testing <a class="header-anchor" href="#testing" aria-label="Permalink to "Testing""></a></h2><p>If your app makes heavy use of timezone-based timestamps or dates, you are encouraged to test this logic. <a href="/api/Brut/SpecSupport/ClockSupport.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::ClockSupport</code></a> (included by default in page, component, and handler tests) provides helper methods to manipulate and use <code>Clock</code> instances. You should not need to mock time or use something like Timecop.</p><p>If your tests just need a clock, or a clock at a time, regardless of time zone, you can use <code>real_clock</code> or <code>clock_at(now:)</code> to pass into your pages, components, and handlers. These will be set at UTC.</p><p>If your tests require a particular timezone, you may use <code>clock_in_timezone_at(timezone_name:, now:)</code>. The timezone name is from <code>tzinfo</code>'s database. <code>clock_in_timezone_at</code> will raise an error if given an invalid timezone. <code>now:</code> is a string that will be parsed as a <code>Time</code>.</p><p>If you are testing something that is sensitive to time, but you do not have access to the clock, <a href="/api/Brut/SpecSupport/SessionSupport.html" target="_self" rel="noopener" data-no-router><code>Brut::SpecSupport::SessionSupport</code></a> provides <code>empty_session</code>, which returns a session object on which you can call <code>timezone=</code>. Beyond this, you may need to provide hooks for setting the time, for example via a query string parameter.</p>',29)])])}const p=t(a,[["render",n]]);export{l as __pageData,p as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as t,c as o,o as s,ag as i}from"./chunks/framework.C4nOkCZI.js";const l=JSON.parse('{"title":"Space/Time Continuum - Making Sense of Times and Time Zones","description":"","frontmatter":{},"headers":[],"relativePath":"space-time-continuum.md","filePath":"space-time-continuum.md"}'),a={name:"space-time-continuum.md"};function n(r,e,d,c,h,m){return s(),o("div",null,[...e[0]||(e[0]=[i("",29)])])}const p=t(a,[["render",n]]);export{l as __pageData,p as default};
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import{_ as a,c as t,o as i,ag as e}from"./chunks/framework.
|
1
|
+
import{_ as a,c as t,o as i,ag as e}from"./chunks/framework.C4nOkCZI.js";const k=JSON.parse('{"title":"Tutorials","description":"","frontmatter":{},"headers":[],"relativePath":"tutorial.md","filePath":"tutorial.md"}'),n={name:"tutorial.md"};function l(o,s,h,p,d,r){return i(),t("div",null,[...s[0]||(s[0]=[e(`<h1 id="tutorials" tabindex="-1">Tutorials <a class="header-anchor" href="#tutorials" aria-label="Permalink to "Tutorials""></a></h1><p>Below are several tutorials, along with screencasts showing the tutorial steps as a video. The first one is to <a href="https://video.hardlimit.com/w/ae7EMhwjDq9kSH5dqQ9swV" target="_blank" rel="noreferrer">build a blog in 15 minutes</a>. The remainder of the tutorials assumey you are going in order, however code for each starting point is available, so you can skip around.</p><p>If you'd just like to read source code, there are two apps you can check out:</p><ul><li><a href="https://github.com/thirdtank/blog-demo" target="_blank" rel="noreferrer">The blog we'll build here</a></li><li><a href="https://github.com/thirdtank/adrs.cloud" target="_blank" rel="noreferrer">ADRs.cloud</a>, which is a more realistic app that has mulitple database tables, progressively-enhanced UI, and background jobs.</li></ul><p>You can be running either of these locally in minutes as long as you have Docker installed.</p><h2 id="understanding-these-tutorials" tabindex="-1">Understanding These Tutorials <a class="header-anchor" href="#understanding-these-tutorials" aria-label="Permalink to "Understanding These Tutorials""></a></h2><p>These tutorials will show you command line invocations and code. You should be able to follow along and just type what we say and it should work.</p><p>That said, it's not always clear what we are talking about.</p><h3 id="understanding-command-line-invocations" tabindex="-1">Understanding Command Line Invocations <a class="header-anchor" href="#understanding-command-line-invocations" aria-label="Permalink to "Understanding Command Line Invocations""></a></h3><p>If you aren't comfortable on the command line, it can be hard to understand what parts of this tutorial represent stuff you should type/paste and what is output from those commands. Here is how that works.</p><p>When we want you to run a command, the preceding text will tell you something like "run this command", and then you'll see a codeblock that has the label "bash" in the upper right corner, like so:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">ls</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> -l</span></span></code></pre></div><p>If you hover over it, an icon with the tooltip "Copy Code" will appear on the right, and you can click that to copy the command-line invocation. Or, you can select it and copy it, or you can type it in manually.</p><p>In any case, you are expected to type/paste/execute the entire thing. Other parts of this documentation site may precede command lines with <code>></code> to indicate it's a shell command. For this tutorial, we aren't doing that.</p><p>Sometimes, commands are long. They can be split up by entering a backslash (<code>\\</code>) as the last character of a line, hitting return, and continuing the command. For example this command:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">git</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> push</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> origin</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> main</span></span></code></pre></div><p>Could be executed like so:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">git</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> push</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> \\</span></span>
|
2
2
|
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> origin</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> \\</span></span>
|
3
3
|
<span class="line"><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> main</span></span></code></pre></div><p>In both cases, you can copy/type these as written and they will work.</p><p>To show output of a command, a separate code block will be used, and the first line of the output will be the string <code># OUTPUT:</code>, and there should <strong>not</strong> be a "bash" label in the upper right corner:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
|
4
4
|
<span class="line"><span>app dx puma.config.rb</span></span>
|
@@ -24,4 +24,4 @@ import{_ as a,c as t,o as i,ag as e}from"./chunks/framework.1L-BeKqY.js";const k
|
|
24
24
|
<span class="line highlighted"><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;"> span </span><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;">do</span></span>
|
25
25
|
<span class="line highlighted"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;"> inline_svg</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">(</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;">"edit_icon"</span><span style="--shiki-light:#24292E;--shiki-dark:#E1E4E8;">)</span></span>
|
26
26
|
<span class="line highlighted"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span>
|
27
|
-
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br></div></div><p>This says to find the line that looks like the first one (preceded with a <code>-</code> and shown in red) and replace it with the second one (preceded with a <code>+</code> and shown in green). <strong>Do not use the <code>+</code> or <code>-</code> in your code</strong>, that is just to indicate which line is which.</p><p>Lastly, we'll try to mention the path to the file either in the preceding text or as a comment in the code.</p><h2 id="tutorials-1" tabindex="-1">Tutorials <a class="header-anchor" href="#tutorials-1" aria-label="Permalink to "Tutorials""></a></h2><p>These go mostly in order, each building on the last, but you can start anywhere by using the tutorial on GitHub. The only one that starts from nothing is the first one.</p><table tabindex="0"><thead><tr><th>Index</th><th>Title</th><th>Tutorial</th><th>Screencast</th></tr></thead><tbody><tr><td>1</td><td>Build a Blog in 15 Minutes</td><td><a href="./tutorials/01-intro.html">Tutorial</a></td><td><a href="https://video.hardlimit.com/w/ae7EMhwjDq9kSH5dqQ9swV" target="_blank" rel="noreferrer">Screencast</a></td></tr><tr><td>2</td><td>Adding a Styled Confirmation Dialog</td><td><a href="./tutorials/02-dialog.html">Tutorial</a></td><td><a href="https://video.hardlimit.com/w/4y8Pjd8VVPDK372mozCUdj" target="_blank" rel="noreferrer">Screencast</a></td></tr><tr><td>3</td><td>Leveraging Externalizable IDs (coming soon)</td><td></td><td></td></tr><tr><td>4</td><td>Form Basics (coming soon)</td><td></td><td></td></tr><tr><td>5</td><td>Advanced Forms (coming soon)</td><td></td><td></td></tr><tr><td>6</td><td>AJax Form Submissions (coming soon)</td><td></td><td></td></tr><tr><td>7</td><td>Authentication (coming soon)</td><td></td><td></td></tr><tr><td>8</td><td>Background Jobs with Sidekiq (coming soon)</td><td></td><td></td></tr><tr><td>9</td><td>How to Leverage BrutJS and Custom Elements (coming soon)</td><td></td><td></td></tr></tbody></table>`,40)]))}const u=a(n,[["render",l]]);export{k as __pageData,u as default};
|
27
|
+
<span class="line"><span style="--shiki-light:#D73A49;--shiki-dark:#F97583;"> end</span></span></code></pre><div class="line-numbers-wrapper" aria-hidden="true"><span class="line-number">12</span><br><span class="line-number">13</span><br><span class="line-number">14</span><br><span class="line-number">15</span><br><span class="line-number">16</span><br><span class="line-number">17</span><br></div></div><p>This says to find the line that looks like the first one (preceded with a <code>-</code> and shown in red) and replace it with the second one (preceded with a <code>+</code> and shown in green). <strong>Do not use the <code>+</code> or <code>-</code> in your code</strong>, that is just to indicate which line is which.</p><p>Lastly, we'll try to mention the path to the file either in the preceding text or as a comment in the code.</p><h2 id="tutorials-1" tabindex="-1">Tutorials <a class="header-anchor" href="#tutorials-1" aria-label="Permalink to "Tutorials""></a></h2><p>These go mostly in order, each building on the last, but you can start anywhere by using the tutorial on GitHub. The only one that starts from nothing is the first one.</p><table tabindex="0"><thead><tr><th>Index</th><th>Title</th><th>Tutorial</th><th>Screencast</th></tr></thead><tbody><tr><td>1</td><td>Build a Blog in 15 Minutes</td><td><a href="./tutorials/01-intro.html">Tutorial</a></td><td><a href="https://video.hardlimit.com/w/ae7EMhwjDq9kSH5dqQ9swV" target="_blank" rel="noreferrer">Screencast</a></td></tr><tr><td>2</td><td>Adding a Styled Confirmation Dialog</td><td><a href="./tutorials/02-dialog.html">Tutorial</a></td><td><a href="https://video.hardlimit.com/w/4y8Pjd8VVPDK372mozCUdj" target="_blank" rel="noreferrer">Screencast</a></td></tr><tr><td>3</td><td>Leveraging Externalizable IDs (coming soon)</td><td></td><td></td></tr><tr><td>4</td><td>Form Basics (coming soon)</td><td></td><td></td></tr><tr><td>5</td><td>Advanced Forms (coming soon)</td><td></td><td></td></tr><tr><td>6</td><td>AJax Form Submissions (coming soon)</td><td></td><td></td></tr><tr><td>7</td><td>Authentication (coming soon)</td><td></td><td></td></tr><tr><td>8</td><td>Background Jobs with Sidekiq (coming soon)</td><td></td><td></td></tr><tr><td>9</td><td>How to Leverage BrutJS and Custom Elements (coming soon)</td><td></td><td></td></tr></tbody></table>`,40)])])}const u=a(n,[["render",l]]);export{k as __pageData,u as default};
|
@@ -0,0 +1 @@
|
|
1
|
+
import{_ as a,c as t,o as i,ag as e}from"./chunks/framework.C4nOkCZI.js";const k=JSON.parse('{"title":"Tutorials","description":"","frontmatter":{},"headers":[],"relativePath":"tutorial.md","filePath":"tutorial.md"}'),n={name:"tutorial.md"};function l(o,s,h,p,d,r){return i(),t("div",null,[...s[0]||(s[0]=[e("",40)])])}const u=a(n,[["render",l]]);export{k as __pageData,u as default};
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.
|
1
|
+
import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.C4nOkCZI.js";const t="/assets/welcome-to-brut.VSWzl17-.png",p="/assets/initial-home-page.DNIaYmgP.png",l="/assets/styled-home-page.BzdI7dWz.png",h="/assets/basic-form.DbHnu0oW.png",o="/assets/basic-form-with-violations.Cv6Y9-Q_.png",k="/assets/styled-form-with-violations.Bv_sa9tg.png",d="/assets/styled-form-with-server-side-violations.Bjxd8Dpv.png",r="/assets/styled-home-page-with-posts.Dd4kG89D.png",c="/assets/new-post-editor.DrHr-5oh.png",g="/assets/new-post-home-page.Bm34lyMg.png",B=JSON.parse('{"title":"Build a Blog in 15 Minutes","description":"","frontmatter":{},"headers":[],"relativePath":"tutorials/01-intro.md","filePath":"tutorials/01-intro.md"}'),E={name:"tutorials/01-intro.md"};function u(y,s,F,b,m,C){return n(),a("div",null,[...s[0]||(s[0]=[e(`<h1 id="build-a-blog-in-15-minutes" tabindex="-1">Build a Blog in 15 Minutes <a class="header-anchor" href="#build-a-blog-in-15-minutes" aria-label="Permalink to "Build a Blog in 15 Minutes""></a></h1><p>This will start from nothing and show you the main features of Brut by building a very basic blog. You'll learn how to make a new Brut app, how to build pages, submit forms, validate data, and access data in a database. You'll also learn how to test it all.</p><h2 id="set-up" tabindex="-1">Set Up <a class="header-anchor" href="#set-up" aria-label="Permalink to "Set Up""></a></h2><p>The only two pieces of software you need are Docker and a code editor:</p><ol><li><p><a href="https://docker.com" target="_blank" rel="noreferrer">Install Docker</a></p><div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p><p>If you are on Windows, we <em>highly</em> recommend you use the Windows Subystem for Linux (WSL2), as this makes Brut, web developement, and, honestly, your entire life as you know it, far easier than trying to get things working natively in Windows.</p></div></li><li><p>If you are new to programming or new to Ruby and don't know what editor to get, use VSCode. If you are a vim or emacs person, those will be far better, but if you are used to an IDE, VSCode will be the easiest to get set up and learn to use.</p></li></ol><p>To check that docker is installed, open up a terminal and run:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">docker</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> info</span></span></code></pre></div><p>This should produce a ton of output:</p><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
|
2
2
|
<span class="line"><span>Client:</span></span>
|
3
3
|
<span class="line"><span> Version: 28.2.2</span></span>
|
4
4
|
<span class="line"><span>«LOTS OF OUTPUT»</span></span></code></pre></div><p>To be extra sure, <strong>right after you ran <code>docker info</code></strong>, check <code>$?</code>, the exit code, to make sure it's a 0, which means the command ran successfully:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">echo</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> $?</span></span></code></pre></div><div class="language- vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang"></span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span># OUTPUT</span></span>
|
@@ -705,4 +705,4 @@ import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.1L-BeKqY.js";const t
|
|
705
705
|
<span class="line"><span>No vulnerabilities found</span></span>
|
706
706
|
<span class="line"><span>[ bin/ci ] Checking to see that all classes have tests</span></span>
|
707
707
|
<span class="line"><span>[ bin/test ] All tests exists!</span></span>
|
708
|
-
<span class="line"><span>[ bin/ci ] Done</span></span></code></pre></div><p>That's it!</p><h2 id="areas-for-self-exploration" tabindex="-1">Areas for Self-Exploration <a class="header-anchor" href="#areas-for-self-exploration" aria-label="Permalink to "Areas for Self-Exploration""></a></h2><p>Here are a few enhancement you can try to make:</p><ul><li>Create a client-side constraint requiring the title to match a certain regexp.</li><li>Add a server-side constraint requiring at least two paragraphs.</li><li>Allow editing the blog post creation date</li><li>Add an author field to allow entering the author's name</li><li>Add pagination to the home page</li></ul>`,330)]))}const f=i(E,[["render",u]]);export{B as __pageData,f as default};
|
708
|
+
<span class="line"><span>[ bin/ci ] Done</span></span></code></pre></div><p>That's it!</p><h2 id="areas-for-self-exploration" tabindex="-1">Areas for Self-Exploration <a class="header-anchor" href="#areas-for-self-exploration" aria-label="Permalink to "Areas for Self-Exploration""></a></h2><p>Here are a few enhancement you can try to make:</p><ul><li>Create a client-side constraint requiring the title to match a certain regexp.</li><li>Add a server-side constraint requiring at least two paragraphs.</li><li>Allow editing the blog post creation date</li><li>Add an author field to allow entering the author's name</li><li>Add pagination to the home page</li></ul>`,330)])])}const f=i(E,[["render",u]]);export{B as __pageData,f as default};
|
data/docs/assets/{tutorials_01-intro.md.B4sUBY3X.lean.js → tutorials_01-intro.md.CzZ3kpF_.lean.js}
RENAMED
@@ -1 +1 @@
|
|
1
|
-
import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.
|
1
|
+
import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.C4nOkCZI.js";const t="/assets/welcome-to-brut.VSWzl17-.png",p="/assets/initial-home-page.DNIaYmgP.png",l="/assets/styled-home-page.BzdI7dWz.png",h="/assets/basic-form.DbHnu0oW.png",o="/assets/basic-form-with-violations.Cv6Y9-Q_.png",k="/assets/styled-form-with-violations.Bv_sa9tg.png",d="/assets/styled-form-with-server-side-violations.Bjxd8Dpv.png",r="/assets/styled-home-page-with-posts.Dd4kG89D.png",c="/assets/new-post-editor.DrHr-5oh.png",g="/assets/new-post-home-page.Bm34lyMg.png",B=JSON.parse('{"title":"Build a Blog in 15 Minutes","description":"","frontmatter":{},"headers":[],"relativePath":"tutorials/01-intro.md","filePath":"tutorials/01-intro.md"}'),E={name:"tutorials/01-intro.md"};function u(y,s,F,b,m,C){return n(),a("div",null,[...s[0]||(s[0]=[e("",330)])])}const f=i(E,[["render",u]]);export{B as __pageData,f as default};
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.
|
1
|
+
import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.C4nOkCZI.js";const t="/assets/02-confirmation-flow.D9gZ0S5U.png",l="/assets/02-confirmation-dialog-browser.DH8ALFO4.png",p="/assets/02-confirmation-dialog-browser-element.DPsf0xUW.png",h="/assets/02-confirmation-dialog-browser-element-styled.3NEGM20-.png",u=JSON.parse('{"title":"Tutorial: Styled Confirmation Dialog","description":"","frontmatter":{},"headers":[],"relativePath":"tutorials/02-dialog.md","filePath":"tutorials/02-dialog.md"}'),r={name:"tutorials/02-dialog.md"};function o(k,s,d,c,g,E){return n(),a("div",null,[...s[0]||(s[0]=[e(`<h1 id="tutorial-styled-confirmation-dialog" tabindex="-1">Tutorial: Styled Confirmation Dialog <a class="header-anchor" href="#tutorial-styled-confirmation-dialog" aria-label="Permalink to "Tutorial: Styled Confirmation Dialog""></a></h1><p>For actions that can't be undone, it's customary to confirm with the visitor that they are sure they want to take that action. Brut provides support for this. You can use <code>window.confirm</code> or create your own styled <code><dialog></code> that Brut will use. Both approaches don't require writing any JavaScript yourself.</p><p><a href="https://video.hardlimit.com/w/4y8Pjd8VVPDK372mozCUdj" target="_blank" rel="noreferrer">You can watching this as a screencast instead</a>.</p><h2 id="set-up" tabindex="-1">Set Up <a class="header-anchor" href="#set-up" aria-label="Permalink to "Set Up""></a></h2><p>If you haven't followed the <a href="/tutorials/01-intro.html">initial tutorial</a>, you'll need to pull down the blog app so you have a place to work.</p><ol><li><p><a href="https://docker.com" target="_blank" rel="noreferrer">Install Docker</a></p><div class="tip custom-block github-alert"><p class="custom-block-title">TIP</p><p>If you are on Windows, we <em>highly</em> recommend you use the Windows Subystem for Linux (WSL2), as this makes Brut, web developement, and, honestly, your entire life as you know it, far easier than trying to get things working natively in Windows.</p></div></li><li><p>Clone the <code>blog-demo</code> repo (<strong>don't use Codespaces as it is not supported</strong>):</p><div class="vp-code-group vp-adaptive-theme"><div class="tabs"><input type="radio" name="group-Nc_py" id="tab-oGGQEg7" checked><label data-title="Terminal" for="tab-oGGQEg7">Terminal</label><input type="radio" name="group-Nc_py" id="tab-gs-HkeB"><label data-title="GitHub CLI" for="tab-gs-HkeB">GitHub CLI</label></div><div class="blocks"><div class="language-bash vp-adaptive-theme active"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">git</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> clone</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> git@github.com:thirdtank/blog-demo.git</span></span></code></pre></div><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">gh</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> repo</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> clone</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> thirdtank/blog-demo</span></span></code></pre></div></div></div></li><li><p><code>cd</code> to what you just cloned.</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;">cd</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> blog-demo</span></span></code></pre></div></li><li><p>Create a branch named <code>confirmation-dialog</code> off of the <code>02-confirmation-dialog/start</code> branch:</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">git</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> checkout</span><span style="--shiki-light:#005CC5;--shiki-dark:#79B8FF;"> -b</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> confirmation-dialog</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> 02-confirmation-dialog/start</span></span></code></pre></div></li><li><p>Build your development image.</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">dx/build</span></span></code></pre></div></li><li><p>Start the environment, which will pull down Postgres and otel-desktop-viewer</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">dx/start</span></span></code></pre></div></li><li><p>In another terminal window, "log in" to your dev environment (note that you can use your editor on your computer to edit code)</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">dx/exec</span><span style="--shiki-light:#032F62;--shiki-dark:#9ECBFF;"> bash</span></span></code></pre></div></li><li><p>Set up and run tests to make sure things are working before you start making changes. Note, this is <strong>inside the container</strong>, not directly on your computer.</p><div class="language-bash vp-adaptive-theme"><button title="Copy Code" class="copy"></button><span class="lang">bash</span><pre class="shiki shiki-themes github-light github-dark vp-code" tabindex="0"><code><span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/setup</span></span>
|
2
2
|
<span class="line"><span style="--shiki-light:#6F42C1;--shiki-dark:#B392F0;">bin/ci</span></span></code></pre></div></li></ol><h2 id="what-we-re-doing" tabindex="-1">What We're Doing <a class="header-anchor" href="#what-we-re-doing" aria-label="Permalink to "What We're Doing""></a></h2><p>When writing a blog post, if the title and content satisfy all constraints, the post is saved and shown on the home page. Because this can't currently be undone, we want the user to confirm the posting, just to avoid any accidents.</p><p>Initially, we will use <code>window.confirm</code> to do this. After that, we'll create a nicely styled dialog to do the confirmation. While this will require that the browser execute JavaScript, we won't be writing any. We'll use Brut-provided Web Components to do this.</p><p><img src="`+t+`" alt="Diagram showing the flow, with a screenshot of the blog post editor on the left, and a pink arrow from
|
3
3
|
the 'Post it' button going to the text 'Are You Sure?'. From there, a pink line labeled 'No' goes back
|
4
4
|
to the editor, while a pink line labeled 'Yes' goes to a screenshot of the home page showing the blog
|
@@ -271,4 +271,4 @@ post."></p><h2 id="initial-version-using-window-confirm" tabindex="-1">Initial V
|
|
271
271
|
<span class="line"><span>[ bin/db ] Database exists. Dropping...</span></span>
|
272
272
|
<span class="line"><span>[ bin/db ] blog_test does not exit. Creating...</span></span>
|
273
273
|
<span class="line"><span>[ bin/db ] Migrations applied</span></span>
|
274
|
-
<span class="line"><span>[ bin/test ] ["bin/db rebuild --env=test"] succeeded</span></span></code></pre></div><h2 id="areas-for-self-exploration" tabindex="-1">Areas for Self-Exploration <a class="header-anchor" href="#areas-for-self-exploration" aria-label="Permalink to "Areas for Self-Exploration""></a></h2><ul><li>Extract the dialog into its own component</li><li>Use Internationalization for all the dialog values</li></ul>`,79)]))}const y=i(r,[["render",o]]);export{
|
274
|
+
<span class="line"><span>[ bin/test ] ["bin/db rebuild --env=test"] succeeded</span></span></code></pre></div><h2 id="areas-for-self-exploration" tabindex="-1">Areas for Self-Exploration <a class="header-anchor" href="#areas-for-self-exploration" aria-label="Permalink to "Areas for Self-Exploration""></a></h2><ul><li>Extract the dialog into its own component</li><li>Use Internationalization for all the dialog values</li></ul>`,79)])])}const y=i(r,[["render",o]]);export{u as __pageData,y as default};
|
data/docs/assets/{tutorials_02-dialog.md.CPNK1SC_.lean.js → tutorials_02-dialog.md.De6iTsWX.lean.js}
RENAMED
@@ -1 +1 @@
|
|
1
|
-
import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.
|
1
|
+
import{_ as i,c as a,o as n,ag as e}from"./chunks/framework.C4nOkCZI.js";const t="/assets/02-confirmation-flow.D9gZ0S5U.png",l="/assets/02-confirmation-dialog-browser.DH8ALFO4.png",p="/assets/02-confirmation-dialog-browser-element.DPsf0xUW.png",h="/assets/02-confirmation-dialog-browser-element-styled.3NEGM20-.png",u=JSON.parse('{"title":"Tutorial: Styled Confirmation Dialog","description":"","frontmatter":{},"headers":[],"relativePath":"tutorials/02-dialog.md","filePath":"tutorials/02-dialog.md"}'),r={name:"tutorials/02-dialog.md"};function o(k,s,d,c,g,E){return n(),a("div",null,[...s[0]||(s[0]=[e("",79)])])}const y=i(r,[["render",o]]);export{u as __pageData,y as default};
|