Skip to content

Vue the Mirage from this angle!

Fire the base of Vue.js! - 2 Part Series

Despite being designed in the late 1970’s, the lightweight Mirage jet fighter is the topic of today’s article!

You might assume that I’m kidding, but I’m not!

We are going to look at Mirage from a different angle in this article. The Vue.js angle!

In the early stages of full-stack app development, everything is subject to change. The database structure is not complete, Web APIs are still under construction, and the front-end development is on-going. Building the layout, and writing all HTML/CSS can be a much faster process when you have a specific design in mind.

At some point, it's not unusual for the front-end developers to be stalled or blocked by the incomplete back-end APIs. They need to populate some dynamic sections based on data eventually coming through from the backend.

Solutions for this problem, which I’ve employed in the past, include using mock files or in-memory databases to see how things will pan out. The down side? It does come with its hiccups.

That’s why Mirage.js exists! Mirage.js is a client-side server that embeds itself into the app. It handles all of the API endpoints calls without the need for a real backend server.

This article will introduce to you Mirage.js as we'll build a Note Writer app in Vue.js.

The source code of this article can be cloned from this GitHub repo: Notes Writer.

How can Mirage.js help?

Developing an app with the backend API development lagging can bring things to a halt for the front-end team. That’s why we tend to use workarounds like reading from mock files, or even building fake in-memory services to simulate API responses. This enables us to build the front-end, display and render any dynamic data, and get a full picture of what the app looks like once it’s done.

For instance, each mock file mimics a separate database entity. You then add client-side Web services to read the mock files, and make their content available to the app. When the backend APIs are ready, you simply visit these services, and amend the code to interact with the real API endpoints instead. That’s easy, but time is wasted replacing the mock Web services with the real ones.

This approach creates extra work for the developer, and extensive time later coding the services to communicate with the real API endpoints.

Mirage.js offers a simple solution for this: an approach that requires no code changes at all when it’s time to use the real API endpoints.

With Mirage.js, you create an instance of a Server object. This object is the window through which Mirage.js interacts with your application and handles all API endpoints.

Routes and Route Handlers

Inside this Server object instance, you define a set of routes and route handlers. A route and a route handler look like this:

this.get("/notes", () => {
	return [
		{ id: "1", body: "The first note body goes here", title: "First Note" },
		{ id: "2", body: "The second note body goes here", title: "Second Note" },
		{ id: "3", body: "The third note body goes here", title: "Third Note" }
	];
})

For a moment, you feel you are writing some Node.js backend code!

This snippet defines a GET route having an endpoint of /notes. The second parameter of the get() route function defines the route handler, that is, the code that runs when the route /notes is called. The route handler returns notes data.

You can define more routes and route handlers as your app needs. For instance, you can define all CRUD routes including POSTING a new note, PATCHING an existing note, and DELETING an existing note.

Mirage.js supports all HTTP Verbs. In addition, to simulate a real API response delay, Mirage.js allows you to pass, as a third argument to the route function, a timing object that specifies the amount of delay in milliseconds to incur before returning a response to the client-side app. You can define this timing object as:

this.get("/notes", () => {
	return [
		{ id: "1", body: "The first note body goes here", title: "First Note" },
		{ id: "2", body: "The second note body goes here", title: "Second Note" },
		{ id: "3", body: "The third note body goes here", title: "Third Note" }
	];
}, { timing: 5000 })

Mirage.js Database and ORM

So far, we introduced the static route handlers. The route handler statically defines the set of data to return every time the route is requested.

What if you want the ability to add new notes? Delete existing? Edit existing? Then you can use the dynamic route handlers supported by Mirage.js.

In order to support dynamic data, Mirage.js offers an in-memory database that is accessed by the server.db object instance.

Along with the database support, Mirage.js comes with an ORM. You will spend most of your time working with the ORM instead of accessing the database directly. With the ORM, you can create, update, delete, and query for data inside the database.

For the ORM/Database integration to work, you should define your models inside the Server object. The models represent the entities you are dealing with within your application. Also, models are registered by the ORM that use them to shape the data returned from the database.

To define a model in your Server object, you:

import { Server, Model } from "miragejs";

new Server({
	models: {	
		note: Model
	},

	routes () {
		this.namespace = "api";

		this.get("/notes", schema => {
			return schema.notes.all();
		})
	}
});

You wrap all your models inside the models object. Any model you define is of type Model class.

Internally, Mirage.js stores a collection of notes. By default, it pluralizes the model name you have given. Now you can add/edit/remove notes from this collection.

Then, you refactor the route handler to make use of the schema object. The schema object is provided as the first parameter on the route handler. It represents the ORM gateway to access all the collections stored inside the database.

The schema object adds functionality to the collections. For example, it adds a set of functions that are essential to retrieve, edit, create, and delete data from the collections.

schema.notes.create();
schema.notes.all();
schema.notes.find();
schema.notes.findBy();
schema.notes.where();
schema.notes.destory();

By using the ORM API, you let Mirage.js shape and use internal serializers to format your data as JSON, or any other supported format, and make your data ready to be transmitted to the front-end app.

A second parameter that a route handler accepts is the request parameter. The request parameter represents the current HTTP Request object.

To complete this section, it’s worth mentioning the seeds() function that you can use to seed your database with some initial data:

import { Server, Model } from "miragejs";

new Server({
	models: {	
		note: Model
	},

	routes () {
		this.namespace = "api";

		this.get("/notes", schema => {
			return schema.notes.all();
		})
	},

	seeds (server) {
		server.create("note", { body: "# An h1 header", title: "# An h1 header" });
		server.create("note", { body: "## An h2 header", title: "## An h2 header" });
		server.create("note", { body: "### An h3 header", title: "### An h3 header" });
		server.create("note", { body: "#### An h4 header", title: "#### An h4 header" });
		server.create("note", { body: "##### An h5 header", title: "##### An h5 header" });
		server.create("note", { body: "###### An h6 header", title: "###### An h6 header" });
	}
});

The seeds() function accepts the server object as an input parameter. You make use of the server.create() function to seed initial data in the database.

One important aspect of Mirage.js is that it auto-generates values for the id field for each and every record in the notes collection.

The Mirage.js team has done a great job of documenting this product. You can consult the documentation website to read more about Mirage.js.

Demo

