Skip to content

Instantly share code, notes, and snippets.

@stowball
Last active October 11, 2020 10:00
Show Gist options
  • Save stowball/c523f263ae1c95f7a998c0d94409bf5f to your computer and use it in GitHub Desktop.
Save stowball/c523f263ae1c95f7a998c0d94409bf5f to your computer and use it in GitHub Desktop.
<!----><div data-v-d671142e="" class="body"><h1 data-v-d671142e="" class="title">The new v-model</h1><!----><div data-v-d671142e="" class="lesson-body"><p>As you probably know,<code>v-model</code> allows us to very quickly and easily capture an input’s value into the state of our application. Every time the user types or interacts with an input, <code>v-model</code> will let the parent know so that it can update our state.</p>
<p>In Vue 3, <code>v-model</code> has gone through a redesign that gives us more power and flexibility when defining how this double binding should be done.</p>
<hr>
<h2>Kicking it off with Native inputs</h2>
<p>Let’s start by looking at a native input element.</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p>In Vue 2, whenever you add a <code>v-model</code> declaration to a native input element, the compiler produces a block of code that handles the correct input value and event to be listened to.</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">input</span> <span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span> <span class="hljs-attr">v-model</span>=<span class="hljs-string">"myValue"</span> /&gt;</span>
export default {
data() {
myValue: null
}
}
</code></pre>
<p>This strategy works fairly well, but what if our component uses a dynamic value to set the type of input?</p>
<p>I’m sure you’ve been in a situation where you have created an input component that can either be a type of <code>input</code> for someone’s name, for example, and that with the change of a property you use it as a type <code>email</code> for their email address.</p>
<hr>
<h2>How does it compile?</h2>
<p>Because Vue 2 cannot “guess” what type of element this is going to be at runtime, due to the possibility of the data changing at any given time, the Vue 2 compiler is forced to output a very lengthy and verbose block of code to handle <em>every possible scenario</em>.</p>
<p><img src="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2FL2-1.opt.jpg?alt=media&amp;token=335c17d1-1c86-4327-ae2c-a619dd164b82" alt="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2FL2-1.opt.jpg?alt=media&amp;token=335c17d1-1c86-4327-ae2c-a619dd164b82"></p>
<p>Don’t worry, we don’t need to go over every line of code. Just know that it basically has to prepare for every type of possible scenario.</p>
<p>In Vue 3, outputting this amount of code is no longer necessary because <code>v-model</code> for input elements behaves almost the same way it does for custom components — with an extra module that helps Vue decide which prop/event to apply in each case.</p>
<p>The compiled result in comparison is incredibly smaller.</p>
<pre><code class="hljs language-javascript">h(<span class="hljs-string">'input'</span>, {
<span class="hljs-attr">modelValue</span>: myValue,
<span class="hljs-string">'onUpdate:modelValue'</span>: <span class="hljs-function"><span class="hljs-params">value</span> =&gt;</span> {
myValue = value
}
})
</code></pre>
<hr>
<h2>The new defaults</h2>
<p>In Vue 3, when creating a component that has <code>v-model</code> capabilities we need to use a new set of defaults for creating the <code>v-model</code> binding.</p>
<p>In Vue 2, no matter what type of native input you were binding to inside the component you would always bind the value of your data to a <code>value</code> property, and you would listen to an <code>input</code> event.</p>
<p>Of course there was a way to modify this default behaviour by declaring a <code>model</code> property in our Vue component, but that’s the Vue 2 API and we’re not going to look in depth into it.</p>
<p>In Vue 3, we now have two new defaults. For the <code>prop</code> that binds the input of the value, we use <code>modelValue</code>, and for the emitted event we use <code>update:modelValue</code>.</p>
<p>I want you to play close attention to the names of the events before you panic about the verboseness of these new defaults. Did you notice how <code>modelValue</code> is actually present in both of them?</p>
<p>The new emit default <code>update:modelValue</code> can be extracted into two different parts. The declaration that something is being updated <code>update:</code> and the model that is being updated <code>modelValue</code>.</p>
<p>This is going to play a very important role later on in the course, when we look at how to create multiple <code>v-model</code> bindings into a single component!</p>
<p>Now, let’s take a look at how the code of a simple <code>input</code> wrapper component might look with the new Vue 3 <code>v-model</code> syntax.</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
<span class="hljs-attr">:value</span>=<span class="hljs-string">"modelValue"</span>
@<span class="hljs-attr">input</span>=<span class="hljs-string">"$emit(
'update:modelValue',
$event.target.value
)"</span>
&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">props</span>: {
<span class="hljs-attr">modelValue</span>: {
<span class="hljs-attr">type</span>: [<span class="hljs-built_in">String</span>, <span class="hljs-built_in">Number</span>],
<span class="hljs-attr">default</span>: <span class="hljs-string">''</span>
}
}
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<hr>
<h2>Using the new v-model in component instances</h2>
<p>Now that we have the base for a <code>v-model</code> capable component, let’s take a look at how we would use it in an application.</p>
<p>As with any other component, we need to import it and declare it in the <code>components</code> object for our parent.</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> BaseInput <span class="hljs-keyword">from</span> <span class="hljs-string">'./BaseInput'</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">components</span>: { BaseInput }
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Next, we add the component to our <code>template</code> so that we can declare the <code>v-model</code> on top of it. After, we are going to use the object syntax (no composition API this time!) and use a <code>data</code> object to create a simple reactive state.</p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">BaseInput</span>
<span class="hljs-attr">v-model</span>=<span class="hljs-string">"myInput"</span>
/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> BaseInput <span class="hljs-keyword">from</span> <span class="hljs-string">'./BaseInput'</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">components</span>: { BaseInput },
data() {
<span class="hljs-keyword">return</span> {
<span class="hljs-attr">myInput</span>: <span class="hljs-string">''</span>
}
}
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>At this point you’re wondering, we’ll that’s all right and nice, but I already know this!</p>
<p>Fair enough, but I wanted to show you one last thing before we wrapped up our lesson. Using <code>v-model</code> like this is now actually a shorthand! The binding has been modified to now accept an intermediate parameter before the binding.</p>
<p>So <code>v-model="myInput"</code> is actually now a shorthand for <code>v-model:modelValue="myInput"</code></p>
<p>You’re probably wondering at this point why this is important or actually useful. In the next lesson when we dive into more advanced parts of the new <code>v-model</code> system, we’re going to talk about <em>multi</em> <code>v-model</code> bindings, and this new syntax is going to play a very important role.</p>
<hr>
<h2>Coming up next</h2>
<p>Now that we understand the basics of the new <code>v-model</code> system and its improved bindings, let’s go into the next lesson and look at a couple other new tools that it provides us for component development.</p>
<p>We will finally look at the multi <code>v-model</code> capabilities that I’ve been hinting at, and a cool new feature to build our own modifiers.</p>
<p>Can you think of any components in your current Vue 2 applications that will benefit or be able to be enhanced already by these new features?</p>
<p>See you in the next lesson!</p>
</div></div><!---->
<div data-v-d671142e="" data-v-33673d7f="" id="lessonContent" class="lesson-content unlock"><!----><div data-v-d671142e="" class="body"><h1 data-v-d671142e="" class="title">Multi v-model bindings</h1><!----><div data-v-d671142e="" class="lesson-body"><p>In our last lesson, we went over the most common way to use <code>v-model</code> in a Vue 3 application, and we looked at the changes that the default bindings for input and event listeners bring. These are things we need to know in order to use <code>v-model</code> effectively.</p>
<p>It is now time to go deep into one of the advanced changes that Vue 3 brings in terms of <code>v-model</code> functionality — multi <code>v-model</code> bindings into a single component.</p>
<hr>
<h2>v-model: Not one, but many</h2>
<p>A really interesting feature of Vue 3’s <code>v-model</code> is the ability to add multiple <code>v-model</code> bindings to a single component instance.</p>
<p>Have you ever created a complex component that <code>$emit</code>s an object as the payload, and inside the object you have a couple of different properties? Maybe you had to pass the parent a lot of information, which forced you to have those properties? Wouldn’t it have been easier if you could have just bound to each of them separately?</p>
<p>Although this approach is fairly common, it brings a bit of overhead in understanding what a component is doing or what you’re supposed to receive out of a <code>v-model</code> payload. Instead of being able to quickly see what’s happening in the <code>template</code> of a component, you have to dig into the function that receives the payload and read through the implementation to figure out where each of the properties of the object are going to be used.</p>
<p>In Vue 3, with the addition of the multi <code>v-model</code> binding, a complex payload is no longer needed.</p>
<hr>
<p>In order to demonstrate multi v-model capabilities, and to better learn how it works, we are going to create a component called <code>SalutationName</code>. This component will have two inputs inside of it — a <code>select</code> element that will allow the user to select their desired salutation, and an <code>input</code> for them to type in their name.</p>
<p>We’ll start out with the template, by adding these two new fields, value bindings, and some attributes.</p>
<p><strong>📃SalutationName.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">select</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"salutation"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">""</span>&gt;</span>-<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">option</span>
<span class="hljs-attr">v-for</span>=<span class="hljs-string">"item of salutations"</span>
<span class="hljs-attr">:value</span>=<span class="hljs-string">"item"</span>
<span class="hljs-attr">:key</span>=<span class="hljs-string">"item"</span>
<span class="hljs-attr">:selected</span>=<span class="hljs-string">"salutation === item"</span>
&gt;</span>
{{ item }}
<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">select</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
<span class="hljs-attr">:value</span>=<span class="hljs-string">"name"</span>
<span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
<span class="hljs-attr">name</span>=<span class="hljs-string">"name"</span>
/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p>Now that we have our markdown ready, let’s take a look at our <code>script</code>. We need to create a <code>salutations</code> array to populate that dropdown, plus declare a couple of <code>props</code> that we can bind to the values we already declared.</p>
<p><strong>📃SalutationName.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">const</span> salutations = [
<span class="hljs-string">'Ms.'</span>,
<span class="hljs-string">'Mrs.'</span>,
<span class="hljs-string">'Miss'</span>,
<span class="hljs-string">'Mx.'</span>,
<span class="hljs-string">'Dr.'</span>
]
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">props</span>: {
<span class="hljs-attr">salutation</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
<span class="hljs-attr">default</span>: <span class="hljs-string">''</span>
},
<span class="hljs-attr">name</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
<span class="hljs-attr">default</span>: <span class="hljs-string">''</span>
}
},
setup () {
<span class="hljs-keyword">return</span> {
salutations
}
}
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>We declare two props, <code>salutation</code> and <code>name</code>. Both will be of type <code>String</code> with an empty string as a default value.</p>
<p>Notice that both these <code>props</code> are already binding to their respective elements in the <code>template</code>. The <code>input</code> through the <code>value</code> binding, and the <code>select</code> through the <code>selected</code> binding in the <code>option</code> loop.</p>
<p>In the <code>setup</code> function of our component, we are going to return the <code>salutations</code> array that we declare at the top of the <code>script</code> tag, so that the <code>select</code> element can <code>v-for</code> loop through it and populate all the <code>options</code>.</p>
<p>Finally, let’s add <code>$emit</code> calls for each of the inputs to broadcast their respective events.</p>
<p><strong>📃SalutationName.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">select</span>
<span class="hljs-attr">name</span>=<span class="hljs-string">"salutation"</span>
@<span class="hljs-attr">change</span>=<span class="hljs-string">"$emit('update:salutation', $event.target.value)"</span>
&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">""</span>&gt;</span>-<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">option</span>
<span class="hljs-attr">v-for</span>=<span class="hljs-string">"item of salutations"</span>
<span class="hljs-attr">:value</span>=<span class="hljs-string">"item"</span>
<span class="hljs-attr">:key</span>=<span class="hljs-string">"item"</span>
<span class="hljs-attr">:selected</span>=<span class="hljs-string">"salutation === item"</span>
&gt;</span>
{{ item }}
<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">select</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
<span class="hljs-attr">:value</span>=<span class="hljs-string">"name"</span>
@<span class="hljs-attr">input</span>=<span class="hljs-string">"$emit('update:name', $event.target.value)"</span>
<span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
<span class="hljs-attr">name</span>=<span class="hljs-string">"name"</span>
/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">const</span> salutations = [
<span class="hljs-string">'Ms.'</span>,
<span class="hljs-string">'Mrs.'</span>,
<span class="hljs-string">'Miss'</span>,
<span class="hljs-string">'Mx.'</span>,
<span class="hljs-string">'Dr.'</span>
]
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">props</span>: {
<span class="hljs-attr">salutation</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
<span class="hljs-attr">default</span>: <span class="hljs-string">''</span>
},
<span class="hljs-attr">name</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
<span class="hljs-attr">default</span>: <span class="hljs-string">''</span>
}
},
setup () {
<span class="hljs-keyword">return</span> {
salutations
}
}
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Notice that we are using the <code>update:X</code> format for Vue 3’s <code>v-model</code> binding syntax that we learned in the last lesson.</p>
<p>For the <code>select</code> element, since we’re binding it to the prop <code>salutation</code>, we will emit <code>update:salutation</code>.</p>
<p>For the <code>input</code> element, since we’re binding it to the prop <code>name</code>, we will emit <code>update:input</code>.</p>
<p>Finally, we can <code>import</code> this new component anywhere in our app to use it.</p>
<p><strong>📃App.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"app"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">SalutationName</span>
<span class="hljs-attr">v-model:salutation</span>=<span class="hljs-string">"form.salutation"</span>
<span class="hljs-attr">v-model:name</span>=<span class="hljs-string">"form.name"</span>
/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">pre</span>&gt;</span>{{ form }}<span class="hljs-tag">&lt;/<span class="hljs-name">pre</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { reactive } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>
<span class="hljs-keyword">import</span> SalutationName <span class="hljs-keyword">from</span> <span class="hljs-string">'./components/SalutationName'</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">name</span>: <span class="hljs-string">'App'</span>,
<span class="hljs-attr">components</span>: {
SalutationName
},
setup () {
<span class="hljs-keyword">const</span> form = reactive({
<span class="hljs-attr">salutation</span>: <span class="hljs-string">''</span>,
<span class="hljs-attr">name</span>: <span class="hljs-string">''</span>
})
<span class="hljs-keyword">return</span> {
form
}
}
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Take a closer look at the <code>template</code> where <code>SalutationName</code> is being used. Did you notice the double <code>v-model</code> binding?</p>
<p><strong>📃App.vue</strong></p>
<pre><code class="hljs language-html">[...]
<span class="hljs-tag">&lt;<span class="hljs-name">SalutationName</span>
<span class="hljs-attr">v-model:salutation</span>=<span class="hljs-string">"form.salutation"</span>
<span class="hljs-attr">v-model:name</span>=<span class="hljs-string">"form.name"</span>
/&gt;</span>
[...]
</code></pre>
<p>The first <code>v-model</code> declaration is going to provide a two way binding from the property <code>salutation</code> of <code>SalutationName</code> to our state <code>form.salutation</code>.</p>
<p>The second <code>v-model</code> declaration is going to provide a two-way binding from the property <code>name</code> of <code>SalutationName</code> to our state <code>form.name</code>.</p>
<p>We can now open this example in our browser. The result is a fully functional double <code>v-model</code>ed component!</p>
<p><img src="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2FL3-1.opt.jpg?alt=media&amp;token=c715ff55-056c-4cc1-b9d0-f86a8df1ea90" alt="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2FL3-1.opt.jpg?alt=media&amp;token=c715ff55-056c-4cc1-b9d0-f86a8df1ea90"></p>
<hr>
<h2>Coming up next</h2>
<p>In this lesson we learned how to make a component ready for multi <code>v-model</code> bindings, and how to bind to an instance of this component with multi <code>v-model</code>s.</p>
<p>In the next lesson, we’re going to take a look at another cool advanced feature of Vue 3’s <code>v-model</code>, the ability to create our own custom model modifiers.</p>
<p>See you in the next lesson!</p>
</div></div><!----></div>
<div data-v-d671142e="" data-v-33673d7f="" id="lessonContent" class="lesson-content unlock"><!----><div data-v-d671142e="" class="body"><h1 data-v-d671142e="" class="title">v-model modifiers</h1><!----><div data-v-d671142e="" class="lesson-body"><p>In our last lesson, we learned how to create a component that is capable of multiple <code>v-model</code> bindings. We also used this component in our application, and applied two simultaneous bindings into an instance of the component.</p>
<p>This time around, we are going to learn another advanced capability of <code>v-model</code> in Vue 3, the ability to create our own custom <code>v-model</code> modifiers.</p>
<p>Let’s dive right in.</p>
<hr>
<h2>v-model: Make it special</h2>
<p>You’ve probably already used <code>v-model</code> modifiers before, Vue comes with quite a few out of the box.</p>
<ul>
<li><a href="https://vue-docs-next-preview.netlify.app/guide/forms.html#lazy">.lazy</a> - listen to <code>change</code> events instead of <code>input</code> (for native inputs)</li>
<li><a href="https://vue-docs-next-preview.netlify.app/guide/forms.html#number">.number</a> - cast valid input string to numbers</li>
<li><a href="https://vue-docs-next-preview.netlify.app/guide/forms.html#trim">.trim</a> - trim input</li>
</ul>
<p>Note that all of these are still available for you in Vue 3.</p>
<p>Let’s learn how to create our own modifiers by building upon the example <code>SalutationName</code> component that we created in the last lesson.</p>
<p>We are going to first add the ability to pass in a modifier called <code>capitalize</code> to both of our bindings.</p>
<p>Here’s the twist:</p>
<p>For the <code>salutation</code> binding, we’ll go ahead and capitalize the whole acronym.</p>
<p>For the <code>name</code> binding, we’ll just capitalize the first letter.</p>
<hr>
<p>We’ll start by adding the modifiers to our <code>App.vue</code>, where the instance of <code>SalutationName</code> is being used.</p>
<p><strong>📃App.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"app"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">SalutationName</span>
<span class="hljs-attr">v-model:salutation.capitalize</span>=<span class="hljs-string">"form.salutation"</span>
<span class="hljs-attr">v-model:name.capitalize</span>=<span class="hljs-string">"form.name"</span>
/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">pre</span>&gt;</span>{{ form }}<span class="hljs-tag">&lt;/<span class="hljs-name">pre</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
[...]
</code></pre>
<p>Notice that custom modifiers are declared just the same as out-of-the box modifiers, by adding a <code>.</code> after the <code>v-model:model</code> declaration and the name of the modifier.</p>
<p>Vue 3 is now going to try to inject two new <code>props</code> into our component.</p>
<ol>
<li><code>salutationModifiers</code> for the <code>salutation</code> v-model binding.</li>
<li><code>nameModifiers</code> for the <code>name</code> v-model bindings.</li>
</ol>
<p>Let’s update our <code>SalutationName.vue</code> component and add these new props.</p>
<p><strong>📃SalutationName.vue</strong></p>
<pre><code class="hljs language-javascript">[...]
<span class="hljs-attr">props</span>: {
<span class="hljs-attr">salutation</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
<span class="hljs-attr">default</span>: <span class="hljs-string">''</span>
},
<span class="hljs-attr">salutationModifiers</span>: {
<span class="hljs-attr">default</span>: <span class="hljs-function">() =&gt;</span> ({}),
<span class="hljs-attr">type</span>: <span class="hljs-built_in">Object</span>
},
<span class="hljs-attr">name</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
<span class="hljs-attr">default</span>: <span class="hljs-string">''</span>
},
<span class="hljs-attr">nameModifiers</span>: {
<span class="hljs-attr">default</span>: <span class="hljs-function">() =&gt;</span> ({}),
<span class="hljs-attr">type</span>: <span class="hljs-built_in">Object</span>
}
},
[...]
</code></pre>
<p>Notice that we are not declaring any particular default state for the <code>capitalize</code> property of these objects. We simply declare that each of these properties will default to an empty object.</p>
<p>Modifiers will be added to these props as booleans, which means that if no modifiers are received by the component instance, they simply will remain an empty object.</p>
<p>If a modifier like <code>capitalize</code> is added, the object will reflect it by adding a <code>true</code> value to the respective modifier that was added.</p>
<pre><code class="hljs language-javascript">{
<span class="hljs-attr">capitalize</span>: <span class="hljs-literal">true</span>
}
</code></pre>
<p>With this knowledge in mind, we are going to refactor our component. Let’s first move the <code>$emit</code> declarations out of the <code>template</code>, and make functions that will hold all of our logic.</p>
<p><strong>📃SalutationName.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">select</span>
<span class="hljs-attr">name</span>=<span class="hljs-string">"salutation"</span>
@<span class="hljs-attr">change</span>=<span class="hljs-string">"updateSalutation"</span>
&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">option</span> <span class="hljs-attr">value</span>=<span class="hljs-string">""</span>&gt;</span>-<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">option</span>
<span class="hljs-attr">v-for</span>=<span class="hljs-string">"item of salutations"</span>
<span class="hljs-attr">:value</span>=<span class="hljs-string">"item"</span>
<span class="hljs-attr">:key</span>=<span class="hljs-string">"item"</span>
<span class="hljs-attr">:selected</span>=<span class="hljs-string">"salutation === item"</span>
&gt;</span>
{{ item }}
<span class="hljs-tag">&lt;/<span class="hljs-name">option</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">select</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
<span class="hljs-attr">:value</span>=<span class="hljs-string">"name"</span>
@<span class="hljs-attr">input</span>=<span class="hljs-string">"updateName"</span>
<span class="hljs-attr">type</span>=<span class="hljs-string">"text"</span>
<span class="hljs-attr">name</span>=<span class="hljs-string">"name"</span>
/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">const</span> salutations = [...]
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">props</span>: {
<span class="hljs-attr">salutation</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
<span class="hljs-attr">default</span>: <span class="hljs-string">''</span>
},
<span class="hljs-comment">// Holds the modifiers for the salutation v-model</span>
<span class="hljs-attr">salutationModifiers</span>: {
<span class="hljs-attr">default</span>: <span class="hljs-function">() =&gt;</span> ({})
},
<span class="hljs-attr">name</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
<span class="hljs-attr">default</span>: <span class="hljs-string">''</span>
},
<span class="hljs-comment">// Holds the modifiers for the name v-model</span>
<span class="hljs-attr">nameModifiers</span>: {
<span class="hljs-attr">default</span>: <span class="hljs-function">() =&gt;</span> ({})
}
},
setup (props, { emit }) {
<span class="hljs-keyword">const</span> updateSalutation = <span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
<span class="hljs-keyword">let</span> val = event.target.value
emit(<span class="hljs-string">'update:salutation'</span>, val)
}
<span class="hljs-keyword">const</span> updateName = <span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
<span class="hljs-keyword">let</span> val = event.target.value
emit(<span class="hljs-string">'update:name'</span>, val)
}
<span class="hljs-keyword">return</span> {
salutations,
updateSalutation,
updateName
}
}
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Notice that we updated the <code>template</code> to reflect the two new functions — <code>updateSalutation</code> and <code>updateName</code> that we declared in the <code>setup</code> function of our component. For now, it does the exact same thing as before.</p>
<p>Notice also that we have modified the <code>setup()</code> function to accept a <code>props</code> parameter as a first argument, and <code>{ emit }</code> from the second argument.</p>
<p>The second argument is the <code>context</code> of the component instance, which in return holds an <code>emit</code> property which has the <code>$emit</code> function in it. We are using JavaScript deconstructing to extract only the <code>emit</code> function directly.</p>
<p>Now that we’re done refactoring, we can add some logic inside our setup <code>updateX</code> functions to modify the emitted value in case that a modifier is present. This is where the <code>props</code> that we added earlier are going to shine, since now we can use a simple <code>if</code> statement to check if they are evaluating to <code>true</code> and modify our emitted value.</p>
<p><strong>📃SalutationName.vue</strong></p>
<pre><code class="hljs language-javascript">[...]
setup (props, { emit }) {
<span class="hljs-keyword">const</span> updateSalutation = <span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
<span class="hljs-keyword">let</span> val = event.target.value
<span class="hljs-keyword">if</span> (props.salutationModifiers.capitalize) {
val = val.toUpperCase()
}
emit(<span class="hljs-string">'update:salutation'</span>, val)
}
<span class="hljs-keyword">const</span> updateName = <span class="hljs-function"><span class="hljs-params">event</span> =&gt;</span> {
<span class="hljs-keyword">let</span> val = event.target.value
<span class="hljs-keyword">if</span> (props.nameModifiers.capitalize) {
val = val.charAt(<span class="hljs-number">0</span>).toUpperCase() + val.slice(<span class="hljs-number">1</span>)
}
emit(<span class="hljs-string">'update:name'</span>, val)
}
<span class="hljs-keyword">return</span> {
salutations,
updateSalutation,
updateName
}
}
[...]
</code></pre>
<p>That’s it! Now we can go back to our browser and test our new modifiers in action.</p>
<p><img src="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2Fresults.jpg?alt=media&amp;token=8c902a59-f137-4bf7-ab60-8cd0c09f8194" alt="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2Fresults.jpg?alt=media&amp;token=8c902a59-f137-4bf7-ab60-8cd0c09f8194"></p>
<h2>Extra challenge</h2>
<p>Are you up for a little fun? Try implementing an extra modifier for <code>name</code> called <code>reverse</code> that reverses everything the user types.</p>
<p>Tip: You’ll have to chain the modifiers together in the <code>v-model</code> declaration like so:</p>
<pre><code class="hljs language-javascript">v-model:name.capitalize.reverse=<span class="hljs-string">"form.name"</span>
</code></pre>
<hr>
<h2>Coming up next</h2>
<p>In this lesson, you learned how to create and use your own modifiers for your <code>v-model</code> ready components.</p>
<p>In our next lesson, we are going to take a deep dive into <code>$attrs</code>, and some of the key differences between Vue 2 and Vue 3 regarding attributes like <code>class</code> and<code>style</code>.</p>
<p>We’ll also take a look at the disappearing act of <code>$listeners</code> and what that means for us developers in terms of component composition.</p>
<p>See you in the next lesson!</p>
</div></div><!----></div>
<div data-v-d671142e="" data-v-33673d7f="" id="lessonContent" class="lesson-content unlock"><!----><div data-v-d671142e="" class="body"><h1 data-v-d671142e="" class="title">The New $attrs</h1><!----><div data-v-d671142e="" class="lesson-body"><p>Welcome back!</p>
<p>In this lesson we’re going to explore the changes that Vue 3 brings to the <code>$attrs</code> of a component.</p>
<p>Understanding these changes is fundamental to the creation of any type of custom components in Vue 3, as the new <code>$attrs</code> not only contains your HTML attributes like it did before, but now also your listeners, classes, and styles.</p>
<p>By learning these new concepts, you will be able to understand how attributes are passed down to ANY component in Vue 3, as well as how to correctly bind events from a parent to a child component.</p>
<hr>
<h2>Differences between Vue 2 and Vue 3 in <code>$attrs</code></h2>
<p>In Vue 2, <code>$attrs</code> is a property of a component’s data that includes all of the attributes that the parent passes into a component, with the exception of <code>class</code>, and <code>style</code>. These two in particular reside at the same level as <code>$attrs</code> in the <code>data</code> object of a component instance.</p>
<p>Also in Vue 2, we had a <code>$listeners</code> object, which included all of the functions that would act as listeners when the child emitted an event.</p>
<p>So what exactly can we find inside <code>$attrs</code> now in Vue 3?</p>
<p>In Vue 3, we no longer have the <code>$listeners</code> part of the data object. This object has been fully merged into the <code>$attrs</code> object, with a few key differences.</p>
<p>Listeners are no longer listed as the exact keyword, such as <code>click</code>, or <code>input</code> like they were in Vue 2 under <code>$listeners</code>.</p>
<pre><code class="hljs language-html">// Vue 2
<span class="hljs-tag">&lt;<span class="hljs-name">MyButton</span>
@<span class="hljs-attr">click</span>=<span class="hljs-string">"handleClick"</span>
@<span class="hljs-attr">custom</span>=<span class="hljs-string">"handleCustom"</span>
<span class="hljs-attr">v-model</span>=<span class="hljs-string">"value"</span>
<span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span>
<span class="hljs-attr">class</span>=<span class="hljs-string">"btn"</span>
/&gt;</span>
</code></pre>
<pre><code class="hljs language-javascript"><span class="hljs-comment">// Inside MyButton.vue</span>
$listeners = {
<span class="hljs-attr">click</span>: handleClick,
<span class="hljs-attr">custom</span>: handleCustom,
<span class="hljs-attr">input</span>: <span class="hljs-function">() =&gt;</span> {}
}
$attrs = {
<span class="hljs-attr">type</span>: <span class="hljs-string">'button'</span>
}
</code></pre>
<p>Instead we can find our listeners with their <code>onEvent</code> format, just like in vanilla JavaScript.</p>
<pre><code class="hljs language-html">// Vue 3
<span class="hljs-tag">&lt;<span class="hljs-name">MyButton</span>
@<span class="hljs-attr">click</span>=<span class="hljs-string">"handleClick"</span>
@<span class="hljs-attr">custom</span>=<span class="hljs-string">"handleCustom"</span>
<span class="hljs-attr">v-model</span>=<span class="hljs-string">"value"</span>
<span class="hljs-attr">type</span>=<span class="hljs-string">"button"</span>
<span class="hljs-attr">class</span>=<span class="hljs-string">"btn"</span>
/&gt;</span>
</code></pre>
<pre><code class="hljs language-javascript"><span class="hljs-comment">// Inside MyButton.vue</span>
$attrs = {
<span class="hljs-attr">class</span>: <span class="hljs-string">'btn'</span>
<span class="hljs-attr">type</span>: <span class="hljs-string">'button'</span>,
<span class="hljs-attr">onClick</span>: handleClick,
<span class="hljs-attr">onCustom</span>: handleCustom,
<span class="hljs-string">'onUpdate:modelValue'</span>: <span class="hljs-function">() =&gt;</span> { value = payload },
}
</code></pre>
<p>As you can see, our <code>click</code> listener can now be found inside $attrs as <code>onClick</code>. A <code>custom</code> event would be <code>onCustom</code>, and even our <code>v-model</code> now shows <code>onUpdate:modelValue</code> with the default bindings that we learned on lesson 2 in this course.</p>
<p>Inside <code>$attrs</code>, you may also find all the attributes that a component receives from the parent, such as <code>id</code>, <code>aria</code> attributes, <code>data</code> attributes and other HTML attributes like <code>col</code>, <code>row</code>, <code>type</code> and <code>src</code>. In the case of this example button, it will receive the <code>type</code> attribute that our parent injected into it.</p>
<p>It is important to remember that in Vue 3, <code>class</code> and <code>style</code> are also part of the <code>$attrs</code> object.</p>
<hr>
<h2>Binding $attrs to a component</h2>
<p>In order to better learn how to correctly use Vue 3 <code>$attrs</code> in a component, we’re going to build upon a plain input wrapper component. I’ve gone ahead and created the starting stub code, so that we can dive right into it.</p>
<p>The component includes both a <code>label</code> and an <code>input</code> element — they are both wrapped up inside a parent <code>div</code> as we usually would do in a Vue 2 application. Don’t worry, we’ll cover how to remove this unnecessary <code>div</code> with multiroot component in the next lesson.</p>
<p>The <code>label</code> is bound to a <code>label</code> property, and the <code>input</code> element is <code>v-model</code> capable with the defaults: <code>modelValue</code> as a property, and <code>update:modelValue</code> as the emitted event.</p>
<p>📃<strong>BaseInput.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>{{ label }}<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
<span class="hljs-attr">:value</span>=<span class="hljs-string">"modelValue"</span>
@<span class="hljs-attr">input</span>=<span class="hljs-string">"$emit('update:modelValue', $event.target.value)"</span>
/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">props</span>: {
<span class="hljs-attr">modelValue</span>: {
<span class="hljs-attr">type</span>: [<span class="hljs-built_in">String</span>, <span class="hljs-built_in">Number</span>],
<span class="hljs-attr">default</span>: <span class="hljs-string">''</span>
},
<span class="hljs-attr">label</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
<span class="hljs-attr">default</span>: <span class="hljs-literal">null</span>
}
}
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>We can now use the <code>BaseInput</code> component directly in <code>App.vue</code> by importing it and adding it to the <code>template</code>.</p>
<p>📃<strong>App.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"app"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">BaseInput</span>
<span class="hljs-attr">v-model</span>=<span class="hljs-string">"email"</span>
<span class="hljs-attr">label</span>=<span class="hljs-string">"Email:"</span>
/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">pre</span>&gt;</span>{{ email }}<span class="hljs-tag">&lt;/<span class="hljs-name">pre</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>
<span class="hljs-keyword">import</span> BaseInput <span class="hljs-keyword">from</span> <span class="hljs-string">'./components/BaseInput'</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">name</span>: <span class="hljs-string">'App'</span>,
<span class="hljs-attr">components</span>: {
BaseInput
},
setup () {
<span class="hljs-keyword">const</span> email = ref(<span class="hljs-string">''</span>)
<span class="hljs-keyword">return</span> {
email
}
}
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Now that we have a component to play with, let’s take a look at what happens when we try to apply a <code>type</code> declaration to our component instance in <code>App.vue</code> in order to change this <code>input</code> to a type of <code>email</code>.</p>
<p>📃<strong>App.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">BaseInput</span>
<span class="hljs-attr">v-model</span>=<span class="hljs-string">"email"</span>
<span class="hljs-attr">label</span>=<span class="hljs-string">"Email:"</span>
<span class="hljs-attr">type</span>=<span class="hljs-string">"email"</span>
/&gt;</span>
</code></pre>
<p><img src="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F1.opt.jpg?alt=media&amp;token=fe49fd01-a5c7-4d17-813f-3ae0d4af35b7" alt="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F1.opt.jpg?alt=media&amp;token=fe49fd01-a5c7-4d17-813f-3ae0d4af35b7"></p>
<p>As expected, we get the same behaviour as in Vue 2. The <code>type</code> declaration has been set into the root <code>div</code> tag of the component.</p>
<p>If we now add a <code>class</code> declaration to our <code>BaseInput</code> instance inside <code>App.vue</code>, we will also get the same behaviour.</p>
<p>📃<strong>App.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">BaseInput</span>
<span class="hljs-attr">v-model</span>=<span class="hljs-string">"email"</span>
<span class="hljs-attr">label</span>=<span class="hljs-string">"Email:"</span>
<span class="hljs-attr">type</span>=<span class="hljs-string">"email"</span>
<span class="hljs-attr">class</span>=<span class="hljs-string">"thicc"</span>
/&gt;</span>
</code></pre>
<p><img src="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F2.opt.jpg?alt=media&amp;token=a2ebbebd-27e1-45fa-a9e3-486179c0fbae" alt="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F2.opt.jpg?alt=media&amp;token=a2ebbebd-27e1-45fa-a9e3-486179c0fbae"></p>
<p>Let’s go back to <code>BaseInput.vue</code> and bind the <code>$attrs</code> correctly into our <code>input</code> element.</p>
<p>📃<strong>BaseInput.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>{{ label }}<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
<span class="hljs-attr">v-bind</span>=<span class="hljs-string">"$attrs"</span>
<span class="hljs-attr">:value</span>=<span class="hljs-string">"modelValue"</span>
@<span class="hljs-attr">input</span>=<span class="hljs-string">"$emit('update:modelValue', $event.target.value)"</span>
/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">pre</span>&gt;</span>{{ $attrs }}<span class="hljs-tag">&lt;/<span class="hljs-name">pre</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p>Now if we check the browser, we’ll notice an interesting thing.</p>
<p><img src="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F3.opt.jpg?alt=media&amp;token=48d14fe9-8a17-4232-8b31-835c6f3c28cf" alt="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F3.opt.jpg?alt=media&amp;token=48d14fe9-8a17-4232-8b31-835c6f3c28cf"></p>
<p>The <code>input</code> element is now correctly binding our <code>type</code>, but <em>also</em> the <code>class</code>! Although this may not seem very exciting at face value, this is actually a huge improvement in Vue 3 over Vue 2.</p>
<p>In Vue 2, not only were the <code>class</code> and <code>style</code> tags not declared inside <code>$attrs</code> — they were always fixed into the root of the component, in this case, our <code>div</code>. In Vue 3 we can use this ability to bind classes into components without having to resort to less-than-ideal solutions like creating a <code>classes</code> property.</p>
<p>Did you notice? Our <code>class</code> and <code>type</code> are still bound to the wrapping <code>div</code> as well —<code>inheritAttrs: false</code> is still a thing.</p>
<p>We need to add this to our component’s object in order to let Vue 3 know that we are going to take care of binding everything ourselves, so let’s go ahead and add it to our component.</p>
<p>📃<strong>BaseInput.vue</strong></p>
<pre><code class="hljs language-jsx"><span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">inheritAttrs</span>: <span class="hljs-literal">false</span>,
<span class="hljs-attr">props</span>: {
<span class="hljs-attr">modelValue</span>: {
<span class="hljs-attr">type</span>: [<span class="hljs-built_in">String</span>, <span class="hljs-built_in">Number</span>],
<span class="hljs-attr">default</span>: <span class="hljs-string">''</span>
},
<span class="hljs-attr">label</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
<span class="hljs-attr">default</span>: <span class="hljs-literal">null</span>
}
}
}
</code></pre>
<p>If we check the browser one more time, now the <code>div</code> will not contain the bindings that we’re setting for our <code>input</code>.</p>
<p><img src="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F4.opt.jpg?alt=media&amp;token=02d6f4d2-8831-49b1-a4e9-99cacccdfb9f" alt="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F4.opt.jpg?alt=media&amp;token=02d6f4d2-8831-49b1-a4e9-99cacccdfb9f"></p>
<hr>
<h2>Binding listeners through our $attrs</h2>
<p>Now that we have our <code>$attr</code> binding set up, we can take a look at how to bind <code>listeners</code> into our component.</p>
<p>We currently have our component set up to listen directly to <code>input</code> events through the <code>@input</code> binding. But sometimes we want to allow our component to respond to any type of event that the parent wants to listen to, especially in the case of binding into native input elements.</p>
<p>For example, let’s go back to <code>App.vue</code> and add an event listener for <code>blur</code> into our <code>BaseInput</code> instance that will change the value of <code>email</code> whenever the user <em>blurs</em> or tabs out of the input field.</p>
<p>📃<strong>App.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">BaseInput</span>
<span class="hljs-attr">v-model</span>=<span class="hljs-string">"email"</span>
@<span class="hljs-attr">blur</span>=<span class="hljs-string">"email = 'blurrr@its.cold'"</span>
<span class="hljs-attr">label</span>=<span class="hljs-string">"Email:"</span>
<span class="hljs-attr">type</span>=<span class="hljs-string">"email"</span>
<span class="hljs-attr">class</span>=<span class="hljs-string">"thicc"</span>
/&gt;</span>
</code></pre>
<p>Let’s go back to the browser and test it out by blurring out of the input element.</p>
<p><img src="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F5.opt.jpg?alt=media&amp;token=6324b915-db5c-4494-a556-96b37a61ef72" alt="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F5.opt.jpg?alt=media&amp;token=6324b915-db5c-4494-a556-96b37a61ef72"></p>
<p>If you had a lot of Vue 2 experience at this point you may be wondering, wait—how?</p>
<p>In Vue 2 we had to add <code>v-on="$listeners"</code> into our <code>input</code> element to tell Vue that we wanted to delegate all the events (other than the ones that have been manually declared and therefore overridden) to our parent. This would allow our parent, <code>App.vue</code> in this case, to handle declaring the logic for when these events happened—like we just did with the <code>blur</code> listener.</p>
<p>In Vue 3, this is no longer necessary. The moment we added our <code>v-bind="$attrs"</code> declaration we also added a binding for all the listeners that the parent passed into our component.</p>
<p>Remember, <code>$attrs</code> now also includes all the listeners, so in this example it will contain an <code>onBlur</code> declaration that holds the function that sets <code>email</code> to <code>blurrr@its.cold</code></p>
<p>How convenient is that? 😎</p>
<hr>
<h2>Last minute cleanup</h2>
<p>If you’re like me and you like to keep your <code>v-bind</code> clean and contained, let me tell you that there is a nice way to refactor the <code>@input</code> listener into our <code>v-bind</code> declaration using the JavaScript spread operator.</p>
<p>We currently have our <code>template</code> inside <code>BaseInput</code> set like this.</p>
<p>📃<strong>BaseInput.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">input</span>
<span class="hljs-attr">v-bind</span>=<span class="hljs-string">"$attrs"</span>
@<span class="hljs-attr">input</span>=<span class="hljs-string">"$emit('update:modelValue', $event.target.value)"</span>
<span class="hljs-attr">:value</span>=<span class="hljs-string">"modelValue"</span>
/&gt;</span>
</code></pre>
<p>Since <code>$attrs</code> is an object, we can safely use the spread operator to wrap it inside an object. This way, we can now declare the <code>input</code> listener as <code>onInput</code> inside our <code>v-bind</code> declaration to keep all of our internal listener’s logic inside of it.</p>
<p>📃<strong>BaseInput.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">input</span>
<span class="hljs-attr">v-bind</span>=<span class="hljs-string">"{
...$attrs,
onInput: (event) =&gt; $emit('update:modelValue', event.target.value)
}"</span>
<span class="hljs-attr">:value</span>=<span class="hljs-string">"modelValue"</span>
/&gt;</span>
</code></pre>
<p>Not only is this clearer to look at, but it’s also a very good tool to make sure that certain events like <code>input</code> don’t get overwritten by the parent.</p>
<hr>
<h2>Wrapping up</h2>
<p>In this lesson we learned how to correctly bind our <code>$attrs</code> object in Vue 3, and the differences that it has with Vue 2. We also learned how to bind our <code>listeners</code> now that the <code>$listeners</code> object has been merged into <code>$attrs</code>.</p>
<p>In our next lesson, we will take a deeper look into single vs multiple root components and what role <code>$attrs</code> plays when migrating from single root to multi root.</p>
<p>See you there!</p>
</div></div><!----></div>
<div data-v-d671142e="" data-v-33673d7f="" id="lessonContent" class="lesson-content unlock"><!----><div data-v-d671142e="" class="body"><h1 data-v-d671142e="" class="title">Multi root components</h1><!----><div data-v-d671142e="" class="lesson-body"><p>In our last lesson we learned how <code>$attrs</code> bindings work in Vue 3, as well as the removal of <code>$listeners</code>, but there’s a little bit more we have to clarify about attribute binding in Vue 3 components before we move on.</p>
<p>Vue 3 allows us the possibility of creating components that have multiple roots, or fragments — note that you may see them also called fragmented root components. This was not possible in Vue 2, so some adjustments to the framework and API were obviously needed.</p>
<p>In this lesson, we will take a look at the differences between single-root and multiple-root components, and Process of transforming a single-root into multi-root.</p>
<p>Understanding these changes, both in how they benefit you as well as potential problems, will allow you to be able to write and debug any component regardless of the template architecture it uses.</p>
<hr>
<h2>Multi-Root Components in Vue</h2>
<p>We’re going to build upon the last lesson’s component <code>BaseInput</code>. In case you’re catching up — here’s the code for the component.</p>
<p>📃<strong>BaseInput.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>{{ label }}<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
<span class="hljs-attr">v-bind</span>=<span class="hljs-string">"{
...$attrs,
onInput: (event) =&gt; $emit('update:modelValue', event.target.value)
}"</span>
<span class="hljs-attr">:value</span>=<span class="hljs-string">"modelValue"</span>
/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">inheritAttrs</span>: <span class="hljs-literal">false</span>,
<span class="hljs-attr">props</span>: {
<span class="hljs-attr">modelValue</span>: {
<span class="hljs-attr">type</span>: [<span class="hljs-built_in">String</span>, <span class="hljs-built_in">Number</span>],
<span class="hljs-attr">default</span>: <span class="hljs-string">''</span>
},
<span class="hljs-attr">label</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
<span class="hljs-attr">default</span>: <span class="hljs-string">''</span>
}
}
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Inside <code>App.vue</code>, we are instantiating it and applying some bindings.</p>
<p><strong>📃App.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"app"</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">BaseInput</span>
<span class="hljs-attr">v-model</span>=<span class="hljs-string">"email"</span>
@<span class="hljs-attr">blur</span>=<span class="hljs-string">"email = 'blurrr@its.cold'"</span>
<span class="hljs-attr">label</span>=<span class="hljs-string">"Email:"</span>
<span class="hljs-attr">type</span>=<span class="hljs-string">"email"</span>
<span class="hljs-attr">class</span>=<span class="hljs-string">"thicc"</span>
/&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">pre</span>&gt;</span>{{ email }}<span class="hljs-tag">&lt;/<span class="hljs-name">pre</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">import</span> { ref } <span class="hljs-keyword">from</span> <span class="hljs-string">'vue'</span>
<span class="hljs-keyword">import</span> BaseInput <span class="hljs-keyword">from</span> <span class="hljs-string">'./components/BaseInput'</span>
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">name</span>: <span class="hljs-string">'App'</span>,
<span class="hljs-attr">components</span>: {
BaseInput
},
setup () {
<span class="hljs-keyword">const</span> email = ref(<span class="hljs-string">''</span>)
<span class="hljs-keyword">return</span> {
email
}
}
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>The result in our browser is a plain email input field and the display of the current <code>v-model</code> bindings.</p>
<p><img src="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F1.opt.1596471405643.jpg?alt=media&amp;token=0572eda1-b0dd-4861-be4e-f66f95244e2b" alt="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F1.opt.1596471405643.jpg?alt=media&amp;token=0572eda1-b0dd-4861-be4e-f66f95244e2b"></p>
<p>Making this component a multi-root component in Vue 3 is as simple as removing the unnecessary <code>div</code> tag that wraps our component. Let’s go ahead and make the change and check in the browser to see if there have been any changes.</p>
<p>📃<strong>BaseInput.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>{{ label }}<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
<span class="hljs-attr">v-bind</span>=<span class="hljs-string">"{
...$attrs,
onInput: (event) =&gt; $emit('update:modelValue', event.target.value)
}"</span>
<span class="hljs-attr">:value</span>=<span class="hljs-string">"modelValue"</span>
/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p><img src="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F2.opt.1596471405644.jpg?alt=media&amp;token=bb6e3858-3eab-4cce-b09e-bbae6e0a5533" alt="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F2.opt.1596471405644.jpg?alt=media&amp;token=bb6e3858-3eab-4cce-b09e-bbae6e0a5533"></p>
<p>As you can see, our <code>BaseInput</code> is correctly being rendered, the bindings are still in place, and the wrapping <code>div</code> tag is nowhere to be found.</p>
<p>I have good news for you! Now that we are working with a multi-root component, we can remove the <code>inheritAttrs</code> property completely.</p>
<p>📃<strong>BaseInput.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>{{ label }}<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
<span class="hljs-attr">v-bind</span>=<span class="hljs-string">"{
...$attrs,
onInput: (event) =&gt; $emit('update:modelValue', event.target.value)
}"</span>
<span class="hljs-attr">:value</span>=<span class="hljs-string">"modelValue"</span>
/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">props</span>: {
<span class="hljs-attr">modelValue</span>: {
<span class="hljs-attr">type</span>: [<span class="hljs-built_in">String</span>, <span class="hljs-built_in">Number</span>],
<span class="hljs-attr">default</span>: <span class="hljs-string">''</span>
},
<span class="hljs-attr">label</span>: {
<span class="hljs-attr">type</span>: <span class="hljs-built_in">String</span>,
<span class="hljs-attr">default</span>: <span class="hljs-literal">null</span>
}
}
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Whenever we are working with multi-root components, Vue will no longer attempt to auto-inject attributes into the root node because there isn’t a single one anymore. Since Vue can not safely assume which of these multiple roots should get the attribute fall-through, it will not attempt it at all.</p>
<p>At this point, Vue will check your component file and see if you have <code>"$attrs"</code> in your component. If you do, then Vue will assume that you are in control of how the attributes and listeners will be handled; if don’t, you will get a warning.</p>
<p>Let’s go ahead and take out the <code>v-bind</code> directive for a moment and take a look at the browser to see what this error looks like.</p>
<p>📃<strong>BaseInput.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>{{ label }}<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
<span class="hljs-attr">:value</span>=<span class="hljs-string">"modelValue"</span>
/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<p><img src="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F3.opt.1596471417558.jpg?alt=media&amp;token=292e90b9-f5f7-4f24-81ed-ad2653985eff" alt="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F3.opt.1596471417558.jpg?alt=media&amp;token=292e90b9-f5f7-4f24-81ed-ad2653985eff"></p>
<p>This wording is a little confusing, but what Vue is trying to tell us is:</p>
<ul>
<li>Hey, listen. The parent is passing down <code>type</code>, <code>class</code>, and an event listener (through <code>v-model</code>) and I have no idea where to put them</li>
</ul>
<p>It’s important that we know what this means in case we run into this in one of our projects. Now we know exactly where to look for the problem.</p>
<p>Let’s put back our <code>v-bind</code> declaration, though, so that our component behaves correctly.</p>
<p>📃<strong>BaseInput.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">template</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">label</span>&gt;</span>{{ label }}<span class="hljs-tag">&lt;/<span class="hljs-name">label</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">input</span>
<span class="hljs-attr">v-bind</span>=<span class="hljs-string">"{
...$attrs,
onInput: (event) =&gt; $emit('update:modelValue', event.target.value)
}"</span>
<span class="hljs-attr">:value</span>=<span class="hljs-string">"modelValue"</span>
/&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">template</span>&gt;</span>
</code></pre>
<hr>
<h2>The emits property</h2>
<p>If you were looking very closely at the error we generated a bit ago, you may have noticed that one of the warnings stated:</p>
<ul>
<li>Extraneous non-emits event listeners (update:modelValue, blur) were passed to component but could not be automatically inherited […]. If the listener is intended to be a component custom event listener only, declare it using the “emits” option.</li>
</ul>
<p>Vue 3 multi-components introduce a new data level property called <code>emits</code>. Let’s check it out.</p>
<p>In more intricate components where you are not declaring a <code>v-bind="$attrs"</code> that functions as a catch-all declaration for your listeners, or in <code>render</code>-based components where your emits may be generated dynamically, Vue will complain that it cannot find a declaration inside of the component file or template for a custom component that you may be adding.</p>
<p>In these cases, we get a new property called <code>emits</code> that sits on the data level right next to others like <code>components</code> and <code>setup</code>.</p>
<p>This new property is, in its simplest form, an array. So if we were expecting our component to emit an event called <code>peekedIntoTheBox</code> we could define it on our component like this.</p>
<p>📃<strong>SchroedingersBox.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">emits</span>: [<span class="hljs-string">'peekedIntoTheBox'</span>]
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>That way, whenever Vue instantiates our component it will know that it may expect an event with the name <code>peekedIntoTheBox</code> to be emitted by the component.</p>
<p>A more advanced syntax similar to the one of <code>props</code> allows us to even set validators for these declared <code>emits</code>.</p>
<p>By using an object format, we can add each of the emitted events as the key of the <code>emits</code> object. Then we can define it as <code>null</code> to avoid a validator, like this:</p>
<p>📃<strong>SchroedingersBox.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">emits</span>: {
<span class="hljs-attr">peekedIntoTheBox</span>: <span class="hljs-literal">null</span>
}
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<p>Now to take it a step further, let’s add a validator that checks that the <code>peekedIntoTheBox</code> event emits only <code>dead</code>, <code>alive</code> or <code>both</code>. The validator function, just as the ones in <code>props</code> should return a boolean value to state whether the payload is valid or not.</p>
<p>If the validation for the emitted value fails, Vue will issue a warning like the following.</p>
<p><img src="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F4.opt.1596471425564.jpg?alt=media&amp;token=ec9c8d69-a039-4bf4-a1ab-ace7e9f1d629" alt="https://firebasestorage.googleapis.com/v0/b/vue-mastery.appspot.com/o/flamelink%2Fmedia%2F4.opt.1596471425564.jpg?alt=media&amp;token=ec9c8d69-a039-4bf4-a1ab-ace7e9f1d629"></p>
<p>📃<strong>SchroedingersBox.vue</strong></p>
<pre><code class="hljs language-html"><span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript">
<span class="hljs-keyword">export</span> <span class="hljs-keyword">default</span> {
<span class="hljs-attr">emits</span>: {
<span class="hljs-attr">peekedIntoTheBox</span>: <span class="hljs-function"><span class="hljs-params">payload</span> =&gt;</span> {
<span class="hljs-keyword">return</span> [<span class="hljs-string">'dead'</span>, <span class="hljs-string">'alive'</span>, <span class="hljs-string">'both'</span>].includes(payload)
}
}
}
</span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span>
</code></pre>
<hr>
<h2>Wrapping up</h2>
<p>Multi-root components are a very welcome addition to the Vue toolbox. It will certainly make Vue application <code>div</code> nesting nightmares less recurrent.</p>
<p>In this lesson, we learned how to transform single-root components into multi-root, and how to deal with the caveats and quirks of multi-root component attribute fall-through.</p>
<p>We also touched upon an advanced topic, the <code>emits</code> property and its correct use.</p>
<p>With this, we wrap up the <strong>From Vue 2 to Vue 3</strong> course. You are now ready to leverage the power of the new Vue 3 capabilities!</p>
<p>Thanks for watching!</p>
</div></div><!----></div>
html{text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased}body{font-family:"Source Sans Pro",sans-serif;font-size:14px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{font-family:"Source Sans Pro",sans-serif;font-weight:700;margin-top:0;padding-top:.5em;margin-bottom:.3em}.h1,h1{font-size:32px}.h2,h2{font-size:28px}.h3,h3{font-size:20px}.h4,h4{font-size:18px}.h5,h5{font-size:12px}p{line-height:1.5}@media screen and (min-width:40em){body{font-size:18px}.h1,h1{font-size:3.25em}.h2,h2{font-size:2.1em}.h3,h3{font-size:1.75em}.h4,h4{font-size:1.25em}.h5,h5{font-size:1em}.h6,h6{font-size:.75em}p{line-height:1.5}}button{outline:0}button[disabled]{opacity:.7}.button{display:inline-flex;height:44px;margin:.25em 0;padding:0 30px;color:#fff;text-decoration:none;align-items:center;justify-content:center;line-height:44px;border:none;border-radius:30px;cursor:pointer;transition:all .55s cubic-bezier(.19,1,.22,1);overflow:hidden;position:relative;-webkit-tap-highlight-color:transparent;-webkit-tap-highlight-color:transparent}.button:after,.button:before{background:linear-gradient(to top right,transparent,#fff);content:"";height:150px;left:-175px;opacity:.1;position:absolute;top:-50px;transform:rotate(35deg);width:100px}.button:disabled{pointer-events:none}.button:focus{outline:0}.button.primary{background:linear-gradient(to top right,#41b782,#86d169)}.button.primary.border{color:#39b982;border-color:#39b982}.button.secondary{background:linear-gradient(to top right,#122549,#203762)}.button.secondary.border{color:#0a2b4e;border-color:#0a2b4e}.button.high-contrast,.button.tertiary{background:linear-gradient(to top right,#3d2c61,#835ec2)}.button.danger{background:linear-gradient(to top right,#eb4641,#f06372)}.button.danger.border{color:#eb4641;border-color:#eb4641}.button.inverted{background:#fff;color:#0a2b4e}.button.inverted.border{color:#fff;border-color:#fff}.button.inverted.border:hover{color:#0a2b4e}.button.border{background:0 0;border:2px solid #0a2b4e;line-height:40px}.button.modern{border-color:#028ebb;border-top:1px solid rgba(3,143,188,.16);border-bottom:1px solid rgba(46,162,200,.29);padding:0 20px}.button.modern.active{background:rgba(2,142,187,.18);box-shadow:inset 0 9px 21px rgba(19,183,166,.1),0 0 2px rgba(60,196,180,.6);border-color:#028ebb;border-top:1px solid rgba(3,143,188,.722);text-shadow:0 0 3px #0a1121;border-bottom:1px solid rgba(3,143,188,.722)}.button.modern.-plain{padding:28px 85px;background:rgba(5,37,59,.21)}.button.modern.-plain:before{-webkit-animation:left 10s infinite;animation:left 10s infinite}.button.modern.-plain:hover{transform:scale(1.1)}.button.-small{height:44px;padding:0 15px}@media screen and (min-width:40em){.button.-small{line-height:44px}}.button.-small.-has-icon{line-height:inherit}.button.-full{white-space:nowrap;width:100%}.button.-has-icon,.button.-only-icon{display:inline-flex;align-items:center;justify-content:center;overflow:hidden}.button.-has-icon i[class*=fa],.button.-has-icon i[class*=fab],.button.-only-icon i[class*=fa],.button.-only-icon i[class*=fab]{margin-right:10px}.button.-has-icon:after,.button.-only-icon:after{content:none}.button.-has-icon i,.button.-only-icon i{transition:transform .5s cubic-bezier(.19,1,.22,1)}.button.link{background:0 0;border:none;padding:0;color:#0a2b4e}.button.link.inverted{color:#fff}.button .icon{margin-right:8px}.control-group{display:flex;align-items:center;flex-direction:row;flex-wrap:wrap}.control-group.-spaced{justify-content:space-evenly}.control-group.-separate{justify-content:space-between}@media screen and (min-width:40em){.button{height:54px;margin:.5em 0;line-height:54px}.button.border{line-height:50px}.button.-small{height:44px;padding:0 30px;line-height:44px}.button.-small.border{line-height:40px}}.button.active,.button:hover,a:hover .button{text-decoration:none}.button.active:after,.button:hover:after,a:hover .button:after{left:120%;transition:left 1.5s cubic-bezier(.19,1,.22,1)}.button.active.primary,.button:hover.primary,a:hover .button.primary{background:linear-gradient(to bottom right,#41b782,#86d169)}.button.active.secondary,.button:hover.secondary,a:hover .button.secondary{background:linear-gradient(to bottom right,#122549,#203762)}.button.active.danger,.button:hover.danger,a:hover .button.danger{background:linear-gradient(to bottom right,#eb4641,#f06372)}.button.active.inverted,.button:hover.inverted,a:hover .button.inverted{background:#fff}.button.active.border,.button:hover.border,a:hover .button.border{border-color:transparent;color:#fff}.button.active.modern,.button:hover.modern,a:hover .button.modern{box-shadow:inset 0 9px 21px rgba(19,183,166,.1),0 0 2px rgba(60,196,180,.6);border-left-color:#028ebb;border-right-color:#028ebb;border-top:1px solid rgba(3,143,188,.722);border-bottom:1px solid rgba(3,143,188,.722)}.button.active.-only-icon,.button:hover.-only-icon,a:hover .button.-only-icon{background:0 0;text-decoration:none}.button.active.-only-icon i,.button:hover.-only-icon i,a:hover .button.-only-icon i{transform:scale(.9)}@-webkit-keyframes left{0%{top:-40px;left:-175px}40%{top:-40px;left:-175px}50%{top:-40px;left:120%}55%{top:-200px;left:-175px}to{top:-40px;left:-175px}}@keyframes left{0%{top:-40px;left:-175px}40%{top:-40px;left:-175px}50%{top:-40px;left:120%}55%{top:-200px;left:-175px}to{top:-40px;left:-175px}}.underline{position:relative;overflow:hidden;padding-bottom:2px;margin-bottom:-2px;cursor:pointer;color:#39b982}.underline.-has-icon{display:inline-flex;align-items:center;justify-content:center;overflow:visible}.underline.-has-icon i[class*=fa],.underline.-has-icon i[class*=fab]{margin-right:10px}.underline:before{position:absolute;z-index:-1;content:"";left:0;bottom:0;width:100%;height:2px;background:linear-gradient(to top right,#41b782,#86d169);transition:transform .5s cubic-bezier(.19,1,.22,1);transform:translateX(-100%)}.underline:hover:before{z-index:0;transform:translateX(0)}.underline:hover{text-decoration:none}a:hover .underline:before{transform:translateX(0)}.input{width:100%;padding:0 27px;margin:5px 0;height:54px;line-height:54px;border:2px solid #bbb;border-radius:30px}.input:focus{width:100%;outline:0}.input.-small{height:44px;line-height:44px}.input.primary.-hollow{color:#39b982;border-color:#39b982}.input.primary.-hollow::-moz-placeholder{color:#39b982}.input.primary.-hollow:-ms-input-placeholder{color:#39b982}.input.primary.-hollow::-ms-input-placeholder{color:#39b982}.input.primary.-hollow::placeholder{color:#39b982}.input.-hollow{background:0 0;border-width:2px;border-style:solid}.input.-is-info{color:#a8d7dd;border-color:#a8d7dd}.input.-is-info::-moz-placeholder{color:#a8d7dd}.input.-is-info:-ms-input-placeholder{color:#a8d7dd}.input.-is-info::-ms-input-placeholder{color:#a8d7dd}.input.-is-info::placeholder{color:#a8d7dd}.input.-is-success{color:#39b982;border-color:#39b982}.input.-is-success::-moz-placeholder{color:#39b982}.input.-is-success:-ms-input-placeholder{color:#39b982}.input.-is-success::-ms-input-placeholder{color:#39b982}.input.-is-success::placeholder{color:#39b982}.input.-is-warning{color:#f3e43c;border-color:#f3e43c}.input.-is-warning::-moz-placeholder{color:#f3e43c}.input.-is-warning:-ms-input-placeholder{color:#f3e43c}.input.-is-warning::-ms-input-placeholder{color:#f3e43c}.input.-is-warning::placeholder{color:#f3e43c}.input.-is-error{color:#e04848;border-color:#e04848}.input.-is-error::-moz-placeholder{color:#e04848}.input.-is-error:-ms-input-placeholder{color:#e04848}.input.-is-error::-ms-input-placeholder{color:#e04848}.input.-is-error::placeholder{color:#e04848}.textarea{width:100%;padding:27px;margin:5px 0;border:2px solid #bbb;border-radius:30px}.textarea:focus{width:100%;outline:0}.upload{display:inline-block;position:relative;overflow:hidden}.upload input[type=file]{font-size:100px;position:absolute;left:0;top:0;opacity:0;cursor:pointer}.checkbox{display:flex;justify-content:flex-start;align-items:center;font-size:15.75px}.checkbox .mark,.checkbox input[type=checkbox]{margin-top:-2px;margin-right:10px}.checkmark{display:block;position:relative;padding-left:22px;margin-bottom:22px;font-size:22px;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.checkmark .check{position:absolute;top:0;left:0;height:22px;width:22px;border:2px solid #39b982;background-color:transparent;border-radius:4px}.checkmark .check:after{content:"";position:absolute;display:none;left:6px;top:1px;width:5px;height:10px;border:solid #39b982;border-width:0 2px 2px 0;transform:rotate(45deg)}.checkmark input[type=checkbox]{position:absolute;opacity:0;cursor:pointer}.checkmark input[type=checkbox]:checked~.check{border-color:#39b982;background-color:#fff}.checkmark input[type=checkbox]:checked~.check:after{display:block}.checkmark:hover input~.check{background-color:#fff}.switch{display:flex;margin-right:10px;margin-left:10px}.switch label{display:block;position:relative;width:66px;height:33px;background:#aaa;border-radius:33px;text-indent:-9999px;cursor:pointer}.switch label:after{content:"";position:absolute;top:4px;left:4px;width:25px;height:25px;background:#fff;border-radius:25px;transition:.3s}.switch label:active:after{width:33px}.switch input[type=checkbox]{height:0;width:0;visibility:hidden}.switch input[type=checkbox]:checked+label{background:#39b982}.switch input[type=checkbox]:checked+label:after{left:calc(100% - 5px);transform:translateX(-100%)}.help-text{display:block;position:relative;margin:0 0 10px 27px;font-size:15.75px}.help-text.-is-info{color:#a8d7dd}.help-text.-is-success{color:#39b982}.help-text.-is-warning{color:#f3e43c}.help-text.-is-error{color:#e04848}.form .label{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.form-title{padding:20px 5%}.form-group{display:flex;flex-direction:column;margin:10px 0}.form-group.-center{align-items:center}.form-group.-inline{position:relative}.form-group.-inline .button{position:absolute;top:10px;right:5%;margin:0;padding-right:10px}.form-error.help-text{padding-top:10px;padding-bottom:10px}.-is-error{color:#e04848}.callout{padding:.5em 1em 0;border-radius:10px}.callout.-info{background:#f2f9fa;border:1px solid #a8d7dd;color:#214e54}.callout.-success{background:#e0f6ec;border:1px solid #39b982;color:#113727}.callout.-warning{background:#fdfbe2;border:1px solid #f3e43c;color:#564f05}.callout.-error{background:#fae4e4;border:1px solid #e04848;color:#4c0d0d}.card{display:flex;background:#fff;box-shadow:0 1px 4px 0 rgba(0,0,0,.3);border-radius:12px;transition:box-shadow .5s cubic-bezier(.19,1,.22,1)}.card .title{transition:color .2s ease-in}.card-body{width:100%;padding:20px}a.card:hover{box-shadow:0 2px 7px 0 rgba(0,0,0,.3)}a.card:hover .title{color:#39b982}.media-block{display:grid;grid-column-gap:4%;grid-row-gap:22.5px;align-items:center;grid-template-columns:1fr;grid-template-areas:"media" "body"}@media screen and (min-width:40em){.media-block{grid-template-columns:auto 1fr;grid-template-areas:"media body";grid-row-gap:45px}}.media-block .media{grid-area:media;display:flex;justify-content:center;align-items:center}.media-block .media.fake{min-height:125px}.media-block .media.fake.-small{width:120px}.media-block .media.fake.-large{width:200px}.media-block .media img{-o-object-fit:contain;object-fit:contain;max-width:100px}@media screen and (min-width:40em){.media-block .media img{max-width:200px}}@media screen and (min-width:40em){.media-block .media img.-small{width:120px}.media-block .media img.-large{width:200px}}.media-block .media.-video{position:relative}.media-block .media.-video .icon{position:absolute;left:50%;top:50%;color:#fff;-webkit-filter:drop-shadow(0 2px 3px #000);filter:drop-shadow(0 2px 3px #000);transform:translate(-50%,-50%);transition:all .3s;transform-origin:0 0}.media-block .media.-video:before{content:"";position:absolute;left:0;top:0;right:0;bottom:0;background:rgba(0,0,0,.4);transition:background .3s ease}.media-block .title-small{transition:color .2s ease-in}.media-block .body{grid-area:body;text-align:center}@media screen and (min-width:40em){.media-block .body{max-width:400px;min-width:calc(100% - 200px);margin:0;text-align:left}.media-block .body .title{font-size:27px}}.media-block .body.fake{min-height:100px}.media-block .body .content{margin:0}.media-block .body .meta{display:flex;align-items:center;justify-content:center;color:#222;margin-top:10px;font-weight:200}@media screen and (min-width:40em){.media-block .body .meta{justify-content:flex-start}}.media-block .body .meta .-has-icon{display:flex;align-items:center}.media-block .body .meta .-has-icon i{margin-right:5px}.grid .media-block .body{text-align:center;margin:0 auto}.media-block:hover a,a:hover .media-block{text-decoration:none}.media-block:hover a .title-small,a:hover .media-block .title-small{color:#39b982}.media-block:hover a .-video:before,a:hover .media-block .-video:before{background:0 0}.media-block:hover a .-video .icon,a:hover .media-block .-video .icon{transform:scale(1.1) translate(-50%,-50%);-webkit-filter:drop-shadow(0 2px 3px #000);filter:drop-shadow(0 2px 3px #000)}.v--modal{border-radius:10px!important}@media screen and (max-width:39.9375em){.v--modal{margin-bottom:100px!important;width:90%!important;left:5%!important;top:40px!important}}.v--modal .form-title{display:flex;align-items:center;background-color:#0a2b4e;color:#fff;margin-bottom:20px}.v--modal .form-title button{margin:0;padding:0;border:none;background:0 0;color:#fff;cursor:pointer}.v--modal .form{color:#222}.v--modal .form>div{padding-left:5%;padding-right:5%}.v--modal .form-footer{background-color:#ececec;margin-top:10px}.paginate{display:flex}.paginate .next,.paginate .prev{display:flex;align-items:center;width:100%;height:80px;width:50%;padding:0 2em;border:none;color:#fff;font-size:18px;font-weight:600;text-transform:capitalize;cursor:pointer;transition:padding .5s cubic-bezier(.19,1,.22,1)}.paginate .next[disabled],.paginate .prev[disabled]{background:rgba(0,0,0,.1);color:#ccc;cursor:not-allowed}.paginate .next[disabled]:hover i.fa,.paginate .prev[disabled]:hover i.fa{transform:translateX(0)}.paginate .next i,.paginate .prev i{font-size:24px;transition:transform .5s cubic-bezier(.19,1,.22,1)}.paginate .next{background:linear-gradient(90deg,#41b782,#86d169);text-align:left}.paginate .next i{margin-left:15px}.paginate .next:hover{padding-left:2.2em}.paginate .next:hover i{transform:translateX(20px)}.paginate .prev{background:linear-gradient(90deg,#122549,#203762);text-align:right;justify-content:flex-end}.paginate .prev:hover{paddin-right:2.2em}.paginate .prev i{margin-right:15px}.paginate .prev:hover i{transform:translateX(-20px)}@media screen and (min-width:40em){.paginate .next,.paginate .prev{height:118px;font-size:32px}.paginate .next i,.paginate .prev i{font-size:50px}}.empty{display:flex;width:100%;min-height:300px;justify-content:center;align-items:center;text-align:center;background:rgba(0,0,0,.03);border-radius:12px;padding:20px}.empty .empty-title{font-weight:600;color:#777}.empty-media-list li{margin-bottom:20px}.ribbon{position:absolute;right:-6px;top:-7px;z-index:1;overflow:hidden;width:146px;height:141px;text-align:center}.ribbon span{font-size:14px;font-weight:700;color:#fff;line-height:30px;transform:rotate(45deg);width:188px;display:block;background:#835ec2;background:linear-gradient(#8269be,#835ec2);box-shadow:0 1px 4px 0 rgba(0,0,0,.3);position:absolute;top:43px;right:-36px}.ribbon span:after,.ribbon span:before{content:"";position:absolute;top:100%;z-index:-1;border-bottom:3px solid transparent;border-top:3px solid #835ec2}.ribbon span:before{left:0;border-left:3px solid #835ec2;border-right:3px solid transparent}.ribbon span:after{right:0;border-left:3px solid transparent;border-right:3px solid #835ec2}.tab-switch{position:relative;overflow:hidden;display:inline-flex;align-items:center;height:3rem;margin:0 auto 3rem;border-radius:4rem;box-shadow:0 1px 1px 0 rgba(0,0,0,.3);font-weight:600;background:#ececec;cursor:pointer}.tab-switch:before{content:"";position:absolute;left:-50%;top:-100%;width:106%;height:300%;border-radius:50%;box-shadow:1px 0 1px 0 rgba(0,0,0,.3);background:linear-gradient(to top right,#0a2b4e,#164373);transition:.25s}.tab-switch .switch-item:first-child{color:#fff}.tab-switch.switch-active:before{left:53%;box-shadow:-1px 0 1px 0 rgba(0,0,0,.3);background:#835ec2}.tab-switch.switch-active .switch-item:nth-child(2){color:#fff}.tab-switch.switch-active .switch-item:first-child{color:#555}.switch-item{position:relative;display:flex;justify-content:center;align-items:center;height:100%;padding:2rem;color:#555;z-index:2;transition:.1s}.page-enter-active,.page-leave-active{transition:opacity .3s,transform .3s}.page-enter{opacity:0;transform:translateY(-20px)}.page-leave-to{opacity:0;transform:translateY(20px)}.course-enter-active,.course-leave-active{transition:none .1s}.course-enter-active .lesson-video,.course-enter-active .lessons-list,.course-leave-active .lesson-video,.course-leave-active .lessons-list{transition:opacity .1s}.course-enter .lesson-video,.course-enter .lessons-list,.course-leave-to .lesson-video,.course-leave-to .lessons-list{opacity:0}.settings-enter-active,.settings-leave-active{transition:none .1s}.settings-enter-active .account-content,.settings-leave-active .account-content{transition:opacity .1s}.settings-enter .account-wrapper,.settings-leave-to .account-content{opacity:0}img{width:100%}p>img{max-width:900px}.img-circle{border-radius:50%}.img-100{width:100px}.img-200{width:200px}.img-300{width:300px}.img-400{width:400px}.img-500{width:500px}.img-600{width:600px}.img-700{width:700px}.img-800{width:800px}.video-wrapper{position:relative;padding-bottom:56.25%;height:0;overflow:hidden;max-width:100%;touch-action:pan-y!important}.video-wrapper embed,.video-wrapper iframe,.video-wrapper object{position:absolute;top:0;left:0;width:100%;height:100%;touch-action:pan-y!important}.vm-toasted,.vm-toasted-subscribe{padding:10px 20px!important;color:#222!important;line-height:1.4!important;background:#fff!important;border-radius:6px!important;box-shadow:0 1px 4px 0 rgba(0,0,0,.3)!important}.vm-toasted,.vm-toasted .action,.vm-toasted-subscribe,.vm-toasted-subscribe .action{font-family:"Source Sans Pro",sans-serif!important;font-size:18px!important;font-weight:400!important}.vm-toasted .action,.vm-toasted-subscribe .action{color:#39b982!important;text-decoration:none!important}.vm-toasted-subscribe .action{padding:0 30px!important;color:#fff!important;border:none!important;border-radius:30px!important;overflow:hidden!important;position:relative!important;line-height:44px!important;height:44px!important;background:linear-gradient(to top right,#41b782,#86d169)!important;transition:all .55s cubic-bezier(.19,1,.22,1)!important;-webkit-tap-highlight-color:transparent!important;-webkit-tap-highlight-color:transparent!important}.vm-toasted-subscribe .action:after{background:linear-gradient(to top right,transparent,#fff);content:"";height:150px;left:-175px;opacity:.1;position:absolute;top:-50px;transform:rotate(35deg);width:100px}.toasted-container .toasted.toasted-primary.success{background:linear-gradient(to top right,#41b782,#86d169)}.toasted-container .toasted.toasted-primary.error,.toasted-container .toasted.toasted-primary.info,.toasted-container .toasted.toasted-primary.success{display:flex;flex-direction:row;align-items:center;font-weight:700;justify-content:space-between;padding:.5rem 1.5rem;border-radius:1rem;text-transform:uppercase}.toasted-container .toasted.toasted-primary.error .action,.toasted-container .toasted.toasted-primary.info .action,.toasted-container .toasted.toasted-primary.success .action{font-weight:700;color:#fff}.toasted-container .toasted.toasted-primary.error .action.fa-sync,.toasted-container .toasted.toasted-primary.info .action.fa-sync,.toasted-container .toasted.toasted-primary.success .action.fa-sync{transform-origin:center;-webkit-animation:rotate 1s linear infinite;animation:rotate 1s linear infinite}.toasted-container .toasted.toasted-primary.error .action:hover,.toasted-container .toasted.toasted-primary.info .action:hover,.toasted-container .toasted.toasted-primary.success .action:hover{text-decoration:none}@media screen and (max-width:1000px){.toasted-container .toasted{flex-direction:column;min-width:350px;text-align:center}}@-webkit-keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(1turn)}}@keyframes rotate{0%{transform:rotate(0)}to{transform:rotate(1turn)}}.badge{display:inline-flex;align-items:center;justify-content:center;font-size:.6em;height:24px;line-height:16px;padding:0 10px;white-space:nowrap;font-weight:400;border-radius:30px;text-transform:uppercase;color:#fff}.badge.primary{background:#39b982}.badge.secondary{background:#0a2b4e}.badge.tertiary{background:#835ec2}.badge.-inverted{background:#fff}.badge.-inverted.primary{color:#39b982}.badge.-inverted.secondary{color:#0a2b4e}*{box-sizing:border-box}a{text-decoration:none;color:#39b982;-webkit-tap-highlight-color:transparent;-webkit-tap-highlight-color:transparent}a:hover{text-decoration:underline}a.-inverted{color:#fff}ul{list-style-type:none;padding:0;margin:0}ul li{margin-bottom:10px}.text-center{text-align:center}hr{height:1px;border:0;background-color:#e6e8eb}code{font-family:Inconsolata,monospace;font-size:18px;background:#f7f9fa;border:1px solid #e6e8eb;padding:0 4px 2px}pre{background:#1e1e1e;padding:10px;border-radius:6px;white-space:pre-wrap}pre>code{background:0 0;border:none}ul{padding:10px 20px;margin:0 0 0 20px;list-style-type:disc}.list-unstyled{padding:0;margin:0;list-style-type:none}.visually-hidden{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}[v-cloak]>*{display:none}[v-cloak]:before{content:"loading…"}.truncate{overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical}.banner{background-size:cover;background-position:50%;background-repeat:no-repeat}.section{padding:45px 4%}.fake{background-color:hsla(0,0%,86.3%,.2);width:100%}.fake.lesson-header{height:116px}.fake .player{min-width:100%!important;max-width:100%}.wrapper{display:grid;width:100%;padding:0 4%}@media screen and (min-width:78em){.wrapper{margin:0 auto;max-width:1400px;padding:0 25px}}.-flash{border-radius:6px;background:#faf7a1;box-shadow:0 0 6px 6px #faf7a1;-webkit-animation:flashfade 1s linear;animation:flashfade 1s linear;-webkit-animation-iteration-count:1;animation-iteration-count:1}@-webkit-keyframes flashfade{0%{background:#faf7a1;box-shadow:0 0 6px 6px #faf7a1}60%{background:#faf7a1;box-shadow:0 0 6px 6px #faf7a1}to{background:rgba(250,247,161,0);box-shadow:0 0 6px 6px rgba(250,247,161,0)}}@keyframes flashfade{0%{background:#faf7a1;box-shadow:0 0 6px 6px #faf7a1}60%{background:#faf7a1;box-shadow:0 0 6px 6px #faf7a1}to{background:rgba(250,247,161,0);box-shadow:0 0 6px 6px rgba(250,247,161,0)}}.hljs{display:block;overflow-x:auto;padding:.5em;background:#1e1e1e;color:#dcdcdc}.hljs-keyword,.hljs-link,.hljs-literal,.hljs-name,.hljs-symbol{color:#569cd6}.hljs-link{text-decoration:underline}.hljs-built_in,.hljs-type{color:#4ec9b0}.hljs-class,.hljs-number{color:#b8d7a3}.hljs-meta-string,.hljs-string{color:#d69d85}.hljs-regexp,.hljs-template-tag{color:#9a5334}.hljs-formula,.hljs-function,.hljs-params,.hljs-subst,.hljs-title{color:#dcdcdc}.hljs-comment,.hljs-quote{color:#57a64a;font-style:italic}.hljs-doctag{color:#608b4e}.hljs-meta,.hljs-meta-keyword,.hljs-tag{color:#9b9b9b}.hljs-template-variable,.hljs-variable{color:#bd63c5}.hljs-attr,.hljs-attribute,.hljs-builtin-name{color:#9cdcfe}.hljs-section{color:gold}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-bullet,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-selector-tag{color:#d7ba7d}.hljs-addition{background-color:#144212}.hljs-addition,.hljs-deletion{display:inline-block;width:100%}.hljs-deletion{background-color:#600}.nuxt-progress{position:fixed;top:0;left:0;right:0;height:2px;width:0;opacity:1;transition:width .1s,opacity .4s;background-color:#39b982;z-index:999999}.nuxt-progress.nuxt-progress-notransition{transition:none}.nuxt-progress-failed{background-color:red}#__layout{overflow:hidden;background-color:#082a4e;background:url(/images/footer.svg);background-position:center top -32px;z-index:-3}.v--modal-block-scroll .main{perspective:none!important}.container[data-v-4fb19cc9]{max-width:1600px;margin:0 auto;background-color:#fff;position:relative}.relative[data-v-4fb19cc9]{z-index:1;background:#fff;min-height:calc(100% - 32.25rem)}.main[data-v-4fb19cc9],.relative[data-v-4fb19cc9]{position:relative}.main[data-v-4fb19cc9]{z-index:2;transition:padding-top .3s ease-out;height:100vh;overflow-x:hidden;overflow-y:auto;perspective:2px}.announcement-bar[data-v-05ebcea3]{position:-webkit-sticky;position:sticky;left:0;top:0;width:100%;display:flex;align-items:center;flex-direction:row;justify-content:center;overflow:hidden;text-align:left;font-size:18px;color:#fff;min-height:90px;background:#0a2b4e;text-align:center;z-index:0;transition:background .25s ease-out}@media screen and (max-width:39.9375em){.announcement-bar[data-v-05ebcea3]{flex-direction:column}}.announcement-bar[data-v-05ebcea3]:hover{text-decoration:none;background-color:#08182f}.announcement-bar a[data-v-05ebcea3]:after{content:"";position:absolute;z-index:20;top:-100%;width:100%;height:200%;background:radial-gradient(50% 50% at 50%,at 50%,#fff 0,hsla(0,0%,76.9%,0) 100%);background:radial-gradient(50% 50% at 50% 50%,#fff 0,hsla(0,0%,76.9%,0) 100%);mix-blend-mode:overlay;pointer-events:none}@media screen and (min-width:82em){.announcement-bar[data-v-05ebcea3]{font-size:22px}}.anounce-icon[data-v-05ebcea3]{display:none;align-items:center}@media screen and (min-width:40em){.anounce-icon[data-v-05ebcea3]{display:flex;width:70px;margin:0 18px}}.button[data-v-05ebcea3]{display:inline-block;font-size:18px;min-width:150px;margin:0 0 0 2rem;position:relative;z-index:21}@media screen and (max-width:39.9375em){.button[data-v-05ebcea3]{margin-left:0;margin-bottom:1rem}}.para[data-v-05ebcea3]{z-index:2;max-width:270px}@media screen and (min-width:82em){.para[data-v-05ebcea3]{max-width:100%}}.para span[data-v-05ebcea3]{text-transform:uppercase;color:#000;font-weight:800}.squares[data-v-05ebcea3]{position:absolute;z-index:0;left:0;display:flex;width:calc((100% - 600px)/ 2);min-width:104px;height:100%}@media screen and (min-width:82em){.squares[data-v-05ebcea3]{width:calc((100% - 1000px)/ 2);height:100%;bottom:auto;top:0;min-width:auto}}.square[data-v-05ebcea3]{width:40%;height:100%;background-color:#091221;position:absolute;left:0;color:#091221;-webkit-animation-name:blinker-data-v-05ebcea3;animation-name:blinker-data-v-05ebcea3;-webkit-animation-iteration-count:infinite;animation-iteration-count:infinite;-webkit-animation-duration:10s;animation-duration:10s}.square[data-v-05ebcea3]:first-child{-webkit-animation-name:blinker2-data-v-05ebcea3;animation-name:blinker2-data-v-05ebcea3;opacity:.5;width:70%}.square[data-v-05ebcea3]:nth-child(2){-webkit-animation-name:blinker3-data-v-05ebcea3;animation-name:blinker3-data-v-05ebcea3;opacity:.25;width:100%}.square[data-v-05ebcea3]:after{content:"";border-top:156px solid transparent;border-bottom:0 solid transparent;border-left:56px solid;position:absolute;left:100%}@media screen and (min-width:82em){.square[data-v-05ebcea3]:after{border-top:45px solid transparent;border-bottom:45px solid transparent;border-left:45px solid}}@-webkit-keyframes blinker-data-v-05ebcea3{36%{opacity:1;width:40%}46%{color:#091221;background-color:#091221}50%{opacity:0;width:1000%}55%{opacity:0;width:0}65%{opacity:1}}@-webkit-keyframes blinker2-data-v-05ebcea3{35%{opacity:.5;width:70%}45%{color:#091221;background-color:#091221}50%{opacity:0;width:1000%}55%{opacity:0;width:0}70%{opacity:.5}}@-webkit-keyframes blinker3-data-v-05ebcea3{34%{opacity:.25;width:100%}44%{color:#091221;background-color:#091221}50%{opacity:0;width:1000%}55%{opacity:0;width:0}70%{opacity:.25}}.right[data-v-05ebcea3]{transform:scaleX(-1);left:auto;right:0}.header[data-v-3ffe7e8c]{background-color:#fff;position:relative;width:100%;top:0;z-index:4;display:flex;justify-content:center}.header .wrapper[data-v-3ffe7e8c]{height:100px;display:flex;flex-wrap:wrap;align-items:center}.logo[data-v-3ffe7e8c]{display:block;max-width:200px;margin-top:-5px;position:relative;z-index:1;transition:all .2s ease-in}.logo img[data-v-3ffe7e8c]{width:100%}@media screen and (min-width:78em){.logo[data-v-3ffe7e8c]{transform:none;max-width:240px;min-width:200px;margin:-10px 90px 0 0}}.vuemastery-logo[data-v-3ffe7e8c]{display:block}.vuemastery-logo-white[data-v-3ffe7e8c]{display:none}.no-header-background[data-v-3ffe7e8c]{position:absolute;width:100%;background:0 0}@media screen and (min-width:78em){.no-header-background[data-v-3ffe7e8c] .navbar-item{color:#fff}}.no-header-background .wrapper[data-v-3ffe7e8c]{position:absolute;width:100%}.no-header-background .hamburger[data-v-3ffe7e8c]:after,.no-header-background .hamburger[data-v-3ffe7e8c]:before{background-color:#fff}.no-header-background .vuemastery-logo[data-v-3ffe7e8c]{display:none}.no-header-background .vuemastery-logo-white[data-v-3ffe7e8c]{display:block}.close[data-v-3ffe7e8c]{position:absolute;top:0;right:10px;background:0 0;border:0;color:#0a2b4e;font-size:2em}.open-nav .logo{transform:translateX(-50%);margin-left:50%}.open-nav .no-header-background .vuemastery-logo{display:block}.open-nav .no-header-background .vuemastery-logo-white{display:none}.open-nav .hamburger:after,.open-nav .hamburger:before{background-color:#222!important}.navbar[data-v-3f7198bf]{position:fixed;height:100%;display:flex;flex-wrap:wrap;align-items:center;justify-content:center;flex:1;left:0;width:100%;flex-direction:column;background-color:#fff;opacity:0;transition:opacity .2s ease-in;top:0}.navbar [data-v-3f7198bf],.navbar[data-v-3f7198bf]{pointer-events:none}@media screen and (min-width:78em){.navbar[data-v-3f7198bf]{top:0;background-color:transparent;pointer-events:none;opacity:1;justify-content:space-between;position:relative;height:100px;flex-direction:row;flex-wrap:nowrap}.navbar [data-v-3f7198bf]{pointer-events:auto}}.navbar-item[data-v-3f7198bf]{color:#0a2b4e;font-family:"Source Sans Pro",sans-serif;font-size:20px;text-decoration:none;white-space:nowrap;opacity:0}.navbar-item.nuxt-link-active[data-v-3f7198bf]{font-weight:700}.navbar-item[data-v-3f7198bf]:last-child{margin-right:0}@media screen and (min-width:78em){.navbar-item[data-v-3f7198bf]{opacity:1;margin-right:25px}}.button[data-v-3f7198bf]{opacity:0;font-size:20px}.button+button[data-v-3f7198bf]{margin:0}@media screen and (min-width:78em){.button[data-v-3f7198bf]{font-size:18px;opacity:1}}.navbar-main[data-v-3f7198bf],.navbar-secondary[data-v-3f7198bf]{display:flex;flex-direction:column;text-align:center;justify-content:space-evenly;align-items:center;pointer-events:none}@media screen and (max-width:77.938em){.navbar-main a[data-v-3f7198bf],.navbar-main button[data-v-3f7198bf],.navbar-secondary a[data-v-3f7198bf],.navbar-secondary button[data-v-3f7198bf]{margin-bottom:40px}.navbar-main .inverted[data-v-3f7198bf],.navbar-secondary .inverted[data-v-3f7198bf]{height:auto;line-height:24px}}@media screen and (min-width:78em){.navbar-main[data-v-3f7198bf],.navbar-secondary[data-v-3f7198bf]{flex-direction:row;height:100px;pointer-events:auto}.navbar-main .button[data-v-3f7198bf],.navbar-secondary .button[data-v-3f7198bf]{margin-left:18px}}@media screen and (max-height:45em){.open-nav .navbar[data-v-3f7198bf]{padding-top:60px}.open-nav .navbar a[data-v-3f7198bf],.open-nav .navbar button[data-v-3f7198bf]{margin-bottom:25px}}.navbar-profile[data-v-3f7198bf]{display:flex;border-radius:50%;overflow:hidden;box-shadow:0 1px 0 0 #e4e4e4;background-color:#fff;width:40px;height:40px}@media screen and (min-width:78em){.navbar-profile[data-v-3f7198bf]{margin-left:18px}}.navbar-profile img[data-v-3f7198bf]{width:100%}.signin-enter-active[data-v-3f7198bf]{transition:opacity .5s ease-in}.signin-enter-active .appear[data-v-3f7198bf]{transition:transform .5s ease-out}.signin-enter-active .navbar-profile[data-v-3f7198bf]{transition:opacity .3s .2s}.signin-leave-active[data-v-3f7198bf]{transition:opacity .3s ease-out}.signin-enter[data-v-3f7198bf],.signin-leave-to[data-v-3f7198bf]{opacity:0}.signin-enter .appear[data-v-3f7198bf]{transform:translateX(58px)}.signin-enter .navbar-profile[data-v-3f7198bf]{opacity:0}.linkin-enter-active[data-v-3f7198bf]{transition:margin-right .5s ease-out,opacity .3s ease-out .5s}.linkin-leave-active[data-v-3f7198bf]{transition:margin-right .5s ease-out .2s,opacity .3s ease-out}.linkin-enter[data-v-3f7198bf],.linkin-leave-to[data-v-3f7198bf]{margin-right:-91px;opacity:0}.signout-enter-active[data-v-3f7198bf],.signout-leave-active[data-v-3f7198bf]{transition:opacity .3s ease-in}.signout-enter[data-v-3f7198bf],.signout-leave-to[data-v-3f7198bf]{opacity:0}@media screen and (min-width:87em){.search-link[data-v-3f7198bf]{display:none}}.open-nav{max-height:100vh;overflow:hidden}.open-nav .navbar *{pointer-events:auto}@media screen and (min-width:78em){.open-nav{max-height:100%}}.open-nav .navbar{opacity:1}.open-nav .button,.open-nav .navbar-item{opacity:1;transition-duration:.4s}.open-nav .button:first-child,.open-nav .navbar-item:first-child{transition-delay:.1s}.open-nav .button:nth-child(2),.open-nav .navbar-item:nth-child(2){transition-delay:.2s}.open-nav .button:first-child{transition-delay:.3s}.open-nav .button:nth-child(2){transition-delay:.4s}.open-nav .header{position:fixed!important}.hamburger[data-v-dbefde78]{background-color:transparent;display:flex;right:0;margin:0;padding:0;width:5rem;height:4rem;font-size:0;text-indent:-9999px;z-index:40;-webkit-appearance:none;-moz-appearance:none;appearance:none;box-shadow:none;border-radius:0;border:none;cursor:pointer;overflow:hidden;position:absolute;top:18px}@media screen and (min-width:78em){.hamburger[data-v-dbefde78]{display:none}}.hamburger[data-v-dbefde78]:focus{outline:0}.hamburger[data-v-dbefde78]:after,.hamburger[data-v-dbefde78]:before{content:"";display:block;height:2px;width:2rem;background-color:#222;transition:transform .1s ease-in-out,margin-top .1s ease-in-out .1s;position:absolute;right:1rem;top:50%}.hamburger[data-v-dbefde78]:before{margin-top:-.6rem}.hamburger[data-v-dbefde78]:after{margin-top:.3rem}.open-nav .hamburger[data-v-dbefde78]:after,.open-nav .hamburger[data-v-dbefde78]:before{transition:transform .1s ease-in-out .1s,margin-top .1s ease-in-out}.open-nav .hamburger[data-v-dbefde78]:before{transform:rotate(45deg);margin-top:0}.open-nav .hamburger[data-v-dbefde78]:after{transform:rotate(-45deg);margin-top:0}[class^=ais-]{font-size:1rem}.search-wrapper{position:relative;top:-100px}.no-header-background{top:0}.ais-wrapper{position:absolute;left:10rem;transition:left .5s ease-out;max-width:246px}.ais-wrapper.signin{left:14rem}.ais-Hits-Img{display:none}.ais-SearchBox-form{display:block;position:relative}.ais-SearchBox-form button{color:#39b982;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none;box-shadow:none;border-radius:0;border:none;position:absolute;right:.8rem;top:.4rem;cursor:pointer}.ais-SearchBox-form button.reset{right:.35rem;top:.45rem}.ais-SearchBox-loadingIndicator,.ais-SearchBox-reset,.ais-SearchBox-submit{background:0 0;-webkit-appearance:none;-moz-appearance:none;appearance:none;position:absolute;z-index:1;width:25px;height:20px;top:50%;right:.3rem;transform:translateY(-50%);border:0;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.ais-Highlight-highlighted,.ais-Snippet-highlighted{text-decoration:underline;text-underline-position:under;background-color:transparent;color:#0a2b4e;-webkit-text-decoration-color:#39b982;text-decoration-color:#39b982}.ais-Hits-Box{flex:1}.ais-Hits-Box .ais-Hits-Title{margin-top:0}.ais-Hits-Box .ais-Hits-Title .badge{font-size:.6rem}.ais-Hits-Box .badge{margin:8px 8px 8px 0}.ais-Hits-Box .ais-Highlight{margin-right:8px}.ais-background{position:fixed;top:0;left:0;width:100%;height:100%}.ais-InstantSearch{display:none;position:absolute;top:28px;right:50%}@media screen and (min-width:87em){.ais-InstantSearch{z-index:5;display:block}}.ais-SearchBox{max-width:246px}.ais-ToggleRefinement-checkbox{display:none}.ais-ToggleRefinement-checkbox:checked+span:before{content:"free";position:absolute;top:0;left:0;background-color:#39b981;width:100%;height:100%;text-align:right;display:flex;justify-content:flex-end;align-items:center;padding-right:1.7rem;box-sizing:border-box}.ais-SearchBox-input{width:100%;height:44px;padding:0 36px 0 23px;font-size:20px;border:2px solid #bbb;border-radius:30px;transition:all .2s ease-out}.ais-SearchBox-input:focus{outline:0}.ais-SearchBox-submitIcon path{fill:#39b982}.ais-StateResults{background:#f5f5fa;padding:0 1.1rem;border-radius:0 0 10px 10px}.search-top{overflow:hidden;position:relative;padding:0 1rem;border-radius:0 10px 0 0;border-bottom:2px solid #fff;display:flex;justify-content:space-between;align-items:center;background:#082a4e;text-transform:uppercase;color:#fff}@media screen and (max-width:39.9375em){.search-top{display:none}}.search-result{opacity:0;width:400px;max-width:100%;height:0;transition:opacity .2s ease-out,transform .2s ease-out;overflow:hidden;pointer-events:none;transform:translateY(30px)}.show.ais-wrapper{max-width:none}.show .search-result{transition:opacity .2s ease-in,transform .2s ease-in;transform:translateY(2px);opacity:1;height:100%;pointer-events:auto;overflow:visible}.show .search-result:before{content:"";position:absolute;height:3px;top:-3px;width:250px;background-color:#0a2b4e;-webkit-mask-image:radial-gradient(circle 3px at 249px,at 0,transparent 0,transparent 3px,#000 0);-webkit-mask-image:radial-gradient(circle 3px at 249px 0,transparent 0,transparent 3px,#000 0)}.show .ais-SearchBox-input{padding:0 36px 0 18px;border-radius:10px 10px 0 0;border-color:#f5f5fa;background:#f5f5fa}.show .ais-SearchBox-submit{opacity:0}.ais-Menu-list{display:flex;list-style-type:none;margin-left:-.5rem;padding:1rem 0;position:relative;z-index:4}.ais-Menu-item{margin-right:.5rem;margin-bottom:0}.ais-Menu-count,.ais-ToggleRefinement-count{display:none}.ais-Menu-label,.ais-ToggleRefinement-labelText{padding:4px 10px;border-radius:24px;font-size:.8rem;cursor:pointer;color:#fff;display:block;text-decoration:none}.ais-Menu-item{border-radius:24px}.ais-Menu-item--selected,.ais-Menu-item:hover,.ais-ToggleRefinement-labelText:hover{background:#835ec2;color:#fff}.ais-Menu-item--selected .ais-Menu-link,.ais-Menu-item:hover .ais-Menu-link,.ais-ToggleRefinement-labelText:hover .ais-Menu-link{text-decoration:none}.ais-ToggleRefinement{padding:1rem 0;margin-left:auto}.ais-ToggleRefinement--noRefinement{display:none}.ais-Hits-list{margin:0;padding:0;justify-content:space-between;list-style-type:none}.ais-Hits-item{cursor:pointer;padding:.7rem 1.5rem;margin:0 -1.1rem}@media screen and (min-width:40em){.ais-Hits-item{padding:.7rem 1.1rem}.ais-Hits-item:hover{margin:0 -.7rem;padding:.7rem}}.ais-Hits-item:hover{background:#fff}.ais-Hits-item:hover .ais-Highlight,.ais-Hits-item:hover a{text-decoration:none}.ais-Hits-item a{display:flex}.ais-Pagination-list{display:flex;list-style-type:none;justify-content:space-evenly;margin:0;padding:20px 0 30px}.ais-Pagination-list li{display:flex;margin-bottom:0}.ais-Pagination-item{border-radius:50%;border:none;background-color:transparent;color:#39b982;width:25px;height:25px;justify-content:center;align-items:center;cursor:pointer;transition:background-color .3s ease-out}.ais-Pagination-item:hover{transition:background-color .1s ease-in}.ais-Pagination-item .ais-Pagination-link{transition:color .3s ease-out;line-height:0;height:2px}.ais-Pagination-item .ais-Pagination-link:hover{transition:color .1s ease-out}.ais-Pagination-item--selected{background-color:#39b982}.ais-Pagination-item--selected .ais-Pagination-link{color:#fff;text-decoration:none}.ais-Pagination-itemhover{background-color:#fff}.ais-Pagination-itemhover .ais-Pagination-link{color:#666;text-decoration:none}.ais-Snippet{display:block;color:#666;font-size:1rem;line-height:1.4rem;padding-bottom:10px;max-width:365px}.no-result{padding:1rem 0}.no-result p{margin-top:0}.no-result q{font-weight:700}.no-result ul{list-style-type:none;padding:0;margin-left:0}.search{top:0;transition:0s}.search .ais-Hits-Img{display:none;width:200px;padding-top:.9rem;padding-bottom:.9rem;-o-object-fit:contain;object-fit:contain;-o-object-position:top;object-position:top}@media screen and (min-width:40em){.search .ais-Hits-Img{display:block}}@media screen and (min-width:40em){.search .ais-Hits-Box{padding-left:2rem}}.search .ais-InstantSearch{opacity:1;pointer-events:auto;position:relative;transform:none;top:0;right:0;display:block;min-height:100vh}.search .search-result{margin:0 auto;width:100%;background:#f5f5fa;opacity:1;transform:none;pointer-events:auto;padding-bottom:1px;height:100%}.search .search-result:before{display:none}.search .search-top{width:100%;max-width:100%;background:#0a2b4e;color:#fff;border-radius:0;border-radius:initial;border-bottom:none;margin-bottom:1rem;display:flex;padding:0 .8rem 0 1.4rem}@media screen and (min-width:40em){.search .search-top{margin-bottom:2rem}}@media screen and (min-width:50em){.search .search-top{padding-left:calc(50% - 345px);padding-right:calc(50% - 357px)}}.search .ais-StateResults{margin:0 auto;background:0 0;max-width:100%;width:785px;min-height:300px}.search .ais-wrapper{position:relative;left:0;margin-right:0;max-width:none}.search .ais-background{display:none}.search .ais-SearchBox{background-color:#0a2b4e;display:flex;justify-content:center;align-items:center;height:5rem;max-width:100%;margin-bottom:-1rem}@media screen and (min-width:40em){.search .ais-SearchBox{height:7rem}}.search .ais-SearchBox-input{padding:0 1.4rem;height:47px;font-size:16px;border:2px solid #bbb;border-radius:30px;color:#000;color:initial}@media screen and (min-width:40em){.search .ais-SearchBox-input{padding:0 2rem;height:54px;font-size:20px}}.search .ais-SearchBox-form{width:760px;max-width:calc(100% - 1.4rem)}.search .ais-SearchBox-form button{right:1.4rem;top:.7rem}.search .ais-SearchBox-form button.reset{top:.75rem}.search .ais-Menu-list{padding:1rem 0 1.3rem}.search .ais-SearchBox-submit{right:2.2rem}.search .ais-SearchBox-reset{right:1.8rem}.search .ais-ToggleRefinement-checkbox:checked+span{background:#39b982;border-radius:24px;color:#fff}.search .ais-ToggleRefinement-checkbox:checked+span:before{content:none}.search .ais-Snippet{max-width:100%}.search .ais-Hits-Title{line-height:1.2rem}.search .ais-Pagination-list{max-width:500px;padding:0;margin:2rem auto 3rem}.search .no-result{text-align:center}.search .no-result ul{list-style-type:none;padding:0;margin-left:0}.lesson-wrapper[data-v-33673d7f]{display:grid;background:#fff;grid-template-columns:1fr;grid-template-areas:"header" "video" "list" "content" "sidebar" "footer"}@media screen and (min-width:78em){.lesson-wrapper[data-v-33673d7f]{grid-template-columns:1fr 1fr 30%;grid-template-areas:"header header header" "video video list" "content content sidebar" "footer footer footer"}}.lesson-header[data-v-33673d7f]{grid-area:header}.lesson-content[data-v-33673d7f]{grid-area:content}.lesson-aside[data-v-33673d7f]{grid-area:sidebar;padding:0 4%}@media screen and (min-width:78em){.lesson-aside[data-v-33673d7f]{padding:0 8%;margin:90px 0}}.lesson-aside>div[data-v-33673d7f]{margin-bottom:20px}.lesson-aside .control-group[data-v-33673d7f]{justify-content:center}@media screen and (min-width:78em){.lesson-aside .control-group[data-v-33673d7f]{justify-content:space-evenly}}.lesson-aside .control-group .button[data-v-33673d7f]{margin-right:4%}@media screen and (min-width:78em){.lesson-aside .control-group .button[data-v-33673d7f]{margin-right:0}}.lessons-nav[data-v-33673d7f]{grid-area:footer}.download[data-v-33673d7f]{color:#fff;text-align:center;align-items:center;background-image:url(/images/bkg-cheatsheet-main.jpg);margin-bottom:45px}.download .button[data-v-33673d7f]{margin:0 auto}.player[data-v-1d0f7d72]{height:200px;width:100%;background-color:#222;display:flex;justify-self:center;align-items:center;justify-content:center}@media screen and (min-width:40em){.player[data-v-1d0f7d72]{height:320px}}@media screen and (min-width:78em){.player[data-v-1d0f7d72]{height:440px}}.play-button[data-v-1d0f7d72]{height:70px;width:70px;background-color:#fff;border-radius:50%;display:flex;align-items:center;justify-content:center}.play-button[data-v-1d0f7d72]:after{content:"";width:0;height:0;border-top:12px solid transparent;border-bottom:12px solid transparent;border-left:20px solid #222;margin-right:-7px}.brand[data-v-52a5e9ad]{grid-area:brand}.nav-1[data-v-52a5e9ad]{grid-area:nav-1}.nav-2[data-v-52a5e9ad]{grid-area:nav-2}.footer[data-v-52a5e9ad]{background:linear-gradient(#041830,#080117);border-bottom:40px solid #080117;color:#fff;position:relative;padding:100px 0 45px}@media screen and (min-width:40em){.footer.course-path[data-v-52a5e9ad],.footer.courses[data-v-52a5e9ad],.footer.index[data-v-52a5e9ad]{padding-top:200px}}.footer.index[data-v-52a5e9ad]{padding-top:100px;margin-top:-3vw;background:url(/images/footer.svg);background-position:top;background-size:cover;z-index:5}@media screen and (min-width:40em){.footer.index[data-v-52a5e9ad]{padding-top:220px}}@media screen and (min-height:44em) and (min-width:40em){.footer[data-v-52a5e9ad]:not(.no-sticky-footer){position:-webkit-sticky;position:sticky;bottom:0;z-index:0}}.wrapper[data-v-52a5e9ad]{display:grid;text-align:center;grid-template-columns:1fr 1fr;grid-template-areas:"nav-1 nav-2" "brand brand"}.wrapper[data-v-52a5e9ad] .social{justify-content:center;margin:50px 0 -50px}@media screen and (min-width:40em){.wrapper[data-v-52a5e9ad]{text-align:left;grid-template-columns:1fr 22% 22%;grid-column-gap:4%;grid-template-areas:"brand nav-1 nav-2"}.wrapper[data-v-52a5e9ad] .social{justify-content:left;margin:0}}.brand[data-v-52a5e9ad]{margin-bottom:45px}.brand .logo[data-v-52a5e9ad]{display:block;max-width:284px;min-width:200px}@media screen and (max-width:39.9375em){.brand .logo[data-v-52a5e9ad]{margin:45px auto}}.brand .logo img[data-v-52a5e9ad]{display:block;width:100%}.brand[data-v-52a5e9ad] .button.link{color:#fff;margin-left:10px}@media screen and (min-width:40em){.brand[data-v-52a5e9ad] .button.link{margin-left:0;margin-right:10px}}.social[data-v-52a5e9ad]{display:flex;font-size:1.5em}.nav-1[data-v-52a5e9ad],.nav-2[data-v-52a5e9ad]{justify-self:center}.nav-1 h4[data-v-52a5e9ad],.nav-2 h4[data-v-52a5e9ad]{text-transform:uppercase}.nav-1 li[data-v-52a5e9ad],.nav-2 li[data-v-52a5e9ad]{margin:15px 0}.social[data-v-d180769e]{display:flex;font-size:2em;margin-right:1em}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment