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.