Basics

Form

Pengantar

Nylo Website v7 menyediakan sistem form yang dibangun di sekitar NyFormWidget. Kelas form Anda meng-extend NyFormWidget dan merupakan widget itu sendiri -- tidak perlu wrapper terpisah. Form mendukung validasi bawaan, banyak tipe field, styling, dan manajemen data.

import 'package:nylo_framework/nylo_framework.dart';

// 1. Define a form
class LoginForm extends NyFormWidget {
  LoginForm({super.key, super.submitButton, super.onSubmit, super.onFailure});

  @override
  fields() => [
    Field.email("Email", validator: FormValidator.email()),
    Field.password("Password", validator: FormValidator.password()),
  ];

  static NyFormActions get actions => const NyFormActions('LoginForm');
}

// 2. Display and submit it
LoginForm(
  submitButton: Button.primary(text: "Login"),
  onSubmit: (data) {
    print(data); // {email: "...", password: "..."}
  },
)

Membuat Form

Gunakan Metro CLI untuk membuat form baru:

metro make:form LoginForm

Ini membuat lib/app/forms/login_form.dart:

import 'package:nylo_framework/nylo_framework.dart';

class LoginForm extends NyFormWidget {
  LoginForm({super.key, super.submitButton, super.onSubmit, super.onFailure});

  @override
  fields() => [
    Field.email("Email", validator: FormValidator.email()),
    Field.password("Password", validator: FormValidator.password()),
  ];

  static NyFormActions get actions => const NyFormActions('LoginForm');
}

Form meng-extend NyFormWidget dan meng-override method fields() untuk mendefinisikan field form. Setiap field menggunakan konstruktor bernama seperti Field.text(), Field.email(), atau Field.password(). Getter static NyFormActions get actions menyediakan cara mudah untuk berinteraksi dengan form dari mana saja di aplikasi Anda.

Menampilkan Form

Karena kelas form Anda meng-extend NyFormWidget, ia merupakan widget itu sendiri. Gunakan langsung di widget tree Anda:

@override
Widget view(BuildContext context) {
  return Scaffold(
    body: SafeArea(
      child: LoginForm(
        submitButton: Button.primary(text: "Submit"),
        onSubmit: (data) {
          print(data);
        },
      ),
    ),
  );
}

Mengirim Form

Ada tiga cara untuk mengirim form:

Menggunakan onSubmit dan submitButton

Kirim onSubmit dan submitButton saat membuat form. Nylo Website menyediakan tombol bawaan yang berfungsi sebagai tombol submit:

LoginForm(
  submitButton: Button.primary(text: "Submit"),
  onSubmit: (data) {
    print(data); // {email: "...", password: "..."}
  },
  onFailure: (errors) {
    print(errors.first.rule.getMessage());
  },
)

Gaya tombol yang tersedia: Button.primary, Button.secondary, Button.outlined, Button.textOnly, Button.icon, Button.gradient, Button.rounded, Button.transparency.

Menggunakan NyFormActions

Gunakan getter actions untuk mengirim dari mana saja:

LoginForm.actions.submit(
  onSuccess: (data) {
    print(data);
  },
  onFailure: (errors) {
    print(errors.first.rule.getMessage());
  },
  showToastError: true,
);

Menggunakan method statis NyFormWidget.submit()

Kirim form berdasarkan namanya dari mana saja:

NyFormWidget.submit("LoginForm",
  onSuccess: (data) {
    print(data);
  },
  onFailure: (errors) {
    print(errors.first.rule.getMessage());
  },
  showToastError: true,
);

Saat dikirim, form memvalidasi semua field. Jika valid, onSuccess dipanggil dengan Map<String, dynamic> data field (kunci adalah versi snake_case dari nama field). Jika tidak valid, error toast ditampilkan secara default dan onFailure dipanggil jika disediakan.

Tipe Field

Nylo Website v7 menyediakan 22 tipe field melalui konstruktor bernama pada kelas Field. Semua konstruktor field berbagi parameter umum ini:

Parameter Tipe Default Deskripsi
key String Wajib Identifier field (posisional)
label String? null Label tampilan kustom (default ke key dalam title case)
value dynamic null Nilai awal
validator FormValidator? null Aturan validasi
autofocus bool false Auto-fokus saat dimuat
dummyData String? null Data tes/pengembangan
header Widget? null Widget ditampilkan di atas field
footer Widget? null Widget ditampilkan di bawah field
titleStyle TextStyle? null Gaya teks label kustom
hidden bool false Sembunyikan field
readOnly bool? null Buat field hanya-baca
style FieldStyle? Bervariasi Konfigurasi gaya khusus field
onChanged Function(dynamic)? null Callback perubahan nilai

Field Teks

Field.text("Name")

Field.text("Name",
  value: "John",
  validator: FormValidator.notEmpty(),
  autofocus: true,
)

Tipe gaya: FieldStyleTextField

Field Angka

Field.number("Age")

// Decimal numbers
Field.number("Score", decimal: true)

Parameter decimal mengontrol apakah input desimal diizinkan. Tipe gaya: FieldStyleTextField

Field Password

Field.password("Password")

// With visibility toggle
Field.password("Password", viewable: true)

Parameter viewable menambahkan toggle tampilkan/sembunyikan. Tipe gaya: FieldStyleTextField

Field Email

Field.email("Email", validator: FormValidator.email())

Secara otomatis mengatur tipe keyboard email dan memfilter spasi. Tipe gaya: FieldStyleTextField

Field URL

Field.url("Website", validator: FormValidator.url())

Mengatur tipe keyboard URL. Tipe gaya: FieldStyleTextField

Field Area Teks

Field.textArea("Description")

Input teks multi-baris. Tipe gaya: FieldStyleTextField

Field Nomor Telepon

Field.phoneNumber("Mobile Phone")

Secara otomatis memformat input nomor telepon. Tipe gaya: FieldStyleTextField

Kapitalisasi Kata

Field.capitalizeWords("Full Name")

Mengkapitalkan huruf pertama setiap kata. Tipe gaya: FieldStyleTextField

Kapitalisasi Kalimat

Field.capitalizeSentences("Bio")

Mengkapitalkan huruf pertama setiap kalimat. Tipe gaya: FieldStyleTextField

Field Tanggal

Field.date("Birthday")

Field.date("Birthday",
  dummyData: "1990-01-01",
  style: FieldStyleDateTimePicker(
    firstDate: DateTime(1900),
    lastDate: DateTime.now(),
  ),
)

Membuka date picker. Tipe gaya: FieldStyleDateTimePicker

Field Tanggal Waktu

Field.datetime("Check in Date")

Field.datetime("Appointment", dummyData: "2025-01-01 10:00")

Membuka picker tanggal dan waktu. Tipe gaya: FieldStyleDateTimePicker

Field Input Masked

Field.mask("Phone", mask: "(###) ###-####")

Field.mask("Credit Card", mask: "#### #### #### ####")

Field.mask("Custom Code",
  mask: "AA-####",
  match: r'[\w\d]',
  maskReturnValue: true, // Returns the formatted value
)

Karakter # dalam mask digantikan oleh input pengguna. Gunakan match untuk mengontrol karakter yang diizinkan. Ketika maskReturnValue adalah true, nilai yang dikembalikan mencakup format mask.

Field Mata Uang

Field.currency("Price", currency: "usd")

Parameter currency wajib dan menentukan format mata uang. Tipe gaya: FieldStyleTextField

Field Checkbox

Field.checkbox("Accept Terms")

Field.checkbox("Agree to terms",
  header: Text("Terms and conditions"),
  footer: Text("You must agree to continue."),
  validator: FormValidator.booleanTrue(message: "You must accept the terms"),
)

Tipe gaya: FieldStyleCheckbox

Field Switch Box

Field.switchBox("Enable Notifications")

Tipe gaya: FieldStyleSwitchBox

Field Picker

Field.picker("Category",
  options: FormCollection.from(["Electronics", "Clothing", "Books"]),
)

