Make Vuejs and Jquery Play Nice

Make VueJS and jQuery play nice

The way to make Vue play nicely with other DOM-manipulating toolkits is to completely segregate them: if you are going to use jQuery to manipulate a DOM widget, you do not also use Vue on it (and vice-versa).

A wrapper component acts as a bridge, where Vue can interact with the component and the component can manipulate its internal DOM elements using jQuery (or whatever).

jQuery selectors outside of lifecycle hooks are a bad code smell. Your validatePhoneNumber uses a selector and a DOM-manipulating call, but you are using Vue to handle keydown events. You need to handle everything on this widget with jQuery. Don't use Vue to set its class or phone_number or handle its events. Those are all DOM manipulations. As I mentioned, if you wrap it in a component, you can pass props to the component and from those props you can use jQuery to set class and phone_number.

Is there a nice way to wrap a JQuery based widget into a module that can be easily used in Vue.js?

As far as creating a portable and cross-framework library is concerned, I would think of jQuery as simply a dependency that allows you create certain elements and perform certain tasks, which you would intercept and/or modify according to the target framework's requirements. So, you are essentially creating a wrapper component around it, as the top 3 JavaScript frameworks (React, Vue, Angular) today are component-based.

One of the key differences (simply put) is: Reactivity system vs. DOM manipulation.

Now, talking about porting a jQuery plugin to Vue — I'm no expert in both libraries but coming from jQuery myself, I'd say it could be as easy as keeping a reference to a widget/plugin instance on a Vue component internal data and/or props and having it optionally expose the corresponding methods. The reason for the methods exposure part being optional is the same reason that characterizes one library from the other—Vue being more versatile as it scales between both a library and a framework.

In jQuery, you would create an instance of an object and pass it around for its public methods usages; whereas in Vue, you don't explicitly create instances except for the root one (you could, but you typically won't have to)—because the component itself is the (internally constructed) instance. And it is the responsibility of a component to maintain its states and data; the sibling and/or parent components will typically have no direct access to them.

Vue and jQuery are similar in that they both support state/data synchronization. With jQuery, it's obvious since all references are in the global scope; with Vue, one would use either v-model or the .sync modifier (replaced with arguments on v-model in Vue 3). Additionally, they also have event subscription with slightly different approaches.

Let's take the jQuery Autocomplete widget and add some Vue support to it. We'll be focusing on 3 things (Options, Events and Methods) and take 3 of their respective items as an example and comparison. I cannot cover everything here, but this should give you some basic ideas.

Setting up: jQuery

For the sake of complying with your specification in question, let's assume this widget/plugin is a new-able class in the window scope.

In jQuery, you would write the following (on document ready or wrapped in IIFE before the closing <body> tag):

var autocomplete = new Autocomplete({
source: [
'vue',
'react',
'angular',
'jquery'
],
appendTo: '#autocomplete-container',
disabled: false,

change: function(event, ui) { },
focus: function(event, ui) { },
select: function(event, ui) { }
});

// And then some other place needing manual triggers on this instance
autocomplete.close();

var isDisabled = autocomplete.option('disabled');

autocomplete.search('ue'); // Matches 'vue' and 'jquery' ;)

With the target element pre-defined or dynamically created somewhere in the parent scope:

<input type="search" class="my-autocomplete" />

Porting to Vue

Since you didn't mention any specific version of Vue in use, I'm going to assume the Macross (latest stable version: 2.6.12, ATTOW) with ES module; otherwise, try the ES modules compatible build.

And for this particular use case in Vue, we want to instantiate this plugin in the mounted hook, because this is where our target element will have been created and available to literally build upon. Learn more on the Lifecycle Hooks in a diagram here.

Creating component: Autocomplete.vue

<template>
<!--
Notice how this `input` element is added right here rather than we requiring
the parent component to add one, because it's now part of the component. :)
-->
<input type="search" class="my-autocomplete" />
</template>

<script>
export default {
// Basically, this is where you define IMMUTABLE "options", so to speak.
props: {
source: {
type: Array,
default: () => []
},

disabled: {
type: Boolean,
default: false
}
},

// And this is where to prepare and/or specify the internal options of a component.
data: () => ({
instance: null
}),

mounted() {
// `this` here refers to the local Vue instance

this.instance = new Autocomplete({
source: this.source,
disabled: this.disabled,
appendTo: this.$el // Refers to the `input` element on the template,

change: (event, ui) => {
// You can optionally pass anything in the second argument
this.$emit('change', this.instance);
},

focus: (event, ui) => {
this.$emit('focus', this.instance, event);
},

select: (event, ui) => {
this.$emit('select', this, event, ui);
}
});
},

methods: {
close() {
this.instance.autocomplete('close');
},

getOption(optionName) {
return this.instance.autocomplete('option', optionName);
},

search(keyword) {
this.instance.autocomplete('search', keyword);
}
}
}
</script>

Using the component: Parent.vue (or whatever)

<template>
<div class="parent">
<autocomplete
ref="autocomplete"
:source="items"
:disabled="disabled"
@change="onChange"
@focus="onFocus"
@select="onSelect">
</autocomplete>
</div>
</template>

<script>
import Autocomplete from 'path/to/your-components/Autocomplete.vue';

export default {
data: () => ({
items: [
'vue',
'react',
'angular',
'jquery'
],
disabled: false
}),

methods: {
onChange() {
},

onFocus() {
},

onSelect() {
}
},

mounted() {
// Manually invoke a public method as soon as the component is ready
this.$refs.autocomplete.search('ue');
},

components: {
Autocomplete
}
}
</script>