Now that we've laid down the basics of Mirage.js, let’s start using it to build a Vue.js Notes Writer app.

Create a new Vue.js app

To start, make sure you install the latest version of the Vue CLI by running this command:

npm install -g @vue/cli

The command downloads and installs the latest bits of the Vue CLI on your computer.

To verify the version of the Vue CLI installed, you can run this command:

vue --version

The command displays:

@vue/cli 4.1.1

To create a new Vue.js app, run the following command:

vue create notes-writer-app

This command triggers a set of questions to shape the features to be included in the new app. You are free to choose the options that suit your app. In my case, I’ve chosen the following:

Vue CLI v4.1.1
? Please pick a preset:
  default (babel, eslint)
❯ Manually select features

? Check the features needed for your project:
 ◉ Babel
 ◯ TypeScript
 ◯ Progressive Web App (PWA) Support
 ◯ Router
 ◉ Vuex
 ◉ CSS Pre-processors
 ◉ Linter / Formatter
❯◯ Unit Testing
 ◯ E2E Testing

? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default)
:
  Sass/SCSS (with dart-sass)
❯ Sass/SCSS (with node-sass)
  Less
  Stylus

? Pick a linter / formatter config:
  ESLint with error prevention only
  ESLint + Airbnb config
❯ ESLint + Standard config
  ESLint + Prettier

? Pick additional lint features:
❯◯ Lint on save
 ◉ Lint and fix on commit

? Where do you prefer placing config for Babel, ESLint, etc.?
  In dedicated config files
❯ In package.json

The CLI takes a few seconds to download and install all the bits. Once done, you can navigate to the newly created app folder and run the app:

cd notes-writer-app
npm run serve

Now that the Vue.js app is created, let’s install the required NPM packages for this app.

Install NPM dependencies

A few additional NPM packages are required by the Notes Writer app. To install them, follow the steps below:

npm install --save axios

This command installs the axios library to allow us to communicate with a backend API using HTTP Requests.

npm install --save lodash

This command installs the lodash library. This library gives us a handful set of JavaScript functions to perform complex tasks.

npm install --save marked

The command above installs the marked library. This library is used to convert Markdown text into HTML content. The Notes Writer app allows the user to write his/her notes using Markdown text. With the help of this library, the app shows the converted content in HTML right away.

npm install --save miragejs

The command above installs the miragejs library. This library will be used to build a client-side server to handle backend API requests.

One more dev-dependency is required since we will be using SASS is the sass library itself. You can install it by running:

npm install --save-dev sass

Those are all the NPM packages we need.

Let’s move on and introduce the Notes Writer app.

Introduce the UI

Before we start building the Notes Writer app, let me share the final app, and what it looks like when running it in a browser:

Screenshot 2019-12-07 at 10.16.31 PM

The app is split into two main sections:

  • Notes List: This section lists all the saved notes in the app.
  • Notes Editor: This section provides an editor to input the note content (Markdown or normal text). In addition, it displays a live preview of whatever text is typed inside the editor.

When the user starts typing, the Save and Clear buttons appear. The Save button is used to save the new or existing notes, while the Clear button clears the editor and disregards any text inside the editor.

The user can click a saved note in order to edit its content. When an existing note is being edited, an additional button named Delete appears to allow the user to delete an existing note.

Let’s start building this app!

Build the UI

Let’s start by building the Notes component. The template of this component is as follows:

<template>
  <div class="notes-container">
    <NotesList
      :notes="notes"
      @set-note="setNote"
      class="notes-container__list"
    ></NotesList>
    <NotesCreate
      :note="currentNote"
      @save-note="saveNote"
      @set-note="setNote"
      @delete-note="deleteNote"
      class="notes-container__create"
    ></NotesCreate>
  </div>
</template>

The template above embeds two other components:

  • NotesList component. This component receives a collection of note records as input.
  • NotesCreate component. This component receives the currently active note (being edited or viewed) as input.

The NotesList component exposes an event set-note that is emitted when the user clicks on an existing note to read or edit.

The NotesCreate component exposes a set of events:

  • save-note, emitted when the user clicks on the Save button to save a note.
  • set-note, emitted when the user starts typing a new note. The app employs a mechanism that keeps track of what the user is typing, and emits the set-note event. This event is handled by the Vuex Store to save the contents of the current note in a temporary placeholder inside the Store. In addition, this event is emitted when the user clicks on the Clear button to clear the editor.
  • delete-note emitted when the user clicks on the Delete button to delete an existing note.

The Notes component handles all the events emitted by its children components, and redirects them to the Vuex Store, where they are handled in one centralized place.

This component and the rest of the components use scoped SCSS in their templates:

<style lang="scss" scoped>
	@import "@/styles/components/notes.scss";
</style>

I’ve placed all the SCSS files inside a single /src/styles folder with the following structure:

  • components folder holding all the SCSS files for all components.
  • _variables.scss file to hold all SCSS variables used in the app
  • global.scss file to hold a reference to the _variables.scss file or any other SCSS that needs to be shared among the components style sheet files.

You may add additional subfolders in your apps depending on the need and scenario at hand.

In order to make the global.scss available to all the components in the app you need to create a new file (or modify the exising one) at the root of the project folder named vue.config.js and paste the following content inside it:

// vue.config.js
module.exports = {
  css: {
    loaderOptions: {
      sass: {
        prependData: `@import "@/styles/global.scss";`
      }
    }
  }
}

The global.scss file will be prepended to any other SCSS files in the app and will be available everywhere.

The NotesList component template is as follows:

<template>
  <div class="notes">
    <div class="notes__items">
      <Note
        v-for="(note, index) in notes"
        :key="index"
        v-bind="note"
        class="notes__items__item"
        @set-note="setNote(note)"
      ></Note>
    </div>
  </div>
</template>

This component iterates over the collection of note records and renders each note inside its own NoteItem component. As well, it handles the set-note event that is triggered by the NoteItem component.

The NoteItem component template is as follows:

<template>
  <div
    class="notes__item"
    @click.prevent="setNote"
  >
    <h4>{{ title }}</h4>
    <p class="notes__item__body">{{ body }}</p>
  </div>
</template>

The component displays the Note title and body.

The NotesCreate component template is as follows:

