DEV Community

Cover image for Shopify theme + Vuejs + Custom Elements: Part 2
Alireza Jahandideh
Alireza Jahandideh

Posted on

Shopify theme + Vuejs + Custom Elements: Part 2

Continuing on the idea presented at part 1 on this series, in this post, I am going to extend on that. I am going to describe the theme directory structure and how it builds into a Shopify theme.

Repository: https://github.com/Youhan/shopify-vuejs-theme

Directory structure



.
├── dist
└── src
    ├── assets
    ├── config
    ├── layout
    ├── locales
    ├── scripts
    │   ├── account.js
    │   ├── cart.js
    │   ├── collection.js
    │   ├── home.js
    │   ├── layout.js
    │   ├── product.js
    │   └── search.js
    ├── sections
    ├── snippets
    ├── styles
    ├── templates
    └── vue
        ├── components
        │   ├── custom-element
        │   └── global
        ├── entry
        │   ├── account
        │   │   ├── components
        │   │   └── custom-elements
        │   ├── cart
        │   │   ├── components
        │   │   └── custom-elements
        │   ├── collection
        │   │   ├── components
        │   │   └── custom-elements
        │   ├── home
        │   │   ├── components
        │   │   └── custom-elements
        │   ├── layout
        │   │   ├── components
        │   │   └── custom-elements
        │   ├── product
        │   │   ├── components
        │   │   └── custom-elements
        │   └── search
        │       ├── components
        │       └── custom-elements
        ├── filters
        ├── plugins
        ├── store
        └── utils


Enter fullscreen mode Exit fullscreen mode

assets, config, layout, locales, sections, snippets, templates directories need to be copied directly to the dist folder as they are standard Shopify directories. We use styles to store our CSS files and scripts for our JavaScript files. vue folder contains the Vue apps.

For each Shopify template file, we may need to build a javascript file that brings us the Webpack.

Webpack Setup

What we need to is consider all .js files in the scripts directory as an entry point and output the built file in src/assets/ directory. getEntries function accepts a path and returns an array of entry names.



const webpackJS = {
  entry: getEntries("src/scripts/*.js"),
  output: {
    path: path.join(__dirname, "src/assets"),
    filename: "[name].js",
  },
};


Enter fullscreen mode Exit fullscreen mode

Then we need a rule for .vue files and .js files. The below rule will find all .vue files and loads them using vue-loader plugin.




