Skip to content

Developer Insights

Join millions of viewers! Our engineers craft human-written articles solving real-world problems weekly. Enjoy fresh technical content and numerous interviews featuring modern web advancements with industry leaders and open-source authors.

Newest First
Tags:Lindsay Wardell
Styling Vue Single-File Components cover image

Styling Vue Single-File Components

Introduction If you have any experience with writing Vue single-file components, you have probably spent some time writing CSS within your component. Single-File Components allow developers to group code together in more logical ways, rather than breaking up components by language utilized (HTML, CSS, or JavaScript). Being able to group component styles directly next to the HTML that it applies to is one of the major benefits of Vue, including the ability to scope CSS to the component so that it doesn't affect other parts of the UI. However, there are a number of features to Vue's CSS integration that you may not be familar with, such as applying styles directly to slotted elements, or the newest features available in Vue 3.2. Let's explore some of these other ways of styling Vue single-file components, and how they can benefit your applications. Scoped Styles Let's start with the most common usage of CSS in Vue: scoped styles. One of the difficulties on writing modern applications is that our CSS files begin to grow larger and larger, until nobody really knows where certain styles are used or what a given change might affect. This can lead to copying certain CSS selectors, and simply duplicating them for each component. There are other solutions for this (such as BEM or utility classes), but when working with a component-based framework like Vue, it makes a lot of sense to group CSS classes within the component. Scoped styles allows us to write CSS that only applies to the component we are working in. Here's an example from the Vue docs: ` With this, the example class will only ever apply within this component. This is achieved by added a unique data attribute to all elements within the component, so the normal CSS cascade is still applied. External styles can still impact the design of this component, but its scoped styles cannot leak out to other components. Deep Styles This leads to an interesting problem. If our component's styles are scoped, what about children components? By default, they would not receive any styling from our scoped styles. However, Vue provides a way to do that. Let's look at an example below. ` By using the :deep() pseudo class, we are able to tell Vue that this particular class (.card-title) should not be scoped. Because the special ID is still applied to the root element (header), the style is still scoped, but it is available for any child component beneath it. Slotted Styles A problem I've run into in many situations is that I have a component being injected with slots, but I cannot control the styling of it like I want. Vue provides a solution for this as well with slotted styles. Let's review the above example, but this time we'll add a slotted style to our Title.vue component. ` Here, we added the :slotted pseudo class, so that any slotted h1 tags have the correct style applied to them. This may be a contrived example, but consider needing to have different header styles for each header tag (or equivalent CSS class). The Title.vue component can manage all of these styles, rather than relying on the consumer of these components to pass in the correct class or styling. Global Styles Of course, sometimes you need to apply styles globally, even within a scoped component. Vue provides us with two different ways to handle this: the :global pseudo selector and multiple style blocks. :global Within a scoped style block, if you only need to provide one class as a global value, you can use the :global pseudo selector to note that the style should not be scoped. From the Vue docs: ` Multiple style blocks There is also nothing stopping you from having multiple style blocks within your Vue component. Simply create another tag, and put your global styles in there. ` Style Modules If you're coming from React, you may be more familiar with CSS modules, where you import a CSS file and access its classes as a JavaScript object. The same can be done within Vue by using instead of . Here's an example from the Vue docs: ` This can be particularly nice to work with, so that you aren't passing strings around in your classes (which are prone to errors and typos). Vue also allows you to rename what the object is, so that you don't need to access them with $style in your template if you don't want to. Dynamic CSS Values The latest feature in Vue is state-driven dynamic CSS values. There is a trend in modern CSS to use custom properties as a way to dynamically update the value of some CSS property. This can allow our CSS to be more flexible, and interact nicely with our other application code. Let's look at an example component that renders a progress bar: ` This component takes in a number (progress), then both displays that number and updates a CSS custom property with the value. As the progress changes, the CSS property is continually updated to stay in sync with the JavaScript value. In Vue 3.2, however, we are provided with a special CSS function that does this whole thing for us! Take a look at the updated code: ` By using v-bind(props.progress), we have elimited the need for our custom watcher, and it's clear that our CSS is being kept in sync with the value of props.progress. Under the hood, Vue is doing the same thing for us with a custom property, but it's so much nicer than having to write it ourselves. Conclusion CSS is a complicated language in practice, and mixing it with JavaScript makes things even more complex. Vue provides developers with the tools to handle CSS in a reliable and predictable way, which encourages building in a component-based architecture. Next time you're running into trouble with CSS in Vue, see if one of these techniques can be useful to you!...

Building Web Components with Vue 3.2 cover image

Building Web Components with Vue 3.2

