Skip to content

Plantillas

En este apartado crearemos las plantillas necesarias para hacer un ejemplo del CRUD tipo Intermedio.

Imagine que usted creo en una base de datos PostgreSQL una tabla llamada tasks a partir de una migración de Laravel como la siguiente.

php
<?php
// omitted for brevity ...
public function up(): void
{
    Schema::create('tasks', function (Blueprint $table) {
        $table->id();
        $table->string('title', 50);
        $table->text('description');
        $table->boolean('done')->default(false);
        $table->softDeletes();
        $table->timestamps();
    });
}
// omitted for brevity ...

Configuración

Para la respectiva configuración necesitará realizar algo como lo siguiente.

.env

sh
DB_DATABASE=crud_todo_db
## omitted for brevity ...

PATH_USER=/home/username/crud-todo
PATH_BACKEND=api-laravel
PATH_BACKEND_ROOT=app
PATH_FRONTEND=spa-vue
PATH_FRONTEND_ROOT=src
PATH_MODULE=

CRUD_SCHEMA=public
CRUD_TABLE_MASTER=tasks
CRUD_TABLE_MASTER_IS_HELPER=FALSE
CRUD_STACK_BACKEND=my-backend-example
CRUD_STACK_FRONTEND=my-frontend-example
## omitted for brevity ...

stack/my-backend-example

Empezaremos con las plantillas del backend.

Para el lado del backend esta será la plantilla del route.

📃./src/stack/my-backend-example/templates/route

txt
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\<%= fn.uCamelCase(fn.singular(tableMaster)); %>Controller;

Route::prefix('<%= tableMaster; %>')->group(function () {
    Route::get('/', [<%= fn.uCamelCase(fn.singular(tableMaster)); %>Controller::class, 'index']);
    Route::get('/{<%= fn.singular(tableMaster); %>}', [<%= fn.uCamelCase(fn.singular(tableMaster)); %>Controller::class, 'show']); 
    Route::post('/', [<%= fn.uCamelCase(fn.singular(tableMaster)); %>Controller::class, 'store']);
    Route::put('/{<%= fn.singular(tableMaster); %>}', [<%= fn.uCamelCase(fn.singular(tableMaster)); %>Controller::class, 'update']);
    Route::delete('/{<%= fn.singular(tableMaster); %>}', [<%= fn.uCamelCase(fn.singular(tableMaster)); %>Controller::class,'destroy']);
});

Esta plantilla deberá generar el siguiente archivo.

📃/home/username/crud-todo/api-laravel/app/Routes/apiTask.php

php
<?php

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\TaskController;

Route::prefix('tasks')->group(function () {
    Route::get('/', [TaskController::class, 'index']);
    Route::get('/{task}', [TaskController::class, 'show']); 
    Route::post('/', [TaskController::class, 'store']);
    Route::put('/{task}', [TaskController::class, 'update']);
    Route::delete('/{task}', [TaskController::class,'destroy']);
});

TIP

Tome en cuenta que las rutas es el único código que deberá cortar y pegar respectivamente en el lugar que corresponde. Es decir que, debe actualizar su correspondiente archivo /home/username/crud-todo/api-laravel/routes/api.php con el código anterior.


Continuamos con el modelo. Este será la respectivo plantilla.

📃./src/stack/my-backend-example/templates/model

txt
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class <%= fn.uCamelCase(fn.singular(tableMaster)); %> extends Model
{
    use HasFactory, SoftDeletes;
    
    protected $fillable = [<%
        tableStructure.forEach(function(field, index) {
        if  (fn.v.noIdAndExcludeFields(field.column_name)) { %>
        '<%= field.column_name; %>'<%= fn.addCommaToArr(tableStructure, index, 0) -%><%
        }}); %>
    ];
}

Esta plantilla deberá generar el siguiente archivo.

📃/home/username/crud-todo/api-laravel/app/Models/Task.php

php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;

class Task extends Model
{
    use HasFactory, SoftDeletes;
    
    protected $fillable = [
        'title', 
        'description', 
        'done'
    ];
}

Concluimos el backend con el controlador. Esta será el respectivo archivo de plantilla.

