Code Monkey home page Code Monkey logo

vue-functional-data-merge's Introduction

vue-functional-data-merge

npm npm downloads GitHub stars GitHub issues Travis Coverage Status GitHub license

Vue.js util for intelligently merging data passed to functional components. (1K => 0.5K gzipped)

Getting Started

Load the util from npm:

# NPM:
npm i vue-functional-data-merge

# Yarn:
yarn add vue-functional-data-merge

Now import and use it in your functional component declaration:

// MyFunctionalComponent.js

// ESM
import { mergeData } from "vue-functional-data-merge";
// Common JS
const { mergeData } = require("vue-functional-data-merge/dist/lib.common.js");

export default {
  name: "my-functional-component",
  functional: true,
  props: ["foo", "bar", "baz"],
  render(h, { props, data, children }) {
    const componentData = {
      staticClass: "fn-component", // concatenates all static classes
      class: {
        // object|Array|string all get merged and preserved
        active: props.foo,
        "special-class": props.bar,
      },
      attrs: {
        id: "my-functional-component", // now overrides any id placed on the component
      },
      on: {
        // Event handlers are merged to an array of handlers at each event.
        // The last data object passed to `mergeData` will have it's event handlers called first.
        // Right-most arguments are prepended to event handler array.
        click(e) {
          alert(props.baz);
        },
      },
    };

    return h("div", mergeData(data, componentData), children);
  },
};

Why do I need this util?

When writing functional Vue components, the render function receives a context.data object (see vue docs). This object that contains the entire data object passed to the component (the shape of which can be found here). In order to write flexible components, the data object used to create the component must be merged with the data received. If not, only the properties defined by the component will be rendered.

Consider this example:

// MyBtn.js
export default {
  name: "my-btn",
  props: ["variant"],
  functional: true,
  render(h, { props, children }) {
    return h(
      "button",
      {
        staticClass: "btn",
        class: [`btn-${props.variant}`],
        attrs: { type: "button" },
      },
      children
    );
  },
};

This exports a functional button component that applies a base .btn class and a .btn-<variant> class based on the variant prop passed to the component. It's just a simple wrapper around some Bootstrap styling to make repetitive usage simpler. Usage would look like this:

<template>
  <form>
    <input type="text" placeholder="Name" required />
    <input type="email" placeholder="email" required />
    <my-btn
      variant="primary"
      type="submit"
      id="form-submit-btn"
      @click="onClick"
      >Submit</my-btn
    >
  </form>
</template>

We've used our Bootstrap button component in a form and conveniently applied the primary variant, but we also wanted to change the button type from button to submit, give it an id, and attach a click handler. This won't work because we haven't passed the attributes, listeners, etc. to the create element call in the component's render function.

To fix this, we might extract out props, merge listeners/attributes, etc. This works well, but gets verbose fast when attempting to support all dom attributes, event listeners, etc. One might think to simply use Object spread or Object.assign to solve this like so:

return h("button", { ...context.data, ...componentData }, children);

Now when we try to add any dom attributes, Object spread is essentially performing something like this:

Object.assign(
	{},
	{
		props: { variant: "primary" },
		attrs: { id: "form-submit-btn", type: "submit" }
		on: { click: onClick }
	},
	{
		staticClass: "btn",
		class: [`btn-${props.variant}`],
		attrs: { type: "button" },
		on: {
			click() {
				alert("Hello from MyBtn!")
			}
		}
	}
)

The component data will wipe out all the context's attrs and on handlers as Object.assign merges these properties. This is where the mergeData util can help you. It will dig into the nested properties of the context.data and apply different merge strategies for each data property. mergeData works like a nested Object.assign in that the util has a variadic argument length—you can pass any number of arguments to it, and they will all be merged from left to right (the right most arguments taking merge priority). You don't have to pass a new target object as the first argument, as the return value will always be a fresh object.

Scoped Styles

You may run into cases where you are using a functional component in another component with scoped styles. This would look something like this:

<template>
  <button class="my-class">
    <slot></slot>
  </button>
</template>
<style scoped>
  .my-class {
    text-align: center;
  }
</style>

This will generate data attributes on the component elements and the css selector.

<style>
  .my-class[data-v-f3f3eg9] {
    text-align: center;
  }
</style>

<button data-v-f3f3eg9 class="my-class">Click me!</button>

When a parent component with scoped styles makes use of a functional component, the data attribute won't be passed down automatically. Instead, you must pull this attribute out manually and add it to the VNodeData used in a render function's createElement call. Doing this requires reaching into Vue internals, which can be risky due to the private nature of the API and its potential to change. For that reason, this is not supported in this util.

However, this util can make that manual merging easier by conforming to the VNodeData shape required by mergeData and Vue itself. Here is an example of a helper function to manually extract a parent's style scope id and conditionally apply it in the functional component's render function.

const FunctionalComponent = {
  functional: true,
  render(createElement, context) {
    let { parent, data, children } = context;
    let componentData = { class: "my-class" };

    return createElement(
      "button",
      mergeData(data, getScopedStyleData(parent), componentData),
      children
    );
  },
};

/**
 * @param {Vue} parent
 * @returns {VNodeData}
 */