<template>
  <div class="editor__md">
    <div class="editor">
      <div v-if="showControls">
        <button
          class="btn btn-new-note"
          @click="saveNewNote"
        >Save</button>
        <button
          class="btn btn-clear-note"
          @click="resetNote"
        >Clear</button>
        <button
          class="btn btn-delete-note"
          v-if="showDeleteBtn"
          @click="deleteNote"
        >Delete</button>
      </div>
      <textarea
        name="markdown"
        :value="currentNote"
        @input="onNoteChanged"
        placeholder="Type your note here ..."
      ></textarea>
    </div>
    <div class="editor__compiled-md">
      <div v-html="compiledMarkdown"></div>
    </div>
  </div>
</template>

This template is a bit more involved. It starts by displaying the Control Buttons. Then it displays the textarea that’s used as an editor. Finally, it displays the compiled Markdown to the right side of the editor.

The Control buttons are shown based on the showControls computed property that’s defined as follows:

currentNote () {
	return this.note && this.note.body
},
showControls () {
  return !!this.currentNote
},

If there is a current note (whether a new one or an existing one selected) to show the Controls buttons.

As for the Delete button, it’s visibility is controlled by the showDeleteBtn button defined as follows:

showDeleteBtn () {
	return !!this.note.id
},

Show the Delete button only when an existing note is being edited.

The rest of the UI files in the app are related to styling that you can check and go through by visiting the GitHub repo of this app.

Build the Server with Mirage.js

Open the main.js file, and add the following just before the instantiation of the main Vue instance:

import { Server, Model } from 'miragejs'

/* eslint-disable no-new */
new Server({
	models: {
		note: Model
	},

	routes () {
		this.namespace = 'api'

		this.get('/notes', schema => {
		  return schema.notes.all()
		})

		this.post('/notes', (schema, request) => {
		  let attrs = JSON.parse(request.requestBody)
		  let newNote = schema.notes.create(attrs)
		  return schema.notes.find(newNote.id)
		})

		this.patch('/notes/:id', (schema, request) => {
		  let newAttrs = JSON.parse(request.requestBody)
		  let id = request.params.id
		  let note = schema.notes.find(id)

		  return note.update(newAttrs)
		})

		this.delete('/notes/:id', (schema, request) => {
		  let id = request.params.id
		  return schema.notes.find(id).destroy()
		})
	},

	seeds (server) {
		server.create('note', { body: '# An h1 header', title: '# An h1 header' })
		server.create('note', { body: '## An h2 header', title: '## An h2 header' })
		server.create('note', { body: '### An h3 header', title: '### An h3 header' })
		server.create('note', { body: '#### An h4 header', title: '#### An h4 header' })
		server.create('note', { body: '##### An h5 header', title: '##### An h5 header' })
		server.create('note', { body: '###### An h6 header', title: '###### An h6 header' })
	}
});

The server instance defines the CRUD routes and route handlers. For instance, the post() route handler parses the request.requestBody to access the payload of this post request. It then uses the ORM schema object to create a new note record. Finally, it uses the ORM schema object to find the newly created note, and returns it to the front-end app.

The server makes use of the seeds() function to initialize the database with some preliminary set of note records.

The routes() function sets the namespace to a value of api. Consequently, this means that any route URL will be suffixed with the /api URL segment. To get all notes stored in the database, you issue a GET request to /api/notes.

Now that the Mirage.js backend server is ready, let’s move on and build the Vuex Store.

Build the Vuex Store

The Store defines the following state:

state: {
	notesList: [],
	note: {}
},

It defines the notesList array to hold the list of all notes in the app, and defines the note object to hold the currently selected or newly created note.

The Store defines a set of actions:

async getNotesList ({ commit }) {
  let notes = [];

	await axios.get('/api/notes')
	.then(response => {
		notes = response.data.notes
	});

	commit('setNotesList', notes)
},

This action issues a GET request to /api/notes to retrieve all notes stored in the database. It then commits the notes into the store by calling the setNotesList mutation.

The setNotesList mutation is defined as follows:

setNotesList (state, notes) {
	state.notesList = notes
},

Another important action is the setNote action defined as:

setNote ({ commit }, { id = '', body = '' } = {}) {
	commit('setNote', { id, body })
},

This action is triggered when the user selects an existing note. Also, it is being triggered while the user is editing an existing note or creating a new note. It receives, as input, the id and body of the note. It commits the data into the store by calling the setNote mutation.

The setNote mutation is defined as:

setNote (state, { id, body }) {
	let note = {}

	if (id) {
		note = state.notesList.find(note => note.id === id)
		const newNoteBody = body || note.body

		note = { ...note, body: newNoteBody, title: newNoteBody.substring(0, 20) }
	} else if (body) {	
		note = { body, title: body.substring(0, 20) }
	}

	state.note = note
},

If the id input parameter is valid, it retrieves the existing note from the state.notesList array, and updates the content of the existing note. Otherwise, this is a new note. The code checks if the user has typed any content. If it does, it creates a new note record.

Finally, it sets the state.note to either the existing note or to the newly created note. This also covers the case in which the user clears the content of the editor, and resets the state.note object.

Another action that’s defined by the Store is the deleteNote action, which is defined as:

async deleteNote ({ commit, state }) {
	let id = (state.note && state.note.id)

	if (id) {
		let url = `/api/notes/${state.note.id}`
		await axios.delete(url)
	}

	commit('deleteNote', { id })
},

The action retrieves the note ID of the note currently stored inside the state.note object. If one exists, this means that there is an existing note that’s being edited right now. It then issues a DELETE Http Request to remove this note. Finally, it commits the results into the Store by calling the deleteNote mutation.

The deleteNote mutation is defined as follows:

deleteNote (state, { id }) {
	if (id) {
		state.notesList = state.notesList.filter(n => n.id !== id)
	}

	state.note = null
},

The deleteNote mutation filters out the deleted note from the state.notesList, and resets the existing state.note object.

The last action defined by the Store is the saveNote action that’s defined as:

async saveNote ({ commit, state }) {
	let note = {}

	let url = state.note.id ? `/api/notes/${state.note.id}` : '/api/notes'
	let method = state.note.id ? 'patch' : 'post'

	await axios({
		method,
		url,
		data: state.note
	}).then(response => {
		note = response.data.note
	})

	commit('saveNote', note)
}

This action checks if the currently edited note is an existing note or a new one. Accordingly, it sends a POST or PATCH request to the backend server to either create the new note or update an existing one. Finally, it commits the results into the Store by calling the saveNote mutation.

