Truncate Text Easily in Your Vue.js App with Vue-Clamp
Tags: vue, webdev, javascript, tutorial
Published: 2020-01-19 01:15:51

**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:

```scss
.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:

```javascript
<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:

```javascript
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:

```javascript
<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:

```javascript
<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:

```javascript
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:

```javascript
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:

```javascript
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:

```html
<!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:

```bash
json-server --watch db.json
```

In `db.json`, change the text to:

```json
{  
  "notes": []  
}
```

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