1 | # vue-functional-data-merge
|
2 |
|
3 | [![npm](https://img.shields.io/npm/v/vue-functional-data-merge.svg?style=for-the-badge)](https://img.shields.io/npm/v/vue-functional-data-merge)
|
4 | [![npm downloads](https://img.shields.io/npm/dt/vue-functional-data-merge.svg?style=for-the-badge)](https://www.npmjs.com/package/vue-functional-data-merge)
|
5 | [![GitHub stars](https://img.shields.io/github/stars/alexsasharegan/vue-functional-data-merge.svg?style=for-the-badge)](https://github.com/alexsasharegan/vue-functional-data-merge/stargazers)
|
6 | [![GitHub issues](https://img.shields.io/github/issues/alexsasharegan/vue-functional-data-merge.svg?style=for-the-badge)](https://github.com/alexsasharegan/vue-functional-data-merge/issues)
|
7 | [![Travis](https://img.shields.io/travis/alexsasharegan/vue-functional-data-merge.svg?style=for-the-badge)](https://github.com/alexsasharegan/vue-functional-data-merge)
|
8 | [![Coverage Status](https://img.shields.io/coveralls/github/alexsasharegan/vue-functional-data-merge.svg?style=for-the-badge)](https://coveralls.io/github/alexsasharegan/vue-functional-data-merge)
|
9 | [![GitHub license](https://img.shields.io/github/license/alexsasharegan/vue-functional-data-merge.svg?style=for-the-badge)](https://github.com/alexsasharegan/vue-functional-data-merge/blob/master/LICENSE.md)
|
10 |
|
11 | Vue.js util for intelligently merging data passed to functional components. (1K
|
12 | => 0.5K gzipped)
|
13 |
|
14 | - [Getting Started](#getting-started)
|
15 | - [Why do I need this util?](#why-do-i-need-this-util)
|
16 | - [Scoped Styles](#scoped-styles)
|
17 | - [Performance](#performance)
|
18 |
|
19 | ## Getting Started
|
20 |
|
21 | Load the util from npm:
|
22 |
|
23 | ```sh
|
24 | # NPM:
|
25 | npm i vue-functional-data-merge
|
26 |
|
27 | # Yarn:
|
28 | yarn add vue-functional-data-merge
|
29 | ```
|
30 |
|
31 | Now import and use it in your functional component declaration:
|
32 |
|
33 | ```js
|
34 | // MyFunctionalComponent.js
|
35 |
|
36 | // ESM
|
37 | import { mergeData } from "vue-functional-data-merge";
|
38 | // Common JS
|
39 | const { mergeData } = require("vue-functional-data-merge/dist/lib.common.js");
|
40 |
|
41 | export default {
|
42 | name: "my-functional-component",
|
43 | functional: true,
|
44 | props: ["foo", "bar", "baz"],
|
45 | render(h, { props, data, children }) {
|
46 | const componentData = {
|
47 | staticClass: "fn-component", // concatenates all static classes
|
48 | class: {
|
49 | // object|Array|string all get merged and preserved
|
50 | active: props.foo,
|
51 | "special-class": props.bar,
|
52 | },
|
53 | attrs: {
|
54 | id: "my-functional-component", // now overrides any id placed on the component
|
55 | },
|
56 | on: {
|
57 | // Event handlers are merged to an array of handlers at each event.
|
58 | // The last data object passed to `mergeData` will have it's event handlers called first.
|
59 | // Right-most arguments are prepended to event handler array.
|
60 | click(e) {
|
61 | alert(props.baz);
|
62 | },
|
63 | },
|
64 | };
|
65 |
|
66 | return h("div", mergeData(data, componentData), children);
|
67 | },
|
68 | };
|
69 | ```
|
70 |
|
71 | ## Why do I need this util?
|
72 |
|
73 | When writing functional Vue components, the render function receives a
|
74 | `context.data` object
|
75 | ([see vue docs](https://vuejs.org/v2/guide/render-function.html#Functional-Components)).
|
76 | This object that contains the entire data object passed to the component (the
|
77 | shape of which
|
78 | [can be found here](https://vuejs.org/v2/guide/render-function.html#The-Data-Object-In-Depth)).
|
79 | In order to write flexible components, the data object used to create the
|
80 | component must be merged with the data received. If not, only the properties
|
81 | defined by the component will be rendered.
|
82 |
|
83 | Consider this example:
|
84 |
|
85 | ```js
|
86 | // MyBtn.js
|
87 | export default {
|
88 | name: "my-btn",
|
89 | props: ["variant"],
|
90 | functional: true,
|
91 | render(h, { props, children }) {
|
92 | return h(
|
93 | "button",
|
94 | {
|
95 | staticClass: "btn",
|
96 | class: [`btn-${props.variant}`],
|
97 | attrs: { type: "button" },
|
98 | },
|
99 | children
|
100 | );
|
101 | },
|
102 | };
|
103 | ```
|
104 |
|
105 | This exports a functional button component that applies a base `.btn` class and
|
106 | a `.btn-<variant>` class based on the `variant` prop passed to the component.
|
107 | It's just a simple wrapper around some Bootstrap styling to make repetitive
|
108 | usage simpler. Usage would look like this:
|
109 |
|
110 | ```html
|
111 | <template>
|
112 | <form>
|
113 | <input type="text" placeholder="Name" required>
|
114 | <input type="email" placeholder="email" required>
|
115 | <my-btn variant="primary" type="submit" id="form-submit-btn" @click="onClick">Submit</my-btn>
|
116 | </form>
|
117 | </template>
|
118 | ```
|
119 |
|
120 | We've used our Bootstrap button component in a form and conveniently applied the
|
121 | `primary` variant, but we also wanted to change the button `type` from `button`
|
122 | to `submit`, give it an `id`, and attach a click handler. This won't work
|
123 | because we haven't passed the attributes, listeners, etc. to the create element
|
124 | call in the component's render function.
|
125 |
|
126 | To fix this, we might extract out props, merge listeners/attributes, etc. This
|
127 | works well, but gets verbose fast when attempting to support all dom attributes,
|
128 | event listeners, etc. One might think to simply use Object spread or
|
129 | `Object.assign` to solve this like so:
|
130 |
|
131 | ```js
|
132 | return h("button", { ...context.data, ...componentData }, children);
|
133 | ```
|
134 |
|
135 | Now when we try to add any dom attributes, Object spread is essentially
|
136 | performing something like this:
|
137 |
|
138 | ```js
|
139 | Object.assign(
|
140 | {},
|
141 | {
|
142 | props: { variant: "primary" },
|
143 | attrs: { id: "form-submit-btn", type: "submit" }
|
144 | on: { click: onClick }
|
145 | },
|
146 | {
|
147 | staticClass: "btn",
|
148 | class: [`btn-${props.variant}`],
|
149 | attrs: { type: "button" },
|
150 | on: {
|
151 | click() {
|
152 | alert("Hello from MyBtn!")
|
153 | }
|
154 | }
|
155 | }
|
156 | )
|
157 | ```
|
158 |
|
159 | The component data will wipe out all the context's `attrs` and `on` handlers as
|
160 | `Object.assign` merges these properties. This is where the `mergeData` util can
|
161 | help you. It will dig into the nested properties of the `context.data` and apply
|
162 | different merge strategies for each data property. `mergeData` works like a
|
163 | nested `Object.assign` in that the util has a variadic argument length—you
|
164 | can pass any number of arguments to it, and they will all be merged from left to
|
165 | right (the right most arguments taking merge priority). You don't have to pass a
|
166 | new target object as the first argument, as the return value will always be a
|
167 | fresh object.
|
168 |
|
169 | ## Scoped Styles
|
170 |
|
171 | You may run into cases where you are using a functional component in another
|
172 | component with scoped styles. This would look something like this:
|
173 |
|
174 | ```html
|
175 | <template>
|
176 | <button class="my-class">
|
177 | <slot></slot>
|
178 | </button>
|
179 | </template>
|
180 | <style scoped>
|
181 | .my-class {
|
182 | text-align: center;
|
183 | }
|
184 | </style>
|
185 | ```
|
186 |
|
187 | This will generate data attributes on the component elements and the css
|
188 | selector.
|
189 |
|
190 | ```html
|
191 | <style>
|
192 | .my-class[data-v-f3f3eg9] {
|
193 | text-align: center;
|
194 | }
|
195 | </style>
|
196 |
|
197 | <button data-v-f3f3eg9 class="my-class">
|
198 | Click me!
|
199 | </button>
|
200 | ```
|
201 |
|
202 | When a parent component with scoped styles makes use of a functional component,
|
203 | the data attribute won't be passed down automatically. Instead, you must pull
|
204 | this attribute out manually and add it to the `VNodeData` used in a render
|
205 | function's `createElement` call. Doing this requires reaching into Vue
|
206 | internals, which can be risky due to the private nature of the API and its
|
207 | potential to change. For that reason, this is not supported in this util.
|
208 |
|
209 | However, this util can make that manual merging easier by conforming to the
|
210 | `VNodeData` shape required by `mergeData` and Vue itself. Here is an example of
|
211 | a helper function to manually extract a parent's style scope id and
|
212 | conditionally apply it in the functional component's render function.
|
213 |
|
214 | ```js
|
215 | const FunctionalComponent = {
|
216 | functional: true,
|
217 | render(createElement, context) {
|
218 | let { parent, data, children } = context;
|
219 | let componentData = { class: "my-class" };
|
220 |
|
221 | return createElement(
|
222 | "button",
|
223 | mergeData(data, getScopedStyleData(parent), componentData),
|
224 | children
|
225 | );
|
226 | },
|
227 | };
|
228 |
|
229 | /**
|
230 | * @param {Vue} parent
|
231 | * @returns {VNodeData}
|
232 | */
|
233 | export function getScopedStyleData(parent) {
|
234 | let data = { attrs: {} };
|
235 |
|
236 | if (parent.$options._scopeId) {
|
237 | data.attrs[`data-v-${parent.$options._scopeId}`] = "";
|
238 | }
|
239 |
|
240 | return data;
|
241 | }
|
242 | ```
|
243 |
|
244 | ## Performance
|
245 |
|
246 | This util was written with performance in mind. Since functional components are
|
247 | perfect for components that are stateless and have many nodes rendered, the
|
248 | `mergeData` util is expected to be called extensively. As such, minimal variable
|
249 | allocations are made as well as minimal internal function calls _(for loops are
|
250 | preferred over `map`, `reduce`, & `forEach` to avoid adding stack frames)_.
|
251 | TypeScript is used with Vue typings to ensure the most accurate merge strategy
|
252 | for each property of the `context.data` object. You can run the benchmark
|
253 | yourself, but simple merges run at ~1,000,000 ops/sec and complex merges at
|
254 | ~400,000 ops/sec.
|