Skip to content

Build Advanced Components in Vue 3 using $attrs

In the third major release of Vue js, we have seen many new features and improvements land on our remote working computers.

In this article, we are going to cover the $attrs attribute. We will explain what it is used for, how its implementation differs from Vue 2's (former $attrs, class, @listener), and build a code example to help understand its power.

Understanding this feature can really support your skills in developing easy to use and scalable components advanced components.

What is $attrs?

The definition of $attrs, varies between the two major versions of the framework, but in this article, we are going to mainly cover Vue 3, where @attrs can be seen as:

A component property that holds all of the attribute, property, and custom events that are not currently defined within the component.

$attrs can also be seen as a safety net, that captures anything that you may not have declared within a component. Let's consider a component that has just a single property and event handler, like the following example:

<template>
  <h1 @click="$emit('custom', 'change title')>{{title}}</h1>
</template>
<script>
export default {
  name: 'Example',
  props: ['title'],
  emits: ['custom'] //This is the new way to declare custom events in vue 3.
}
</script>

If we would instantiate our component like so:

<example 
  id="myId" 
  class="myClass" 
  data-cy="cypress-testing" 
  @blur="...." 
  title="this was expected"
  description="non declared props"
  @custom="..."
/>

Our example component would have access to a $attrs property with the following information:

$attrs = {
  id: "myId" 
  class: "myClass" 
  data-cy: "cypress-testing" 
  @blur: "...." 
  description: "non declared props"
}

If the above is not yet making sense, it is absolutely fine. In the next few sections, we will cover, in more granular details, how to actually make use of this feature.

$attrs V3 vs $attrs V2

Even if I do not want this article to be a comparison between V2 and V3, it is essential that we touch base on the differences that the $attrs feature offers betweent the two major versions.

If you have used VueJs in the past (version 2), there is a significant chance that you have already used $attrs before. The main reason is that almost all the attributes included in the $attrs property were already present in the previous version of the framework. Just split it into different properties.

If we take into consideration the example proposed above, the $attrs object is going to appear as follows:

vue 2 vs v3 $attrs

Main differences to notice in V2 are:

  • custom events go into a @listerner bucket
  • the class is not available (using class in this way requires you to set a property).

Most of the content provided in the following chapter can still be applied in V2, as long as we adhere to the above differences and define the extra properties ($listners and a class property).

Real Life example

As with most of my content, I like to always cover a real life example. Building something from the bottom up, can really help in understanding the reason beind a feature, and help you introduce its usage within your codebase.

In the following sections, we are going to build a nice slider (or more precisely a few of them). The complete code can be found on Stackblitz following the following link (stackblitz-vue-example).

Let's start from scratch

The first step requires us to create a simple component. This is going to be plain Vue and will have nothing to do with the $attrs feature (yet).

// Slider-1.vue

<template>
  <input 
    class="slider__input"
    type="range" 
    :value="modelValue"
    @input="$emit('update:modelValue', $event.target.value)" />
</template>

<script>
export default {
  name: "Slider",
  props: {
    modelValue: [Number, String]
  }
};
</script>

<style lang="scss">
  // The style of this slider have been taken from the W3School website. Link available here: https://www.w3schools.com/howto/howto_js_rangeslider.asp
  // See the Stackblitz link for the full CSS.
</style>

The above code will create a slider that includes single HTML element at the root, and a simple two way binding for a property called value.

To use the above component we would do something like this:

    <Slider-1 v-model="value" />

The result should be something like this: vue-attrs-1

Let's add some attributes

The above "hello world" example, would never stand the real web development industry.

As we know, our components are always full of requirements and specifications, and are never this simple. So to make it a bit more realistic, let's add a couple of attributes (min, max, class, id, data-cy, @keydown and aria-label).

    <Slider-1
      v-model="value"
      min="0"
      max="50"
      class="blue_slider"
      id="special_id"
      data-cy="cypress-slider"
      @keydown="() => true"
      aria-label="Example slider"
    />

