DEV Community

Cover image for Vue Exit Intent & How to Publish on NPM | Vue
nickap
nickap

Posted on • Edited on

Vue Exit Intent & How to Publish on NPM | Vue

update:
The package is refactored, and it's now a composable instead of a component.
This guide is about the old "component version"

You can check the composable here

Vue Exit Intent

Let's create an exit intent component, and then, see how we can publish it to NPM.

We will:

  • Make use of the new <script setup> API to build our component.
  • Publish on NPM.
  • Check our package with NPM LINK

Visit the repo here.
Component file here.

First...

...Let's create our new project

npm init vue@latest
This command will install and execute create-vue
The recommended way to start a Vite-powered Vue project.

Let's name our project vue-exit-intent and select default answers (Answer No) to anything else, so we can focus on our component.
If you wish to work on this component even further you can make your choises or at least accept Prettier and ESLint as they are two necessary tools.

So you will end up with something like this:

✔ Project name: … vue-exit-intent
✔ Add TypeScript? … No
✔ Add JSX Support? … No
✔ Add Vue Router for Single Page Application development? … No
✔ Add Pinia for state management? … No
✔ Add Vitest for Unit Testing? … No
✔ Add an End-to-End Testing Solution? › No
✔ Add ESLint for code quality? … Yes
✔ Add Prettier for code formatting? … Yes
Enter fullscreen mode Exit fullscreen mode

Now,
cd vue-exit-intent to your project directory, and run:
npm install to install all the dependencies according to package.json, and then run:
npm run dev.
A simple Vue app will be exposed in your localhost. Follow the link to see it.

Now, let's create a new component with the name 'vueExitIntent.vue' under the /src/components directory.

files in vscode editor

Delete the components that are selected in the picture.
No need to have them in our app.

Let's edit our vueExitIntent.vue component to this:

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

/**
 * This is exposed to our template cause we use <script setup/>.
 * So, we can use the value of this reference in our template.
 */
const show = ref(false);

onMounted(() => {
  /**
   * onMounted we attach a mouseleave Event Listener to the documentElement
   * */
  document.documentElement.addEventListener('mouseleave', handleMouseLeave);
});

onUnmounted(() => {
  /**
   * onUnmounted this hook will run as a callback.
   * It's the perfect time to remove our Event Listener.
   */
  document.documentElement.removeEventListener('mouseleave', handleMouseLeave);
});

const handleMouseLeave = () => {
  showModal();
};

/**
 * Remember? closeModal() is available to our template.
 * No need to export it like in the first version of Composition API because of <script setup>
 * */
const closeModal = () => {
  show.value = false;
  document.body.style.overflowY = 'auto';
};

const showModal = () => {
  show.value = true;
  document.body.style.overflowY = 'hidden';
};
</script>

<template>
  <div class="exit-intent-backdrop" v-show="show">
    <div class="exit-intent-content">
      <!-- Show an X button to close the Pop-up  -->
      <button
        type="button"
        class="btn-close"
        aria-label="Close"
        @click="closeModal()"
      >
        &#x2715;
      </button>
      <!-- We use slots to show the content of the Pop-up -->
      <slot>
        <!-- For the needs of this tutorial.. -->
        <!-- Let's create a fallback content -->
        <div class="fallback-content">
          <h2 class="fallback-content-header">
            Subscribe and get a 10% discount!
          </h2>
        </div>
      </slot>
    </div>
  </div>
</template>

<style scoped>
.exit-intent-backdrop {
  display: flex;
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  background-color: rgba(0, 0, 0, 0.7);
  z-index: 999;
}
.exit-intent-content {
  position: relative;
  margin: auto;
  color: #555;
  background-color: #fefefe;
}

.btn-close {
  position: absolute;
  right: 0;
  top: 0;
  border: none;
  font-size: 20px;
  padding: 4px;
  cursor: pointer;
  font-weight: bold;
  color: #555;
  background-color: transparent;
  z-index: 999;
}

.fallback-content {
  background-color: #fefefe;
  overflow-x: auto;
  display: flex;
  flex-direction: column;
  margin: 0px;
}

.fallback-content-header {
  margin: 0;
  padding: 25px 10px;
  text-align: center;
  border-bottom: 1px solid #eee;
  color: #555;
}
</style>

Enter fullscreen mode Exit fullscreen mode

Now we also have to change the code in our src/App.vue file.
Remove the unneeded code (components we deleted in the previous step), and use our newly created component.
So you will end up to something like this:

<template>
  <vue-exit-intent></vue-exit-intent>
