Skip to content

Commit 760cdd0

Browse files
Merge pull request #418 from feathersjs-ecosystem/input-wrapper
Input wrapper
2 parents 1b08ac2 + 3a70201 commit 760cdd0

File tree

7 files changed

+1073
-238
lines changed

7 files changed

+1073
-238
lines changed

docs/feathers-vuex-forms.md

Lines changed: 226 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
---
22
title: Working with Forms
3-
sidebarDepth: 3
3+
sidebarDepth: 4
44
---
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
@@ -274,3 +274,226 @@ export default {
274274
}
275275
</script>
276276
```
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+
```

package.json

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "feathers-vuex",
33
"description": "FeathersJS, Vue, and Nuxt for the artisan developer",
4-
"version": "3.4.1",
4+
"version": "3.5.0",
55
"homepage": "https:feathers-vuex.feathersjs-ecosystem.com",
66
"main": "dist/",
77
"module": "dist/",
@@ -106,9 +106,9 @@
106106
"@vue/composition-api": "^0.3.4"
107107
},
108108
"dependencies": {
109-
"@feathersjs/adapter-commons": "^4.4.3",
110-
"@feathersjs/commons": "^4.4.3",
111-
"@feathersjs/errors": "^4.4.3",
109+
"@feathersjs/adapter-commons": "^4.5.1",
110+
"@feathersjs/commons": "^4.5.1",
111+
"@feathersjs/errors": "^4.5.1",
112112
"@types/feathersjs__feathers": "^3.1.5",
113113
"@types/inflection": "^1.5.28",
114114
"@types/lodash": "^4.14.149",
@@ -128,26 +128,24 @@
128128
"lodash.pick": "^4.4.0",
129129
"lodash.trim": "^4.5.1",
130130
"serialize-error": "^5.0.0",
131-
"sift": "^9.0.4",
132-
"steal-typescript": "^0.5.0",
133-
"vuepress-theme-default-prefers-color-scheme": "^1.0.3"
131+
"sift": "^9.0.4"
134132
},
135133
"devDependencies": {
136-
"@feathersjs/authentication-client": "^4.4.3",
134+
"@feathersjs/authentication-client": "^4.5.1",
137135
"@feathersjs/authentication-jwt": "^2.0.10",
138-
"@feathersjs/client": "^4.4.3",
139-
"@feathersjs/feathers": "^4.4.3",
140-
"@feathersjs/rest-client": "^4.4.3",
141-
"@feathersjs/socketio-client": "^4.4.3",
142-
"@types/chai": "^4.2.7",
143-
"@types/mocha": "^5.2.7",
144-
"@typescript-eslint/eslint-plugin": "^2.16.0",
145-
"@typescript-eslint/parser": "^2.16.0",
136+
"@feathersjs/client": "^4.5.1",
137+
"@feathersjs/feathers": "^4.5.1",
138+
"@feathersjs/rest-client": "^4.5.1",
139+
"@feathersjs/socketio-client": "^4.5.1",
140+
"@types/chai": "^4.2.8",
141+
"@types/mocha": "^7.0.1",
142+
"@typescript-eslint/eslint-plugin": "^2.18.0",
143+
"@typescript-eslint/parser": "^2.18.0",
146144
"@vue/composition-api": "^0.3.4",
147145
"@vue/eslint-config-prettier": "^6.0.0",
148146
"@vue/eslint-config-typescript": "^5.0.1",
149-
"@vue/test-utils": "^1.0.0-beta.30",
150-
"axios": "^0.19.1",
147+
"@vue/test-utils": "^1.0.0-beta.31",
148+
"axios": "^0.19.2",
151149
"babel-cli": "^6.26.0",
152150
"babel-core": "^6.26.3",
153151
"babel-eslint": "^10.0.3",
@@ -160,28 +158,30 @@
160158
"date-fns": "^2.9.0",
161159
"deep-object-diff": "^1.1.0",
162160
"eslint": "^6.8.0",
163-
"eslint-config-prettier": "^6.9.0",
161+
"eslint-config-prettier": "^6.10.0",
164162
"eslint-plugin-prettier": "^3.1.2",
165163
"eslint-plugin-vue": "^6.1.2",
166164
"feathers-memory": "^4.1.0",
167165
"istanbul": "^1.1.0-alpha.1",
168-
"jsdom": "^16.0.0",
166+
"jsdom": "^16.0.1",
169167
"jsdom-global": "^3.0.2",
170-
"mocha": "^7.0.0",
168+
"mocha": "^7.0.1",
171169
"omit-deep-lodash": "^1.1.4",
172170
"prettier": "^1.19.1",
173171
"shx": "^0.3.2",
174172
"socket.io-client": "^2.3.0",
175173
"standard": "^14.3.1",
176174
"steal": "^2.2.4",
177175
"steal-mocha": "^2.0.1",
176+
"steal-typescript": "^0.5.0",
178177
"testee": "^0.9.1",
179178
"ts-node": "^8.6.2",
180-
"typescript": "^3.7.4",
179+
"typescript": "^3.7.5",
181180
"vue": "^2.6.11",
182181
"vue-server-renderer": "^2.6.11",
183182
"vue-template-compiler": "^2.6.11",
184-
"vuepress": "^1.2.0",
183+
"vuepress": "^1.3.0",
184+
"vuepress-theme-default-prefers-color-scheme": "^1.0.4",
185185
"vuex": "^3.1.2"
186186
}
187187
}

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

0 commit comments

Comments
 (0)