Let's consider we're building a menu navigation. It's a simple list of anchors to other pages right? Oh, and we also want to link our social media accounts there. No big deal, our trusty anchor tag can handle any type of link!
That was more or less the story with links before shiny front-end frameworks come into play. Now, for Vue.js, Vue Router introduced a router-link
component. Nuxt.js extended it with nuxt-link
, so did Gridsome with g-link
. Similarly, Next.js has a simply named link
component.
Basically, what all those link components are providing is client-side routing. This is also known as "not hard reloading the page when navigating". Some also do provide other fanciness like page prefetching but that's not what interests us here. Indeed, because now we have them, we have to decide each time, depending on our link type, between them and the regular HTML anchor tag.
Due to that, most of the time we'll end up using the framework component with internal links and an anchor tag with external ones. While this gymnastic is not too complicated, it can become an issue when we don't know what kinds of links we're dealing with. This happens especially when those are coming from user-generated content or a CMS.
So how can we handle that case within Vue.js? v-if/v-else
everywhere is an answer but I don't really like it. Let's instead build together a smart link component that we'll be able to reuse from project to project!
A Smart Link Component
Before writing anything let's state something first: links are opinionated. Indeed some people like to do links with a specific flavor while some others might prefer another one. Here we'll build a link component with a flavor that I like. Of course, feel free to drift to yours while following along!
Now that we're clear on that, let's define what we need to do:
- Figure out the interface of our smart link component;
- Define a way of choosing between the HTML anchor tag and our framework link component;
- Implement the two above within a functional Vue.js component;
- Improve and iterate on our link component.
Looking good? Let's go!
Our Dream Interface
When it comes to interfaces I have a confession to make. I don't like the to
prop name most of our framework link components use. So let's state that we'll use href
as our prop to tell our smart link component its destination.
Another thing that can be hard to deal with is the native target="_blank"
attribute:
- Why has it to be
_blank
instead of simplyblank
? Short answer: it can be used to target a previously named window to open the link in. The underscore is here to define the blank special use case. More info here~ - When adding a blank target to a link it's also better to add a
rel
attribute to prevent some malicious usages. Let's never forget about it by automating that part later on.
Okay, I think that's it! With that we'll be able to use our new link component as follows:
Now that we're happy with our interface, let's define, and actually code, the logic that will choose between an anchor tag and the framework link component.
To Anchor or Not to Anchor
In that part, we need to write a function that figures out for us what link tag needs to be used. Let's name it: getLinkTag
and state that it will return either "a"
for an anchor tag or "router-link"
for our framework link component.
We use"router-link"
here as it's Vue Router default. If you want to use this smart link component with Nuxt.js, for example, simply replace the string with"nuxt-link"
.
As for its inputs, let's assume our function gets the component props we defined earlier as an object. With that we are starting with something like this:
So how do we know what to return? A really easy way that works most of the time is to check if the link we want to create, our href
prop, starts with one /
, but not two, as //example.com
is a valid external link. If a link passes these two checks, then it leads to an internal page:
We're already pretty happy with that! There's one edge case to consider although: if our link uses a blank target, meaning we used our blank
prop, then, whatever the input href
prop is, we need to return the HTML anchor tag. Indeed, while powerful, our framework link components aren't meant to handle this type of link. Let's implement that:
That's already it for our getLinkTag
function! We can now to export it. We'll also give it a quick refactor for maintainability, turning our keywords into constants and our two checks on href
into a single regular expression test:
We are done with the preparation work! Let's now dive into the heart of the matter with our actual smart link component.
A Low Level Vue.js Component
As teased we won't code a regular Vue.js component. Instead, we'll code a functional Vue.js component. What does that mean? Well, we can think of functional components as lower-level regular ones:
- They are stateless, meaning they do not create reactive data;
- They also do not have access to
this
, making them instanceless; - Finally, they do not have access to Vue.js lifecycle.
So when we remove all of that, what are we left with?
- Props, functional components can receive props;
- A render function, it allows us to programmatically define its template.
The point of using a functional component here is that our smart link component doesn't need much actually, so let's keep it to the minimum. The render function will also help us map our props correctly to whatever our getLinkTag
function tells us to render. Let's start by declaring our component:
As we can see functional components takes the shape of an object with the following properties:
functional
: it is always set totrue
and tells Vue.js that this component is... functional;props
: this is our array or object props definition like we'd define one on a standard component;render
: is our render function, it receives two arguments:createElement
andcontext
, we'll explain them later on.
Now that we're more familiar with the shape of our functional component let's start by filling its props. In the first step we defined our smart link component interface, we just have to represent it here:
Great! We'll focus on our render function now. As we can see, it receives two arguments. The first one, createElement
, is a helper function that returns a virtual node instance. We'll need to use it in the return
statement of our render function. The second one, context
, replaces this
and contains information about our component instance: props, slots, etc.
Let's start slowly by actually rendering the tag our getLinkTag
function returns us:
OK, so what have we done here? We imported our getLinkTag
function. Then, we executed it inside our render function, passing it our props from the context
object it receives. Finally, we created an element with the tag it returned back.
In the createElement
function call, we also passed two additional values:
context.data
, it's an object that contains Vue.js and HTML attributes used on our component (class
,style
,v-on
, etc.);context.children
, this one is our component default slot! That is to say, whatever gets passed between its opening tag and closing tag. Indeed, we want it to behave just like a regular tag at that level.
With that, we're already outputting the correct tag with the correct content on our front-end. One issue though, we didn't map any attribute yet to it! Perhaps it even led to a fatal error in our render. Let's fix that~
As explained the second argument we're passing to createElement
is its attributes. We are going to extend this object with the extra information we need in it. Because the data object is quite a complex one, we will use vue-functional-data-merge. It's a widely adopted utility specifically designed for that use case. Install it with your package manager of choice:
We can now use it within our component:
Let's break down what we did here. We imported mergeData
from our freshly installed package. We also brought along our constants we defined earlier inside getLinkTag.js
. We then proceeded to create our attributes, named attrs
, and props
objects to fit in the data object. Here we mapped our props to fit each tag interface, also taking into account the blank
prop. Finally, we merged it with existing context.data
using our imported helper, and forwarded the result to the createElement
function.
Eeew! I hope this part was clear enough~ At least I have one great news for us. Our component is now working! Try it along, it should now behave as expected, following the interface we defined in the first part. Cool! Well, actually, I lied. I also have one bad news for us. But feel free to take a break before now!
OK, fresh? The last issue we're facing is related to Vue.js event system. When we want to listen to native click events on a component we have to use the .native
modifier. This works as expected when our smart link component actually renders the framework link component, but fails when it renders an anchor tag. Indeed as we're forwarding events inside the data object directly to the created element, the HTML anchor tag has no idea what to do of a .native
event. Thankfully, we can easily fix that by turning .native
events into regular ones when our render function wants to render an anchor tag:
Voilà! We also added the export so our link component is now ready for production! No worries if you're not quite there yet, full code will be shared at the end of the article. But stay with me! We still have some exciting things to see together~
Iterating to Make This Component Our
This is perhaps the most important step. What we coded so far is a pretty solid smart link component that works everywhere. Its interface is pretty basic, but it provides little to no fanciness. Now comes time for you to make it more opinionated in the way you'd like it to. So go ahead! Add additional features that will be useful for you across the different websites you'll build with it. That's the kind of power Vue.js gives us.
No idea what to do? OK let's implement a little one together. What about a new external
prop that we'll use to force a link to be rendered with an anchor tag? This can become useful if we want, for example, to link to an image inside our static
folder:
Looks great? Let's code it! The first thing we need to do is to reflect it on our interface, to do so, we update our props definition:
Good! Now we need to take into account this new prop by updating our getLinkTag
function:
Done! That's all we needed to do. Now if we slap an external
attribute to any kind of link it will force our component to render it with an HTML anchor tag.
Migrating to Vue 3
There's every chance that some of you already knew about it: functional components are getting somewhat deprecated with Vue 3. Hehe, stop! Don't run away. They are not actually deprecated: with Vue 2, functional components were coming with a nice performance gain, Vue 3 functional components don't provide it anymore. Not that they got slower, that's rather regular components that got as fast! Therefore, there's indeed much less point in pushing for functional components. Instead, we should rather use the new render functions that also make our life easier while being quite similar. Here's what a refactor of our smart link component could look like with render functions:
So what are the differences? Our props remain the same. The render function no longer receives createElement
and context
: we now have to import the first one as h
from Vue, and can access the context through this
. We also no longer have to worry about native events, nor about merging with a complex data object. Our attributes and props objects also got merged into one, and that's the only thing we now have to forward. Cool! One drawback although, we now have to resolve our framework link with the resolveComponent
function coming from Vue.
Overall, I have mixed feelings about those changes. They are quite convenient still! That's just that I've always been quite a hipster about the thing I use...
To Conclude...
Every time I start to work on a new post I'm like: "Oh I'll talk about this, sounds simple to do", and then I spend hours trying my best to explain it clearly. I hope it was!
As promised, here's a gist with the final code we discussed, even the Vue 3 one!
Here are also the references from Vue.js documentation if you want to dig the topic deeper on your side:
- Vue 2 Functional Components;
- Vue 3 Migration Guide for Functional Components;
- Vue 3 New Render Functions.
One last cool thing you could do with your brand new smart link component is publishing it as a package! This way you can easily maintain it and reuse it across your projects. That's what I did for mine, you can find its code here, and install it as @lihbr/utils-nuxt.smart-link
from GitHub's registry (remember what I was saying about being a hipster...). Oh, and by the way, I've written an article about hosting private packages on GitHub two months ago, check it out!
Anyway, happy to discuss this article or answer your questions on Twitter, thanks for reading!