export function getScopedStyleData(parent) {
  let data = { attrs: {} };

  if (parent.$options._scopeId) {
    data.attrs[`data-v-${parent.$options._scopeId}`] = "";
  }

  return data;
}

vue-functional-data-merge's People

Contributors

alexsasharegan avatar dependabot[bot] avatar kaelonr avatar tmorehouse avatar wxsms avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

Forkers

avesus

vue-functional-data-merge's Issues

lib@next documentation

The documentation of the vue-3.x branch is no longer valid since it spies examples that reflect the Vue 2.x syntax.

Source map error: request failed with status 404

I get the following error message in Firefox console for a project using Laravel and VueJS:

Source map error: request failed with status 404
Resource URL : http://backend.test/js/app.js
Source Map URL : lib.esm.js.map

When I check where lib.esm.js.map is used in my project:

$ grep -rn lib.esm.js.map *

I get this answer:

public/js/app.js:95525://# sourceMappingURL=lib.esm.js.map
node_modules/vue-functional-data-merge/dist/lib.esm.js:2://# sourceMappingURL=lib.esm.js.map
node_modules/bootstrap-vue/dist/bootstrap-vue-icons.js.map:1

And when I go further to check public/js/app.js, I can see this paragraph:

/***/ "./node_modules/vue-functional-data-merge/dist/lib.esm.js":
/*!****************************************************************!*\
  !*** ./node_modules/vue-functional-data-merge/dist/lib.esm.js ***!
  \****************************************************************/
/*! exports provided: mergeData */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mergeData", function() { return a; });
var e=function(){return(e=Object.assign||function(e){for(var t,r=1,s=arguments.length;r<s;r++)for(var a in t=arguments[r])Object.prototype.hasOwnProperty.call(t,a)&&(e[a]=t[a]);return e}).apply(this,arguments)},t={kebab:/-(\w)/g,styleProp:/:(.*)/,styleList:/;(?![^(]*\))/g};function r(e,t){return t?t.toUpperCase():""}function s(e){for(var s,a={},c=0,o=e.split(t.styleList);c<o.length;c++){var n=o[c].split(t.styleProp),i=n[0],l=n[1];(i=i.trim())&&("string"==typeof l&&(l=l.trim()),a[(s=i,s.replace(t.kebab,r))]=l)}return a}function a(){for(var t,r,a={},c=arguments.length;c--;)for(var o=0,n=Object.keys(arguments[c]);o<n.length;o++)switch(t=n[o]){case"class":case"style":case"directives":if(Array.isArray(a[t])||(a[t]=[]),"style"===t){var i=void 0;i=Array.isArray(arguments[c].style)?arguments[c].style:[arguments[c].style];for(var l=0;l<i.length;l++){var y=i[l];"string"==typeof y&&(i[l]=s(y))}arguments[c].style=i}a[t]=a[t].concat(arguments[c][t]);break;case"staticClass":if(!arguments[c][t])break;void 0===a[t]&&(a[t]=""),a[t]&&(a[t]+=" "),a[t]+=arguments[c][t].trim();break;case"on":case"nativeOn":a[t]||(a[t]={});for(var p=0,f=Object.keys(arguments[c][t]||{});p<f.length;p++)r=f[p],a[t][r]?a[t][r]=[].concat(a[t][r],arguments[c][t][r]):a[t][r]=arguments[c][t][r];break;case"attrs":case"props":case"domProps":case"scopedSlots":case"staticStyle":case"hook":case"transition":a[t]||(a[t]={}),a[t]=e({},arguments[c][t],a[t]);break;case"slot":case"key":case"ref":case"tag":case"show":case"keepAlive":default:a[t]||(a[t]=arguments[c][t])}return a}
//# sourceMappingURL=lib.esm.js.map

Is this warning linked to vue-functional-data-merge ? How can I fix it ?

Vue 3 support

Since Vue 3 changed the VNode props format, this library is only compatible with Vue 2.

It would be awesome to have a new major version with Vue 3 support.

License

Hello,
can you please add license file?

Consider merging scopeId from parent to support scoped styles

Over here a bug is described which is about scoped styles not being properly applied when nesting functional components in regular components with scoped styles.

Not every scenario described there might be solvable, but the one where a functional component is nested in a scoped regular component might be.
I did some digging and it turns out the scope id is exposed in the $options object, so getting it from the parent would be as simple as:
const scope = this.$parent.$options._scopeId

With some extra logic, this could make functional components apply the correct scoped styles from the parent when a class attribute has passed overwritten as a prop and the style definition lives in the parent component's scoped stylesheet.

This might not be the intended responsibility of this library, but since it is about increasing the compatibility of functional and regular components within Vue it might be worth to have this fixed here :)

Object mutation

Hi @alexsasharegan,

in my local tests I am getting this strange behavior where the passed in objects are being mutated.
Example

const def = {on: { event }}
const def2 = {on: { event2 }}

const data = dataMerge(data, def, def2)

console.log(def) // { on: [event, event2] }

Before making some deeper tests and solutions I wanted to expose this issue and see if there is any reason behind.

Thank you.

Changelog?

Hi. thank you for the nice util 👍

Is there any changelog that I can found what's new in the releases?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.