UNPKG

8.61 kBMarkdownView Raw
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
11Vue.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
21Load the util from npm:
22
23```sh
24# NPM:
25npm i vue-functional-data-merge
26
27# Yarn:
28yarn add vue-functional-data-merge
29```
30
31Now import and use it in your functional component declaration:
32
33```js
34// MyFunctionalComponent.js
35
36// ESM
37import { mergeData } from "vue-functional-data-merge";
38// Common JS
39const { mergeData } = require("vue-functional-data-merge/dist/lib.common.js");
40
41export 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
73When 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)).
76This object that contains the entire data object passed to the component (the
77shape of which
78[can be found here](https://vuejs.org/v2/guide/render-function.html#The-Data-Object-In-Depth)).
79In order to write flexible components, the data object used to create the
80component must be merged with the data received. If not, only the properties
81defined by the component will be rendered.
82
83Consider this example:
84
85```js
86// MyBtn.js
87export 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
105This exports a functional button component that applies a base `.btn` class and
106a `.btn-<variant>` class based on the `variant` prop passed to the component.
107It's just a simple wrapper around some Bootstrap styling to make repetitive
108usage 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
120We'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`
122to `submit`, give it an `id`, and attach a click handler. This won't work
123because we haven't passed the attributes, listeners, etc. to the create element
124call in the component's render function.
125
126To fix this, we might extract out props, merge listeners/attributes, etc. This
127works well, but gets verbose fast when attempting to support all dom attributes,
128event listeners, etc. One might think to simply use Object spread or
129`Object.assign` to solve this like so:
130
131```js
132return h("button", { ...context.data, ...componentData }, children);
133```
134
135Now when we try to add any dom attributes, Object spread is essentially
136performing something like this:
137
138```js
139Object.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
159The 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
161help you. It will dig into the nested properties of the `context.data` and apply
162different merge strategies for each data property. `mergeData` works like a
163nested `Object.assign` in that the util has a variadic argument length&mdash;you
164can pass any number of arguments to it, and they will all be merged from left to
165right (the right most arguments taking merge priority). You don't have to pass a
166new target object as the first argument, as the return value will always be a
167fresh object.
168
169## Scoped Styles
170
171You may run into cases where you are using a functional component in another
172component 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
187This will generate data attributes on the component elements and the css
188selector.
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
202When a parent component with scoped styles makes use of a functional component,
203the data attribute won't be passed down automatically. Instead, you must pull
204this attribute out manually and add it to the `VNodeData` used in a render
205function's `createElement` call. Doing this requires reaching into Vue
206internals, which can be risky due to the private nature of the API and its
207potential to change. For that reason, this is not supported in this util.
208
209However, 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
211a helper function to manually extract a parent's style scope id and
212conditionally apply it in the functional component's render function.
213
214```js
215const 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 */
233export 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
246This util was written with performance in mind. Since functional components are
247perfect for components that are stateless and have many nodes rendered, the
248`mergeData` util is expected to be called extensively. As such, minimal variable
249allocations are made as well as minimal internal function calls _(for loops are
250preferred over `map`, `reduce`, & `forEach` to avoid adding stack frames)_.
251TypeScript is used with Vue typings to ensure the most accurate merge strategy
252for each property of the `context.data` object. You can run the benchmark
253yourself, but simple merges run at ~1,000,000 ops/sec and complex merges at
254~400,000 ops/sec.