If we would run the app with the above changes, we would see that all changes take effect. In fact, analysing the app will show the following UI and HTML:

vue-$attrs-2

As we can notice, all the information has already been applied to our HTML.

WAIT A SECOND... Why did I make such a big introduction to $attrs, when all the "non property/event" attributes are already automatically applied to the inner HTML element?

Do not worry, I have not wasted your time.. In the next section, we will shake things a bit, because as we know, requirements always change.. :)

Change request: Add a title and value

In this section we are going to apply some further changes to our component. More precisely, our product owner has assigned us the following ticket:

As a user of the slider, I would like to be able to see a title, and its value in numerical form being shown on screen.

The modified component will look like this:

// Slider-2.vue

<template>
  <div class="slider">
    <h1>{{ title }}</h1>
    <input 
      type="range" 
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      class="slider__input" />
      <div>Value: {{ value }}</div>
  </div>
</template>

<script>
export default {
  name: "Slider-2",
  props: {
    modelValue: [Number, String]
    title: [String],
    value: [Number, String],
  }
};
</script>
vue-attrs-3

At first glance, everthing seemed to work, but if we look closely, we can see that something is not right.

First, the slider is not blue. Second, the value is going way over 50, and lastly if we look at the html, we'll notice that all of our extra attributes (min, max, data-cy) are assigned to the root element, and not our input element anymore!

The best way to solve the above problem would be to find a way to "apply" all the properties, classes, arguments, and events directly to the input field, without them needing to manually declare them- something like a "bucket" of data.. !$ATTRS!

Let's jump in the next section and see how we can use $atts to accomplish our goals.

$attrs to the rescue

At the start of this article, we introduced $attrs as a bucket of information. It is a place that holds all the "undeclared" properties and events, and this is precisely what we need to solve our issue.

To use this feature, we can just apply the $attrs property to one or more HTML element, using the v-bind operator:

<template>
  <div class="slider">
    <h1>{{ title }}</h1>
    <input 
      type="range"  
      v-bind="$attrs"
      :value="modelValue"
      @input="$emit('update:modelValue', $event.target.value)"
      class="slider__input" />
      <div>Value: {{ modelValue }}</div>
  </div>
</template>

As we can see, the above change will make things much better. The use of attrs in our component will act as bridge that copies all out attributes (class, attribute, property and custom events) to one or more elements:

vue-attrs-4

The slider thumb is back to being blue. The max value is set to 100, and our extra attributes are set correctly... almost.

There is only one problem- our extra attributes have not only been assigned to the input element, but also to the root element!