📃./src/stack/my-backend-example/templates/controller

txt
<?php

namespace App\Http\Controllers;

use App\Models\<%= fn.uCamelCase(fn.singular(tableMaster)); %>;
use Illuminate\Http\Request;
use \Illuminate\Http\JsonResponse;

class <%= fn.uCamelCase(fn.singular(tableMaster)); %>Controller extends Controller
{
    /** Display a listing of the resource. */
    public function index(): Array
    {
        $<%= tableMaster; %> = <%= fn.uCamelCase(fn.singular(tableMaster)); %>::all()->toArray();
        return array_reverse($<%= tableMaster; %>);
    }

    /** Store a newly created resource in storage. */
    public function store(Request $request): JsonResponse
    {
        $<%= fn.singular(tableMaster); %> = new <%= fn.uCamelCase(fn.singular(tableMaster)); %>([<%
            tableStructure.forEach(function(field, index) {
            if  (fn.v.noIdAndExcludeFields(field.column_name) && !['done'].includes(field.column_name)) { %>
            '<%= field.column_name; %>' => $request-><%= field.column_name; %><%= fn.addCommaToArr(tableStructure, index, 1) -%><%
            }}); %>
        ]);

        $<%= fn.singular(tableMaster); %>->save(); 
        $<%= fn.singular(tableMaster); %>->refresh();
        
        return response()->json([
            "<%= fn.singular(tableMaster); %>Id" => $<%= fn.singular(tableMaster); %>->id,
            "message"=> "The <%= fn.singular(tableMaster); %> successfully stored"
        ], 201);
    }    

    /** Display the specified resource. */
    public function show(<%= fn.uCamelCase(fn.singular(tableMaster)); %> $<%= fn.singular(tableMaster); %>): JsonResponse
    {
        return  response()->json($<%= fn.singular(tableMaster); %>);
    }

    /** Update the specified resource in storage. */
    public function update(Request $request, <%= fn.uCamelCase(fn.singular(tableMaster)); %> $<%= fn.singular(tableMaster); %>): JsonResponse
    {<%
        tableStructure.forEach(function(field) {
        if  (fn.v.noIdAndExcludeFields(field.column_name)) { %>
        $<%= fn.singular(tableMaster); %>-><%= field.column_name; %> = $request-><%= field.column_name; %>;<%
        }}); %>

        $<%= fn.singular(tableMaster); %>->save();

        return response()->json(["message"=> "The <%= fn.singular(tableMaster); %> successfully updated"], 200);
    }   

    /** Remove the specified resource from storage. */
    public function destroy(<%= fn.uCamelCase(fn.singular(tableMaster)); %> $<%= fn.singular(tableMaster); %>): JsonResponse
    {
        $<%= fn.singular(tableMaster); %>->delete();        
 
        return response()->json([], 204);
    }
    
}

Esta plantilla deberá generar el siguiente archivo.

📃/home/username/crud-todo/api-laravel/app/Http/Controllers/TaskController.php

php
<?php

namespace App\Http\Controllers;

use App\Models\Task;
use Illuminate\Http\Request;
use \Illuminate\Http\JsonResponse;

class TaskController extends Controller
{
    /** Display a listing of the resource. */
    public function index(): Array
    {
        $tasks = Task::all()->toArray();
        return array_reverse($tasks);
    }

    /** Store a newly created resource in storage. */
    public function store(Request $request): JsonResponse
    {
        $task = new Task([
            'title' => $request->title, 
            'description' => $request->description
        ]);

        $task->save(); 
        $task->refresh();
        
        return response()->json([
            "taskId" => $task->id,
            "message"=> "The task successfully stored"
        ], 201);
    }    

    /** Display the specified resource. */
    public function show(Task $task): JsonResponse
    {
        return  response()->json($task);
    }

    /** Update the specified resource in storage. */
    public function update(Request $request, Task $task): JsonResponse
    {
        $task->title = $request->title;
        $task->description = $request->description;
        $task->done = $request->done;

        $task->save();

        return response()->json(["message"=> "The task successfully updated"], 200);
    }   

