DEV Community

John Au-Yeung
John Au-Yeung

Posted on

Truncate Text Easily in Your Vue.js App with Vue-Clamp

Subscribe to my email list now at http://jauyeung.net/subscribe/

Follow me on Twitter at https://twitter.com/AuMayeung

Many more articles at https://medium.com/@hohanga

Long text often needs to be truncated to fit on the browser window. You can do that with CSS or JavaScript. However, there’s no quick solution with CSS. With CSS, you have to do something like:

.truncate {  
  width: 500px;  
  white-space: nowrap;  
  overflow: hidden;  
  text-overflow: ellipsis;  
}

This does not let you control how many lines of text you want to show, so we need a better solution. The Vue-Clamp package lets us truncate text to display the number of lines that we want. It updates automatically when you resize the window so you always get the number of lines displayed that you specify.

In this article, we will make a note-taking app that lets users write notes, save them, and delete them. In the home page, we will use Vue-Clamp to truncate the text to only display the first 3 lines. There will be an edit form where the full text is displayed.

We start by creating the new project. To start, we run Vue CLI to create the project files. We run npx @vue/cli create note-app to start the wizard. Then in the wizard, we select ‘Manually select features’, and select Babel, CSS preprocessor, Vuex, and Vue Router.

Next, we install some packages. We need Axios to make HTTP requests to our back end, Bootstrap-Vue for styling, Vee-Validate for form validation, and Vue-Clamp for the text truncation. To install the packages, we run npm i axios bootstrap-vue vee-validate vue-clamp. After installing the packages we can start building our note-taking app.

First, we create our form for letting users take notes. In the components folder, create a file called NoteForm.vue and add:

<template>
  <ValidationObserver ref="observer" v-slot="{ invalid }">
    <b-form @submit.prevent="onSubmit" novalidate>
      <b-form-group label="Name">
        <ValidationProvider name="name" rules="required" v-slot="{ errors }">
          <b-form-input
            type="text"
            :state="errors.length == 0"
            v-model="form.name"
            required
            placeholder="Name"
            name="name"
          ></b-form-input>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
       <b-form-group label="Note">
        <ValidationProvider name="note" rules="required" v-slot="{ errors }">
          <b-form-textarea
            :state="errors.length == 0"
            v-model="form.note"
            required
            placeholder="Note"
            name="note"
            rows="10"
          ></b-form-textarea>
          <b-form-invalid-feedback :state="errors.length == 0">{{errors.join('. ')}}</b-form-invalid-feedback>
        </ValidationProvider>
      </b-form-group>
      <b-button type="submit" variant="primary">Save</b-button>
    </b-form>
  </ValidationObserver>
</template>
<script>
import { requestsMixin } from "@/mixins/requestsMixin";
export default {
  name: "NoteForm",
  props: {
    note: Object,
    edit: Boolean
  },
  mixins: [requestsMixin],
  methods: {
    async onSubmit() {
      const isValid = await this.$refs.observer.validate();
      if (!isValid) {
        return;
      }
      if (this.edit) {
        await this.editNote(this.form);
      } else {
        await this.addNote(this.form);
      }
      const { data } = await this.getNotes();
      this.$store.commit("setNotes", data);
      this.$emit("saved");
    },
    cancel() {
      this.$emit("cancelled");
    }
  },
  data() {
    return {
      form: {}
    };
  },
  watch: {
    note: {
      handler(val) {
        this.form = JSON.parse(JSON.stringify(val || {}));
      },
      deep: true,
      immediate: true
    }
  }
};
</script>

This form lets users search for dishes with the given keyword, then return a list of ingredients for the dishes and then the user can add them to a list with the duplicates removed. We use Vee-Validate to validate our inputs. We use the ValidationObserver component to watch for the validity of the form inside the component and ValidationProvider to check for the validation rule of the inputted value of the input inside the component. Inside the ValidationProvider , we have our BootstrapVue input for the text input fields. In the b-form-input components. We also add Vee-Validate validation to make sure that users have filled out the date before submitting. We make the name and note field required in the rules prop so that users will have to enter both to save the note.

