Categories
Vue Vuetify

Create a Desktop App with Vue, Vuetify, and Electron

Electron is an app framework to let us build desktop apps that are based on web apps.

Vuetify lets us build a web app with Material Design.

We can use the vue-cli-plugin-electron-builder generator to build an Electron app based on Vue.js.

In this article, we’ll look at how to build a simple Electron Vue app with Vuetify and Electron.

Getting Started

We can create our Vue project by running:

npx vue create .

after going into our project folder.

We follow the instructions to create the Vue project.

Then in the project folder, we run:

vue add electron-builder

Once we did that, we add Vuetify to our Vue app by running:

vue add vuetify

Now all the boilerplate code has been added for us.

We then run:

npm run electron:serve

or

yarn electron:serve

to preview our app.

Writing the Code

Now can create our app with Vuetify.

We can create a simple app by adding the following to App.vue :

<template>
  <v-app>
    <v-app-bar app color="primary" dark>
      <div class="d-flex align-center">
        <v-img
          alt="Vuetify Logo"
          class="shrink mr-2"
          contain
          src="https://cdn.vuetifyjs.com/images/logos/vuetify-logo-dark.png"
          transition="scale-transition"
          width="40"
        />

    <span class="mr-2">App</span>
      </div>
    </v-app-bar>

    <v-main>
      <v-form v-model="valid" @submit.prevent="add">
        <v-container>
          <v-row>
            <v-col cols="12" md="6">
              <v-text-field v-model="title" :rules="rule" label="book title" required></v-text-field>
            </v-col>

            <v-col cols="12" md="6">
              <v-text-field v-model="author" :rules="rule" label="book author" required></v-text-field>
            </v-col>
          </v-row>
          <v-row>
            <v-col cols="12">
              <v-btn text type="submit" color="primary">add</v-btn>
            </v-col>
          </v-row>
        </v-container>
      </v-form>

      <v-simple-table>
        <template v-slot:default>
          <thead>
            <tr>
              <th class="text-left">title</th>
              <th class="text-left">author</th>
              <th></th>
            </tr>
          </thead>
          <tbody>
            <tr v-for="(b, i) in books" :key="b.id">
              <td>{{ b.title }}</td>
              <td>{{ b.author }}</td>
              <td>
                <v-btn text color="primary" @click="remove(i)">remove</v-btn>
              </td>
            </tr>
          </tbody>
        </template>
      </v-simple-table>
    </v-main>
  </v-app>
</template>

<script>
import { v4 as uuidv4 } from "uuid";

export default {
  name: "App",
  data: () => ({
    title: "",
    author: "",
    rule: [(v) => !!v || "required"],
    valid: false,
    books: [],
  }),
  methods: {
    add() {
      if (!this.valid) {
        return;
      }
      const { title, author } = this;
      this.books.push({
        id: uuidv4(),
        title,
        author,
      });
    },
    remove(index) {
      this.books.splice(index, 1);
    },
  },
};
</script>

We created a book app with a form and a table.

v-app-bar is the top app bar.

v-main has the main content of the app.

v-form creates the form.

The v-model attribute on the form has the form validation state.

The @submit.prevent directive listens for the submit event to be emitted.

prevent calls preventDefault implicitly.

Inside the form, we have the v-text-field to add the text field.

v-model binds to the model states.

The rules prop has the form validation rules.

The rules are defined with an array of functions with the inputted value as the parameter.

The table is created with the v-simple-table component.

And we populate the default slot with the regular table elements.

In the methods object, we have the add method to let us add entries to this.books .

We check for form data validity with the this.valid property.

A unique ID is generated for each entry with the uuid package.

We need a unique ID for the key prop in the table so the items are rendered correctly.

We install that by running:

npm i uuid

Also, we have the remove method to remove items by its index.

Now we should be able to add and remove items as we wish.

Build Our App

To build our app into an executable file, we run:

yarn electron:build

with Yarn or:

npm run electron:build

with NPM.

Conclusion

We can create our a good looking Vue desktop app with Vuetify, Vue, and the vue-cli-plugin-electron-builder code generator.

Categories
Vuetify

Vuetify — Virtual Scroller and Click Outside

Vuetify is a popular UI framework for Vue apps.

In this article, we’ll look at how to work with the Vuetify framework.

Virtual Scroller

The v-virtual-scroll component lets us display a virtual, infinite list.

It supports dynamic height and scrolling vertically.

To use it, we can write:

<template>
  <v-card class="mx-auto" max-width="400">
    <v-virtual-scroll :items="items" :item-height="50" height="300">
      <template v-slot="{ item }">
        <v-list-item>
          <v-list-item-avatar>{{ item.initials }}</v-list-item-avatar>

          <v-list-item-content>
            <v-list-item-title>{{ item.fullName }}</v-list-item-title>
          </v-list-item-content>

          <v-list-item-action>
            <v-btn depressed small>
              View User
              <v-icon color="orange darken-4" right>mdi-open-in-new</v-icon>
            </v-btn>
          </v-list-item-action>
        </v-list-item>
      </template>
    </v-virtual-scroll>
  </v-card>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    colors: ["#2196F3", "#90CAF9", "#64B5F6"],
    names: ["Oliver", "Jake", "Noah", "James", "Jack"],
    surnames: ["Smith", "Anderson", "Clark", "Wright", "Mitchell"],
  }),
  computed: {
    items() {
      const namesLength = this.names.length;
      const surnamesLength = this.surnames.length;
      const colorsLength = this.colors.length;

      return Array.from({ length: 10000 }, (k, v) => {
        const name = this.names[this.genRandomIndex(namesLength)];
        const surname = this.surnames[this.genRandomIndex(surnamesLength)];

return {
          color: this.colors[this.genRandomIndex(colorsLength)],
          fullName: `${name} ${surname}`,
          initials: `${name[0]}${surname[0]}`,
        };
      });
    },
  },
  methods: {
    genRandomIndex(length) {
      return Math.ceil(Math.random() * (length - 1));
    },
  },
};
</script>

We create an array of random names with the items computed property.

Then in the template, we use the v-virtual-scroll component to render the items with the items prop.

The item slot prop has the array entry with the item.

Click Outside

The v-click-outside directive calls a function when we click on something outside the target element.

This is used with components like v-menu and v-dialog .

For example, we can use it by writing:

<template>
  <v-list>
    <v-list-item v-click-outside="onClickOutsideStandard" @click="models.base = true">
      <v-list-item-title>Default Click Outside</v-list-item-title>

      <v-list-item-action>
        <v-icon :color="models.base ? 'green' : 'red'">mdi-record</v-icon>
      </v-list-item-action>
    </v-list-item>
    <v-list-item
      v-click-outside="{
        handler: onClickOutsideWithConditional,
        closeConditional,
      }"
      @click="models.conditional = true"
    >
      <v-list-item-title>Close Conditional</v-list-item-title>
      <v-list-item-action>
        <v-icon :color="models.conditional ? 'green' : 'red'">mdi-record</v-icon>
      </v-list-item-action>
    </v-list-item>
  </v-list>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    models: {
      base: false,
      conditional: false,
    },
  }),

  methods: {
    onClickOutsideStandard() {
      this.models.base = false;
    },
    onClickOutsideWithConditional() {
      this.models.conditional = false;
    },
    closeConditional(e) {
      return this.models.conditional;
    },
  },
};
</script>

We have the v-click-outside directive on the v-list-item to ley us toggle the dot’s color when we click outside the list item.

The closeConditional has the callback to run when we click outside the component.

Conclusion

We can add a virtual scroller and a directive to detect clicks outside an element with Vuetify.

Categories
Vuetify

Vuetify — Treeview Search and Open

Vuetify is a popular UI framework for Vue apps.

In this article, we’ll look at how to work with the Vuetify framework.

Open-All

Treeview nodes can be pre-opened on page load.

To do that, we can add the open-all prop to the v-treeview component:

<template>
  <v-treeview open-all :items="items"></v-treeview>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    items: [
      {
        id: 1,
        name: "Root",
        children: [
          { id: 2, name: "Child 1" },
          { id: 3, name: "Child 2" },
          {
            id: 4,
            name: "Child 3",
            children: [
              { id: 5, name: "Grandchild 1" },
              { id: 6, name: "Grandchild 2" },
            ],
          },
        ],
      },
    ],
  }),
};
</script>

It’ll expand all the nodes when we load the page.

Treeview Slots

Treeviews have various slots.

The prepend slot lets us add content to the left of the node.

The label has the label for the node.

And the append slot lets us add content to the right of the label.

For example, we can write:

<template>
  <v-treeview v-model="tree" :open="open" :items="items" activatable item-key="name" open-on-click>
    <template v-slot:prepend="{ item, open }">
      <v-icon v-if="item.children">{{ open ? 'mdi-folder-open' : 'mdi-folder' }}</v-icon>
      <v-icon v-else>{{ item.id }}</v-icon>
    </template>
  </v-treeview>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    tree: undefined,
    open: ["Root"],
    items: [
      {
        id: 1,
        name: "Root",
        children: [
          { id: 2, name: "Child 1" },
          { id: 3, name: "Child 2" },
          {
            id: 4,
            name: "Child 3",
            children: [
              { id: 5, name: "Grandchild 1" },
              { id: 6, name: "Grandchild 2" },
            ],
          },
        ],
      },
    ],
  }),
};
</script>