    /** Remove the specified resource from storage. */
    public function destroy(Task $task): JsonResponse
    {
        $task->delete();        
 
        return response()->json([], 204);
    }
    
}

Si ha llegado hasta aquí, entonces ya debe tener construido su respectivo código de la API que funcionará como backend para el CRUD de Tareas.

stack/my-frontend-example

Para el lado del frontend esta será la plantilla que genera la vista datagrid.

📃./src/stack/my-frontend-example/templates/masterDatagrid

txt
<script lang="ts">
import { defineComponent } from 'vue'
import axios from 'axios'

export default defineComponent({
  data() {
    return {
      <%= tableMaster; %>: []
    }
  },
  mounted() {
    this.get<%= fn.uCamelCase(tableMaster); %>();
  },
  methods: {
    get<%= fn.uCamelCase(tableMaster); %>() {
      axios
        .get('http://localhost:8000/api/<%= tableMaster; %>')
        .then(response => this.<%= tableMaster; %> = response.data )
        .catch(
          error => console.log({
            errorCode: error.code, errorMessage: error.message
          })
        );
    },
    remove<%= fn.uCamelCase(fn.singular(tableMaster)); %>(id) {
      axios
        .delete(`http://localhost:8000/api/<%= tableMaster; %>/${id}`)
        .then(response => {
          console.log({ statusCode: response.status })
          if (response.status===204)
            this.get<%= fn.uCamelCase(tableMaster); %>();
          })
        .catch(
          error => console.log({
            errorCode: error.code, errorMessage: error.message
          })
        );
    }
  }
})
</script>

<template>
  <div class="container mx-auto">
    <h1 class="text-2xl" align="center">ToDo List</h1>    
    <router-link
      :to="{name: 'create'}"
      class="btn btn-primary"
      >Create
    </router-link>
    <table class="min-w-full text-left text-sm font-light">
      <thead class="border-b font-medium dark:border-neutral-500">
        <tr><%
          tableStructure.forEach(function(field){%>
          <th class=""><%= field.column_name; %></th><%
          }); %>
          <th class="p-2">Actions</th>
        </tr>
      </thead>
      <tbody>
        <tr
          v-for="<%= fn.singular(tableMaster); %> in <%= tableMaster; %>"
          :key="<%= fn.singular(tableMaster); %>.id"
          class="border-b dark:border-neutral-500"
        ><%
        tableStructure.forEach((field) => { %>
          <td class="">{{ <%= fn.singular(tableMaster); %>.<%= field.column_name; %> }}</td><%});
        %>        
          <td class="p-2">
            <button
              class="btn btn-success m-1 text-sm"
              @click="$router.push({name: 'edit', params: {id: <%= fn.singular(tableMaster); %>.id}})"
            >
              Edit
            </button>
            <button
              class="btn btn-danger m-1 text-sm"
              @click="remove<%= fn.uCamelCase(fn.singular(tableMaster)); %>(<%= fn.singular(tableMaster); %>.id)"
            >
              Delete
            </button>
          </td>
        </tr>
      </tbody>
    </table>
    <h4 v-if="<%= tableMaster; %>.length === 0">Empty list.</h4>
  </div>
</template>

Esta plantilla deberá generar el siguiente archivo.

📃/home/username/crud-todo/spa-vue/src/views/Task/Index.vue

vue
<script lang="ts">
import { defineComponent } from 'vue'
import axios from 'axios'

export default defineComponent({
  data() {
    return {
      tasks: []
    }
  },
  mounted() {
    this.getTasks();
  },
  methods: {
    getTasks() {
      axios
        .get('http://localhost:8000/api/tasks')
        .then(response => this.tasks = response.data )
        .catch(
          error => console.log({
            errorCode: error.code, errorMessage: error.message
          })
        );
    },
    removeTask(id) {
      axios
        .delete(`http://localhost:8000/api/tasks/${id}`)
        .then(response => {
          console.log({ statusCode: response.status })
          if (response.status===204)
            this.getTasks();
          })
        .catch(
          error => console.log({
            errorCode: error.code, errorMessage: error.message
          })
        );
    }
  }
})
</script>