In this case, there is no visual change to show us this issue (and usually there isn't one in real life either. That is why I have not created any). But these extra variables can really create some side effects. Let's fix this.

inheritAttrs: false

By default, any extra argument being passed to a component is automatically applied to the root element (and to all elements that have the $attrs binding).

To switch this feature off, and get control of what elements receive this extra attribute, we can use a flag called inheritAttrs, and set it to false.

Our script tag will look like this:

export default {
  name: "Slider",
  inheritAttrs: false,
  props: {
    title: [String],
    modelValue: [Number, String],
  }
};

After this change, our HTML is nice and clean. All the extra properties are applied to the Input element only.

Conclusion

Before we wrap up, there are a couple of extra points that I want to share, so that you can make furhter use of this feature and understand it more deeply.

  1. I prefer not to use the v-model so that I can actually also omit the update:modelValue event (you can see at the file name Slider.vue in the stackblitz code)
  2. You can access, and play with the individual properties of the $attrs. So for example, you could apply the $attrs.class to one element, and the $attrs['aria-label]' to another.
  3. It is always best to declare property and events, and just use this when you have an element that emits a native event and/or accepts many attributes (like video tag, or all Input fields).

Time to say goodbye

I have personally taken some time to fully understand this feature, but I really hope that this article may help you in understanding this feature, and helps you define complex but very readable components.

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

Getting Started with Vuetify in Vue 3 cover image

Getting Started with Vuetify in Vue 3

If you're here to learn about Vuetify and how to use it with Vue 3, you've come to the right place. In this article, we'll introduce Vuetify, a Material Design component framework for Vue.js, and walk you through setting it up in a Vue 3 project. We will also build a simple web app that allows us to create and vue jokes. Oh, sorry, view jokes! What is Vuetify? Vuetify is a VueJS Material Design component framework. That is to say that Vuetify provides us with prebuilt components and features (e.g. internationalization, tree shaking, theming, etc.) to help us build Vue applications faster. Additionally, Vuetify components have built-in standard functionality, such as validation in form inputs, or various disabled states in buttons. You can check out a full list of components supported by Vuetify on their official documentation website. Setting things up Before we get started, you will need the following on your machine: - NodeJS installed - A NodeJS package manager e.g. yarn or npm installed - Intermediate knowledge of Vue 3.x with the composition API If you’d like to follow along, feel free to clone the github repository for this article. `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 getting-started-with-vuetify ` Installing Vuetify We will be using yarn` to install and setup Vuetify 3 (the version that supports Vue 3) in this case. First, let's create a new Vuetify project. You can do this by running the following command in your terminal or command prompt: `shell yarn create vuetify ` This will create a new Vuetify project. For our jokes app, we will use the Essentials (Vuetify, VueRouter, Pinia)` preset when creating our app. We will also be using TypeScript for our app, but this is not necessary. Since VueJS allows us to build incrementally, if you would like to instead add Vuetify to an existing project, you can use the manual steps provided by the Vuetify team. Testing our application Once we have installed and configured our application, cd` into the project's directly, and run the app using the following command: `shell yarn dev #or npm run dev` (if using npm instead) ` Visit localhost:3000/` to see your app in action. Vuetify project folder structure Our Vuetify project is generally structured as follows: - public` - Contains static assets that do not need preprocessing eg. our application `favicon` - src` - Contains our VueJS source code. We'll mostly be working here. - assets` - Assets that need to be preprocessed eg. our images that may need to be compressed when building for production - components` - layouts` - Layouts - plugins` - Everything gets wired up here (registration of our app as well as vuetify, our router & pinia) - router` - Vue router-related functionality - store` - Pinia store - styles` - views` - our web app's "pages" Worth noting before building It is worth noting that all components in Vuetify start with a v-` for example: - v-form` is a form - v-btn` is a button component - v-text-field` is an input field and so on and so forth. When creating your own components, it is recommended to use a different naming approach so that it is easier to know which components are Vuetify components, and which ones are your components. Building our Vuetify application We will build a web app that allows us to add, view and delete jokes. Here are the steps we will take to build our app: - Delete unnecessary boilerplate from generated Vuetify app - Add a joke` pinia store - we'll be using this to store our jokes globally using Pinia - Create our joke components - components/jokes/CreateJokeForm.vue` - the form that allows us to add jokes - components/jokes/JokeList.vue` - Lists our jokes out. - Add our components to our Home.vue` to view them in our home page Setting up the jokes pinia store In the src/store/` directory, create a new file called `joke.ts` that will serve as our Pinia store for storing our jokes. The file content for this will be as follows: `ts import { defineStore } from "pinia"; export interface Joke { id: number; title: string; punchline: string; } export const useJokeStore = defineStore({ id: "joke", state: () => ({ jokes: [] as Joke[], }), actions: { addJoke(joke: Joke) { this.jokes.push(joke); }, removeJoke(id: number) { this.jokes = this.jokes.filter((joke) => joke.id !== id); }, }, }); ` This code defines a special storage space called a "store" for jokes in our Vue.js app. This store keeps track of all the jokes we've added through our app's form. Each joke has an ID, title, and punchline. The addJoke` function in the store is used to add a new joke to the store when a user submits the form. The `removeJoke` function is used to delete a joke from the store when a user clicks the delete button. By using this store, we can keep track of all the jokes that have been added through the app, and we can easily add or remove jokes without having to manage the list ourselves. Creating the joke components CreateJokeForm.vue Create a file in src/components/jokes/` called `CreateJokeForm.vue`. This file defines a Vue.js component that displays a form for adding new jokes. The file should have the following contents: Template section `html Submit Joke ` In the template section, we define a form using the v-form component from Vuetify. We bind the form's submit event to the submitJoke method, which will be called when the form is submitted. Inside the form, we have two text fields" one for the joke title, and one for the punchline. These text fields are implemented using the v-text-field component from Vuetify. The label prop sets the label for each text field, and the outlined prop creates an outlined style for each text field. The required prop sets the fields as required, meaning that they must be filled in before the form can be submitted. Finally, we have a submit button implemented using the v-btn component from Vuetify. The button is disabled until both the title and punchline fields are filled in, which is accomplished using the :disabled prop with a computed property that checks if both fields are empty. Script section `ts import { computed, ref } from "vue"; import { Joke, useJokeStore } from "@/store/joke"; const jokeStore = useJokeStore(); const jokeTitle = ref(""); const jokePunchline = ref(""); const joke = computed(() => ({ id: jokeStore.jokes.length + 1, title: jokeTitle.value, punchline: jokePunchline.value, })); function submitJoke() { jokeStore.addJoke(joke.value); jokeTitle.value = ""; jokePunchline.value = ""; } ` In the script section, we import some functions and types from Vue.js and the joke` store. We then define a `jokeStore` variable that holds the instance of the `useJokeStore` function from the `joke` store. We also define two ref`s, `jokeTitle`, and `jokePunchline`, which hold the values of the form's title and punchline fields, respectively. We then define a computed property, joke`, which creates a new `Joke` object using the `jokeTitle` and `jokePunchline` refs, as well as the length of the `jokes` array in the `jokeStore` to set the `id` property. Finally, we define a submitJoke` function that calls the `addJoke` method from the `jokeStore` to add the new `joke` object to the store. We also reset the `jokeTitle` and `jokePunchline` refs to empty strings. JokeList.vue Template section This one looks bulky. But in essence, all we are doing is displaying a list of jokes when they are found, and a message that lets us know that there are no jokes if we have none that have been added. `html My Jokes {{ joke.title }} {{ joke.punchline }} mdi-delete You have no jokes. Add some! ` In the template section, we define a v-card` component, which is a container component used to group related content in Vuetify. The card contains a title, which includes an excited emoticon icon from the `mdi-emoticon-excited-outline` icon set from Material Design Icons, displayed using the `v-icon` component. The jokes are displayed in a v-list`, which is a component used to display lists in Vuetify. Each joke is represented by a `v-list-item` containing a title and subtitle. The `v-row` and `v-col` components from Vuetify are used to divide each list item into two columns: one column for the joke title and punchline, and another column for the delete button. The delete button is implemented using the v-btn` component from Vuetify. The button is red, and outlined using the `color="error"` and `variant="outlined"` props, respectively. The `@click` event is used to call the `deleteJoke` function when the button is clicked. If there are no jokes in the jokeStore`, the component displays an `v-alert` component with a message to add some jokes. Script section `ts import { Joke, useJokeStore } from "@/store/joke"; const jokeStore = useJokeStore(); function deleteJoke(joke: Joke) { jokeStore.removeJoke(joke.id); } ` In the script section, we import some functions and types from the joke` store. We then define a `jokeStore` variable that holds the instance of the `useJokeStore` function from the `joke` store. We also define a deleteJoke` function that takes a `joke` object as an argument and calls the `removeJoke` method from the `jokeStore` to remove the joke from the store. This component is called JokeList.vue` and displays a list of jokes using Vuetify components like `v-card`, `v-list`, `v-list-item`, `v-row`, `v-col`, and `v-btn`. The component includes a `deleteJoke` function to remove a joke from the `jokeStore` as well. Wiring it up To display our form as well as list of jokes, we will go to the src/views/Home.vue` file and change its contents to the following: `html import CreateJokeForm from "@/components/jokes/CreateJokeForm.vue"; import JokeList from "@/components/jokes/JokeList.vue"; ` The Home.vue` file defines a Vue.js view that displays the home page of our app. The view contains a `v-container` component, which is a layout component used to provide a responsive grid system for our app. Inside the v-container`, we have a `v-row` component, which is used to create a horizontal row of content. The `v-row` contains two `v-col` components, each representing a column of content. The `cols` prop specifies that each column should take up 12 columns on small screens (i.e. the entire row width), while on medium screens, each column should take up 6 columns (i.e. half the row width). The first v-col` contains the `CreateJokeForm` component, which displays a form for adding new jokes. The second `v-col` contains the `JokeList` component, which is used to display a list of jokes that have been added through the form. In the script` section of the file, we import the `CreateJokeForm` and `JokeList` components, and register them as components for use in the template. This view provides a simple and responsive layout for our app's home page, with the CreateJokeForm` and `JokeList` components displayed side by side on medium screens and stacked on small screens. Bonus: Layouts & Theming Layouts Even though we had no need to adjust our layouts in our current jokes application, layouts are an important concept in Vuetify. They allow us to pre-define reusable layouts for our applications. These could include having a different layout for when users are logged in, and when they are logged out or layouts for different types of users. In our application, we used the default Layout (src/layouts/default/Default.vue`) but Vuetify offers us the flexibility to build different layouts for the different domains in our applications. Vuetify also supports nested layouts. You can learn more about layouts in Vuetify in the official Vuetify documentation. Theming If you have specific brand needs for your application. Vuetify has a built-in theming system that allows you to customize the look and feel of your application. You can learn more about theming in the official Vuetify theming documentation. Conclusion In this article, we introduced Vuetify, and covered how to set it up with Vue 3. We built a VueJS app that allows us to add and manage jokes. We also discussed how to use various Vuetify components to compose our UI, from v-form` for declaring forms to `v-row` for creating a row/column layout, and `v-list` for displaying a list of items among others. With this knowledge, you can start using Vuetify in your Vue 3 projects and create stunning user interfaces. Also, if you'd like to start your own VueJS project but need help with how to structure it or would like to skip the tedious setup steps of setting up a VueJS project, you can use the Vue 3 Starter.dev kit to skip the boilerplate and start building! Next steps for learning Vuetify and Vue 3 development Now that you have an understanding of Vuetify, it's time to dive deeper into its features, and explore more advanced use cases. To continue your learning journey, consider the following resources: 1. Official Vuetify documentation: The Vuetify documentation is an excellent resource for learning about all the features and components Vuetify offers. 2. Vue 3 documentation: To get the most out of Vuetify, it's essential to have a solid understanding of Vue 3. Read the official Vue 3 documentation and practice building Vue applications. Happy coding, and have fun exploring the world of Vuetify and Vue 3!...

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!...

Apply SVG filters on HTML using Vue.js cover image

Apply SVG filters on HTML using Vue.js

SVG is a powerful format, based on XML and is well known for its scalability. This image format is usually known for its application on logos, icons and graphs, but in this article, we are going to use it with the help of VueJs to create a dynamic styled HTML container. Prologue The idea of this project occurred to me while listening to Syntax FM podcast with Sara Soueidan. I never had a chance to use SVG before and I was astonished to hear the countless possibilities that lie within this image format, and I wanted to see it with my own eyes. The article is going to walk through all the steps necessary to produce a full working component. A full version of the code can be found in this codepen: vuejs-svg-component-with-filter-and-html-slot-bnejy The CSS filter property already offers us the possibility to apply a great set of filters to our elements, and even load SVG filter directly with the use of the URL filter function. But in this article we are going to use plain SVGs. The component Our first step requires the creation of a Vue component: `html export default {}; ` Next, we will add a very simple SVG within our Template tags. Our object will include four main parts: basic structure, shape, filter and foreignObject. Basic structure First we will create the structure of the SVG. For the purpose of our example we are going to create a simple SVG with a fixed size ViewBox: `html ` Shape SVGs are well known for the multitude of simple shape elements available within its standards. If you have worked with HTML before, SVG elements will look quite familiar, as they exhibit the same features of a normal HTML element, with the addition of specific XML attributes. In our example we are going to create a simple Polygon. This shape is described as: Polygons are made of straight lines, and the shape is "closed" (all the lines connect up). https://www.w3schools.com/graphics/svgpolygon.asp The next code will define a polygon made of 4 lines and filled with the colour blue: `html ` Filter There are a great number of filter primitives that can be used within an SVG document. Their power comes from the possibility to merge multiple primitives together, to create very complex results, and the ability for the filters to be applied to a single shape/ object within an SVG document. The actual definition of filters is: > The filter SVG element defines a custom filter effect by grouping atomic filter primitives. It is never rendered itself, but must be used by the filter attribute on SVG elements, or the filter CSS property for SVG/HTML elements. https://developer.mozilla.org/en-US/docs/Web/SVG/Element/filter In our example we are going to apply two different filters: feTurbulence** and **feDisplacementMap**. This will provide a distorted effect to all elements in which it is applied. `html ` As described above, the filter needs to be used within another object with the use of the filter attribute**. In our case, we will have to apply the ID of turbulence to our polygon: `html ` A rendered version of our component will look like this: HTML object Our SVG is finally getting shape. The last step required involves the use of the foreignObject** element. This element will allow us to include normal HTML elements within our SVG. Our first step requires us to add the element within the SVG. This is going to be added alongside the shape, but it is going to be completely independent to it. This means that the filter that we have applied to the polygon will bleed into this SVG element. `html ...HTML CODE HERE ` Now that we have defined our element, we are able to add any HTML we want within its tags. It is important to note that this approach may produce unwanted behaviour across different browsers, and on site performance. Because of the nature of SVG, all elements within foreignObject element are going to be readable by a screen reader and can be used for accessibility purposes. For the purpose of our example, we are going to add a few simple elements within our SVG: `html The HTML works.. ` > TIPS:** We are able to use any CSS declaration within the SVG itself and we can even use an existing declaration. For example, we would have defined the `` to be green globally, this declaration would have been applied to our element too. Our component will render like this: Make it dynamic The component above works perfectly, but it is still static. In the following section we are going to make it more dynamic by using features available within the VueJs framework. Slot First, we are going to modify the code, so that the content of the foreignObject is actually going to be a slot. These are very useful when you want to provide the flexibility of passing any HTML to the component. `html The HTML works.. ` Then we are going to pass some HTML in the parent component that is using our newly introduced slot: `html My heading This is my paragraph ` Running this code in the browser will produce the following design: Summary Our fully working VueJs SVG component is now completed. The code can be found at the following codesandbox: vueJs Svg component with HTML slot Using SVG within a framework, could be a bit tricky, as reactivity and virtual DOM, produce unwanted results, and this example may not be used in production, without the right set of tests. The article just covers a couple of examples and introduces a few topics, but the possibilities are endless. We could provide a set of shapes as props, introduce SVG animations and finally create a set of filters that can easily be applied to the passed HTML. I hope this proof of concept turns out to be helpful, and I look forward to seeing different permutations and applications of this approach online. References SyntaxFM: https://syntax.fm/show/154/svgs-with-sara-soueidan SVG definition: https://en.wikipedia.org/wiki/ScalableVector_Graphics CSS filters: https://css-tricks.com/almanac/properties/f/filter/ SVG filters explained: https://tympanus.net/codrops/2019/01/15/svg-filters-101/ SVG filters: https://developer.mozilla.org/en-US/docs/Web/SVG/Element/filter VUE slots: https://vuejs.org/v2/guide/components-slots.html...

Testing a Fastify app with the NodeJS test runner cover image

Testing a Fastify app with the NodeJS test runner

Introduction Node.js has shipped a built-in test runner for a couple of major versions. Since its release I haven’t heard much about it so I decided to try it out on a simple Fastify API server application that I was working on. It turns out, it’s pretty good! It’s also really nice to start testing a node application without dealing with the hassle of installing some additional dependencies and managing more configurations. Since it’s got my stamp of approval, why not write a post about it? In this post, we will hit the highlights of the testing API and write some basic but real-life tests for an API server. This server will be built with Fastify, a plugin-centric API framework. They have some good documentation on testing that should make this pretty easy. We’ll also add a SQL driver for the plugin we will test. Setup Let's set up our simple API server by creating a new project, adding our dependencies, and creating some files. Ensure you’re running node v20 or greater (Test runner is a stable API as of the 20 major releases) Overview `index.js` - node entry that initializes our Fastify app and listens for incoming http requests on port 3001 `app.js` - this file exports a function that creates and returns our Fastify application instance `sql-plugin.js` - a Fastify plugin that sets up and connects to a SQL driver and makes it available on our app instance Application Code A simple first test For our first test we will just test our servers index route. If you recall from the app.js` code above, our index route returns a 501 response for “not implemented”. In this test, we're using the createApp` function to create a new instance of our Fastify app, and then using the `inject` method from the Fastify API to make a request to the `/` route. We import our test utilities directly from the node. Notice we can pass async functions to our test to use async/await. Node’s assert API has been around for a long time, this is what we are using to make our test assertions. To run this test, we can use the following command: By default the Node.js test runner uses the TAP reporter. You can configure it using other reporters or even create your own custom reporters for it to use. Testing our SQL plugin Next, let's take a look at how to test our Fastify Postgres plugin. This one is a bit more involved and gives us an opportunity to use more of the test runner features. In this example, we are using a feature called Subtests. This simply means when nested tests inside of a top-level test. In our top-level test call, we get a test parameter t` that we call methods on in our nested test structure. In this example, we use `t.beforeEach` to create a new Fastify app instance for each test, and call the `test` method to register our nested tests. Along with `beforeEach` the other methods you might expect are also available: `afterEach`, `before`, `after`. Since we don’t want to connect to our Postgres database in our tests, we are using the available Mocking API to mock out the client. This was the API that I was most excited to see included in the Node Test Runner. After the basics, you almost always need to mock some functions, methods, or libraries in your tests. After trying this feature, it works easily and as expected, I was confident that I could get pretty far testing with the new Node.js core API’s. Since my plugin only uses the end method of the Postgres driver, it’s the only method I provide a mock function for. Our second test confirms that it gets called when our Fastify server is shutting down. Additional features A lot of other features that are common in other popular testing frameworks are also available. Test styles and methods Along with our basic test` based tests we used for our Fastify plugins - `test` also includes `skip`, `todo`, and `only` methods. They are for what you would expect based on the names, skipping or only running certain tests, and work-in-progress tests. If you prefer, you also have the option of using the describe` → `it` test syntax. They both come with the same methods as `test` and I think it really comes down to a matter of personal preference. Test coverage This might be the deal breaker for some since this feature is still experimental. As popular as test coverage reporting is, I expect this API to be finalized and become stable in an upcoming version. Since this isn’t something that’s being shipped for the end user though, I say go for it. What’s the worst that could happen really? Other CLI flags —watch` - https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--watch —test-name-pattern` - https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--test-name-pattern TypeScript support You can use a loader like you would for a regular node application to execute TypeScript files. Some popular examples are tsx` and `ts-node`. In practice, I found that this currently doesn’t work well since the test runner only looks for JS file types. After digging in I found that they added support to locate your test files via a glob string but it won’t be available until the next major version release. Conclusion The built-in test runner is a lot more comprehensive than I expected it to be. I was able to easily write some real-world tests for my application. If you don’t mind some of the features like coverage reporting being experimental, you can get pretty far without installing any additional dependencies. The biggest deal breaker on many projects at this point, in my opinion, is the lack of straightforward TypeScript support. This is the test command that I ended up with in my application: I’ll be honest, I stole this from a GitHub issue thread and I don’t know exactly how it works (but it does). If TypeScript is a requirement, maybe stick with Jest or Vitest for now 🙂...