The saveNote mutation is defined as follows:

saveNote (state, note) {
	const notePosition = state.notesList.findIndex(n => n.id === note.id)

	if (notePosition < 0) {
		state.notesList.push(note)
	} else {
		state.notesList.splice(notePosition, 1, note)
	}

	state.note = null
}

The mutation above adds, or amends, a note inside the state.notesList to properly sync with the backend database.

Finally, the Store defines two getters to allow components to retrieve data from the state as:

getters: {
	notes: state => state.notesList,
	currentNote: state => state.note
},

That’s all that we have for the Store for the time being. Let’s see how the components make use of this Store in the next section.

Integrate the Store in the app

The components NoteItem, NotesList, and NotesCreate communicate with the Notes component via the this.$emit() event bus. And in turn, the Notes component communicates with the Store via Getters and Actions.

The Notes component defines the following code:

computed: {
	...mapGetters(['notes', 'currentNote'])
},
mounted () {
	this.getNotesList()
},
methods: {
	...mapActions(['getNotesList', 'saveNote', 'setNote', 'deleteNote'])
}

Inside the computed property, it imports the notes and currentNote Store getters. These getters are now available as computed properties inside this component. Inside the methods section, it imports the Store actions: getNotesList, saveNote, setNote, and deleteNote. These actions become methods defined in this component, and can be called like any other method defined.

The mounted lifecycle hook calls the getNotesList() action to retrieve the list of notes from the backend API, and fill the Store with the data.

<NotesCreate
	:note="currentNote"
	@save-note="saveNote"
	@set-note="setNote"
	@delete-note="deleteNote"
	class="notes-container__create"
></NotesCreate>

Notice how the events emitted by the NotesCreate component are bound directly to the methods (actions mapped to method names inside the component). When the user clicks the Save button, the action method saveNote is triggered automatically inside the Store.

The same applies to the rest of the actions in this component.

Run the app

Run the following command and start playing around with the app:

npm run serve

The app runs on the port 8080, and can be accessed by visiting the following URL: http://localhost:8080.

Start typing text inside the editor. You can use Markdown text. Notice how the compiled Markdown text is displayed on the right side of the editor. You may click the Save button to create a new note, or clear the editor. You can also click an existing note to load its content inside the editor.

Conclusion

This article demonstrates how you can continue working on the front-end side of your application while the backend is being built, especially during the early stages of development.

In future installments, I will be showing you how to integrate this app with Google Firebase to store the data, and authenticate users. You will be able to login to the app and manage your own set of notes.

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

Understanding Vue's Reactive Data cover image

Understanding Vue's Reactive Data