Introduction Have you ever worked across multiple projects, and wanted a set of custom components you could just leverage across all of them? Whether for a job or just for side projects, having a suite of components you can reach for is an excellent way to get going faster in a new or existing project. But what if not all of your projects are using the same UI framework? Or, what if you have one that isn't using any JavaScript framework at all, and is completely server-rendered? As a Vue developer, ideally we would like to just use our framework of choice to build complex user interfaces. But sometimes we find ourselves in the above situation, working with another JavaScript framework such as React or Angular, or using a backend rendering system like Rails or Laravel. How can we build a reusable UI across these various frontend options? In Vue 3.2, we now have a solution to this problem: Web Components, powered by Vue! Web Components According to MDN, "Web Components is a suite of different technologies allowing you to create reusable custom elements — with their functionality encapsulated away from the rest of your code — and utilize them in your web apps." Consider a few existing elements in HTML, such as select or video. These interactive elements contain their own basic styling (typically provided by the browser), some internal logic, and a way to listen to events. Web Components allow developers to build their own elements, and reference them in their HTML - no framework required. Here's a very basic web component example of a component that would display the current time. ` Once a custom Web Component has been defined, they can be rendered as part of the DOM, just like any standard HTML element. We can use this element in our HTML like this: ` We can also use custom attributes with these elements, allowing us to pass data into them (similar to props in Vue). Note that objects cannot be passed in as attributes, because that is a JavaScript concept, not an HTML feature. ` While we could write this logic pretty easily in a script tag using vanilla JavaScript, utilizing Web Components gives us the ability to encapsulate specific logic and functionality within the component, thus keeping our code more organized and understandable. This is the same reason we utilize component frameworks like Vue and React. Also, as we discussed earlier, Web Components are flexible in that they can be used without a JS framework, but are also compatible with modern frameworks (React and Vue both other support for using Web Components). Vue-Powered Web Components Vue 3.2 includes built-in support for defining custom Web Components while utilzing the Vue API. In this way, we get the best of both worlds - custom, reusable components across frameworks/interfaces, plus the excellent API of Vue. Let's take our example of getting the current time, and translate that into a Vue component. We will be using , which is the recommended way to write Vue single-file components today. To start, let's create our new file, CurrentTime.ce.vue (ce in this case stands for custom element). ` Great, our component is doing exactly what we were doing before. Next, we need to import this into our main Javascript somewhere, and define it as a custom element. ` What did we do here? 1. First, we import Vue's defineCustomElement function, which converts a Vue component into a custom element. 2. We then import our Vue SFC, and pass it into defineCustomElement, generating the constructor required for the web components APIs. 3. Then, we define the custom element in the DOM, supplying it with the tag that we want to use (current-time) and the constructor it should use to render. With that, our Vue web component can now be rendered in our app! And since web components work in all modern frameworks (as well as non-JS frameworks like Ruby on Rails or Laravel), we can now build out a suite of web components for our application using Vue, and then utlize them in any frontend we want. Here's a basic example using vanilla JS, and the default Vite template: ` You can see a working example of this on Stackblitz. More Features Creating a basic Vue component isn't always what you want to do, though. What happens if we need to utilize props or events? Fortunately, Vue has us covered here as well. Let's explore some of the basic functions we'd expect from our custom Vue components. Props The first thing we want to do is pass in props to our web component. Using our component, we want to be able to set the time zone. For this case, we can use props as we normally would for an HTML element. In our HTML template, let's change our code to the following: ` If you save your file now, you will probably get an error in your console like this one: ` This is because our prop isn't defined in the component yet. Let's take care of that now. Go back to your Vue component, and make the following changes: ` In our Vue component, we are now defining props (using Vue 3.2's defineProps helper, which we do not need to import), then using the timeZone prop to translate the date into the correct time zone string. Nice! Save your files, and our app should work again as expected, but this time, it will display the date in a different time zone. Feel free to play around with it a bit, trying out some different time zones. By default, Vue will translate props into their defined types. Since HTML only allows strings to be passed in as attributes, Vue is handling the translation to different types for us. Events From the docs: "Events emitted via this.$emit or setup emit are dispatched as native CustomEvents on the custom element. Additional event arguments (payload) will be exposed as an array on the CustomEvent object as its details property." Let's add a basic emit from our component that will trigger a console.log. In our Vue component, update our script block to the following: ` The main change we're making here is to add defineEmits (also available without import, similar to defineProps) in order to define what events this component makes. We then begin to use this in our setInterval step, to emit the new date as an event. In our main.js, we'll now add the event listener to our web component. ` With this, whenever the first component emits a datechange event, we will be able to listen for it, and act accordingly. Nice! Slots Slots are used exactly as expected within Vue, including named slots. Scoped slots, as they are an advanced feature within a full Vue application, are not supported. Also, when utilizing named slots in your application, you will need to use the native slot syntax, rather than Vue's specific slot syntax. Let's give this a try now. In you Vue component, change your template to the following: ` For this example, we are using a standard slot (we'll get back to named slots later). Now, in your HTML, add some text in between the tags: ` If you save and reload your page, you should now see that your text (or whatever content you want) is correctly passing into your web component. Now, let's do the same thing using named slots. Change your Vue component to have a named slot (`, for example), and your HTML like this: ` The result should be the same! And now we can use named slots in our web components. Styles One of the great parts about web components is that they can utilize a shadow root, an encapsulated portion of the DOM that contains their own styling information. This feature is available to our Vue web components as well. When you name your file with the .ce.vue extension, it defaults to having inline styles. This is perfect if you want to use your components as a library in an application. Provide/Inject Provide and inject also work as expected within Vue web components. One thing to keep in mind, however, is that they only can pass data to other Vue web components. From the docs, "a Vue-defined custom element won't be able to inject properties provided by a non-custom-element Vue component." Conclusion Vue 3.2 provides the ability to write custom web components using Vue's familiar syntax, and the flexibility of the Composition API. Keep in mind, however, that this is not the recommended approach to writing Vue applications, or application development in general. The documentation goes to great lengths to explain the differences between web components and Vue-specific components, and why the Vue team feels their approach is preferable for web development. However, web components are still an amazing technology for building cross-framework applications. Plenty of tools exist in the web development ecosystem focused purely on web components, such as Lit or Ionic. While this may not be the recommended approach to building applications with Vue, it can provide an encapsulated way to get certain features developed and functional across teams or projects. Regardless of your stance on web components, I highly encourage you to check out this new feature of Vue and experiment with it yourself. You could even try mounting your component in React or Svelte, and see how easy it is to work with across JavaScript frameworks. Most important of all, remember that the development ecosystem is always improving and growing, and it's up to you to be ready to grow with it. StackBlitz Demo Play around with the StackBlitz demo below and here's an example of a Vue web component I am utilizing in a side project I'm working on. Have fun!...

Vue 3.2 - Using Composition API with Script Setup cover image

Vue 3.2 - Using Composition API with Script Setup

Introduction Vue 3 introduced the Composition API as a new way to work with reactive state in a Vue application. Rather than organizing code by functionality, (data, computed, methods, watch, etc), you can group code by feature (users, API, form). This allows for a greater amount of flexibility while building a Vue application. We've already talked about the Composition in other articles (if you haven't read them, check them out!), but with the release of Vue 3.2, another Composition-related feature has been released as stable - . In short, allows developers to define a component without having to export anything from your JavaScript block - simply define your variables and use them in your template! This style of writing a component resembles Svelte in many ways, and is a massive improvement for anyone coming into Vue for the first time. Basics Let's look at an example. If you were using the Options API (the standard of Vue 2), all of your single-file components would look something like this: ` We have our template (a simple form), and our script block. Within the script block, we export an object with three keys: name, computed, and methods. If you are familiar with Vue, this should look familiar to you. Now, let's switch this code to use the Composition API. ` Our component does the exact same thing as before. We define our state (name), a computed property (isNamePresent), and our submit function. If any of this is unfamiliar, check out my previous articles on the Vue Composition API. Rather than having to scaffold our application within an object being exported, we are free to define our variables as we want. This flexibility also allows us to extract repeated logic from the component if we want to, but in this case our component is pretty straightforward. However, we still have that awkward export default statement. Our code all lives within the setup function, while the rest is really just boilerplate. Can't we just remove it? Actually, we can now! This is where comes in. Let's switch to use script setup instead of the standard script block. ` Let's go over what changed here. First, we added the word "setup" to our script tag, which enables this new mode for writing Vue components. Second, we took our code from within the setup function, and replaced our existing exported object with just our code. And everything works as expected! Note that everything declared within the script tags is available in the template of your component. This includes non-reactive variables or constants, as well as utility functions or other libraries. The major benefit of this is that you don't need to manually bind an external file (Constants.js, for example) as a value of your component - Vue handles this for you now. Additional Features You may be wondering how to handle some of the core aspects of writing Vue components, like utilizing other components or defining props. Vue 3.2 has us covered for those use cases as well! Let's take a look at some of the additional features provided by this approach to building Vue single-file components. Defining Components When using , we don't have to manually define our imported components any more. By importing a component into the file, the compiler is able to automatically add it to our application. Let's update our component by abstracting the form into its own component. We'll call it Form.vue. For now, it will simply be the template, and we'll get to the logic in a moment. ` That's it! Our component now has to be imported into our Vue file, and it's automatically available in our template. No more components block taking up space in our file! Now, we need to pass name into our child component as a prop. But wait, we can't define props! We don't have an object to add the props option to! Also, we need to emit that the form was submitted so that we can trigger our submission. How can we define what our child component emits? defineProps and defineEmits We can still define our components props and emits by using new helper methods defineProps and defineEmits. From the Vue docs, "defineProps and defineEmits are compiler macros only usable inside . They do not need to be imported, and are compiled away when is processed." These compile-time functions take the same arguments as the standard keys would use with a full export object. Let's update our app to use defineProps and defineEmits. ` Let's go over what changed here. - First, we used defineProps to expect a modelValue (the expected prop for use with v-model in Vue 3). - We then defined our emits with defineEmits, so that we are both reporting what this component emits, and are also getting access to the emit function (previously available on `this.$emit). - Next, we create a computed property that utilizes a custom getter and setting. We do this so we can easily use v-model on our form input, but it's not a requirement. The getter returns our prop, where the setter emits the update event to our parent component. - Last of all, we hook up our submitHandler function to emit a submit event as well. Our App.vue component is more or less as we left it, with the addition of v-model="name" and @submit="submitForm" to the Form child component. With that, our application is working as expected again! Other Features There are a lot more features available to us here, but they have fewer use cases in a typical application. - Dynamic Components - Since our components are immediately available in the template, we can utilize them when writing a dynamic component (, for example). - Namespaced Components - If you have a number of components imported from the same file, these can be namespaced by using the import * as Form syntax. You then have access to or , for example, without any extra work on your part. - Top-Level Await - If you need to make an API request as part of the setup for a component, you are free to use async/await syntax at the top level of your component - no wrapping in an async function required! Keep in mind that a component that utilizes this must be wrapped externally by a component - read more here to learn how to use Suspense in Vue. Another point to keep in mind is that you aren't locked into using . If you are using this new syntax for a component and run into a case where you aren't able to get something done, or simply want to use the Options syntax for a particular case, you are free to do so by adding an additional block to your component. Vue will mix the two together for you, so your Composition code and Options code can remain separate. This can be extremely useful when using frameworks like Nuxt that provide additional methods to the standard Options syntax that are not exposed in . See the Vue docs for a great example of this. Conclusion This is a big step forward for Vue and the Composition API. In fact, Evan You has gone on the record as saying this is intended to be the standard syntax for Vue single-file components going forward. From a discussion on Github: > There's some history in this because the initial proposal for Composition API indicated the intention to entirely replace Options API with it, and was met with a backlash. Although we did believe that Composition API has the potential to be "the way forward" in the long run, we realized that (1) there were still ergonomics/tooling/design issues to be resolved and (2) a paradigm shift can't be done in one day. We need time and early adopters to validate, try, adopt and experiment around the new paradigm before we can confidently recommend something new to all Vue users. > That essentially led to a "transition period" during which we intentionally avoided declaring Composition API as "the new way" so that we can perform the validation process and build the surrounding tooling /ecosystem with the subset of users who proactively adopted it. > Now that has shipped, along with improvements in IDE tooling support, we believe Composition API has reached a state where it provides superior DX and scalability for most users. But we needed time to get to this point. Earlier in that same thread, Evan expressed his views on what development looks like going forward for Vue: > The current recommended approach is: > - Use SFC + + Composition API > - Use VSCode + Volar (or WebStorm once its support for ships soon) > - Not strictly required for TS, but if applicable, use Vite for build tooling. If you're looking to use Vue 3 for either a new or existing application, I highly recommend trying out this new format for writing Vue single-file components. Looking to try it out? Here's a Stackblitz project using Vite and the example code above....