to add a prepend slot so that we can show the nodes our way.

The slot has the item property with the node object.

And the open property is a boolean to indicate whether the node is open or not.

We can use them to display our nodes differently.

Searching Nodes

We can easily filter our treeview with the search prop.

The filtering can be customized.

For example, we can write:

<template>
  <v-card class="mx-auto" max-width="500">
    <v-sheet class="pa-4 primary lighten-2">
      <v-text-field
        v-model="search"
        label="Search"
        dark
        flat
        solo-inverted
        hide-details
        clearable
        clear-icon="mdi-close-circle-outline"
      ></v-text-field>
      <v-checkbox v-model="caseSensitive" dark hide-details label="Case sensitive search"></v-checkbox>
    </v-sheet>
    <v-card-text>
      <v-treeview :items="items" :search="search" :filter="filter" :open.sync="open"></v-treeview>
    </v-card-text>
  </v-card>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    open: [1, 2],
    search: null,
    caseSensitive: false,
    items: [
      {
        id: 1,
        name: "Root",
        children: [
          { id: 2, name: "Child 1" },
          { id: 3, name: "Child 2" },
          {
            id: 4,
            name: "Child 3",
            children: [
              { id: 5, name: "Grandchild 1" },
              { id: 6, name: "Grandchild 2" },
            ],
          },
        ],
      },
    ],
  }),
  computed: {
    filter() {
      return this.caseSensitive
        ? (item, search, textKey) => item[textKey].includes(search)
        : undefined;
    },
  },
};
</script>

We have the v-text-field to let us enter our search keyword.

The search state is passed into the search prop of the v-treeview to let us filter the items.

Also, we customize the search with the filter prop.

It returns a function that lets us return the condition that we want to match with the search.

Conclusion

We can add search to treeviews and also open all the nodes when the page loads.

Categories
Vuetify

Vuetify — Scrolling and Breakpoints

Vuetify is a popular UI framework for Vue apps.

In this article, we’ll look at how to work with the Vuetify framework.

Watching Bound Element

We can watch any element’s scrolling, including the element that v-scroll is applied to.

To do that, we add the self modifier to the v-scroll directive.

For instance, we can write:

<template>
  <v-card v-scroll.self="onScroll" class="overflow-y-auto" max-height="400">
    <v-banner class="justify-center headline font-weight-light" sticky>
      Scroll Me - Method invoked
      <span class="font-weight-bold" v-text="scrollInvoked"></span>
      times
    </v-banner>

    <v-card-text>
      <div
        v-for="n in 12"
        :key="n"
        class="mb-4"
      >Lorem ipsum dolor sit amet consectetur adipisicing elit.</div>
    </v-card-text>
  </v-card>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    scrollInvoked: 0,
  }),

  methods: {
    onScroll() {
      this.scrollInvoked++;
    },
  },
};
</script>

We have the v-scroll directive with the self modifier to let us watch the scrolling of itself.

Whenever the card is scrolled, the onScroll method is run.

Touch Support

The v-touch directive lets us capture swipe gestures and apply directional callbacks.

For example, we can write:

`<template>
  <v-row
    v-touch="{
      left: () => swipe('Left'),
      right: () => swipe('Right'),
      up: () => swipe('Up'),
      down: () => swipe('Down')
    }"
    align="center"
    class="grey lighten-2"
    justify="center"
    style="height: 500px"
  >
    <v-subheader>Swipe Direction</v-subheader>
    {{ swipeDirection }}
  </v-row>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    swipeDirection: "None",
  }),

  methods: {
    swipe(direction) {
      this.swipeDirection = direction;
    },
  },
};
</script>
`

We have the v-touch directive with the value being an object with the left , right , up and down properties that have functions that run when the swipe are in those directions.

Breakpoints

We can get the breakpoints from the app with the this.$vuetify.breakpoint property.

For instance, we can write:

`<template>
  <v-row align="center" justify="center">
    <v-col cols="12">{{ imageHeight }}</v-col>
  </v-row>
</template>
<script>
export default {
  name: "HelloWorld",
  mounted() {
    console.log(this.$vuetify.breakpoint);
  },

  computed: {
    imageHeight() {
      switch (this.$vuetify.breakpoint.name) {
        case "xs":
          return "220px";
        case "sm":
          return "400px";
        case "md":
          return "500px";
        case "lg":
          return "600px";
        case "xl":
          return "800px";
      }
    },
  },
};
</script>
`