We validate the values in the onSubmit function by running this.$refs.observer.validate() . If that resolves to true , then we run the code to save the data by calling the functions in the if block, then we call getNotes to get the notes. These functions are from the requestsMixin that we will add. The obtained data are stored in our Vuex store by calling this.$store.commit.

In this component, we also have a watch block to watch the note value, which is obtained from the Vuex store that we have to build. We get the latest list of ingredients as the note value is updated so that the latest can be edited by the user as we copy the values to this.form.

Next, we create a mixins folder and add requestsMixin.js into the mixins folder. In the file, we add:

const APIURL = "http://localhost:3000";
const axios = require("axios");
export const requestsMixin = {
  methods: {
    getNotes() {
      return axios.get(`${APIURL}/notes`);
    },
    addNote(data) {
      return axios.post(`${APIURL}/notes`, data);
    },
    editNote(data) {
      return axios.put(`${APIURL}/notes/${data.id}`, data);
    },
    deleteNote(id) {
      return axios.delete(`${APIURL}/notes/${id}`);
    }
  }
};

These are the functions we use in our components to make HTTP requests to our back end to save the notes.

Next in Home.vue , replace the existing code with:

<template>
  <div class="page">
    <b-button-toolbar>
      <b-button @click="openAddModal()">Add Note</b-button>
    </b-button-toolbar>
    <br />
    <b-card v-for="(n, i) in notes" :key="i" :title="n.name">
      <b-card-text class="note">
        <v-clamp autoresize :max-lines="3">{{n.note}}</v-clamp>
      </b-card-text>
      <b-button variant="primary" @click="openEditModal(n)">Edit</b-button>
      <b-button variant="warning" @click="deleteOneNote(n.id)">Delete</b-button>
    </b-card>
    <b-modal id="add-modal" title="Add Note" hide-footer>
      <NoteForm @saved="closeModal()" @cancelled="closeModal()" :edit="false"></NoteForm>
    </b-modal>
    <b-modal id="edit-modal" title="Edit Note" hide-footer>
      <NoteForm @saved="closeModal()" @cancelled="closeModal()" :edit="true" :note="selectedNote"></NoteForm>
    </b-modal>
  </div>
</template>
<script>
// @ is an alias to /src
import NoteForm from "@/components/NoteForm.vue";
import { requestsMixin } from "@/mixins/requestsMixin";
import VClamp from "vue-clamp";
export default {
  name: "home",
  components: {
    NoteForm,
    VClamp
  },
  mixins: [requestsMixin],
  computed: {
    notes() {
      return this.$store.state.notes;
    }
  },
  beforeMount() {
    this.getAllNotes();
  },
  data() {
    return {
      selectedNote: {}
    };
  },
  methods: {
    openAddModal() {
      this.$bvModal.show("add-modal");
    },
    openEditModal(note) {
      this.$bvModal.show("edit-modal");
      this.selectedNote = note;
    },
    closeModal() {
      this.$bvModal.hide("add-modal");
      this.$bvModal.hide("edit-modal");
      this.selectedNote = {};
      this.getAllNotes();
    },
    async deleteOneNote(id) {
      await this.deleteNote(id);
      this.getAllNotes();
    },
    async getAllNotes() {
      const { data } = await this.getNotes();
      this.$store.commit("setNotes", data);
    }
  }
};
</script>
<style lang="scss" scoped>
.note {
  white-space: pre-wrap;
}
</style>

This is where we display the notes in BootstrapVue cards and have buttons to open an edit note modal or delete the note in each card. We also added an ‘Add Note’ button to open the modal to let users add a note. The notes are obtained from the back end by running the this.getAllNotes function in the beforeMount hook which stores the data in our Vuex store.

We use the v-clamp component, which is provided by the Vue-Clamp library to truncate long text to 3 lines. The autoresize prop will make it resize according to our screen size, so we never get more than 3 lines of text displayed. The openAddModal, openEditModal, closeModal open the open and close modals, and close the modal respectively. When openEditModal is called, we set the this.selectedNote variable so that we can pass it to our NoteForm .