TC39 - How Changes are Made to JavaScript cover image

TC39 - How Changes are Made to JavaScript

Introduction The JavaScript ecosystem is constantly changing. As developers, we are very familiar with the ever-shifting landscape of frameworks, libraries, and tooling required to write our applications. In addition, there are other runtimes for Javascript beyond the browser, including Node, Deno, Cloudflare Workers, with more being released all the time. All of this - the tooling, the frameworks, the runtimes, even the language - are based on standards developed by a group of individuals and companies know as TC39. TC39 (Technical Committee 39) is a committee organized by Ecma International, a nonprofit standards organization for information and communication systems. In 1996, Netscape (the original creators of JavaScript) began meeting with Ecma to discuss standardizing the language. The first standard edition of JavaScript (called ECMAScript) was adopted in 1997, with further releases of the standard happening since then. The JavaScript we use today is an implementation of these standards, and each runtime of JavaScript works to implement them for use by developers. This standardization across runtimes was not always a guarantee, however. For a long time, the Node project tended to go its own way, implementing Node-specific APIs and methods of accomplishing development work. Many within Node originally felt that TC39 was forcing their standards on the Node project, despite Node havings its own needs and solutions. There are a number of examples where Node went one way, and the JavaScript standards went the other - Promises and imports are two good examples. However, the Node steering committee today is much more open to adopting standards, any many of its members participate in discussions with TC39 regarding new features and changes to JavaScript. This is in part because developers want the same language and APIs in both the browser and their Node environments, but also, because there are other runtimes to consider when developing JavaScript code. This standardization has brought about a number of changes to the language and the JS ecosystem, as more voices are coming together to work on new solution to existing problems. What does TC39 do? As I mentioned, TC39 is a committee focused on developing and ensuring the JavaScript standard. From their website, "Ecma International's TC39 is a group of JavaScript developers, implementers, academics, and more, collaborating with the community to maintain and evolve the definition of JavaScript." The committee takes proposals from the community, and determines which are going to be worked on to be implemented in the JavaScript standard. A number of major companies are directly involved with TC39, with members representing Microsoft, Google, Apple, Intel, Mozilla, eBay, and more. Some are connected to universities, while others participate as individuals. In addition to voting members, many people participate in discussions regarding the various proposals that have been submitted. While the committee itself only meets every two months, these discussions on the proposals and specifications are taking place publicly, and anyone can participate in the conversation. Proposals are hosted on GitHub, and so discussions are as simple as creating an issue or pull request. A TC39 Discourse page is another way for the JavaScript community to discuss any current proposals or new ideas that haven't been formalized yet. When the committee votes to approve a new standard, this change is then implemented in the runtime authors (such as Google's V8). But how does a new standard get added to JavaScript? The Stages of Proposal There are 5 stages to adding a new standard to JavaScript, starting at Stage 0. Each of these stages has different requirements for completion. There is no time limit on moving a proposal from one stage to the next, and no guarantee that a given proposal will be completed. TC39's website hosts a process document that explains in detail what a given stage means, and how a proposal advances to the next stage. Let's walk through the stages, and look at some of the proposals currently at each stage. Stage 0 The first stage for any proposal is stage 0. This stage is the first step in adding a feature to JavaScript. Anyone can make a proposal. You don't have to be a member of TC39. A detailed document outlines the process for submitting a new proposal into stage 0. The pain purpose of this stage is to start a conversation and begin formalizing the proposal in order for future work to be done with it. The first thing that needs to be done when a proposal is stage 0 is to find a champion. A champion is someone from TC39 who will take the lead on moving a proposal forward. In addition, work will need to go into the documentation for the proposal, such as an outline of the problem that is being addressed and a high-level API design. Once these requirements are met, the committee can vote to move the proposal to Stage 1. An interesting Stage 0 proposal is to add a deprecated global or directive to the language, so that it's easier to alert a developer when a given API has been deprecated. Example: ` Stage 1 The purpose of Stage 1 is to make the case for changing the JavaScript standard, describing the proposed solution, and any potential problems that it could cause or could be impacted by. The main goal of the committee for a Stage 1 proposal is to devote time to examining the problem, and ensuring the proposal resolves it. Typically, browser/runtimes won't make any changes to implement a Stage 1 proposal, because the API could still change pretty drastically. However, polyfills or demos may be created in order to get additional feedback on a given API. These features should not be considered production ready. Once the initial spec has been developed, the committee can vote to move the proposal to Stage 2. The pipeline operator is a great example of a Stage 1 proposal. Its goal is to add a pipeline operator (|>) to JavaScript, in order to pipe function returns or values from one function to the next. There has been some discussion around how it should pass arguments into the second function ` Another Stage 1 proposal is the compartments proposal, which helps resolve a number of issues regarding global scope of a JS file or application. Check it out! Stage 2 When a proposal reaches Stage 2, the committee is focused on writing a precise syntax using formal language. This still doesn't mean that a feature is going to make it to JavaScript, but some experimental implementations will start appearing. This process to create a defined syntax could take from months to a year, with some proposals sitting in Stage 2 for much longer than that. However, when a feature leaves Stage 2, it typically means that the proposal will eventually make it to the final spec. Changes may still happen, but typically only limited changes will happen once a proposal moves out of Stage 2. There are a number of interesting proposals in Stage 2 at the moment, including decorators and iterator helpers. Often, proposals may get stalled in Stage 2. Decorators are a good example of that. According to the TC39 proposals repository, Decorators haven't been presented since September 2020, and were originally discussed back in 2018. Sometimes, the problem being solved has multiple solutions, or there are multiple competing solutions that could be adopted. Other times, the problem turns out to be less urgent or important than previously thought. While it can be frustrating to have a proposal stall out, it's important to remember that any change to JavaScript is permanent - no standardized feature in JavaScript is removed from the spec. Better to move slowly than to end up with half-finished APIs that don't actually solve anything. Stage 3 Stage 3 is the final stage for changes to be made to the specification. Spec compliant implementations will start to roll out, typically behind feature flags, in order to get developers to start using the feature and provide feedback. Changes are still possible, but they are expected to be limited in nature. The new Temporal object is a Stage 3 proposal that's pretty exciting for the JS ecosystem. Temporal will act as an upgrade from the Date object and support additional feature such as time zones. A prototype polyfill can be found on NPM, although keep in mind that it doesn't create a global Temporal object like the finished spec would do. And again, remember that this is still a proposal, and should not be treated as production ready. Another great example of a Stage 3 proposal is Realms, which provides a way to create distinct global environments. Stage 4 When a proposal reaches Stage 4, it is considered complete and ready for implementation by the different runtime vendors. Browsers will start to ship the feature, and other runtimes like Node and Deno will also work to include it in upcoming versions. A features is ready for Stage 4 when it passes all the agreed upon tests, and there has been sufficient testing by developers to ensure that the API is sound. Once a feature is in Stage 4, its spec is not intended to be changed. This is to ensure that the web platform is stable into the future - it's important to not break the web with changes to JavaScript. Two good examples of recent Stage 4 proposals are nullish coalescing and Promise.any. These features have been released into major browsers, and are available to be used today in modern JavaScript applications. Conclusion It's pretty amazing that the JavaScript language is developed in the open like this, for all interested parties to add a voice to the discussion. Not every standard or programming language is developed like this. However, this level of openness can also be difficult, especially if a specific feature gets stalled or a proposed API ends up not being accepted. If you submit your own proposal to TC39, remember that you are trying to solve a specific problem, not simply create a feature in JavaScript. Your proposal may be adjusted or replaced as other voices are added to the discussion. Also, keep in mind that it could take a long time for a proposal to make it into the language, if ever (looking at you, decorators). Also, while I've highlighted mostly good things about this process, it's also possible for a single member to hold back a feature from advancing into the next stage. This can be frustrating, but as noted above, it's important for JavaScript to be developed methodically. Having multiple standards or multiple interpretations of those standards wouldn't benefit anyone, after all. At the end of the day, remember that TC39 is made up of indivduals who are invested in the JavaScript ecosystem, and want to work together with developers to improve the language. They have a lot of context and understanding for how features are implemented that developers may not have. Proposals that don't make it into the language may not make it for valid reasons. Does any of this interest you? Do you want to contribute to the discussion? You can find ways to participate on TC39's website, including links to their Github and Discourse....

Progressive Web Apps and Mobile Apps cover image

Progressive Web Apps and Mobile Apps

