Skip to content

Commit 1e96781

Browse files
committed
Add FeathersVuexInputWrapper
Includes docs
1 parent 7bf0dba commit 1e96781

File tree

5 files changed

+388
-11
lines changed

5 files changed

+388
-11
lines changed

docs/feathers-vuex-forms.md

Lines changed: 190 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ sidebarDepth: 4
55

66
# Working with Forms
77

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.
99

1010
## The Mutation Multiplicity (anti) Pattern
1111

@@ -151,7 +151,7 @@ The default slot contains only four attributes. The `clone` data can be passed
151151
- `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.
152152
- `remove`: {Function} When called, it removes the record from the API server and the Vuex store.
153153
154-
## Example Usage: CRUD Form
154+
## FormWrapper Example: CRUD Form
155155
156156
### TodoView
157157
@@ -277,7 +277,11 @@ export default {
277277
278278
## FeathersVuexInputWrapper
279279
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. 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:
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:
281285
282286
```html
283287
<template>
@@ -288,9 +292,89 @@ Building on the same ideas as the FeathersVuexFormWrapper, the FeathersVuexInput
288292
</template>
289293
</FeathersVuexInputWrapper>
290294
291-
<FeathersVuexInputWrapper :item="user" prop="carColor">
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">
292371
<template #default="{ current, prop, createClone, handler }">
293-
<input v-model="current[prop]" type="color" @focus="createClone" @blur="e => handler(e, save)" />
372+
<input
373+
v-model="current[prop]"
374+
type="text"
375+
@focus="createClone"
376+
@blur="e => handler(e, save)"
377+
/>
294378
</template>
295379
</FeathersVuexInputWrapper>
296380

@@ -302,8 +386,14 @@ Building on the same ideas as the FeathersVuexFormWrapper, the FeathersVuexInput
302386
<script>
303387
export default {
304388
name: 'InputWrapperExample',
389+
props: {
390+
user: {
391+
type: Object,
392+
required: true
393+
}
394+
},
305395
methods: {
306-
// Optionally make the event handler async.
396+
// The callback can be async
307397
async save({ event, clone, prop, data }) {
308398
const user = clone.commit()
309399
return user.patch(data)
@@ -313,4 +403,97 @@ export default {
313403
</script>
314404
```
315405
316-
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.
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+
```

src/FeathersVuexFormWrapper.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ export default {
1010
required: true
1111
},
1212
/**
13-
* By default, when you call the `save` method, the cloned data will be
14-
* committed to the store BEFORE saving tot he API server. Set
15-
* `:eager="false"` to only update the store with the API server response.
16-
*/
13+
* By default, when you call the `save` method, the cloned data will be
14+
* committed to the store BEFORE saving tot he API server. Set
15+
* `:eager="false"` to only update the store with the API server response.
16+
*/
1717
eager: {
1818
type: Boolean,
1919
default: true
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import FeathersVuexInputWrapper from './FeathersVuexInputWrapper.vue'
2+
import { makeModel } from '@rovit/test-model'
3+
import { debounce } from 'lodash'
4+
5+
const User = makeModel()
6+
7+
const user = new User({
8+
_id: 1,
9+
email: 'marshall@rovit.com',
10+
carColor: '#FFF'
11+
})
12+
13+
export default {
14+
title: 'FeathersVuexInputWrapper',
15+
component: FeathersVuexInputWrapper
16+
}
17+
18+
export const basic = () => ({
19+
components: {
20+
FeathersVuexInputWrapper
21+
},
22+
data: () => ({
23+
user
24+
}),
25+
methods: {
26+
save({ clone, data }) {
27+
const user = clone.commit()
28+
user.patch(data)
29+
}
30+
},
31+
template: `
32+
<div class="p-3">
33+
<FeathersVuexInputWrapper :item="user" prop="email">
34+
<template #default="{ current, prop, createClone, handler }">
35+
<input
36+
v-model="current[prop]"
37+
type="text"
38+
@focus="createClone"
39+
@blur="e => handler(e, save)"
40+
/>
41+
</template>
42+
</FeathersVuexInputWrapper>
43+
44+
<pre class="bg-black text-white text-xs mt-2 p-1">{{user}}</pre>
45+
</div>
46+
`
47+
})
48+
49+
export const handlerAsPromise = () => ({
50+
components: {
51+
FeathersVuexInputWrapper
52+
},
53+
data: () => ({
54+
user
55+
}),
56+
methods: {
57+
async save({ clone, data }) {
58+
const user = clone.commit()
59+
return user.patch(data)
60+
}
61+
},
62+
template: `
63+
<div class="p-3">
64+
<FeathersVuexInputWrapper :item="user" prop="email">
65+
<template #default="{ current, prop, createClone, handler }">
66+
<input
67+
v-model="current[prop]"
68+
type="text"
69+
@focus="createClone"
70+
@blur="e => handler(e, save)"
71+
class="bg-gray-200 rounded"
72+
/>
73+
</template>
74+
</FeathersVuexInputWrapper>
75+
76+
<pre class="bg-black text-white text-xs mt-2 p-1">{{user}}</pre>
77+
</div>
78+
`
79+
})
80+
81+
export const multipleOnDistinctProperties = () => ({
82+
components: {
83+
FeathersVuexInputWrapper
84+
},
85+
data: () => ({
86+
user
87+
}),
88+
methods: {
89+
async save({ event, clone, prop, data }) {
90+
const user = clone.commit()
91+
return user.patch(data)
92+
}
93+
},
94+
template: `
95+
<div class="p-3">
96+
<FeathersVuexInputWrapper :item="user" prop="email">
97+
<template #default="{ current, prop, createClone, handler }">
98+
<input
99+
v-model="current[prop]"
100+
type="text"
101+
@focus="createClone"
102+
@blur="e => handler(e, save)"
103+
/>
104+
</template>
105+
</FeathersVuexInputWrapper>
106+
107+
<FeathersVuexInputWrapper :item="user" prop="carColor">
108+
<template #default="{ current, prop, createClone, handler }">
109+
<input
110+
v-model="current[prop]"
111+
type="color"
112+
@click="createClone"
113+
@change="e => handler(e, save)"
114+
/>
115+
</template>
116+
</FeathersVuexInputWrapper>
117+
118+
<pre class="bg-black text-white text-xs mt-2 p-1">{{user}}</pre>
119+
</div>
120+
`
121+
})
122+
123+
export const noInputInSlot = () => ({
124+
components: {
125+
FeathersVuexInputWrapper
126+
},
127+
data: () => ({
128+
user
129+
}),
130+
methods: {
131+
async save({ clone, data }) {
132+
const user = clone.commit()
133+
user.patch(data)
134+
}
135+
},
136+
template: `
137+
<div class="p-3">
138+
<FeathersVuexInputWrapper :item="user" prop="email" />
139+
</div>
140+
`
141+
})

0 commit comments

Comments
 (0)