{
    test: /\.vue$/,
    loader: "vue-loader",
    include: [
        path.resolve(__dirname, "src"),
        // any other package that we need to build
}


Enter fullscreen mode Exit fullscreen mode

For JavaScript files, we add a rule to build them using babel



{
    test: /\.js$/,
    use: {
        loader: "babel-loader"
    },
    exclude: /node_modules/
},


Enter fullscreen mode Exit fullscreen mode

Then we include the vue-loader and extract CSS plugins.



plugins: [
  new VueLoaderPlugin(),

  new MiniCssExtractPlugin({
    filename: "[name].css",
  }),
];


Enter fullscreen mode Exit fullscreen mode

The complete file can be found here. webpack.config.js

Vue

vue/components contains the global components and global custom elements. For each entry point, we can add a directory that will contain all private components and private custom elements to itself. And it also contains an index.js to create and register custom elements using Vue.

Example Custom elements using Vuex store

Let's create two components.

  • an add to cart button
  • a cart counter in the header

We also need to keep the count of cart items in a persistent place so that it would not reset when you navigate to another page. In the image below you can see whenever we click on the add to cart button, the window.localStorage API is called to persist the value.

Shopify vue vuex

Vue Entry

First, we include the src/vue/entry/layout/index.js in src/scripts/layout.js file



// load vue
import "@vue/entry/layout/index.js";


Enter fullscreen mode Exit fullscreen mode

The src/vue/entry/layout/index.js file will look like below:



import Vue from "vue";
import Vuex from "vuex";
import store from "@vue/store";
import "document-register-element";

/**
 * import a list of custom elements / web components
 * =================================================================*/
import customElements from "./custom-elements/index.js";

/**
 * import all needed vue components as global components
 * =================================================================*/
import "./components/index.js";

/**
 * Setup Vuex
 * =================================================================*/
Vue.use(Vuex);
const vuexStore = new Vuex.Store(store);

/**
 * Register Custom Elements
 * =================================================================*/
Object.entries(customElements).forEach((component) => {
  const [name, module] = component;
  module.store = vuexStore;
  Vue.customElement(name, module);
  Vue.config.ignoredElements = [name];
});


Enter fullscreen mode Exit fullscreen mode

Vue Components

To include all regular vue components we need to include all global components that will be share across all entry points. These components are mainly layout related components (if any).

In the src/vue/entry/layout/components/index.js we include global and private components



import Vue from "vue";

/**
 * Register global components
 * =================================================================*/
const requireGlobalComponent = require.context(
  "../../../components/global/",
  true,
  /\.vue$/
);
RegisterComponents(requireGlobalComponent);

/**
 * Register local components
 * =================================================================*/
const requireComponent = require.context(".", true, /\.vue$/);
RegisterComponents(requireComponent);


Enter fullscreen mode Exit fullscreen mode

The RegisterComponents function is just looping over what is passed by require.context() and registers them using Vue.component()



import { upperFirst, camelCase } from "@vue/utils/Helpers.js";

function RegisterComponents(requireComponents) {
  requireComponents.keys().forEach((fileName) => {
    // get component config
    const componentConfig = requireComponents(fileName);
    // get pascal-case name of the component
    const componentName = upperFirst(
      camelCase(fileName.replace(/^\.\//, "").replace(/\.\w+$/, ""))
    );
    // register the component Globally
    Vue.component(componentName, componentConfig.default || componentConfig);
  });
}


Enter fullscreen mode Exit fullscreen mode

Vue Custom Elements

Now that we have all Vue components registered, let's see how we register the custom elements.

We have two custom elements that we want to use in our Liquid files.

  • add to cart button
  • cart counter (in the header)

Inside src/vue/entry/layout/custom-elements/index.js file, we import the globally available custom elements as a list which is exported by vue/components/layout.js



// Layout specific
import layoutElements from "@vue/components/layout.js";

export default {
  ...layoutElements,
  // any local custom element here
};


Enter fullscreen mode Exit fullscreen mode

The vue/components/layout.js file itself is just a list of imports, like so:



import ExampleAddToCart from "@vue/components/custom-element/ExampleAddToCart.vue";
import ExampleCartCounter from "@vue/components/custom-element/ExampleCartCounter.vue";

export default {
  "theme-add-to-cart": ExampleAddToCart,
  "theme-cart-counter": ExampleCartCounter,
};


Enter fullscreen mode Exit fullscreen mode

In this case we don't have any local custom element, so it is just to import the global (layout) custom elements.

At this point our 2 custom elements can be used in Liquid files. Let's see how they look like

Add to cart button



<template>
  <div class="flex flex-col items-center justify-center">
    <h2 class="font-heading text-lg mb-4">Example Add to cart Button</h2>
    <button
      class="bg-brand-500 text-white px-4 py-2 rounded hover:bg-brand-700 transition duration-200"
      v-on:click="addOne"
    >
      Click to simulate Add to cart
    </button>
    <p class="mt-4">You have {{ count }} items in your cart.</p>
    <p class="mt-4">You can also reload this page or navigate to other pages</p>
  </div>
</template>

<script>
  import { mapMutations, mapState } from "vuex";
  export default {
    computed: {
      ...mapState("cart", ["count"]),
    },
    methods: {
      ...mapMutations("cart", ["addOne"]),
    },
  };
</script>


Enter fullscreen mode Exit fullscreen mode

Here we are using mapMutations to provide this component with a way to mutate the store state and mapState to get the state.

Cart Counter

This component is just displaying the state.



<template>
<div>({{ count }})</div>
</template>

<script>
import { mapState } from "vuex";

export default {
computed: {
...mapState("cart", ["count"]),
},
};
</script>

Enter fullscreen mode Exit fullscreen mode




Summary

You can find the complete code I put on https://github.com/Youhan/shopify-vuejs-theme

  • for each Shopify template file we build a Javascript file
  • each Javascript file can/may include Vue custom elements
  • each Webpack entry point is responsible to bundle regular js files and also can include a number of custom elements.
  • some custom elements can be shared as global custom elements
  • other custom elements are local to each entry point and are only bundled in one of the js files.

Originally published at my personal blog

Top comments (0)