Introduction In a talk by Alex Russell titled, "The Mobile Web: MIA", Alex discussed a number of issues facing the expansion of the web platform on mobile devices. He noted that, "People don't use the web the way they lean on it, and rely on it, and come to depend on it on desktop." In his talk, he noted that people use the web about 4% of the time they are using phones, and dropping. The rest of the time, users are typically interacting with mobile apps, rather than the browser. Much of this is driven by the fact that companies that own the platforms (Google and Apple, in particular) are primarily focused on native app developments. This focus on mobile app development pushes the market to accept the importance of a mobile app, and drive users to leave the web for a native experience. Mobile app development locks experiences to devices and operating systems, which increases the cost of development. Users, in turn, grow to expect mobile applications, even for simple tasks such as viewing bus routes or filling out a form. As web developers, we know there is another option for app development. Progressive Web Apps (PWAs) are built with standard web technologies - HTML, CSS, JavaScript, and modern browser APIs - to provide an enhanced experience when using them on supported platforms. By utilizing the latest browser features, web developers can construct the same experiences users expect of native applications. These features include: accessing the camera, notifications, Bluetooth and network information- even augmented/virtual reality and payments. Browsers have been working to support these features for years, and some companies (such as Twitter) have been building PWAs to provide an improved experience for their platforms. Let's say that we have a company, BetterX, which is looking to build a new app for their users. The primary goal is to provide an excellent experience for mobile users, including offline support and hardware features such as notifications and payments. We will explore and compare the benefits of native mobile applications and PWAs, and discuss why each platform may be the better choice. Progressive Web Apps - The Open Web One of the key benefits when considering a progressive web app is that we are utilizing modern web development tools to build our application. As web developers, we are already familiar with a number of complex tasks, such as state management, caching, and performance optimization. To build a PWA, we need to take these concepts to their natural conclusions. By utilizing a service worker to cache assets and IndexedDB or other methods to store local data, we can build a system that is capable of working fully offline. By using network detection, our application can determine whether an internet connection is available, and provide that information to the user. Another benefit of building with web technologies is that we have a better chance of achieving the goal of, "write once, run anywhere". By utilizing standard architecture patterns in our application, and relying on progressive enhancement as the browser/platform we are running on allows, our PWA can run on both mobile devices (as an installed app) or on browsers. Most developers are already familiar with responsive design, which allows a website to change its appearance depending on the viewport or device. The same concepts can be applied to a PWA, incrementing our functionality as the device allows it, and providing a fallback for when certain features are not available. Web development also has the benefit of traditionally being cheaper than mobile app development. Smaller companies don't always have the time or money to invest in a mobile development team. Most of them, however, do have a website. By utilizing some of the APIs available to progressive web applications, these shops and companies can provide a mobile experience. Also, if a website/web app is built with mobile devices in mind, the time it takes to build a fully functional PWA could be weeks, compared to a brand new mobile application taking months. PWAs can also be significantly smaller than their native alternatives. In a report by Google, the Twitter PWA "requires less than 3% of the device storage space compared to Twitter for Android". As fewer mobile devices have ports for expanded storage space, the size of applications becomes increasingly more important. Drawbacks However, there are some drawbacks to choosing a progressive web app. Users expect to find mobile applications in the app store, not on a website. In his talk, Alex Russell shares a screen of an Android device with a Google search bar at the time, and a row of icons at the bottom, including the Google Play Store. He explains that people click on the search bar when they are looking for "answers", and click on the store when they are looking for "experiences". For PWAs, the way to install them is to visit the URL, and click on the install button when prompted. This is not how users have been trained to find apps for their smartphones. It's also not clear to a user what installing a PWA achieves. On an Android device where a user installs a PWA, an icon for that app appears on their desktop as any other app would. However, depending on the app, this could be a complete experience, including offline support, or it could simply be a wrapper to load a website. In many cases, a PWA is little more than an enhanced bookmark on a mobile phone. Mobile Applications - Platform Builders Mobile apps are the standard established by Google and Apple for delivering user experiences on phones and tablets. Apps are an expected feature of any new platform - it's rare to see a new service thrive without a presence in the Google Play Store, or Apple App Store. Keystone applications, like Facebook or Twitter, are regularly highlighted by these platforms as a way to bring new users into their walled gardens. Users are trained to search for, and install, mobile applications. Often, websites will guide users directly to the respective app store. On iPhones and iPads, the app store is the only way for apps to be installed on a phone, making the store even more crucial to a product's success on the platform. Since Apple does not support PWAs in Safari, this makes mobile development a requirement to reach customers within their ecosystems. Mobile development has first-class support from both Apple and Google, providing access to APIs and features as new hardware is released. Apps being developed for newer devices can do more, and utilize more resources than ever before. Resource-intensive apps like Adobe Photoshop or Procreate can leverage these resources to achieve results previously held only on desktops and laptops. Modern mobile development frameworks, such as Flutter and React Native, allow developers to target these devices in a cross-platform way. They provide access to the APIs and features of the hardware, and a streamlined way to write a majority of your app once, while targeting multiple platforms. Other frameworks such as Cordova or Capacitor even allow for using modern web technologies, and having a fully bundled app that can be released on the app store. Downsides Mobile development provides amazing functionality and allows for powerful applications to be built. However, it comes at a cost. These applications can only run on the latest and greatest hardware and OS version. Most mobile users do not have access to the hardware we, as developers, are using to build our applications. What takes a few seconds to load on 5G using an iPhone 12 Max could take nearly a minute to load on phones common in most of the world. Also, final application sizes are going to impact how many users can actually download our app in the first place. In many cases, a mobile application in the app store could become more of a burden than a benefit. Consider that you're visiting a foreign country. Because you are roaming, your internet speed is significantly slower than you are used to. While traveling in a city, you want to check for bus routes and schedules. You go to the website for the municipal bus system, and are directed to download an app to view schedules. This app is not too large (my local bus system's app is 8.6 MB), but on your slower connection still takes a long time to download. Also, you may only need this app once or twice, before you travel to your next destination. A website (or PWA) would provide a much smoother experience than requiring a mobile app be downloaded. Considerations Regardless of which architecture you decide to use for building out your mobile application, there are some considerations to keep in mind. First, your developers are going to have better hardware and internet connection than many of your users. Most users do not have a high-end iPhone or Android device, and are not on 5G or gigabit internet. Whether you're building a PWA or a native app, remember that every megabyte will take substantially longer to download and initialize, and your application will run slower. If able, you should test your applications on slower or ittermitant internet speeds, or on lower end hardware. In general, if you are going to build a mobile application, it has to be lean, loadable, and support offline use. Many companies, (and some end users), will try to push for new features or content without regard for the experience of all users. Setting up a truly performant and offline-friendly experience is complicated, but it truly is worth taking into account all potential users as you work to build and deploy it. If you decide that building a progressive web app is the way to go for your app or company, it is important to remember that PWAs are not supported in Safari or on iOS/iPadOS. On both iPhones and iPads, the only browser engine is WebKit, regardless of which browser you are using. This means that users will not be able to install your PWA on Apple mobile devices, and the browser APIs may not be available. Take this into account while building your app, and allow for graceful degredation when features are not available. This is not to say that you shouldn't build a PWA if you want to target Apple's ecosystem - much the opposite! The more PWAs that exist, and have a large number of users on Apple's devices, the better chance that Apple will support the standardized browser features that enable PWAs. At the end of the day, choose the architecture that best supports your users, and build with them in mind. Your app should help your users and customers in some way, and should not be a burden to them. It may be fun to try out a new mobile framework, or build a PWA with enhanced features, but not if it does not serve the end user....

Getting Started with Tailwind in Vue cover image

Getting Started with Tailwind in Vue

Introduction Tailwind CSS has taken the front end development world by storm. If you haven't heard of it yet, Tailwind describes itself as, "a utility-first CSS framework packed with classes like flex, pt-4, text-center, and rotate-90, that can be composed to build any design, directly in your markup." Rather than using semantic class names (like "button" or "title"), you utilize these smaller utility classes to build your components. There are many ways to integrate Tailwind within a Vue application. In this article, we'll be talking about a few of the best practices for installing and utilizing Tailwind in a Vue application. Vue CLI Installing Tailwind in a Vue CLI app is probably the easiest experience available. There is a CLI plugin that does everything we need, including installing Tailwind, and configuring PostCSS for us. Even better, it works for both Vue 2 and 3! In your terminal, at the root of your application, run the following command: ` The CLI will then install the plugin. You will be asked how complete a Tailwind configuration file you want (none, minimal, or full). Choose "minimal" for now - it will create the file, but will not populate it with any custom values. Once the terminal has finished, you're done! There are a couple considerations to keep in mind when using this approach: - As of this writing, the CLI plugin has not been updated in a few months. It is currently installing Tailwind version 2.0.2 (the current latest is 2.2.4). This is easy enough to adjust manually, but you need to be aware of it to make the change yourself. - The plugin is also providing the PostCSS 7 compatible build. For the most part, this doesn't make a huge difference. The Tailwind docs notes that the compatibility build is identical with the main build, so you won't be missing out on any features. For now, this should be fine, but it could lead to issues down the road if you want to use PostCSS 8 plugins (or if Tailwind decides to stop providing backwards-compatible builds). If you're using Vue 3.0.6 or greater, you should be able to upgrade to the main builds. However, your should make sure to update all your PostCSS plugins to the latest main builds, not just Tailwind. Vite The Tailwind Docs include instructions on installing Tailwind with Vite. While it isn't as straightforward as the Vue CLI plugin, it's still pretty simple to get started in Vite. First, install Tailwind, PostCSS 8, and Autoprefixer in your repository: ` Then, run the Tailwind CLI command to create a default configuration file for both Tailwind and PostCSS: ` This will create two files: - tailwind.config.js - postcss.config.js Unless you want to make a change to either Tailwind or PostCSS, you won't have to touch these files. The docs recommend that you configure the purge option, which is used by Tailwind to purge its unused styles. With this change, your tailwind.config.js file should look like this: ` Note that we also added the mode: 'jit' key to the configuration. This enables Tailwind's Just-In-Time mode, which is a more performant experience, and enables a number of other Tailwind features. Read the documentation to learn more, and better understand what you're getting out of this. Now, let's create a base CSS file to import Tailwind's classes into. The Tailwind docs suggest using ./src/index.css, but I would recommend creating a separate CSS file just for Tailwind. Let's call it tailwind.css. In that file, put the following: ` This uses PostCSS to import the various classes and utilities that Tailwind uses. Then, we just need to import this file in our main.js file like this: ` And we're done! Tailwind is now available in your Vite application. Make sure to clear out the index.css file of any styles you do not want. In practice, that file will probably be much smaller than you may be used to, but it's still useful to keep your custom styles separate from Tailwind. Nuxt Nuxt has an amazing plugin ecosystem, and there is a Tailwind plugin available for us to use here as well. Unlike with Vue CLI, there's a bit more manual work involved to install a Nuxt plugin, so let's get started! First, make sure your Nuxt application is at least version 2.15.3. This is the release of Nuxt that supports PostCSS 8. Unlike Vue CLI, this plugin provides the main build of Tailwind, not the compatibility build for PostCSS 7. You can upgrade Nuxt by running npm update nuxt. Once you have confirmed that your Nuxt app is ready, run the following command in your terminal: ` This will install Tailwind and its required plugins for you. Then, in your nuxt.config.js, add the plugin to your buildModules: ` Finally, run npx tailwindcss init to create the Tailwind configuration file. And that's it! Tailwind is now ready to go in your Nuxt application. You may notice that there's a lot less work here than in Vite (and a lot fewer files being created than in Vue CLI). With the Nuxt plugin, if you do not have a tailwind.css file where styles are being injected, Nuxt will create it for you (the same is true for the tailwind.config.js file, but typically you'll want that anyway to enable JIT mode, or other custom configurations). Tailwind Viewer One benefit of the Nuxt plugin is the Tailwind Viewer. This allows you to previous the styles currently enabled in your Tailwind configuration, and copy/paste the classes into your UI. By itself this is a great feature, but when you start working with custom colors or other features, it can be invaluable to preview them before adding the classes to your template. Conclusion We've walked through adding Tailwind to a Vue application in three of the most common tools to build Vue applications. In general, the process is the same: 1. Install Tailwind, PostCSS, and Autoprefixer 2. Configure Tailwind and PostCSS 3. Import a CSS file that includes the Tailwind classes to your application If you are using a custom setup for your Vue site, you should be able to apply these steps to your own setup in order to get Tailwind up and running. There are also a number of resources available if you run into any problems, including a Discord server and GitHub Discussions, where you can ask for help from other Tailwind users. A note on utility-first CSS As I'm writing this, I'm aware that utility-first CSS is not for everyone. If that sounds like you, and you made it this far, one thing about Tailwind that isn't highlighted as much as it should be is how the configuration allows for building a coherent design system. How often do you see multiple configuration files for Sass that are nothing but variables ($primary, $red, etc.)? All of this can be handled within Tailwind's configuration file, with the added benefit of generating classes for text, backgrounds, borders, and more to utilize those colors. Also, Tailwind already comes built-in with a number of great design decisions for padding and margin, flexbox, and more, making the work of building a design system that much easier. Also, Tailwind provides a special directive, @apply, that lets you utilize Tailwind classes in standard CSS. This means that, rather than using the utility classes Tailwind provides in your template, you can build out custom classes (like .card or .button) while still benefitting from Tailwind's other features. Even better, using a plugin like PostCSS Nested, you can get nested CSS, just like in Sass or other CSS preprocessors. Conisder the following component: ` This is a fairly simple button, with a background color, padding on width and height, and a hover attribute. Already, we can see a number of other changes to make (add some drop shadow, transitions, different styles for disabled state, and so on). Using purely Tailwind, that could lead to a large number of classes on the button element. Let's say you don't like having those large class attributes, but you still want the other benefits of Tailwind. Here's the same example, using @apply to build a custom .button class: ` Nice! Our template is a lot simpler, and the styles are contained in our style block. That said, most Tailwind users will probably want to go without using @apply (Adam Wathan, the creator of Tailwind, has said that he almost never uses @apply). That doesn't make this any less valid, and is a perfect way to balance to strengths of Tailwind with traditional methodologies of writing CSS like BEM. If you're hesitant to try Tailwind because of its large number of classes, give this a try and see how you like it! And remember, the goal of using Tailwind or any other CSS framework or methodology is to build a stellar experience for your application's users. Don't lose focus of that while deciding whether to adopt Tailwind in your latest Vue application. Try it out, see if it works for you, and decide for yourself whether it fits your project or style of writing CSS. Until next time!...

