Geavanceerde formulieren

AdvancedForm is het ingebouwde formuliervalidatie- en rendersysteem van Finch. Het combineert veldbepalingen, validators, CSRF-beveiliging en sjabloonrendering in één klasse.

Een formulier definiëren

Breid AdvancedForm uit en override name, widget en fields():

import 'package:finch/finch_ui.dart';

class PersonForm extends AdvancedForm {
  @override
  String get name => 'form_person';

  @override
  String get widget => 'forms/person.j2.html';

  @override
  List<Field> fields() {
    return [
      csrf(),           // CSRF-beveiligingstoken (altijd toevoegen)
      Field('name', validators: [
        FieldValidator.requiredField(),
        FieldValidator.fieldLength(min: 3, max: 100),
      ]),
      Field('email', validators: [
        FieldValidator.requiredField(),
        FieldValidator.isEmailField(),
      ]),
      Field('age', validators: [
        FieldValidator.isNumberField(isRequired: false),
      ]),
    ];
  }
}

Field(name, {validators, initValue, type, initOptions}) — initValue vult het veld vooraf in (bijv. een standaarddatum), en initOptions is een asynchrone callback die wordt gebruikt bij select-achtige velden om hun optielijst te vullen (zie het categorievoorbeeld hieronder).

Ingebouwde validators

FieldValidator behandelt zowel eenvoudige waardecontroles als MongoDB/SQL-relatie- en uniekheidscontroles:

Validator Beschrijving
FieldValidator.requiredField() Veld mag niet leeg zijn
FieldValidator.requiredFieldMultiLanguage() Verplicht, verwacht een JSON-object met waarden per taal (ten minste één niet-leeg)
FieldValidator.fieldLength({min, max}) Bereik voor stringlengte
FieldValidator.isNumberField({min, max, isRequired}) Moet een geheel getal zijn, optioneel begrensd (isRequired is standaard false)
FieldValidator.isNumberDoubleField({min, max, isRequired}) Hetzelfde als hierboven, voor decimale getallen
FieldValidator.isEmailField() Moet een geldig e-mailadres zijn
FieldValidator.isPasswordField() Ten minste 8 tekens, met een hoofdletter, kleine letter, cijfer en speciaal teken
FieldValidator.isColorField() Moet een hexadecimale kleur zijn (#fff of #ffffff)
FieldValidator.isSelectField(options) Waarde moet één van de opgegeven List met opties zijn
FieldValidator.isDateField({isRequired, checkUtc}) Moet een parseerbare datum zijn; checkUtc vereist bovendien dat deze UTC is
FieldValidator.contains(values, {isRequired}) Waarde moet één van values zijn
FieldValidator.hasRelation({collectionModel, relationField, isRequired}) MongoDB: waarde moet verwijzen naar de _id van een bestaand document in collectionModel (een DBCollectionFree)
FieldValidator.hasSqlRelation({db, table, field, isRequired, operator, where}) SQL: waarde moet bestaan als field in table — zie MySQL/SQLite voor db
FieldValidator.isUniqueSQLField({db, table, field, operator, where}) SQL: waarde mag niet al bestaan als field in table
FieldValidator.checkByRegexp(pattern, {isRequired}) Aangepaste RegExp-controle — niet regExp()

Een select-achtig veld met dynamisch geladen opties (aangepast van het boekformulier uit het voorbeeldproject, dat categorieën laadt vanuit MySQL of SQLite):

Field(
  'category_id',
  validators: [
    FieldValidator.hasSqlRelation(
      isRequired: false,
      db: app.mysqlDriver,
      table: 'categories',
      field: 'id',
    ),
  ],
  initOptions: (field) async {
    var categories = await CategoriesRepository(app.mysqlDriver).getAllCategories();
    return categories.rows.assoc; // de geretourneerde lijst wordt de `options` van dit veld
  },
),

Het formulier gebruiken in een controller

Future<String> personForm() async {
  var form = PersonForm();

  return form.check(
    onValid: (formData) async {
      // form.get<String>('name') retourneert de gevalideerde waarde nadat check() is uitgevoerd
      var name  = form.get<String>('name');
      var email = form.get<String>('email');

      // verwerk gegevens...
      return rq.redirect('/example/person');
    },
    onInvalid: (formData) async {
      // formulierstatus (waarden + fouten) is beschikbaar in het sjabloon via rq-params
      return rq.renderView(path: 'example/person');
    },
  );
}

rq binnen AdvancedForm is al Context.rq — er is geen aparte stap nodig om het huidige verzoek te koppelen. onValid/onInvalid ontvangen beide de data-map van het gecontroleerde formulier als hun enige argument (ook als je die niet gebruikt) — een callback die zonder parameters wordt gedeclareerd, compileert wel (het veld is los getypeerd als Function), maar crasht tijdens runtime wanneer check() deze aanroept, omdat er in werkelijkheid met één argument wordt aangeroepen. Bij een GET-verzoek roept form.check() altijd onInvalid aan, zodat het lege formulier wordt getoond.

De formulierwidget opnemen in een sjabloon

Voeg de formulierwidget toe aan een bovenliggend sjabloon:

{% include form_person.widget | unscape %}

Het pad van de formulierwidget is de widget-eigenschap van je klasse.

Veldstatus lezen in sjablonen

De waarde en fouten van elk veld zijn beschikbaar via $n('formName/fieldName/...'). Het CSRF-veld wordt toegevoegd in fields() via csrf(), maar de werkelijke veldnaam is token (de csrfTokenName van de klasse, standaard 'token') — niet csrf:

<form method="POST" action="/example/person">
  <input
    type="text"
    name="name"
    value="{{ $n('form_person/name/value') }}"
    class="{{ 'border-red-500' if $n('form_person/name/errors/0') else '' }}"
  />
  <p class="text-red-600">{{ $n('form_person/name/errors/0') }}</p>

  <input
    type="email"
    name="email"
    value="{{ $n('form_person/email/value') }}"
  />
  <p class="text-red-600">{{ $n('form_person/email/errors/0') }}</p>

  <!-- CSRF-token -->
  <input type="hidden" name="token" value="{{ $n('form_person/token/value') }}" />

  <button type="submit">Save</button>
</form>

Gedrag van het API-eindpunt

Wanneer de verzoek-URL begint met /api/, retourneert AdvancedForm een JSON-respons in plaats van de widget te renderen:

{
  "form_person": {
    "name": { "value": "Alice", "errors": [] },
    "email": { "value": "", "errors": ["Email is required"] }
  }
}

CSRF-beveiliging

Door csrf() op te nemen in fields() wordt een verborgen CSRF-tokenveld toegevoegd (genaamd token). Het token wordt per formuliernaam gegenereerd, opgeslagen in de sessie en automatisch gevalideerd bij POST/PUT — hetzelfde token wordt tot 6 uur hergebruikt voordat het wordt vernieuwd. Er hoeft verder niets te worden aangeroepen om dit in te schakelen, behalve het opnemen van csrf() in je fields()-lijst en het renderen van de verborgen token-input zoals hierboven getoond.