And we're not there just yet! I purposefully left out the "two-way binding" portion of the above example for us to take a closer look at now. However, this step is optional and should only be done if you need to synchronize data/state between the components (parent ↔ child), for example: You have some logic on the component that sets the input's border color to red when certain values get entered. Now, since you are modifying the parent state (say invalid or error) bound to this component as a prop, you need inform them of its changes by $emit-ting the new value.

So, let's make the following changes (on the same Autocomplete.vue component, with everything else omitted for brevity):

{
model: {
prop: 'source',
event: 'modified' // Custom event name
},

async created() {
// An example of fetching remote data and updating the `source` property.
const newSource = await axios.post('api/fetch-data').then(res => res.data);

// Once fetched, update the jQuery-wrapped autocomplete
this.instance.autocomplete('option', 'source', newSource);

// and tell the parent that it has changed
this.$emit('modified', newSource);
},

watch: {
source(newData, oldData) {
this.instance.autocomplete('option', 'source', newData);
}
}
}

We're basically watch-ing "eagerly" for data changes. If preferred, you could do it lazily with the $watch instance method.

Required changes on the parent side:

<template>
<div class="parent">
<autocomplete
ref="autocomplete"
v-model="items"
:disabled="disabled"
@change="onChange"
@focus="onFocus"
@select="onSelect">
</autocomplete>
</div>
</template>

That's going to enable the aforementioned two-way binding. You could do the same with the rest of the props that you need be "reactive", like the disabled prop in this example—only this time you would use .sync modifier; because in Vue 2, multiple v-model isn't supported. (If you haven't got too far though, I'd suggest going for Vue 3 all the way ).

Finally, there are some caveats and common gotchas that you might want to look out for:

  • Since Vue performs DOM updates asynchronously, it could be processing something that won't take effect until the next event loop "tick", read more on Async Update Queue.
  • Due to limitations in JavaScript, there are types of changes that Vue cannot detect. However, there are ways to circumvent them to preserve reactivity.
  • The this object being undefined, null or in unexpected instance when referenced within a nested method or external function. Go to the docs and search for "arrow function" for complete explanation and how to avoid running into this issue.

And we've created ourselves a Vue-ported version of jQuery Autocomplete! And again, those are just some basic ideas to get you started.

Live Demo

const Autocomplete = Vue.extend({
template: `
<div class="autocomplete-wrapper">
<p>{{label}}</p>
<input type="search" class="my-autocomplete" />
</div>
`,

props: {
source: {
type: Array,
default: () => []
},

disabled: {
type: Boolean,
default: false
},

label: {
type: String
}
},

model: {
prop: 'source',
event: 'modified'
},

data: () => ({
instance: null
}),

mounted() {
const el = this.$el.querySelector('input.my-autocomplete');

this.instance = $(el).autocomplete({
source: this.source,
disabled: this.disabled,

change: (event, ui) => {
// You can optionally pass anything in the second argument
this.$emit('change', this.instance);
},

focus: (event, ui) => {
this.$emit('focus', this.instance, event);
},

select: (event, ui) => {
this.$emit('select', this, event, ui);
}
});
},

methods: {
close() {
this.instance.autocomplete('close');
},

getOption(optionName) {
return this.instance.autocomplete('option', optionName);
},

search(keyword) {
this.instance.autocomplete('search', keyword);
},

disable(toState) {
this.instance.autocomplete('option', 'disabled', toState);
}
},

watch: {
source(newData, oldData) {
this.instance.autocomplete('option', 'source', newData);
},

disabled(newState, oldState) {
this.disable(newState);
}
}
});

new Vue({
el: '#app',

data: () => ({
items: [
'vue',
'react',
'angular',
'jquery'
],
disabled: false
}),

computed: {
computedItems: {
get() {
return this.items.join(', ');
},
set(val) {
this.items = val.split(', ')
}
}
},

methods: {
onChange() {
// Do something
},

onFocus() {},

onSelect(instance, event, ui) {
console.log(`You selected: "${ui.item.value}"`);
}
},

components: {
Autocomplete
}
})
#app {
display: flex;
justify-content: space-between;
}

#app > div {
flex: 0 0 50%;
}
<link rel="stylesheet" href="//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css" />
<link rel="stylesheet" href="/resources/demos/style.css" />

<script src="https://vuejs.org/js/vue.min.js"></script>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<script src="https://code.jquery.com/ui/1.12.1/jquery-ui.js"></script>

<div id="app">
<autocomplete
v-model="items"
:disabled="disabled"
label='Type something (e.g. "ue")'
@change="onChange"
@focus="onFocus"
@select="onSelect">
</autocomplete>

<div>
<p>Edit this comma-separated list of items and see them reflected on the component</p>

<textarea
v-model.lazy="computedItems"
cols="30"
rows="3">
</textarea>
</div>
</div>

Vue.js removes jQuery event handlers

The event handlers are lost because Vue replaces the elements that they're bound to. You can use $.holdReady to delay the $(document).ready until the component is mounted.

$.holdReady(true)

document.addEventListener("DOMContentLoaded", () => {
new Vue({
el: "#app-body",
mounted() {
$.holdReady(false)
}
})
});

How do I prevent vue.js from overwriting jQuery bindings in components?

When you create a component, Vue.js compiles your template (inline or not) and insert it into the document (lifecycle).

Hence, you should attach the jQuery events after the root instance of Vue or reinitiate them into the ready function, because the component will change the DOM.

Examples

Into the ready function

After the root instance



Related Topics



Leave a reply



Submit