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.

Categories
Vuetify

Vuetify — Intersection and Mutation Observers

Vuetify is a popular UI framework for Vue apps.

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

Intersection Observer

We can add the v-intersect directive to use the Intersection Observer API.

It’s used to detect whether an element is visible within the user’s viewport.

For example, we can use it by writing:

<template>
  <div>
    <div class="d-flex align-center text-center justify-center">
      <v-avatar
        :color="isIntersecting ? 'green' : 'red'"
        class="mr-3 swing-transition"
        size="32"
      ></v-avatar>
    </div>

    <v-responsive class="overflow-y-auto" max-height="400">
      <v-responsive height="200vh" class="d-flex align-center text-center pa-2">
        <v-card
          v-intersect="{
            handler: onIntersect,
            options: {
              threshold: [0, 0.5, 1.0]
            }
          }"
          class="mx-auto"
          max-width="336"
        >
          <v-card-title>Title</v-card-title>
          <v-card-text>
            Lorem ipsum dolor sit amet.
          </v-card-text>
        </v-card>
      </v-responsive>
    </v-responsive>
  </div>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    isIntersecting: false,
  }),

  methods: {
    onIntersect(entries, observer) {
      this.isIntersecting = entries[0].intersectionRatio >= 0.5;
    },
  },
};
</script>

We have the v-card inside the v-responsive component.

The overflow-y-auto class lets us scroll the content inside it up and down.

We have another v-responsive component inside it which is taller than the 400px of the outer v-responsive component.

Therefore, we’ll scroll whatever we have inside.

We use the v-intersect directive to let us run a callback when we scroll to check for intersection of the viewport and the card.

The threshold is the portion of the card that’s in the viewport.

The onIntersect method when these thresholds are met.

Now when we scroll, we’ll see the dot change color when the card goes in and out of the viewport.

If we’re using Vuetify with IE11, then we need to add the Intersection Observer API polyfill in order to use this directive.

Mutation Observer

The v-mutate directive uses the Mutation Observer API.

It lets us detect whether an element is updated.

For example, we can write:

<template>
  <div>
    <div>
      <div class="text-center">
        <v-btn [@click](http://twitter.com/click "Twitter profile for @click")="content = !content">Change Content</v-btn>
      </div>

<v-container>
        <v-row>
          <v-col cols="12" md="6">
            <v-card>
              <v-card-title>Card 1</v-card-title>

<div class="title text-center pb-3">Times Mutated: {{ card1 }}</div>

<v-card-text v-mutate="() => onMutate('card1')">
                <p
                  v-for="n in +content + 2"
                  :key="n"
                  :class="n === +content + 2 && 'mb-0'"
                >Phasellus nec sem in justo pellentesque facilisis.</p>
              </v-card-text>
            </v-card>
          </v-col>

<v-col cols="12" md="6">
            <v-card>
              <v-card-title>Card 2</v-card-title>

<div class="title text-center pb-3">Times Mutated: {{ card2 }}</div>

<v-card-text v-mutate.once="() => onMutate('card2')">
                <p
                  v-for="n in +content + 2"
                  :key="n"
                  :class="n === +content + 2 && 'mb-0'"
                >Suspendisse enim turpis.</p>
              </v-card-text>
            </v-card>
          </v-col>
        </v-row>
      </v-container>
    </div>
  </div>
</template>
<script>
export default {
  name: "HelloWorld",
  data: () => ({
    content: false,
    card1: 0,
    card2: 0,
  }),

  methods: {
    onMutate(card) {
      this[card]++;
    },
  },
};
</script>

to detect changes in the cards.

We have the Change Content button to toggle the content state.

In the v-card-text components, we have the v-mutate directive to let us run the onMutate method when we click on the Change Content button.

This is because when the button is clicked, the content value changes.

The card1 and card2 will increment when we click on the Change Content button.

The 2nd card has the once modifier on the v-mutate directive, so card2 will only be incremented once.

Conclusion

We can add the intersection and mutation observers on components with Vuetify directives.