Introduction Web development has always been about creating dynamic experiences. One of the biggest challenges developers face is managing how data changes over time and reflecting these changes in the UI promptly and accurately. This is where Vue.js, one of the most popular JavaScript frameworks, excels with its powerful reactive data system. In this article, we dig into the heart of Vue's reactivity system. We unravel how it perfectly syncs your application UI with the underlying data state, allowing for a seamless user experience. Whether new to Vue or looking to deepen your understanding, this guide will provide a clear and concise overview of Vue's reactivity, empowering you to build more efficient and responsive Vue 3 applications. So, let’s kick off and embark on this journey to decode Vue's reactive data system. What is Vue's Reactive Data? What does it mean for data to be ”'reactive”? In essence, when data is reactive, it means that every time the data changes, all parts of the UI that rely on this data automatically update to reflect these changes. This ensures that the user is always looking at the most current state of the application. At its core, Vue's Reactive Data is like a superpower for your application data. Think of it like a mirror - whatever changes you make in your data, the user interface (UI) reflects these changes instantly, like a mirror reflecting your image. This automatic update feature is what we refer to as “reactivity”. To visualize this concept, let's use an example of a simple Vue application displaying a message on the screen: `javascript import { createApp, reactive } from 'vue'; const app = createApp({ setup() { const state = reactive({ message: 'Hello Vue!' }); return { state }; } }); app.mount('#app'); ` In this application, 'message' is a piece of data that says 'Hello Vue!'. Let's say you change this message to 'Goodbye Vue!' later in your code, like when a button is clicked. `javascript state.message = 'Goodbye Vue!'; ` With Vue's reactivity, when you change your data, the UI automatically updates to 'Goodbye Vue!' instead of 'Hello Vue!'. You don't have to write extra code to make this update happen - Vue's Reactive Data system takes care of it. How does it work? Let's keep the mirror example going. Vue's Reactive Data is the mirror that reflects your data changes in the UI. But how does this mirror know when and what to reflect? That's where Vue's underlying mechanism comes into play. Vue has a behind-the-scenes mechanism that helps it stay alerted to any changes in your data. When you create a reactive data object, Vue doesn't just leave it as it is. Instead, it sends this data object through a transformation process and wraps it up in a Proxy. Proxy objects are powerful and can detect when a property is changed, updated, or deleted. Let's use our previous example: `javascript import { createApp, reactive } from 'vue'; const app = createApp({ setup() { const state = reactive({ message: 'Hello Vue!' }); return { state }; } }); app.mount('#app'); ` Consider our “message” data as a book in a library. Vue places this book (our data) within a special book cover (the Proxy). This book cover is unique - it's embedded with a tracking device that notifies Vue every time someone reads the book (accesses the data) or annotates a page (changes the data). In our example, the reactive function creates a Proxy object that wraps around our state object. When you change the 'message': `javascript state.message = 'Goodbye Vue!'; ` The Proxy notices this (like a built-in alarm going off) and alerts Vue that something has changed. Vue then updates the UI to reflect this change. Let’s look deeper into what Vue is doing for us and how it transforms our object into a Proxy object. You don't have to worry about creating or managing the Proxy; Vue handles everything. `javascript const state = reactive({ message: 'Hello Vue!' }); // What vue is doing behind the scenes: function reactive(obj) { return new Proxy(obj, { // target = state and key = message get(target, key) { track(target, key) return target[key] }, set(target, key, value) { target[key] = value // Here Vue will trigger its reactivity system to update the DOM. trigger(target, key) } }) } ` In the example above, we encapsulate our object, in this case, “state”, converting it into a Proxy object. Note that within the second argument of the Proxy, we have two methods: a getter and a setter. The getter method is straightforward: it merely returns the value, which in this instance is “state.message” equating to 'Hello Vue!' Meanwhile, the setter method comes into play when a new value is assigned, as in the case of “state.message = ‘Hey young padawan!’”. Here, “value” becomes our new 'Hey young padawan!', prompting the property to update. This action, in turn, triggers the reactivity system, which subsequently updates the DOM. Venturing Further into the Depths If you have been paying attention to our examples above, you might have noticed that inside the Proxy` method, we call the functions `track` and `trigger` to run our reactivity. Let’s try to understand a bit more about them. You see, Vue 3 reactivity data is more about Proxy objects. Let’s create a new example: `vue import { reactive, watch, computed, effect } from "vue"; const state = reactive({ showSword: false, message: "Hey young padawn!", }); function changeMessage() { state.message = "It's dangerous to go alone! Take this."; } effect(() => { if (state.message === "It's dangerous to go alone! Take this.") { state.showSword = true; } }); {{ state.message }} Click! ` In this example, when you click on the button, the message's value changes. This change triggers the effect function to run, as it's actively listening for any changes in its dependencies__. How does the effect` property know when to be called? Vue 3 has three main functions to run our reactivity: effect`, `track`, and `trigger`. The effect` function is like our supervisor. It steps in and takes action when our data changes – similar to our effect method, we will dive in more later. Next, we have the track` function. It notes down all the important data we need to keep an eye on. In our case, this data would be `state.message`. Lastly, we've got the trigger` function. This one is like our alarm bell. It alerts the `effect` function whenever our important data (the stuff `track` is keeping an eye on) changes. In this way, trigger`, `track`, and `effect` work together to keep our Vue application reacting smoothly to changes in data. Let’s go back to them: `javascript function reactive(obj) { return new Proxy(obj, { get(target, key) { // target = state & key = message track(target, key) // keep an eye for this return target[key] }, set(target, key, value) { target[key] = value trigger(target, key) // trigger the effects! } }) } ` Tracking (Dependency Collection) Tracking is the process of registering dependencies between reactive objects and the effects that depend on them. When a reactive property is read, it's "tracked" as a dependency of the current running effect. When we execute track()`, we essentially store our effects in a Set object. But what exactly is an "effect"? If we revisit our previous example, we see that the effect method must be run whenever any property changes. This action — running the effect method in response to property changes — is what we refer to as an "Effect"! (computed property, watcher, etc.) > Note: We'll outline a basic, high-level overview of what might happen under the hood. Please note that the actual implementation is more complex and optimized, but this should give you an idea of how it works. Let’s see how it works! In our example, we have the following reactive object: `javascript const state = reactive({ showSword: false, message: "Hey young padawn!", }); // which is transformed under the hood to: function reactive(obj) { return new Proxy(obj, { get(target, key) { // target = state | key = message track(target, key) // keep an eye for this return target[key] }, set(target, key, value) { target[key] = value trigger(target, key) // trigger the effects! } }) } ` We need a way to reference the reactive object with its effects. For that, we use a WeakMap. Which type is going to look something like this: `typescript WeakMap>> ` We are using a WeakMap to set our object state as the target (or key). In the Vue code, they call this object `targetMap`. Within this targetMap` object, our value is an object named `depMap` of Map type. Here, the keys represent our properties (in our case, that would be `message` and `showSword`), and the values correspond to their effects – remember, they are stored in a Set that in Vue 3 we refer to as `dep`. Huh… It might seem a bit complex, right? Let's make it more straightforward with a visual example: With the above explained, let’s see what this Track` method kind of looks like and how it uses this `targetMap`. This method essentially is doing something like this: `javascript let activeEffect; // we will see more of this later function track(target, key) { if (activeEffect) { // depsMap` maps targets to their keys and dependent effects let depsMap = targetMap.get(target); // If we don't have a depsMap for this target in our targetMap`, create one. if (!depsMap) { depsMap = new Map(); targetMap.set(target, depsMap); } let dep = depsMap.get(key); if (!dep) { // If we don't have a set of effects for this key in our depsMap`, create one. dep = new Set(); depsMap.set(key, dep); } // Add the current effect as a dependency dep.add(activeEffect); } } ` At this point, you have to be wondering, how does Vue 3 know what activeEffect` should run? Vue 3 keeps track of the currently running effect by using a global variable. When an effect is executed, Vue temporarily stores a reference to it in this global variable, allowing the track` function to access the currently running effect and associate it with the accessed reactive property. This global variable is called inside Vue as `activeEffect`. Vue 3 knows which effect is assigned to this global variable by wrapping the effects functions in a method that invokes the effect whenever a dependency changes. And yes, you guessed, that method is our effect` method. `javascript effect(() => { if (state.message === "It's dangerous to go alone! Take this.") { state.showSword = true; } }); ` This method behind the scenes is doing something similar to this: `javascript function effect(update) { //the function we are passing in const effectMethod = () => { // Assign the effect as our activeEffect` activeEffect = effectMethod // Runs the actual method, also triggering the get` trap inside our proxy update(); // Clean the activeEffect after our Effect has finished activeEffect = null } effectMethod() } ` The handling of activeEffect` within Vue's reactivity system is a dance of careful timing, scoping, and context preservation. Let’s go step by step on how this is working all together. When we run our `Effect` method for the first time, we call the `get` trap of the Proxy. `javascript function effect(update) const effectMethod = () => { // Storing our active effect activeEffect = effectMethod // Running the effect update() ... } ... } effect(() => { // we call the the get` trap when getting our `state.message` if (state.message === "It's dangerous to go alone! Take this.") { state.showSword = true; } }); ` When running the get` trap, we have our `activeEffect` so we can store it as a dependency. `javascript function reactive(obj) { return new Proxy(obj, { // Gets called when our effect runs get(target, key) { track(target, key) // Saves the effect return target[key] }, // ... (other handlers) }) } function track(target, key) { if (activeEffect) { //... rest of the code // Add the current effect as a dependency dep.add(activeEffect); } } ` This coordination ensures that when a reactive property is accessed within an effect, the track function knows which effect is responsible for that access. Trigger Method Our last method makes this Reactive system to be complete. The trigger` method looks up the dependencies for the given target and key and re-runs all dependent effects. `javascript function trigger(target, key) { const depsMap = targetMap.get(target); if (!depsMap) return; // no dependencies, no effects, no need to do anything const dep = depsMap.get(key); if (!dep) return; // no dependencies for this key, no need to do anything // all dependent effects to be re-run dep.forEach(effect => { effect() }); } ` Conclusion Diving into Vue 3's reactivity system has been like unlocking a hidden superpower in my web development toolkit, and honestly, I've had a blast learning about it. From the rudimentary elements of reactive data and instantaneous UI updates to the intricate details involving Proxies, track and trigger functions, and effects, Vue 3's reactivity is an impressively robust framework for building dynamic and responsive applications. In our journey through Vue 3's reactivity, we've uncovered how this framework ensures real-time and precise updates to the UI. We've delved into the use of Proxies to intercept and monitor variable changes and dissected the roles of track and trigger functions, along with the 'effect' method, in facilitating seamless UI updates. Along the way, we've also discovered how Vue ingeniously manages data dependencies through sophisticated data structures like WeakMaps and Sets, offering us a glimpse into its efficient approach to change detection and UI rendering. Whether you're just starting with Vue 3 or an experienced developer looking to level up, understanding this reactivity system is a game-changer. It doesn't just streamline the development process; it enables you to create more interactive, scalable, and maintainable applications. I love Vue 3, and mastering its reactivity system has been enlightening and fun. Thanks for reading, and as always, happy coding!...

