Getting started with Vue 3 and CodeIgniter 4

Video thumbnail

Content Index

Before starting to develop our application in Vue.js, which will consume the REST API we have implemented in CodeIgniter 4, I'd like to talk a little about the technology we're going to use. As I mentioned before, we chose Vue.

You can opt for Vue, React, Angular, or any other frontend technology if you already master one of them. If you don't want to learn Vue, you can perfectly perform this integration layer with another framework or library. In short, what we'll do are requests using the Axios library (which I will explain later), and we will build certain HTML components or blocks for the listing, in addition to some buttons for the other actions like deleting and other functionalities. In essence, that's what it's about.

If you don't know anything about Vue, I have this express tutorial in which you can take the first steps with Vue.

Why Choose Vue.js?

I use Vue to create SPA (Single Page Application) Webs because, as I mentioned at the beginning, it's a very simple technology. It's a web technology; therefore, it fits perfectly with server frameworks like CodeIgniter, since even though Vue is a client-side web technology, it's still a web technology.

Vue is easy to understand if we compare it with other alternatives. Additionally, by using it, we learn a new technology, which is Vue.js. Obviously, Vue also has certain advantages compared to, for example, React or Angular.

Note: This, by the way, is a fragment from my book, which has a separate cost. I use it, like the CodeIgniter book, to present certain theoretical aspects or developments that we will be carrying out.

The Advantages of Vue and the SPA Concept

Basically, the advantage that Vue (or similar frameworks like React or Angular) gives us is the fact that we don't have to manipulate the DOM manually. By DOM, I mean the structure of the web page.

When we change a state or status, for example, when a user goes from not authenticated to authenticated, we usually add some more options to the menu or remove the login button. We don't have to perform any of those actions manually.

Beyond this, we can also build an SPA (Single Page Application) type Web. This means that, through data that we call reactive (which are tied to the interface, as we will see later), when we change this data programmatically (that is, through the Script), the changes are automatically made or reflected on the screen. That is, in essence, an SPA type Web.

Other frameworks like React or Angular can do the same, but Vue is much simpler to understand, lighter, and friendlier when starting with a new technology.

Components, Props, and Watchers

From here, we have many options, for example, the use of Props (properties), which allow us to modularize components.

The word component is something you're going to hear a lot, as Vue is component-based. Components are nothing more than small pieces of code that fulfill a specific functionality. For example:

  •     It can be a button, which we can use to execute certain actions, such as deleting.
  • It can be a modal directly.
  • It can be a table or a list.

We will be creating some as we progress, but remember this:

  •     A component is a small piece of code that can be easily reused.

A component can be:

  •     Generic, as I mentioned, directly a button, and then we implement the action depending on what we need.
  • Something a little more  specific, for example, a movie list that we can then place anywhere in the application.

Properties, Watchers, and Reactivity

We also have many features in Vue that, again, we will introduce little by little, such as the use of observers or watchers. These allow us to observe changes on reactive data, in this case, focused from the Script, not from the HTML. I'll explain a little later how a Vue component is formed.

The SPA type Web was what I mentioned about the HTML DOM (which is the entire page). When we change a reactive data (that is, a JavaScript variable), it automatically changes in the HTML. In this case, the observer we place here comes from the Script side.

You might not fully follow me now; it doesn't matter if you don't quite understand the concept yet. Just stick with the idea that Vue is a client-side JavaScript framework that allows us to create SPA type Webs. That is, webs where, in a simple and practically automatic way, we can change the state of multiple HTML elements. Everything depends on how we implement it as we change certain values of variables (a set of variables that we will also see how to define).

️ Requirements and Installation Options
Again, no prior knowledge of Vue is necessary; we are going to see everything from scratch. But in case you want to delve deeper, remember that I have a book associated with the technology, and I also have several videos on YouTube, a playlist to start developing in Vue.

To use Vue, we basically have two ways: through Node.js or through CDN (Content Delivery Network).

Basic Structure of a Vue Component

A component consists of three main elements:

The HTML: This element is usually always there, which is a small piece of HTML. As I mentioned before, in this case, it's an incremental button.

The Script: This is the logic part. In this case, it's not visible because it's a very simple structure, so its inclusion is a bit optional.

The Style part: This is quite optional, since the style is usually taken from a master sheet (a main sheet). However, you can also place it directly in the component.

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<div id="app">{{ message }}</div>
<script>
  const { createApp } = Vue
  createApp({
    data() {
      return {
        message: 'Hello Vue!'
      }
    }
  }).mount('#app')
</script>

⚙️ Application Startup

And this is the application startup; that is, we always have, in this case, a root part of the application where we start Vue.

Node.js and NPM: Key Elements

Remember that in the development environment we have two very important elements:

Node.js: Which is the JavaScript runtime environment itself.

NPM: Stands for Node Package Manager, that is, the Node package manager. With it, we can install packages, such as Vue in this case.

⚠️ Solution for the “Command Not Found” Error

If nothing appears, if it shows up as:

zsh: command not found:

Or something similar (like the command not found error), try restarting your computer. Just in case, here it appears like this (specifically the command not found), try restarting your computer, and that should be practically everything.

✅ Important Recommendation During Installation

Always be aware, when you are installing, if a check box appears indicating whether you want to add it to the System Path or something similar.

Check it, obviously, so you can then use it from the terminal!