<template>
  <div class="container mx-auto">
    <h1 class="text-2xl" align="center">ToDo List</h1>    
    <router-link
      :to="{name: 'create'}"
      class="btn btn-primary"
      >Create
    </router-link>
    <table class="min-w-full text-left text-sm font-light">
      <thead class="border-b font-medium dark:border-neutral-500">
        <tr>
          <th class="">id</th>
          <th class="">title</th>
          <th class="">description</th>
          <th class="">done</th>
          <th class="p-2">Actions</th>
        </tr>
      </thead>
      <tbody>
        <tr
          v-for="task in tasks"
          :key="task.id"
          class="border-b dark:border-neutral-500"
        >
          <td class="">{{ task.id }}</td>
          <td class="">{{ task.title }}</td>
          <td class="">{{ task.description }}</td>
          <td class="">{{ task.done }}</td>        
          <td class="p-2">
            <button
              class="btn btn-success m-1 text-sm"
              @click="$router.push({name: 'edit', params: {id: task.id}})"
            >
              Edit
            </button>
            <button
              class="btn btn-danger m-1 text-sm"
              @click="removeTask(task.id)"
            >
              Delete
            </button>
          </td>
        </tr>
      </tbody>
    </table>
    <h4 v-if="tasks.length === 0">Empty list.</h4>
  </div>
</template>

Esta será la plantilla que genera la vista CreateOrEdit.

📃./src/stack/my-frontend-example/templates/masterCreateOrEdit

txt
<script lang="ts">
import { defineComponent } from 'vue'
import axios from 'axios'
import Form<%= fn.uCamelCase(fn.singular(tableMaster)); %> from '../../components/<%= fn.uCamelCase(fn.singular(tableMaster)); %>/Form<%= fn.uCamelCase(fn.singular(tableMaster)); %>.vue'

export default defineComponent({
  props: {
    id: String
  },
  components: {
    Form<%= fn.uCamelCase(fn.singular(tableMaster)); %>
  },
  data() {
    return {
      <%= fn.singular(tableMaster); %>: {}
    }
  },
  mounted() {
    if (this.$props.id)
      this.get<%= fn.uCamelCase(fn.singular(tableMaster)); %>();
  },
  computed: {
    isRenderable() {
        return (this.$props.id && Object.keys(this.<%= fn.singular(tableMaster); %>).length > 0)
          || this.$props.id===undefined
    }
  },  
  methods: {
    get<%= fn.uCamelCase(fn.singular(tableMaster)); %>() {
      axios
        .get(`http://localhost:8000/api/<%= tableMaster; %>/${this.$props.id}`)
        .then(response => this.<%= fn.singular(tableMaster); %> = response.data )
        .catch(
          error => console.log({
            errorCode: error.code, errorMessage: error.message
          })
        );
    },
    submit(payload) {
      if (this.$props.id===undefined) {
        axios
          .post("http://localhost:8000/api/<%= tableMaster; %>", payload)
          .then(response => (
            this.$router.push({name: 'index'})            
          ))
          .catch(error => console.log(error))
          //.finally(() => this.pending = false)
      } else {
        axios
          .put(`http://localhost:8000/api/<%= tableMaster; %>/${this.$props.id}`, payload)
          .then(response => (
            this.$router.push({name: 'index'})            
          ))
          .catch(error => console.log(error))
          //.finally(() => this.pending = false)
      }
    }  
  }
})
</script>

<template>
  <div class="container row col-md-6 mx-auto w-1/2">
    <h1 class="text-2xl" align="center">
      {{$props.id ? 'Editing' : 'Creating'}} Tast
    </h1>
    <Form<%= fn.uCamelCase(fn.singular(tableMaster)); %> v-if="isRenderable" :<%= fn.singular(tableMaster); %>="<%= fn.singular(tableMaster); %>" @submit='submit' />
  </div>
</template>

Esta plantilla deberá generar el siguiente archivo.

📃/home/username/crud-todo/spa-vue/src/views/Task/CreateOrEdit.vue

