Thêm trường động cho trang bằng js (ok)

database\migrations\2025_06_02_025221_create_pages_table.php

<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
  /**
   * Run the migrations.
   */
  public function up(): void
  {
    Schema::create('pages', function (Blueprint $table) {
      $table->id();
      $table->string('title');
      $table->string('slug')->unique();
      $table->json('fields')->nullable(); // chứa các trường tùy biến
      $table->timestamps();
    });
  }
  /**
   * Reverse the migrations.
   */
  public function down(): void
  {
    Schema::dropIfExists('pages');
  }
};

app\Http\Controllers\Admin\PageController.php

public function store(Request $request)
  {
    $raw = json_decode($request->input('fields_json'), true);
    Page::create([
      'title' => $request->input('page_title'),
      'slug' => Str::slug($request->input('page_title')),
      'fields' => $raw, // ✅ Đây là format bạn muốn
    ]);
    return redirect()->route('pages.index')->with('success', 'Tạo trang thành công!');
  }

app\Models\Page.php

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Page extends Model
{
  protected $casts = [
    'fields' => 'array',
  ];
  protected $fillable = [
    'title',
    'slug',
    'fields'
  ];
}

resources\views\admin\pages\create.blade.php

@extends('layouts.admin')
@section('content')
<div class="p-4">
  @if(session('success'))
  <div class="text-green-600">{{ session('success') }}</div>
  @endif
  <form method="POST" action="{{route('pages.store')}}">
    @csrf
    <div class="mb-4">
      <label class="block font-bold">Tên Trang</label>
      <input name="page_title" class="form-input w-full border" required>
    </div>
    <ul id="field-list" class="list-group mb-3">
      {{-- Field items sẽ được thêm vào đây --}}
    </ul>
    <button type="button" class="btn btn-primary" onclick="addField('text')">+ Text</button>
    <button type="button" class="btn btn-primary" onclick="addField('textarea')">+ Textarea</button>
    <button type="button" class="btn btn-primary" onclick="addField('image')">+ Image</button>
    <input type="hidden" name="fields_json" id="fields-input">
    <button class="btn btn-primary">Lưu</button>
  </form>
</div>
@endsection
@section('scripts')
<script src="https://cdn.ckeditor.com/ckeditor5/41.0.0/classic/ckeditor.js"></script>
<script>
  document.querySelectorAll('.ckeditor').forEach(el => {
    ClassicEditor.create(el).catch(console.error);
  });
  console.log('CKEditor initialized');
</script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<script>
  const fieldList = document.getElementById('field-list');
  const fieldsInput = document.getElementById('fields-input');
  let fields = [];
  function slugify(text) {
    return text.toLowerCase()
      .normalize('NFD').replace(/[\u0300-\u036f]/g, '') // Remove accents
      .replace(/[^a-z0-9]+/g, '_') // Replace non-alphanum with _
      .replace(/^_+|_+$/g, ''); // Trim _
  }
  function renderFields() {
    fieldList.innerHTML = '';
    fields.forEach((field, index) => {
      const li = document.createElement('li');
      li.className = 'list-group-item';
      let valueInput = '';
      if (field.type === 'textarea') {
        valueInput = `<textarea class="form-control mb-1" oninput="updateField(${index}, 'value', this.value)">${field.value || ''}</textarea>`;
      } else if (field.type === 'image') {
        valueInput = `<input type="url" placeholder="https://..." class="form-control mb-1" value="${field.value || ''}" oninput="updateField(${index}, 'value', this.value)">`;
      } else {
        valueInput = `<input type="text" class="form-control mb-1" value="${field.value || ''}" oninput="updateField(${index}, 'value', this.value)">`;
      }
      li.innerHTML = `
      <strong>${field.type.toUpperCase()}</strong><br>
      <label>Tên (name):</label>
      <input type="text" class="form-control mb-1" value="${field.name}" oninput="updateField(${index}, 'name', this.value)">
      <label>Giá trị:</label>
      ${valueInput}
      <button type="button" class="btn btn-danger btn-sm mt-1" onclick="removeField(${index})">Xoá</button>
    `;
      fieldList.appendChild(li);
    });
    fieldsInput.value = JSON.stringify(fields, null, 2);
  }
  // Tạo object từ fields
  function updateHiddenInput() {
    const transformed = fields
      .filter(f => f.name)
      .map(f => ({
        [f.name]: f.value || ''
      }));
    fieldsInput.value = JSON.stringify(transformed, null, 2);
  }
  const output = {};
  fields.forEach(f => {
    if (f.name) output[f.name] = f.value || '';
  });
  fieldsInput.value = JSON.stringify(output, null, 2); // thêm indent cho dễ debug
  function addField(type) {
    fields.push({
      type
      , name: ''
      , value: ''
    });
    renderFields();
    updateHiddenInput();
  }
  function removeField(index) {
    fields.splice(index, 1);
    renderFields();
  }
  function updateField(index, key, value, manual = false) {
    fields[index][key] = value;
    updateHiddenInput(); // ✅ Chỉ cập nhật dữ liệu JSON
  }
  new Sortable(fieldList, {
    animation: 150
    , onEnd: () => {
      const newOrder = [];
      document.querySelectorAll('#field-list li').forEach((li, i) => {
        newOrder.push(fields[i]);
      });
      fields = newOrder;
      renderFields();
    }
  });
</script>
@endsection

resources\views\layouts\admin.blade.php

<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <!-- CSRF Token -->
  <meta name="csrf-token" content="{{ csrf_token() }}">
  <title>{{ config('app.name', 'Laravel') }}</title>
  <!-- Scripts -->
  @vite(['resources/sass/app.scss','resources/css/app.css', 'resources/js/app.js'])
</head>
<body>
  <div id="app">
    <main class="py-4">
      @yield('content')
    </main>
  </div>
  @yield('scripts')
</body>
</html>

routes\web.php

<?php
use App\Http\Controllers\Admin\PageController;
use App\Models\Page;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
  return view('welcome');
});
Auth::routes();
Route::get('/home', [App\Http\Controllers\HomeController::class, 'index'])->name('home');
Route::get('/api/pages/{slug}', function ($slug) {
  $page = Page::where('slug', $slug)->firstOrFail();
  return response()->json([
    'title' => $page->title,
    'slug' => $page->slug,
    'fields' => $page->fields
  ]);
});
Route::resource('pages', PageController::class);

Last updated

Was this helpful?