Skip to content

Commit f8411c2

Browse files
author
Julien Neuhart
committed
Validation documentation (almost) done
1 parent d4f1e9e commit f8411c2

File tree

2 files changed

+204
-6
lines changed

2 files changed

+204
-6
lines changed

docs/docs/5_Validation/5_4_GraphQL.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,19 @@ title: GraphQL
33
slug: /validation/graphql
44
---
55

6-
Thanks to [GraphQLite](https://graphqlite.thecodingmachine.io/), validation for the GraphQL API does not require
7-
extra work (usually):
6+
Most of the time, validation for the GraphQL API does not require extra work:
87

98
* [GraphQLite](https://graphqlite.thecodingmachine.io/) translates your models (with `@Type` and `@Field` annotations)
109
into strongly typed GraphQL types.
1110
* [GraphQLite](https://graphqlite.thecodingmachine.io/) translates your use cases' methods' signatures
12-
(with `@Mutation` or `@Query` annotations) into strongly typeed GraphQL mutations or queries.
11+
(with `@Mutation` or `@Query` annotations) into strongly typed GraphQL mutations or queries.
1312
* [GraphQLite](https://graphqlite.thecodingmachine.io/) resolves the `InvalidModel` and `InvalidStorable` exceptions
1413
into valid GraphQL responses (400 HTTP code).
1514

1615
However, you may have some use cases that are GraphQL mutations or queries, and these use cases do not manipulate models
1716
(i.e., no `InvalidModel` nor `InvalidStorable` exceptions).
1817

19-
The solution is actually quite simple. For instance:
18+
For such scenarios, [GraphQLite](https://graphqlite.thecodingmachine.io/) provides the `@Assertion` annotation:
2019

2120
```php title="src/api/src/UseCase/User/ResetPassword/ResetPassword.php"
2221
use Symfony\Component\Validator\Constraints as Assert;
@@ -30,7 +29,7 @@ use TheCodingMachine\Graphqlite\Validator\Annotations\Assertion;
3029
public function resetPassword(string $email): bool;
3130
```
3231

33-
Here, [GraphQLite](https://graphqlite.thecodingmachine.io/) validates the argument `email` according to the list of
32+
Here, [GraphQLite](https://graphqlite.thecodingmachine.io/) validates the `email` argument according to the list of
3433
constraints.
3534

3635
Only caveat is that it does not work if you call your use case in PHP.
Lines changed: 200 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,203 @@
11
---
22
title: Forms
33
slug: /validation/forms
4-
---
4+
---
5+
6+
In the previous chapters, we saw how to centralize the validation in the API.
7+
The next step is to integrate this validation mechanism with the web application.
8+
9+
Let's see how to do that! 😊
10+
11+
:::note
12+
13+
📣  The Symfony Boilerplate uses [BootstrapVue](https://bootstrap-vue.org/) as templating framework.
14+
However, most of the explanations from this chapter should work with most frameworks with little adjustments.
15+
16+
:::
17+
18+
## Browser Validation
19+
20+
Event if most of the validation occurs in the API, there are some rules that are better to validate directly in the
21+
browser.
22+
23+
For instance:
24+
25+
```html title="Vue component <template> block"
26+
<b-form-input
27+
type="text"
28+
autofocus
29+
trim
30+
required
31+
/>
32+
```
33+
34+
Here there are three rules:
35+
36+
1. The browser focuses this input first (UI logic).
37+
2. The browser trims the input's content.
38+
3. The browser does not allow to submit the form until this input has content.
39+
40+
Most of the time, that's all you have to do on the browser side.
41+
42+
## API Validation
43+
44+
The Symfony Boilerplate provides the `Form` mixin. This mixin brings everything you need for integrating
45+
the API validation in your UI.
46+
47+
:::note
48+
49+
📣&nbsp;&nbsp;A mixin content merges with the content of your Vue component.
50+
51+
:::
52+
53+
```js title="Vue component <script> block"
54+
import { Form } from '@/mixins/form'
55+
56+
export default {
57+
mixins: [Form],
58+
}
59+
```
60+
61+
Most of the logic occurs in the `onSubmit` method from your component:
62+
63+
```js title="Vue component <script> block"
64+
import { Form } from '@/mixins/form'
65+
import { UpdateEmailMutation } from '@/graphql/examples/update_email.mutation'
66+
67+
export default {
68+
mixins: [Form],
69+
data() {
70+
return {
71+
form: {
72+
email: '',
73+
},
74+
}
75+
},
76+
methods: {
77+
async onSubmit() {
78+
// We reset the form errors on submit.
79+
// This method comes from the Form mixin.
80+
this.resetFormErrors()
81+
82+
try {
83+
const result = await this.$graphql.request(UpdateEmailMutation, {
84+
email: this.form.email
85+
})
86+
87+
// Your UI logic on success.
88+
// ...
89+
} catch (e) {
90+
// If the API returns an error response (400 HTTP code),
91+
// we hydrate the Form mixin with its content.
92+
// This method comes from the Form mixin.
93+
this.hydrateFormErrors(e)
94+
}
95+
},
96+
},
97+
}
98+
```
99+
100+
:::note
101+
102+
📣&nbsp;&nbsp;If the error response is not about validation (i.e., 400 HTTP code), the `hydrateFromErrors` method
103+
throws the error back. The *src/webapp/layouts/error.vue* component will then catch it.
104+
105+
:::
106+
107+
Now, we have to display the validation errors to the user. Thanks to [BootstrapVue](https://bootstrap-vue.org/) and
108+
the `Form` mixin, it can be done quite easily:
109+
110+
```html title="Vue component <template> block"
111+
<b-form @submit.stop.prevent="onSubmit">
112+
<b-form-group
113+
id="input-group-email"
114+
:label="$t('common.email.label_required')"
115+
label-for="input-email"
116+
>
117+
<b-form-input
118+
id="input-email"
119+
v-model="form.email"
120+
type="text"
121+
:placeholder="$t('common.email.placeholder')"
122+
autofocus
123+
trim
124+
required
125+
:state="formState('email')"
126+
/>
127+
<b-form-invalid-feedback :state="formState('email')">
128+
<ErrorsList :errors="formErrors('email')" />
129+
</b-form-invalid-feedback>
130+
</b-form-group>
131+
<b-button type="submit" variant="primary">
132+
{{ $t('common.submit') }}
133+
</b-button>
134+
</b-form>
135+
```
136+
137+
* `<b-form @submit.stop.prevent="onSubmit">` binds the `onSubmit` method to this form.
138+
* The `:state` attribute from the `<b-form-input>` component displays the input in red in case of error.
139+
* The `<b-form-invalid-feedback>` component also uses the `:state` attribute. It works like a `v-if` to display or not
140+
a list of errors related to the previous input.
141+
* The Symfony Boilerplate provides the `<ErrorsList>` component.
142+
* The `formState` method returns the current state of a given GraphQL key (see below for more details).
143+
* The `formErrors` method returns the list of errors of a given GraphQL key (see below for more details).
144+
* Both `formState` and `formErrors` methods come from the `Form` mixin.
145+
146+
A GraphQL key is either:
147+
148+
* The GraphQL field name if `InvalidModel`.
149+
* The upload's directory name if `InvalidStorable`.
150+
* The PHP argument name if it's a [GraphQLite](https://graphqlite.thecodingmachine.io/) annotation.
151+
152+
## Loading State
153+
154+
Most of the time, you want to display a loader or make your form read-only when the user is submitting the form.
155+
156+
The Symfony Boilerplate provides the `GlobalOverlay` mixin for that task:
157+
158+
```js title="Vue component <script> block"
159+
import { GlobalOverlay } from '@/mixins/global-overlay'
160+
import { Form } from '@/mixins/form'
161+
import { UpdateEmailMutation } from '@/graphql/examples/update_email.mutation'
162+
163+
export default {
164+
mixins: [Form, GlobalOverlay],
165+
data() {
166+
return {
167+
form: {
168+
email: '',
169+
},
170+
}
171+
},
172+
methods: {
173+
async onSubmit() {
174+
this.resetFormErrors()
175+
// Displays the full page loader.
176+
// This method comes from the GlobalOverlay mixin.
177+
this.displayGlobalOverlay()
178+
179+
try {
180+
const result = await this.$graphql.request(UpdateEmailMutation, {
181+
email: this.form.email
182+
})
183+
184+
// Your UI logic on success.
185+
// ...
186+
} catch (e) {
187+
this.hydrateFormErrors(e)
188+
} finally {
189+
// Hides the full page loader.
190+
// This method comes from the GlobalOverlay mixin.
191+
this.hideGlobalOverlay()
192+
}
193+
},
194+
},
195+
}
196+
```
197+
198+
:::note
199+
200+
📣&nbsp;&nbsp;All layouts from the Symfony Boilerplate works with this mixin. If you add a custom layout, make sure
201+
it integrates well with the `GlobalOverlay` mixin.
202+
203+
:::

0 commit comments

Comments
 (0)