to create an imageHeight computed property which is derived from the breakpoint.

We get the breakpoint name with the name property.

And we display that in the template.

We can define our own breakpoint values.

For example, we can write:

import Vue from 'vue';
import Vuetify from 'vuetify/lib';
Vue.use(Vuetify);

export default new Vuetify({
  breakpoint: {
    thresholds: {
      xs: 340,
      sm: 540,
      md: 800,
      lg: 1280,
    },
    scrollBarWidth: 24,
  },
});

in src/plugins/vuetify.js .

We defined our breakpoints with the breakpoint property.

The keys are the name and the numbers are the min-width of the breakpoint in pixels.

Conclusion

We can watch the scrolling of a bound element and define and use breakpoints with Vuetify.

Categories
Vuetify

Vuetify — Resizing and Scrolling

Vuetify is a popular UI framework for Vue apps.

In this article, we’ll look at how to work with the Vuetify framework.

Resize Directive

We can use the v-resize directive to call a function when the window resizes.

For example, we can use it by writing:

<template>
  <v-row v-resize="onResize" align="center" justify="center">
    <v-subheader>Window Size</v-subheader>
    {{ windowSize }}
  </v-row>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    windowSize: {
      x: 0,
      y: 0,
    },
  }),

  mounted() {
    this.onResize();
  },

  methods: {
    onResize() {
      this.windowSize = { x: window.innerWidth, y: window.innerHeight };
    },
  },
};
</script>

We add the v-resize directive on the v-row so that we can run the onResize method when the window resizes.

Then we’ll see the value of the this.windowSize state on the template.

Ripple Directive

The v-ripple directive lets us show action from a user.

It can be applied to any block-level element.

For example, we can write:

<template>
  <div v-ripple class="text-center elevation-2 pa-12 headline">v-ripple</div>
</template>
<script>
export default {
  name: "HelloWorld",
};
</script>

Then when we click on the div, we’ll see a ripple effect shown because of the v-ripple directive.

Custom Ripple Color

The ripple’s color can be changed.

For instance, we can write:

<template>
  <v-list>
    <v-list-item
      v-for="color in ['primary', 'secondary', 'info', 'success', 'warning', 'error']"
      :key="color"
      v-ripple="{ class: `${color}--text` }"
    >
      <v-list-item-title>{{ color }}</v-list-item-title>
    </v-list-item>
  </v-list>
</template>
<script>
export default {
  name: "HelloWorld",
};
</script>

We have the v-ripple directive with the class option with the `${color} — text` class to set the ripple color.

Centered Ripple

The ripple effect can be centered.

We can do this with the center property:

<template>
  <div v-ripple="{ center: true }" class="text-center elevation-2 pa-12 headline">centered ripple</div>
</template>
<script>
export default {
  name: "HelloWorld",
};
</script>

Ripple in Components

The ripple prop lets us control the ripple effect.

For instance, we can write:

<template>
  <v-row class="py-12" justify="space-around">
    <v-btn color="primary">default ripple</v-btn>
    <v-btn :ripple="false" color="primary">no ripple</v-btn>
    <v-btn :ripple="{ center: true }" color="primary">centered ripple</v-btn>
    <v-btn :ripple="{ class: 'red--text' }" text>red ripple</v-btn>
  </v-row>
</template>
<script>
export default {
  name: "HelloWorld",
};
</script>

to change the ripple options with the ripple prop on the v-btn .

Scrolling Directive

The v-scroll directive lets us provide callbacks when the window, target or the element itself is scrolled.

For example, we can write:

<template>
  <div>
    <v-row justify="center" align="center">
      <v-subheader>Offset Top</v-subheader>
      {{ offsetTop }}
    </v-row>
    <v-container id="scroll-target" style="max-height: 400px" class="overflow-y-auto">
      <v-row
        v-scroll:#scroll-target="onScroll"
        align="center"
        justify="center"
        style="height: 1000px"
      ></v-row>
    </v-container>
  </div>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    offsetTop: 0,
  }),

  methods: {
    onScroll(e) {
      this.offsetTop = e.target.scrollTop;
    },
  },
};
</script>

We have the v-scroll directive with the #scroll-target argument to watch the scrolling of the element with ID scroll-target .

Then when we scroll the container, we see the latest offsetTop value displayed.

Conclusion

Vuetify provides us with various directives to watch resizing and scrolling.