// With key-value pairs
Field.picker("Country",
  options: FormCollection.fromMap({
    "us": "United States",
    "ca": "Canada",
    "uk": "United Kingdom",
  }),
)

Parameter options memerlukan FormCollection (bukan list mentah). Lihat FormCollection untuk detail. Tipe gaya: FieldStylePicker

Field Radio

Field.radio("Newsletter",
  options: FormCollection.fromMap({
    "Yes": "Yes, I want to receive newsletters",
    "No": "No, I do not want to receive newsletters",
  }),
)

Parameter options memerlukan FormCollection. Tipe gaya: FieldStyleRadio

Field Chip

Field.chips("Tags",
  options: FormCollection.from(["Featured", "Sale", "New"]),
)

// With key-value pairs
Field.chips("Engine Size",
  options: FormCollection.fromMap({
    "125": "125cc",
    "250": "250cc",
    "500": "500cc",
  }),
)

Memungkinkan multi-seleksi melalui widget chip. Parameter options memerlukan FormCollection. Tipe gaya: FieldStyleChip

Field Slider

Field.slider("Rating",
  label: "Rate us",
  validator: FormValidator.minValue(4, message: "Rating must be at least 4"),
  style: FieldStyleSlider(
    min: 0,
    max: 10,
    divisions: 10,
    activeColor: Colors.blue,
    inactiveColor: Colors.grey,
  ),
)

Tipe gaya: FieldStyleSlider -- konfigurasikan min, max, divisions, warna, tampilan nilai, dan lainnya.

Field Range Slider

Field.rangeSlider("Price Range",
  style: FieldStyleRangeSlider(
    min: 0,
    max: 1000,
    divisions: 20,
    activeColor: Colors.blue,
    inactiveColor: Colors.grey,
  ),
)

Mengembalikan objek RangeValues. Tipe gaya: FieldStyleRangeSlider

Field Kustom

Gunakan Field.custom() untuk menyediakan widget stateful Anda sendiri:

Field.custom("My Field",
  child: MyCustomFieldWidget(),
)

Parameter child memerlukan widget yang meng-extend NyFieldStatefulWidget. Ini memberi Anda kontrol penuh atas rendering dan perilaku field.

Field Widget

Gunakan Field.widget() untuk menyematkan widget apa pun di dalam form tanpa menjadi field form:

Field.widget(child: Divider())

Field.widget(child: Text("Section Header", style: TextStyle(fontSize: 18)))

Field widget tidak berpartisipasi dalam validasi atau pengumpulan data. Mereka murni untuk tata letak.

FormCollection

Field picker, radio, dan chip memerlukan FormCollection untuk opsi mereka. FormCollection menyediakan antarmuka terpadu untuk menangani format opsi yang berbeda.

Membuat FormCollection

// From a list of strings (value and label are the same)
FormCollection.from(["Red", "Green", "Blue"])

// Same as above, explicit
FormCollection.fromArray(["Red", "Green", "Blue"])

// From a map (key = value, value = label)
FormCollection.fromMap({
  "us": "United States",
  "ca": "Canada",
})

// From structured data (useful for API responses)
FormCollection.fromKeyValue([
  {"value": "en", "label": "English"},
  {"value": "es", "label": "Spanish"},
])

FormCollection.from() secara otomatis mendeteksi format data dan mendelegasikan ke konstruktor yang sesuai.

FormOption

Setiap opsi dalam FormCollection adalah FormOption dengan properti value dan label:

FormOption option = FormOption(value: "us", label: "United States");
print(option.value); // "us"
print(option.label); // "United States"

Mengkueri Opsi

FormCollection options = FormCollection.fromMap({"us": "United States", "ca": "Canada"});

options.getByValue("us");          // FormOption(value: us, label: United States)
options.getLabelByValue("us");     // "United States"
options.containsValue("ca");      // true
options.searchByLabel("can");      // [FormOption(value: ca, label: Canada)]
options.values;                    // ["us", "ca"]
options.labels;                    // ["United States", "Canada"]

Validasi Form

Tambahkan validasi ke field apa pun menggunakan parameter validator dengan FormValidator:

// Named constructor
Field.email("Email", validator: FormValidator.email())

// Chained rules
Field.text("Username",
  validator: FormValidator()
    .notEmpty()
    .minLength(3)
    .maxLength(20)
)

// Password with strength level
Field.password("Password",
  validator: FormValidator.password(strength: 2)
)

// Boolean validation
Field.checkbox("Terms",
  validator: FormValidator.booleanTrue(message: "You must accept the terms")
)

// Custom inline validation
Field.number("Age",
  validator: FormValidator.custom(
    message: "Age must be between 18 and 100",
    validate: (data) {
      int? age = int.tryParse(data.toString());
      return age != null && age >= 18 && age <= 100;
    },
  )
)

Saat form dikirim, semua validator diperiksa. Jika ada yang gagal, error toast menampilkan pesan error pertama dan callback onFailure dipanggil.

Lihat juga: Untuk daftar lengkap validator yang tersedia, lihat halaman Validasi.

Mengelola Data Form

Data Awal

Ada dua cara untuk mengatur data awal pada form.

Opsi 1: Override getter init di kelas form Anda

class EditAccountForm extends NyFormWidget {
  EditAccountForm({super.key, super.submitButton, super.onSubmit, super.onFailure});

  @override
  Function()? get init => () async {
    final user = await api<ApiService>((request) => request.getUserData());

    return {
      "First Name": user?.firstName,
      "Last Name": user?.lastName,
    };
  };

  @override
  fields() => [
    Field.text("First Name"),
    Field.text("Last Name"),
  ];

  static NyFormActions get actions => const NyFormActions('EditAccountForm');
}

Getter init dapat mengembalikan Map sinkron atau Future<Map> async. Kunci dicocokkan dengan nama field menggunakan normalisasi snake_case, jadi "First Name" dipetakan ke field dengan kunci "First Name".

Opsi 2: Kirim initialData ke widget form

EditAccountForm(
  initialData: {
    "first_name": "John",
    "last_name": "Doe",
  },
)

Mengatur Nilai Field

Gunakan NyFormActions untuk mengatur nilai field dari mana saja:

// Set a single field value
EditAccountForm.actions.updateField("First Name", "Jane");

Mengatur Opsi Field

Perbarui opsi pada field picker, chip, atau radio secara dinamis:

EditAccountForm.actions.setOptions("Category", FormCollection.from(["New Option 1", "New Option 2"]));

Membaca Data Form

Data form diakses melalui callback onSubmit saat form dikirim, atau melalui callback onChanged untuk pembaruan real-time:

EditAccountForm(
  onSubmit: (data) {
    // data is a Map<String, dynamic>
    // {first_name: "Jane", last_name: "Doe", email: "jane@example.com"}
    print(data);
  },
  onChanged: (Field field, dynamic value) {
    print("${field.key} changed to: $value");
  },
)

Menghapus Data

// Clear all fields
EditAccountForm.actions.clear();

// Clear a specific field
EditAccountForm.actions.clearField("First Name");

Memperbarui Field

// Update a field value
EditAccountForm.actions.updateField("First Name", "Jane");

// Refresh the form UI
EditAccountForm.actions.refresh();

// Refresh form fields (re-calls fields())
EditAccountForm.actions.refreshForm();

Tombol Submit

Kirim submitButton dan callback onSubmit saat membuat form:

UserInfoForm(
  submitButton: Button.primary(text: "Submit"),
  onSubmit: (data) {
    print(data);
  },
  onFailure: (errors) {
    print(errors.first.rule.getMessage());
  },
)

submitButton secara otomatis ditampilkan di bawah field form. Anda dapat menggunakan gaya tombol bawaan atau widget kustom.

Anda juga dapat menggunakan widget apa pun sebagai tombol submit dengan mengirimnya sebagai footer:

UserInfoForm(
  onSubmit: (data) {
    print(data);
  },
  footer: ElevatedButton(
    onPressed: () {
      UserInfoForm.actions.submit(
        onSuccess: (data) {
          print(data);
        },
      );
    },
    child: Text("Submit"),
  ),
)

