Key things:
v-bind
=> :
v-on
=> @
v-bind:class="klass-holder"
=> :class="klass-holder"
v-on:click="callMethod"
=> @click="callMethod"
We used computet object of vue -which act as cache of data you want to render
When creating components or any child component,it takes the exact same shape as app.vue/ vue.createApp(), which means; it can have a data method, mounted hook and it's own reactivity. It's 'small' app buy itself, it takes the interface/architecture perse.
Since we can have these many components, in the next episode we are going to extract these components to a separate directory and plug them as child components whenever we need them.
-We have successfully extracted each Vue component to it's own file. And we can create these components as many as we can;
-Using devtool extension in the browser, we can be able to see the nested components from rootapp and by selecting each component, we get to see it's reactivity data that's associated with.
-NB: For use to import this js component to our script section of out html, we use the <script type="module">
otherwise it will throw an error.
-Props are like parameters we pass when we create new instance of any component/class. -Which means, you have to pass these parameters when creating it's instance, but if we don't want to pass parameters, we can give default values incase someone does not pass.
-We went back to our backup.html file, and we want to implement component structure as we learned from last class. We bring the code to main index.html and delete the backupfile.html
We you want to separate an html element to a component, thing of what is the importance of doing so. In most cases you want to make it as component because of reusability, but if you're not going to reuse it, no need to refractor it.
Key things:
-Note that inside assignments component, we import AssignmentList and we pass the props/parameters and is must since we didn't provide the defaults incase one doesn't pass.
-Note when we create an instance of the component, inside template, we have given it props/parameters: :assignments="filters.inProgress" title="In Progress"
.
For the title
prop/parameter, we passed in a string without using :
or v-bind
you can call it. Therefore it will pass it as a string.
But if we do the same in 'assignments' prop/parameter, we will be passing just a string and the component require array of the assignments. That's why we had to use :
to bind the data from source of truth instead of a string.
-When submitting the form, the default action makes the browser to reload, and we want to prevent that default action.
-In our form that we created, we passed add method to submit event. In our method add(e)
, we pass the event and we use that event to command the behavior we want i.e e.preventDefault()
behavior. This tells us that the default behavior of submit event is page reload.
Therefore we can prevent this behavior at higher level since the submit has this behavior.
-Instead of passing the event to our function add(e)
, we do this:
<form @submit.prevent="add"> <button type="submit" class="bg-white p-2 border-l">Add</button> </form>
instead of ...
<form @submit="add"> ********************************* methods: { add(e){ e.preventDefault(); alert("hi there!"); } }
-We have separated create concern, therfore assignment-create component.
-The compent has to do it's work, create new assignement and then communicates back to the parent component "Here is the new item i have created, do what you want to do with it, otherwise I'm done".
-You will find out in most cases that when it comes to parent-child component communication, is that the parent will communicate to child by passing down the props
. The child on the hand will communicate back to the parent by emiting
an event.
-Now in our main/parent component Assignments
, it will listen to add
event which was emitted by child component AssignmentCreate
, and the calls it's own method add(title)
which receives the parameter and it takes care of pushing it to the assignments array.
- In this episode we don't have a direct objective. We just have to demonstrate how things get easy when you learn the basics.
- If you an set of array like this:
return new Set(this.assignments.map(a => a.tag));
, this will list each assignment tag. And we will have number of tags equal to the number of assignments if any. - What if in this newList/Array of
tags[]
, you want to append new tag/item? - You will simply map your new item to the list of tags.
return ['all', ...new Set(this.assignments.map(a => a.tag))];
- The 3 dots will append 'all' item to set that will be created on the right.
- In this section we will be refactoring our components.
- When refactoring, we usaully talk of responsibility or separation of concerns. Byt this we mean, when you're reviewing your components, you will always be thinking 🤔, ...should this component be responsible for that?
- Take an example of
AssignmentList component
, it's responsible for displaying a list of assignments. - The moment we introduced tags in the same component, we add so many things just to make the tags functionality work. Now the component which was responsible for listing assignments, is now having so many things and probably more to come.
- Let's refractor the tag concern out.
- Start with the template by extractiin it out and create a new component of it's name.
- Check the extracted component , which data does it need?
- We create
assignemenr-tags
component, and passed two $props to itcurrentTag
&initialTags
. - And then when we click on the tag, we emit change event together with the currentTag user selected:
@click="$emit('change', tag)"
- and then listen this event in our parent component:
@change="currentTag = $event"
-we will assign that event varuable we have received to the currentTag in our parent component. And becuase of reactivity, the currentTag will pass it's self to child component and change the color of selected tag::current-tag="currentTag"
.
- and then listen this event in our parent component:
- For sure, this magical and pretty easy on how we can achiev dynamicity of data even on separation of concerns/compoments. They are able to communicate effectively and we also have clean code.
-
We learned the basic of the V-Model. We know that we can apply it to a form input :
v-model="newAssignment"
, and then magically it keeps everything in sync. -
But, what exactly happens for it to behave as such? Let's have a look...
- First, it binds the value to the input field.
- Second, it listens for when the value changes.
-
The long form of
v-model
is equivalent to this :<input type="text" :value="newAssignment" @input="newAssignment = $event.target.value" >
image.png -
Lets see how we can apply v-model knownledge somewhere else other than input element.
-
In our assignment-list component, we have embedded assignment tags component, which needs
currentTag
as prop. -
Therefore, we pass our the prop
currentTag
to assignment-tag component. Since Assignement-List component is the owner/source of truth of this variable:currentTag
, we need to also to listen when assignmet-tag component changes the value ofcurrentTag
so that we can update it instantly to anyone else who might need that value, hencesource of truth
.
-
We are basically passing/binding a value, and then, we also listening a kind of input event.
<assignment-tags :initial-tags="assignments.map(a => a.tag)" :current-tag="currentTag" @change="currentTag = $event" > </assignment-tags>
-
From our long version(how v-model works under the hood) of
v-model
=><input type="text" :value="newAssignment" @input="newAssignment = $event.target.value" >
, is the exact same as what we are doing when binding a value to the child component. -
We actually can use
v-model
here becuase it does the exact same thing we have explained above. -
From the above code, we are listening for change event and then manually update it on the parent. We can leavetage
v-model
to do that(listen & update) for us behind the scene.<assignment-tags v-model:currentTag="currentTag" :initial-tags="assignments.map(a => a.tag)" > </assignment-tags>
-
Note that, we are explicitly telling
v-model
to which modelValue value should listen/update. That's we are doing thisv-model:currentTag
. -
And then in our child component: assignment-tags, we can update the value of
currentTag
directly and it will be passed byv-model
.<button @click="$emit('update:currentTag', tag)" v-for="tag in tags" class="border rounded px-1 py-px text-xs" :class="{ 'border-blue-500 text-blue-500': tag == currentTag }" > {{ tag }} </button>
-
On click, update the prop currentTag and since there's a v-model binded with it, it will be magically updated.
-So far, we've been hard-coding the list of assignments directly within our Vue component. But of course, that's not overly realistic. Let's switch over to using the fetch() JavaScript API to request data from a fake API.
-
We need to setup a fake API with
npm install json-server --save-dev
-
After we create the server, we need to get the server up running using
npx json-server
-
If will run without a database, it will obvisouly complain that it needs a database to work with. Therefore let's create a database. Remember they are all fake, fake API and fake db.
-
We create a file name
db.json
and move our assignments data from assignments component and fetch them from our new fake API. -
Now we run the server:
npx json-server db.json -p 3001
-
We need to understand how vue component Lifecycle works. From beforeCreated(), create(), mounted(), unmounted(). =>Hooks
-
We want to fetch data before the component is mounted, therefore we need to perform an axios/fetch request inside created() hook.
- Fetch API returns which know as a
promise
- In a asynchronously, a promise is to say : i will give you your money eventually when i get it. even though for now you can't get your money. Working with promises is to hope for feedback eventually.
created(){
fetch('http://localhost:3001/assignments') //fetch api giving a promise to return data
.then(response => response.json()) //tell api, when you will have this data, we call it 'response', I want it in json form. The API will say, i will give you that json but not right away. Another Promise! So we need to do a second 'then`
.then(data => {
console.log(data);
}); //when you have data, console it.
},
- Here we cover on how to make a componet flexible for reusability and being able to tweak/modified them.
- You notice if you run
npm serve
and the link on the browser. You're not going to see the assignment list becuase the array is empty. Until we run our server again. So that api can fetch our assignments from fake api. - We can create one script to run these two concurrently.
- Inside
package.json
add scripts"scripts": { "start": "npx serve & npx json-server db.json -p 3001" },
- Note we have used single
&
instead of&&
. Using double and symbol it will run the first part of the command and the next one. We don't want that. - Now if we run our script with keyword
start
=>npm run start
, we are sure all servers will boot simultaneously. - If you are on Windows, the script will only run the first part of the command. For some reason, the concurrency is limted.
- Alternatively, install concurrency package from here
npm install concurrently --save-dev
. - In package.json, instead of
"start": "..."
use"dev": "concurrently --kill-others \"npx serve\" \"npx json-server db.json -p 3001\""
- Now run
npm run dev
to start your servers. - We can now do some stlying of our assignments component, change to column orientation.
- When add flex class in the section wrapper of in asignments component. assignment-create component seems to be misplaced
- We could like to have it just below the in progress assignment-list component by sloting it right below the
<ul></ul>
. - By using
slots
or addingflags
to the component, it gives us a way to selectively extend the component when we want to(where the component is called).
-
In the last episode, we managed to create a component that accepts a single defualt slot. But in practicle situations, you will find that some of your components require multiple slots.
-
What if you need a single panel like that follows a certain pattern where by you can stack any element within.
-
For example from our
assignment-list
component, we have a wrapper that is common across thein progress
andcomplete
assignements. -
Let's create a reusable card/panel that accepts heading slot, footer, and content with the same styling as the
assignment-list
component. It will be responsible for card styling and any behaviour. It can be inherited and implemented/extended/overrided by maybe color code.<div class="bg-gray-700 p-4 border border-gray-600 rounded-lg"> <h2 class="font-bold"> <slot name="heading"/> </h2> </div>
-
We have created a panel which accepts a slot name 'heading', if you pass any slot with the keyword 'heading' directive, it will be slotted at that space.
-
Implementation:
<panel> <template v-slot:heading> Hey There! </template> </panel>
-
The
v-slot:...
is where we specify the name of the slot, where it should. otherwise it will be a defualt slot. -
You can choose to use this pound sign
#
which is same asv-slot:
, it's the same thing.<panel> <template #heading> Hey There! </template> </panel>
-
NOTE: In our
panel
component, is expecting two slots; heading & content.export default { template: ` <div class="bg-gray-700 p-4 border border-gray-600 rounded-lg"> <h2 class="font-bold"> <slot name="heading" /> </h2> <p> <slot name="content" /> </p> </div> `, }
-
If we slot in only one slot, like this:
<panel> <template #content> Sample random paragraph here...with no heading </template> </panel>
of course the 'content' will display, but the 'heading' will also render even when we don't need it.
-
How do with make sure that uneeded components do not render?
-
We use a vue property called
$slots
which stores all defined slots. Therefore, we can use it to check if the slot have something(it's empty by default) and render iff it does.<div class="bg-gray-700 p-4 border border-gray-600 rounded-lg"> <h2 v-if="$slots.heading" class="font-bold"> <slot name="heading" /> </h2> <p> <slot name="content" /> </p> <footer v-if="$slots.footer"> <slot name="footer" /> </footer> </div>
-
This way, we are able to create more configurable and flexible components.
-
Cool. Now let's take it further and suport themes : 'dark' and 'light'.
-
We simply define a prop 'theme' with a default value/theme. And we can pass our desired theme whenever we want from anywhere. The styling will be determined by the theme property.
props: { theme: {type: String, default:'dark'}, },
and then use styling in reference to the theme property.
:class="{ 'p-4 border rounded-lg':true, 'bg-gray-700 border-gray-600 text-white' : theme == 'dark', 'bg-white border-gray-300 text-black' : theme == 'light', }"
-
Now that we have a ready made panel, we can use it anywhere in application.
-
We can go to our assignment-list component and import the panel, register it as a component, and then use it instead of section element.
import Panel from './Panel.js'; export default { components: {Assignment, AssignmentTags, Panel}, template: ` <Panel :class="class_type" v-show="assignments.length" class="w-60">
-
That is it. Well done. 😊
- Vite is a French word meaning(quick).
- It is a build tool that takes care of server hot realoading. Which basically means, when i make change on file, the browser will load the changes instantly.
- It also includes a build tool that will bundle up your code to make it as performance as possible.
-
In our project directory
vue-learning
, we are going to to create another project withvite
and give it a name "_testVite". -
At the time of the course, the command for creating new app with vite is this
npm create vite@latest
-
Vite will ask you few questions like
? Project name: >> _testVite
-
? Package name: >> _testVite
-
? Select a framework: >> Vue
-
? Select a variant: >> JavaScript
-
Then the project will scaffold int directly
_testVite
, cd to this directory and then runnpm install
&npm run dev
-
We can configure our app to use
@
to asalias
to our source path, which will make our life easier. -
Inside
vite.config.js
add this:
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
}
})
- Now in our
main.js
file we can use@
instead of directly accessing pathe./../comp.js
//from this
import App from './App.vue'
//to this
import App from '@/App.vue'
- Vue/cli give this capability to achieve this.
-
When creating new vue project at least for Nov, 2022. Vue ships with some extra directory like
router
, someconfigurations
and more. -
The last project
_testVite
we created didn't prompt us to install some of these shipped directories and configurations/base code. -
I realise using this command
npm create vite@latest
and thisnpm init vue@latest
brings different results. -
Now, we are going to create a new project using the command
npm init vue@latest
and then answer the questions of what packages we want to install. -
? Project name: >> _vueFirstProject
-
? Package name: >> _vuefirstproject
-
? Add TypeScript: >> No
-
? Add JSX Support: >> No
-
? Add Vue Router for Single Page Application development: >> Yes
-
? Add Pinia for state management: >> No
-
? Add Vitest for Unit Testing: >> No
-
? Add an End-to-End Testing Solution: >> No
-
? Add ESLint for code quality: >> Yes
-
? Add Prettier for code formatting: >> Yes
-
Now we have a porject to demo these 'little confusing things'.
-
cd vueFirstProject
and run the following commands:npm install npm run lint npm run dev
-
Now our project is ready!
-
We went from building basic Vue components to scaffolding a full single-page application with routing, configuration, aliases, and more. In this episode, let's review a handful of small things that I think you might initially find to be confusing.
alias (@)
-
Inside
vite.config.js
which looks like this:import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })
-
The
@
is configured to represent file url path of./src
directory. -
Which means we can go to our vue components and import our component using
@
intead./
//from import HelloWorld from './components/HelloWorld.vue' //to import HelloWorld from '@/components/HelloWorld.vue'
-
If you're using phpstorm, you will notice after using that symbol, phpstorm get confused and put red Squiggly lines somewhere in your code.
-
To slove this, we need to create our configuration file called
jsconfig.json
. Then create and object inside with compiler options porperty.{ "compilerOptions": { "paths": { "@/*" : ["./src/*"] //you want the left symbol side to resolve to the right side of the directoty } } }
-
Of course this is a kind od repetion of what is done inside
vite.config.js
butjsconfig.js
is what phpstorm understands at least for this time of writing and it will interpret. -
With vscode, it does also put red Squiggly lines somewhere in your code. Which we will solve later.
RouterLink
-
It allows you to navigate to other web pages with a tag without performing full page request as a traditional page requests does.
-
It kind use ajax api to request for a page dynamically.
<script setup> import { RouterLink, RouterView } from 'vue-router' import HelloWorld from '@/components/HelloWorld.vue' </script> <template> <header> <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="125" height="125" /> <div class="wrapper"> <HelloWorld msg="You did it!" /> <nav> <RouterLink to="/">Home</RouterLink> <RouterLink to="/about">About</RouterLink> </nav> </div> </header> <RouterView /> </template>
-
From the above code we have nav tag which has RouterLinks to different pages.
-
And then we have
<RouterView />
which renders the corresponding vue component that<RouterLink to="/about">About</RouterLink>
will request. -
Our router link (url) is
/about
. if we visit torouter
directory, we will see what is assigned to this url. Let's have a look'.import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'home', component: HomeView }, { path: '/about', name: 'about', // route level code-splitting // this generates a separate chunk (About.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import('../views/AboutView.vue') } ] }) export default router
-
We can see how the
urls
are asigned to various components. We are saying, if the url is/
the corresponding component isHomeView
. -
Use this
RouterLink
even when you're in a different vue component instead ofhref
. -
You just need to import in yout vue component:
import { RouterLink} from 'vue-router'
-
I must admit, I feel bad for what I'm about to do. You were becoming so comfortable, and I had to suddenly throw a wrench into the gears. In this lesson, let's mentally adjust to working with the Composition API and script setup.
-
There are two different ways to structure and organise your project in Vue.
-
Originally when Vue was launched, we only had
Options API
. -
As of Vue 3.0, they gave us
Composition API
. -
Composition API
comes withscript setup
which will compile down your code to required standard. -
One of the keys things in
script setup
, you do not have to return anything i.e data. All you need to do is to define it and it's magically available in the template.<script setup> import TheWelcome from '../components/TheWelcome.vue' import { ref } from 'vue' let message = ref("Hello World"); setTimeout(() => { message.value ="I have been changed !" }, 3000); </script> <template> <p> <input type="text" v-model="message" /> </p> </template>
-
We use the macro word
ref
which makes our value reactive to changes. -
As per the popular opinion by devs, they argued that there will be a tendency to forget use
.value
to access any of the reactive properties. -
Another option to solve this, is to use again a macro feature
reactivityTransform
- which is an experimental feature at the time of this writting. -
For experimental features, we have to turn them on inside
vite.config.js
file;export default defineConfig({ plugins: [vue({ reactivityTransform: true, //experimental feature. } )], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) } } })
-
Having done so, we don't need now to import
ref
in ourscript setup
and also, we do not need to get.value
from our reactive props. Instead to this:<script setup> import TheWelcome from '../components/TheWelcome.vue' let message = $ref("Hello World"); setTimeout(() => { message ="I have been changed !" }, 3000); </script>
- To understand the benefits of
script setup
, let's begin implementing code reuse and see how it goes. - Let's create notification alert on button click using
Options API
;
<script> export default { methods: { flash(message){ alert(message); } }, } </script> <template> <main> <button @click="flash('It Works!')">Click Me</button> </main> </template>
- To understand the benefits of
-
We can use
sweetalert
package to make our notification fancy. -
Let's cd to
vueFirstProject
directory -
npm install sweetalert --save-dev
-
Then import it inside
HomeView.vue
component.import swal from 'sweetalert'
- Then replace
alert
withswal
<script> import swal from 'sweetalert' export default { methods: { flash(message){ swal(message); } }, } </script>
-
If we want to take this kind of notifcation to
About Page
orContact Us Page
, we have to repeatedly copy and paste. And if you want to change the styling of the notification, you will have again to edit the same code in all pages. This does not make sense. We need to have things in sync. -
To solve this, we can use some
Vue
mechanism calledMixins
, which is a sort of liketraits
inPHP
orThreadTools
if you tookJava Multi-threading course
😊 -
Let's a new directory called
mixins
and fileflash.js
. -
When you are creating
mixins
, one nice thing is; the object you're exporting will be identical to all of your vue components and whatever you create inside your object, it will bemixed in
iniside your component. -
Iniside our
flash.js
file:import swal from 'sweetalert' export default { methods: { flash(message){ return swal('Success!',message, 'success'); } }, }
-
And now if we want to use this flash in any component, we will import it like this;
<script> import flash from '@/mixins/flash'; export default{ mixins: [flash], } </script>
-
This traditional, mixin-based approach works for use. But again we still have a problem. Imagine you want have several mixins being pulled in and you want to find where the flash message is defined. You will have to go through every mixin just find a message variable. Even worse, is when you will be having mixins from a library. This can be confusing. The more recommended approach is to use something know as
composables
. -
Let's create new directory
composables
and make a file called:useFlash.js
import swal from "sweetalert"; export function useFlash() { function flash(message){ return swal('Success!',message, 'success'); } //return whatever you want to expose to outside world return { flash }; }
-
Let's go to our
HomeView.vue
component and use this approach instead ofmixins
<script setup> import {useFlash} from "@/composables/useFlash"; let { flash } = useFlash(); </script>
-
Now we can easily modify from one single source of truth.
- Let's review another example of a composable. This time, we'll leverage localStorage and Vue reactivity to "remember" a form input's value - even if you refresh the page.
import { ref, watch } from 'vue'; export function useLocalStorage(key, val = null) { let storedVal = read(); if (storedVal) { val = ref(storedVal); } else { val = ref(val); write(); } watch(val, write, {deep : true}); //when you turn deep into true it can be costly in terms of performance function read() { return JSON.parse(localStorage.getItem(key)); } function write(){ if (val.value == '' || val.value == null) { localStorage.removeItem(key); } else { localStorage.setItem(key, JSON.stringify(val.value)); } } return val; }
- Now we can leverage this tool in our Vue components.
<script setup> import { ref } from 'vue'; import { useLocalStorage } from "@/composables/useLocalStorage"; let food = useLocalStorage('food'); let age = useLocalStorage('age'); let obj = useLocalStorage('obj', {one : 'one', two : 'two'} ); setTimeout(() => { obj.value.one = 1; obj.value.two = 2; }, 3000); </script> <template> <main> <p> What is your favorite food? <input type="text" v-model="food"/> </p> <p> What is yourage? <input type="text" v-model="age"/> </p> </main> </template>
-
Let's review a couple other Composition API gotchas in this episode. Using the example of an enhanced "tabbable" textarea, we'll learn how to use defineProps and defineEmits when using script setup.
-
Let's say you have a
textarea
and you want the user to be able to use tab key to move text within the the textarea. -
We create a function called
onTabPress(e)
which takes that event from the textarea.function onTabPress(e){ let textarea = e.target; let val = textarea.value, start = textarea.selectionStart, end = textarea.selectionEnd; textarea.value = val.substring(0, start) + "\t" + val.substring(end); textarea.selectionStart = textarea.selectionEnd = start +1; }
-
And the we listen keydown in our textarea element.
<textarea @keydown.tab.prevent="onTabPress" style="width: 300px; height:300px;">Hi there</textarea>
-
Note that, with vue you can specify the type of key you want to listen to, it can be
Enter
orTab
for our case. -
The we have have to prevent the default behaviour of the keydown.
-
Imagine if you would be having multiple textarea elements where you want the same behaviour. The best approach is to extract the above code to dedicated component.
-
Inside our
components
directory, lets make ourTabbableTextarea
component.<script setup> defineProps({ modelValue: String }); let emit = defineEmits(['update:modelValue']) function onTabPress(e){ let textarea = e.target; let val = textarea.value, start = textarea.selectionStart, end = textarea.selectionEnd; textarea.value = val.substring(0, start) + "\t" + val.substring(end); textarea.selectionStart = textarea.selectionEnd = start +1; } </script> <template> <textarea @keydown.tab.prevent="onTabPress" @keyup="emit('update:modelValue', $event.target.value);" v-text="modelValue" /> </template>
-
Let's next move on to a discussion about dependency injection, and what that might look like within the context of a Vue application. One issue you'll quickly run into is what we refer to as
"prop drilling."
Let's learn what it is, and how to fix it. -
Sometimes you run into awkward situations, where you have to pass a prop down in many levels. We refer thia as
prop drilling
-
In Vue 3, you can use the
provide
andinject
functions to share state and behavior between components in a hierarchical tree. This is known as dependency injection. -
From our example of
Quiz
, we are passing propsquiz
to child componentQuiz
from ourHomeview
parent component.<script setup> import Quiz from "@/components/Quiz/Quiz.vue"; </script> <template> <main> <Quiz :quiz = "{name: 'My first question ', questions:[]}"/> </main> </template>
-
Inside
Quiz
component, we are still passing it downQuizFooter
child component.<template> <div> <QuizHeader /> <QuizQuestion /> <QuizFooter :quiz = "quiz"/> </div> </template> <script setup> import QuizHeader from "@/components/Quiz/QuizHeader.vue"; import QuizFooter from '@/compenents/Quiz/QuizFooter.vue'; import QuizQuestion from '@/components/Quiz/QuizQuestion.vue'; defineProps({ quiz:Object }); </script>
-
And then still pass the quiz object to the
QuizFooterLinks
and that just a few.<template> <footer> <h4>Quiz Footer</h4> <QuizFooterLinks :quiz="quiz" /> </footer> </template> <script setup> import QuizFooterLinks from '@/components/Quiz/QuizFooterLinks.vue'; defineProps({ quiz: Object }); </script>
-
You may find yourself drillimg to more than 5 components. In other words, you are just forcing certain components to accept a prop for a sole purpose of passing it to nested component.
-
That's why we need to leaverage
provide
andinject
where it's needed only (must be to child component only). -
Note that provide and inject only work within the same parent-child tree. If you want to share state between unrelated components, you can use a global store like Vuex/Spinia now or a custom event bus.
-
We provide our value from the parent component and inject it to only components that needs it.
import { provide, ref } from "vue"; defineProps({ quiz:Object }); let name = ref('John Doe'); provide('name', name);
-
From the example above we are providing data but in a reactive way such that if the data is changed from child component where it's injected, it's immediately updated to the source of truth, i.e the provider.
-
Inside the receiver component, we inject the data like this:
import { inject } from 'vue'; defineProps({ quiz: Object }); let name = inject('name'); setTimeout(() => { name.value = 'New Name'; }, 3000); //and display it like this <template> <div> <h5>{{ quiz.name }}}</h5> <h5>{{ name.value }}}</h5> <ul> <li> <a href="#">Get a Job</a></li> <li> <a href="#">Contact Us</a></li> </ul> </div> </template>
-
The setTimeout() method is to test if the value of name is changed from child component can be updated to the parent. And it will since we provided it as a reactive data by using
ref()
=>let name = ref('John Doe')
-
In some other cases, the property my be updated and you don't know the specific component that is changing/updating data that you provide. If you want to take control of this, you can create a rule, that the only place you can change the data is in the parent component where it's being provided.
-
If we take this approach, we have to change how we provide data, we provide an object instead of a string. The object will contain the data and the equivalent of a mutator - which will be a function that will be responsible for changing the name/updating data.
provide('name', { name: name, changeName: () => name.value = 'Name Changed' });
-
Notice, now th logic for updating name, now exits within the parent. And if we wanna trigger the change, we simply pass a reference to this function to the child component.
import { inject } from 'vue'; defineProps({ quiz: Object }); //we accept the object properties for us to use them let {name, changeName } = inject('name'); //and change the name when we click this button <template> <button @click="changeName">{{ name.value }}}</button> </template>
-
We can agree that, this approach is cleaner compared to drilling down of props to all children components.
-
So far, we've reviewed two different ways to share state across a wide range of components. But we're not done yet! Let's review a simple example that will take you a long way. There's nothing keeping you from extracting data, or state, to a reusable external file.
-
In our
HomeView
component, we hard coded the initial state for quiz.<Quiz :quiz = "{name: 'My first question ', questions:[]}"/>
-
We can extract this and call it
state
//extracted state/data let state = "{name: 'My first question ', questions:[]}";
-
You can actually store this state/date within it's own file.
-
Within
src
directory, lets create a folder and call itstores
. Within here, we will have a file calledquizStore.js
. -
Then the grab the extracted state and put it inside
quizStore.js
file. -
Then you obviously want to make it accessible to the outside world. so you will have to export it.
//quizStore.js export let state = "{name: 'My first question ', questions:[]}";
-
Inside our
HomeView
component, we can import it with the name we called it.<script setup> import Quiz from "@/components/Quiz/Quiz.vue"; import {state} from "@/stores/quizStore.js"; </script> <template> <main> <Quiz :quiz = "state"/> </main> </template>
-
In our
Quiz
component, we can get rid ofprovide
andinject
stuff and import thestate
from it's file instead of accepting anyprops
. -
We can also get rid of passing quiz on down drill:
//from <QuizFooter :quiz = "quiz"/> //to <QuizFooter />
-
Inside
QuizFooterLinks
, we can get rid ofinject
entirely and import a state:<template> <div> <h5>{{ state.name }}}</h5> <ul> <li> <a href="#">Get a Job</a></li> <li> <a href="#">Contact Us</a></li> </ul> </div> </template> <script setup> import {state} from "@/stores/quizStore.js"; </script>
-
This is an alternative way of managing a
state
ordata
as we were doing in other approach ofprovide
andinject
, in a particularly, astate/data
that needs to be accessible globally or at least a wide range of components. -
These are situations where you need to access a wide range of components:
- You can have a state for the current user i.e
currentUserStore.js
- A shooping cart, when you have a shopping cart, you ned to access it's
state
across the entire page.
- You can have a state for the current user i.e
-
What we have now, is quite simple and it's not flexible. Let's see how...
-
In our
QuizFooterLinks
component. lets add a button and when clicked it changes the state.name or quiz name to a new given name.<template> <div> <h5>{{ state.name }}}</h5> <button @click="state.name = 'A New Quiz Name'">Change Quiz Name</button> <ul> <li> <a href="#">Get a Job</a></li> <li> <a href="#">Contact Us</a></li> </ul> </div> </template> <script setup> import {state} from "@/stores/quizStore.js"; </script>
-
If you run our project, you will notice that the name is being updated but not being reflected in
<h5>{{ state.name }}}</h5>
, and that's because, we just updated the string, and no point we specified that it should bereactive
-
We can to our store and make our
state/data
reactive.//quizStore.js import { reactive } from "vue"; export let state = reactive({ name : 'My first question ', questions:[] });
-
Notice we using keyword
reactive
instead ofref
. Well,reactive
is used when we are dealing with objects wherehasref
is good when we have variables. -
This will get job done in most cases but until you start building signficantly more complex applications that you might run into some roadblocks. And those roadblocka are like:
- This
state
is changing and i don't know why? - Or I need to hook in when the state changes and do some kind of operation like ajax query or update localstorage.
- This
-
It's those situations when you might need to reach for something a bit more flexible. And we shall see in the next episode.
-
Before we move on to a dedicated tool for managing globally accessible state (Pinia), let's first review some new terminology and discuss potential concerns related to directly mutating state.
- Action = Method
- State = data
-
Let's create a new page called
CounterView
which will show thecount
. -
We create a
state
of this counter in external filecounterStore.js
and call itcounter
import { reactive } from "vue"; export let counter = reactive({ //state count: 0, //action increment (){ if (this.count <=9) { this.count++; } } });
-
Then and import the counter inside
CounterView
component.<template> <div> <h1>{{ counter.count }}</h1> <button @click="counter.increment()">Incriment</button> </div> </template> <script setup> import { counter } from "@/stores/counterStore.js"; </script>
-
We have a button to increment our count, such that we do not increment/chnage/mutate the state of data directly from outside the it's 'store'. Rather we could like to have the logic of changing the state to be inside it's store and us is to call it.
-
This is what tool called
Pinia
give you and even more features. -
In the next episode we will learn how to manage state with
Pinia
- a dedicated tool fro dealing with global state management
-
I think we're finally ready to review dedicated tooling for managing global state. The go-to option these days is Pinia. Compared to our make-shift store from the previous episodes, Pinia will provide us with better devtool support, hot reloading, time traveling, and more!
-
We will use the counter store that we created and convert it to pinia.
-
First of all, installl
pinia
npm install pinia
-
Next, go to the entry point which is
main.js
and let's importpinia
.import { createPinia } from 'pinia';
-
And then register it as plugin
app.use(createPinia())
-
main.js
Entry file:import { createApp } from 'vue'; import { createPinia } from 'pinia'; import App from './App.vue'; import router from './router'; import './assets/main.css' const app = createApp(App) app.use(router) app.use(createPinia()) app.mount('#app')
-
Let's buold our counterStore newly with
pinia
import { defineStore } from 'pinia' export let usecounterStore = defineStore('counter', { //data state(){ return { count: 0, }; }, //actions actions: { increment(){ if (this.count < 10) { this.count ++; } } }, //computed getters : { remaining(){ return 10 - this.count; } } }); //inside CountView Component import { usecounterStore } from "@/stores/CounterStore.js"; let counter = usecounterStore();
-
The difference here from previsous manaul way is that we need to call the function
usecounterStore
inCounterStore.js
<template> <div> <h1>{{ counter.count }}</h1> <button @click="counter.increment()" :disabled="!counter.remaining" >Incriment ({{ counter.remaining }}) Remaining</button> </div> </template> <script setup> import { usecounterStore } from "@/stores/CounterStore.js"; let counter = usecounterStore(); </script>
-
From the above example, we have seen the structure of
pinia
in our store
- Now that you're little more comfortable to how you might go about in mantaining and organising the state. Let's work on a more realistic exercise in this chapter and all the pieces together.
- Let's prepare a dashboard for managing a team - perhaps for a SaaS. In this first episode, we'll review the design and then begin organizing the layout into a series of Vue components.
- As we have been creating this project, we have faced the first annoying thing of
prop drilling
down to child components. Nevertheless, the app is good and dynamic. - In the next episode we will be using a dedicated tool -
pinia
to manage state of team.
-
In this episode, we'll extract a dedicated Pinia store for managing the state for a team. We'll also discuss how you might go about seeding a store with initial date that is fetched asynchronously.
-
The first step is to create our store -
TeamStore.js
within stores directory. -
We can import it in
TeamView
component :import {useTeamStore} from "@/stores/TeamStore"; let team = useTeamStore();
-
Which means now we are not getting data directly from
team.js
file toTeamView
-
All data management will be done in our store file.
-
We then need to seed our store from database, or API call, or in our case from json file.
import { defineStore } from "pinia" export let useTeamStore = defineStore('team', { state(){ return { name: '', spots: 0, members: [] } }, actions: { fill(){ //fill the initial state from AJax call or any state import ('@/team.json').then(r => { let data = r.default; this.$patch({ name: data.name, spots: data.spots, members: data.members, }); }); } } });
-
You can make the fill method to be
async
actions: { async fill(){ //fill the initial state from AJax call or any state let r= await import('@/team.json'); this.$state = r.default; } }
-
One big benefit to this approach, is that we delay the importing of out
team.json
file until when it's needed. Take an example when you are browsing a website and you are visiting about page, home page etc...all this time your are not importingteam.json
, you old do so when the user visits Team page. -
Because now we have shared a global state for team, we can now remove props of team object and replace it by importing the store whenever we need it.
-
Now we are leavaraging a dedicated store tool
pinia
, which allows your to manipulate the state on a fly.import {useTeamStore} from "@/stores/TeamStore"; let team = useTeamStore(); team.fill(); setTimeout(() => { team.spots = 10; }, 2000);
-
We can also move logic calculations/computed to the store under getters. For example where we get remaining spots there are in a team.
-
One last thing is to review how we declare a
state
in our store. You may find it different in the documentation and it's the syntax for those using a typescript, which can still work here. I also find it awesome. Let's update it://from this: state(){ return { name: '', spots: 0, members: [] } }, //to this: state: () => ({ name: '', spots: 0, members: [] }),
-
Recommend this approach. Thank you.
- This chapter we will be focusing on something else.
- Before I show you how transitions work, let's first build a modal component with Vue. As you'll find, it's surprisingly easy to create and toggle.
- In our TeamView, we have a button to add a member but isn't working at the moment. Let's take a chance to create a modal and open modal when user clicks this button.
- Let's create a new vue component
Modal
- Once we design the our modal, we can define a boolean prop
show
which will toggle the display of the modal.<script setup> defineProps({ show: Boolean, }); </script> <template> <div v-if="show" class="modal-mask"> <div class="modal-container"> <header>Header Text</header> <div>Main Text</div> <footer>Footer</footer> </div> </div> </template>
- We then create a source of truth which can toggle :
true
/false
to show/hide the modal. - We have achived in creating the Modal and interact with it effectively. In the next episode, let's do some transitions.
- NB : We had some vscode squash lines for showing errors and we solved by adding
vetur.config.js
file with some rules. Her the links to the solution:Error: [vue/no-multiple-template-root] The template root requires exactly one element.eslint-plugin-vue Answer: https://vuejs.github.io/vetur/guide/setup.html#advanced https://stackoverflow.com/questions/64867504/vue-3-the-template-root-requires-exactly-one-element-eslint-plugin-vue https://stackoverflow.com/questions/65601841/problem-vue-no-multiple-template-root-occurs-how-do-i-fix-it
- The modal we created in the previous episode is looking decent now. The next step is to add a bit of animation for when it shows and hides. To allow for this, we can use Vue's
Transition
component. - We can wrap anything we want to transition within
Transition
component.<template> <Transition> <div v-if="show" class="modal-mask"> <div class="modal-container"> <div> <slot>default body</slot> </div> <footer class="modal-footer"> <slot name="footer"> <button @click="$emit('close')">Close</button> </slot> </footer> </div> </div> </Transition> </template>
- The next step is to instruct it how tom perform a transition.
<Transition enter-from-class="opacity-0 scale-125" enter-to-class="opacity-100 scale-100" enter-active-class="transition duration-300" leave-active-class="transition duration-200" leave-from-class="opacity-100 scale-100" leave-to-class="opacity-0 scale-125" >
- Second approach is to pass the name to the transition have
css
classes starting with the same name.<Transition name="modal" > </Transition> //css .modal-enter-active, .modal-leave-to{ transition: opacity 1s; } .modal-enter-from{ opacity: 0; } .modal-enter-to, .modal-leave-from{ opacity: 100; }
- If you've ever built a modal before, you're probably aware that it's generally considered a good practice to place it at the bottom of the document, just before the closing body tag. In this episode, I'll show you how to do just that by leveraging Vue's
Teleport
component. - Let's go to our
TeamView
component where we are referencing ourModal
, we also wrap it in withTeleport
<Teleport to="body">
<Modal :show="showModal" @close="showModal = false">
<template #default>
<p>Need to add a new member to your team?</p>
<form class="mt-6">
<div class="flex gap-2">
<input type="email" placeholder="Email Address..." class="flex-1">
<button>Add</button>
</div>
</form>
</template>
</Modal>
</Teleport>
- Just think of this like saying; anything within inside
Teleport
, i want you teleportto
somewhere. - In our cae we are telling
Teleport
to append everything to the bottom of the bodytag
- You can the project and inspect the element, you will notice that it's a direct child of a body tag.
- NB: Should never deeply nest your modal inside other components.
- This structure gives us flexibility to separate
Modal
to it's domain even with it's toggle button, - For example you can have
AddMemberModal
component and haveAddUserModal
component, each Modal will have it's own toggle button. i.eAdd Member
andAdd User
buttons respectiely. - Let's refractor this, by cutting off the modal from
TeamView
component to it's domain modalAddMemberModal
. - Whenever the button should display, we can reference the
Modal
, once you reference the Modal and it has it's own button, the button will be loaded by defau - And this is
Teleporting
!
- I hate to say it, but that's all she wrote. We're all wrapped up for this series. But, of course, there's still more to learn. In this final episode, we'll review some recommended follow-up resources and courses. Thanks for your effort!