brut 0.0.20 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/lib/brut/back_end/seed_data.rb +19 -2
- data/lib/brut/back_end/sidekiq/middlewares/server.rb +2 -1
- data/lib/brut/back_end/sidekiq/middlewares.rb +2 -1
- data/lib/brut/back_end/sidekiq.rb +2 -1
- data/lib/brut/back_end/validator.rb +5 -1
- data/lib/brut/back_end.rb +4 -2
- data/lib/brut/cli.rb +4 -3
- data/lib/brut/factory_bot.rb +0 -5
- data/lib/brut/framework/app.rb +70 -5
- data/lib/brut/framework/config.rb +5 -3
- data/lib/brut/framework/container.rb +3 -2
- data/lib/brut/framework/errors.rb +12 -4
- data/lib/brut/framework/mcp.rb +62 -1
- data/lib/brut/framework/project_environment.rb +6 -2
- data/lib/brut/framework.rb +1 -1
- data/lib/brut/front_end/component.rb +35 -12
- data/lib/brut/front_end/components/constraint_violations.rb +1 -1
- data/lib/brut/front_end/components/form_tag.rb +1 -1
- data/lib/brut/front_end/components/inputs/csrf_token.rb +1 -1
- data/lib/brut/front_end/components/inputs/text_field.rb +1 -1
- data/lib/brut/front_end/components/time_tag.rb +1 -1
- data/lib/brut/front_end/layout.rb +16 -0
- data/lib/brut/front_end/page.rb +51 -26
- data/lib/brut/front_end/routing.rb +5 -1
- data/lib/brut/front_end.rb +4 -13
- data/lib/brut/i18n/base_methods.rb +37 -3
- data/lib/brut/i18n/for_back_end.rb +3 -0
- data/lib/brut/i18n/for_cli.rb +3 -0
- data/lib/brut/i18n/http_accept_language.rb +47 -0
- data/lib/brut/instrumentation/open_telemetry.rb +25 -0
- data/lib/brut/instrumentation.rb +3 -5
- data/lib/brut/sinatra_helpers.rb +1 -0
- data/lib/brut/spec_support/component_support.rb +18 -4
- data/lib/brut/spec_support/e2e_support.rb +1 -1
- data/lib/brut/spec_support/general_support.rb +3 -0
- data/lib/brut/spec_support/handler_support.rb +6 -1
- data/lib/brut/spec_support/matcher.rb +1 -0
- data/lib/brut/spec_support/matchers/be_page_for.rb +1 -0
- data/lib/brut/spec_support/matchers/have_html_attribute.rb +1 -0
- data/lib/brut/spec_support/matchers/have_i18n_string.rb +2 -5
- data/lib/brut/spec_support/matchers/have_link_to.rb +1 -0
- data/lib/brut/spec_support/matchers/have_redirected_to.rb +1 -0
- data/lib/brut/spec_support/matchers/have_rendered.rb +1 -0
- data/lib/brut/spec_support/matchers/have_returned_rack_response.rb +44 -0
- data/lib/brut/spec_support.rb +1 -1
- data/lib/brut/version.rb +1 -1
- data/lib/brut.rb +5 -4
- metadata +2 -9
- data/doc-src/architecture.md +0 -102
- data/doc-src/assets.md +0 -98
- data/doc-src/forms.md +0 -214
- data/doc-src/handlers.md +0 -83
- data/doc-src/javascript.md +0 -265
- data/doc-src/keyword-injection.md +0 -183
- data/doc-src/pages.md +0 -210
- data/doc-src/route-hooks.md +0 -59
data/doc-src/architecture.md
DELETED
@@ -1,102 +0,0 @@
|
|
1
|
-
# Brut's High Level Architecture
|
2
|
-
|
3
|
-
Brut attempts to have parity with the web platform in general, and how web browsers work. For example, a web browser shows you a web
|
4
|
-
page, thus to do this in Brut, you create a page class.
|
5
|
-
|
6
|
-
Brut's core ethos is to captialize on fundamental knowledge you must already posses to build a web app, such as HTML, JavaScript, the
|
7
|
-
Web Platform, SQL, etc.
|
8
|
-
|
9
|
-
The best way to understand Brut is to break down how a request is handled.
|
10
|
-
|
11
|
-
## HTTP Requests at a High Level
|
12
|
-
|
13
|
-
1. HTTP Request is received
|
14
|
-
2. If the request's route is mapped, handle it and return the result
|
15
|
-
3. Otherwise, 404
|
16
|
-
|
17
|
-
This pretty much describes every web app server in the world. Let's dig into step 2
|
18
|
-
|
19
|
-
## Pages, Forms, Actions
|
20
|
-
|
21
|
-
HTML allows for exactly two ways to interact with a server: Issuing a `GET` for a resource (like a web page), or submitting a form via
|
22
|
-
a `GET` or `POST`.
|
23
|
-
|
24
|
-
1. HTTP Request is received
|
25
|
-
2. If the route is a mapped page, render that page
|
26
|
-
3. If the route is a configured form, submit that form handle it
|
27
|
-
4. If the route is an asset like CSS or an image, send that.
|
28
|
-
5. Otherwise, 404
|
29
|
-
|
30
|
-
A browser can use JavaScript to submit other requests, and Brut handles those, too:
|
31
|
-
|
32
|
-
1. HTTP Request is received
|
33
|
-
2. If the route is a mapped page, render that page (See {file:doc-src/pages.md})
|
34
|
-
3. If the route is a configured form, submit that form handle it (See {file:doc-src/forms.md})
|
35
|
-
4. If the route is a configured action, perform it and return the results. (See {file:doc-src/handlers.md})
|
36
|
-
5. If the route is an asset like CSS or an image, send that. (See {file:doc-src/assets.md})
|
37
|
-
6. Otherwise, 404
|
38
|
-
|
39
|
-
Before we dig deeper, it's worth pointing out at this point that *Brut is not an MVC framework*, mostly because there are no
|
40
|
-
controllers. *Models* in the MVC sense are instead *pages* or *components*—classes that provide all dynamic behavior and data needed
|
41
|
-
to render some HTML. Brut uses ERB templates to generate HTML and you could think of this as the view, if you want.
|
42
|
-
|
43
|
-
## Back End
|
44
|
-
|
45
|
-
Most of Brut is concerned with what it calls the *front end*, which is everything about receiving an HTTP request and producing the
|
46
|
-
reponse. But you can almost never make a web app with no back end. You almost always need a database and a place to execute logic
|
47
|
-
outside of a web browser. Brut refers to this as the *back end*.
|
48
|
-
|
49
|
-
Since web back ends are less constrained to protocols, like a front end is to HTTP and other Web APIs, Brut provides a lot less for
|
50
|
-
the back end and puts many fewer restrictions on it. This ends up being a good thing, since the back-end of most web apps are were
|
51
|
-
most of the differentiation in behavior and logic tend to be.
|
52
|
-
|
53
|
-
Brut provides access to a SQL database via the Sequel Ruby library. Brut provides some integration with Sidekiq, to allow running
|
54
|
-
background jobs. Brut also provides a CLI library for creating one-off tasks (like you'd use Rake for in a Rail apps).
|
55
|
-
|
56
|
-
### SQL
|
57
|
-
|
58
|
-
Almost all web apps that have a database use SQL. And Postgres is a great choice. This is the SQl database that Brut supports,
|
59
|
-
though support for other databases may be added later. This is because almost all of Brut's SQL integration is via the Sequel library
|
60
|
-
and it supports many databases.
|
61
|
-
|
62
|
-
Brut provides some configuration for Sequel to make managing your data easier and to better-support practices that you often want
|
63
|
-
to follow. For example:
|
64
|
-
|
65
|
-
* All tables have a primary key of type `int` by default.
|
66
|
-
* All tables have a `created_at` field that is set on row insertion.
|
67
|
-
* Timestamps use `timestamp with time zone`.
|
68
|
-
* You must provide a comment to document all tables.
|
69
|
-
* You can have Brut manage an external unique key for each table, so you can keep your primary keys private.
|
70
|
-
* `find!` is available on Sequel models, working like `find` does in Rails.
|
71
|
-
|
72
|
-
See {file:doc-src/sql.md} for more details.
|
73
|
-
|
74
|
-
Brut also uses Sequel's model support to provide access to your database. It is configured to namespace all models in the `DB::`
|
75
|
-
namespace, so if you have a table named `widgets`, Brut will expect `DB::Widget` to be defined to access that table.
|
76
|
-
|
77
|
-
The reason for this is that it is often confusing when an app conflates the concept of a database table and a domain model, it can be
|
78
|
-
difficult to manage the code. If the model to access the `widgets` table were called, simply, `Widget`, then you would lose a great
|
79
|
-
class name to use for modeling the widget as a domain object.
|
80
|
-
|
81
|
-
### Sidekiq
|
82
|
-
|
83
|
-
Sidekiq can be added to any app without much fanfare. That said, Brut provides a few convieniences:
|
84
|
-
|
85
|
-
* `bin/run-sidekiq` is provided to run Sidekiq alongside your web app
|
86
|
-
* {Brut::SpecSupport::RSpecSetup} well arrange for Sidekiq to be set up in a useful way during tests:
|
87
|
-
- For non-E2E tests, jobs are cleared between test runs.
|
88
|
-
- During E2E tests, actual Sidekiq is used, as started by `/bin/run-sidekiq`, and jobs are cleared between tests.
|
89
|
-
|
90
|
-
### CLI / Tasks
|
91
|
-
|
92
|
-
Rake is not a great tool for task automation, mostly because it exposes a cumbersome command line interface that relies on environment
|
93
|
-
variables, square brackets, and commas. It's often easier to create a full-blown command line app.
|
94
|
-
|
95
|
-
Brut's {Brut::CLI::App} can form the basis for any command line app or task you need for your system. It provides access to Brut
|
96
|
-
internals and your app, as needed, and shares much of its startup code with your web app, ensuring parity for all code shared.
|
97
|
-
|
98
|
-
Brut's CLI support also allows for an expedient definition of a subcommand-style UI that behaves like a canonical UNIX command line
|
99
|
-
app, without having to write a lot of code. It wraps `OptionParser`, so if you are familiar with this library that's part of Ruby,
|
100
|
-
you will be familiar with Brut's CLI API.
|
101
|
-
|
102
|
-
See {file:doc-src/cli.md}.
|
data/doc-src/assets.md
DELETED
@@ -1,98 +0,0 @@
|
|
1
|
-
# Assets - CSS, JavaScript, Images
|
2
|
-
|
3
|
-
To learn about Brut's JavaScript API support, see {file:doc-src/javascript.md}. This page is about how requests for assets are
|
4
|
-
managed.
|
5
|
-
|
6
|
-
At a high level, all assets are served out of the *public folder*, which is in `app/public`. Brut copies files into this folder as
|
7
|
-
part of the build and development process.
|
8
|
-
|
9
|
-
Currently, Brut supports JavaScript, CSS, Fonts, and Images. These are all copied and/or processed from a source location into
|
10
|
-
`app/public` via {Brut::CLI::Apps::BuildAssets}:
|
11
|
-
|
12
|
-
* JavaScript - From `app/src/front_end/js`, bundled to `app/public/js`.
|
13
|
-
* CSS - From `app/src/front_end/css`, bundled to `app/public/css`.
|
14
|
-
* Fonts - From `app/src/front_end/fonts`, bundled to `app/public/css` (yes, they are bundled to `css` as that is the only reason to have a built step for fonts - see below).
|
15
|
-
* Images - From `app/src/front_end/iamges` to `app/public/images`.
|
16
|
-
|
17
|
-
## Images
|
18
|
-
|
19
|
-
Images are the simplest. Images in Brut are not hashed, so they are essentially synced from `app/src/front_end/images` to
|
20
|
-
`app/public/images`. Your CDN should arrange for cache invalidation.
|
21
|
-
|
22
|
-
## SVGs
|
23
|
-
|
24
|
-
SVGs are treated specially. They are located in `app/src/front_end/svgs`. To use an svg, your ERB should use the
|
25
|
-
{Brut::FrontEnd::Component::Helpers#svg} to inline the SVG into the page. You can put SVGs intended to be linked-to in
|
26
|
-
`app/src/front_end/images`, but for SVGs to be used as icons, for example, place them in `app/src/front_end/svgs` and use the `svg`
|
27
|
-
helper.
|
28
|
-
|
29
|
-
## JavaScript
|
30
|
-
|
31
|
-
JavaScript is currently managed by ESBuild. No fancy options are used nor currently possible. By default, there is a single entry
|
32
|
-
point for all your JavaScript, located in `app/src/front_end/javascript/index.js`. This is compiled into `app/public/js/app-«HASH».js`, for example `app/public/js/app-EAALH2IQ.js`. A sourcemap is included. Third party JS can be referenced and is assumed to be in `node_modules`.
|
33
|
-
|
34
|
-
This should be sufficient for most apps, however you can use additional entry points. See {file:doc-src/javascript.md} for how to set
|
35
|
-
this up. Also see "Hashing" below for how hashing works and is managed.
|
36
|
-
|
37
|
-
## CSS
|
38
|
-
|
39
|
-
CSS is also managed by ESBuild. There is a single entry point located in `app/src/front_end/css/index.css`, and this is compiled into
|
40
|
-
`app/public/css/styles-«HASH».css`, for example `app/public/css/styles-EAALH2IQ.css`. A sourcemap is included. Third party CSS can be
|
41
|
-
referenced and is assumed to be in `node_modules`.
|
42
|
-
|
43
|
-
Currently, there is no support for multiple CSS entry points - your entire app's CSS is expected to be in (or referenced by)
|
44
|
-
`index.css`.
|
45
|
-
|
46
|
-
To do that, Brut assumes you will use standard APIs, namely `@import`, and this is how you can bring in third party CSS as well as to
|
47
|
-
manage your app's CSS in multiple files:
|
48
|
-
|
49
|
-
@import "bootstrap/index.css";
|
50
|
-
@import "colors.css";
|
51
|
-
|
52
|
-
html { font-size: 20px; }
|
53
|
-
|
54
|
-
## Fonts
|
55
|
-
|
56
|
-
ESBuild will handle fonts when CSS is built. Fonts are hashed. You should place fonts in `app/src/front_end/fonts`, however this is
|
57
|
-
merely a convention. ESBuild will find your font as long as you properly use `url(...)` to reference it.
|
58
|
-
|
59
|
-
To follow the convention, here is how you might write your CSS:
|
60
|
-
|
61
|
-
/* index.css */
|
62
|
-
@font-face {
|
63
|
-
font-family: "Monaspace Xenon";
|
64
|
-
src: url("../fonts/monaspace-xenon.ttf") format("truetype");
|
65
|
-
font-display: swap;
|
66
|
-
}
|
67
|
-
|
68
|
-
ESBuild treats the relative path in `url` as relative to where the file being procssed is, thus it will expect to find
|
69
|
-
`app/src/front_end/fonts/monaspace-xenon.ttf`. While it's not relevant to you where it's copied, the file will be hashed and copied
|
70
|
-
to `app/public/css` and the `url(..)` will be adjusted, for example:
|
71
|
-
|
72
|
-
@font-face {
|
73
|
-
font-family: "Monaspace Xenon";
|
74
|
-
src: url("./monaspace-xenon-VZ5IIHXZ.ttf") format("truetype");
|
75
|
-
font-display: swap;
|
76
|
-
}
|
77
|
-
|
78
|
-
## Hashing
|
79
|
-
|
80
|
-
Hashing is on in development, testing, and production, as a way to minimize differences between the three environments. The way Brut
|
81
|
-
manages this is via the file `app/config/asset_metadata.json`. This file maps the logical name of an asset to its hashed name. For
|
82
|
-
example:
|
83
|
-
|
84
|
-
{
|
85
|
-
"asset_metadata": {
|
86
|
-
".js": {
|
87
|
-
"/js/app.js": "/js/app-L6VPFHLG.js"
|
88
|
-
},
|
89
|
-
".css": {
|
90
|
-
"/css/styles.css": "/css/styles-PHUHEJY3.css"
|
91
|
-
}
|
92
|
-
}
|
93
|
-
}
|
94
|
-
|
95
|
-
{Brut::FrontEnd::Component::Helpers#asset_path} accepts the logical name (`/js/app.js`) and returns the actual name
|
96
|
-
`/js/app-L6VPFHLG.js`). `asset_metadata.json` is managed by `bin/build-assets`.
|
97
|
-
|
98
|
-
Note that the fonts are not present in this file since they are only needed by CSS, and ESBuild handles the translation there.
|
data/doc-src/forms.md
DELETED
@@ -1,214 +0,0 @@
|
|
1
|
-
# Forms
|
2
|
-
|
3
|
-
Brut's form support is designed to have parity with the Web Platform and allow you to both generate HTML and pasrse the results of a
|
4
|
-
form submission from a single source of truth. Brut's forms also allow the unification of client-side and server-side constraint
|
5
|
-
violations so that you can provide client-side validations but not require JavaScript for all validations.
|
6
|
-
|
7
|
-
## High Level Overview of Forms
|
8
|
-
|
9
|
-
The purpose of a form is to allow the website visitor to submit data to you. The Web Platform and web browser have deep support for
|
10
|
-
this, but the core way this works is that you have a `<form>` element that contains form elements like `<input>` or `<select>` elements and, when this form is submitted, your server can access the values of each element and take whatever action is necessary.
|
11
|
-
|
12
|
-
The lifecycle of this process looks like so:
|
13
|
-
|
14
|
-
1. HTML is generated, including a form with elements based on a definition you provide.
|
15
|
-
2. The visitor submits the form.
|
16
|
-
3. If there are constraint violations that the browser can detect, the form will not be submitted to your server.
|
17
|
-
4. If all constraints are satisfied (or the visitor bypasses client-side constraints), the server receives the form submission.
|
18
|
-
5. A {Brut::FrontEnd::Handler} is triggered to process that submission. The handler should re-check the client-side constraints and
|
19
|
-
can perform further server-side checks.
|
20
|
-
6. The handler will decide what the website visitor will experience next (e.g. re-render the form, proceed to another page, etc).
|
21
|
-
|
22
|
-
As a developer, you must write four pieces of code:
|
23
|
-
|
24
|
-
* Call {Brut::SinatraHelpers::ClassMethods.form} to declare the route.
|
25
|
-
* Create a subclass of {Brut::FrontEnd::Form} (whose name is determined by the route name). This class declares the inputs of your
|
26
|
-
form.
|
27
|
-
* Create a subclass of {Brut::FrontEnd::Handler} (whose name is determined by the route name). This class processes the form.
|
28
|
-
* ERB to generate the form. The classes in {Brut::FrontEnd::Components::Inputs} have methods like {Brut::FrontEnd::Components::Inputs::TextField.for_form_input} that will generate HTML for you.
|
29
|
-
|
30
|
-
## Concepts
|
31
|
-
|
32
|
-
Brut tries to create concepts that have a direct analog to the web platform.
|
33
|
-
|
34
|
-
These basic concepts form the basis for Brut's form support:
|
35
|
-
|
36
|
-
* *Form* is an HTML `<form>`
|
37
|
-
* *Input* is an HTML `<input>`, `<select>`, `<textarea>`, etc.
|
38
|
-
* *Input Name* is the name of an input, as defined by the `name` attribute.
|
39
|
-
* *Submitting a form* is when the browser submits a form to the form's action. This is done with an HTTP GET or HTTP POST only. No
|
40
|
-
other HTTP methods can submit a form.
|
41
|
-
* *Constraint Violation* describes invalid data in an input.
|
42
|
-
|
43
|
-
Building on these, Brut specifies how it manages Forms, Inputs, and Submission:
|
44
|
-
|
45
|
-
* *Form Class* defines the inputs a specific form will have.
|
46
|
-
* *Input Definition* defines the input for a given input name.
|
47
|
-
* *Form Object* holds the values and constraint violations of all inputs.
|
48
|
-
* *Handler* is a class that processes a form submission. It's `handle!` method can access the Form Object representing a submission.
|
49
|
-
* *Server-Side Constraint Violation* describes invalid data that required server processing to determine.
|
50
|
-
|
51
|
-
## Basic Workflow for Handling a Form
|
52
|
-
|
53
|
-
### Define Your Form
|
54
|
-
|
55
|
-
In Brut, a *form* class (or *form object*) holds metadata about the form in question. Namely, it defines all of the inputs and any
|
56
|
-
client-side constraints. For example, here is a form to create a new widget, where the name must be at least 3 characters and is
|
57
|
-
required, and there is an optional description:
|
58
|
-
|
59
|
-
class NewWidgetForm < AppForm
|
60
|
-
input :name, minlength: 3
|
61
|
-
input :description, required: false
|
62
|
-
end
|
63
|
-
|
64
|
-
This form class provides a few features:
|
65
|
-
|
66
|
-
* It can be used to generate HTML. For example, the "name" field will generate `<input type="text" name="name" required minlength="3">`
|
67
|
-
* It holds the data submitted to the server, serving as an allowlist of which parameters are accepted. For example, if the browser
|
68
|
-
submits "name", "description", and "price", since "price" is not an input of this form, the server will discard that value. Your code
|
69
|
-
will only have access to "name" and "description"
|
70
|
-
* It can validate the client-side constraints on the server. If a visitor submits the form, bypassing client-side constraint
|
71
|
-
validations, and "name" is only two characters, the form object will see that the "name" field has a violation.
|
72
|
-
|
73
|
-
### Define Your Handler
|
74
|
-
|
75
|
-
Handlers are like controller methods in Rails - they receive the data from a request and process it. Unlike a Rails controller, a
|
76
|
-
Handler is a normal class. It implements the method `handle`. The method signature you use for `handle` determines what data will
|
77
|
-
be passed into it. The return value of `handle` determines what happens after processing is complete.
|
78
|
-
|
79
|
-
Suppose that creating a widget requires that the name be unique. If it's not, we re-render the page containing the form and show the
|
80
|
-
user the errors. Suppose that the page in question is `/new_widget`, which would be the class `NewWidgetPage`.
|
81
|
-
|
82
|
-
class NewWidgetHandler < AppHandler
|
83
|
-
def handle(form:)
|
84
|
-
if !form.constraint_violations?
|
85
|
-
if DB::Widget[name: form.name]
|
86
|
-
form.server_side_constraint_violation(input_name: :name, key: :not_unique)
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
if form.constraint_violations?
|
91
|
-
return NewWidgetPage.new(form:)
|
92
|
-
end
|
93
|
-
|
94
|
-
DB::Widget.create(name: form.name, description: form.description)
|
95
|
-
redirect_to(WidgetsPage)
|
96
|
-
end
|
97
|
-
end
|
98
|
-
|
99
|
-
|
100
|
-
{file:doc-src/pages.md Pages} provides more information about what `NewWidgetPage` and `WidgetsPage` might be or do, but the logic in
|
101
|
-
the handler is, hopefully, clear. If our form is free of client-side constraint violations, we check to see if there is another
|
102
|
-
widget with the name from the form. If there is, we set a server-side constraint violation.
|
103
|
-
|
104
|
-
After that, if the form has any constraint violations (server-side or client-side), we return `NewWidgetPage` initialized with our
|
105
|
-
existing form. This will allow that page to generate HTML that includes information about the constraint violations detected.
|
106
|
-
|
107
|
-
If there aren't constraint violations, we create a widget in the database, then redirect to `WidgetsPage`. {Brut::FrontEnd::HandlingResults#redirect_to} is a convienience method for figuring out the URL for a given page.
|
108
|
-
|
109
|
-
### Generate HTML
|
110
|
-
|
111
|
-
In this example, `NewWidgetPage` would be generating the HTML form. It's class might look like so:
|
112
|
-
|
113
|
-
class NewWidgetPage < AppPage
|
114
|
-
|
115
|
-
attr_reader :form
|
116
|
-
|
117
|
-
def initialize(form: nil)
|
118
|
-
@form ||= NewWidgetForm.new
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
Here is the most direct way to use the form object to render HTML.
|
123
|
-
|
124
|
-
<%= form_tag for: form do %>
|
125
|
-
<%= component(Brut::FrontEnd::Components::TextField.for_form_input(form:, input_name: :name)) %>
|
126
|
-
<%= component(Brut::FrontEnd::Components::Textarea.for_form_input(form:, input_name: :description)) %>
|
127
|
-
<button>Save</button>
|
128
|
-
<% end %>
|
129
|
-
|
130
|
-
`for_form_input` uses the metadata in the form, along with the name of the field, to generate the appropriate HTML. This will only
|
131
|
-
generate an `<input>` tag, so you have complete flexibility to style it however you like.
|
132
|
-
|
133
|
-
You can also use `constraint_violations` to render any errors:
|
134
|
-
|
135
|
-
<%= form_tag for: form do %>
|
136
|
-
|
137
|
-
<%= component(Brut::FrontEnd::Components::TextField.for_form_input(form:, input_name: :name)) %>
|
138
|
-
<%= constraint_violations(form:, input_name: :name) %>
|
139
|
-
|
140
|
-
<%= component(Brut::FrontEnd::Components::Textarea.for_form_input(form:, input_name: :description)) %>
|
141
|
-
<%= constraint_violations(form:, input_name: :description) %>
|
142
|
-
|
143
|
-
<button>Save</button>
|
144
|
-
<% end %>
|
145
|
-
|
146
|
-
## Multiple Inputs with the Same Name
|
147
|
-
|
148
|
-
The HTTP spec allows for any number of inputs with the same name. All values are submitted. Rack, upon which Brut is based, further
|
149
|
-
provides a way to access such duplicate names as an array, using a naming convention.
|
150
|
-
|
151
|
-
Brut forms support this via `array: true` when defining an input:
|
152
|
-
|
153
|
-
class NewWidgetForm < AppForm
|
154
|
-
input :name, minlength: 3, array: true
|
155
|
-
input :description, required: false, array: true
|
156
|
-
end
|
157
|
-
|
158
|
-
When you do this, a call to `for_form_input` will require an index, as will any other method you use to interact with the form, such
|
159
|
-
as `server_side_constraint_violation`. When the HTML is generated, the `name=` of the `<input>` will use Rack's naming convention:
|
160
|
-
|
161
|
-
<input type="text" name="name[]" ...>
|
162
|
-
|
163
|
-
To access the values, you can pass an index to the generated method name, or use the `_each` form:
|
164
|
-
|
165
|
-
def handle!(form:)
|
166
|
-
form.name(1) # get the second value for the 'name' input
|
167
|
-
form.name_each do |value,i|
|
168
|
-
value # is the value of the input, empty string if omitted
|
169
|
-
i # is the zero-based index
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
|
-
When generating HTML, the form object will generate any number of inputs that you request:
|
174
|
-
|
175
|
-
<%= form_tag for: form do %>
|
176
|
-
|
177
|
-
<% 10.times do |index| %>
|
178
|
-
<%= component(Brut::FrontEnd::Components::TextField.for_form_input(form:, input_name: :name, index:)) %>
|
179
|
-
<%= constraint_violations(form:, input_name: :name) %>
|
180
|
-
<% end %>
|
181
|
-
|
182
|
-
<button>Save</button>
|
183
|
-
<% end %>
|
184
|
-
|
185
|
-
## Styling and Client-Side Behavior
|
186
|
-
|
187
|
-
While browsers have long-supported client-side constraint validations, there are a few complications that make them hard to use in
|
188
|
-
practice. Brut provides solutions for most of these issues and allows you to unify your error reporting into a single user experien
|
189
|
-
ce, regardless of where the constraint violation was identified. This does, however, require JavaScript. But, it is entirely opt-in.
|
190
|
-
|
191
|
-
### Issue: Blank Forms Match `:invalid` Pseudo-Class
|
192
|
-
|
193
|
-
If an input is required, it will match the `:invalid` pseudo class if it has no value, even if a user has not interacted with the
|
194
|
-
input. While Brut cannot change this behavior, it *does* allow you to have better control.
|
195
|
-
|
196
|
-
If you surround your `<form>` with the `<brut-form>` custom element, that element will add `data-submitted` to the `<form>` element
|
197
|
-
when submission is attempted. This means that your CSS can target something like `form[data-submitted] input:invalid` so that any
|
198
|
-
styling for constraint violations will only show up if the user has attempted to submit the form.
|
199
|
-
|
200
|
-
### Issue: App-Controled Messaging for Client-Side Constraint Violations
|
201
|
-
|
202
|
-
While it's not currently possible to control the browser's UI around client-side constraint violations, Brut does allow you to provide
|
203
|
-
your own error messages and UX when this happens. This means you can unify your client-side and server-side messaging so it looks the
|
204
|
-
same no matter what.
|
205
|
-
|
206
|
-
When a field is detected to be invalid, `<brut-form>` will locate a `<brut-cv-messages>` custom element and provide it with the
|
207
|
-
`ValidityState` of the input. This will create one `<brut-cv>` custom element for each constraint violation. The `<brut-cv>` custom
|
208
|
-
element will use its `key=` attribute to locate the appropriate `<brut-i18n-translation>` custom element, which your server should've
|
209
|
-
rendered with the appropriate error messages.
|
210
|
-
|
211
|
-
This has the effect of inserting a localized message you control into the DOM wherever you want it for reporting the error to the
|
212
|
-
user.
|
213
|
-
|
214
|
-
|
data/doc-src/handlers.md
DELETED
@@ -1,83 +0,0 @@
|
|
1
|
-
# Handlers
|
2
|
-
|
3
|
-
In Brut, a *handler* responds to an HTTP request that *isn't* inteded to render a web page. Primarily, a handler is used to process a
|
4
|
-
form submission, but a handler can also respond to an Ajax request. A handler is like a controller in Rails, however in Brut, a
|
5
|
-
handler is a class you implement that has a method that receives arguments and a return value that controls the response (unlike controllers in Rails).
|
6
|
-
|
7
|
-
## Defining a Route to be Handled
|
8
|
-
|
9
|
-
There are three ways to define a route that requires a handler, `form`, `action`, and `path`.
|
10
|
-
|
11
|
-
|
12
|
-
class App < Brut::Framework::App
|
13
|
-
|
14
|
-
routes do
|
15
|
-
|
16
|
-
form "/new_widget"
|
17
|
-
action "/archive_widget/:id"
|
18
|
-
path "/payment_received", method: :get
|
19
|
-
end
|
20
|
-
end
|
21
|
-
|
22
|
-
`form` indicates you have a form you are going to render in HTML and process its submission. The use of `form` requires that you have
|
23
|
-
a {Brut::FrontEnd::Form} defined (see {file:doc-src/forms.md}) as well as a handler. The names are conventional based on the route.
|
24
|
-
In the example above, Brut will expect `NewWidgetForm` and `NewWidgetHandler` to exist.
|
25
|
-
|
26
|
-
You can create these with `bin/scaffold`:
|
27
|
-
|
28
|
-
bin/scaffold form NewWidget
|
29
|
-
|
30
|
-
`action` is used when you have a form to submit, but it has no user controls to collect the data to submit. This is akin to Rails'
|
31
|
-
`button_to` where the URL describes everything needed to handle a user's action. In the example above, you can imagine a form with a
|
32
|
-
method to `/archive_widget/<%= widget.id %>` that has a button labeled "Archive Widget". All that's needed is to submit to the
|
33
|
-
server.
|
34
|
-
|
35
|
-
In that case, only a handler is required. The name is again conventional. In this case, `ArchiveWidgetWithId`. You can create this
|
36
|
-
with `bin/scaffold`
|
37
|
-
|
38
|
-
bin/scaffold handler ArchiveWidthWithId
|
39
|
-
|
40
|
-
The last case, `path`, is for arbitray routes. It works the same way as `action`, but requires a `method:` to declare the HTTP
|
41
|
-
method.
|
42
|
-
|
43
|
-
## Implementing a Handler
|
44
|
-
|
45
|
-
Regardless of how you declare a route, all handlers must inherit {Brut::FrontEnd::Handler} (though realistically, they will inherit
|
46
|
-
`AppHandler`, which inherets `Brut::FrontEnd::Handler`) and implement `handle`.
|
47
|
-
|
48
|
-
`handle` can accept keyword arguments that are injected by Brut according to the rules outlined in {file:/doc-src/keyword-injection.md Keyword Injection}. Note that handlers that process forms should declare `form:` as a keyword argument. They will be given an instantiated instance of their form, based on the values in the form submission from the browser.
|
49
|
-
|
50
|
-
The return value of the method determines what will happen:
|
51
|
-
|
52
|
-
* `URI` - the visitor is redirected to this URI. Typically, you'd achieve this with the {Brut::FrontEnd::HandlingResults#redirect_to}
|
53
|
-
helper.
|
54
|
-
* {Brut::FrontEnd::Component} - this component's HTML is rendered. Note that since {Brut::FrontEnd::Page} is a subclass of
|
55
|
-
`Brut::FrontEnd::Component}, returning a page instance will render that entire page. This is useful when re-rendering a page with
|
56
|
-
form errors.
|
57
|
-
- You can also return a two-element array with the first element being a component and the second being a `Brut::FrontEnd::HttpStatus`. This will render the component's HTML but use the given status as the HTTP status.
|
58
|
-
* {Brut::FrontEnd::HttpStatus} - this status is returned with no content. Typically, you'd achieve this with the {Brut::FrontEnd::HandlingResults#http_status}
|
59
|
-
* {Brut::FrontEnd::Download} - a file will be downloaded
|
60
|
-
|
61
|
-
## Hooks
|
62
|
-
|
63
|
-
See {file:/doc-src/hooks.md} for more discussiong, but implementing `before_handle` will allow you to run code before `handle` is
|
64
|
-
called. This feature is mostly useful for a base class or module to share re-usable logic.
|
65
|
-
|
66
|
-
## Testing Handlers
|
67
|
-
|
68
|
-
Since a handler is just a class, you can test it conventionally, but there are a few things to keep in mind that can make testing your
|
69
|
-
handler easier.
|
70
|
-
|
71
|
-
First is that you should call `handle!`, not `handle`. The public interface of a handler is `handle!`—`handle` is a template method.
|
72
|
-
`handle!` will call `before_render`, so you should still call `handle!`, even if you are testing the logic in `before_render`.
|
73
|
-
|
74
|
-
You can also simplify your expectations with the following matchers:
|
75
|
-
|
76
|
-
* `have_redirected_to` - asserts that the handler redirected to the given URL. It can be given a `URI` or a page class.
|
77
|
-
* `have_rendered`- asserts that the handler renders the given component or page. It expects the page or component class.
|
78
|
-
* `have_returned_http_status` - asserts that the handler returned the given HTTP status. This works for a lot of return values that
|
79
|
-
the handler can return:
|
80
|
-
- If the handler returned a `URI`, the matcher will match on a 302 and fail otherwise
|
81
|
-
- If the handler returns an HTTP status code, the matcher's code must match (or the code must be omitted)
|
82
|
-
- If the handler returns anything else, the matcher will match on a 200 and fail otherwise
|
83
|
-
|