Tata Letak Form

Tempatkan field berdampingan dengan membungkusnya dalam List:

@override
fields() => [
  // Single field (full width)
  Field.text("Title"),

  // Two fields in a row
  [
    Field.text("First Name"),
    Field.text("Last Name"),
  ],

  // Another single field
  Field.textArea("Bio"),

  // Slider and range slider in a row
  [
    Field.slider("Rating", style: FieldStyleSlider(min: 0, max: 10)),
    Field.rangeSlider("Budget", style: FieldStyleRangeSlider(min: 0, max: 1000)),
  ],

  // Embed a non-field widget
  Field.widget(child: Divider()),

  Field.email("Email"),
];

Field dalam List dirender dalam Row dengan lebar Expanded yang sama. Jarak antar field dikontrol oleh parameter crossAxisSpacing pada NyFormWidget.

Visibilitas Field

Tampilkan atau sembunyikan field secara programatis menggunakan method hide() dan show() pada Field. Anda dapat mengakses field di dalam kelas form Anda atau melalui callback onChanged:

// Inside your NyFormWidget subclass or onChanged callback
Field nameField = ...;

// Hide the field
nameField.hide();

// Show the field
nameField.show();

Field tersembunyi tidak dirender di UI tetapi masih ada dalam daftar field form.

Styling Field

Setiap tipe field memiliki subkelas FieldStyle yang sesuai untuk styling:

Tipe Field Kelas Gaya
Text, Email, Password, Number, URL, TextArea, PhoneNumber, Currency, Mask, CapitalizeWords, CapitalizeSentences FieldStyleTextField
Date, DateTime FieldStyleDateTimePicker
Picker FieldStylePicker
Checkbox FieldStyleCheckbox
Switch Box FieldStyleSwitchBox
Radio FieldStyleRadio
Chip FieldStyleChip
Slider FieldStyleSlider
Range Slider FieldStyleRangeSlider

Kirim objek gaya ke parameter style dari field apa pun:

Field.text("Name",
  style: FieldStyleTextField(
    filled: true,
    fillColor: Colors.grey.shade100,
    border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
    contentPadding: EdgeInsets.symmetric(horizontal: 16, vertical: 12),
    prefixIcon: Icon(Icons.person),
  ),
)

Field.slider("Rating",
  style: FieldStyleSlider(
    min: 0,
    max: 10,
    divisions: 10,
    activeColor: Colors.blue,
    showValue: true,
  ),
)

Field.chips("Tags",
  options: FormCollection.from(["Sale", "New", "Featured"]),
  style: FieldStyleChip(
    selectedColor: Colors.blue,
    checkmarkColor: Colors.white,
    spacing: 8.0,
    runSpacing: 8.0,
  ),
)

Method Statis NyFormWidget

NyFormWidget menyediakan method statis untuk berinteraksi dengan form berdasarkan nama dari mana saja di aplikasi Anda:

Method Deskripsi
NyFormWidget.submit(name, onSuccess:, onFailure:, showToastError:) Kirim form berdasarkan namanya
NyFormWidget.stateRefresh(name) Refresh state UI form
NyFormWidget.stateSetValue(name, key, value) Atur nilai field berdasarkan nama form
NyFormWidget.stateSetOptions(name, key, options) Atur opsi field berdasarkan nama form
NyFormWidget.stateClearData(name) Hapus semua field berdasarkan nama form
NyFormWidget.stateRefreshForm(name) Refresh field form (memanggil ulang fields())
// Submit a form named "LoginForm" from anywhere
NyFormWidget.submit("LoginForm", onSuccess: (data) {
  print(data);
});

// Update a field value remotely
NyFormWidget.stateSetValue("LoginForm", "Email", "new@email.com");

// Clear all form data
NyFormWidget.stateClearData("LoginForm");

Tips: Lebih baik gunakan NyFormActions (lihat di bawah) daripada memanggil method statis ini secara langsung -- lebih ringkas dan minim kesalahan.

Referensi Konstruktor NyFormWidget

