|
1 | 1 | ---
|
2 | 2 | title: Working with Forms
|
3 |
| -sidebarDepth: 3 |
| 3 | +sidebarDepth: 4 |
4 | 4 | ---
|
5 | 5 |
|
6 | 6 | # Working with Forms
|
7 | 7 |
|
8 |
| -The `FeathersVuexFormWrapper` is a renderless component which assists in connecting your feathers-vuex data to a form. The next two sections review why it exists by looking at a couple of common patterns. Proceed to the [FeathersVuexFormWrapper](#feathersvuexformwrapper) section to learn how to implement. |
| 8 | +The `FeathersVuexFormWrapper` and `FeathersVuexInputWrapper` are renderless components which assist in connecting your feathers-vuex data to a form. The next two sections review why they exist by looking at a couple of common patterns. Proceed to the [FeathersVuexFormWrapper](#feathersvuexformwrapper) or [FeathersVuexInputWrapper](#feathersvuexinputwrapper) sections to learn how to implement. |
9 | 9 |
|
10 | 10 | ## The Mutation Multiplicity (anti) Pattern
|
11 | 11 |
|
@@ -151,7 +151,7 @@ The default slot contains only four attributes. The `clone` data can be passed
|
151 | 151 | - `reset`: {Function} When called, the clone data will be reset back to the data that is currently found in the store for the same record.
|
152 | 152 | - `remove`: {Function} When called, it removes the record from the API server and the Vuex store.
|
153 | 153 |
|
154 |
| -## Example Usage: CRUD Form |
| 154 | +## FormWrapper Example: CRUD Form |
155 | 155 |
|
156 | 156 | ### TodoView
|
157 | 157 |
|
@@ -274,3 +274,226 @@ export default {
|
274 | 274 | }
|
275 | 275 | </script>
|
276 | 276 | ```
|
| 277 | +
|
| 278 | +## FeathersVuexInputWrapper |
| 279 | +
|
| 280 | +Building on the same ideas as the FeathersVuexFormWrapper, the FeathersVuexInputWrapper reduces boilerplate for working with the clone and commit pattern on a single input. |
| 281 | +
|
| 282 | +An important difference with the FeathersVuexInputWrapper is that it is built using the Vue Composition API. This means that in order to use it you will need to install and use the `@vue/composition-api` package in your Vue project, [as described here](/composition-api.html). |
| 283 | +
|
| 284 | +One use case for this component is implementing an "edit-in-place" workflow. The following example shows how to use the FeathersVuexInputWrapper to automatically save a record upon `blur` on text and color inputs: |
| 285 | +
|
| 286 | +```html |
| 287 | +<template> |
| 288 | + <div class="p-3"> |
| 289 | + <FeathersVuexInputWrapper :item="user" prop="email"> |
| 290 | + <template #default="{ current, prop, createClone, handler }"> |
| 291 | + <input v-model="current[prop]" type="text" @focus="createClone" @blur="e => handler(e, save)" /> |
| 292 | + </template> |
| 293 | + </FeathersVuexInputWrapper> |
| 294 | +
|
| 295 | + <!-- Simple readout to show that it's working. --> |
| 296 | + <pre class="bg-black text-white text-xs mt-2 p-1">{{user}}</pre> |
| 297 | + </div> |
| 298 | +</template> |
| 299 | + |
| 300 | +<script> |
| 301 | +export default { |
| 302 | + name: 'InputWrapperExample', |
| 303 | + methods: { |
| 304 | + // Optionally make the event handler async. |
| 305 | + async save({ event, clone, prop, data }) { |
| 306 | + const user = clone.commit() |
| 307 | + return user.patch(data) |
| 308 | + } |
| 309 | + } |
| 310 | +} |
| 311 | +</script> |
| 312 | +``` |
| 313 | +
|
| 314 | +Notice that in the `save` handler in the above example, the `.patch` method is called on the user, passing in the data. Because the data contains only the user property which changed, the patch request will only send the data which has changed, saving precious bandwidth. |
| 315 | +
|
| 316 | +### Props |
| 317 | +
|
| 318 | +The `FeathersVuexInputWrapper` has two props, both of which are required: |
| 319 | +
|
| 320 | +- `item`: The original (non-cloned) model instance. |
| 321 | +- `prop`: The property name on the model instance to be edited. |
| 322 | +
|
| 323 | +### Default Slot Scope |
| 324 | +
|
| 325 | +Only the default slot is used. The following props are available in the slot scope: |
| 326 | +
|
| 327 | +- `current {clone|instance}`: returns the clone if it exists, or the original record. `current = clone || item` |
| 328 | +- `clone { clone }`: the internal clone. This is exposed for debugging purposes. |
| 329 | +- `prop {String}`: the value of the `prop` prop. If you have the prop stored in a variable in the outer scope, this is redundant and not needed. You could just use this from the outer scope. It mostly comes in handy when you are manually specifying the `prop` name on the component. |
| 330 | +- `createClone {Function}`: sets up the internal clone. Meant to be used as an event handler. |
| 331 | +- `handler {Function}`: has the signature `handler(event, callback)`. It prepared data before calling the callback function that must be provided from the outer scope. |
| 332 | +
|
| 333 | +### The Callback Function |
| 334 | +
|
| 335 | +The `handler` function in the slot scope requires the use of a callback function as its second argument. Here's an example callback function followed by an explanation of its properties: |
| 336 | +
|
| 337 | +```js |
| 338 | +myCallback({ event, clone, prop, data }) { |
| 339 | + clone.commit() |
| 340 | +} |
| 341 | +``` |
| 342 | +
|
| 343 | +- `event {Event}`: the event which triggered the `handler` function in the slot scope. |
| 344 | +- `clone {clone}`: the cloned version of the `item` instance that was provided as a prop. |
| 345 | +- `prop {String}`: the name of the `prop` that is being edited (will always match the `prop` prop.) |
| 346 | +- `data {Object}`: An object containing the changes that were made to the object. Useful for calling `.patch(data)` on the original instance. |
| 347 | +
|
| 348 | +This callback needs to be customized to fit your business logic. You might patch the changes right away, as shown in this example callback function. |
| 349 | +
|
| 350 | +```js |
| 351 | +async save({ event, clone, prop, data }) { |
| 352 | + const user = clone.commit() |
| 353 | + return user.patch(data) |
| 354 | +} |
| 355 | +``` |
| 356 | +
|
| 357 | +Notice in the example above that the `save` function is `async`. This means that it returns a promise, which in this case is the `user.patch` request. Internally, the `handler` method will automatically set the internal `clone` object to `null`, which will cause the `current` computed property to return the original instance. |
| 358 | +
|
| 359 | +Note that some types of HTML input elements will call `handler` repeatedly, so the handler needs to be debounced. See an example, below. |
| 360 | +
|
| 361 | +## InputWrapper Examples |
| 362 | +
|
| 363 | +### Text Input |
| 364 | +
|
| 365 | +With a text input, you can use the `focus` and `blur` events |
| 366 | +
|
| 367 | +```html |
| 368 | +<template> |
| 369 | + <div class="p-3"> |
| 370 | + <FeathersVuexInputWrapper :item="user" prop="email"> |
| 371 | + <template #default="{ current, prop, createClone, handler }"> |
| 372 | + <input |
| 373 | + v-model="current[prop]" |
| 374 | + type="text" |
| 375 | + @focus="createClone" |
| 376 | + @blur="e => handler(e, save)" |
| 377 | + /> |
| 378 | + </template> |
| 379 | + </FeathersVuexInputWrapper> |
| 380 | + |
| 381 | + <!-- Simple readout to show that it's working. --> |
| 382 | + <pre class="bg-black text-white text-xs mt-2 p-1">{{user}}</pre> |
| 383 | + </div> |
| 384 | +</template> |
| 385 | +
|
| 386 | +<script> |
| 387 | +export default { |
| 388 | + name: 'InputWrapperExample', |
| 389 | + props: { |
| 390 | + user: { |
| 391 | + type: Object, |
| 392 | + required: true |
| 393 | + } |
| 394 | + }, |
| 395 | + methods: { |
| 396 | + // The callback can be async |
| 397 | + async save({ event, clone, prop, data }) { |
| 398 | + const user = clone.commit() |
| 399 | + return user.patch(data) |
| 400 | + } |
| 401 | + } |
| 402 | +} |
| 403 | +</script> |
| 404 | +``` |
| 405 | +
|
| 406 | +### Color Input |
| 407 | +
|
| 408 | +Here is an example of using the FeathersVuexInputWrapper on a color input. Color inputs emit a lot of `input` and `change` events, so you'll probably want to debounce the callback function if you are going to immediately save changes. The example after this one shows how you might debounce. |
| 409 | + |
| 410 | +```html |
| 411 | +<template> |
| 412 | + <div class="p-3"> |
| 413 | + <FeathersVuexInputWrapper :item="user" prop="email"> |
| 414 | + <template #default="{ current, prop, createClone, handler }"> |
| 415 | + <input |
| 416 | + v-model="current[prop]" |
| 417 | + type="text" |
| 418 | + @click="createClone" |
| 419 | + @change="e => handler(e, save)" |
| 420 | + /> |
| 421 | + </template> |
| 422 | + </FeathersVuexInputWrapper> |
| 423 | + |
| 424 | + <!-- Simple readout to show that it's working. --> |
| 425 | + <pre class="bg-black text-white text-xs mt-2 p-1">{{user}}</pre> |
| 426 | + </div> |
| 427 | +</template> |
| 428 | + |
| 429 | +<script> |
| 430 | +export default { |
| 431 | + name: 'InputWrapperExample', |
| 432 | + props: { |
| 433 | + user: { |
| 434 | + type: Object, |
| 435 | + required: true |
| 436 | + } |
| 437 | + }, |
| 438 | + methods: { |
| 439 | + // The callback can be async |
| 440 | + async save({ event, clone, prop, data }) { |
| 441 | + const user = clone.commit() |
| 442 | + return user.patch(data) |
| 443 | + } |
| 444 | + } |
| 445 | +} |
| 446 | +</script> |
| 447 | +``` |
| 448 | +
|
| 449 | +### Color Input with Debounce |
| 450 | +
|
| 451 | +Here is an example of using the FeathersVuexInputWrapper on a color input. Notice how the debounced callback function is provided to the `handler`. This is because color inputs trigger a `change` event every time their value changes. To prevent sending thousands of patch requests as the user changes colors, we use the debounced function to only send a request after 100ms of inactivity. |
| 452 | +
|
| 453 | +Notice also that this example uses the Vue Composition API because creating a debounced function is much cleaner this way. |
| 454 | +
|
| 455 | +```vue |
| 456 | +<template> |
| 457 | + <div class="p-3"> |
| 458 | + <FeathersVuexInputWrapper :item="user" prop="email"> |
| 459 | + <template #default="{ current, prop, createClone, handler }"> |
| 460 | + <input |
| 461 | + v-model="current[prop]" |
| 462 | + type="text" |
| 463 | + @click="createClone" |
| 464 | + @change="e => handler(e, debouncedSave)" |
| 465 | + /> |
| 466 | + </template> |
| 467 | + </FeathersVuexInputWrapper> |
| 468 | + |
| 469 | + <!-- Simple readout to show that it's working. --> |
| 470 | + <pre class="bg-black text-white text-xs mt-2 p-1">{{user}}</pre> |
| 471 | + </div> |
| 472 | +</template> |
| 473 | +
|
| 474 | +<script> |
| 475 | +import _debounce from 'lodash/debounce' |
| 476 | +
|
| 477 | +export default { |
| 478 | + name: 'InputWrapperExample', |
| 479 | + props: { |
| 480 | + user: { |
| 481 | + type: Object, |
| 482 | + required: true |
| 483 | + } |
| 484 | + }, |
| 485 | + setup() { |
| 486 | + // The original, non-debounced save function |
| 487 | + async function save({ event, clone, prop, data }) { |
| 488 | + const user = clone.commit() |
| 489 | + return user.patch(data) |
| 490 | + } |
| 491 | + // The debounced wrapper around the save function |
| 492 | + const debouncedSave = _debounce(save, 100) |
| 493 | +
|
| 494 | + // We only really need to provide the debouncedSave to the template. |
| 495 | + return { debouncedSave } |
| 496 | + } |
| 497 | +} |
| 498 | +</script> |
| 499 | +``` |
0 commit comments