Create A Recipe App To Learn Laravel
Lasted edited: December 4, 2022 June 19,2024
A work in progress. Maybe.
Creating A Recipe App To Learn Laravel/Mysql/Inertia/Vite/React
Intro
In August 2022, I started to put together a shopping list app to learn Laravel/Mysql/Inertia/Vite/React.
The app can be seen here. You can try it out, too.
As of December 3, 2022, the shopping list app is working well. It’s very simple. Just a list with a delete button next to to each item.
The menu tab works.
And there are different menus if the user is logged out.
The password reminder email also works.
I’ve used React for the front end and git to track the changes.
It’s where I want it to be.
And you can see my blog post about my experience making the shopping list here.
Now, I want to push my learning further by creating a recipe app.
Here I go.
Recipe App Data Design
Since I last worked on this recipe app, ChatGPT has come online. So, I figured I should use ChatGPT and Gemini and Meta.AI to see how they’ll do on this recipe app.
I figured that the mysql tables that are needed are:
Users - standard Laravel user table.
Recipes - table will have the name of the recipes along with the name of the user who created the recipe.
Ingredients - name of the ingredients that belong to each recipe. Ingredients should also have the name of the user who created the recipe. There will also be some ingredients that will be added by the admin user (me).
When I asked ChatGPT to design the mysql database, this is how it responded:
Designing a MySQL database for a recipe app involves defining the entities, their attributes, and the relationships between them. Here is a structured approach to design a MySQL database for such an application:
### 1. Identify the Entities and Their Attributes
**Entities:**
- Users
- Recipes
- Ingredients
- RecipeIngredients (junction table for many-to-many relationship)
- Categories
- Comments
- Ratings
- Favorites
**Attributes:**
- Users: `user_id`, `username`, `email`, `password`, `created_at`, `updated_at`
- Recipes: `recipe_id`, `user_id` (foreign key), `title`, `description`, `instructions`, `image_url`, `category_id` (foreign key), `created_at`, `updated_at`
- Ingredients: `ingredient_id`, `name`
- RecipeIngredients: `recipe_id` (foreign key), `ingredient_id` (foreign key), `quantity`, `unit`
- Categories: `category_id`, `name`
- Comments: `comment_id`, `recipe_id` (foreign key), `user_id` (foreign key), `content`, `created_at`, `updated_at`
- Ratings: `rating_id`, `recipe_id` (foreign key), `user_id` (foreign key), `rating`, `created_at`, `updated_at`
- Favorites: `favorite_id`, `user_id` (foreign key), `recipe_id` (foreign key), `created_at`
### 2. Define the Relationships
- A user can create many recipes.
- A recipe can belong to one category.
- A recipe can have many ingredients, and an ingredient can be part of many recipes (many-to-many relationship).
- A user can comment on many recipes, and a recipe can have many comments.
- A user can rate many recipes, and a recipe can have many ratings.
- A user can have many favorite recipes.
### 3. Create the Database Schema
Here is the SQL code to create the database schema:
```sql
-- Users table
CREATE TABLE Users (
user_id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
email VARCHAR(100) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- Categories table
CREATE TABLE Categories (
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE
);
-- Recipes table
CREATE TABLE Recipes (
recipe_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
title VARCHAR(255) NOT NULL,
description TEXT,
instructions TEXT NOT NULL,
image_url VARCHAR(255),
category_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES Users(user_id),
FOREIGN KEY (category_id) REFERENCES Categories(category_id)
);
-- Ingredients table
CREATE TABLE Ingredients (
ingredient_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL UNIQUE
);
-- RecipeIngredients junction table
CREATE TABLE RecipeIngredients (
recipe_id INT,
ingredient_id INT,
quantity VARCHAR(50),
unit VARCHAR(50),
PRIMARY KEY (recipe_id, ingredient_id),
FOREIGN KEY (recipe_id) REFERENCES Recipes(recipe_id),
FOREIGN KEY (ingredient_id) REFERENCES Ingredients(ingredient_id)
);
-- Comments table
CREATE TABLE Comments (
comment_id INT AUTO_INCREMENT PRIMARY KEY,
recipe_id INT,
user_id INT,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (recipe_id) REFERENCES Recipes(recipe_id),
FOREIGN KEY (user_id) REFERENCES Users(user_id)
);
-- Ratings table
CREATE TABLE Ratings (
rating_id INT AUTO_INCREMENT PRIMARY KEY,
recipe_id INT,
user_id INT,
rating TINYINT NOT NULL CHECK (rating >= 1 AND rating <= 5),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (recipe_id) REFERENCES Recipes(recipe_id),
FOREIGN KEY (user_id) REFERENCES Users(user_id)
);
-- Favorites table
CREATE TABLE Favorites (
favorite_id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT,
recipe_id INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES Users(user_id),
FOREIGN KEY (recipe_id) REFERENCES Recipes(recipe_id)
);
### 4. Additional Considerations
- **Indexes**: Add indexes on foreign keys and any columns that are frequently searched.
- **Data Integrity**: Ensure that the foreign key constraints are set up correctly to maintain data integrity.
- **Normalization**: The design above is normalized to the third normal form (3NF), which eliminates redundancy and ensures data integrity.
- **Scalability**: Consider future scalability, such as sharding the database if the dataset becomes very large.
This design provides a robust and scalable foundation for a recipe app, supporting essential features like user management, recipe creation, ingredient listing, comments, ratings, and favorites.
ChatGPT Writes A Laravel Recipe App###
I then figured, I should just ask ChatGPT to write the recipe app in app using mysql.
My prompt for ChatGPT was:
can you show me how to create a recipe app using mysql and laravel
The response was:
Sure! Below is a step-by-step guide to create a simple recipe app using MySQL and Laravel. This guide assumes you have some basic knowledge of PHP and Laravel.
Prerequisites
- PHP: Version 7.3 or higher.
- Composer: For dependency management.
- MySQL: For the database.
- Laravel: Version 8 or higher.
Step 1: Set Up Laravel Project
- Install Laravel:
composer create-project --prefer-dist laravel/laravel recipe-app cd recipe-app
- Set Up Environment Variables:
Configure your
.env
file with your database credentials.DB_CONNECTION=mysql DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=recipe_app DB_USERNAME=your_db_user DB_PASSWORD=your_db_password
- Create Database:
Create a database in MySQL named
recipe_app
.
Step 2: Create Models and Migrations
- Generate Models and Migrations:
php artisan make:model User -m php artisan make:model Recipe -m php artisan make:model Ingredient -m php artisan make:model RecipeIngredient -m php artisan make:model Category -m php artisan make:model Comment -m php artisan make:model Rating -m php artisan make:model Favorite -m
-
Define Migrations:
Edit the migration files in
database/migrations
to match the schema.Users Migration (
2021_xx_xx_create_users_table.php
):public function up() { Schema::create('users', function (Blueprint $table) { $table->id(); $table->string('username')->unique(); $table->string('email')->unique(); $table->string('password'); $table->timestamps(); }); }
Categories Migration (
2021_xx_xx_create_categories_table.php
):public function up() { Schema::create('categories', function (Blueprint $table) { $table->id(); $table->string('name')->unique(); $table->timestamps(); }); }
Recipes Migration (
2021_xx_xx_create_recipes_table.php
):public function up() { Schema::create('recipes', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->string('title'); $table->text('description')->nullable(); $table->text('instructions'); $table->string('image_url')->nullable(); $table->foreignId('category_id')->constrained()->onDelete('set null'); $table->timestamps(); }); }
Ingredients Migration (
2021_xx_xx_create_ingredients_table.php
):public function up() { Schema::create('ingredients', function (Blueprint $table) { $table->id(); $table->string('name')->unique(); $table->timestamps(); }); }
RecipeIngredients Migration (
2021_xx_xx_create_recipe_ingredients_table.php
):public function up() { Schema::create('recipe_ingredients', function (Blueprint $table) { $table->foreignId('recipe_id')->constrained()->onDelete('cascade'); $table->foreignId('ingredient_id')->constrained()->onDelete('cascade'); $table->string('quantity'); $table->string('unit'); $table->primary(['recipe_id', 'ingredient_id']); }); }
Comments Migration (
2021_xx_xx_create_comments_table.php
):public function up() { Schema::create('comments', function (Blueprint $table) { $table->id(); $table->foreignId('recipe_id')->constrained()->onDelete('cascade'); $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->text('content'); $table->timestamps(); }); }
Ratings Migration (
2021_xx_xx_create_ratings_table.php
):public function up() { Schema::create('ratings', function (Blueprint $table) { $table->id(); $table->foreignId('recipe_id')->constrained()->onDelete('cascade'); $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->tinyInteger('rating')->check('rating >= 1 AND rating <= 5'); $table->timestamps(); }); }
Favorites Migration (
2021_xx_xx_create_favorites_table.php
):public function up() { Schema::create('favorites', function (Blueprint $table) { $table->id(); $table->foreignId('user_id')->constrained()->onDelete('cascade'); $table->foreignId('recipe_id')->constrained()->onDelete('cascade'); $table->timestamps(); }); }
- Run Migrations:
php artisan migrate
Step 3: Define Relationships in Models
Edit the models in app/Models
to define relationships.
User Model (User.php
):
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class User extends Authenticatable
{
use HasFactory;
protected $fillable = ['username', 'email', 'password'];
public function recipes()
{
return $this->hasMany(Recipe::class);
}
public function comments()
{
return $this->hasMany(Comment::class);
}
public function ratings()
{
return $this->hasMany(Rating::class);
}
public function favorites()
{
return $this->hasMany(Favorite::class);
}
}
Recipe Model (Recipe.php
):
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Recipe extends Model
{
use HasFactory;
protected $fillable = ['title', 'description', 'instructions', 'image_url', 'category_id'];
public function user()
{
return $this->belongsTo(User::class);
}
public function category()
{
return $this->belongsTo(Category::class);
}
public function ingredients()
{
return $this->belongsToMany(Ingredient::class)->withPivot('quantity', 'unit');
}
public function comments()
{
return $this->hasMany(Comment::class);
}
public function ratings()
{
return $this->hasMany(Rating::class);
}
public function favorites()
{
return $this->hasMany(Favorite::class);
}
}
Ingredient Model (Ingredient.php
):
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Ingredient extends Model
{
use HasFactory;
protected $fillable = ['name'];
public function recipes()
{
return $this->belongsToMany(Recipe::class)->withPivot('quantity', 'unit');
}
}
Category Model (Category.php
):
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Category extends Model
{
use HasFactory;
protected $fillable = ['name'];
public function recipes()
{
return $this->hasMany(Recipe::class);
}
}
Comment Model (Comment.php
):
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Comment extends Model
{
use HasFactory;
protected $fillable = ['content', 'recipe_id', 'user_id'];
public function recipe()
{
return $this->belongsTo(Recipe::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}
Rating Model (Rating.php
):
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Rating extends Model
{
use HasFactory;
protected $fillable = ['rating', 'recipe_id', 'user_id'];
public function recipe()
{
return $this->belongsTo(Recipe::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}
Favorite Model (Favorite.php
):
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Favorite extends Model
{
use HasFactory;
protected $fillable = ['recipe_id', 'user_id'];
public function recipe()
{
return $this->belongsTo(Recipe::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}
Step 4: Create Controllers and Routes
-
Generate Controllers:
php artisan make:controller UserController php artisan make:controller RecipeController php artisan make:controller IngredientController php artisan make:controller CategoryController php artisan make:controller CommentController php artisan make:controller RatingController php artisan make:controller FavoriteController
-
Define Routes in
routes/web.php
:use App\Http\Controllers\{UserController, RecipeController, IngredientController, CategoryController, CommentController, RatingController, FavoriteController}; Route::get('/', [RecipeController::class, 'index']); Route::resource('users', UserController::class); Route::resource('recipes', RecipeController::class); Route::resource('ingredients', IngredientController::class); Route::resource('categories', CategoryController::class); Route::resource('comments', CommentController::class); Route::resource('ratings', RatingController::class); Route::resource('favorites', FavoriteController::class);
Step 5: Implement Basic CRUD Operations in Controllers
Here is an example of the RecipeController
:
RecipeController (RecipeController.php
):
namespace App\Http\Controllers;
use App\Models\Recipe;
use App\Models\Ingredient;
use App\Models\Category;
use Illuminate\Http\Request;
class RecipeController extends Controller
{
public function index()
{
$recipes = Recipe::with(['user', 'category', 'ingredients'])->get();
return view('recipes.index', compact('recipes'));
}
public function create()
{
$categories = Category::all();
$ingredients = Ingredient::all();
return view('recipes.create', compact('categories', 'ingredients'));
}
public function store(Request $request)
{
$validatedData = $request->validate([
'title' => 'required|max:255',
'description' => 'nullable',
'instructions' => 'required',
'category_id' => 'required|exists:categories,id',
'ingredients' => 'required|array',
'ingredients.*.id' => 'exists:ingredients,id',
'ingredients.*.quantity' => 'required',
'ingredients.*.unit' => 'required',
]);
$recipe = new Recipe([
'title' => $validatedData['title'],
'description' => $validatedData['description'],
'instructions' => $validatedData['instructions'],
'category_id' => $validatedData['category_id'],
'user_id' => auth()->id(),
]);
$recipe->save();
foreach ($validatedData['ingredients'] as $ingredient) {
$recipe->ingredients()->attach($ingredient['id'], [
'quantity' => $ingredient['quantity'],
'unit' => $ingredient['unit'],
]);
}
return redirect()->route('recipes.index')->with('success', 'Recipe created successfully');
}
public function show($id)
{
$recipe = Recipe::with(['user', 'category', 'ingredients', 'comments.user', 'ratings.user'])->findOrFail($id);
return view('recipes.show', compact('recipe'));
}
public function edit($id)
{
$recipe = Recipe::with('ingredients')->findOrFail($id);
$categories = Category::all();
$ingredients = Ingredient::all();
return view('recipes.edit', compact('recipe', 'categories', 'ingredients'));
}
public function update(Request $request, $id)
{
$validatedData = $request->validate([
'title' => 'required|max:255',
'description' => 'nullable',
'instructions' => 'required',
'category_id' => 'required|exists:categories,id',
'ingredients' => 'required|array',
'ingredients.*.id' => 'exists:ingredients,id',
'ingredients.*.quantity' => 'required',
'ingredients.*.unit' => 'required',
]);
$recipe = Recipe::findOrFail($id);
$recipe->update([
'title' => $validatedData['title'],
'description' => $validatedData['description'],
'instructions' => $validatedData['instructions'],
'category_id' => $validatedData['category_id'],
]);
$recipe->ingredients()->detach();
foreach ($validatedData['ingredients'] as $ingredient) {
$recipe->ingredients()->attach($ingredient['id'], [
'quantity' => $ingredient['quantity'],
'unit' => $ingredient['unit'],
]);
}
return redirect()->route('recipes.index')->with('success', 'Recipe updated successfully');
}
public function destroy($id)
{
$recipe = Recipe::findOrFail($id);
$recipe->delete();
return redirect()->route('recipes.index')->with('success', 'Recipe deleted successfully');
}
}
Step 6: Create Blade Views
Create Blade templates for displaying and managing recipes in the resources/views
directory.
Example structure:
resources/views/layouts/app.blade.php
: Main layout.resources/views/recipes/index.blade.php
: List of recipes.resources/views/recipes/create.blade.php
: Form to create a recipe.resources/views/recipes/edit.blade.php
: Form to edit a recipe.resources/views/recipes/show.blade.php
: Display a single recipe.
Main Layout (layouts/app.blade.php
):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Recipe App</title>
<link rel="stylesheet" href="{{ asset('css/app.css') }}">
</head>
<body>
<nav>
<!-- Navigation links -->
</nav>
<main>
@yield('content')
</main>
<script src="{{ asset('js/app.js') }}"></script>
</body>
</html>
Recipe Index (recipes/index.blade.php
):
@extends('layouts.app')
@section('content')
<h1>Recipes</h1>
<a href="{{ route('recipes.create') }}">Create New Recipe</a>
<ul>
@foreach ($recipes as $recipe)
<li>
<a href="{{ route('recipes.show', $recipe->id) }}">{{ $recipe->title }}</a>
<form action="{{ route('recipes.destroy', $recipe->id) }}" method="POST">
@csrf
@method('DELETE')
<button type="submit">Delete</button>
</form>
</li>
@endforeach
</ul>
@endsection
Create Recipe (recipes/create.blade.php
):
@extends('layouts.app')
@section('content')
<h1>Create Recipe</h1>
<form action="{{ route('recipes.store') }}" method="POST">
@csrf
<label for="title">Title:</label>
<input type="text" id="title" name="title" required>
<label for="description">Description:</label>
<textarea id="description" name="description"></textarea>
<label for="instructions">Instructions:</label>
<textarea id="instructions" name="instructions" required></textarea>
<label for="category_id">Category:</label>
<select id="category_id" name="category_id" required>
@foreach ($categories as $category)
<option value="{{ $category->id }}">{{ $category->name }}</option>
@endforeach
</select>
<h2>Ingredients:</h2>
<div id="ingredients">
@foreach ($ingredients as $ingredient)
<div>
<input type="checkbox" id="ingredient_{{ $ingredient->id }}" name="ingredients[{{ $ingredient->id }}][id]" value="{{ $ingredient->id }}">
<label for="ingredient_{{ $ingredient->id }}">{{ $ingredient->name }}</label>
<input type="text" name="ingredients[{{ $ingredient->id }}][quantity]" placeholder="Quantity">
<input type="text" name="ingredients[{{ $ingredient->id }}][unit]" placeholder="Unit">
</div>
@endforeach
</div>
<button type="submit">Create</button>
</form>
@endsection
Step 7: Authentication
Laravel provides a simple way to handle authentication. Use Laravel Breeze or Jetstream for a quick setup.
- Install Laravel Breeze:
composer require laravel/breeze --dev php artisan breeze:install npm install && npm run dev php artisan migrate
- Set Up Auth Routes:
Route::middleware(['auth:sanctum', 'verified'])->get('/dashboard', function () { return view('dashboard'); })->name('dashboard');
Step 8: Testing and Finalization
1. **Test Your Application**:
Ensure all routes, forms, and views are functioning as expected.
2. **Deploy Your Application**:
Deploy your application to a web server or cloud service such as Laravel Forge, DigitalOcean, or AWS.
By following these steps, you will have a functional recipe application built with Laravel and MySQL. This app will support user authentication, recipe creation, ingredient management, and more.
And that is how ChatGPT suggested that I write the app.
Laravel/Mysql Table Relationships
We all know how difficult personal relationships can be.
Mysql relationships aren’t much easier.
By creating relationships between tables, Laravel makes it easier to extract the data with predefined methods. You see, that’s one of the things about using Laravel. You need to understand the different methods (i.e. - functions) that are built into the framework.
And so, let’s think about the different relationships.
The first relationship is One To Many.
This should apply between the Users Table and the Recipes Tables.
Each User could create multiple recipes. One to Many. And, each user could also create multiple ingredients.
Relational databases has specific terms to describe the relationship between different tables.
Here’s an article on database tutorials from dotnettutorials.
One to Many
is the most common database relationship. For example, in my shopping list app, one user has a relationship to many items. Of course, there is also an inverse relationship where certain items belong to only one user.
In Laravel eloquent, a one to many
is called HasMany. And the inverse relationship where the many items belonging to one user is called belongsTo.
Once these relationships are defined, Eloquent makes it easier to retrieve the appropriate records.
This is how I first created the store function in my shopping app:
public function store(Request $request)
{
$validated = $request->validate([
'name' => ['required', 'max:20'],
]);
//creates an object item from the Class Item
$item = new Item;
//name is the property of $item and its value is assigned from the request
$item->name = $request->name;
// Stores id of user into user_id
$item->user_id = Auth::id();
$item->save();
return Redirect::route('items.index');
}
Instead, in the bootcamp tutorial, it is recommended to use relationships and the function is written like this:
public function store(Request $request)
{
//
$validated = $request->validate([
'name' => 'required|string|max:25',
]);
$request->user()->items()->create($validated);
return redirect(route('items.index'));
}
The first and last statements are basically the same as in the store function I created.
It’s the middle part that is totally different.
In the bootcamp version, the middle statement is creating a record that will belong to the logged in user by leveraging an items relationship.
In the below code, items()
is a hasMany method that is created in the User
Model at app/Models/User.php
.
<?php
...
class User extends Authenticatable
{
public function items()
{
return $this->hasMany(Item::class);
}
This items()
method defines a hasMany relationship.
The first argument passed to the hasMany method is the name of the related model class (i.e. - Item::class).
Once the relationship is defined, Eloquent will automatically determine the proper foreign key column for the Item model.
By convention, Eloquent will take the “snake case” name of the parent model (User here) and suffix it with _id. So, in this example, Eloquent will assume the foreign key column on the Item model is user_id.
This will allow Eloquent to find all of the user’s items.
Then to find the user from the item, the inverse relationship must also be created. It is called belongsTo.
<?php
...
class Item extends Model
{
...
public function user()
{
return $this->belongsTo(User::class);
}
}
Once these relations are created, the store function works likes this:
Public function store(Request $request)
{
//
$validated = $request->validate([
'name' => 'required|string|max:25',
]);
$request->user()->items()->create($validated);
return redirect(route('items.index'));
}
And the index function in Item Controller can be written like this:
public function index()
{
$user = Auth::user();
$items = Item::whereBelongsTo($user)->latest()->get();
return Inertia::render('Index', ['items' => $items]);
}
Before using relationships, index was written like this:
public function index()
{
$items = Item::all()->sortByDesc('id')->values();
return Inertia::render('Index', ['items' => $items]);
}
Recipe App Mysql Table Relationships
MYSQL Tables
Users Table
User-ID | User-Name |
---|---|
1 | Sam |
2 | Sadie |
Recipes Table
Recipe-ID | Recipe-Name | Foreign-Key User-ID |
---|---|---|
1 | Pizza | 2 |
2 | Bread | 1 |
3 | Tortilla | 2 |
4 | Rolls | 1 |
Ingredients Table
Ingredient-ID | Ingredient-Name | Foreign-Key Recipe-ID |
---|---|---|
1 | flour | 1,2, 3 |
2 | water | 1,2, 3 |
3 | sugar | 1,2 |
Between Users and Recipes, there is one User with multiple recipes. This is a one to many relationship.
In Laravel eloquent, we call this hasMany.
It needs to be defined in the User Model (every table has to have a Model).
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class User extends Model
{
/**
* The recipes that belong to the User.
*/
public function recipes()
{
return $this->hasMany(Recipe::class);
}
}
When you have a one to many, you also have the inverse relationship of many to one. In Laravel, we have belongsTo.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Recipe extends Model
{
/**
* The users that belong to a Recipe.
*/
public function users()
{
return $this->belongsTo(User::class);
}
}
Many To Many Relationship
Unlike “Users to Recipes” (One To Many) , with “Recipes to Ingredients,” we have a “Many To Many” Relationship.
Each recipe will have many ingredients. And each ingredient will have many recipes.
Recipes Table
Recipe-ID | Recipe-Name | Foreign-Key User-ID | Foreign-Key Ingredient-ID |
---|---|---|---|
1 | Pizza | 2 | 1, 2, 3 |
2 | Bread | 1 | 1, 2, 3 |
3 | Tortilla | 2 | 1,2 |
4 | Rolls | 1 | 1, 2, 3 |
Ingredients Table
Ingredient-ID | Ingredient-Name | Foreign-Key Recipe-ID |
---|---|---|
1 | flour | 1,2, 3, 4 |
2 | water | 1, 2, 3, 4 |
3 | sugar | 1, 2, 4 |
But you can’t define a multiple values in a row of a column. So, you can’t have multiple values in “Foreign-Key Ingredient-Id” or “Foreign-Key Recipe-Id.”
Instead, to define this many-to-many relationship, three (3) database tables are needed: “ingredients,” “recipes,” and “ingredient_recipe.”
The “ingredient_recipe table” is derived from the alphabetical order of the related model names and contains “ingredient_id” and “recipe_id columns.” This table is used as an intermediate table linking the ingredients and recipes.
The relationship’s table structure is:
ingredients | |
---|---|
id | integer |
name | string |
recipes | |
---|---|
id | integer |
name | string |
ingredient_recipe | |
---|---|
ingredient_id | integer |
recipe_id | integer |
As pointed out in Laravel docs, Many-to-many relationships are defined by writing a method that returns the result of the belongsToMany method.
The belongsToMany method is provided by the Illuminate\Database\Eloquent\Model base class that is used by all of your application’s Eloquent models.
For example, below is a “ingredients” belongstomany method on the “Recipe model”. The first argument passed to this method is the name of the related model class, “Ingredient.”
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Recipe extends Model
{
/**
* The ingredients that belong to the recipe.
*/
public function ingredients()
{
return $this->belongsToMany(Ingredient::class);
}
}
Once the relationship is defined, you can access the Recipe’s ingredients using the “ingredients” relationship property.
use App\Models\Recipe;
$recipe = Recipe::find(1);
foreach ($recipe->ingredients as $ingredient) {
//
}
And you can access the Ingredients’ recipes by first creating an inverse belongstoMany “recipes” function in the “Ingredient” Model.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Ingredient extends Model
{
/**
* The recipes that belong to the ingredient.
*/
public function recipes()
{
return $this->belongsToMany(Recipe::class);
}
}
The the recipes that contain known ingredient can be reached with:
use App\Models\Ingredient;
$ingredient = Ingredient::find(1);
foreach ($ingredient->recipes as $recipe) {
//
}
Today, there was a lot of publicity about Chatgpt.
I asked the ai bot to explain and provide example code for “many to many” in laravel.
This is how it responded:
In Laravel, a "many to many" relationship refers to a relationship between two models where one model can have many instances of the other model, and vice versa. For example, a blog post may have many tags, and a tag may be used in many blog posts. In a many to many relationship, a join table is used to connect the two models, with a foreign key for each model in the join table. This allows you to easily retrieve all of the related records for a given model instance, and also to attach or detach related records as needed.
Here is an example of how you might create a many to many relationship and use a join table in Laravel:
First, you would define the relationship between the two models in their respective classes. For example, if you have a Post model and a Tag model, you might define the relationship like this in the Post model class:
public function tags()
{
return $this->belongsToMany('App\Tag');
}
And in the Tag model class, you would define the relationship like this:
public function posts()
{
return $this->belongsToMany('App\Post');
}
Next, you would create a migration to create the join table in the database. The join table will typically have a name that is the combination of the two model names, in alphabetical order, and the suffix _pivot. So, in this case, the join table would be named post_tag_pivot. You would create the join table like this:
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreatePostTagPivotTable extends Migration
{
public function up()
{
Schema::create('post_tag_pivot', function (Blueprint $table) {
$table->unsignedBigInteger('post_id');
$table->unsignedBigInteger('tag_id');
$table->foreign('post_id')->references('id')->on('posts')->onDelete('cascade');
$table->foreign('tag_id')->references('id')->on('tags')->onDelete('cascade');
});
}
public function down()
{
Schema::dropIfExists('post_tag_pivot');
}
}
Once you have defined the relationship and created the join table, you can use Eloquent methods to attach and detach related records, as well as retrieve related records. For example, to attach a tag to a post, you could do this:
$post = App\Post::find(1);
$tag = App\Tag::find(2);
$post->tags()->attach($tag);
To retrieve all of the tags for a given post, you could do this:
$post = App\Post::find(1);
$tags = $post->tags;
And to detach a tag from a post, you could do this:
$post = App\Post::find(1);
$tag = App\Tag::find(2);
$post->tags()->detach($tag);
I hope this helps! Let me know if you have any other questions.
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Recipe extends Model { /** * The ingredients that belong to the Recipe. */ public function ingredients() { return $this->belongsToMany(Ingredient::class); } }
use App\Models\Recipe;
$recipe = Recipe::find(1);
foreach ($recipe->ingredients as $ingredient) { // }
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Ingredient extends Model { /** * The recipes that belong to the ingredients. */ public function recipes() { return $this->belongsToMany(Recipe::class); } }
Retrieving Intermediate Columns Of many-to-many
use App\Models\Recipe;
$recipe = Recipe::find(1);
foreach ($recipe->ingredients as $ingredient) { echo $incredient->pivot->created_at; }
My Todo List
- Would like to learn how to use a lookup table so that a user can choose a store where the product can be found. In other words have a separate table named shops that’ll can be selected and added to the item. Totally useless. Maybe a pantry table where you select items that can be added to shopping list.
- Make a recipe app.
Background.
I run this genealogy website.
Unlike a typical genealogy website, my focus has switched from finding the “second cousin of my great-grandfather” to integrating world historical information to my family history. I currently use Webtrees which is a great open source genealogy application.
One tool that I think will work well for a history/genealogy website is a timeline. Webtrees has a way of adding historical facts to a timeline. And I have written a holocaust-timeline module to add facts to a webtrees timeline. But I want something more than what is offered by Webtreees.
My initial thoughts was to use VUE with Laravel. But I found an impressive timeline component that uses react. It’s called react-chrono and can be found on github.
Summary Of Process To Create A Laravel App With React
This summary uses “items” as the name of the example table. It will likely be different in your app.
- Create MYSQL Database.
Mysql
Create database name_db
- Create Laravel Project
composer create-project laravel/laravel --prefer-dist laravel_name_app
- Add database info to .env file in root of app
vim .env
- Install the authentication package into the laravel app.
composer require laravel/breeze --dev
- Install React
php artisan breeze:install react
- Need to Run NPM
npm install && npm run dev
- Start Server
php artisan serve
- Make Table Name - Singular - Capital Letter
php artisan make:model Item --migration
- Add Schema To Migration File
- Add fillable properties to Model. Must be done if create method is to be used.
- Make Table with columns.
php artisan migrate
- Add Controller.
php artisan make:controller ItemController --resource
. - Add under namespace to Controller just made.
use Inertia\Inertia;
-
Add
use App\Http\Controllers\ItemController;
toroutes/web.php
. - Store images in
storage/app/public
. Create symbolic link by runningphp artisan storage:link
. Then images can be reached by:storage/image.jpg
- EXAMPLE -<img src="storage/example-image.jpg">
Building a Shopping List With Laravel, Inertia, Vite, React, and MYSQL
Here’s my app to make a shopping list. It’s a work-in-progress.
First:
composer create-project laravel/laravel --prefer-dist react1-app
Second: Create a MYSQL Database
Tip: To not have to use a password with mysql, create a filed called .my.cnf in the user ~ directory. Add the following to this file and substitute with the real name/password of the admin database user. You also need to have [client].
[client]
user=admin_user_name
password=admin_user_password.
Now, to create a database for the app you first have to call up the mysql console.
mysql
Then, you create the database by:
create database shopping1_db;
exit;
cd react1_app (change into the larval app directory)
Edit the .env file and add in the database name, user, password.
vim .env
To install the authentication package into the laravel app:
composer require laravel/breeze --dev
To install react:
php artisan breeze:install react
Then:
npm install && npm run dev
To start the server, make sure you’re in the app root directory:
php artisan serve
At this point, a generic app will be running.
A helpful app to install is the laravel debug bar.
composer require barryvdh/laravel-debugbar --dev
If the debug bar is not working, then check that in the .env file, app_debug=true.
At this point, there’s a database that’s connected to Laravel. But the database is empty. To make the tables that need to be included for the authentication app:
php artisan migrate
Once this runs, the database with have tables to create a user, password, etc.
For the shopping list app, I’ll also need a table for the items.
Larval has a neat way to create tables, it’s called migrations. For my app, the items table migration file needs to be created first:
php artisan make:migration create_items_table
Or
php artisan make:model Item --migration
The above command will create a Model and migration file at the same time.
php artisan make:model Item -mcr
This command will hit the jackpot and make the Model, migration and controller at the same time.
There’s more explanation about Models and Controllers below.
Switch to the migrations folder that is stored in the database folder.
cd database/migrations
In that folder, there will be a new filed called something like 2022_08_13_005708_create_items_table.php.
In this file, a schema is automatically added that will eventually cause columns to be inserted into the table.
Schema::create('items', function (Blueprint $table) {
$table->id();
$table->timestamps();
});
And so, there are two columns that are automatically included:
id() - which is the primary key
timestamps() - which will cause two columns to be made in the table - "created_at" and "updated_at"
We’re going to have to add at least three more columns in the items table for the shopping list app.
$table->string('name'); (which is going to be the name of the shopping item - "bread" for example)
And these two statements are important:
$table->bigInteger('user_id')->nullable()->unsigned();
$table->foreign('user_id')->references('id')->on('users');
The purpose of these statements is to create a column called user_id that’ll store the id of the user from the users table.
This way when you’re later going to read the data, you can limit the shopping list items shown to the user who’s signed in. In other words, my friend doesn’t need to the see all the crap I buy.
To create the tables with these columns in the database, run:
php artisan migrate
And now, the table items
has been created with following columns;
So, id
is the primary key for each name/item that’ll be in the shopping list.
created_at
and updated_at
are self-explanatory.
name
is the specific item that’ll be in the shopping list.
user_id
is the specific user who is logged in.
By the way, the command to add a column to an existing table is:
php artisan make:migration add_ColumnName_to_TableName --table="name-of-existing-table"
Example where I want to add a “description” column to the table “items”:
php artisan make:migration add_description_to_items --table="items"
MODEL
Laravel has an MVC architecture.
MVC stands for Model-View-Controller.
Model is basically the backend end. View is the frontend. Controller is the way to interact between the Model and the Frontend.
Model creates a class with the name of the Model. That class is then used in controllers to create functions.
Every table in the database has to have a model. To make a model, use the following command. Here the model name is Item. Laravel uses this naming convention where when you make the Item Model, it will assume that it connects to the ‘items’ table.
php artisan make:model Item
The command to create a migration file can be combined with this command as follows:
php artisan make:model Item --migration
This would create the migration file date_create_items_table
and the model file Item.php
.
php artisan make:model Item -mcr
The above command will create a Model, migration, and controller file at the same time.
Make sure to add ‘use App\Models\Item;’ to the ItemController.php
.
And then when you run php artisan migration
, it will create the table named “items”, the model “Item.php”, and “ItemController”.
ROUTING
To see all registered routes use the following command:
php artisan route:list
Now that I have the database structure completed, let me talk about routing which is the way the app will know what to return when the user goes to a certain web address.
There’s a folder called routes. In that folder, there’s a file called web.php.
The routes/web.php
file defines routes that are for your web interface. These routes are assigned the web middleware group, which provides features like session state and CSRF protection. Laravel Routing
Any controllers that are added will have to be added to web.php
with a use
statement.
use App\Http\Controllers\NameController;
At this point, web.php
looks like this.
<?php
use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use App\Http\Controllers\ItemController;
/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/
Route::get('/', function () {
return Inertia::render('Welcome', [
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
'laravelVersion' => Application::VERSION,
'phpVersion' => PHP_VERSION,
]);
});
Route::get('/dashboard', function () {
return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
require __DIR__.'/auth.php';
On the top of this file, Illuminate
is Laravel’s code, and Inertia
connects Laravel to REACT.
Laravel routes use this general format:
Route::get(URI, function() {
})'
The ‘URI - Uniform Resource Identifier’ is the page name. Here '/'
represents the home page. If it was '/greeting'
then it would refer to http://website.com/greeting
.
The function()
gives the behavior that should be followed. An easy example from Laravel is:
Route::get('/greeting', function () {
return 'Hello World';
});
So, if you went to http://website.com/greeting
, you’d see Hello World
.
Our routing function is slightly more complex:
First Route
function () {
return Inertia::render('Welcome', [
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register'),
'laravelVersion' => Application::VERSION,
'phpVersion' => PHP_VERSION,
]);
}
Since this function includes Inertia, the Welcome
file that is rendered is contained in resources/js/Pages/Welcome.jsx
. If Inertia was not included in the app, then the default templating blades, resources/views/welcome.blade.php
, would be called instead.
Anyway, the above function instructs the app to return the javascript component (Welcome.jsx) and the props (properties) that are contained between the brackets [ ]. Those props are passed to the rendered page.
And so the function has to be read in combination with resources/js/Pages/Welcome.jsx
.
In that file, there is the following script at the top:
<script setup>
import { Head, Link } from '@inertiajs/inertia-vue3';
defineProps({
canLogin: Boolean,
canRegister: Boolean,
laravelVersion: String,
phpVersion: String,
})
</script>
Focusing on the defineProps portion, we see that in Welcome.jsx, four properties are defined.
The first two variables are “Boolean” (True or False) and the second two are “String” (text string).
Those variables get their values from the routing function.
'canLogin' => Route::has('login'),
'canRegister' => Route::has('register')
Route::has(‘login’) and Route::has(‘register’) are checks to see if these Route actually exist. If they do, “True” is returned. When Breeze Authentication was installed, these particular routes were added. So, the value for ‘canLogin’ and ‘canRegister’ are both true.
'laravelVersion' => Application::VERSION,
'phpVersion' => PHP_VERSION,
The above two variables just assigned the string value of the application and php version.
Now, when we look through the rest of resources/js/Pages/Welcome.jsx
, these values will be passed IF they appear.
But, in Welcome.jsx
, the props of canLogin
and canRegister
do not appear. Instead, this react version of the page uses a conditional ternary operator.
The conditional (ternary) operator is the only JavaScript operator that takes three operands: a condition followed by a question mark (?), then an expression to execute if the condition is truthy followed by a colon (:), and finally the expression to execute if the condition is falsy. This operator is frequently used as an alternative to an if…else statement.
<div className="fixed top-0 right-0 px-6 py-4 sm:block">
{props.auth.user ? (
<Link href={route('dashboard')} className="text-sm text-gray-700 dark:text-gray-500 underline">
Dashboard
</Link>
) : (
<>
<Link href={route('login')} className="text-sm text-gray-700 dark:text-gray-500 underline">
Log in
</Link>
<Link
href={route('register')}
className="ml-4 text-sm text-gray-700 dark:text-gray-500 underline"
>
Register
</Link>
</>
)}
</div>
An so when we look at the top of resources/js/Pages/Welcome.jsx
, we see that there is a conditional statement in the form of ? :
.
Basically, if props.auth.user is true (i.e.- user is logged in), then the first link ‘dashboard’ is shown. If props.auth.usr is false (user is not logged in), then the links after :
will be shown (‘login’ and ‘register’).
In the Vue version of this app, resources/js/Pages/Welcome.vue
, the top of the file is written as:
<div class="relative flex items-top justify-center min-h-screen bg-gray-100 dark:bg-gray-900 sm:items-center sm:pt-0">
<div v-if="canLogin" class="hidden fixed top-0 right-0 px-6 py-4 sm:block">
<Link v-if="$page.props.auth.user" :href="route('dashboard')" class="text-sm text-gray-700 dark:text-gray-500 underline">Dashboard</Link>
<template v-else>
<Link :href="route('login')" class="text-sm text-gray-700 dark:text-gray-500 underline">Log in</Link>
<Link v-if="canRegister" :href="route('register')" class="ml-4 text-sm text-gray-700 dark:text-gray-500 underline">Register</Link>
</template>
</div>
Here caLogin and canRegister do appear after v-if statements.
The directive v-if is used to conditionally render a block. The block will only be rendered if the directive’s expression returns a truthy value.
So, in the vue version, the same results as in the react version, but using different terminology.
Okay, let me get back to the routes in web.php
.
Second Route in web.php
Route::get('/dashboard', function () {
return Inertia::render('Dashboard');
})->middleware(['auth', 'verified'])->name('dashboard');
Here we see that if you go to http://website.com/dashboard
another function with Inertia is returned.
Here resources/js/Pages/Dashboard.vue
will be rendered but only if the user is logged in.
Laravel includes a middleware that verifies the user of your application is authenticated. If the user is not authenticated, the middleware will redirect the user to your application’s login screen. However, if the user is authenticated, the middleware will allow the request to proceed further into the application. See Laravel
Third Route in web.php
require __DIR__.'/auth.php';
This includes all the routes needed for its authorization software and is located at routes/auth.php.
Okay, I kind of know a little about routing, did the columns in the database (ie - migrations), guess it’s time for Controllers.
Controllers
Controllers work together with routing. They are stored at app/Http/Controllers
.
The generic controller at app/Http/Controllers/Controller.php
doesn’t explain very much.
The laravel documentation states:
Controllers are not required to extend a base class. However, you will not have access to convenient features such as the middleware and authorize methods.
Guess, we need those features. Time to construct a controller for my items table
.
The naming convention for a controller is Upper Camel Case with Controller at the end and singular throughout. Since my tables is items, my controller will be:
php artisan make:controller ItemController --resource
When --resource
is added, the made controller will contain the typical create, read, update, and delete (“CRUD”) routes.
And after running the above command, I now have this file:
app/Http/Controllers/ItemController.php
(As noted above, this ItemController.php has to be added to web.php
with use app/Http/Controllers/ItemController.php;
)
ItemController.php
has the following boilerplate stuff:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class ItemController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
//
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}
It now looks like I have to add the appropriate function into each of these sections.
But first, since this app is using inertia, at the top of this controller, use Inertia\Inertia;
must be added.
So, the top of ItemController.php
is now:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Inertia\Inertia;
class ItemController extends Controller
Another important thing is that when the bare-bones functions are created in the controller by php artisan make:controller ItemController --resource
, web.php
, has to include the routes to these functions. This is done in one line of code.
Route::resource('items', ItemController::class);
And if I want the route to be protected by authentication, I can just add the authentication middleware ->middleware('auth');
to the route in the web.php
file.
Route::resource('items', ItemController::class)->middleware('auth');
Once this code (with or without the middleware) is included, the following routes are created as shown in Laravel documentation. In Laravel’s chart, they were working with a controller named PhotoController. Here it’s ItemController. So just replace “photo” with “item”:
Table From Laravel Documentation
This chart has to be read in conjunction with ItemController.php
.
The seven items in the chart match the seven functions in the ItemController.
The next step will be to code the seven functions to match the chart’s actions.
A few of the functions, should just return a view (i.e.-:form) and the others a method to manipulate a table in the database.
Index
displays a listing - that should be a view of all the items on the shopping list.
Create
should be a view of a form that is used to create an item on the shopping listing. Basically, the name of the item to be inserted. Per documentation, “before using the create method, you will need to specify either a fillable or guarded property on your model class. These properties are required because all Eloquent models are protected against mass assignment vulnerabilities by default.” Mass assignment can be enable for safe attributes by marking them as “fillable”.
Store
should be a method of storing the form called in Create. Store should be the way to take the name from the create form and store it into the name column in the items table.
Show
should be the specified resource. Unlike index, it is only one item.
Edit
should show the form that is used to edit the name of an existing item.
Update
should take the resource from edit and modify it.
Destroy
should be a way to delete the specific resource.
The first function to fill in is a
Make sure to add ‘use App\Models\Item;’ to the ItemController.php
.
The second function is to return a route to the form that’ll create a shopping list item. But only logged in users should be able to create an item. So, I should just modify the Second Route from above.
public function create()
{
$item = Item::create([
'name' => $request->name,
]);
return Inertia::render('Create');
})->middleware(['auth', 'verified'])->name('create');
}
Controllers, Functions, Methods, Classes, Oh my!
This concept of adding methods/functions to controller is hard for me to get my head around.
Web.php
has some routes. I understand that when a person goes on the web ‘/”, the code at /pages/Welcome.jsx
is rendered. It’s that concept of passing props/variable to the web page that is confusing to me. I’m sure most of it is because I don’t have the underlying knowledge required. But it is what it is.
The shorthand of Route::resource('items', ItemController::class)->middleware('auth');
is impressive. This idea that with this one line of code/boilerplate, numerous routes are created that are protected via authentication.
But I’m confused by this chart in Laravel documentation.
Verb | URI | Action | Route Name |
---|---|---|---|
GET | /items | index | items.index |
GET | /items/create | create | items.create |
POST | /items | store | items.store |
GET | /items/{item} | show | items.show |
GET | /items/{item}/edit | edit | items.edit |
PUT/PATCH | /items/{item} | update | items.update |
DELETE | /items/{item} | destroy | items.destroy |
I understand that GET is telling us to return views for index, create, show, and edit. I assume that create and edit will be forms to create and edit. But what’s the difference between index and show. And why does post, put, and delete have separate routes.
After looking at stuff for a couple of weeks, it looks like the purpose of these different routes are to allow different functions in the controller. Namely, every action is attached to a different function in the controller.
Difference between show and index.
According to a discussion here, index will show all the entries in a table and show will only show one item.
That’s helpful.
So, for the first function index, this might work:
public function index()
{
$items = Item::all();
return Inertia::render('Index', compact('name'));
}
According to laravel, the model’s all()
method will retrieve all of the records from the model’s associated database table.
And the compact() function creates an array from variables and their values.
For my shopping list app, I envision:
Get - index page - which would be a list of the items. And next to each item would be a button to delete. There could also be a button to update. And a button to add an item that would take you to create.
Overall View Of Laravel/Vite/Inertia/React
In July 2022, Laravel started using vite rather than mixjs to bundle frontend assets.
So, I guess I need to learn how Vite works. Here’s Laravel’s documentation on Vite.
Summary of Laravel Vite Documentation
When building applications with Laravel, you will typically use Vite to bundle your application’s CSS and JavaScript files into production ready assets.
Within a fresh installation of Laravel, you will find a package.json file in the root of your application’s directory structure. The default package.json file already includes everything you need to get started using Vite and the Laravel plugin. You may install your application’s frontend dependencies via NPM:
npm install [frontend dependency]
This is what the package.json
file in my app looks like now.
This gives me a better idea of what vite does. It seems like I shouldn’t need to touch the package.json
file which is controlled by Laravel and npm.
There’s an additional vite configuration file that can be used according to vite.
Vite is configured via a vite.config.js file in the root of your project. You are free to customize this file based on your needs, and you may also install any other plugins your application requires, such as @vitejs/plugin-vue or @vitejs/plugin-react.
If you are building an SPA (Single page Application), including applications built using Inertia, Vite works best without CSS entry points.
In other words, don’t add css into vite.config.js
. Instead, import the css into ‘resources/js/app.js’ like this:
import './bootstrap';
import '../css/app.css';
By the way, vite.config.js
is found in the root directory (not in the folder called app).
Adding An Email SMTP Server To Laravel App
In the .env file, you adjust the email info. Before doing that, you should stop the server.
php artisan down
Then go to the .env
and put in the your values.
MAIL_MAILER=smtp
MAIL_HOST=smtp.your_smtp_server.com
MAIL_PORT=465
MAIL_USERNAME=support@your_website.com
MAIL_PASSWORD=password
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="support@your_website.com"
MAIL_FROM_NAME="${APP_NAME}"
After making the changes, run:
php artisan config:cache
Restart the server.
php artisan up
From atymic.dev, this is how to test the email:
php artisan tinker
Mail::raw('Hello World!', function($msg) {$msg->to('test_email_recipient@gmail.com')->subject('Test Email'); });
When I tried this, I first got an error that the sender could not be authenticated.
After I ran php artisan config:cache
, it worked.
Different App Bars (Navbars) Depending On User
I wanted to have different menus depending on whether the user is logged in or not.
When the user is logged out, I want links to “Register” or “Login.”
Those links don’t apply, if the user is logged in.
To do this, I used the javascript conditional operator that is also called the ternary operator.
The general format is condition ? true result : false result
.
Below, the condition checks if the user is authorized. If true, one appbar is used. If false, the LoggedOutBar is used.
<div >
{props.auth.user ?
<DrawerAppBar />
:
<LoggedOutBar />
}
</div>
Centering the Registration Form
The Registration Form had a big Laravel logo which I didn’t like. After I deleted the logo, it left a large gap on the top of the screen that looked ugly.
To correct this, I edited a line of code in resources/js/Layouts/Guest.jsx
.
Before the edit:
<div className="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100">
I deleted min-h-screen
and bg-gray-100
:
<div className="flex flex-col sm:justify-center items-center pt-6 sm:pt-0">
It now looks much better.
Adding “Need To Register?” Link
The default login form that comes with Laravel did not have the “Need To Register?” line.
I believe the login form works better with it.
To get this link, I added to the “Login page” at resources/js/Pages/Auth/Login.jsx
the following code:
<div className="flex items-center justify-end mt-4">
<Link
href={route('register')}
className="underline text-sm text-gray-600 hover:text-gray-900"
>
Need To Register?
</Link>
</div>
React Curley Braces
Using { } in jsx allows you to add javascript into the html markup.
Below is an example from React Beta Docs.
const today = new Date();
function formatDate(date) {
return new Intl.DateTimeFormat(
'en-US',
{ weekday: 'long' }
).format(date);
}
export default function TodoList() {
return (
<h1>To Do List for {formatDate(today)}</h1>
);
}
Above today
is assigned the current date and time through the javascript constructor.
The formatDate
function takes the date and provides the day of the week.
The export default function has {formatDate(today)}
within <h1> tags.
(today)
calls new Date()
and that date is formatted.
You get To Do List for Friday (This assumes the function is called (instantiated) on a Friday.
{{ }} -Double Curlies are used to pass a JS object in JSX. This is oftentimes seen with inline CSS styles in JSX.
<ul style={{
backgroundColor: 'black',
color: 'pink'
}}>
Adding A Snackbar To The App
The way my shopping app works, an item is added to the top of the list. That makes it easy to see if the app works.
When an item is deleted, it’s sometimes hard to see if it’s actually be deleted. To overcome this issue, I decided to add a snackbar that would momentarily popup “Item Deleted” notification.
Basically, the “Item Deleted” pops up on the bottom of the screen when the delete button is pressed.
To get this functionality, I added snackbar code to `Index.jsx’.
import React from 'react';
import ScopedCssBaseline from '@mui/material/ScopedCssBaseline';
import DrawerAppBarLoggedIn from './Components/DrawerAppBarLoggedIn';
import DrawerAppBar4a from './Components/DrawerAppBarLoggedIn';
import { Link, usePage, Head } from '@inertiajs/inertia-react';
import ItemCreate from './Components/ItemCreate';
import Paper from '@mui/material/Paper';
import Grid from '@mui/material/Grid';
import Container from '@mui/material/Container';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Button from '@mui/material/Button';
import Snackbar from '@mui/material/Snackbar';
export default function shoppingList(props) {
const [open, setOpen] = React.useState(false);
const handleClick = () => {
setOpen(true);
};
const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
setOpen(false);
};
return (
<>
<ScopedCssBaseline>
<Head title="Sympile Shopping List" />
<div >
{props.auth.user ? (
<DrawerAppBarLoggedIn />
) : (
<>
<DrawerAppBar4></DrawerAppBar4>
</>
)}
</div>
<Container maxWidth="sm">
<Grid container justifyContent="center">
<ItemCreate></ItemCreate>
</Grid>
<br></br>
<Grid container justifyContent="center">
<TableContainer component={Paper}>
<Table sx={{ minWidth: 350 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>Shopping List</TableCell>
</TableRow>
</TableHead>
<TableBody>
{props.items.map((item, index) => (
<TableRow
key={item.id}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
data-index={index}>
<TableCell>{item.name}</TableCell>
<TableCell align="left">
<Button variant="outlined" onClick={handleClick}><Link href={route('items.destroy', item.id)} method="delete" as="button" type="button" >Delete</Link></Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Grid>
</Container>
</ScopedCssBaseline>
<Snackbar
open={open}
autoHideDuration={800}
onClose={handleClose}
message="Item Deleted"
/>
</>
);
}
Explanation of Snackbar Code
On the delete Button, the following onclick code
was added.
<Button variant="outlined" onClick={handleClick}><Link href={route('items.destroy', item.id)} method="delete" as="button" type="button" >Delete</Link></Button>
Specifically, onClick={handleClick}
calls a javascript function handleClick`.
handleClick
is defined as follows:
const handleClick = () => {
setOpen(true);
};
handleClick makes setOpen(true)
.
const [open, setOpen] = React.useState(false);
useState
is a React Hook that lets you add React state to function components.
In my app, useState
appears like this.
const [open, setOpen] = React.useState(false);
A variable open
is declared, and its value is set to false
.
To change the value of open
,setOpen
is used.
Which brings us back to the function handleClick
which calls setOpen(true)
and updates open
to true
.
Button
contains onClick={handleClick}
.
So, when the button is clicked, the variable open
is set to true
by calling the function handleClick
.
And snackbar contains:
<Snackbar
open={open}
autoHideDuration={800}
onClose={handleClose}
message="Item Deleted"
/>
The Snackbar “open” property calls the javascript “open” variable.
In other words, when the button is clicked, “open” gets set to “true” and the snackbar is opened.
This only took me about three days to figure out.
HTTPS Not Working Until I Changed An AWS Setting
Everytime I think I’ve turned the corner, I smash my head into a wall. This time it was moving my site from http to https.
I used certbot like I’ve done a hundred times before. It has always worked with no issues.
This time when I wanted to move from http to https I get unable to connect.
At first, I thought it was because I forgot to switch the field in .env
from http to https.
APP_URL=https://shoppinglist.sympile.com
But, I switched it, cleared the cache, and it still didn’t work.
I then wasted another three to four hours until I found the problem.
The site is hosted on AWS. When the server is setup, there are some default firewall settings that stop ssl traffic (443) from connecting to the server.
All I needed to do was allow 443 traffic through and the problem was solved.
What a waste of time.
There are two virtual host files:
First, Unsecured.conf
<VirtualHost *:80>
ServerName servername.com
ServerAdmin webmaster@servername.com
DocumentRoot /var/www/html/shoppinglist/public
<Directory /var/www/html/shoppinglist/public>
Options Indexes FollowSymLinks
AllowOverride All
Require all granted
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
RewriteEngine on
RewriteCond %{SERVER_NAME} =shoppinglist.sympile.com
RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>
Second, Secured.conf
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName servername.com
ServerAdmin webmaster@servername.com
DocumentRoot /var/www/html/shoppinglist/public
<Directory /var/www/html/shoppinglist/public>
AllowOverride All
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
SSLCertificateFile /etc/letsencrypt/live/shoppinglist.servername.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/shoppinglist.servername.com/privkey.pem
Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>
Would have taken five minutes if only the firewall had permitted 443 traffic.
I’m such an idiot.
Deploying Laravel To A Remote Server On AWS
I have a somewhat working model of my shopping list app. I now want to deploy it to a server that is being hosted on AWS.
The server is using Apache2 and MYSQL.
There are some paid versions that would make it easier to deploy the app. But given this is a learning exercise, it’s time to figure out how to deploy for free.
At this point, I don’t know if it would be easier to deploy using docker or just github.
I was thinking of just figuring out a way of deploying using github. That way when I make changes locally, I see just pushing out the changes to the remote server. A software engineer suggested that I may want to use docker.
Should I Use Docker To Deploy A Laravel App
The only thing I know about Docker is that it’s some type of a container.
Laravel has Sail to interact with Docker.
Reading this Sail documentation is giving me a headache. I’m not sure if I have the patience to learn Sail to deploy a Laravel docker.
Here’s a tutorial by Gershan Manandhar that explains using Docker with Laravel. This tutorial is very impressive. It just that it recommends using Google Cloud Run to deploy the container. My server is on AWS. Not sure if I want to switch now to Google. I see that Amazon has Fargate. I think it’s a little too much for just my simple shopping app.
Docker is probably something I’ll learn in the future, I think I’m going to use Git to deploy my app.
Useful Git Commands
Commands to discard unstaged changes
git restore .
To discard unstaged changes for a specific file
git restore path/to/file/to/revert
Commands to discard all local changes that haven’t been committed:
-
git reset –hard
-
git clean -fxd
Switch branch
git switch name_of_branch
Delete branch
git branch -d name_of_branch
Force Delete Branch Where Branch Has Not been Merged (should roll back local changes first)
git branch -D name_of_branch
Using Git To Deploy Laravel App From A Private Git Repository
The first step is to clone a private repository is to connect to the remote server. For this example I’ll call the user - “user1” (very original).
ssh user1@ip_address
If you have not yet generated an ssh key for this user, you must do it now:
ssh-keygen
- this generates a ssh. Do not use sudo.
Then cd .ssh
- this will switch you into the hidden directory .ssh
cat id_rsa.pub
- will show contents of public key
copy the contents of the public key.
Switch to the repository in github that you want to clone, then go to settings -> Deploy keys
.
Click on the button Add deploy key
Give this key a title and paste in the contents of the public key for user1.
Go back to the remote server to the home directory of user1 - /home/user1
.
mkdir repository
(this will create a directory called repository) - do not use sudo. Because you are in you user’s home directory, you do not use sudo. Otherwise, the directory will belong to root and not to the actual user.
Remember, you did NOT use sudo
to create the user’s ssh key. If you did, you will need to delete the public and private keys using sudo
and then regenerate the keys without sudo
.
cd repository
- this is the directory where the github repository will be cloned.
The command to clone a repository is git clone git_address
.
For my project it is:
git clone git@github.com:murwell/shopping.git
To clone without the top level folder shopping
.
git clone git@github.com:murwell/shopping.git .
- Notice the period after the space.
Do not use sudo.
This cloned the project to \home\user1\repository
.
Done cloning.
Now, the cloned app is in \home\user1\repository\shopping
.
cd \home\user1\repository
- this switched me into the repository directory.
Since, sudo
was not used to create the directory, the ownership of the directory is user1:user1
.
Group permissions need to be worked out. This is something I still need to work out.
I gonna go back to the tutorial by Decode Web.
The tutorial is a few years old, so I don’t know what’s changed since then. Hopefully, nothing.
Here’s a different tutorial from Kenean that discusses how to automate a laravel app deployment using Github actions.
Got The Website Somewhat Running
I got the website somewhat running.
It was a disaster. Needed to delete files that had errors in them and needed to update node.js using nvm. Ubuntu comes with an old version of node.js. I didn’t know that. That old version caused lots of problems. Once I updated to node 18.12.1
with nvm, the app loaded fine.
To get it running, this tutorial from laravel daily helped.
These are my steps for getting laravel app running from github into a directory that is owned by user1 and getting stored in a directory called shoppinglist:
sudo mkdir /var/www/html/shoppinglist
sudo chown -R user1:user1 shoppinglist
cd /var/www/html/shoppinglist
git clone git@github.com:murwell/shopping.git .
- run inside /var/www/html/shopplinglist and without sudo
vim .env
- edit the .env file in the laravel app
composer install
- adds the dependencies
php artisan key:generate
- generates an application key and automatically adds it .env
php artisan migrate
- migrates the schema to the database
npm ci
npm run build
cd /var/www/html
sudo chown -R www-data:www-data shoppinglist
- changes ownership of app directory to www-data:www-data
Procedure To Update the App Where Git Is Used
Again, this tutorial from laravel daily helped.
cd /var/www/html/laravelapp
Changes into the laravel app directory
php artisan down
Shuts down laravel app for web users
cd ..
Moves into /var/www/html
sudo chown -R user1:user1 laravelapp
Changes ownership of laravelapp directory to user1. Change user1 to the actual name of the user. Also, this is cumbersome. I will probably add user1 to the www-data to stopping these steps.
cd laravelapp
Changes directory into laravel app
git pull
Pulls the changes from to Git
composer install
Adds in any new dependencies
If you have created some new migrations, this is the time to run php artisan migrate
npm run build
Rebuilds the app. The first time I had made changes, I forgot to do this step. Without running npm run build
, changes were not being seen by the web user.
cd ..
Changes directory up one level to /var/www/html
sudo chown -R www-data:www-data laravelapp
Changes ownership of laravelapp directory back to www-data
cd laravelapp
Changes directory back into laravelapp directory
php artisan up
Puts the laravel app back up
Laravel Commands That Are Needed
php artisan config:cache
will cache the .env configuration file.
php artisan down
for maintenance mode.
php artisan up
to stop maintenance.
php artisan config:clear
php artisan config:cache
php artisan route:clear
php artisan route:cache
php artisan view:clear
php artisan event:clear
php artisan cache:clear
- application cache cleared
php artisan optimize:clear
- clear all caches
Problems Noticed With Production App
After getting the app running on a web server, here are some problems I’ve noticed:
- After logging in, the app redirects the page to the dashboard. I needed to change that and instead wanted the app to redirect the user to the actual app.
In the AuthenticatedSessionController.php
, there is a line of code that redirects as follows:
return redirect()->intended(RouteServiceProvider::HOME);
Home
is defined in /app/Providers/RouteServiceProvider.php
.
In that file there is public const HOME = '/dashboard';
I changed that to public const HOME = '/items';
Now, when someone logs in, the user is redirected to the app’s shopping list.
This is one of those issues with Laravel that has made it difficult for me (besides my lack of knowledge). Code is sometimes hidden behind other code. Without digging to find the buried treasure, just looking at the top layer doesn’t explain what’s going on.
- If you’re inactive on a page, the default setting is to timeout the page for security purposes. There is a plugin that should take care of this problem. I am still testing it out.
The plugin is called laravel-caffeine and is from GeneaLabs. It can be found on Github here.
- I made some changes to a few pages locally. I pushed the changes to Git. I then went to the server and updated the app. Once the pages were update on the remote server, the pages were not changing to the user on the web. I tried clearing caches, nothing seemed to work.
I then realized, I needed to run npm run build
. That updated the app for the web user and all was good.
- The user gets signed out after some time. There are a couple of suggestions here that includes storing sessions in a table or extending the sessions lifetime value.
Initially, extening the sessions lifetime in app/config/session.php
is the easiest solution. I will try that first and edit the sessions lifetime from 120 to 2880 minutes (i.e. - two days).
php artisan down
cd app/config
sudo vim session.php
edit ‘lifetime’ => env(‘SESSION_LIFETIME’, 120 2880)
php artisan config:clear
php artisan up
Learning React
I’m having difficulty with the front end. I’ve got a lot things working, but one of the problems are the menus. The logout button is complicated for me. Rather than just plowing through, I’m going to spend some time learning react.
There is a beta tutorial from React that I am reviewing. It can be found here. This section is taken from the beta tutorial. It is much clearer than react’s previous tutorial.
First, just learning the lingo is helpful. In React, reference is repeatedly made to adding “markup” to jsx files. “Markup” is html - HyperText Markup Language. And so, the right way to look at React is that it allows you to add html (markup) to javascript. Markup are html elements like <h1>, <img>, <ul>
.
In React, rendering logic and markup are kept together in components. Each React component is a JavaScript function that may contain some markup that React renders into the browser.
The export default keywords specify the main component in the file.
To convert html to react, one can use a converter.
The general format of a component is:
export default function ComponentName() {
return (
);
}
A file can only have one default export, but it can have numerous named exports.
export function WhateverName() {
// ...
}
To import a non-default component, use curly braces.
import { NonDefaultName } from './ComponentName.js';
For default components, no curly braces.
import DefaultName from './DefaultComponentFile.js';
React component names must always start with a Capital letter, while HTML tags must be lowercase.
Component names should use the PascalName format - ComponentName.jsx
To return multiple elements from a component, wrap them with a single parent tag or with <> … </>.
<div>
<h1>Hedy Lamarr's Todos</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
class="photo"
>
<ul>
...
</ul>
</div>
OR if you don’t want to add an extra <div> to your markup, you can write <> and </> instead:
<>
<h1>Hedy Lamarr's Todos</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
class="photo"
>
<ul>
...
</ul>
</>
Root Component File
The root component file is the file that is returned by the route. So, in my Laravel shopping app, when the URI /items
route is called, the page *Index.jsx is the root component file.
Verb | URI | Action | Route Name | |
---|---|---|---|---|
GET | /items | index | items.index | |
GET | /items/create | create | items.create | |
POST | /items | store | items.store | |
GET | /items/{item} | show | items.show | |
GET | /items/{item}/edit | edit | items.edit | |
PUT/PATCH | /items/{item} | update | items.update | |
DELETE | /items/{item} | destroy | items.destroy |
Within the Root Component File, you would import other components, if needed.
If you use a framework with file-based routing, such as Laravel, your root component will be different for every page.
One thing that I noticed after looking at this for the millionth time is that I my have a flaw in my controller for create
.
It is written like this:
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
return Inertia::render('Create');
}
This is a security issue as there is no check for authentication on the create
page. Anyone would be able to add to the database.
My create page is:
import React from 'react'
import { useForm } from '@inertiajs/inertia-react'
export default function Create() {
const { data, setData, post, processing, errors } = useForm({
name: '',
})
function submit(e) {
e.preventDefault()
post('/items')
}
return (
<form onSubmit={submit}>
<input type="text" value={data.name} onChange={e => setData('name', e.target.value)} />
<button type="submit" disabled={processing}>Submit</button>
{errors.name && <div>{errors.name}</div>}
</form>
)
}
This means that if someone goes to /items/create
, they’ll be able to add to the database because the input box would be shown.
So, I changed the page to be rendered on create to the same code that is within the index function in ItemController.
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//get authenticated user who's making request via the Auth facade's user method
$user = Auth::user();
// gets the items that belongs to the authenticated user and sorts by latest
$items = Item::whereBelongsTo($user)->latest()->get();
//renders the index page and passes the prop 'items' that contains items attributes
return Inertia::render('Index', ['items' => $items]);
}
So far, it works. I pushed the change to Git.
JavaScript in JSX with Curly Braces
JSX lets you write HTML-like markup inside a JavaScript file, keeping rendering logic and content in the same place. Sometimes you will want to add a little JavaScript logic or reference a dynamic property inside that markup. In this situation, you can use curly braces in your JSX to open a window to JavaScript.
export default function TodoList() {
const name = 'Gregorio Y. Zara';
return (
<h1>{name}'s To Do List</h1>
);
}
Any JavaScript expression will work between curly braces.
Curly Brace Usage in Jsx
- As text directly inside a JSX tag: <h1>{name}’s To Do List</h1> works, but <{tag}>Gregorio Y. Zara’s To Do List</{tag}> will not.
- As attributes immediately following the = sign: src={avatar} will read the avatar variable, but src=”{avatar}” will pass the string “{avatar}”.
- To pass a JS object in JSX, you must wrap the object in another pair of curly braces.
person={{ name: "Hedy Lamarr", inventions: 5 }}
- To pass an inline style use a pair of curly braces. Inline style properties are written in camelCase rather than normal css. (i.e. - backgroundColor NOT background-color)
<ul style={{backgroundColor: 'black',color: 'pink'}}>
Conditional Rendering
<div>
{isLoggedIn ? (
<AdminPanel />
) : (
<LoginForm />
)}
</div>
I think this is key for me. In my app, I will try:
<div>
{props.auth.user ? (
<NavBarLoggedIn />
) : (
<LoginForm />
)}
</div>
No else branch
<div>
{props.auth.user && <NavBarLoggedIn />}
</div>
Rendering Lists
const products = [
{ title: 'Cabbage', isFruit: false, id: 1 },
{ title: 'Garlic', isFruit: false, id: 2 },
{ title: 'Apple', isFruit: true, id: 3 },
];
export default function ShoppingList() {
const listItems = products.map(product =>
<li
key={product.id}
style= {{
color: product.isFruit ? 'magenta' : 'darkgreen'
}}
>
{product.title}
</li>
);
Responding to Events
function MyButton() {
function handleClick() {
alert('You clicked me!');
}
return (
<button onClick={handleClick}>
Click me
</button>
);
}
Using State To Remember Some Info
import { useState } from 'react';
function MyButton() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<button onClick={handleClick}>
Clicked {count} times
</button>
);
}
**If you render the same component multiple times, each will get its own state. **
So, in the above example, if you call MyButton
will have its own state (value).
const [count, setCount) = useState(0);
useState
creates a value and a function.
const [something, setSomething) = useState(value);
The function name should be a combination of “set” with the name of the value - “setSomething”.
Using Hooks
Functions starting with use are called Hooks. useState is a built-in Hook provided by React. You can find other built-in Hooks in the React API reference. You can also write your own Hooks by combining the existing ones.
Hooks are more restrictive than regular functions. You can only call Hooks at the top level of your components (or other Hooks). If you want to use useState in a condition or a loop, extract a new component and put it there.
Passing Data With useState
import { useState } from 'react';
function MyButton({ count, onClick }) {
return (
<button onClick={onClick}>
Clicked {count} times
</button>
);
}
export default function MyApp() {
const [count, setCount] = useState(0);
function handleClick() {
setCount(count + 1);
}
return (
<div>
<h1>Counters that update together</h1>
<MyButton count={count} onClick={handleClick} />
<MyButton count={count} onClick={handleClick} />
</div>
);
}
Passing Props To Child Components in React
I am having difficulty with the concept of props passing data to child components.
One way that props are used is with components. React uses components to build out the web app. Rather than one large page, its divided into “components” that are used to construct the app. That way components can be reused.
The basic format of a component with an event handler in React is:
import * as React from 'react';
import Component from '/directory/Component';
import Component2 from '/directory/Component2;
export default function ComponentName() {
//event handler
function handleClick() {
alert('You clicked me!');
}
const nameName = [
' ',
]
const [
name,
name2,
]
return (
THIS IS WHAT YOU WANT RENDERED IN THE BROWSER
);
}
React components are regular JavaScript functions, but their names must start with a capital letter or it won’t work.
The const declaration creates block-scoped constants, much like variables declared using the let keyword. The value of a constant can’t be changed through reassignment (i.e. by using the assignment operator), and it can’t be redeclared (i.e. through a variable declaration). However, if a constant is an object or array its properties or items can be updated or removed.
React Beta docs explains passing props to components as follows:
React components use props to communicate with each other. Every parent component can pass some information to its child components by giving them props. Props might remind you of HTML attributes, but you can pass any JavaScript value through them, including objects, arrays, and functions.
React And CSS
In React, you specify a CSS class with className. It works the same way as HTML class attribute.
Use PascalCase for the actual className.
<img className="AvatarPicture" />
Write the CSS in a separate file:
/* In your CSS */
.AvatarPicture{
border-radius: 50%;
}
Adding Strikethrough On Text
This is from React Conditional Rendering
<del> tag will strikethrough text.
Use conditional rendering to strikethrough completed items rather than deleting altogether. This may be something that I can use in my shopping app.
function Item({ name, isPacked }) {
return (
<li className="item">
{isPacked ? (
<del>
{name + ' ✔'}
</del>
) : (
name
)}
</li>
);
}
export default function PackingList() {
return (
<section>
<h1>Sally Ride's Packing List</h1>
<ul>
<Item
isPacked={true}
name="Space suit"
/>
<Item
isPacked={true}
name="Helmet with a golden leaf"
/>
<Item
isPacked={false}
name="Photo of Tam"
/>
</ul>
</section>
);
}
Deploying A Laravel App To An Apache2 Server Running Ubuntu 20.04
I used this tutorial on LinuxOpsys.
My installation is with mysql. To get app running, after it is installed, run php artisan
.
Here is Laravel’s documents on deployment. Below are some of its points.
Important in a production environment, the value for APP_DEBUG in .env
must be set to FALSE.
When deploying your application to production, you should make sure that you run the config:cache Artisan command during your deployment process:
php artisan config:cache
If you execute the config:cache command during your deployment process, you should be sure that you are only calling the env function from within your configuration files. Once the configuration has been cached, the .env file will not be loaded and all calls to the env function for .env variables will return null.
I think I will need to learn how to use Github.
Learning Github
Github is a way to track changes to files. At this point, I’m hoping to use Github as follows:
- Develop Locally with VSCode.
- Push code to github
- Push code from github to server.
At this point, I have no idea what I’m doing.
Time for a Github Tutorial.
Github One Hypen Versus Hyphen
Before I begin, I want to point out that sometimes a github command uses one hyphen (-) and aometimes two hyphens (–).
One hyphen is used for abbreviated flags. Two hyphens are used for complete word flags.
Abbreviated flag
git add -a
Complete word flag
git add --all
Github Intro
So, I tried using the github extension in vscode and it was too confusing. Instead, I used the command line.
At this point, my laravel project was contained on my local computer. Let’s say it’s in /home/laravel_app
.
With github, I want to track different versions. To do that,Github uses a concept called repository.
A repository contains all of your project’s files and each file’s revision history. You can discuss and manage your project’s work within the repository.
So, to get it working a little, I created an empty repository on github that is called shopping.
Then I’ll push files from /home/laravel_app
to the repository on github. The address to use to push files to Github is:
https://github.com/user_name/repository_name.git`
So for me, it will be:
https://github.com/murwell/shopping.git
Local Github Repository
Now, I have to set up a repository on my local computer. This is done in the command line.
Change into the folder of where the laravel is stored:
cd /home/laravelapp
Laravel already contains a file in root folder called .gitignore
. The purpose of this file to tell git to omit certain files and folders so that they do not get pushed/pulled to github. For example, .env
is included in .gitignore
because it contains sensitive info.
This is the contents of laravel.gitignore
.
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.idea
/.vscode
After making sure that you app folder has a .gitignore
folder, run the following command while in /home/laravel_app
folder to create a hidden folder called git
:
git init
After you run git init, it’ll ask you if you want to rename master
. You should because the main branch on github is called main
.
The local branch should be renamed to “main” with:
git branch -M main
With a -m or -M option, oldbranch will be renamed to newbranch. If oldbranch had a corresponding reflog (reference log], it is renamed to match newbranch, and a reflog entry is created to remember the branch renaming. If newbranch exists, -M must be used to force the rename to happen.
Then run the following commands and substitute your name and email address between the quotation marks (“ “).
git config --global user.name "user_name"
git config --global user.email "user@email.com"
This will add your name and email so that if you’re working with someone, you can see who made the changes.
Now, you have to add the local files to your local repository. Since you are in the folder of the laravel app, run:
git add --all
This will add all the files and folders in the directory to the local repository. It will ignore the files and folders listed in .gitignore
.
These files are added but sill need to be committed. Commit will not work unless a file has been added prior to running it. (i.e. -git add <FILENAME>
)
This process of adding a file is called staging. Commit comes after stage (add).
git commit creates a commit, which is like a snapshot of your repository. These commits are snapshots of your entire repository at specific times. You should make new commits often, based around logical units of change. Over time, commits should tell a story of the history of your repository and how it came to be the way that it currently is. Commits include lots of metadata in addition to the contents and message, like the author, timestamp, and more. Commits are the building blocks of “save points” within Git’s version control.
git commit -m "Committing initial files to the repository"
The flag -m
indicates that you’re going to add a message to the commit so you can explain why you’re taking the snapshot.
Common Commit Commands
git commit
This starts the commit process, but since it doesn’t include a -m flag for the message, your default text editor will be opened for you to create the commit message. If you haven’t configured anything, there’s a good chance this will be VI or Vim. (To get out, press esc, then :w, and then Enter. :wink:)
git commit -m "descriptive commit message"
This starts the commit process, and allows you to include the commit message at the same time.
git commit -am "descriptive commit message"
In addition to including the commit message, this option allows you to skip the staging phase. The addition of -a will automatically stage any files that are already being tracked by Git (changes to files that you’ve committed before).
git commit --amend
Replaces the most recent commit with a new commit.
git branch
Shows the branches with an asterisk next to the active branch
git branch name_of_branch
Creates branch that is called “name_of_branch”
git branch -d name_of_branch
Deletes branch if merged (only use lowercase “d” to prevent a delete of a branch that has not yet been merged)
git branch -m new_name
Renames the active branch to “new_name”
git checkout name_of_branch
Switches to the “name_of_branch”
git checkout -b new_branch
Creates a new branch with the name “new_branch” and switches to that branch.
To merge branches with main
git checkout main
Switches to main
git merge branch_to_be_merged
Merges branch to main
git branch -d branch_to_be_merged
Deletes branch that has been merged
Adding Remote Repository
So far, I’ve been mainly working with the local repository.
I previously made a repository on Github that I want to now use to sync my local files.
The remote github repository is:
https://github.com/murwell/shopping.git
To add this remote repository info to my local repository so that the two can be synced, use:
git remote add <remote_name> <remote_url>
Traditionally, origin
is used as the remote name. You can use anything. Recommend to stay with origin
.
git remote add origin https://github.com/murwell/shopping.git
Now, to push the files:
git push origin main
This pushes to the main branch on my remote repository.
At this point, these two commands should work if all I’ve done is change exisiting files.
git commit -am "add message"
git push
If I’ve added a file, I should run:
git add -a
git commit -am "add message"
git push
What Is Illuminate\Http\Request;
In many controllers, there is this line: use Illuminate\Http\Request;
In the Laravel documentation, this is described as:
Laravel’s Illuminate\Http\Request class provides an object-oriented way to interact with the current HTTP request being handled by your application as well as retrieve the input, cookies, and files that were submitted with the request.
The file is found at: vendor/laravel/framework/src/Illuminate/Http/Request.php
It is recommended to browse through the contents of the file to see how it’s set up. You can see different methods that are defined in this class. The request class is the way to get the information that is being submitted by the user.
And what you’ll see it that Laravel has all of these predefined methods. And when you look at code, it may not be clear how something works. It just does because there’s an underlying method.
Introduction
When I began this journey on August 19, 2022, there was no Laravel Bootcamp. It wasn’t until after I had been painfully reading documentation for almost a month that Laravel released a tutorial that is beginner/intermediate friendly. If you’re starting on Laravel, go to Laravel Bootcamp. It may not all make sense, but it’s definitely helpful. Sure beats reading their documentation.
Much I what I am writing here is for my own benefit. I don’t even bother looking at how many people visit this blog. I don’t care. Documenting code, being able to explain concepts in a simple manner is the way I learn. If I can’t explain something, then I know nothing.
This documentation will be a work in progress for months. In the movie Mank, the screenwriter of Citizen Kane is criticized for turning in a very long screenplay. His response, “If I had more time, I could make it much shorter.”
With that in mind, I hope that this will eventually become concise. But I make no promises. I’m no Mank.
Why I Want To Learn About CRUD Apps
I run this genealogy website based on open source software called webtrees. The people behind the software are brilliant. The software works so well that if you’re putting up a genealogy website, webtrees is it.
While I working on my genealogy website, I became interested in how world history impacted my family. Webtrees has a way to add a historical timeline, but I would like something more. And to do that, I decided to learn how to code a CRUD app.
CRUD stands for create, read, update, delete. The basic functions that is needed for every database.
To learn, I’m going to create a shopping list app. That’s about as simple of a CRUD app as I could envision. But, it would give me the basics so that one day I will have this enormous website that will have beautiful timelines and maps to explain my family.
Before beginning this project, I had little familiarity with databases. I’ve worked with spreadsheets. Put up wordpress websites that used MYSQL. And even used MongoDB, a non-relational database. But the reality is I didn’t understand how a database worked. There are enough tutorials out there that one can get by just copying and pasting. Something like plagiarizing a term paper off of wikipedia.
This time around I figured I should understand a little more.
Choosing Laravel
I knew enough that I couldn’t just bang out some CRUD app without lots of help.
Years ago, I put up static websites. That’s when the website basically stays the same. There’s some html, some js, no real database. You might store some photos in an image directory. It’s just text and photos.
But even with a static website, I used a framework to make it easier. In [Bootstrap}(https://getbootstrap.com/) and other similar frameworks, smart people develop shortcuts to make coding a website faster and better. Rather than having to design a button or a table, there are some words you type in and you’re done. Or you can even just download a template, add in your texts and images, and it looks like you know what you’re doing.
After static websites, I wanted to do something more interactive. I then put up some websites using a framework called MeteorJs.
I didn’t know what I was doing, but I knew how about ctrl + c
. Eventually, I got a CRUD app running. Although, I didn’t even know the term CRUD back then.
This time around, I knew I needed lots of help and wanted to find a complete framework to build my Crud app. When I say complete, I wanted something that would take care of the backend (the database) to the frontend (the pretty stuff on the webpage).
Now, MeteorJS is still around. But it doesn’t seem like it’s that popular any more. It also uses MongoDB which is a document-oriented database. And even though I’m not sure how much difference it would actually make, I decided I wanted a more traditional relational database. And so, I skipped going back to Meteor.
Laravel is a full framework that works with many of the most popular relational databases. It has built-in user authentication. And is actively developed.
And so, I went for dinner at the restaurant that has line outside of the door.
MYSQL
While Laravel is considered full framework, it’s actually comprised of different components. The first component to choose is the database.
I went with MYSQL because when you spin up a server on a site like DigitalOcean or AWS, MYSQL is usually one of the default configurations. But for my purposes, any of the supported databases would work.
Frontend Choice - Blade, Vue, or React
Choosing MYSQL for backend took about ten seconds. The frontend was much more complicated.
Laravel has a frontend called Blade. I’m sure it works fine. But from my past experience, I wanted something that is very popular. It makes it easier when creating a website to copy/modify someone elses work rather than starting from nothing. I’m not saying that Blade isn’t well supported. But in my cursory review, it looked like VUE and React were much more polished.
There’s also something called limewire which is described on their homepage as “a full-stack framework for Laravel that makes building dynamic interfaces simple, without leaving the comfort of Laravel.”
Limewire sounds great. I looked into it briefly. But again decided to go with a more popular choice. Although, I don’t know the actual popularity of the different Laravel combos: Laravel/Blade, Laravel/Limewire, Laravel/VUE, Laravel/React, Laravel/Next, etc.
It seems like choosing a front end is like deciding on an ice cream flavor at Baskin Robbins. 31 choices are just too many.
Anyway, I had to make a choice. So, I decided to go with VUE because it was supposedly easier to learn than React.
Using VUE With Laravel Means Inertia
When I first chose to go with VUE (and later React), I didn’t understand that a consequence would be InertiaJs.
Inertia describes itself “as glue that connects [Laravel to React].”
I don’t know if its the glue. But for someone with little experience, it has made learning Laravel a lot messier.
Inertia complicates Laravel, in part, because much of the Laravel documentation is written for Blade. And Blade does not use Inertia.
Also the Laravel documentation isn’t particularly clear on Inertia’s importance. They don’t hide the connection. But all of these little bits and pieces need to be looked at because of Inertia.
Oh, yeah, there’s also something called vitejs which laravel describes as an asset bundler.
This splatter of information is also compounded because much of the documentation is lacking. Someone with more experience would likely understand it, but for me it’s close to hieroglyphics.
All this junk just to write a shopping list app. Pen and paper is looking pretty good at this point.
End Of Introduction
Overview of Laravel
There are five basic areas of using Laravel: the Model, the Migrations, the Controller, The Routing, the Views.
But before even getting there, I need to discuss the relational database.
A database is divided into tables. Each table represents a type of informations. In my shopping list app, I have two tables: Users and Items. In Laravel, whenever you create a table, some columns are automatically created: id, created_at, updated_at.
Here’s how the Users Table Looks:
Here’s how the Items Table Looks:
Each table is divided into columns that hold a specific type of data. The fields are the name of the columns. And every time an entry is made in the database, a row is filled in the table.
Mysql tables are setup similar to the the sheets of spreadsheet.
For my frontend, I liked VUE. I played around with for about a week or two.
But going back to my concept of eventually building a historical timeline, I switched to React.
The reason is because I came across React-Chrono, a react timeline component.
And so for my ultimate purpose, building a timeline, I am using Laravel with MYSQL as the database and React as the frontend.
Off we go.
Creating Gates & Policies
Gates
Gates are simply closures that determine if a user is authorized to perform a given action.
Gates are written in the boot function
at:
app/Providers/AuthServiceProvider.php
Below is an example where a method destroy-item
is defined. It returns a boolean on whether the user is the same user who created the post.
public function boot()
{
$this->registerPolicies();
Gate::define('destroy-item', function (User $user, Item $item) {
return $user->id === $item->user_id;
});
}
Then in the ItemController
, you include the defined gate with use Illuminate\Support\Facades\Gate;
and in the destroy function
you add an if statement
:
public function destroy($id)
{
$item = Item::findOrFail($id);
if (! Gate::allows('destroy-item', $item)) {
abort(403);
}
$item->delete();
return redirect('/items')->with('completed', 'Item has been deleted!');
}
With the if statement
, you can check if the gate is working by trying it with and without the inequality operator (exclamation mark - ! - ).
Policies
I had difficulty getting this working. Mainly, it because I had written “destroy” instead of “delete” in a method.
I modified the example from the Laravel Bootcamp. But even with that, I had written the wrong word and wound up wasting too much time.
Laravel policies are classes that organize authorization logic around a particular model or resource. The idea being that you create policies that limit what an authorized user can do. For example, the authorized user should only be allowed to modify or delete the items that particular user created.
Immediately below is what I had gotten out of the documentation from Laravel. The bootcamp is more to the point.
/**
* Determine whether the user can update the model.
*
* @param \App\Models\User $user
* @param \App\Models\Item $item
* @return \Illuminate\Auth\Access\Response|bool
*/
public function update(User $user, Item $item)
{
//
return $user->id === $item->user_id;
}
/**
* Determine whether the user can delete the model.
*
* @param \App\Models\User $user
* @param \App\Models\Item $item
* @return \Illuminate\Auth\Access\Response|bool
*/
public function delete(User $user, Item $item)
{
//
return $user->id === $item->user_id;
}
The above functions will return true if the current user’s id (i.e. - id) is the same as the user who created the item (i.e. - user_id was stored in the items table and is the id of the person who created the item).
A policy may be generated using php artisan make:policy ItemPolicy
. The generated policy will be placed in the app/Policies directory. If this directory does not exist, Laravel will create it.
After running the artisan make:policy, the following file is created in app/Policies/ItemPolicy.php
.
<?php
namespace App\Policies;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
class ItemPolicy
{
use HandlesAuthorization;
/**
* Create a new policy instance.
*
* @return void
*/
public function __construct()
{
//
}
}
By adding a model option, php artisan make:policy ItemPolicy --model=Item
, a class ItemPolicy
will be generated with sample methods in app/Policies/ItemPolicy.php
. The methods will have to be filled in.
The Resource Controller methods relate to the policies as follows:
Controller Method | Policy Method |
---|---|
index | viewAny |
show | view |
create | create |
store | create |
edit | update |
update | update |
destroy | delete |
Each of the methods in the policy file below return a boolean. **When requests are routed to the given controller method, the corresponding policy method will automatically be invoked before the controller method is executed. **
Each of the methods in the policy file below return a boolean.
@return \Illuminate\Auth\Access\Response|bool
.
bool
is a boolean. A bool expresses a truth value. It can be either true or false.
In the Laravel Bootcamp, the policy is called into the controller like this.
$this->authorize('update', $item);
Once ‘$this->authorize( controllerMethod,$__ )’ is placed in the controller method, it blocks all users.
Then in the policy file, the following is inserted:
public function update(User $user, Item $item)
{
return $item->user()->is($user);
}
This would authorize th euser who created the item to modify it.
In the bootcamp, it is also suggested to create the delete policy method with:
public function delete(User $user, Item $item)
{
return $this->update($user, $item);
}
This makes the same authorization as the update method.
And in the ItemController method or destroy, the following line of code is inserted:
$this->authorize('delete', $item);
That is same format as used in update method of the controller.
The error I had made was to write the ItemController method with $this->authorize('destroy', $item);
. That was WRONG because the function in the policy is called ‘delete’. Not destroy
.
Remember!!!
Controller Method | Policy Method |
---|---|
index | viewAny |
show | view |
create | create |
store | create |
edit | update |
update | update |
destroy | delete |
Eloquent Eager Loading Shortcut To Address N+1 Problem
For me, someone who is just trying to get a simple CRUD app to work, this is a pretty advance topic. Eager loading versus lazy loading has to do with making a query more efficient.
The purpose is to address the “N + 1” problem that exists in relational databases. I had never heard of this issue before which isn’t surprising given my basic knowledge.
But here’s an article from Kevin Gilpon of appmap explaining this problem.
Laravel’s solution is using the eager loading with
method.
This does not to be appear a problem with my simple shopping list app. But it’s definitely something to keep in mind where you will be returning multiple results from multiple tables.
Laravel’s example is a list of all books and their authors from two tables: books and authors with a Book model that “belongs to” an Author model.
The with
method allows you to specify which relationships should be eager loaded. That means related data is loaded at the time of the query. Here the author data is eager loaded.
$books = Book::with('author')->get();
foreach ($books as $book) {
echo $book->author->name;
}
Database Relations - And you thought your family was complicated
Another area that caused me frustration was matching the creator to the items they added.
I’ve worked with databases before and was familiar with the concept of adding an identifier from one table to another so that that the two tables are related.
Generally, as Microsoft points out, a relationship defines how two entities relate to each other. In a relational database, this is represented by a foreign key constraint.
Adding Foreign Key To Table
MYSQL is a relational database and it’s oftentimes necessary to relate one table to another.
In a shopping list app where different users insert different data into the table, keeping track of who is inserting what is important so that when the list is shown only the items that were added by the actual creator are visible.
It’s too complicated to just keep adding columns to tables. It’s better to separate tables into natural divisions.
Laravel automatically seta up a users table that will track the name, email, etc. And every record (row) of the table will have a distinctive id
.
In fact, every table that is created will have an id
field (column ). And it is that id that can be referenced in a different table. That way you can connect tables to each other and be able to query them easier.
In my shopping list app, I currently have two tables: users
and items
.
Everytime a user registers, a new record (row) is made in the users table. And, of course, whenever a new item is added, a new record is added to the items table.
By adding a field (column) into items that tracks the user who created the item, the shopping list can be easily individualized. In other words, no one needs to know how much I love junk food.
To do this, there’s this concept of a foreign key.
And in Laravel, there are different ways to create a foreign key. They all involve adding schema in a migration file.
Here’s the schema from the items table migration file.
public function up()
{
Schema::create('items', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->string('name');
$table->bigInteger('user_id')->nullable()->unsigned();
$table->foreign('user_id')->references('id')->on('users');
});
}
The last two lines link the “id” column from the “users” table to a column (“user_id”) in the “items” table.
$table->bigInteger('user_id')->nullable()->unsigned();
creates the user_id column that will store the user’s id.
$table->foreign('user_id')->references('id')->on('users');
establishes the link between “user_id” in the “items” table and “id” in the “users” table.
There’s also shorter code that combines the above last two lines. It uses Laravel’s foreignId() method.
Schema::table('items', function (Blueprint $table) {
$table->foreignId('user_id')->constrained();
});
The foreignId method creates an UNSIGNED BIGINT equivalent column, while the constrained method will use conventions to determine the table and column name being referenced. If your table name does not match Laravel’s conventions, you may specify the table name by passing it as an argument to the constrained method:
Schema::table('posts', function (Blueprint $table) {
$table->foreignId('user_id')->constrained('users');
});
Once the foreignId column (“user_id”) has been established, it must be referenced in the appropriate table controller to store the appropriate data.
In my shopping app, it would be referenced in the ItemController.php
.
namespace App\Http\Controllers;
use Auth;
use Illuminate\Http\Request;
use App\Models\Item;
use Inertia\Inertia;
class ItemController extends Controller
{
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required', 'max:50',
]);
$item = new Item;
$item->name = $request->name;
$item->user_id = Auth::id();
$item->save();
return redirect('/items')->with('completed', 'Item has been saved!');
}
}
use Auth;
has to be added to the top of the file because Auth
is called at $item->user_id = Auth::id();
.
The store function will add a new item to the items table. Here are the columns.
If there was another table, let’s call it “shops”, that could be incorporated like this:
Schema::create('items', function (Blueprint $table) {
$table->id();
$table->timestamps();
$table->string('name');
$table->bigInteger('user_id')->nullable()->unsigned();
$table->foreign('user_id')->references('id')->on('users');
$table->bigInteger('shop_id')->nullable()->unsigned();
$table->foreign('shop_id')->references('id')->on('shops');
});
Then, we would need to add another line to store function in the controller.
Of course, we would also need to add a use statement to the top of the controller. use App\Models\Shop;
which would make the shop table available to the controller.
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required', 'max:50',
]);
$item = new Item;
$item->name = $request->name;
$item->user_id = Auth::id();
$item->shop_id = Shop::id();
$item->save();
return redirect('/items')->with('completed', 'Item has been saved!');
}
HasMany Relationship
Laravel has another way to accomplish the above. And this is done by establishing relation methods in the appropriate Models.
Relational databases has specific terms to describe the relationship between different tables.
Here’s an article on database tutorials from dotnettutorials.
One to Many
is the most common database relationship. For example, in my shopping list app, one user has a relationship to many items. Of course, there is also an inverse relationship where certain items belong to only one user.
In Laravel eloquent, a one to many
is called HasMany. And the inverse relationship where the many items belonging to one user is called belongsTo.
Once these relationships are defined, Eloquent makes it easier to retrieve the appropriate records.
Previously, this is how I created the store function:
public function store(Request $request)
{
$validated = $request->validate([
'name' => ['required', 'max:20'],
]);
//creates an object item from the Class Item
$item = new Item;
//name is the property of $item and its value is assigned from the request
$item->name = $request->name;
// Stores id of user into user_id
$item->user_id = Auth::id();
$item->save();
return Redirect::route('items.index');
}
Instead, in the bootcamp tutorial, it is recommended:
public function store(Request $request)
{
//
$validated = $request->validate([
'name' => 'required|string|max:25',
]);
$request->user()->items()->create($validated);
return redirect(route('items.index'));
}
The first and last statements are basically the same as in the store function I created.
It’s the middle part that is totally different.
In the bootcamp version, the middle statement is creating a record that will belong to the logged in user by leveraging an items relationship.
items()
is a hasMany method that must be created in the User
Model at app/Models/User.php
.
The eloquent create method accepts an array of attributes, creates a model, and inserts it into the database. The difference between save
and create
is that save
accepts a full Eloquent model instance while create
accepts a plain PHP array.
<?php
...
class User extends Authenticatable
{
public function items()
{
return $this->hasMany(Item::class);
}
This items()
method defines a hasMany relationship.
And once that relationship is established, Laravel’s Eloquent will automatically determine the proper foreign key column for the Item model. By convention, Eloquent will take the “snake case” name of the parent model (User here) and suffix it with _id. So, in this example, Eloquent will assume the foreign key column on the Item model is user_id.
This will allow Eloquent to find all of the user’s items.
Then to find the user from the item, the inverse relationship must also be created. It is called belongsTo.
<?php
...
class Item extends Model
{
...
public function user()
{
return $this->belongsTo(User::class);
}
}
Once these relations are created, the store function works likes this:
Public function store(Request $request)
{
//
$validated = $request->validate([
'name' => 'required|string|max:25',
]);
$request->user()->items()->create($validated);
return redirect(route('items.index'));
}
And the index function in Item Controller can be written like this:
public function index()
{
$user = Auth::user();
$items = Item::whereBelongsTo($user)->latest()->get();
return Inertia::render('Index', ['items' => $items]);
}
Before using relationships, index was written like this:
public function index()
{
$items = Item::all()->sortByDesc('id')->values();
return Inertia::render('Index', ['items' => $items]);
}
Sort By Descending
In my shopping list app, when I added an item, it was inserted at the bottom of the table. That’s annoying because you don’t easily see that an item was added.
Eloquent has a sortbyDescending Method. When I tried using it, all I got was a blank page.
I did a search and found the answer on Github.
There are multiple solutions.
Sort with eloquent methods like these:
$events = Events::orderByDesc('date')->get();
$events = Events::all()->sortByDesc('date')->values();
The sorting worked after I changed the index function in my ItemController to:
public function index()
{
$items = Item::all()->sortByDesc('id')->values();
return Inertia::render('Index', ['items' => $items]);
}
all(), sortByDesc(), values() are eloquent methods.
all()
- returns the underlying array represented by the collection.
sortByDesc()
sorts the collection in the opposite order from sort().
values()
returns a new collection with the keys reset to consecutive integers.
Now, whenever I add an item it goes right to the top of the list.
Sort with query building
Another way to sort is with hasMany
and belongsTo
relationships. These are queries and are different from eloquent methods. Before these queries can be used, the methods must be created in the Model. Then these queries can be linked together with latest()
query builder like this:
public function index()
{
$user = Auth::user();
$items = Item::whereBelongsTo($user)->latest()->get();
return Inertia::render('Index', ['items' => $items]);
}
latest()
does not work with all()
. So,Item::all()->latest()
does not work.
This is because all()
is a eloquent collection method and latest()
is a query builder.
Eloquent collection methods and query builders do not work together.
sortbyDesc()
is an eloquent collection method so it works with all()
.
whereBelongsTo
is a query and so latest()
works with it.
This highlights one of the difficulties that I’ve had learning laravel.
One of which is the concept that there are two fundamental ways to pull data from databases in Laravel: eloquent and database queries.
And if you confuse the two (which is easy for me to do), it does not work.
A google search of Eloquent vs query builder
returns numerous discussions about which to use and when. For the time being, I will try to get them separate. It may be a good exercise to use each for the same thing.
Using React On The Front End
I finally got CRUD working, and so I’m going to start working on making my shopping list app look nice.
As far as structure, I will have a homepage that’ll have a couple of cards on them. I don’t expect to have many users. Probably just me. Maybe a couple of friends will try it. But that’s about it. After finishing the shopping list app, I’ll probably switch to a recipe app. What I’m thinking is that I’ll just do it all in one database. They could even work together. So, maybe I’ll have three cards. One with Shopping list. One with registration info. And one with recipe maker coming soon.
As far as the shopping app, I’m thinking it should all be in one page. At the top, an input line with and add button. Underneath, the list of items. And next to each item, a delete button. I think I’ll leave out an edit button. I’ve used a shopping list app before and all I care about is the list. This will be the first iteration. Just to see it up and running.
At the top will be the navabar. Below the menu, the input line with an add button.
Index page, with a store component, and a destroy (delete) button.
So, the index page will be the main page.
Then I’ll have:
- navar component
- create compoent ( a form with a box and a store button.
- a loop component that’s list all the items and have a delete button
I think that’s it.
Some Styling
And here’s how version 1 looks:
Most of this uses MUI material-UI. Responsive Bar With Menu, Button, and Basic Table.
Responsive Bar With Menu
The Responsive Bar can be found here.
The source code is some serious work and this is why we use components like material-ui. I could spend the next ten years trying to come up that code, and it would never happen.
It’s all complicated, but the part that I needed to change was the * const navItems*:
import Button from '@mui/material/Button';
const drawerWidth = 240;
const navItems = ['Home', 'About', 'Contact'];
The const navItems
are the menu links that’ll appear in the AppBar. I needed to change the names and add in some links. This is how I coded it:
const navItems = [
<a href='/'>Home</a>,
<a href={route('items.index')}>Shopping</a>,
<a href='https://emlekezik.com/'>Emlekezik</a>
];
The first link will take you home. The second link takes you to the shopping app. And the third link is an external genealogy website that I run.
Since these are links, I tried using Inertia Links. But I couldn’t get it work. All I got was a blank page. So, I left out the Inertia Link.
Material-UI recommends using a prop called component="a"
in the ListItemButton.
In this Responsive Bar, there’s a ListItemButton. So, I added the component like this:
<List>
{navItems.map((item) => (
<ListItem key={item} disablePadding>
<ListItemButton component="a" sx={{ textAlign: 'center' }}>
<ListItemText primary={item} />
</ListItemButton>
</ListItem>
))}
</List>
The link worked even without this component, but I left it in because it also worked with it.
And now, on the desktop, the app looks like this:
And in mobile, with the side menu open, it looks like this:
And the links all work.
Life is good.
bootstrap.laravel.com
For whatever reason, laravel doesn’t advertise this tutorial atboostrap.laravel.com. I only found out about it from a posting on a forum. If I had seen it earlier, the tutorial would have saved me a couple of weeks of time.
Here are a few things I learned:
Limit the authentication to certain resource routes with ->only(['index', 'store])
like below:
Route::resource('chirps', ChirpController::class)
->only(['index', 'store'])
->middleware(['auth', 'verified']);
Use verified
only if email verification has been setup.
###FrontEnd Part 2 - Typescript Here We Come
Life was good for a day. But then I began working on my homepage and it feels king of disjointed with a messy code. So, I’m going to start over on the front end with React, Mui Material-ui and typescript.
Text Bar
The text box is from Inertia useForm. I haven’t yet figured out how I can change the styling to go with Material UI.
Inertia points out that “While it’s possible to make classic form submissions with Inertia, it’s not recommended, as they cause full page reloads. Instead, it’s better to intercept form submissions and then make the request using Inertia.”
That’s something I noticed when I was playing around with the text field. When I added an item, the entire page would reload. With useForm, only an item would be added. I did change the button to the one from material-UI. But the tes field is still from
Important Stuff
Eloquent is different from query builder.
The key to Laravel fun is href={ route('items.destroy', item.id)}
1The above code, with the below function in ItemController, worked to delete an item:
public function destroy($id)
{
$item = Item::findOrFail($id);
$item->delete();
return redirect('/items');
}
To define routes in your code, you must follow the default routes established in Laravel.
Verb | URI | Action | Route Name |
---|---|---|---|
GET | /items | index | items.index |
GET | /items/create | create | items.create |
POST | /items | store | items.store |
GET | /items/{item} | show | items.show |
GET | /items/{item}/edit | edit | items.edit |
PUT/PATCH | /items/{item} | update | items.update |
DELETE | /items/{item} | destroy | items.destroy |
The route names are the way to reach the appropriate functions in the controller.
Routing is very important in Laravel. It makes everything work. This means you’ll need to get a grasp of Inertia if you’re gonna use Laravel with React or Vue.
Lttle Things Are Annoying
- When using Redirect in the Controller, import the Redirect class with
use Illuminate\Support\Facades\Redirect;
If not, you’ll get an error.
Passing Data From Laravel Controller To React Page
The basic way to show passed data in a web page is by using {props.name_of_collection.name_of_field}
or for my shopping app {props.item.name}
.
Here’s a more thorough explanation.
The standard Laravel Controller has a show function. The purpose of this function is to show more information about a specific item. This is used all the time on the web. You’re on Amazon and see a list of possible products. You click on an item, and it takes you to the specific item page that’ll have more info. That’s show biz.
In my shopping app, a show function is not needed. You’re just making a list. But here’s how to use the show function anyway in the shopping app.
Let’s start at the beginning. There’s a database and in the database there is a table called items
that looks like this.
There are five fields (ie -columns). id
is the way the items are tracked. There can never be a duplicate. Whenever a new item is added, a new id
is created. Even if the item is eventually deleted, the id
will never be duplicated.
When the list of items is returned in the index page, the name of each item was hyperlinked with the following code:
<td><Link href={ route('items.show', item.id)}>{item.name}</Link></td>
(A map function will automatically create this link for each item.)
The route of the link is route('items.show', item.id)
. It will link to a show page with the appropriate id
.
http://127.0.0.1:8000/items/20
The 127.0.0.1:8000 is the local server. items is the name of the table. And 32 is the id of the item. This matches the route format created by the Laravel Controller resources.
It will call the show function that was created in ItemController.php
.
public function show($id)
{
$item = Item::findOrFail($id);
return Inertia::render('Show', [
'item' => $item,
]);
}
This function will take the id of the item clicked (i.e.- 20 in my example) and do two things:
First, it will render the page /resources/js/Pages/Show.jsx
.
Second, it will also pass the contents of the item with id 20 to the Show.jsx
.
Then in the page Show.jsx
, you can use the data by calling it with [props.item.name]
, for example.
As previously noted, there are five columns (fields) in the items table:
id, created_at, updated_at, name, user_id
Any of those columns can be called in Show.jsx
by using:
{props.item.column_name}
or {props.item.created_at}
, etc.
Per Inertia, the amount of data that is passed can be limited by passing the desired columns with only
in the controller as follows:
public function show($id)
{
$item = Item::findOrFail($id);
return Inertia::render('Show', [
'item' => $item->only('id', 'name'),
]);
}
Updating
Here is the useForm code from Inertia to modify an item.
It was changed to work for my shopping app.
import React from 'react'
import { useForm } from '@inertiajs/inertia-react'
export default function Modify(props) {
const { data, setData, patch, processing, errors } = useForm({
name: '',
})
function submit(e) {
e.preventDefault()
patch(route("items.update", props.item.id));
}
return (
<div>
<form onSubmit={submit}>
<input type="text" value={data.name} onChange={e => setData('name', e.target.value)} />
<button type="submit" disabled={processing}>Submit</button>
{errors.name && <div>{errors.name}</div>}
</form>
<h1>{props.item.name}</h1>
</div>
)
}
Remember, the above code comes from Inertia and uses their helper useForm.
The tricky part is the route format in the function submit(e)
. But before we go through to code, it’s important to understand that this code resides in the edit.jsx
page.
Add so, to get to this edit page, the following link is clicked: http://127.0.0.1:8000/items/40/edit
127.0.0.1:8000 is the local server address. Items is the name of the table. 40 is the id of the speciic item. edit is the function.
This link follows the default edit route for Laravel: GET /items/{item}/edit
.
And when the link is clicked, it calls the edit
function in the ItemController. That function is written as:
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
$item = Item::findOrFail($id);
return Inertia::render('Edit', [
'item' => $item,
]);
}
***
The function causes the following to happen when the following link is clicked: http://127.0.0.1:8000/items/40/edit
The id of 40 from the link is passed to function. The findorFail Method will assign the properties of the item with id:40 to the variable $item
. Inertia will render edit.jsx
and receive the contents of the item with id:40
.
The code of edit.jsx
is:
import React from 'react'
import { useForm } from '@inertiajs/inertia-react'
export default function Modify(props) {
const { data, setData, patch, processing, errors } = useForm({
name: '',
})
function submit(e) {
e.preventDefault()
patch(route("items.update", props.item.id));
}
return (
<div>
<form onSubmit={submit}>
<input type="text" value={data.name} onChange={e => setData('name', e.target.value)} />
<button type="submit" disabled={processing}>Submit</button>
{errors.name && <div>{errors.name}</div>}
</form>
<h1>{props.item.name}</h1>
</div>
)
}
This page will receive item from the controller. And pass it in function submit(e)
to the update (patch) route.
function submit(e) {
e.preventDefault()
patch(route("items.update", props.item.id));
}
And the function callls the Laravel update route and passes the id of 40 to the update function in the ItemController when the submit button is clicked. It will also pass in the update function the value of the textbox to the target value of name.
And the update function in ItemController is:
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
$item = Item::findOrFail($id);
$item->name = $request->name;
$item->save();
return Redirect::route('items.index');
}
Here we see item receives the value of id:40 and the new value of name. The item is saved and then the pages is redirected to the index page. When using redirect in a controller, make sure to import the Redirect class with use Illuminate\Support\Facades\Redirect;
Otherwise, there will be an error.
Stuck In Laravel With ID Blues Again
I’ve been stuck for days on how to edit, show, or delete a specific item in my shopping app.
Basically, I’m abble to create an item. I can return a list of the items that are in the data, but that’s it.
This is what I envision, a list of items on my shopping list. Each item is hyperlinked and if I click on that link, it would take me to a page (show) where I could have more info about that item. On the show page there would be button that would take me back to the overall list.
On the overall list (index), there are two buttons next to each hyperlinked item. The two buttons are edit and delete,
The frustration is that the Laravel documentation makes it sound so easy:
In addition to retrieving records from the database table, Eloquent models allow you to insert, update, and delete records from the table as well.
My issue is that I don’t know how to pass the id value of the item to the appropriate spot.
Let’s say you have an item - id: 1 name: milk. When I click on the button delete that is next to the item milk, how do I pass the id of milk to the destroy function.
Finally, figured it out.
Here Comes The Sun - Maybe
It’s been days, maybe even weeks, since I’ve been trying to delete an item from the shopping list using laravel/inertia/react.
And I just had a breakthrough.
The code to delete the item is below:
<Button variant="contained">
<Link href={ route('items.destroy', item.id)}
method="delete" as="button" type="button" >
Delete
</Link>
</Button>
The Button component is from MUI Material-UI.
The key is href={ route('items.destroy', item.id)}
There was discussion here that led me to the above line of code.
The above code, with the below function in ItemController worked:
public function destroy($id)
{
$item = Item::findOrFail($id);
$item->delete();
return redirect('/items')->with('completed', 'Item has been deleted!');
}
I don’t know why Inertia doesn’t explain this in their documentation on links.
Anyway, I think I had a breakthrough. Finally.
Probably, tomorrow, inertia or laravel will change their code and it’ll be done. But for now, I’m happy.
Here’s a show link that can be clicked and take to another page:
<td><Link href={ route('items.show', item.id)}>{item.name}</Link></td>
Assuming the item has an id of 20, the url will be:
http://127.0.0.1:8000/items/20
The show function in the ItemComtroller will be called:
public function show($id)
{
$item = Item::findOrFail($id);
return Inertia::render('Show', [
'item' => $item,
]);
}
This function will take the id of the item clicked (i.e.- 20 in my example) and do two things:
First, it will render the page /resources/js/Pages/Show.jsx
.
Second, it will also pass the contents of the item with id 20 to the Show.jsx
.
In the function in Show.jsx
, you can use the data by calling it with [props.item.name]
,for example.
items is a table in the database that is being used. In that table, there are five columns (fields):
id, created_at, updated_at, name, user_id
Any of those columns can be called in Show.jsx
by using:
{props.item.column_name}
or {props.item.created_at}
, etc.
Per Inertia, the amount of data that is passed can be limited by passing the desired columns with only
in the controller:
public function show($id)
{
$item = Item::findOrFail($id);
return Inertia::render('Show', [
'item' => $item->only('id', 'name'),
]);
}
Laravel Eloquent Methods vs Laravel Collection Methods
Another confusing thing about the Laravel documentation is the difference between Eloquent and Collections. Eloquent is a query builder that has the same methods available to it as collections. Collections are created whenever an eloquent query is run.
Since they use many of the same methods, it gets confusing.
The all method returns the underlying array represented by the collection:
collect([1, 2, 3])->all();
// [1, 2, 3]
Above is from Laravel Collections
The model's all method will retrieve all of the records from the model's associated database table:
use App\Models\Flight;
foreach (Flight::all() as $flight) {
echo $flight->name;
}
Above is from Laravel Eloquent
Laravel says “You can think of each Eloquent model as a powerful query builder allowing you to fluently query the database table associated with the model.”
Laravel introduces its collections class as:
The Illuminate\Support\Collection class provides a fluent, convenient wrapper for working with arrays of data. For example, check out the following code. We'll use the collect helper to create a new collection instance from the array, run the strtoupper function on each element, and then remove all empty elements:
$collection = collect(['taylor', 'abigail', null])->map(function ($name) {
return strtoupper($name);
})->reject(function ($name) {
return empty($name);
});
As you can see, the Collection class allows you to chain its methods to perform fluent mapping and reducing of the underlying array. In general, collections are immutable, meaning every Collection method returns an entirely new Collection instance.
Almost every method returns a new Collection instance, allowing you to preserve the original copy of the collection when necessary
I’m easily confused but Laravel emphasizes that “The results of Eloquent queries are always returned as Collection instances.”
Also, “All Eloquent collections extend the base Laravel collection object; therefore, they inherit all of the powerful methods provided by the base collection class.”
Based on this, it seems that on Laravel you use eloquent first, then collection.
findOrFail and firstOrFail
In different routes I have in my app, the findOrFail methods are used to return an $id.
This is a Laravel method.
The findOrFail method will retrieve the first result of the query; however, if no result is found, an Illuminate\Database\Eloquent\ModelNotFoundException will be thrown:
$flight = Flight::findOrFail(1);
$flight = Flight::where('legs', '>', 3)->firstOrFail();
If the ModelNotFoundException is not caught, a 404 HTTP response is automatically sent back to the client:
use App\Models\Flight;
Route::get('/api/flights/{id}', function ($id) {
return Flight::findOrFail($id);
});
Form Validation
Codes that I see sometimes is in this format:
e.preventDefault()
or e.target.value
This code is in a create form that I am currently using:
import React from 'react'
import { useForm } from '@inertiajs/inertia-react'
export default function Create() {
const { data, setData, post, processing, errors } = useForm({
name: '',
})
function submit(e) {
e.preventDefault()
post('/items')
}
return (
<form onSubmit={submit}>
<input type="text" value={data.name} onChange={e => setData('name', e.target.value)} />
{errors.name && <div>{errors.name}</div>}
<button type="submit" disabled={processing}>Submit</button>
</form>
)
}
The preventDefault() method of the Event interface tells the user agent that if the event does not get explicitly handled, its default action should not be taken as it normally would be.
So, in the above code, the preventDefault function stops the uploading of the form data (post(‘/items) to the mysql database if there is an error (i.e.- the input fails the validation function).
In ItemController.php, there is the following validation rules in the store function:
public function store(Request $request)
{
$validated = $request->validate([
'name' => ['required', 'max:50'],
]);
This means that the name field is required and cannot exceed 50 characters. If the submit button is clicked and the field is empty, nothing is sent to the database and an error message will appear: “The name field is required.”
In Laravel, form validation is discussed here.
React’s useState
Hook
import React, { useState } from 'react';
In React, hooks let you use state and other React features without writing a class. useState
is a hook provided by React. A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.
useState
declares a “state variable”. State returns a pair of values: the current state and a function that updates it. In the below example, the variable is called count but could be called anything else, like apple. Unlike with classes, the state doesn’t have to be an object. This is a way to “preserve” some values between the function calls — useState is a new way to use the exact same capabilities that this.state provides in a class. Normally, variables “disappear” when the function exits but state variables are preserved by React.
1: import React, { useState } from 'react';
2:
3: function Example() {
4: const [count, setCount] = useState(0);
5:
6: return (
7: <div>
8: <p>You clicked {count} times</p>
9: <button onClick={() => setCount(count + 1)}>
10: Click me
11: </button>
12: </div>
13: );
14: }
When we want to display the current count in a class, we read this.state.count:
<p>You clicked {this.state.count} times</p>
In a function, we can use count directly:
<p>You clicked {count} times</p>
useState
uses a square bracket to declare the variable and function.
const [fruit, setFruit] = useState('banana');
The square bracket in JavaScript syntax is called “array destructuring”.
useState
returns an array with two items. The first item is the current value, and the second is a function that lets us update it. Using [0] and [1] to access them is a bit confusing because they have a specific meaning. This is why array destructuring is used instead.
Creating A REACT Table To Return Data
Using Props To Pass Data From Controller To View
The Index function from Controller at app/Http/Controllers/ItemController.php
is:
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Auth;
use Illuminate\Http\Request;
use App\Models\Item;
use Inertia\Inertia;
use Illuminate\Support\Facades\Redirect;
class ItemController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//get authenticated user who's making request via the Auth facade's user method
$user = Auth::user();
// gets the items that belongs to the authenticated user and sorts by latest
$items = Item::whereBelongsTo($user)->latest()->get();
//renders the index page and passes the prop 'items' that contains items attributes
return Inertia::render('Index', ['items' => $items]);
}
From the last line, we have return Inertia::render('Index', ['items' => $items]);
That code renders the index page and passes the prop ‘items’ that contains the items attributes. It needs to be read into the react page as props.items
. The function in the receiving page must include “props” as a parameter (i.e. - function nameExample (props)
).
In the index page at resources/js/Pages/Index.jsx
, we have placed props.items
in the following table:
<TableBody>
{props.items.map((item, index) => (
<TableRow
key={item.id}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
data-index={index}>
<TableCell>{item.name}</TableCell>
<TableCell align="left">
<Button variant="outlined"><Link href={route('items.destroy', item.id)} method="delete" as="button" type="button" >Delete</Link></Button>
</TableCell>
</TableRow>
))}
</TableBody>
The props have now been passed to the view. The pass is complete. Touchdown!!!
Using Inertia Helper usePage() To Return Data
usePage ahould NOT be used to pass props. Below was a mistake. But it’s being left in as a reminder of what not to do. And also when to actually use this inertia method.
To return the data that is the table, we use the index function in the appropriate controller.
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$items = Item::all();
return Inertia::render('Index', ['items' => $items]);
}
On the first line, the double colon, is a php token that allows access to static, constant, and overridden properties or methods of a class.
Here, Item:all() is a Laravel eloquent method that returns all of the results in the Item Model’s Table. These results are assigned to the variable items
. This is different than the all() method for collections which returns the underlying array represented by the collection.
On the second line, Inertia is used to render the file Index.jsx
and to pass the single prop items
to the rendered file resources/js/Pages/Index.jsx
.
To reach this prop on the react page, I use the hook from inertia that is called usepage
. USEPAGE SHOULD NOT TO BE USED TO PASS PROPS. This is a mistake I made when I began learning react. I had read the shared data section on Inertia and used the usepage method to pass props. Wrong. Usepage is only used for data that is to be shared among different pages. Instead, it should only be used in the circumstances as described by Inertia:
Sometimes you need to access certain data on numerous pages within your application. For example, a common use-case for this is showing the current user in the site header. Passing this data manually in each response isn’t practical. In these situations shared data can be useful.
import {usePage} from '@inertiajs/inertia-react';
into Index.jsx.
The recommended way to use this hook from inertia is:
import { usePage } from '@inertiajs/inertia-react'
export default function Layout({ children }) {
const { auth } = usePage().props
return (
<main>
<header>
You are logged in as: {auth.user.name}
</header>
<content>
{children}
</content>
</main>
)
}
In my app, the shared server data items
is used liked this:
import React from 'react';
import { Inertia } from "@inertiajs/inertia";
import { usePage} from '@inertiajs/inertia-react';
function TestLoop() {
const { items } = usePage().props
return (
<div className="container">
<h1>TestLoop</h1>
<table className="table table-bordered">
<tr>
<th>ID</th>
<th>Name</th>
</tr>
{items.map((item, index) => (
<tr data-index={index}>
<td>{item.id}</td>
<td>{item.name}</td>
</tr>
))}
</table>
</div>
);
}
export default TestLoop;
Once the data items is brought into the react page, the map function is used to list the rows of the table. Here, id and name are placed into a html table and all the rows will be returned.
Creating The Show Function With Inertia In Controller
The show function in the controller is a GET function that is used to show just one entry. Index function is used to show all entries.
The classic way is use show would be a list of items (ie. - books, movies, some collection. Then when you click on one of those items, you can more info about that specific item. Probably not that useful in a shopping list app. But can still be used as an example.
The route name is items.show.
The uri is /items/{item}.
Inertia has an example of a show function.
In the below example we’re passing a single prop, called event, which contains four attributes (id, title, start_date and description) to the Event/Show page component.
use Inertia\Inertia;
class EventsController extends Controller
{
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show(Event $event)
{
return Inertia::render('Event/Show', [
'event' => $event->only('id', 'title', 'start_date', 'description'),
]);
// Alternatively, you can use the inertia() helper
return inertia('Event/Show', [
'event' => $event->only('id', 'title', 'start_date', 'description'),
]);
}
}
In my app, I’m going to pass a few attributes as a test.
My proposed controller function is:
public function show(Items $items)
{
return Inertia::render('Items/Show', [
'items' => $item->only('id', 'name', 'user_id', 'created_at'),
]);
}
I just now need to figure out how to write a react page items/show
that will render the data. Basically, if I am using show, should there be a way to limit the response.
Using Inertia Helper useForm() to Return Data
?
MYSQL Commands
mysql
- begins terminal monitor
mysql -u<username> -p
- switch mysql user
use database_name;
- switches to database in monitor
show columns
- after switching to specific database with the above command
show column_name from table_name;
- show column
select column_name from table_name;
- show contents of column
show * from table_name;
- show all from column
Inertia Forms
It took me days to figure out how to use Laravel/Inertia/React to store data to MYSQL.
One stupid problem that wasted about two hours was I started npm incorrectly. I kept typing npm run -dev
. And because of this stupidity, my pages were not rendering. After two hours, I reread this document and noticed that I should be using npm run dev
. There is no -
.
Then I spent way too many hours trying to figure out how to put together a form that will submit the text field to the database when I click a button.
The answer is simple. You have to use the format required by inertia.
I changed their recommended code to work with my Items table that has a column “name” where I will list the names of the items on my shopping list.
import { Inertia } from '@inertiajs/inertia'
import React, { useState } from 'react'
export default function Edit() {
const [values, setValues] = useState({
name: "",
})
function handleChange(e) {
const key = e.target.id;
const value = e.target.value
setValues(values => ({
...values,
[key]: value,
}))
}
function handleSubmit(e) {
e.preventDefault()
Inertia.post('/items', values)
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="name">Item Name:</label>
<input id="name" value={values.name} onChange={handleChange} />
<button type="submit">Submit</button>
</form>
)
}
I don’t understand every nuance of this code. But it worked. The only other change has to be with the store function
of the controller. In my case, it’s
ItemController.php
.
class ItemController extends Controller
{
public function index()
{
return Inertia::render('Items/Index', [
'items' => Item::all(),
]);
}
public function store()
{
$validated = $request->validate([
'name' => 'required', 'max:50',
]);
Item::create($request->all());
return redirect('/items');
}
}
The validation in the above code is different than how Inertia wrote it. Instead, the validation came from Laravel.
Other code that worked in the ItemController.php to store the data is:
public function store(Request $request)
{
Item::create($request->all());
return redirect()->route('items.index');
}
And
public function store(Request $request)
{
$item = new Item;
$item->name = $request->name;
$item->save();
return redirect('/items')->with('completed', 'Item has been saved!');
}
The last code came from Laravel at eloquent/inserts.
Inertia Forms And The Inertia Helper “UseForm”
The above form worked to submit the data to a database. As Inertia notes:
Since working with forms is so common, Inertia comes with a form helper (useForm) designed to help reduce the amount of boilerplate needed for typical forms.
What I have noticed is that many tutorials out there just copy the below helper without noting it comes from Inertia. And it makes the writer of the tutorial seem much smarter than they are. After looking at many tutorials, most are just copies of each other without any real value added.
Anyway, below is from Inertia.
import React from 'react'
import { useForm } from '@inertiajs/inertia-react'
const { data, setData, post, processing, errors } = useForm({
email: '',
password: '',
remember: false,
})
function submit(e) {
e.preventDefault()
post('/login')
}
return (
<form onSubmit={submit}>
<input type="text" value={data.email} onChange={e => setData('email', e.target.value)} />
{errors.email && <div>{errors.email}</div>}
<input type="password" value={data.password} onChange={e => setData('password', e.target.value)} />
{errors.password && <div>{errors.password}</div>}
<input type="checkbox" checked={data.remember} onChange={e => setData('remember', e.target.checked)} /> Remember Me
<button type="submit" disabled={processing}>Login</button>
</form>
)
To submit the form, use the get, post, put, patch and delete methods. (I have no idea what this means. I don’t know why documentation doesn’t take it all the way home and show us the next step). I assume this means that we use the above as a template and they
const { submit, get, post, put, patch, delete: destroy } = useForm({ ... })
submit(method, url, options)
get(url, options)
post(url, options)
put(url, options)
patch(url, options)
destroy(url, options)
This is the code that I used to create an item in the database:
import { Inertia } from '@inertiajs/inertia'
import React, { useState } from 'react'
export default function Edit() {
const [values, setValues] = useState({
name: "",
})
function handleChange(e) {
const key = e.target.id;
const value = e.target.value
setValues(values => ({
...values,
[key]: value,
}))
}
function handleSubmit(e) {
e.preventDefault()
Inertia.post('/items', values)
}
return (
<form onSubmit={handleSubmit}>
<label htmlFor="name">Item name:</label>
<input id="name" value={values.name} onChange={handleChange} />
<button type="submit">Submit</button>
</form>
)
}
Using the helper, the code turns into this:
import React from 'react'
import { useForm } from '@inertiajs/inertia-react'
export default function Edit() {
const { data, setData, post, processing, errors } = useForm({
name: '',
})
function submit(e) {
e.preventDefault()
post('/items')
}
return (
<form onSubmit={submit}>
<input type="text" value={data.name} onChange={e => setData('name', e.target.value)} />
{errors.name && <div>{errors.name}</div>}
<button type="submit" disabled={processing}>Submit</button>
</form>
)
}
And it worked. Of course, I don’t know how the inertia helper works. Just that it does. That’s one of the issues I’ve had in understanding all this stuff. Much of the code is hidden behind helpers, methods, classes, etc. And so, when you look at the code, it doesn’t make much sense. It just works.
PHP
A class is a template for an object. It is made up of properties (variables) and methods (functions).
Classes are extensively used in Laravel.
A Model of Laravel creates a class that is then used to create objects in controllers to interact with the database.
The basic process involving a class is:
- Define a class that has properties and methods.
- Create an object (instant) from the class. The object will have the generic properties and methods of the class.
- Assign specific values to the properties of the object or call a method from the object.
A class is defined by using the class keyword, followed by the name of the class and a pair of curly braces { }. All of the class properties and methods go inside the braces. In a class, variables are called properties and functions are called methods. Default values can be set.
To define a class:
<?php
class Fruit {
// Properties
public $name;
public $color = 'default value';
// Methods
function set_name($name) {
$this->name = $name;
}
function get_name() {
return $this->name;
}
}
?>
Then to create an object from the class that was previously defined, the keyword new is used:
$apple = new Fruit();
When the object is first created, it only has the generic properties and methods of the class. The single arrow operator, ->, is used to assign specific properties to the object or to call a method from the object.
Single Arrow Operator And Classes
The single arrow, ->, is an object operator and is used to access the properties and methods of the object. It can be used to create (instantiate) the actual values of the properties.
$apple->set_name('Apple'); //A method (function) of the object is used to set a property.
$apple->name = 'Apple'; // The property 'name' is assigned the value 'Apple'.
The term to the left of the single arrow is the name of the object that had been previously instantiated (created) from a class. On the point of the arrow is the property or function of the object.
Double Arrow Operator - From StackOverFlow
The double arrow operator, =>, is used for access to arrays. This means that what is on the left side will have the value of what is on the right side. It can be used to set values of an array.
$myArray = array(
0 => 'Big',
1 => 'Small',
2 => 'Up',
3 => 'Down'
);
In javascript, the => is used to define an arrow function.
const square2 = x => x * x;
It’s been more than a week since I last added to these notes. It will eventually need some serious editing.
I’ve been spending the last ten days or so refreshing my memory of javascript. I switched from learning VUE to learning REACT. The reason I did is because I want to eventually build aan extensive timeline that incorporates family history into a timeline with world events/mapping. Webtrees does it some extent. But, I’m envision something more. I came across React Chrono and it seems that it would be very useful for my purposes.
Anyway, that meant learning React. When I began looking at React, the javascript notation was confusing me. I had studied javascript years ago but had forgotten much of it. To get it back, I read over Eloquent Javascript. It’s a great learning manual and is available for free. Many of my examples are lifted from this manual. As noted in the manual: Licensed under a Creative Commons attribution-noncommercial license. All code in this book may also be considered licensed under an MIT license. Much of the below javascript info is taken from this manual
Curried Arrow Functions
Here’s an example from an accordian component:
const handleChange = (panel) => (event, isExpanded) => {
setExpanded(isExpanded ? panel : false);
};
Currying arrow functions takes multiple arguments (parameters) and splits them into a sequence of functions that each take an individual argument. See CodeKirei.
This is very complicated to me. Don’t really understand it.
Some Javascript Notation To Know
const square = function(x) {
return x * x;
};
function square(x) {
return x * x;
}
React seems to use a lot of arrow functions.
const square1 = (x) => { return x * x; };
const square2 = x => x * x;
No parameters arrow function
const horn = () => {
console.log("Toot"):
};
Creating An Object
Let day1 = {
squirrel: false,
events: ["work", "touched tree", "pizza", "running"]
};
Inside the braces, there is a list of properties separated by commas. Each property has a name followed by a colon and a value. This means that braces have two meanings in JavaScript. At the start of a statement, they start a block of statements. In any other position, they describe an object.
Arrays, then, are just a kind of object specialized for storing sequences of things.
Destructuring Assignment
The destructuring assignment syntax is a JavaScript expression that makes it possible to unpack values from arrays, or properties from objects, into distinct variables.
This is a complicated concept for me.
Below are a few examples from mdn web docs. To see more.
Array Destructuring [ ] = [ ]
let a, b, rest;
[a, b] = [10, 20];
console.log(a);
// expected output: 10
console.log(b);
// expected output: 20
[a, b, ...rest] = [10, 20, 30, 40, 50];
console.log(rest);
// expected output: Array [30,40,50]
Object Destructuring - const { } =
const user = {
id: 42,
isVerified: true,
};
const { id, isVerified } = user;
console.log(id); // 42
console.log(isVerified); // true
Repeat Function
function repeat(n, action) {
for (let i = 0; i < n; i++) {
action(i);
}
}
Higher-Order Functions
Functions that operate on other functions, either by taking them as arguments or by returning them, are called higher-order functions.
Higher-Order Methods For Arrays
Arrays provide a number of useful higher-order methods. You can use forEach
to loop over the elements in an array. The filter
method returns a new array containing only the elements that pass the predicate function. Transforming an array by putting each element through a function is done with map
. You can use reduce
to combine all the elements in an array into a single value. The some
method tests whether any element matches a given predicate function. And findIndex
finds the position of the first element that matches a predicate.
The difference between forEach()
and map
is that they both will call a function on each element in the array but map
returns a new array and forEach()
does NOT return anything.
Use forEach()
to add the elements of an array to a database. Use map
to create a new array.
forEach() example
let sum = 0;
const numbers = [16, 77, 8];
numbers.forEach(myFunction);
function myFunction(item) {
sum += item;
}
The above example uses forEach
to sum up the numbers in the array.
Map Example
const originalArray = [16, 77, 8];
const newArray = originalArray.map(NameOfFunctionToBeUsed)
function NameOfFunctionToBeUsed(elementsFromArray) {
return elementsFromArray * 5;
}
This uses map
to multiply the numbers in the array by 5 and place them into a new array.
React
From what I’ve learned in the last week, React’s general format in Laravel is:
- import statements
- functions
- export default function statement
import React from 'react';
import ScopedCssBaseline from '@mui/material/ScopedCssBaseline';
import DrawerAppBar4 from './Components/DrawerAppBar4';
import DrawerAppBar4a from './Components/DrawerAppBar4a';
import { Link, Head } from '@inertiajs/inertia-react';
const Murwell = function (x,y) {
return x*y
};
const symp = ["hi","bye"];
const MenuHappy = (g) => symp.at(g);
const Peace = (b) => b*8;
const Choice = function choice (c) {
if (c === 3) {
return ('Yay, I love chocolate ice cream!');
}
else {
return ('Awwww, but chocolate is my favorite…');
}
}
export default function Welcome(props) {
return (
<>
<Head title="Welcome" />
<div >
{props.auth.user ? (
<DrawerAppBar4a></DrawerAppBar4a>
) : (
<>
<DrawerAppBar4></DrawerAppBar4>
</>
)}
</div>
<h1>hi</h1>
<h1>{Murwell(3,4)}</h1>
<h1>{MenuHappy(0)}</h1>
<h1>{Peace(6)}</h1>
<h1>{Choice(4)}</h1>
</>
);
}
This block of code is stored in Laravel at resources/js/Pages/Index.jsx
.
Some components are stored at resources/js/Pages/Components
.
A library of React components was added from Miu Material UI. It was installed by running npm install @mui/material @emotion/react @emotion/styled
.
To use the components, I’d copy an example component from the Miu website and insert the code into a file at resources/js/Pages/Components
.
For example, for a checkbox, I’d copy an example into a file I create at resources/js/Pages/Components/CheckBoxMaterial.jsx
.
To use that component, I’d first import it into a file with the following code: import CheckBoxMaterial from './Components/CheckBoxMaterial';
Then to use it, I can write <CheckBoxMaterial></CheckBoxMaterial>
.
Much of the code above is where I was just practicing writing functions and importing it into the page.
One more complicated section is:
<div >
{props.auth.user ? (
<DrawerAppBar4a></DrawerAppBar4a>
) : (
<>
<DrawerAppBar4></DrawerAppBar4>
</>
)}
</div>
Above a ternary operator is used to return different menus depending on whether the user is logged in.
I’m going to start with a shopping-list app based off of Laravel/React.
Mysql also needs to be installed. Here are the Mac instructions for mysql.
Beginning With Vue.js
One of the many difficulties that I’ve had is figuring out how to create the frontend using Vue.
In the old days when I was doing simple static websites, I’d go to the bootstrap toolkit. Then load the css and js into an html template via cdn, copy a basic template, and I’d be off and running. Like this from Bootstrap.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Bootstrap demo</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-gH2yIJqKdNHPEq0n4Mqa/HGKIhSkIHeL5AyhkYV8i59U5AR6csBvApHHNl/vI1Bx" crossorigin="anonymous">
</head>
<body>
<h1>Hello, world!</h1>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-A3rJD856KowSb7dwlZdYEkO39Gagi7vIsF0jrRAoQmDKKtQBHUuLZ9AsSv4jD4Xa" crossorigin="anonymous"></script>
</body>
</html>
Much of my difficulty with Laravel/React is because of my lack of knowledge (duh). And so, I’ve spent the last couple of days reading documentation.
It’s hard for me to read docs without having some understanding of what I’m reading. So, the last week or so that I’ve spent getting nothing being rendered has been useful into building up some basic knowledge to make the docs somewhat comprehensible.
Structure Of Laravel App
I think understanding the basic structure of Laravel has helped. Installing a barebones Laravel app is easy, understanding how it works is nuts. There are so many parts, my head still hurts.
Anyway, this is my simplistic understanding of the Laravel structure after the app is created.
app.js
located at resources/js/app.js
is very important.
So is resources/views/app.blade.php
and package.json
. Package.json is located at the root of the entire app - not in the app folder.
The place to begin is with package.json
because you can see what dependencies have been loaded/installed.
{
"private": true,
"scripts": {
"dev": "vite",
"build": "vite build"
},
"devDependencies": {
"@headlessui/react": "^1.4.2",
"@inertiajs/inertia": "^0.11.0",
"@inertiajs/inertia-react": "^0.8.0",
"@inertiajs/progress": "^0.2.6",
"@tailwindcss/forms": "^0.5.2",
"@vitejs/plugin-react": "^2.0.0",
"autoprefixer": "^10.4.2",
"axios": "^0.27",
"laravel-vite-plugin": "^0.5.0",
"lodash": "^4.17.19",
"postcss": "^8.4.6",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"tailwindcss": "^3.1.0",
"vite": "^3.0.0"
}
}
devDependicies
are the packages mainly installed automatically by Laravel.
- Inertia which works as the bridge between VUE and Laravel.
- Tailwindscss which is another framework css.
- Vitejs which is the bundler that Laravel now uses.
- autoprefixer looks cool as its a PostCSS plugin which parses CSS and adds vendor prefixes. Lodash is a javascript utility.
- Now, I believe I installed material-design-icons with npm after I installed vueistic (which will be discussed later).
- postcss seems to work with autoprefixer.
Dependencies
are all packages I installed.
Vuestic-UI
Let’s focus on vuestic-ui which a user interface Vue3 framework. I decided to use vueistic because it seems similar to bootstrap and I certainly don’t want to make components from scratch. Nor do I want a hodgepodge of components from different developers.
Once vueistic is installed, its components can be found in Laravel at node_modules/vuestic-ui/dist/esm/src/components
. These components then can be used “as-is” or modified and combined into other components.
The instructions to install vuestic.ui are here.
npm install vuestic-ui
After running this command, vuestic is added as a dependency to ‘package.json’. Like above.
The second instruction is to include Source Sans Pro and Material Icons fonts.
This can done by importing into the <head>
element.
So, I went to app.blades.php
which is found at resources/views
and added in the TWO ASTERISKED Styles Sheets.
<!DOCTYPE html>
<html lang=". . . ">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title inertia> . . . </title>
** <link href="https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,400;1,700&display=swap" rel="stylesheet">
** <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<!-- Scripts -->
@routes
@vite('resources/js/app.js')
@inertiaHead
</head>
<body class="font-sans antialiased">
@inertia
</body>
</html>
Vueistic also indicates that they can be included in css with the following code. I wonder if it would then work by including these lines in vite.config.js
. I’ll have to try it on another day.
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,400;1,700&display=swap');
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
Examining app.blades.php
, Laravel’s structure is beginning to make more sense.
Under scripts, I can see that routes are called.
There’s also something called @inertiaHead
. This is a little confusing. There’s a story from Laravel about inertia-head.. There’s also a story about <head> on inertia. I’m going to have to figure this another day.
Anyway, in app.blades.php
I see that app.js
is referred to with vite (that make sense because vite is the bundler).
And if we go to app.js
in ‘resources/js’, you can see that’s where I added the THREE LINES OF ASTERISKED CODE (**) related to Vuestic.
Two import lines:
import { createVuestic } from 'vuestic-ui'**
import 'vuestic-ui/css'**
And one line that is added to the return
.use(createVuestic()
import './bootstrap';
import '../css/app.css';
**import { createVuestic } from 'vuestic-ui'**
**import 'vuestic-ui/css'**
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/inertia-vue3';
import { InertiaProgress } from '@inertiajs/progress';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m';
const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel';
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
setup({ el, app, props, plugin }) {
return createApp({ render: () => h(app, props) })
.use(plugin)
.use(ZiggyVue, Ziggy)
**.use(createVuestic())**
.mount(el);
},
});
InertiaProgress.init({ color: '#4B5563' });
So, now that app.js
is where I think it should be, Vueistic components can be added to html code in vue just like bootstrap 5.
I can now type in the name of vueistic components and they will appear.
<va-switch></va-switch>
But default components usually need to be modified or combined.
To make it easier to reuse these modified components, one can just make another component from the basic components.
How To Make A Vue Component For Laravel
In resources/js/Components
make a file: component_name.vue
The structure within the file is:
<script setup>
Import if needed;
Function if needed;
</script>
<template>
Html code goes here
</template>
[optional css coding]
<style scoped>
.class {
Attribute: ;
}
#id {
Attribute: ;
}
</style>
Once the component is made, just go to the vue file in js/pages
and insert the component where it should go.
Here’s a simple example of a component that only has taken me about a week to figure out. The navbar color is #FFFF00 (yellow). And the id #greenman changes the text color to green. Also, note that the tag is <style scoped>
. By using scoped
, the css is limited to just this file.
<script setup>
</script>
<template>
<va-navbar color="#FFFF00" shape class="mb-2">
<template #left>
<va-navbar-item id="greenman">Green Color</va-navbar-item>
</template>
<template #center>
<va-navbar-item>Primary</va-navbar-item>
</template>
<template #right>
<va-navbar-item>Right</va-navbar-item>
</template>
</va-navbar>
</template>
<style scoped>
#greenman {
Color:green;
}
</style>
I named this file NewNavBar.vue
and placed it in resources/js/Components
.
Now, if I need to use it in some file in resources/js/Pages
, like HappyPage.vue, I can just type in
Eureka!!! It works!!!
Okay, now that I got Vue all figured out (yeah, right), it’s time to move on.
TO BE CONTINUED