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);

PreviousHướng tiếp cận dynamic_fields use post (ok)NextCấu hình tên miền ảo chạy trên local .htaccess (ok)
Last updated
Was this helpful?