Layouts & Theming in Vuetify 3 cover image

Layouts & Theming in Vuetify 3

Introduction Do you want to avoid tinkering with CSS to get your website looking just right? Branding is everything in today's digital landscape. With Vuetify 3's customizable theming options, you can create web projects that perfectly reflect your unique branding, and visual identity. Additionally, you can reduce the duplicated code by creating layouts representing your application's different page structures. In a previous article introducing Vuetify 3 with Vue 3, we created an application that allowed us to add, display, and delete jokes. ** If you'd like to follow along with this article, please clone the GitHub repository. `shell git clone https://github.com/thisdot/blog-demos.git or `git@github.com:thisdot/blog-demos.git` for ssh then cd into the project directory cd layouts-and-theming-in-vueityf ` In this article, we'll go over how you can create layouts in Vuetify 3 to allow you to reuse the same layout for pages that share the same structure. We will also go over how you can customize the look and feel of your application to match your brand's visual identity through the powerful theming tools that Vuetify 3 offers us. Layouts in Vuetify 3 What are layouts, and how are they useful? In web development, layouts are akin to blueprints for the structure of your web pages. They provide a consistent and reusable frame for the different pages of an application. A well-designed layout can improve the user experience by providing familiarity and predictability as the user navigates through other parts of your application. Layouts are particularly helpful in reducing code duplication. Instead of defining the same structure for each page—headers, footers, and navigation menus, you define them once in a layout, and then apply that layout to any page that uses the same structure. This speeds up development, and makes your code easier to maintain. Using Layouts in Vuetify 3 Creating a layout in Vuetify 3 is as simple as creating a new Vue component. This component will include the common elements shared across multiple pages like headers, footers, and navigation bars. Here is an example of a basic layout: `html ... ... ... ` This layout has a header, a navigation drawer, and a footer. The ` component is where the content of the specific pages will be injected. Building a layout for our application Let's create a layout for our application. We'll have a top navigation bar and a footer. Inside the layouts` directory, create a new file named `Default.vue` and add the following code: `html Joke Machine &copy; 2023 Joke Machine ` Additionally, in order for us to use our layout, we will need to modify our router/index.ts file to use the Default.vue component: `js const routes = [ { //... component: () => import('@/layouts/Default.vue'), //... } ` This will make sure that our respective pages use the Default.vue` layout that we created. The contents of the pages will appear in place of `` in the layout. It is also worth noting that you can create, and nest as many layouts as you want. Theming in Vuetify 3 Themes allow you to customize your application's default colors, surfaces, and more. If your brand has specific colors and styling, you can theme your Vuetify application to resemble your brand through theming better. Additionally, Vuetify 3 allows you to modify your theme in real-time programmatically. [Vuetify 3] also comes with light and dark themes pre-installed. Theming API Vuetify 3 offers us 2 main APIs for working with themes: - useTheme` is a composable that allows us to get information about the current theme and will enable us to modify the existing theme. - v-theme-provider` is used in the `` section of your Vue files to modify the theme of all of its children. Updating the theme of our application Updating the theme of our application is straightforward with Vuetify 3. Let's customize our application's primary and secondary colors. In the vuetify.ts file, modify the themes section to contain the following styles: `ts import { createVuetify } from 'vuetify' import 'vuetify/styles' const vuetify = createVuetify({ theme: { themes: { light: { background: '#FFFFFF', surface: '#F2F5F8', primary: '#6200EE', secondary: '#03DAC6', error: '#B00020', info: '#2196F3', success: '#4CAF50', warning: '#FB8C00', }, }, }, }) ` In this example, we're defining a custom 'light' theme. If you want to use a dark theme, or any other named theme, you can add those as additional properties within the themes` object. For example: `ts themes: { light: { / ... */ }, dark: { / ... */ }, } ` And... That's it! By changing the various config options in the vuetify.ts` file, you can modify how your application looks and feels to match your brand. If you'd like to learn more about themes and all the options you can provide, please check out the official Vuetify documentation. Conclusion In this article, we've gone through the concepts of layouts and theming in Vuetify 3. We've seen how layouts can reduce code duplication, and provide consistency across your application. We've looked at how Vuetify's theming features allow you to customize your application to match your brand's visual identity. Understanding and utilizing these features effectively can significantly enhance the development experience and the end user's interaction with your application. Remember, a well-structured and visually appealing application not only attracts users, but also retains them. Happy coding!...

Making sense of Multiple v-model Bindings in Vue 3 cover image

Making sense of Multiple v-model Bindings in Vue 3

