Cara Membuat Form Input Dinamis dengan PHP dan JavaScript

| | PHP
Cara Membuat Form Input Dinamis dengan PHP dan JavaScript

Form input dinamis adalah form yang field inputnya bisa bertambah atau berkurang secara otomatis tanpa perlu reload halaman. Form ini sangat berguna untuk kasus seperti input data gaji karyawan, daftar pesanan, atau data keluarga yang jumlahnya tidak tetap.

Dalam tutorial ini, kita akan membuat form input dinamis dengan PHP dan JavaScript yang bisa menambah baris input baru, menghapus baris, dan menyimpan semua data ke database MySQL sekaligus.

Kasus Penggunaan Form Dinamis

  • Input nilai siswa — jumlah siswa bisa berubah setiap semester
  • Daftar pesanan — customer bisa tambah item pesanan
  • Data keluarga — input data anggota keluarga yang jumlahnya bervariasi
  • Invoice/faktur — tambah baris item dalam invoice
  • Jadwal kuliah — input jadwal per hari yang bisa ditambah

Persiapan

  • XAMPP/Laragon yang sudah berjalan
  • PHP 7.4+
  • MySQL

Langkah 1: Membuat Database

CREATE DATABASE form_dinamis_db;
USE form_dinamis_db;

CREATE TABLE pesanan (
    id INT AUTO_INCREMENT PRIMARY KEY,
    no_pesanan VARCHAR(20) NOT NULL,
    nama_pelanggan VARCHAR(100) NOT NULL,
    tanggal DATE NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE detail_pesanan (
    id INT AUTO_INCREMENT PRIMARY KEY,
    pesanan_id INT NOT NULL,
    nama_produk VARCHAR(100) NOT NULL,
    jumlah INT NOT NULL,
    harga INT NOT NULL,
    subtotal INT GENERATED ALWAYS AS (jumlah * harga) STORED,
    FOREIGN KEY (pesanan_id) REFERENCES pesanan(id) ON DELETE CASCADE
);

Langkah 2: Form Input Dinamis (HTML + JavaScript)

Buat file index.php:

<?php
require_once 'config/database.php';

$pesan = '';

// Proses form saat disubmit
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $no_pesanan = trim($_POST['no_pesanan']);
    $nama_pelanggan = trim($_POST['nama_pelanggan']);
    $tanggal = $_POST['tanggal'];
    $produk = $_POST['produk'] ?? [];
    $jumlah = $_POST['jumlah'] ?? [];
    $harga = $_POST['harga'] ?? [];

    // Validasi
    if (empty($no_pesanan) || empty($nama_pelanggan) || empty($tanggal)) {
        $pesan = '<div class="alert alert-danger">Semua field utama harus diisi!</div>';
    } elseif (empty($produk) || count($produk) === 0) {
        $pesan = '<div class="alert alert-danger">Minimal satu item pesanan harus diisi!</div>';
    } else {
        try {
            $pdo->beginTransaction();

            // Simpan data pesanan
            $stmt = $pdo->prepare('INSERT INTO pesanan (no_pesanan, nama_pelanggan, tanggal) VALUES (?, ?, ?)');
            $stmt->execute([$no_pesanan, $nama_pelanggan, $tanggal]);
            $pesanan_id = $pdo->lastInsertId();

            // Simpan detail pesanan
            $stmt_detail = $pdo->prepare('INSERT INTO detail_pesanan (pesanan_id, nama_produk, jumlah, harga) VALUES (?, ?, ?, ?)');
            $total_semua = 0;

            for ($i = 0; $i < count($produk); $i++) {
                if (!empty($produk[$i])) {
                    $jml = (int)($jumlah[$i] ?? 0);
                    $hrg = (int)($harga[$i] ?? 0);
                    $subtotal = $jml * $hrg;
                    $total_semua += $subtotal;

                    $stmt_detail->execute([$pesanan_id, $produk[$i], $jml, $hrg]);
                }
            }

            $pdo->commit();
            $pesan = '<div class="alert alert-success">Pesanan <strong>' . htmlspecialchars($no_pesanan) . '</strong> berhasil disimpan! Total: Rp ' . number_format($total_semua, 0, ',', '.') . '</div>';
        } catch (Exception $e) {
            $pdo->rollBack();
            $pesan = '<div class="alert alert-danger">Gagal menyimpan: ' . $e->getMessage() . '</div>';
        }
    }
}