Again: if in the terminal, or when—sorry—when you are installing, a check box appears (as in the case of Python) that indicates whether you want to add Node to the system Path (which I don't know if it asks), check it there, because it will be necessary. Otherwise, you won't be able to use it this way and you'll have to reinstall it.

Creating a new Vue project (npm create vue) 

Video thumbnail

Let's get started, and for that, remember we'll need Node to install our Vue.

Note on the CDN alternative: In case you don't want to use Node, have problems with it, or want to use the CDN and don't know how to start, you can check at the end of the section. If I haven't added anything related to this, you can ask me to include that development.

We start from what we did in the previous class: simply talking a little about what Vue is and why we are going to use it.

Advantages of Vue and Node

We are going to use the Node version with Vue because here we have the entire ecosystem at our disposal, and that is the one we will usually employ when we want to do any development in Vue.

The small disadvantage we have is that we cannot link Vue directly with the CodeIgniter project. That is, we will have:

A project in CodeIgniter 4 (which is this one).

And, independently, the application in Vue.

For example, in Laravel, when we create a project, we automatically have integration with Node, and with this, we can install our beloved Vue along with Laravel in the same project.

But in CodeIgniter, we cannot do this type of implementation, since it does not include or bring the Node ecosystem by default.

Independent Projects

So we have to do it in a separate project. Although, usually, this is what is done. What interests us about CodeIgniter is the API we built previously, and we want to consume it from here, it's that simple.

Usually, this is done from separate projects. In this case, we are doing a demonstration to consume the REST API using a Vue application, but it could be a React application, Angular, or a mobile application (in Flutter, native Android, etc.). At that point, obviously, it won't be the same project.

It is important to know that projects are usually managed separately.

Vue Project Creation

Here, the first thing you have to do is position yourself in a place where you want to create your project and execute the following:

$ npm create vue@latest

The Role of NPM (Node Package Manager)

It is important that to install a dependency like Vue, we first need a project. It's the same as what we did with CodeIgniter 4: if we want to install a package in PHP, remember that it is installed using Composer.

Fact: npm (the Node package manager) is the equivalent of Composer, but in this case, Composer is for PHP, and the Node package manager is for JavaScript.

Dependency Generation

Once the project is generated, if we do not have the node_modules folder (which is where all the project dependencies are located), we must generate it. For this, we execute the following at the root of the Vue project:

$ npm install

Vue project structure

Video thumbnail

We won't be overly theoretical; we'll focus on the essential files that make up the application. Remember that the official documentation should always be your main source for deeper understanding.

️ Non-Essential Files and Folders

  • .vscode: This folder is specific to Visual Studio Code and contains base configurations for the editor (syntax, error highlighting, etc.). It's not crucial for the application's logic.
  • .gitignore: Contains the files that Git should ignore (like the Node modules folder). This is standard for setting up a repository.
  • README.md: Informational file for the repository.

public/
The public folder is the public access folder. It contains the output files of the final build and the static files of the application.

Files such as the favicon, images, or videos that are part of the Vue application should go here.

  • index.html (The Startup File)
    This is the startup file. Vue is a web technology, and the browser only understands HTML, CSS, and JavaScript. This is the output page of our application.

Key Point: Look at the div with the identifier id="app". This is where our Vue application is mounted. Everything we develop in the src/ folder (our components and the framework structure) is ultimately injected and displayed inside this div.

  • package.json
    This file contains the versions of the packages we are using.
  • Here you will see the Vue version and other dependencies (like Vite and its plugins).

Vite: The Transpiler and Server

Vite is a frontend tool that allows us to build our application.

As we mentioned, the browser will not understand the structure of the src/ folder (with .vue files and complex logic) by default.

What does Vite do?

Transpilation (Compilation): Vite allows us to transpile (or compile) our files. Transpiling differs from compiling in that, although we go from a source code to a machine or equivalent code, in web development the term transpiling is used because we transform one type of JavaScript/syntax into another JavaScript/HTML/CSS that the browser *does* understand (we are not moving to a totally different language, like C to an executable).

Serving the Application: On the other hand, Vite allows us to serve our application to view it on a development server.

In summary: Vite is used to mount our application and to convert (transpile) everything we generate in the src/ folder into something the browser can interpret.

Flexibility: Vite doesn't only work for Vue; you can also use it for React, Angular, and each one has its specific package.

  • ⚙️ vite.config.js (Vite Configuration)
    This file is for configuring Vite. You don't need to "tweak" much at the beginning, but important configurations are made here:   

    • The installed plugins are referenced (the main one being the Vue plugin).    

       

    • Configurations are established for relative imports, which we will see when components are created.    
  • ️ The src/ Folder (Source)
    This is where we will place our application as such:   

    • CSS files: For styles.    

       

    • components/ folder: This is where all the reusable pieces of code (our Vue components) will go.    
  • ▶️ Project Execution: Development Commands
    Just as we do with CodeIgniter 4 with the `spark serve` command, we need to raise the Vue application to be able to view and consume it.

This is done by consulting the scripts section of the package.json file. By default, some commands are set up:

How to Execute the `dev` Command
To execute any of these script commands, we use the Node command `npm run`.

$ npm run dev

Explanation:

  • npm: The Node package manager.
  • run: Indicates that we are going to execute a script defined in `package.json`.
  • dev: Is the abbreviation for the complete Vite command that is defined in the script.

❌ The Common Error

When executing `npm run dev` for the first time without the modules folder, you will get an error that a very important folder is missing: the node_modules folder (where all the Node packages are, including Vue and Vite).

Solution: You must execute the command `$ npm install` at the root of the project to download the dependencies and generate that folder, as we saw previously.

Node modules folder

We left off at the point where the project couldn't start. This happens because the Node modules folder (`node_modules`) is missing, which is the equivalent of the vendor folder in CodeIgniter or PHP projects: all the project's dependencies are located within it. For some reason, the installation didn't generate it.

There are several ways to create Vue projects (using the CLI, or starting from a Node project, etc.).

To solve this, we are going to execute the Node installation command:

npm install

We wait a moment for the process to finish. This will take a while.

Note on `node_modules`
Important point: Don't be scared by the size of the node_modules folder, as we won't use it when we move to production. This folder is only for development purposes.

Inside `node_modules`, you will find many intermediate packages that will allow us to develop in Vue. Remember that, when we move to production, we will execute the build command (`npm run build`), which would generate the output files in the dist folder here that we would have to copy and paste.

package-lock.json and Version Control

Notice the number of packages that were added. Just like with CodeIgniter or PHP, each of these packages has dependencies.

Now, another very important file should appear: package-lock.json. This file contains the specific versions being used, which wasn't there before. Dependencies have dependencies, and this file is essential.

While the package.json contains the versions that are, so to speak, general, notice that this caret (^) indicates, for example: "install Vite in version 6.0.5 or any compatible superior version."

"vite": “^6.0.5

▶️ Development Command and Watcher

The dev command is located in the `package.json` file under the script: `"dev": "vite"`. Here you will be able to see the commands for development and production.

When executing the command (notice that this time it worked), besides starting the server, it will also generate a watch (or an observer) here. This means that every time we make a change to the source code of our Vue application, it will automatically reload or should reload.

Executing the Development Server
To start the application in development mode, we use:

npm run dev

⚙️ The Vue Startup File: main.js

This is the startup file: main.js.

import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.config.globalProperties.$axios = axios
window.axios = axios
app.mount('#app')

Notice that we are importing `createApp` from `vue`. It is a named import (`{ createApp }`), meaning if you change the name here, it will fail, as it won't be found.

Recommendation: I recommend pressing F12 in your browser to see client-side errors. It will clearly indicate if an element has not been found.

The Parent/Root Component (App) and its Mounting

In `main.js` we have: `app.mount('#app')`.

We are mounting the main component (the parent/root component), which is called App (the output component). We are mounting it in a div with the identifier #app, which is exactly the one we had here in the `index.html` page:

<div id="app"></div>

If you change the DIV or the identifier in `index.html` without updating it in `main.js`, Vue will not find where to mount the application and it will not work.

A Little More About Components in Vue

Video thumbnail

Components are the key piece in all development with Vue, and we will spend most of our time creating and interacting with them.

A Vue component is nothing more than a file with the .vue extension that can be:

  • From a simple reusable or non-reusable button.
  • Up to a complete page.

The components that the newly created application brings by default are an excellent example of this. For example, the file `src\App.vue` represents the main page, but internally it uses other reusable components like `TheWelcome.vue` and `HelloWorld.vue`.

<script setup>
import HelloWorld from '@/components/HelloWorld.vue'
// ...
<TheWelcome />

Component Elements

Let's remember that components have three main pieces:

  • HTML (`template`): The structure section, usually always present.
  • JavaScript (`script`): The logic section (script).
  • CSS (`style`): The style section (optional).

CSS with Scope (Scoped CSS)

The CSS block is usually not defined if a global style is being managed. A specific CSS is only defined if something very concrete needs to be applied to a component.

The `scoped` attribute in the `style` tag is crucial:

<style scoped>
.item {
margin-top: 2rem;
display: flex;
position: relative;
}
</style>

The scope means that the applied style does not escape from this component. This prevents the style from clashing with the style of another component, since, although we have this beautiful structure, everything is injected into the same output page.

Composition Modes: Options API vs. Composition API

There are several ways to work with Vue to create the logic of our components: the Options API and the Composition API.

I won't delve too deeply, but it's important that you keep them in mind:

1. Options API

It is the traditional syntax, in which an `export default` is used and options (e.g., `created()`, `data()`, `methods`) are defined in separate blocks:

<script>
export default {
   created() {
       // ...
   },
   // ...
}
</script>

2. Composition API

It is a more modern and expressive way, which uses the `script setup` syntax. All the code is placed at the same level, making the reading and reuse of logic easier (the `setup` function).

<script setup>
import WelcomeItem from './WelcomeItem.vue'
// ...
const openReadmeInEditor = () => fetch('/__open-in-editor?file=README.md')
</script>

Reactive Variables (ref)

Within the Composition API, reactive variables are defined simply, for example, using the `ref()` function:

const count = ref(0)
console.log(count.value) // 0

The use of `ref()` means that the variable will be reactive and, therefore, every time we make a change to its value (`count.value = 1`), this change will be automatically detected and applied in the interface where the variable is being used.

️ Relative Imports with @ in Vue

When importing components, we often use relative paths:

import WelcomeItem from './WelcomeItem.vue' // Nesting problem

The problem with this syntax is that, in a large application, components can be nested in many folders (`pages`, `helpers`, `users`, etc.). This creates a navigation mess (`../../...`) when trying to import files.

To solve this, we use relative imports with the @ symbol:

import HelloWorld from ‘@/components/HelloWorld.vue’

This @ (referenced in the Vite configuration) is a relative import to the src/ folder. No matter where the component you are editing is located, it will always point to the root of your source code (`src/`), making importing easier.

Component Reuse and Slots

The interesting thing about components is reusability (components of components). For example, `App.vue` imports `TheWelcome.vue`, and `TheWelcome.vue` in turn imports and uses `WelcomeItem.vue`.

Using Slots

We have two ways to use components:

Referencing them dryly: You simply import the content of the component and that's it.

Using Slots (Custom Content): The component is defined as a kind of wrapper for the content that will be placed inside.

The use of slots allows you to respect the structure defined in the parent component, but customizing certain key parts of the dynamic content:

<WelcomeItem>
   <template #icon>
     <DocumentationIcon />
   </template>
   <template #heading>Documentation</template>
   
   Vue’s <a href="...">official documentation</a>...
</WelcomeItem>

Named Slots (`#icon`, `#heading`): They are defined to indicate specific parts where content can be injected (e.g., where the title goes, where an icon goes).

Default Slot: This is the content that doesn't have a specific name and is placed directly between the opening and closing tags of the component.

In the example above, the `WelcomeItem` component defines a nice HTML and CSS structure, and you define what content goes in the icon, what content goes in the header (heading), and the main content.

This is very similar to what we did with templates in CodeIgniter 4, but here the technical term is slot.

Our First Fetch

Video thumbnail

The next step is to try to consume our application. This listing that we have here is consumed via GET —spoiler alert— it's not going to work. We have to configure something additional, but we'll see that in the next class. Even so, it's important to do it to see what happens, and then I'll explain.

So, first we have to start the application. For that, we use:

npm run dev

We can actually execute several commands here, but it's not relevant now, as that's a Node thing. I don't want to complicate things too much, but usually when we use this command, it's because we want to execute one of those defined in `package.json`. Of course, these can be customized according to the tool, but in our case, it's not necessary. We simply use `dev`, as it's the command that is already configured.

Default and Named Imports

Let's review a bit about exports in JavaScript. Here we have a named export:

import { createApp } from 'vue'

When we use curly braces (`{}`), it means we are importing a module by name. You might not clearly see where this export is, as it may be "hidden" within some other module.

And this, on the other hand, is a default export:

import ListComponent from '@/components/CRUD/Movies/ListComponent.vue';

When you see that there are no curly braces, it is a default export. In this course, we will mainly use default exports, because that's how components work in Vue.

Components are default exports

HTTP Requests with fetch and Promises

To make HTTP requests, we have the fetch API, which is available in vanilla JS:

console.log(fetch('http://code4movies.test/api/pelicula'))

We will see that it prints ``, as I mentioned before. Additionally, we will see an error like this:

Access to fetch at 'http://code4movies.test/api/pelicula' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

So, you can already see why it doesn't work. Notice that `fetch` returns a Promise. We can resolve it with `.then()`:

fetch('http://code4movies.test/api/pelicula')
.then(res => res.json())
.then(res => console.log(res))

Whatever those languages implement, in this case, JavaScript, and that's the reason for this syntax. You can put it with parentheses if there are many parameters, or without—I mean, you can put it with parentheses if there are several parameters, or you can skip them if it is only one parameter:

param => expression
(param) => expression
(param1, paramN) => expression

CORS Error in CodeIgniter 4

In CodeIgniter 4, by default, it does not allow external applications, such as the Vue application, to connect. When making the previous fetch, we will see:

Access to fetch at 'http://code4movies.test/api/pelicula' from origin 'http://localhost:5173' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

Understanding the CORS Error

We are currently running into an error, as you can see. However, here it is no longer clear what is happening: the response supposedly should be there, but it returns nothing. It is evident that a problem occurred, and the browser clearly indicates what is happening.

You can easily search for these types of errors on the internet (copying and pasting the message helps a lot), but in essence, what is happening here is an issue with CORS (Cross-Origin Resource Sharing).

What is Cross-Origin?

Although it sounds complicated, basically what we are trying to do is exchange resources (data, information) between applications that are on different domains (origins).

Example: Your Vue application is running on `localhost:5173` and is trying to consume resources from `code4movies.test`, which is another domain.

This is where the browser's and server's security policy comes into play.

CodeIgniter 4's Security Policy

By default, CodeIgniter 4 does not allow this type of external connection.

Why does this restriction exist?
The restriction exists to protect you, as it would be a huge risk to allow any application (developed by anyone) to freely make requests to our API without any kind of authorization.

You can even make requests from anywhere in the browser to any route of the application, including the root. If there were no restrictions, any malicious site could easily exploit that.

Therefore, these policies exist to protect the API. From the perspective of CodeIgniter 4, your Vue application is seen as a possible attacker who is trying to consume resources without permission. Therefore, the API is protected and blocks the request.

To solve this error and allow Vue to consume the API, we need to configure CORS in CodeIgniter 4.

Testing with Fetch

Video thumbnail

To understand better, let's remember that with fetch we can make an HTTP request to an API. By its very definition, being an external request to the application (to another server), it is asynchronous.

This means that the response:

May take milliseconds or seconds.

May simply not resolve (due to network problems, API downtime, etc.).

Function Chaining (.then())

An important point is that we are performing chained functions using the `.then()` method.

fetch('http://code4movies.test/api/pelicula')
.then(res => res.json())
.then(res => console.log(res)) 
fetch(...): The HTTP request is executed. This returns a Promise that, when resolved, returns the generic response object (Response).
  • First .then(): This function is executed after the request. Its input (`res`) is the generic response object. This is where we call `res.json()` to cast and convert the API response to JSON format (the format we are using to share the Data). This, in turn, returns a new Promise.
  • Second .then(): The input of this function is the return from the previous step (the already converted JSON). Here we already have the data ready and we can print it or process it (`console.log(res)`).
  • Important: The request and the calls to `.then()` are chained. If you comment out the conversion to JSON, the next function would receive the generic response object, which is not useful for working with the data. If you remove the `.then()`, you only make the request and do nothing with the result.

Error Handling (.catch())

It is crucial to handle cases where the request does not resolve correctly.

To handle when the response is not correct (a network error, server failure, etc.), we use the `.catch()` method:

`fetch('http://code4movies.test/api/pelicula')`
`.then(res => res.json())`
`.then(res => console.log(res))`
`.catch(error => console.log(error))`

The `.catch(error => console.log(error))` block executes if any of the Promises in the chain (`fetch` or `res.json()`) are rejected, allowing us to handle the failure in a controlled manner and print the error.

Introduction to Axios: Fetch Replacement

Video thumbnail

We're going to learn about the library I mentioned two or three classes ago: Axios, which functions as a replacement for fetch.

As I mentioned, you can search for comparisons like "Axios versus Fetch" for more information. However, in my opinion, Axios has two clear advantages:

  1. More expressive syntax: Although it's a minimal difference, it's simpler, minimalist, and easier to understand and follow.
  2. Better support: Although `fetch` is supported by most browsers, it's important to verify it. You can search for "fetch support" on Google and you'll see a table, usually from Mozilla, that clearly shows which versions it is available in. In general, support is quite broad, except in very old versions, so it shouldn't be a problem.

Installing Axios in a Vue project

We are going to install Axios, since we can do it perfectly in our project.

For that, we use the following command:

npm install axios

This is common when working with the Node ecosystem. Remember that we are using Vue, and npm is the Node package manager.

When we install Node, we get two tools:

`node`: the JavaScript engine.

`npm`: Node Package Manager, the package manager.

You can also use `npm i axios`, which is the abbreviated version. With this, Axios will be installed in your Vue project. I recommend doing it with the server off just in case, although it normally shouldn't cause problems if it's running.

An advantage of using Node and npm instead of a CDN is that we don't need to download the JS file, manually copy it to the project, configure it, or link it in various places. It's much cleaner and more professional.

Global Configuration of Axios

Once installed, we must configure it globally to be able to use it throughout the application. We will do this configuration in the Vue startup file, that is, in main.js.

In that file, as we already saw, the Vue instance is created and the parent component (`App.vue`), where all other components are loaded, is defined.

Package Import

First, we import Axios. I like to keep third-party package imports separate, so we'll do it like this:

import axios from 'axios';

Note that this is a default import, which is why we don't use curly braces `{}`.

Creation of the Vue Instance

Next, we divide the operation of creating and mounting the application into two steps:

const app = createApp(App);
app.mount('#app');

This allows us to make additional configurations before mounting the app.

In some cases, the editor might issue a warning if we import something we haven't used yet. If that's your case, you can temporarily comment out the line for testing.

Global Assignment of Axios

Now we configure Axios as a global property. This is done as follows:

app.config.globalProperties.$axios = axios;

The $ prefix is optional, but in Vue, it's used by convention to indicate that it's a special or internal property of the framework. This helps avoid conflicts with other variables, for example, if you define something called `axios` later within a component.

In any case, if you don't like the `$`, you can omit it, although following the convention is recommended.

Configuration Verification

To ensure that everything is working, we can perform a test in the browser console. First, remember that Axios is now available throughout the application via `this.$axios` inside components.

If you want to access Axios from the global console, you can assign it to the `window` object like this:

window.axios = axios;

With this, you will be able to access it directly in the browser console:

console.log(window.axios);

If you don't assign it to the `window` object, the browser will not recognize it and will show undefined.

Sending Data in Axios Requests

Video thumbnail

Axios simplifies how data is sent to the server, especially by differentiating between GET and POST requests.

1. GET Type Requests

Remember that in GET requests, data travels through the URL as parameters (query parameters). You have two options for sending it:

Option 1: Directly in the URL (Manual)

You can place the parameters directly in the URL.

axios.get('http://code4movies.test/api/pelicula?parameterGet=1')
Option 2: Using the params Object (Recommended)

This is the recommended way, as Axios takes care of formatting the URL correctly. An options object is passed as the second parameter of the `get` method.

axios.get('/user', {
   params: {
     id: 12345 // This is converted to: /user?id=12345
   }
 }

The `params` object within the options is the equivalent of placing the parameters directly in the URL.

2. POST Type Requests (Data in the Body)

For POST, PUT, or PATCH requests, the data travels in the body of the request.

In this case, the route is still the first parameter, and the second parameter is directly the object containing the data to be sent:

axios.post('/user', {
   data1: 'Value1', // This is sent in the BODY of the request
   data2: 'Value2'
 })

Remember that in the case of POST, this data travels via the body.

Error Capturing with .catch()

Just like with `fetch`, requests with Axios are asynchronous (they are Promises), so they can fail due to server, client, or connection errors.

Axios uses the same Promise structure (`.then()` and `.catch()`) to handle the response and possible errors:

axios.post('/user', {
   data1: 'Value1',
   data2: 'Value2'
 })
 .then(function (response) {
   // Executes if the request was successful (2xx code)
   console.log(response); 
 })
 .catch(function (error) {
   // Executes if an error occurred (network failure or server error)
   console.log(error); 
 })

Key Differences with fetch

The main advantage of using Axios over `fetch` is that:

No need to cast to JSON: Axios automatically deserializes the response (if it is JSON) into a JavaScript object, so you don't need the intermediate `res.json()` step.

Method Handling: You can define the types of methods (`.get()`, `.post()`, `.put()`) in a cleaner and simpler way.

Listing Component

Video thumbnail

You have created the following modular structure:

  • Path: `src/components/CRUD/Movies/ListComponent.vue`

The component already has the three main blocks defined: template, script (using the Options API with `export default`), and style.

<template>
   <div class="list-container">
       <h2>Movie Listing</h2>
       </div>
</template>
<script>
export default {
   name: 'ListComponent', // It's good practice to give the component a name
   
   data() {
       return {
           // We define the reactive variable 'movies' as an empty array.
           // Data fetched from the API will be stored here.
           movies: [] 
       }
   },
   // Methods, 'created', etc., will go here.
}
</script>
<style scoped>
/* 'scoped' ensures these styles only apply to this component */
</style>

The data() Block (Options API)

In the Options API you are using (`export default`), the `data()` block is a function that must return an object. This object contains all the state variables (or reactive variables) of the component.

Purpose: The variable `movies: []` that you have defined will initialize an empty array. This array will be reactive, which means that when we get the data from the API and assign it to `this.movies`, Vue will automatically detect the change and update the section of the template that is using that variable.

The next step would be to use the `created()` or `mounted()` hook to execute the API request and populate this array with data.

Component Integration in App.vue

The `src/App.vue` file acts as the root component of your application, and it is the ideal place to import and mount other main components, like your `ListComponent`.

1. Cleanup and Import

Before importing, it is common to clean up App.vue, removing content or styles that you will not use. Then, we import the component:

<script setup>
// Import ListComponent
import ListComponent from '@/components/CRUD/Movies/ListComponent.vue'</script>

Simplified Path: We use the @ symbol to indicate a direct access to the project's `src/` folder. This avoids long, relative paths.

Mandatory Extension: In recent versions of Vue (using `script setup` and Vite), the `.vue` extension is mandatory when importing components.

2. Usage and Display in the Template

Once imported, you can use the component in the <template> section using its name (just as you imported it):

<template>
 <h1>Hello, component ready</h1> 
 
 <ListComponent />
</template>

By using the <ListComponent /> tag, Vue injects the HTML defined in `ListComponent.vue`'s template inside the `App.vue` template. If you temporarily placed the <h1>Hello, component ready</h1> inside `ListComponent.vue` and saw it on the screen, the integration was successful.

Reusability: One of the advantages of components is that you can use them as many times as necessary. Although a main listing is used once, you can insert <ListComponent /> multiple times if the logic allows it.

PascalCase (Recommended)    `<ListComponent />`    Uses upper and lower case; it's the standard way to name files and classes in JS.
kebab-case    `<list-component />`    Uses lowercase with hyphens. It's also valid, as the browser is case-insensitive.
Full Closing    `<list-component></list-component>`    Necessary only if you are going to use slots to inject content.

Lifecycle in Vue.js

Video thumbnail

We have to introduce the highly complicated (and boring) lifecycle of a Vue application. You can also search for it in English as "Vue lifecycle." Here is a page that explains it:

https://vuejs.org/guide/essentials/lifecycle

The chart might look like a nightmare, but ultimately what you have to understand is the following: when the Vue application is created (what they call `new Vue`, which is basically what we are doing here), many things happen internally. But at some point, certain lifecycle methods are called, which are the ones we are interested in.

Lifecycle Methods

The main methods we should know are:

  • `beforeCreate`
  • `created`
  • `beforeMount`
  • `mounted`
  • `beforeUpdate`
  • `updated`
  • `beforeUnmount`
  • `unmounted`

I won't dwell too much on this, but basically, when we switch from one page to another—for example, from a listing page (like in CodeIgniter 4) to a detail page—the new component is mounted and the previous one is unmounted. That's where `mounted` and `unmounted` come into play.

  • When the detail component is mounted, `mounted` is executed.
  • When the listing component is unmounted, `unmounted` is executed.

The most used in practice are:

  • `created`: when you need to prepare the data.
  • `mounted`: when you need to interact with the DOM.

You can use either of the two, but since in this case we are only interested in loading the data at the beginning, we will use created.

<template>
    {{ this.movies }}
</template>
<script>
export default {
    created() {
        axios.get('http://code4movies.test/api/pelicula')
            .then(res => this.movies = res.data)
            .catch(error => console.log(error))
    },
    data() {
        return {
            movies: []
        }
    },
}
</script>

***

Vue Reactivity and Function Usage

Excellent! Modularizing the code with methods, even in small applications, is great practice for keeping the code clean and better understanding Vue's Options API.

Here is the structured explanation of methods and a practical demonstration of reactivity.

️ Modularizing with Methods in the Options API

To keep the application logic organized, we define the API call inside a method in the `ListComponent.vue` component.

1. Block Structure

In the Options API, the component is organized into functional blocks (the order doesn't matter, but convention suggests `created`, `data`, `methods`):

2. Creation and Usage of the `getMovies()` Method

We define the `getMovies()` function inside the `methods` block and call it from `created()`.

export default {
   created() {
       // We execute the method that contains the request logic
       this.getMovies(); 
   },
   data() {
       return {
           movies: [],
           confirmDeleteActive: false,
           deleteMovieRow: ''
       }
   },
   methods: {
       async getMovies() { // Declared as async to use await
           try {
               // We use this to access variables (e.g., this.movies)
               const res = await axios.get('http://code4movies.test/api/pelicula');
               this.movies = res.data; 
           } catch (error) {
               console.log(error);
           }
       }
   }
}

Important Notes:
Usage of this: To access any property (like `this.movies`) or method (like `this.getMovies()`) defined within the Options API blocks, it is mandatory to use the `this` keyword. Without `this`, the script will not recognize the function and will throw an error.

async/await: If you wish to use the `await` syntax to wait for the Axios response, the method must be declared as `async`.

✨ Reactivity in Vue: The Practical Demonstration

Reactivity is the "magic" of Vue and one of its greatest advantages.

1. Reactivity Mechanism

Vue automatically detects changes in properties declared within the `data()` block (like `movies`). Any part of the interface (`template`) that is using that variable will update without you having to manually manipulate the DOM.

Demonstration:
The `setTimeout` example perfectly illustrates this:

setTimeout(() => {
// After 5 seconds, the view automatically updates:
this.movies = [/* new data */]; 
}, 5000);

When transitioning from an empty array to one filled with records, Vue takes care of changing the view for you. There is no need to use `document.getElementById` or `querySelector`, as was done with traditional libraries like jQuery.

2. Difference between data() and ref

Options API (`data()`): When working with the Options API (using `export default`), everything declared within the `data()` function is assumed to be reactive by Vue. Therefore, you do not need to use the `ref()` function.

Composition API (`ref`): The keyword `ref` is only used in the Composition API (using `script setup`) to explicitly declare that a variable must be reactive.

SaveComponent.vue: Edit: Sending Data to the Server 

Video thumbnail

The next step here would be to make the PUT request. We return and execute the POST, and if not, it enters here (in case you don't see it clearly):

src\router.js

{
   name: 'save',
   path:'/save/:id?',
   component: Save
}
async mounted() {
   if (this.$route.params.id) {
       await this.getMovie()
       this.init()
   }
   this.getCategories()
},

As you can see, the key logic in the script is to verify the presence of an identifier to determine the action to execute:

  • It checks for the ID in the route or the object (`post` or `movie` in this case).
  • If the ID is defined (i.e., it already exists), then an update operation is performed (usually with the PUT or PATCH method).
  • If the ID is not defined, then the creation of a new record proceeds (usually with the POST method).
async getMovie() {
   this.movie = await axios.get('http://code4movies.test/api/pelicula/' + this.$route.params.id)
       .then(res => res.data)
       .catch(error => error)
},

Configure Oruga UI in Vue 3

Video thumbnail

Oruga is a lightweight UI component library for Vue.js with no CSS dependency. Since it does not rely on any specific style or CSS framework (like Bootstrap, Bulma, TailwindCSS, etc.) it does not provide any grid system or CSS utilities; it only offers a set of components that are easy to customize with your own stylesheets using a CSS framework, custom styling, or the optional Oruga UI style.

We already talked about how to create a project in Vue 3 in Laravel, now, let's integrate Oruga UI:

We install Oruga in the Vue project with:

npm install @oruga-ui/oruga-next --save

We configure `main.js`:

import { createApp } from "vue";
import Oruga from '@oruga-ui/oruga-next'
import '@oruga-ui/oruga-next/dist/oruga.css'
import '@oruga-ui/oruga-next/dist/oruga-full.css'
import App from "./App.vue"
const app = createApp(App).use(Oruga)
app.mount("#app")

In the previous step, let's remember that a Vue 3 project looks like this:

import { createApp } from "vue";
import App from "./App.vue"
app.mount("#app")

And all we do is include the Oruga components:

import Oruga from '@oruga-ui/oruga-next'

A minimum Oruga style:

import '@oruga-ui/oruga-next/dist/oruga.css'

And the optional Oruga style:

import '@oruga-ui/oruga-next/dist/oruga-full.css'

Then, we install the plugin in Vue:

createApp(App).use(Oruga)

Delete item, reload list array.splice in Vue

Video thumbnail

If we delete a record, where we simply call the REST API:

remove(movie){
   axios.delete('http://code4movies.test/api/pelicula/' + movie.row.id)
       .then(res => res.data)
       .catch(error => error)
},

The table list does not reload:

<o-table :data="movies" :bordered="true" :striped="true" :hoverable="true" :selectable="true">
    <o-table-column field="id" label="ID" v-slot="m" sortable>
        {{ m.row.id }}
    </o-table-column>
    <o-table-column field="titulo" label="Title" v-slot="m" sortable>
        {{ m.row.titulo }}
    </o-table-column>
    <o-table-column label="Actions" v-slot="m">
        <o-button @click="$router.push({ name: 'movie.save', params: { id: m.row.id } })"
            icon-left="pencil">Edit</o-button>
        <div class="inline ms-3">
            <o-button variant="danger" size="small" @click="confirmDeleteActive = true; deleteMovieRow = m"
                icon-left="delete">Delete</o-button>
        </div>
    </o-table-column>
</o-table>

This is because we are not reloading the array called `movies`.

Removing Elements from Arrays in JavaScript

Vue cannot automatically detect the following changes when they are made directly on a reactive array property:

  1. Setting an element directly by index:
    1. Problematic example: `this.myArray[indexOfItem] = newValue`
  2. Modifying the array length:
    1. Problematic example: `this.myArray.length = newLength`

If you perform these operations, the array will change, but Vue will not update the template, breaking reactivity.

  • `pop()`
  • `shift()`
  • `splice`
  • Direct assignments
  • `push()`

So, besides calling the REST API, we must remove the element from the array. To do this:

remove(movie){
    axios.delete('http://code4movies.test/api/pelicula/' + movie.row.id)
        .then(res => res.data)
        .catch(error => error)
    this.movies.splice(movie.index,1)
},

This is one way to remove an element using its index, which is exactly what we need. For this, the native JavaScript method `splice()` is used.

The `splice()` method allows you to change the contents of an array by removing or replacing existing elements.

***

ListComponent.vue: Confirmation Dialog when Deleting in Vue

Video thumbnail

It is essential to define a confirmation dialog before performing the delete operation.

We only have a single modal here; we have the `o-modal` component, which you can also see here in the official documentation:

<o-modal v-model:active="isActive">
     ***
</o-modal>

Notice that we also have the variable that will allow the confirmation dialog to be displayed or not, called `isActive`.

And along with Tailwind, we can add those small details, so it becomes:

<o-modal v-model:active="confirmDeleteActive">
   <div class="m-4">
       <p class="mb-3">Are you sure you want to delete the selected record?</p>
       <div class="flex gap-2 flex-row-reverse">
           <o-button @click="confirmDeleteActive = false">Cancel</o-button>
           <o-button variant="danger" @click="remove">Delete</o-button>
       </div>
   </div>
</o-modal>

In the table, we place a reference to the index of the element we want to delete:

<o-table-column label="Actions" v-slot="m">
    <o-button variant="danger" size="small" @click="remove(m)">Delete</o-button>
</o-table-column>
***
remove(movie){
    axios.delete('http://code4movies.test/api/pelicula/' + movie.row.id)
        .then(res => res.data)
        .catch(error => error)
    this.movies.splice(movie.index,1)
},

And that's it; with this, we now have a scheme to delete a record.

***

Tailwind: Container - To Prevent Content from Being Stretched Out

Video thumbnail
Video thumbnail

Since the visual aspect is important, and although you could create a custom style (or use rapid prototyping libraries like Bootstrap), we are going to use Tailwind CSS.

Although a custom style might be interesting for small applications like this, we will use Tailwind for the following reasons:

  1. It is a widely used standard in the industry.
  2. It allows us to demonstrate its integration and functionality.
  3. It is a tool that is almost always useful and applicable to any project.

The Philosophy of Tailwind CSS

Tailwind CSS, unlike frameworks like Bootstrap, is a utility-first CSS framework based on utility classes.

  • What It Means: Instead of providing pre-designed components (like a button or a complete card with fixed margin and padding), Tailwind gives you thousands of small, atomic classes that perform a single function (e.g., `text-lg` for large text, `p-4` for 4 units of padding, `flex` to use flexbox).
  • Your Role: You are the one who builds the components by combining these utility classes directly in your HTML. As you can see, you add classes to the element, and you see how the structure and style of the element changes.

Adding Tailwind to the Project

To add Tailwind to the project:

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

And in the stylesheet, we add:

index.css

@tailwind base;
@tailwind components;
@tailwind utilities;

We load the CSS in `main.js`:

import { createApp } from 'vue'
import App from './App.vue'
import './index.css' // Imports your CSS file with Tailwind directives
createApp(App).mount('#app')

Container

In Tailwind, we also have the `container` class for this purpose, but unlike Bootstrap, it doesn't center the content. For this reason, we can set the margin to auto on the x-axis:

src/App.vue

<div class="container mx-auto my-3">
 <router-view></router-view>
</div>

Card Component

Video thumbnail

We're going to create a card component or simply a set of classes that define a structure, in this case for a card. A card is nothing more than a container, usually white, with some border, perhaps a shadow, and little else, along with some spacing:

src/css/main.css

.card {
   @apply bg-white p-6 rounded-md shadow-lg
}

And then we use it:

src/components/CRUD/Movies/ListComponent.vue

<h1>List</h1>
<div class="card">
   <div class="mb-4 ms-2">
    ***
  </o-table>
</div>

So, this is usually placed as a parent element to hold content, for example, all this content:

src/components/CRUD/Movies/SaveComponent.vue

<div class="card">
  <o-field label="Title" :variant="errors.title ? 'danger' : ''" :message="errors.title">
  ***
  <o-button variant="primary" @click="send">Send</o-button>
</div>

You can customize the size, color, among other aspects you consider important at the HTML or CSS level.

Expanded in Oruga UI o-input

Video thumbnail

To display Oruga UI fields as expanded, you just need to add the `expanded` attribute:

src/components/CRUD/Movies/SaveComponent.vue
***
<o-input v-model="form.title" expanded></o-input>
***
<o-input v-model="form.description" type="textarea" expanded></o-input>
***
<o-select v-model="form.category_id" expanded>

CRUD in Vue + Rest Api CodeIgniter

The CRUD summary is as follows:

We will follow the same process for categories, creating their components in Vue, resulting in:

src/components/CRUD/Categories/ListComponent.vue

<template>
    <o-modal v-model:active="confirmDeleteActive">
        <div class="m-4">
            <p class="mb-3">Are you sure you want to delete the <span class="font-bold">{{ deleteCategoryRow && deleteCategoryRow.row ? deleteCategoryRow.row.titulo : '' }}</span> record?</p>
            <div class="flex gap-2 flex-row-reverse">
                <o-button @click="confirmDeleteActive = false">Cancel</o-button>
                <o-button variant="danger" @click="remove">Delete</o-button>
            </div>
        </div>
    </o-modal>
    <h1>List</h1>
    <div class="card">
        <div class="mb-4 ms-2">
            <o-button @click="$router.push({ name: 'category.save' })" variant="primary" size="large">Create</o-button>
        </div>
        <o-table :data="categories" :bordered="true" :striped="true" :hoverable="true" :selectable="true">
            <o-table-column field="id" label="ID" v-slot="c" sortable>
                {{ c.row.id }}
            </o-table-column>
            <o-table-column field="titulo" label="Title" v-slot="c" sortable>
                {{ c.row.titulo }}
            </o-table-column>
            <o-table-column label="Actions" v-slot="c">
                <o-button @click="$router.push({ name: 'category.save', params: { id: c.row.id } })"
                    icon-left="pencil">Edit</o-button>
                <div class="inline ms-3">
                    <o-button variant="danger" size="small" @click="confirmDeleteActive = true; deleteCategoryRow = m"
                        icon-left="delete">Delete</o-button>
                </div>
            </o-table-column>
        </o-table>
    </div>
</template>
<script>
export default {
    created() {
        this.getCategories();
    },
    data() {
        return {
            categories: [],
            confirmDeleteActive: false,
            deleteCategoryRow: ''
        }
    },
    methods: {
        remove() {
            axios.delete('http://code4movies.test/api/categoria/' + this.deleteCategoryRow.row.id)
                .then(res => res.data)
                .catch(error => error)
            this.categories.splice(this.deleteCategoryRow.index, 1)
            this.confirmDeleteActive = false
        },
        async getCategories() {
            this.categories = await axios.get('http://code4movies.test/api/categoria')
                .then(res => res.data)
                .catch(error => error)
        },
    },
}
</script>

src/components/CRUD/Categories/SaveComponent.vue

<template>
    <h1>
        <template v-if="category">
            Update: <span class="font-bold"> {{ category.titulo }}</span>
        </template>
        <template v-else>
            Create
        </template>
    </h1>
    <div class="card max-w-lg mx-auto">
        <!-- <o-field label="Title"> -->
        <o-field label="Title" :variant="errors.title ? 'danger' : ''" :message="errors.title">
            <o-input v-model="form.title" expanded></o-input>
        </o-field>
        <o-button variant="primary" @click="send">Send</o-button>
    </div>
</template>
<script>
export default {
    data() {
        return {
            category: '',
            form: {
                title: '',
            },
            errors: {
                title: ''
            }
        }
    },
    async mounted() {
        if (this.$route.params.id) {
            await this.getCategory()
            this.init()
        }
    },
    methods: {
        init() {
            this.form.title = this.category.titulo
        },
        async getCategory() {
            this.category = await axios.get('http://code4movies.test/api/categoria/' + this.$route.params.id)
                .then(res => res.data)
                .catch(error => error)
        },
        async send() {
            let res = ''
            if (this.category == '') {
                const formData = new FormData()
                formData.append('titulo', this.form.title)
                res = await axios.post('http://code4movies.test/api/categoria', formData)
                    .then(res => res.data) // 200
                    .catch(error => error) // 500-400
                this.cleanForm()
            } else {
                res = await axios.put('http://code4movies.test/api/categoria/' + this.category.id, this.form)
                    .then(res => res.data) // 200
                    .catch(error => error) // 500-400
                this.cleanForm()
            }
            // error
            if (res.response && res.response.data.titulo) {
                this.errors.title = res.response.data.titulo
            }
        },
        cleanForm() {
            this.errors = {
                title: ''
            }
        }
    },
}
</script>

Grouping Routes

Since we can use the Vue application for different modules—for example, we could create another module for the end-user in the same application where we created the dashboard—it is very useful to group the routes. To do this, we can use the following configuration:

const routes = [
    {
        path: '/dashboard',
        component: Base,
        children: [
            {
                name: 'movie.list',
                path: 'movie',
                component: ListMovie
            },
            {
                name: 'movie.save',
                path: 'movie/save',
                component: SaveMovie
            },
            {
                name: 'movie.save',
                path: 'movie/save/:id?',
                component: SaveMovie
            },
            {
                name: 'category.list',
                path: 'category',
                component: ListCategory
            },
            {
                name: 'category.save',
                path: 'category/save',
                component: SaveCategory
            },
            {
                name: 'category.save',
                path: 'category/save/:id?',
                component: SaveCategory
            }
        ]
    }
]

As you can see, we replaced the base routes and made them children of the `children` option, where we defined the route prefix, which must always start with a `/`:

path: '/dashboard',

Followed by the child routes, which were given better names. Remember to update the name references throughout the application with this, for example:

src/components/CRUD/Movies/ListComponent.vue

<o-button @click="$router.push({ name: 'movie.save' })" variant="primary" size="large">Create</o-button>

Specific Component

We can also create a Base component for these routes, meaning not just using `App.vue`. This is particularly useful for making a clear distinction between different modules of our application using a distinct design. To do this:

src/components/CRUD/Base.vue

<template>
 <div class="container mx-auto my-3">
    <router-view></router-view>
  </div>
</template>

And since we demonstrably defined the container in the new component, we remove it from `App.vue`:

src/App.vue

<template>
    <router-view></router-view>
</template>

In this way, you can now create other completely customizable modules, including their design.

If you have an API in other frameworks like Django, Laravel, or FastApi, to name a few, it's the same, since the REST API is the way we connect using Vue and is independent of the latter.

Navbar: Navigation Links in Vue Router

Video thumbnail

The next step is to configure a navbar. We'll configure it now. There's tons of inspiration on the Internet, and you're free to adapt any of them:

src/components/CRUD/Base.vue

<template>

 <nav class="bg-white border-gray-200 dark:bg-gray-900">
   <div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
     <span href="" class="flex items-center space-x-3">
       <!-- <img src="" class="h-8" alt="Logo" /> -->
       <span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">VueCode4</span>
     </span>
     <button data-collapse-toggle="navbar-default" type="button"
       class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"
       aria-controls="navbar-default" aria-expanded="false">
       <span class="sr-only">Open main menu</span>
       <svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
         <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
           d="M1 1h15M1 7h15M1 13h15" />
       </svg>
     </button>
     <div class="hidden w-full md:block md:w-auto" id="navbar-default">
       <ul
         class="font-medium flex flex-col p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:flex-row md:space-x-8 rtl:space-x-reverse md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
         <li>
           <router-link 
             class="block py-2 px-3 text-white bg-blue-700 rounded-sm md:bg-transparent md:text-blue-700 md:p-0 dark:text-white md:dark:text-blue-500"
           :to="{ name:'movie.list' }">Movies</router-link>
         </li>
         <li>
           <router-link 
             class="block py-2 px-3 text-white bg-blue-700 rounded-sm md:bg-transparent md:text-blue-700 md:p-0 dark:text-white md:dark:text-blue-500"
           :to="{ name:'category.list' }">Category</router-link>
         </li>       
       </ul>
     </div>
   </div>
 </nav>


 <div class="container mx-auto my-3">
   <router-view></router-view>
 </div>
</template>

Enable Hamburger Menu in Navbar

Video thumbnail

Now let's go back to what would be our navbar that we created earlier:

Redirect 404 in Vue Router - 51

Specifically, the hamburger menu. Before we begin, it's important to remember that there was a link here, which is where we defined the hamburger button link:

<button data-collapse-toggle="navbar-default" type="button"
  class="*** md:hidden ***"
  aria-controls="navbar-default" aria-expanded="false">
  <span class="sr-only">Open main menu</span>
</button>

The Problem in Mobile Mode

So, going back to the hamburger menu, which is the one we have configured here... what's the problem?

Here, in desktop mode, everything works perfectly. The menu displays without any problems.

But when we switch to mobile mode, we have the hamburger menu. Based on what I explained earlier, what's happening here is that the md:block rule is no longer met. That class is somewhere — if we do a Control+F and search for md:block, we'll see that it's responsible for displaying the menu on desktop. Since that condition isn't met on mobile, the menu is hidden, and from there other mobile-specific rules apply.

Suppose we remove that class: notice that the menu reappears, and we can navigate again without any problem. However, I don't want the menu to be visible by default; rather, I want it to be shown or hidden when we press the hamburger button.

Mobile menu

We've already identified that the "problem" in quotes is in this hidden class we have here. So, programmatically, we have to hide or show it based on the user's criteria.

Implementing Toggle Logic

Let's handle the toggle logic directly in the component. I'll place the <script> block and the data inside:

<button @click="showMenuMobile=!showMenuMobile" data-collapse-toggle="navbar-default" type="button" ***>***</button>
***
<div class="w-full md:block md:w-auto" :class="{ 'hidden' : showMenuMobile }" id="navbar-default">
***
<script>
export default {
  data() {
    return {
      showMenuMobile: false,
    }
  },
}
</script>

Apply Conditional Classes

The next step is to configure the classes, i.e., remove or show the menu depending on the state:

<button data-collapse-toggle="navbar-default" type="button"
  class="*** md:hidden ***"
  aria-controls="navbar-default" aria-expanded="false">
  <span class="sr-only">Open main menu</span>
</button>

For this, we use :class. We may not have seen this in the course yet, but it's very useful. Vue is smart enough to evaluate the condition and automatically append or remove the class:

<div class="w-full md:block md:w-auto" :class="{ 'hidden' : showMenuMobile }" id="navbar-default">

Style Details

You can apply transitions, but that's not the focus here (this is a basic course). What we're doing is selecting the menu, which has many classes. Notice that the hidden attribute appears at the beginning, and when we click on it, it disappears, thanks to the logic we applied.

This works the same as using a conditional attribute: if it's true, it's applied; if not, it's not. Vue handles this :class attribute internally, because in HTML there can only be one class attribute per element.

What Vue does is mix static classes with dynamic ones based on the condition. You can add more conditions if necessary. For example:

:class="{ 'hidden': !showMenuMobile, 'otra-clase': otraCondicion }"

Extra: We expose the Oruga UI variables to vary the style

We can easily customize Caterpillar UI at the graphical interface level, change aspects such as the border, background color... in short, any aspect that you can customize with CSS can be easily done by exposing the Caterpillar UI variables used by the caterpillar components UI

We import the package of variables with which we can easily vary the theme:

resources\js\vue\main.js

//***
import '@oruga-ui/oruga-next/dist/oruga-full.css'
import '@oruga-ui/oruga-next/dist/oruga-full-vars.css'

We modify the variables

And with this, now the CSS of the Oruga UI component is based on variables, and with this, we can customize them; in this example, using Tailwind:

resources\css\vue.css

h1{
    @apply text-3xl text-center my-5
}
:root{
    --oruga-modal-content-padding:0
}

Action done message, toast in Vue 3 with Oruga UI

Another very interesting component that we have at our disposal is that of displaying a toast-type message; for that we have the function:

this.$oruga.notification.open

Which receives as parameters:

  • message, The message.
  • position, The position that can be: top-right toptop-left bottom-right bottom bottom-left
  • variant, The variant that can be: primary info warning danger
  • duration, The duration expressed in thousandths of a second.
  • closable, and whether it can be closed by clicking.

For example:

this.$oruga.notification.open({
        message: "Registro eliminado",
        position: "bottom-right",
        variant: "danger",
        duration: 4000,
        closable: true,
});

Or to save:

this.$oruga.notification.open({
   message: "Registro procesado con éxito",
   position: "bottom-right",
   duration: 4000,
   closable: true,
});

Confirmation modal to delete records from Vue 3 with Oruga UI

In the previous article, we saw how to create the option to delete record from the list with Vue 3; but, the deletion is direct and does not present the typical confirmation dialogs to avoid deleting a record by mistake.

Confirmation modal in Oruga UI

In Oruga UI we have a component called modal, which is like an empty canvas ready for us to place all the content we want to place; in the case of a confirmation dialog, it would be a text to delete and the action buttons:

resources\js\vue\componets\List.vue

<o-modal v-model:active="confirmDeleteActive">
  <div class="p-4">
    <p>¿Seguro que quieres eliminar el registro seleccionado?</p>
  </div>
  <div class="flex flex-row-reverse gap-2 bg-gray-100 p-3">
    <o-button variant="danger" @click="deletePost()">Eliminar</o-button>
    <o-button @click="confirmDeleteActive = false">Cancelar</o-button>
  </div>
</o-modal> 

From the listing, all we do is call the modal via a boolean property:

  • true, to display the modal
  • false, to hide the modal

In the Oruga UI table, we now activate the modal and set the row of the element we want to remove; since, having a process involved, the deletion cannot be triggered directly:

<o-table-column field="slug" label="Acciones" v-slot="p">
        <router-link
          class="mr-3"
          :to="{ name: 'save', params: { slug: p.row.slug } }"
          >Editar</router-link
        >
 
        <o-button
          iconLeft="delete"
          rounded
          size="small"
          variant="danger"
          @click="
            deletePostRow = p;
            confirmDeleteActive = true;
          "
          >Eliminar</o-button
        >
      </o-table-column>
    </o-table>

The property in question:

data() {
    return {
      //***
      confirmDeleteActive: false,
      deletePostRow: "",
    };

And the delete function which now deletes by the index of the record:

deletePost() {
      this.confirmDeleteActive = false;
      this.posts.data.splice(this.deletePostRow.index, 1);
      this.$axios.delete("/api/post/" + this.deletePostRow.row.id);
    },

Remember that the table row in Caterpillar UI has a lot of information such as the table index, in addition to the ID, which is a field that you can customize to indicate, for example, the PK of the record with which to interact.

Extra: How to easily reuse forms in Vue

Web applications contain many forms today. Many times, we have the same form layout when creating or editing something (can be anything: user, project, to-do item, product, etc.). Usually, creating and editing a resource is implemented on 2 separate pages; but, being similar processes, we can always easily reuse it.

Fortunately, if you use Vue, you can easily implement reusable form components. Let's start then.

Let's create a reusable form component

We will create a simple form to create or edit a user. Without further ado, this is what the reusable form will look like in the end.

To simplify the process, the form will only have two fields, but you can use the same logic if you have more fields.

We are going to know first the components of creating and editing separately, then, we will see the combination of both.

Component to create elements

Easy to follow structure, a form, which we capture the submit event with @submit.prevent, our form fields with the v-model and the submit function to save data.

<template>
 <form @submit.prevent="submit">
   <input type="text" v-model="form.title" />
   <textarea v-model="form.description"></textarea>
   <button type="submit">Enviar</button>
 </form>
</template>
<script>
export default {
 mounted() {},
 data() {
   return {
     form: {
       title: "", //this.$root.animes[this.position].title,
       description: "",
     },
   };
 },
 methods: {
   submit: function () {
         //GUARDAR
     });
   },
 },
};
</script>

Component to Edit

For the initialization, it is exactly the same, except that we initialize the data, for that the mounted, and in the process of saving which you can update some list, send requests etc:

<template>
<form @submit.prevent="submit">
   <input type="text" v-model="form.title">
   <textarea v-model="form.description"></textarea>
   <button type="submit">Enviar</button>
</form>
</template>
<script>
export default {
   mounted() {

       this.position = this.$route.params.position
       this.form.title = this.$root.animes[this.position].title
       this.form.description = this.$root.animes[this.position].description
   },
 data() {
   return {
     position: 0,
     form: {
       title:"", 
       description: "",
     },
   };
 },
 methods: {
   submit: function () {
       // EDITAR
   },
 },
};
</script>

Component to Edit and Create

Now we are going to merge the previous components; for that, we have to receive some parameter from the parent component to know if we are editing or creating; in this case, we assume that it is a process that we can carry out through Vue Router:

this.position = this.$route.params.position || -1;

Already with this initialization, we determine if we initialize the data or not, as well as if we create or edit it when we send the form:

<template>
 <form @submit.prevent="submit">
   <input type="text" v-model="form.title" />
   <textarea v-model="form.description"></textarea>
   <button type="submit">Enviar</button>
 </form>
</template>
<script>
export default {
 mounted() {
   this.position = this.$route.params.position || -1;
   this.form.title =
     this.position !== -1 ? this.$root.animes[this.position].title : "";
   this.form.description =
     this.position !== -1 ? this.$root.animes[this.position].description : "";
 },
 data() {
   return {
     position: 0,
     form: {
       title: "", //this.$root.animes[this.position].title,
       description: "",
     },
   };
 },
 methods: {
   submit: function () {
     if (this.position !== -1)
// EDITAR
       this.$root.animes[this.position].title = this.form.title;
     else {
// CREAR
       this.$root.animes.push({
         id: this.$root.animes.length,
         title: this.form.title,
         description: this.form.description,
       });
     }
   },
 },
};
</script>

Extra: Material Design iconography in Vue 3 with Oruga UI

Video thumbnail

Material Design is a design template used by Google that has been developed to create visual and consistent user interfaces and is used on different platforms such as Android. Material Design draws on design principles such as depth, light and motion, shadows, animations, and transitions to create a sense of depth and realism in user interfaces, with the goal of providing an engaging and intuitive user experience.

Vibrant colors and legible typography are also used to improve readability and accessibility and as you can guess, it also uses a very minimalistic set of icons that we can use, which is what we are going to talk about in this post.

Now with our project created in Vue 3, in these guides it would be with Laravel, although you can follow the same steps with Vue Cli, we are going to install an iconography to be able to use the icons in Vue with Oruga UI.

In this case, it will be Google's Material Design:

For the iconography, we will use MaterialDesign:

$ npm install @mdi/font

And we reference it:

resources/js/vue/main.js
//Material Design
import "@mdi/font/css/materialdesignicons.min.css"

With this, you can use any of the icons that you can find on the official website:

https://materialdesignicons.com/

Extra: Route Grouping in Vue Router: Child Routes, Prefix and Component

Video thumbnail

The grouping is done by defining a main route (/dashboard) that acts as a container and uses the base component (Base.vue or similar). The CRUD-specific routes (movies and categories) are defined within the children array.

const routes = [
    {
        path: '/dashboard',
        component: Base,
        children: [
            {
                name: 'movie.list',
                path: 'movie',
                component: ListMovie
            },
            {
                name: 'movie.save',
                path: 'movie/save',
                component: SaveMovie
            },
            {
                name: 'movie.save',
                path: 'movie/save/:id?',
                component: SaveMovie
            },
            {
                name: 'category.list',
                path: 'category',
                component: ListCategory
            },
            {
                name: 'category.save',
                path: 'category/save',
                component: SaveCategory
            },
            {
                name: 'category.save',
                path: 'category/save/:id?',
                component: SaveCategory
            }
        ]
    }
]

The syntax of Vue Router might seem a bit strange at first glance. Logically, if we use the `children` property, we would expect the routes to be placed at the same level as the object, but that's not the case.

Actually, the grouped route is the entire structure. That is, the configuration of the grouped routes starts in the main object (in your case, from line 9, which starts the `children` array). Therefore, the nested configuration goes there.

The path isn't placed directly inside here; instead, the parent route defines the layout and prefix. Within the `children` array, we only place our child routes, to which we can then assign their own paths relative to the parent.

{
    path: '/dashboard',

Here, in case we could define a component, which would be a route, a component like the App component, a component like the Vue component, is what we could put here:

    {
        path: '/dashboard',
        component: Base,

This way you can define a common style for grouped routes:

src/components/CRUD/Base.vue

<template>
<div class="container mx-auto my-3">
   <router-view></router-view>
 </div>
</template>

Pagination Component in Oruga UI Vue 3

Video thumbnail

With the list with the Oruga UI table ready, the next thing to do is the pagination; to do this, we are going to use the Oruga pagination component, which receives several parameters; many of them in shape, for styles and little else:

  • @change="updatePage" Event that is executed when clicking on a paginated link, in these cases you must use it to update the list
  • :total="posts.total" Indicates the total number of records
  • v-model:current="currentPage" Indicates the v-model to be updated with the current page index
  • :range-before="2" Number of pages to appear before the current page.
  • :range-after="2" Number of pages to appear after the current page.
  • order="centered" Centered pagination.
  • size="small" Pagination type.
  • :simple="false" Layout, if simple or complete.
  • rounded="true" Rounded layout.
  • :per-page="posts.per_page" How many posts to show per page.

In Laravel, when using the paging component, we have all these parameters already defined; therefore, the integration is direct.

  ***
 </o-table>
    <br />
    <o-pagination
      v-if="posts.current_page && posts.data.length > 0"
      @change="updatePage"
      :total="posts.total"
      v-model:current="currentPage"
      :range-before="2"
      :range-after="2"
      order="centered"
      size="small"
      :simple="false"
      :rounded="true"
      :per-page="posts.per_page"
    >
    </o-pagination>
  </div>
</template>
<script>
export default {
  data() {
    return {
      posts: [],
      isLoading: true,
      currentPage:1,
    };
  },
methods: {
  updatePage(){
    setTimeout(this.listPage, 100);
  },
  listPage(){
    this.isLoading = true;
     this.$axios.get("/api/post?page="+this.currentPage).then((res) => {
      this.posts = res.data;
      console.log(this.posts);
      this.isLoading = false;
    });
  }
},
  async mounted() {
   this.listPage()
  },
};
</script>

Extra: Manejar el token de autenticación

Vamos a partir de un proyecto creado en Laravel en la cual configuramos Vue en un proyecto en Laravel.

Instalar plugin para la cookie

Vamos a instalar un plugin para manejar las Cookies en Vue 3:

https://www.npmjs.com/package/vue3-cookies

Ejecutamos en la terminal:

$ npm install vue3-cookies --save

Configurar el plugin en el proyecto

Su uso es muy sencillo vamos a primero a inicializar el plugin de la cookie en el proyecto, para ello, usaremos el archivo de configuración de la aplicación:

import { createApp } from "vue";
//tailwind
import '../../css/vue.css'
// Oruga
import Oruga from '@oruga-ui/oruga-next'
import '@oruga-ui/oruga-next/dist/oruga.css'
import '@oruga-ui/oruga-next/dist/oruga-full.css'
import '@oruga-ui/oruga-next/dist/oruga-full-vars.css'
//Material Design
import "@mdi/font/css/materialdesignicons.min.css"
import axios from 'axios'
import App from "./App.vue"
import router from "./router"
import VueCookies from 'vue3-cookies'
const app = createApp(App).use(Oruga).use(router)
app.use(VueCookies);
app.config.globalProperties.$axios = axios
window.axios = axios
app.mount("#app")

En el script anterior, tenemos otros plugins configurados, en este caso, Oruga UI con Laravel y Vue, ya que, este es un proyecto que forma parte de mi curso completo sobre Laravel.

Uso de las cookies

Ya con esto, podemos hacer uso de la cookie y con esto, poder guardar valores:

this.$cookies.set(keyName,'value');

U obtenerlos:

$cookies.get(keyName)

Como puedes ver, su uso una vez configurado, es extremadamente sencillo.

Configurar vue3-cookies con los datos de autenticación

Vamos a crear una función para establecer los valores de usuario en el componente padre, con lo cual, podremos usarlo en cualquier parte de la aplicación mediante los componentes hijos:

resources\js\vue\App.vue

setCookieAuth(data) {
  this.$cookies.set("auth", data);
},

La misma, la usaremos desde el login:

resources\js\vue\componets\Auth\Login.vue

submit() {
  this.cleanErrorsForm();
  return this.$axios
    .post("/api/user/login", this.form)
    .then((res) => {
   this.$root.setCookieAuth({
        isLoggedIn: res.data.isLoggedIn,
        token: res.data.token,
        user: res.data.user,
      });
   ***
}

La función anterior, es la que usamos para configurar la cookie de autenticación en el proyecto al momento de hacer el login desde la aplicación; la función de login en Laravel es:

    public function login(Request $request)
    {
        $credentials = [
            'email' => $request->email,
            'password' => $request->password
        ];


        if (Auth::attempt($credentials)) {
            $token = Auth::user()->createToken('myapptoken')->plainTextToken;
            session()->put('token', $token);


            return response()->json([
                'isLoggedIn' => true,
                'user' => auth()->user(),
                'token' => $token,
            ]);
        }
        return response()->json("Usuario y/o contraseña inválido", 422);
    }

Extra: Handling the auth token

Let's start from a project created in Laravel in which we configure Vue in a Laravel project.

Install cookie plugin

We are going to install a plugin to handle Cookies in Vue 3:

https://www.npmjs.com/package/vue3-cookies

Ejecutamos en la terminal:

$ npm install vue3-cookies --save

Configure the plugin in the project

Its use is very simple, we are going to first initialize the cookie plugin in the project, for this, we will use the application configuration file:

import { createApp } from "vue";
//tailwind
import '../../css/vue.css'
// Oruga
import Oruga from '@oruga-ui/oruga-next'
import '@oruga-ui/oruga-next/dist/oruga.css'
import '@oruga-ui/oruga-next/dist/oruga-full.css'
import '@oruga-ui/oruga-next/dist/oruga-full-vars.css'
//Material Design
import "@mdi/font/css/materialdesignicons.min.css"
import axios from 'axios'
import App from "./App.vue"
import router from "./router"
import VueCookies from 'vue3-cookies'
const app = createApp(App).use(Oruga).use(router)
app.use(VueCookies);
app.config.globalProperties.$axios = axios
window.axios = axios
app.mount("#app")

In the script above, we have other plugins configured, in this case, Oruga UI with Laravel and Vue, as this is a project that is part of my complete course on Laravel.

Use of cookies

With this, we can make use of the cookie and with this, be able to save values:

this.$cookies.set(keyName,'value');

Or get them:

$cookies.get(keyName)

As you can see, its use once configured is extremely simple.

Configure vue3-cookies with the authentication data

We are going to create a function to set the user values in the parent component, with which we can use it anywhere in the application through the child components:

resources\js\vue\App.vue

setCookieAuth(data) {
  this.$cookies.set("auth", data);
},

The same, we will use it from the login:

resources\js\vue\componets\Auth\Login.vue

submit() {
  this.cleanErrorsForm();
  return this.$axios
    .post("/api/user/login", this.form)
    .then((res) => {
   this.$root.setCookieAuth({
        isLoggedIn: res.data.isLoggedIn,
        token: res.data.token,
        user: res.data.user,
      });
   ***
}

The above function is the one we use to configure the authentication cookie in the project when logging in from the application; The login function in Laravel is:

    public function login(Request $request)
    {
        $credentials = [
            'email' => $request->email,
            'password' => $request->password
        ];


        if (Auth::attempt($credentials)) {
            $token = Auth::user()->createToken('myapptoken')->plainTextToken;
            session()->put('token', $token);


            return response()->json([
                'isLoggedIn' => true,
                'user' => auth()->user(),
                'token' => $token,
            ]);
        }
        return response()->json("Usuario y/o contraseña inválido", 422);
    }

I agree to receive announcements of interest about this Blog.

Learn how to build a dynamic CRUD (Create, Read, Update, Delete) application using Vue 3 (Options API) and consuming a RESTful API developed in CodeIgniter 4. This guide covers Vue configuration, integration of key libraries, and development best practices.

| 👤 Andrés Cruz

🇪🇸 En español