</template>

<script setup>
import vueExitIntent from "./components/vueExitIntent.vue";
</script>
Enter fullscreen mode Exit fullscreen mode

So simple with <script setup>.

Done! We have the basic functionality of an Exit Intent Pop-up.
You can add your content or another child component in between the opening and closing tags, this way you will make use of slots, and you will not get the fallback content
<vue-exit-intent>Your Content Here</vue-exit-intent>
or even better
<vue-exit-intent><create-and-add-a-new-component-here/></vue-exit-intent>

Take it to another level

Of course, showing the pop-up continuously in every mouseleave Event is at least annoying.

So, how to make it even better.

  • We can think of using LocalStorage to add a key when the popup is showed, and check if that key exists next time the user will visit your page.
  • Add a timestamp as the value of the LS key, so you can check next time and show the popup if more than 7-days passed from last time.
  • Add a Scroll Event Listener and show the popup if the user scrolled to let's say 40% of the page-height.

I actually added this functionality in my file, and you can grab it from here

Feel free to fork and contribute repo.

Package and Publish on NPM

We are going to build this component library so to be used as a global component.

To create our Vue 3 plugin, we need to create a new src/index.js file:

import vueExitIntent from './components/vueExitIntent.vue';

export default {
   /**
   * We need an install step
   */
  install: (app, options) => {
    /**
     * 1st argument is the name of our component.
     * 2nd argument is our actuall component we imported at the top of the file.
     */
    app.component('vueExitIntent', vueExitIntent);
  }
};

Enter fullscreen mode Exit fullscreen mode

And also, we have to make some changes on vite's configuration file. Let's define what we want to happen when vite builds:

import { fileURLToPath, URL } from 'node:url';
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { resolve } from 'path';

/* https://vitejs.dev/config/ */
export default defineConfig({
  /* Define a build Property */
  build: {
    /* We want to create a library */
    lib: {
      /**
       * 1st argument is our directory name.
       * 2nd is the file we defined the install
       * */
      entry: resolve(__dirname, 'src/index.js'),
      /* Name of our library */
      name: 'vue-exit-intent',
      /**
       * 'format' will be 'es' and 'umd'.
       * So, we will create 2 files for the user to use either with 'import' (ES Modules) or 'require' (CommonJS Format)
       */
      fileName: (format) => `vue-exit-intent.${format}.js`
    },
    /* Options for Rollup*/
    rollupOptions: {
      /* Externalize vue so its won't get bundled in our library */
      external: ['vue'],
      output: {
        globals: {
          /* Global variable to use in our .umd build */
          vue: 'Vue'
        }
      }
    }
  },
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  }
});

Enter fullscreen mode Exit fullscreen mode

Finally, let's add some configuration to our package.json:

  • Key 'files' shows where vite will export our bundled files.
  • 'main' is for the .umd export we just configured in vite.
  • 'module' is for the .es export.
  • In key 'exports' we define the entrypoints for our app.
  • 'import' and 'require' will resolve to the path in the right.
  "files": [
    "dist"
  ],
  "main": "./dist/vue-exit-intent.umd.js",
  "module": "./dist/vue-exit-intent.es.js",
  "exports": {
    ".": {
      "import": "./dist/vue-exit-intent.es.js",
      "require": "./dist/vue-exit-intent.umd.js"
    },
    "./dist/style.css": "./dist/style.css"
  },
Enter fullscreen mode Exit fullscreen mode

All done!
Run npm run build and you will have a new /dist directory with the files that vite built.
These are the files we will share through NPM.

If you are sure you have tested your app, you can login to npm running
npm login
and after you login run:
npm publish to publish your app.

and... voila our package is published

NPM LINK

I couldn't resist to not mention npm link

It is a very handy tool that creates a symlink to your global folder /lib/node_modules/ that links to the package that the npm link command executed.
You can try and run npm link while you are in your vue-exit-intent project.
Now, you can go and create a different project PROJECT2 and run npm link vue-exit-intent

Remember to unlink when you are done!

Now you will have your package vue-exit-intent installed in PROJECT2, and you can test it before publishing.
Remember that, to use your component, you need to import it in PROJECT2 main.js like this:

import { createApp } from "vue";
import App from "./App.vue";
import "./assets/main.css";


import vueExitIntent from "vue-exit-intent";
import 'vue-exit-intent/dist/style.css';


const app = createApp(App);

app.use(vueExitIntent);

app.mount("#app");
Enter fullscreen mode Exit fullscreen mode

Thanks for reading this!

Top comments (0)