vue
<script lang="ts">
import { defineComponent } from 'vue'
import axios from 'axios'
import FormTask from '../../components/Task/FormTask.vue'

export default defineComponent({
  props: {
    id: String
  },
  components: {
    FormTask
  },
  data() {
    return {
      task: {}
    }
  },
  mounted() {
    if (this.$props.id)
      this.getTask();
  },
  computed: {
    isRenderable() {
        return (this.$props.id && Object.keys(this.task).length > 0)
          || this.$props.id===undefined
    }
  },  
  methods: {
    getTask() {
      axios
        .get(`http://localhost:8000/api/tasks/${this.$props.id}`)
        .then(response => this.task = response.data )
        .catch(
          error => console.log({
            errorCode: error.code, errorMessage: error.message
          })
        );
    },
    submit(payload) {
      if (this.$props.id===undefined) {
        axios
          .post("http://localhost:8000/api/tasks", payload)
          .then(response => (
            this.$router.push({name: 'index'})            
          ))
          .catch(error => console.log(error))
          //.finally(() => this.pending = false)
      } else {
        axios
          .put(`http://localhost:8000/api/tasks/${this.$props.id}`, payload)
          .then(response => (
            this.$router.push({name: 'index'})            
          ))
          .catch(error => console.log(error))
          //.finally(() => this.pending = false)
      }
    }  
  }
})
</script>

<template>
  <div class="container row col-md-6 mx-auto w-1/2">
    <h1 class="text-2xl" align="center">
      {{$props.id ? 'Editing' : 'Creating'}} Tast
    </h1>
    <FormTask v-if="isRenderable" :task="task" @submit='submit' />
  </div>
</template>

Y esta será la plantilla que genera el componente FormTask.

📃./src/stack/my-frontend-example/templates/masterForm

txt
<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    <%= fn.singular(tableMaster); %>: Object    
  },
  data() {
    return {
      form: this.$props.<%= fn.singular(tableMaster); %>
    }
  },
  emits: ['submit'],
  methods: {
    submit() {          
      this.$emit('submit', this.form )
    }
  }
})
</script>

<template>
  <form @submit.prevent="submit">
    <% tableStructure.forEach(function(field) {
      if  (!['id', 'created_at', 'updated_at', 'deleted_at'].includes(field.column_name)) {
      if (field.data_type === "character varying") {%><div class="m-2">
      <label><%= fn.uCamelCase(field.column_name); %></label>
      <input type="text" v-model="form.<%= field.column_name; %>">
    </div>        
    <%} else if ((field.data_type === "text")) {%><div class="m-4">          
      <label><%= fn.uCamelCase(field.column_name); %></label>
      <textarea v-model="form.<%= field.column_name; %>"></textarea>
    </div>
    <%} else if ((field.data_type === "boolean")) {%><div class="m-4">
      <label><%= fn.uCamelCase(field.column_name); %></label>
      <input type="checkbox" v-model="form.<%= field.column_name; %>"/>
    </div>
    <%}%><%}});%>
    <button type="submit" class="btn btn-primary m-2">
      Save
    </button>
  </form>
</template>

Esta plantilla deberá generar el siguiente archivo.

📃/home/username/crud-todo/spa-vue/src/components/Task/FormTask.vue

vue
<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    task: Object    
  },
  data() {
    return {
      form: this.$props.task
    }
  },
  emits: ['submit'],
  methods: {
    submit() {          
      this.$emit('submit', this.form )
    }
  }
})
</script>

<template>
  <form @submit.prevent="submit">
    <div class="m-2">
      <label>Title</label>
      <input type="text" v-model="form.title">
    </div>        
    <div class="m-4">          
      <label>Description</label>
      <textarea v-model="form.description"></textarea>
    </div>
    <div class="m-4">
      <label>Done</label>
      <input type="checkbox" v-model="form.done"/>
    </div>
    
    <button type="submit" class="btn btn-primary m-2">
      Save
    </button>
  </form>
</template>

Si ha llegado hasta aquí, entonces ya debe tener construido también su respectivo código de SPA que funcionará como frontend para el CRUD de Tareas.

Released under the MIT License.