// Generate nomor pesanan otomatis
$no_urut = $pdo->query("SELECT COUNT(*) + 1 FROM pesanan WHERE DATE(created_at) = CURDATE()")->fetchColumn();
$no_pesanan_otomatis = 'PSN-' . date('Ymd') . '-' . str_pad($no_urut, 4, '0', STR_PAD_LEFT);
?>
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Form Input Pesanan Dinamis</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: 'Segoe UI', Arial, sans-serif; background: #f3f4f6; padding: 30px; }

        .container { max-width: 900px; margin: 0 auto; }

        h1 { color: #1e293b; margin-bottom: 20px; }

        .card {
            background: white;
            border-radius: 12px;
            padding: 30px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.08);
            margin-bottom: 20px;
        }

        .form-row {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 15px;
            margin-bottom: 15px;
        }

        .form-group { margin-bottom: 15px; }
        .form-group label { display: block; margin-bottom: 5px; font-weight: 600; color: #374151; font-size: 14px; }
        .form-group input, .form-group select {
            width: 100%;
            padding: 10px 12px;
            border: 1px solid #d1d5db;
            border-radius: 6px;
            font-size: 14px;
        }
        .form-group input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59,130,246,0.1); }

        /* Tabel Item Dinamis */
        .item-table { width: 100%; border-collapse: collapse; margin-top: 10px; }
        .item-table th {
            background: #f1f5f9;
            padding: 10px;
            text-align: left;
            font-size: 13px;
            color: #475569;
            border-bottom: 2px solid #e2e8f0;
        }
        .item-table td { padding: 8px; border-bottom: 1px solid #f1f5f9; vertical-align: middle; }
        .item-table input {
            width: 100%;
            padding: 8px 10px;
            border: 1px solid #d1d5db;
            border-radius: 4px;
            font-size: 14px;
        }
        .item-table input:focus { outline: none; border-color: #3b82f6; }

        .col-no { width: 40px; }
        .col-produk { width: 35%; }
        .col-jumlah { width: 100px; }
        .col-harga { width: 150px; }
        .col-subtotal { width: 150px; }
        .col-aksi { width: 50px; }

        .btn {
            display: inline-block;
            padding: 8px 16px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            text-decoration: none;
            transition: all 0.3s;
        }
        .btn-primary { background: #3b82f6; color: white; }
        .btn-primary:hover { background: #2563eb; }
        .btn-success { background: #22c55e; color: white; }
        .btn-success:hover { background: #16a34a; }
        .btn-danger { background: #ef4444; color: white; padding: 6px 10px; font-size: 12px; }
        .btn-danger:hover { background: #dc2626; }
        .btn-add { background: #8b5cf6; color: white; margin-top: 10px; }
        .btn-add:hover { background: #7c3aed; }

        .total-row {
            text-align: right;
            padding: 15px 10px;
            font-size: 18px;
            font-weight: bold;
            color: #1e293b;
            border-top: 2px solid #e2e8f0;
        }

        .alert { padding: 12px 18px; border-radius: 8px; margin-bottom: 20px; }
        .alert-success { background: #f0fdf4; color: #16a34a; border: 1px solid #bbf7d0; }
        .alert-danger { background: #fef2f2; color: #dc2626; border: 1px solid #fecaca; }

        .action-buttons { display: flex; gap: 10px; margin-top: 20px; }

        @media (max-width: 768px) {
            .form-row { grid-template-columns: 1fr; }
            .item-table { font-size: 12px; }
            .item-table input { padding: 6px 8px; font-size: 12px; }
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Form Input Pesanan</h1>

        <?= $pesan ?>

        <form method="POST" id="formPesanan">
            <!-- Data Pesanan -->
            <div class="card">
                <h2 style="margin-bottom:15px; font-size:18px;">Data Pesanan</h2>
                <div class="form-row">
                    <div class="form-group">
                        <label>No. Pesanan</label>
                        <input type="text" name="no_pesanan" value="<?= htmlspecialchars($no_pesanan_otomatis) ?>" readonly style="background:#f9fafb;">
                    </div>
                    <div class="form-group">
                        <label>Tanggal</label>
                        <input type="date" name="tanggal" value="<?= date('Y-m-d') ?>" required>
                    </div>
                </div>
                <div class="form-group">
                    <label>Nama Pelanggan</label>
                    <input type="text" name="nama_pelanggan" placeholder="Masukkan nama pelanggan" required>
                </div>
            </div>

            <!-- Item Pesanan (Dinamis) -->
            <div class="card">
                <h2 style="margin-bottom:15px; font-size:18px;">Item Pesanan</h2>
                <table class="item-table" id="tabelItem">
                    <thead>
                        <tr>
                            <th class="col-no">No</th>
                            <th class="col-produk">Nama Produk</th>
                            <th class="col-jumlah">Jumlah</th>
                            <th class="col-harga">Harga Satuan</th>
                            <th class="col-subtotal">Subtotal</th>
                            <th class="col-aksi"></th>
                        </tr>
                    </thead>
                    <tbody id="bodyItem">
                        <tr class="item-row">
                            <td class="row-no">1</td>
                            <td><input type="text" name="produk[]" placeholder="Nama produk" required></td>
                            <td><input type="number" name="jumlah[]" value="1" min="1" class="input-jumlah" required></td>
                            <td><input type="number" name="harga[]" value="0" min="0" class="input-harga" required></td>
                            <td class="cell-subtotal">Rp 0</td>
                            <td><button type="button" class="btn btn-danger btn-hapus" title="Hapus">&times;</button></td>
                        </tr>
                    </tbody>
                </table>

                <button type="button" class="btn btn-add" id="btnTambah">+ Tambah Item</button>

                <div class="total-row">
                    Total: <span id="totalSemua">Rp 0</span>
                </div>
            </div>

            <!-- Tombol Aksi -->
            <div class="action-buttons">
                <button type="submit" class="btn btn-success">Simpan Pesanan</button>
                <a href="index.php" class="btn btn-primary">Reset Form</a>
            </div>
        </form>
    </div>

    <script>
        let noUrut = 1;

        // Tambah baris item baru
        document.getElementById('btnTambah').addEventListener('click', function() {
            noUrut++;
            const tbody = document.getElementById('bodyItem');
            const tr = document.createElement('tr');
            tr.className = 'item-row';
            tr.innerHTML = `
                <td class="row-no">${noUrut}</td>
                <td><input type="text" name="produk[]" placeholder="Nama produk" required></td>
                <td><input type="number" name="jumlah[]" value="1" min="1" class="input-jumlah" required></td>
                <td><input type="number" name="harga[]" value="0" min="0" class="input-harga" required></td>
                <td class="cell-subtotal">Rp 0</td>
                <td><button type="button" class="btn btn-danger btn-hapus" title="Hapus">&times;</button></td>
            `;
            tbody.appendChild(tr);
            bindEvents(tr);
        });

        // Hapus baris item
        function bindEvents(row) {
            row.querySelector('.btn-hapus').addEventListener('click', function() {
                const rows = document.querySelectorAll('.item-row');
                if (rows.length > 1) {
                    row.remove();
                    updateNomor();
                    hitungTotal();
                } else {
                    alert('Minimal harus ada satu item!');
                }
            });

            // Hitung subtotal saat input berubah
            row.querySelectorAll('.input-jumlah, .input-harga').forEach(function(input) {
                input.addEventListener('input', function() {
                    hitungSubtotal(row);
                    hitungTotal();
                });
            });
        }

        // Hitung subtotal per baris
        function hitungSubtotal(row) {
            const jumlah = parseInt(row.querySelector('.input-jumlah').value) || 0;
            const harga = parseInt(row.querySelector('.input-harga').value) || 0;
            const subtotal = jumlah * harga;
            row.querySelector('.cell-subtotal').textContent = formatRupiah(subtotal);
        }

        // Hitung total semua item
        function hitungTotal() {
            let total = 0;
            document.querySelectorAll('.item-row').forEach(function(row) {
                const jumlah = parseInt(row.querySelector('.input-jumlah').value) || 0;
                const harga = parseInt(row.querySelector('.input-harga').value) || 0;
                total += jumlah * harga;
            });
            document.getElementById('totalSemua').textContent = formatRupiah(total);
        }

        // Update nomor urut setelah hapus
        function updateNomor() {
            document.querySelectorAll('.row-no').forEach(function(td, index) {
                td.textContent = index + 1;
            });
            noUrut = document.querySelectorAll('.item-row').length;
        }

        // Format angka ke Rupiah
        function formatRupiah(angka) {
            return 'Rp ' + angka.toLocaleString('id-ID');
        }

        // Bind events ke baris pertama
        bindEvents(document.querySelector('.item-row'));
    </script>
</body>
</html>

Penjelasan kode utama:

  • Baris dinamis — tombol “+ Tambah Item” menambahkan baris <tr> baru ke tabel dengan JavaScript createElement
  • Hapus baris — tombol “x” menghapus baris, tapi minimal harus ada 1 baris
  • Hitung subtotal — setiap kali jumlah/harga berubah, subtotal per baris dihitung otomatis
  • Hitung total — total semua item dihitung ulang setiap kali ada perubahan
  • Format Rupiah — angka diformat menjadi “Rp 1.000.000” menggunakan toLocaleString
  • Transaction database — menggunakan beginTransaction() dan commit() untuk memastikan data pesanan dan detail tersimpan bersama atau tidak sama sekali

Langkah 3: Koneksi Database

Buat file config/database.php:

<?php
$host = 'localhost';
$dbname = 'form_dinamis_db';
$username = 'root';
$password = '';

try {
    $pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8", $username, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
    $pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
} catch (PDOException $e) {
    die('Koneksi database gagal: ' . $e->getMessage());
}

Langkah 4: Halaman Daftar Pesanan

Buat file daftar.php untuk melihat pesanan yang sudah disimpan:

<?php
require_once 'config/database.php';

$stmt = $pdo->query('SELECT p.*, (SELECT SUM(d.jumlah * d.harga) FROM detail_pesanan d WHERE d.pesanan_id = p.id) AS total FROM pesanan p ORDER BY p.created_at DESC');
$pesanan_list = $stmt->fetchAll();
?>
<!DOCTYPE html>
<html lang="id">
<head>
    <meta charset="UTF-8">
    <title>Daftar Pesanan</title>
    <style>
        body { font-family: 'Segoe UI', Arial, sans-serif; background: #f3f4f6; padding: 30px; }
        .container { max-width: 900px; margin: 0 auto; }
        table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.08); }
        th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #e5e7eb; }
        th { background: #1e293b; color: white; font-size: 13px; }
        tr:hover { background: #f9fafb; }
        .btn-detail { padding: 5px 12px; background: #3b82f6; color: white; border-radius: 4px; text-decoration: none; font-size: 12px; }
        .btn-back { display: inline-block; margin-bottom: 20px; color: #3b82f6; text-decoration: none; }
    </style>
</head>
<body>
    <div class="container">
        <a href="index.php" class="btn-back">&larr; Kembali ke Form</a>
        <h1>Daftar Pesanan</h1>
        <table>
            <thead>
                <tr>
                    <th>No. Pesanan</th>
                    <th>Tanggal</th>
                    <th>Pelanggan</th>
                    <th>Total</th>
                    <th>Aksi</th>
                </tr>
            </thead>
            <tbody>
                <?php foreach ($pesanan_list as $p): ?>
                <tr>
                    <td><?= htmlspecialchars($p['no_pesanan']) ?></td>
                    <td><?= date('d/m/Y', strtotime($p['tanggal'])) ?></td>
                    <td><?= htmlspecialchars($p['nama_pelanggan']) ?></td>
                    <td>Rp <?= number_format($p['total'], 0, ',', '.') ?></td>
                    <td><a href="detail.php?id=<?= $p['id'] ?>" class="btn-detail">Detail</a></td>
                </tr>
                <?php endforeach; ?>
            </tbody>
        </table>
    </div>
</body>
</html>

Tips Form Input Dinamis

1. Validasi di client dan server JavaScript untuk UX, PHP untuk keamanan. Jangan hanya mengandalkan validasi JavaScript.

2. Gunakan array name di input name="produk[]" membuat PHP menerima data sebagai array, sehingga bisa menampung banyak nilai.

3. Limit jumlah baris Tambahkan batas maksimal baris untuk mencegah abuse:

if (document.querySelectorAll('.item-row').length >= 50) {
    alert('Maksimal 50 item!');
    return;
}

4. Auto-save draft Simpan data ke localStorage agar tidak hilang saat tidak sengaja refresh:

document.getElementById('formPesanan').addEventListener('input', function() {
    localStorage.setItem('draft_pesanan', JSON.stringify(getFormData()));
});

5. Konfirmasi sebelum hapus Selalu tampilkan konfirmasi sebelum menghapus baris:

if (confirm('Yakin hapus item ini?')) {
    row.remove();
}

Praktik Terbaik

Beberapa tips saat menulis kode PHP:

  1. Gunakan prepared statement — Hindari SQL injection dengan menggunakan PDO atau MySQLi prepared statement, jangan concatenate query secara langsung.
  2. Validasi input — Selalu validasi dan sanitasi data dari user menggunakan filter_var(), htmlspecialchars(), atau library validasi.
  3. Error reporting — Saat development, aktifkan error_reporting(E_ALL) dan display_errors untuk memudahkan debugging.
  4. Gunakan autoloading — Manfaatkan Composer autoload atau PSR-4 untuk mengelola file class secara otomatis.
  5. Pisahkan logic dan view — Gunakan pola MVC atau minimal pisahkan kode PHP dan HTML agar lebih rapi dan mudah dipelihara.

Dengan menerapkan praktik di atas, kode PHP Anda akan lebih aman, bersih, dan mudah dikembangkan.

Kesimpulan

Form input dinamis dengan PHP memungkinkan pengguna menambah atau menghapus field input secara otomatis tanpa reload halaman. Teknik utamanya:

  1. JavaScript — menambah/menghapus baris input secara dinamis
  2. Array namename="field[]" untuk mengirim banyak nilai
  3. Loop di PHPfor loop untuk memproses setiap item
  4. TransactionbeginTransaction() untuk memastikan data tersimpan konsisten

Form ini bisa dikembangkan lebih lanjut dengan fitur drag-and-drop untuk mengurutkan item, auto-complete untuk field produk, atau export ke PDF/Excel.


Sigit Nurhanafi avatar
Full-stack developer & technical writer. Berpengalaman di PHP, Laravel, NodeJS, MySQL, dan Python. Aktif menulis tutorial pemrograman dan maintaining open-source projects di PemburuKode sejak 2021.