Saat meng-extend NyFormWidget, ini adalah parameter konstruktor yang dapat Anda kirim:

LoginForm(
  Key? key,
  double crossAxisSpacing = 10,  // Horizontal spacing between row fields
  double mainAxisSpacing = 10,   // Vertical spacing between fields
  Map<String, dynamic>? initialData, // Initial field values
  Function(Field field, dynamic value)? onChanged, // Field change callback
  Widget? header,                // Widget above the form
  Widget? submitButton,          // Submit button widget
  Widget? footer,                // Widget below the form
  double headerSpacing = 10,     // Spacing after header
  double submitButtonSpacing = 10, // Spacing after submit button
  double footerSpacing = 10,     // Spacing before footer
  LoadingStyle? loadingStyle,    // Loading indicator style
  bool locked = false,           // Makes form read-only
  Function(dynamic data)? onSubmit,   // Called with form data on successful validation
  Function(dynamic error)? onFailure, // Called with errors on failed validation
)

Callback onChanged menerima Field yang berubah dan nilai barunya:

LoginForm(
  onChanged: (Field field, dynamic value) {
    print("${field.key} changed to: $value");
  },
)

NyFormActions

NyFormActions menyediakan cara mudah untuk berinteraksi dengan form dari mana saja di aplikasi Anda. Definisikan sebagai getter statis di kelas form Anda:

class LoginForm extends NyFormWidget {
  LoginForm({super.key, super.submitButton, super.onSubmit, super.onFailure});

  @override
  fields() => [
    Field.email("Email", validator: FormValidator.email()),
    Field.password("Password", validator: FormValidator.password()),
  ];

  static NyFormActions get actions => const NyFormActions('LoginForm');
}

Aksi yang Tersedia

Method Deskripsi
actions.updateField(key, value) Atur nilai field
actions.clearField(key) Hapus field tertentu
actions.clear() Hapus semua field
actions.refresh() Refresh state UI form
actions.refreshForm() Panggil ulang fields() dan rebuild
actions.setOptions(key, options) Atur opsi pada field picker/chip/radio
actions.submit(onSuccess:, onFailure:, showToastError:) Kirim dengan validasi
// Update a field value
LoginForm.actions.updateField("Email", "new@email.com");

// Clear all form data
LoginForm.actions.clear();

// Submit the form
LoginForm.actions.submit(
  onSuccess: (data) {
    print(data);
  },
);

Override NyFormWidget

Method yang dapat Anda override di subkelas NyFormWidget Anda:

Override Deskripsi
fields() Definisikan field form (wajib)
init Sediakan data awal (sinkron atau async)
onChange(field, data) Tangani perubahan field secara internal

Referensi Semua Tipe Field

Konstruktor Parameter Kunci Deskripsi
Field.text() -- Input teks standar
Field.email() -- Input email dengan tipe keyboard
Field.password() viewable Password dengan toggle visibilitas opsional
Field.number() decimal Input numerik, desimal opsional
Field.currency() currency (wajib) Input berformat mata uang
Field.capitalizeWords() -- Input teks title case
Field.capitalizeSentences() -- Input teks sentence case
Field.textArea() -- Input teks multi-baris
Field.phoneNumber() -- Nomor telepon terformat otomatis
Field.url() -- Input URL dengan tipe keyboard
Field.mask() mask (wajib), match, maskReturnValue Input teks masked
Field.date() -- Date picker
Field.datetime() -- Picker tanggal dan waktu
Field.checkbox() -- Checkbox boolean
Field.switchBox() -- Toggle switch boolean
Field.picker() options (wajib FormCollection) Seleksi tunggal dari daftar
Field.radio() options (wajib FormCollection) Grup tombol radio
Field.chips() options (wajib FormCollection) Chip multi-seleksi
Field.slider() -- Slider nilai tunggal
Field.rangeSlider() -- Slider nilai rentang
Field.custom() child (wajib NyFieldStatefulWidget) Widget stateful kustom
Field.widget() child (wajib Widget) Sematkan widget apa pun (non-field)