This article is one of a series of articles on what’s new in Vue 3. If you haven’t checked that series yet, you can do so by visiting the links below: - Take your App to the Next Level with Vue 3 - Async Components in Vue 3 - Teleporting in Vue 3 - Your first Vue 3 app using TypeScript - Vue 3 Composition API, do you really need it? In this installment, I will introduce the new v-model` in Vue 3 and go through a new feature that allows you to use multiple `v-model` on the same component! By design, the v-model` directive allows us to bind an input value to the state of an app. We use it to create a two-way data binding on the form input, textarea, and select elements. It handles updates in two opposite directions: When the input value changes, it reflects the value onto the state inside the Component. When the Component state changes, it reflects the changes onto the form input elements. The core concept of v-model` remains the same in Vue 3 with more enhancements and features. Let’s dig in! Vue 2: `v-model` Vue 2 supports a single v-model` on any given Component. In Vue 2, to build a complex Component that supports two-way data binding, it utilizes a single `v-model` with one full-blown payload. The Component handles the state internally for all the input elements. It generates a single payload object representing the state of the Component. Finally, it emits an event to the parent Component with the payload attached. This method had several pitfalls, especially for creating Vue UI Libraries. Of these pitfalls is the vagueness of the payload interface. It’s unclear what’s being included in the payload. A developer had to loop through the payload object in order to uncover what properties were there. Another is the need to write the logic inside the Component to handle the internal state and the generation of the payload object. Shortly, we will uncover what has been improved in this regard with Vue 3. However, before this, let’s review some basics on how Vue 2 handles implementing two-way data binding in Components. Vue 2: Two-way Data Binding As mentioned, Vue 2 uses the v-model` directive to support two-way data binding on Components. Internally, it follows certain steps and rules in order to support the `v-model` directive. By default, the v-model` directive uses different properties and emits different events for different input elements: Text and Textarea elements use the value` property and the `input` event Checkboxes and Radio buttons use the `checked` property and the `change` event Select fields use the `input` property and the `change` event. Building a Component with a single input element will internally use something similar to the snippet below: `html ` The custom Component above defines a single prop` named `value` as follows: `js props: { value: { type: String, default: '', required: true } } ` Then, in the parent Component, you use the new custom Component as follows: `html ` The v-model` directive assumes that the `CustomComponent` defines an internal property named `value` and emits a single event named `input`. What if the CustomComponent` has to handle multiple inputs? How do we accomplish that in Vue 2? Well, there is no official solution. However, there are two methods that you can use: The CustomComponent` defines a single property named `value` of type `Object`. Internally, it parses the object into `data` fields and does the mapping manually on the template. On every change of any of the fields, it prepares a payload for all the fields and emits a single `input` event, and attaches the payload. That’s a lot of code to write for such a custom component. The other option is to skip using the v-model` directive and instead utilize individual input/event pairs. I will illustrate this in a moment. Assuming you have a custom Component to handle the user’s first name and last name, you would employ something similar: `html ` As for the properties, the Component defines the following: `js props: { input-firstname: { type: String, required: true }, input-lastname: { type: String, required: true }, } ` Finally, the parent Component uses the new component as follows: `html ` We are not using the v-model` anymore and providing multiple two-way data bindings on the new component. Further your understanding by reading the official docs on Using v-model on Components Vue 3: `v-model` In Vue 3, the v-model` directive has had an overhaul to give developers more power and flexibility when building custom components that support two-way data binding. The v-model` directive now supports new defaults. The default v-model` property is renamed to `modelValue` instead of the old name of `value`. The default v-model` event is renamed to `update:modelValue` instead of the old name of `input`. You might be thinking that's more typing when using the new v-model` directive. The Vue team are one step ahead and have given you a shorthand to use instead. Let’s rebuild the custom component using it. `html ` The custom component defines a single prop` named `modelValue` as follows: `js props: { modelValue: { type: String, default: '', required: true } } ` Then, in the parent component, use the new custom component as follows: `html ` The new v-model` directive offers the new shorthand that is used like this: `html ` The v-model` directive assumes that the `CustomComponent` defines an internal property named `modelValue` and emits a single event named `update:ModelValue`. In case you don’t want to use the default naming convention, feel free to use another name. Just remember to be consistent when naming properties. Here’s an example of using a custom name for the modelValue` property. `html ` The custom component above defines a single prop` named `modelValue` as follows: `js props: { fullName: { type: String, default: '', required: true } } ` Then, in the parent component, you use the new custom component like so: `html ` Notice the use of the property fullName` instead of the default property name. Vue 3: Multiple `v-model` directive bindings I hope the Vue 3 shorthand form of the v-model` directive has given you a "hand up". With this, the v-model` gives the flexibility to use multiple `v-model` directives on a single component instance. The `modelValue` can be renamed to whatever you want, allowing you to do this! This great new feature eliminates the previous two solutions I offered up on handling complex custom components for Vue 2. Let's jump in and go through an example demonstration! Demo - Multiple `v-model` directive bindings Let’s build a custom Address component that can be embedded in any form to collect a user’s address. > You can play with the example live on: vue3-multiple-v-model. > You can check the source code for the example on: vue3-multiple-v-model. Figure 1__ below shows the final app in action. Let’s start by building the HTML template of the new component. Figure 1 shows that all the fields used are of type input elements. Except for the last one which is a checkbox element. Therefore, it’s suffice to focus on a single input field that will eventually be replicated for the rest of fields. `html Address Line 1 ` The address-line` input field binds the `:value` directive and the `@input` event as per the new `v-model` directive specifications in Vue 3. The component defines the following property: `js props: { addressLine: { type: String, default: "" }, … // more props here ` The other fields follow the same structure and naming convention. Let’s look at the checkbox field and see how it’s defined: `html Is home address? ` In the case of a checkbox field element, we bind to the :checked` directive instead of the `:value` directive. Also, we use the `@change` event instead of the `@input` event as in the case of input field elements. The event name follows the same standard way of emitting events in the new `v-model` directive. The component defines the following property: `js props: { … // more props here homeAddress: { type: Boolean, default: false, }, ` Let’s now embed the new custom Address component into the App component: `html ` For each and every property on the custom component, we bind using the v-model:{property-name}` format. The modelValue` was replaced with the specific property names we have in hand. When there was a single input binding, the shorthand format was so much easier. However, when there are multiple input elements, the `modelValue` is in a league of its own! Now, let’s define the properties inside the App component using the new Composition API setup() function: `js setup() { const address = reactive({ name: "" eaddressLine: "", streetNumber: "", town: "", country: "", postcode: "", phoneNumber: "", homeAddress: false, }); return { address }; } ` You create a new reactive` property with an object payload. Finally, you return the reactive property to the component and use it to set bindings on the custom Address component as follows: `html v-model:addressLine="address.addressLine" ` That’s it! Conclusion Vue 3 has many new features and improvements. Today, we saw how we use multiple v-model` directives on a single component instance. There is so much more to learn and uncover in Vue 3. The coming installments of this series will continue to look at different features to help you move from Vue 2 to Vue 3. Happy Vueing!...