Next in App.vue , we replace the existing code with:

<template>
  <div id="app">
    <b-navbar toggleable="lg" type="dark" variant="info">
      <b-navbar-brand to="/">Note Taker App</b-navbar-brand>
      <b-navbar-toggle target="nav-collapse"></b-navbar-toggle>
      <b-collapse id="nav-collapse" is-nav>
        <b-navbar-nav>
          <b-nav-item to="/" :active="path  == '/'">Home</b-nav-item>
        </b-navbar-nav>
      </b-collapse>
    </b-navbar>
    <router-view />
  </div>
</template>
<script>
export default {
  data() {
    return {
      path: this.$route && this.$route.path
    };
  },
  watch: {
    $route(route) {
      this.path = route.path;
    }
  }
};
</script>
<style lang="scss">
.page {
  padding: 20px;
}
button,
.btn.btn-primary {
  margin-right: 10px !important;
}
.button-toolbar {
  margin-bottom: 10px;
}
</style>

to add a Bootstrap navigation bar to the top of our pages, and a router-view to display the routes we define. This style section isn’t scoped so the styles will apply globally. In the .page selector, we add some padding to our pages. We add some padding to the buttons in the remaining style code.

Then in main.js , replace the existing code with:

import Vue from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import BootstrapVue from "bootstrap-vue";
import "bootstrap/dist/css/bootstrap.css";
import "bootstrap-vue/dist/bootstrap-vue.css";
import { ValidationProvider, extend, ValidationObserver } from "vee-validate";
import { required } from "vee-validate/dist/rules";
extend("required", required);
Vue.component("ValidationProvider", ValidationProvider);
Vue.component("ValidationObserver", ValidationObserver);
Vue.use(BootstrapVue);
Vue.config.productionTip = false;
new Vue({
  router,
  store,
  render: h => h(App)
}).$mount("#app");

We added all the libraries we need here, including BootstrapVue JavaScript and CSS and Vee-Validate components along with the required validation rule here.

In router.js we replace the existing code with:

import Vue from "vue";
import Router from "vue-router";
import Home from "./views/Home.vue";
Vue.use(Router);
export default new Router({
  mode: "history",
  base: process.env.BASE_URL,
  routes: [
    {
      path: "/",
      name: "home",
      component: Home
    }
  ]
});

to include the home page in our routes so users can see the page.

And in store.js , we replace the existing code with:

import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
  state: {
    notes: []
  },
  mutations: {
    setNotes(state, payload) {
      state.notes = payload;
    }
  },
  actions: {}
});

to add our notes state to the store so we can observe it in the computed block of NoteForm and HomePage components. We have the setNotes function to update the notes state and we use it in the components by call this.$store.commit(“setNotes”, data); like we did in NoteForm and HomePage .

Finally, in index.html , we replace the existing code with:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
    <link rel="icon" href="<%= BASE_URL %>favicon.ico" />
    <title>Note Taker App</title>
  </head>
  <body>
    <noscript>
      <strong
        >We're sorry but vue-clampy-tutorial-app doesn't work properly without
        JavaScript enabled. Please enable it to continue.</strong
      >
    </noscript>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>

to change the title of our app.

After all the hard work, we can start our app by running npm run serve.

To start the back end, we first install the json-server package by running npm i json-server. Then, go to our project folder and run:

json-server --watch db.json

In db.json, change the text to:

{  
  "notes": []  
}

So we have the notes endpoints defined in the requests.js available.

Top comments (4)

Collapse
 
ajobi profile image
Ajo Bilec

Thank you for the article John, it's been a great read!

Also, for everyone interested in this topic, I have recently built a similar library that builds on some of the frustrations / limitations me and my team have experienced with the existing clamping solutions. Might work better for some of you guys too:

npmjs.com/package/vue-snip

Collapse
 
aumayeung profile image
John Au-Yeung

Thanks for reading.

Vue-snip looks good. I think your library would also do the job well.

Collapse
 
larswww profile image
larswww

John thanks buddy I needed this !

Collapse
 
aumayeung profile image
John Au-Yeung

Thanks so much for reading!