Vue 3.1 - Official Migration Build from Vue 2 to 3 cover image

Vue 3.1 - Official Migration Build from Vue 2 to 3

Introduction A few weeks ago, the Vue core team released the first minor version of Vue 3 (release notes here). This latest release of Vue brought a number of fixes and changes, but most importantly, introduced a migration path from Vue 2 to Vue 3. Bilal Haidar previously wrote about how to upgrade a Vue 2 application to Vue 3. But at this point, the Vue core team has provided a straightforward method to upgrade an existing application. In this post, we'll explore the path to upgrade an existing Vue 2 application, as well as when to hold off on your upgrade. We will be referencing the official Migration documentation as we go through this blog post. What is the Migration Build? From the Vue documentation: > @vue/compat (aka "the migration build") is a build of Vue 3 that provides configurable Vue 2 compatible behavior. > The migration build runs in Vue 2 mode by default - most public APIs behave exactly like Vue 2, with only a few exceptions. Usage of features that have changed or have been deprecated in Vue 3 will emit runtime warnings. A feature's compatibility can also be enabled/disabled on a per-component basis. What does this mean for us? In general, it means that components and application features that were written in Vue 2 will be made compatible in Vue 3. This incldues breaking changes such as v-model, which had a syntactic change in Vue 3. The intent of the migration build is to provide a way to shift your version of Vue from 2.6 to 3.1, without having to rewrite every component before it works. During the upgrade, you will be able to start up your application and see a list of warnings reported in the browser console. These warnings reflect the various API changes between Vue 2 and 3, and are meant to guide you through the upgrade process. If you were to upgrade without the migration build, your app would crash on the first error, leading to a very frustrating experience. The migration build is _not_ intended to be used long-term. It is provided as a way to upgrade your codebase from Vue 2 to Vue 3, not as a way to continue using deprecated APIs. If your application is reliant on Vue 2 for a library or deprecated API, I would recommend holding off until Vue 2.7 is released (which will backport some new Vue 3 features such as the composition API). That said, you absolutely can deploy your Vue 3 application with the migration build if you have to. From the documentation: "If you do get your app running on the migration build, you can ship it to production before the migration is complete. Although there is a small performance/size overhead, it should not noticeably affect production UX." Keep in mind that this is not the intent of the migration build, and plans should be made to migrate completely as soon as possible rather than rely on the migration build to keep old code working as expected. Ready to Upgrade? We will be using this repository as an example for our Vue 3 migration. It is a Vue CLI application that implements a few components: v-model, Vuex, and Vue Router. Note that we aren't using any major third-party libraries like Vuetify or Nuxt. Why does that matter? Per the documentation: > Dependencies that rely on Vue 2 internal APIs or undocumented behavior. The most common case is usage of private properties on VNodes. If your project relies on component libraries like Vuetify, Quasar or ElementUI, it is best to wait for their Vue 3 compatible versions. > The migration build can be used for [server-side rendering], but migrating a custom SSR setup is much more involved. The general idea is replacing vue-server-renderer with @vue/server-renderer. Vue 3 no longer provides a bundle renderer and it is recommended to use Vue 3 SSR with Vite. If you are using Nuxt.js, it is probably better to wait for Nuxt 3. Because these large frameworks rely on a great deal of internal APIs or custom configuration, we will have to wait until they release their own compatible builds for Vue 3. Luckily, each of these has new builds under development already. - Vuetify has released an alpha build of its component library. I would expect a final build sometime this year. - Quasar has already previewed a release candidate, and should be ready to release in the near future (at the time of writing). - Nuxt has not announced anything publicly, but in a conversation with Daniel Roe from the framework team, private alpha builds have been in testing since Q1 of 2021, with public builds expected by the end of June/start of July. A final build is expected by the end of 2021. Keep in mind that the Vue documentation is only referencing the larger libraries and frameworks. There may be a library that you use in your project that is incompatible with Vue 3, and could cause errors even with the migration build. Upgrading to Vue 3 is a major change, and issues should be expected. Also, there are some applications that simply won't be able to migrate, even with the migration build. Some larger applications could simply be too much effort to migrate to Vue 3, or rely on certain undocumented features that are not available in Vue 3. Per the documentation, "If your application is large and complex, migration will likely be a challenge even with the migration build. If your app is unfortunately not suitable for upgrade, do note that we are planning to backport Composition API and some other Vue 3 features to the 2.7 release." Perform Upgrade To perform the upgrade, we will be following the same steps as noted in the documentation. For our purposes, we will be using the Vue CLI instructions. But please refer to the documentation for using a custom Webpack build or Vite with Vue 2. Upgrade dependencies The first step we need to take is upgrade our tooling. Since we are using Vue CLI, we can simply run vue upgrade in the terminal. If you have any Vue CLI dependencies that need to be updated, you will see a screen like the below: Proceed with the upgrade for each of the plugins. You may be asked for additional input depending on which plugins you have installed. In my case, I was not prompted for any additional input. Next, we need to update the version of Vue that we're using as well as install the migration build. Run the following commands in your terminal: ` With these commands, we are installing Vue 3.1, the migration build, and the single-file component compiler for Vue 3. We then uninstall vue-template-compiler, which was the SFC compiler for Vue 2. You can also make these changes manually in package.json if you prefer. With that, our dependencies have been updated. Consider making a commit at this point so you don't have to start from scratch if something goes wrong. Remember, this is a migration - ensure your work is being backed up just as if you were upgrading your operating system! Configure compatibility mode in Webpack Now that we have our dependencies, let's enable the migration build. To do that, open (or create) your vue.config.js file at the root of your project, and add the following code: ` What does this do? Rather than use Vue 3 itself (which would throw errors), we are configuring Webpack to use the migration build instead. We then add some configuration to vue-loader (which is used by Webpack to bundle Vue components) to return a compatConfig object. This object allows us to determine which features are enabled or disabled, and whether we are running in Vue 2 or Vue 3 mode. This configuration is available at both the global and component levels. In this instance, we are simply setting our application to Vue 2 mode, and leaving all compatibility features enabled. Start up your application At this point, we should be able to start our newly-migrated Vue 3 application, but we are far from done. Start up your Vue application (npm run serve for a Vue CLI app) and see what happens. You may get a number of compile-time errors (features that are completely incompatible with Vue 3, such as filters, could cause errors at this step). Fix those, then your application should render and act properly in the browser. If you open your browser tools, you will probably notice a number of warnings in the console. Some of them will be related to your own code, others may be dependencies that you're using. Here's an example from the test runner app: For this example, I'm not using Vue.set(), or destroyed, or most of these other warnings being reported. That's because they all come from dependencies that I'm using (in this case, Vue Router). At this point, let's upgrade our dependencies to use the latest version of the libraries, which should resolve the errors. Upgrade dependencies Let's start with upgrading Vue Router. Run the following command in your terminal: ` We'll first upgrade our router. For this application, it's pretty straightforward. If you have a more complex router configuration, here's the documentation on upgrading Vue Router. Below is our updated configuration (removed code is commented out): ` Let's go through the changes. 1. Rather than import Vue and VueRouter into the file, we use the functions createRouter and createWebHashHistory. The first is what generates the router (rather than invoking new VueRouter()), while the second defines the mode (history replaces the mode option in the previous version, and is now required). 2. Because plugins are no longer registered with Vue.use(), this line is removed. 3. We create the router using createRouter() instead of new VueRouter(), including the routes and the required history key. Now let's update our Vuex store. In this code example, we're using a module, which does not have to be changed (how Vuex works has not changed). We only need to update how our Vuex store is initialized. ` The same concepts we used in the router are applied here. Rather than invoking new Vuex(), we use the new function createStore(). We also don't need to register Vuex, because that's not how plugins are registered in Vue 3. Finally, let's update our initialization of Vue. Open main.js, and make the following changes: ` As with the router and Vuex, we no longer use new Vue(), and instead use createApp(). We then chain .use() after that function call to include the router and store (and any other plugins you may have), rather than supplying them as keys in an object. Finally, we call .mount('#app') to render the application in the browser. If we restart our application at this point, we should have cleared out most, if not all, of the errors we were seeing previously. Go through the remaining errors you see, and try to resolve them. Any components with errors should be properly reporting such in the terminal (see below screenshot). While writing this article, I noticed that the Vue Router was still causing at least one warning to be thrown. Make sure you read the warning message before you start to worry too much, it could just be an alert that there is a behavior change. Compat Configuration During this step, you can configure the compatConfig at either the global or component level. For example, if you have components that are using the destroyed lifecycle hook (instead of Vue 3's unmounted), and you really want to disable that feature, you can make the following configuration change: ` Using this configuration, the destroyed lifecycle hook will not be called. In this case, Vue will continue to report any instances of the destroyed hook in your code, and note that not fixing it could cause runtime errors. Other warnings, such as changes to how watch functions, can simply be disabled if you understand the change, and the risks involved. Remove Compat Build Now that we've finished the upgrade, it should be safe to remove the migration build (remember, it's not meant to be included forever!). Run the following command: ` Then, make sure you undo your changes in vue.config.js. Since we aren't using the compatibility build any more, the alias can be removed, and the vue-loader config can be completely removed. Try running your application again, and make sure you don't have any lingering errors. If not, congratulations! You have successfully migrated to Vue 3! Conclusion In this article, we have taken a fairly simple application through the migration process from Vue 2 to 3. Depending on the complexity of your application, you may run into more incompatible features that need to be adjusted, or you may be blocked altogether due to a library/framework you rely on not being ready for Vue 3 yet. Remember that the ecosystem is still working towards adopting Vue 3 as the default, and there is still some way to go before everything will work as we expect. Also, keep in mind that if your application won't be able to make the upgrade, Vue 2.7 will be bringing a large number of features for Vue 3 back to Vue 2. Remember - this is a major upgrade. Errors and warnings are expected. It could be frustrating at some points. Keep this in mind, and stay curious! If you weren't following along, here's the link to the application we upgraded. Have a good one!...

Custom Composable Methods with Vue 3 cover image

Custom Composable Methods with Vue 3

Introduction One of the greatest strenghts of modern Javascript frameworks is the ability to reuse components. Components (especially Vue's single-file components) allow you to build a reusable piece of code that handles the template, styling, and logic for a part of your application. Building frontend applications with components is fairly ubiquitous at this point, with the most popular frameworks all adopting this style of building an application. However, there are times that you need to share code between components. Either it's a utility to help with form validation or an API request that needs to be made from different parts of your application. Maybe it's a timer, where the rendering of the countdown is different depending on what page you're on. Whatever the case may be, you will find yourself in a situation where building a component ends up duplicating code rather than reducing it. Until Vue 3 and the Composition API, we had two methods of handling this - utility functions imported for a single use or implementing mixins for our Vue components. Utility functions can do a lot, but they are typically framework agnostic and do not handle state or other Vue features. Mixins can cover a large number of cases, but they cause a lot of issues on their own: - Multiple mixins could use the same key names, causing values to be different than expected. - It's not obvious what methods and attributes are available within a component, leading to confusion about what is and is not present on this. - While they help with reusing logic in multiple components, we can't pass parameters into a mixin to customize it for our needs. This leads mixins to be a bit too rigid in practice, and end up being less reusable than we'd like. In this article, we will explore new options for creating reusable code - custom methods using the Vue 3 Composition API. If you haven't read my previous articles on the Composition API, I highly recommend reading them before we continue. We won't be using all of these concepts in this article, but it's helpful to keep in mind everything you can do with the Composition API. - "Ref" and "Reactive" - "Computed" - "Watch" and "WatchEffect" Example - Weight Conversion Let's start with an example of a mixin where we convert a weight to other weight scales. It allows us to configure whether the weight is pounds or kilograms, and uses computed properties to generate the correct measure for each type we want to support. For this example, we will handle pounds, kilograms, metric tons, and short tons. Here's what that mixin might look like: ` Great! Now in our template we can bind to weight and weightType and utilize the various weights as needed. There are a couple problems with this mixin, however. Because of its nature, it won't be obvious within the component which weights are supported. What if another developer is expecting imperial tons? Or grams? By using this mixin, the developer would need to examine the mixin in order to know for certain what was available. Also, there is a risk that any of these values could be overridden within the component. If this.lbs is defined anywhere else, it will probably cause errors with the other computed properties. This is due to the mixin not being encapsulated from the rest of the component. Let's address both of these problems by converting this mixin to a custom Composition API method. We'll still have the benefits of using computed properties and triggering Vue's reactivity system, but the code will be more explicit to future developers. ` Let's go over what we're doing here: 1. We have created a function that is the default export called useWeights. This is a common naming scheme in React for similar types of functions (custom hooks) and has been picked up in the Vue community as well. 2. We're using ref to store two local variables - weight and weightType. Because we are using ref, any updates to them will trigger Vue template rendering and the computed properties will recalculate. 3. We are using computed to create four computed properties, just like in our mixin. 4. We then return the refs and computed properties. Now, in our Vue component, we can import this function and use it to access these variables. ` By utilizing our useWeights function, we can instantiate the values needed to perform our weight conversions. All of the logic to handle our conversions is bundled inside of the custom function, which means there's no chance of accidentally changing what lbs is supposed to be. And because we are destructuring the return from useWeights, we have full control over the variable names being returned, and they are explicitly set in our component. No more guessing what values are available! For those of you familiar with Typescript, you may have noticed that our useWeights function implements a small amount of type checking for its inputs. The weightType, for example, has the implicit type of Ref. This means that in our Vue component, we can see exactly what types are expected, and even get proper error messaging. For example, setting weightType.value to an unexpected value of "MT" causes the Typescript error, Type '"MT"' is not assignable to type '"LBS" | "KG"'. This isn't as simple to get if we were using mixins! Example - API Requests Let's look at another example of a custom Composition API function - making API requests. This is a great example because we can bundle a lot of logic that we'd otherwise need to handle in our component. ` In this function (useDadJoke), we have two more functions - fetchJoke and refetchJoke. fetchJoke is the main content here, using Fetch to make a request, confirm its status, and then return the result. refetchJoke simply calls fetchJoke and stores it on our local ref, joke. We then declare the two refs that we need, status and joke, then return everything but fetchJoke. This provides the two refs and the refetch function to our Vue component. We can then use this custom function in our component like this: ` As we can see, all of our logic is now contained within the custom function, and can be reused in other components as well. We could also refactor our useDadJoke function to accept a URL as an argument. We could also leverage common libraries like Axios here, and put our configuration into the custom function. This is a great example of what we can do with custom Composition API functions. Conclusion Custom Composition API functions have a lot of potential to clean up application code. They really highlight the benefit of what the Composition API provides - being able to group code by feature, rather than function. By breaking up your application's logic into separate functions that are then imported into components, it will be far easier to utilize that logic across your application. In addition, keep in mind that the Composition API (and Vue's reactivity system) can be used in other contexts than a Vue single-page application. The same logic that you are using in your frontend could also work on your Node backend! By bundling this logic into functions, it helps prevent code duplication in more than just your Vue app. One last thing - a large number of libraries are already being created to leverage custom Composition API functions. A great example of this is Vue Use by Anthony Fu, which includes a large number of functions for everyday use. Some of them include: - useEventListener - useDebounce - onClickOutside - useOnline - useInterval/useTimeout - useStorage (localStorage and sessionStorage.) Other libraries, like Vuex and Pinia, also provide custom functions to integrate their libraries using the Composition API, with more on the way. Just like with the React ecosystem when Hooks came out, the Vue ecosystem is slowly adopting these new changes and finding where they work best. Now is a great time to start working on custom Composition API functions for your own applications! Until next time!...