Nuxt DevTools v1.0: Redefining the Developer Experience Beyond Conventional Tools cover image

Nuxt DevTools v1.0: Redefining the Developer Experience Beyond Conventional Tools

In the ever-evolving world of web development, Nuxt.js has taken a monumental leap with the launch of Nuxt DevTools v1.0. More than just a set of tools, it's a game-changer—a faithful companion for developers. This groundbreaking release, available for all Nuxt projects and being defaulted from Nuxt v3.8 onwards, marks the beginning of a new era in developer tools. It's designed to simplify our development journey, offering unparalleled transparency, performance, and ease of use. Join me as we explore how Nuxt DevTools v1.0 is set to revolutionize our workflow, making development faster and more efficient than ever. What makes Nuxt DevTools so unique? Alright, let's start delving into the features that make this tool so amazing and unique. There are a lot, so buckle up! In-App DevTools The first thing that caught my attention is that breaking away from traditional browser extensions, Nuxt DevTools v1.0 is seamlessly integrated within your Nuxt app. This ensures universal compatibility across browsers and devices, offering a more stable and consistent development experience. This setup also means the tools are readily available in the app, making your work more efficient. It's a smart move from the usual browser extensions, making it a notable highlight. To use it you just need to press Shift + Option + D` (macOS) or `Shift + Alt + D` (Windows): With simple keystrokes, the Nuxt DevTools v1.0 springs to life directly within your app, ready for action. This integration eliminates the need to toggle between windows or panels, keeping your workflow streamlined and focused. The tools are not only easily accessible but also intelligently designed to enhance your productivity. Pages, Components, and Componsables View The Pages, Components, and Composables View in Nuxt DevTools v1.0 are a clear roadmap for your app. They help you understand how your app is built by simply showing its structure. It's like having a map that makes sense of your app's layout, making the complex parts of your code easier to understand. This is really helpful for new developers learning about the app and experienced developers working on big projects. Pages View lists all your app's pages, making it easier to move around and see how your site is structured. What's impressive is the live update capability. As you explore the DevTools, you can see the changes happening in real-time, giving you instant feedback on your app's behavior. Components View is like a detailed map of all the parts (components) your app uses, showing you how they connect and depend on each other. This helps you keep everything organized, especially in big projects. You can inspect components, change layouts, see their references, and filter them. By showcasing all the auto-imported composables, Nuxt DevTools provides a clear overview of the composables in use, including their source files. This feature brings much-needed clarity to managing composables within large projects. You can also see short descriptions and documentation links in some of them. Together, these features give you a clear picture of your app's layout and workings, simplifying navigation and management. Modules and Static Assets Management This aspect of the DevTools revolutionizes module management. It displays all registered modules, documentation, and repository links, making it easy to discover and install new modules from the community! This makes managing and expanding your app's capabilities more straightforward than ever. On the other hand, handling static assets like images and videos becomes a breeze. The tool allows you to preview and integrate these assets effortlessly within the DevTools environment. These features significantly enhance the ease and efficiency of managing your app's dynamic and static elements. The Runtime Config and Payload Editor The Runtime Config and Payload Editor in Nuxt DevTools make working with your app's settings and data straightforward. The Runtime Config lets you play with different configuration settings in real time, like adjusting settings on the fly and seeing the effects immediately. This is great for fine-tuning your app without guesswork. The Payload Editor is all about managing the data your app handles, especially data passed from server to client. It's like having a direct view and control over the data your app uses and displays. This tool is handy for seeing how changes in data impact your app, making it easier to understand and debug data-related issues. Open Graph Preview The Open Graph Preview in Nuxt DevTools is a feature I find incredibly handy and a real time-saver. It lets you see how your app will appear when shared on social media platforms. This tool is crucial for SEO and social media presence, as it previews the Open Graph tags (like images and descriptions) used when your app is shared. No more deploying first to check if everything looks right – you can now tweak and get instant feedback within the DevTools. This feature not only streamlines the process of optimizing for social media but also ensures your app makes the best possible first impression online. Timeline The Timeline feature in Nuxt DevTools is another standout tool. It lets you track when and how each part of your app (like composables) is called. This is different from typical performance tools because it focuses on the high-level aspects of your app, like navigation events and composable calls, giving you a more practical view of your app's operation. It's particularly useful for understanding the sequence and impact of events and actions in your app, making it easier to spot issues and optimize performance. This timeline view brings a new level of clarity to monitoring your app's behavior in real-time. Production Build Analyzer The Production Build Analyzer feature in Nuxt DevTools v1.0 is like a health check for your app. It looks at your app's final build and shows you how to make it better and faster. Think of it as a doctor for your app, pointing out areas that need improvement and helping you optimize performance. API Playground The API Playground in Nuxt DevTools v1.0 is like a sandbox where you can play and experiment with your app's APIs. It's a space where you can easily test and try out different things without affecting your main app. This makes it a great tool for trying out new ideas or checking how changes might work. Some other cool features Another amazing aspect of Nuxt DevTools is the embedded full-featured VS Code. It's like having your favorite code editor inside the DevTools, with all its powerful features and extensions. It's incredibly convenient for making quick edits or tweaks to your code. Then there's the Component Inspector. Think of it as your code's detective tool. It lets you easily pinpoint and understand which parts of your code are behind specific elements on your page. This makes identifying and editing components a breeze. And remember customization! Nuxt DevTools lets you tweak its UI to suit your style. This means you can set up the tools just how you like them, making your development environment more comfortable and tailored to your preferences. Conclusion In summary, Nuxt DevTools v1.0 marks a revolutionary step in web development, offering a comprehensive suite of features that elevate the entire development process. Features like live updates, easy navigation, and a user-friendly interface enrich the development experience. Each tool within Nuxt DevTools v1.0 is thoughtfully designed to simplify and enhance how developers build and manage their applications. In essence, Nuxt DevTools v1.0 is more than just a toolkit; it's a transformative companion for developers seeking to build high-quality web applications more efficiently and effectively. It represents the future of web development tools, setting new standards in developer experience and productivity....