Provide/Inject API With Vue 3 cover image

Provide/Inject API With Vue 3

Introduction One of the most difficult problems to solve when building single-page applications is state management. With component-based frameworks like Vue, this is typically solved in one of two ways: 1. State is managed by components, with data being passed to child components as props. The parent component then listens for events and performs actions on the state accordingly. 2. State is managed with a global state management library (Vuex, Redux, etc). Global state is then injected into the desired components, and those components triggers actions in the state management library (such as API requests or data updates). This can provide a layer of separation between logic and templating which is useful, and helps with passing data between parts of the application. Vue offers an interesting middleground between these two approaches with the Provide/Inject API. This API is similar to React's Context API, in that it allows a component to provide some sort of data to any component beneath it in the component tree. For example, a parent component could provide a piece of data (say, the user's username), and then a grandchild component could inject that value into itself. Provide/Inject gives developers a way to share data between parent and child components while avoiding prop drilling (passing a prop from one component to the next in a chain). This can make your code more readable, and reduce the complexity of your props for any components that don't rely on this data. In this article, we will explore a basic example using the Provide/Inject API, building up a new application with a user dashboard to update their name and email. Getting Started - Using Props We'll first set up our example application using the standard approach to passing data between components - props. Below is our homepage and a navigation page. ` ` This is a very straightforward parent/child structure. Our parent component has a reactive object (state) with a user's name and email. The username is passed into the Nav component as a prop, and then displayed. This works well because we only have two components, but what if there were other layout components between the root and the navigation component? We can use Provide/Inject to send this data from the parent to the navigation component. In our parent component, we will provide the data we want available (the username), and then inject that data into the Nav component. Provide/Inject with Composition API Let's start with App.vue, and explore how to use provide. Below is our rewritten root component: ` With Vue 3, we have access to a Composition API method provide. This function takes a key and a value. The key is how the provided value will be accessed in other components. In this example. we are passing a computed property with the user's username as the value. Why are we passing a computed property? By default, the provided value is not reactive. If we just wrote provide('username', state.name), any component that injected this value would only have the initial state of the username. If it were to change in the future, the name would be out of sync with the root component. If we wanted to provide the entire state, we could write this instead: ` That's because we're using a reactive object for our state. Alternatively, if the username was a ref, we could also use that in the same way. Keep in mind that any value could be passed as an argument to provide, including functions. This will come up later in our example, but it's important to think about when you're wanting to use this API. Let's look our our navigation component now, using inject to get the value of state.name. ` Similar to what we did in App.vue, we used a Composition API method called inject to get the value. inject takes the key we used when providing the data, and then returns the value as a variable. Since we provided a computed property, inject returns a computed property as well. inject has two other arguments as well: 1. defaultValue: This is the value that should be returned in the event a provided value is not found with that key. 2. treatDefaultAsFactory: As I noted above, any value (including functions) can be provided as a value to inject. In the event that a function is what you are providing, you don't want it to be invoked by mistake. But what if you're providing an object? Unless it is returned from a function, you could end up with duplicate objects that have the same reference (this is also why data and props recommend returning objects from a function rather than setting them directly). When using a function as the default value, this argument tells inject whether the default is the function itself, or the value returned by the function. The two below examples return the same default, a string: ` In our component example, the username is being injected and returned from setup, making it available in the template. We can then use that variable as we normally would, but without having to worry about props. Nice! This could save us a lot of time and effort with prop drilling. Providing Reactivity In the last section, we discussed reactivity and how the argument in provide needs to be a reactive object if we want data to stay in sync. Let's build out our user dashboard so they can update their name and email. In our App.vue, we're going to add a single line to our setup method: ` This will provide the entire state object (a reactive object) to whatever component wants to inject it. Now, let's build out a dashboard to work with that data: ` In this component, we inject the entire userDetails provided value, and return it to the template. We can then use v-model to bind directly to the injected values. With this in place, it all works as expected! Any changes made to the username field would properly update in the navigation as well. However, there's a small catch to doing things this way. Per the Vue 3 documentation, "When using reactive provide / inject values, it is recommended to keep any mutations to reactive properties inside of the provider whenever possible." The reason for this is that allowing any child component to mutate a value could lead to confusion about where a particular mutation is happening. The more disciplined our codebase is about mutating state, the more stable and predictable it will be. Rather than directly using v-model on our reactive state, let's provide a couple functions that will do the updating for us. First, we'll update App.vue to handle the new providers: ` We have added two functions - updateUsername and updateEmail. These functions are nearly identical, just updating the value on our state object that they are associated to. We then provide these two functions using the provide method, so that they are available to children components. Remember above when we discussed that any value could be provided? This is why treatDefaultAsFactory is important. Here, we are providing two functions that don't return anything. If inject by default invoked the function and returns its value, we would get undefined is not a function errors in our child component. In this case, we're really wanting a function to be injected into our component, so defaulting treatDefaultAsFactory to false is excellent. Here's the updated code for MyProfile.vue: ` Rather than binding directly on userDetails, we created two computed properties, each with a getter and setter. We can then bind to the computed properties, since they will return the desired value (username or email) and trigger the update methods we injected. Now our reactivity is fully controlled by the root component, App.vue, rather than the child. Global State Management I mentioned above that Provide/Inject gives us a middle ground between global state and component state. With the introduction of the Composition API, however, there's no reason we can't use Provide/Inject as our global management. Let's take everything we've written so far and extract it to a separate file: ` In this file, we have two functions - initStore and useStore. initStore creates our reactive object, getters for both the username and email, and methods to perform updates, then provides each of those values. These three groups (state, computed, and methods) maps very nicely to how Vuex works (state, getters, and actions). The second method, useStore, simply returns an object with the injected values. This lets us use the store we've created from a single location, so if we change the key used in provide, we can also update it in the inject. This ensures we aren't duplicating our inject calls, and we only have one file to check if something goes wrong. Our App.vue file is now a lot simpler: ` Since we don't need the store values in our root component, we can safely call initStore to generate the store, and provide its values to our child components. Then, in MyProfile.vue, we can do the following: ` Because useStore injects the values for us, we have access to the username, password, and their update methods. This is one of the ways that the Composition API can help keep our code clean, and our logic bundled by feature rather than functionality. If this concept interests you, there's a library for Vue 2 and 3 called Pinia that takes this approach to the next level. Pinia provides you a typesafe, easy to maintain global store. Check it out! Conclusion Using Provide/Inject can help remove some of the complexity of passing data between parent and child components. Keep in mind that provided values do not have the same checks as props, such as required or type, so they are inherently less safe to use. There is no guarantee that the value you want to inject is present in the component tree, nor do you know for certain what shape that value is in. Also, up until recently, the Vue documentation included, "provide and inject are primarily provided for advanced plugin / component library use cases. It is NOT recommended to use them in generic application code." This has since been removed, and libraries like Pinia show the power of using this API in application code. I would still recommend being careful when choosing to implement a feature using Provide and Inject. That said, have fun and try it out! Here's a link to a Stackblitz example of the final form of the appliation we worked through above. Until next time!...

Vue 3 Composition API - watch and watchEffect cover image

Vue 3 Composition API - watch and watchEffect

The Vue 3 Composition API introduces watch and watchEffect for flexible reactivity. watch can lazily trigger side effects and provide current and previous values, while watchEffect immediately responds to state changes....

Computing Application State in Vue 3 cover image

Computing Application State in Vue 3

Introduction We've all been there before- working on an application, and suddently we need to determine what state something is in. Maybe it's whether the form has been submitted already, or the class a certain element should have. You may be tempted to set that value to a variable, and move on. What's the harm, right? It turns out that there are a number of reasons this could be a problem. Your state could be out of date due to a change from a different function. You could cause an 'impossible state', or a state in the UI that was never intended by the developer. And at the very least, your code is more imperative, meaning that you as the developer are having to write and maintain more lines of code. It's almost like manually juggling all the values in your application - what happens if you drop one? Luckily, Vue provides a solution for this - the computed property. With the computed property (or Composition API method), we can perform calculations like we described above by declaring them and getting a readonly, reactive ref to use in our application. Vue is able to determine when a dependency in the computed property has changed, and recalculate its result. We can then use these calculated values as if they were another variable, and use them in our template and logic with ease. Setting up our example Let's start with a common example: You have been tasked with building a form that accepts a name, email, and comments. We want to track the number of characters a user has entered, and allow them to submit the form. Below is an example of this form: ` The above form provides the basic functionality that we need. But there are a few things missing: - We're just logging out messages when the API comes back. We should report the error or success state to the user. - We probably shouldn't allow users to submit the form without filling out the fields. - We also shouldn't allow users to submit the form while their submission is being processed. We don't want to receive duplicate form entries. With this in mind, let's rewrite our code. The template is the same, but we'll need to track the form's state - whether it has been submitted or had an error, and whether the user can click the submit button. Sounds simple enough, so let's add some booleans - submitting, hasError, hasSuccess. That should handle the state. We don't want the user to have to click a "Validate" button, of course - that would be frustrating. So let's use the watch Composition API method, and calculate whether to show the submit button. Below is our updated code: ` The way this is written works, but there are a number of issues: - In the onSubmitFormHandler, we are manually resetting each status to where it should be. - In addition, the fact that we are using multiple booleans to track our form's state means that we could end up in an impossible state, where both "submitting" and "hasError" are true. That could lead to unexpected, and unpredictable, user experiences. - The watcher does the job of recalculating whenever the formState is changed, but we're still having the manually track this value. - We're still manually handling the input event on the comments field to get the character count. Most importantly, it's going to be much harder to refactor this going forward, because it's harder to determine what is going on. Is there a relationship between "hasError" and "hasSuccess"? What is calling "validateHasInput"? Implementing Computed Property Let's upgrade our application using the computed Composition method now. First, we'll replace the boolean states with a single state, and use computed properties to determine what state we are in. We can also use a computed property to get the character count, and remove that extra event on the comment textarea. Finally, we'll move the validateHasInput into its own computed property. Here's the updated form: ` We have now refactored to use the computed property. What does that give us? - The boolean values (submitting, hasError, hasSuccess) are no longer being set imperatively. They are being calculated based off of a status variable. If we were using something like Typescript, we could force the type of status to be a certain subset, but for now, we are using an object with a few states - including IDLE. - The character count is now being calculated off of the length of the comment string, rather than checking the length from the emitted event. This means that if we change the value of formState.comment programmatically, our character count is up to date without us having to change anything. - In the onSubmitFormHandler, we set which status our form is in as we go. We aren't having to set each status individually, which means we don't have the possibility of impossible states. Using Getters in Vuex Our application is in a much better state now, but there is still room to improve. One way we can better encapsulate our logic is by utilizing Vuex for managing the state of the form submission. Luckily, we can implement our computed logic in Vuex pretty easily with getters. In Vuex, getters fill the same role as computed properties in single-file components. Let's move our API logic out of the component, and into Vuex: ` In Vuex, we create a store, which is then plugged into our app. This store contains our state (the submission status), a single mutation to handle updating the state, and an action for submitting the form. We then have our four computed properties, which match the three we had previously as well as getting the raw state. By using Vuex, the state of our form submission is disconnected from the template. This can be useful when building out larger applcations, so that your components remain focused on the user experience, and the logic is handled within your global state management. Here's what our updated component's logic looks like: ` In our component, we are now importing useStore in order to access our Vuex store. The component then makes an API call via a dispatch, which calls our action. Neat! Using Get/Set with Computed Properties One more nice feature of computed properties is how they interact with ES5 getters and setters. If you aren't aware, with ES5 you can add a function to an object that is either a get() or a set(val). This function is not invoked like a normal function, but instead, whenever the value is read or assigned to. For example: ` With computed properties, we can leverage this system to build additional functionality. Let's say that you want to add a reset function to the form. One potential way to do that could be like this: ` With the Composition API, if you pass an object in as the first parameter (rather than a function), you can use the get and set keys to make your own custom getter and setter. This way, rather than making the function call directly to store.commit, we can simply set the currentStatus to the status we want. This ability to set to computed properties is especially useful when using v-model on a custom component. Below is an example where this can help to model a custom input. ` By leveraging getters and setters in your computed properties, you can cut down on the amount of code you need to write in order to perform bindings between components and Vuex. You can also add additional logic, such as validation or data cleanup. Just make sure that you aren't overburdening your setters- if they start to get large, you might need to use a separate function anyway. Conclusion Computed properties can help ensure that your applications are easy to maintain and understandable. By leveraging a computed property, rather than manually assigning values, you allow the framework to do the heavy lifting for you. Computed properties are great for a number of complex tasks, such as: - Form status - API calls - Formulaic calculations (temperature conversion, weight conversion) - State-based CSS classes - Determining which state to render your component in. Keep in mind - computed properties should not cause side effects. If you are writing a computed property that needs to alter your state, it should probably be a watcher. Take a look at your own applications, and see where using a computed property could be beneficial. Have fun!...

Improve User Experience in Vue 3 with Suspense cover image

Improve User Experience in Vue 3 with Suspense

Not every user will have the same connection speed that developers have. They may experience parts of your application that appear to be broken or stuck while content is loading. Vue 3 provides a new way to handle situations like this, called Suspense....

Let's innovate together!

We're ready to be your trusted technical partners in your digital innovation journey.

Whether it's modernization or custom software solutions, our team of experts can guide you through best practices and how to build scalable, performant software that lasts.

Prefer email? hi@thisdot.co