aurelia / validation

A validation plugin for Aurelia.
MIT License
132 stars 129 forks source link

Custom element that holds the validation controller instance? #542

Open jasonhjohnson opened 4 years ago

jasonhjohnson commented 4 years ago

Is it possible to create an "app-form" custom element that contains the validation controller instance and handles the validation so parent components can use it without knowing about validation?

I'm considering something like:

parent.vm.html

<app-form validate.bind="true">
  <app-form-group>
    <app-label slot="label">First Name</app-label>
    <app-input slot="input" type="text" value.bind="model.firstName"></app-input>
  </app-form-group>
</app-form>

input.element.html

<template>
  <input  
    value.bind="value & validateOnBlur"
  />
</template>

sample.model.ts

export class SampleModel {
  firstName: string;
}

ValidationRules.ensure((m: SampleModel) => m.firstName)
  .required()
  .on(SampleModel);
bigopon commented 4 years ago

@jasonhjohnson nice Q. The scope of a ValidationController created in parent.html

<app-form validate.bind="true">
  <app-form-group>
    <app-label slot="label">First Name</app-label>
    <app-input slot="input" type="text" value.bind="model.firstName"></app-input>
  </app-form-group>
</app-form>

will be different with the scope of a ValidationController created inside app-form.html itself.

At the moment, we don't support this, but probably there's a way to do it, via a template controller custom attribute with a ValidationController encapsulated in the scope of app-form in the above template.

@jods4 requested this feature, in the name of template controller custom element, maybe he can chime in.

cc @fkleuver @EisenbergEffect

jods4 commented 4 years ago

It's possible, that's more or less how I do it in my projects. The way I achieved it is rather complex, though.

It's based on two custom elements, data-form and data-field, e.g.

<data-form entity.bind='model' validation.bind='rules' with.bind='model'>
  <data-field label='First name'>
    <input value.bind='firstName' />
  </data-field>
  <data-field label='Last name'>
    <input value.bind='lastName' />
  </data-field>
</data-form>

Those elements do a fair bit of presentation work (e.g. creating a localized label that is automatically associated with the inner control, doing the layout, etc.) but that's beside the point.

The first trick is that <data-field> processes its children (and I won't be able to provide the details out of the top of my head, sorry. It's a fairly advanced / obscure Aurelia api). For a whole set of known elements / attributes, it modifies the template of its contents from <input value.bind='firstName'> to <input value.bind='firstName & validate:"firstName"'> It does so only when there is no validate binding behaviour in your template already, which enables you to customize the default validation (e.g. specify a different fieldName if it can't be figured out from the expression or disable validation althogether).

From here, validate binding behavior is the one actually doing the heavy lifting. Of course it does all the things you imagine, such as performing validation when the binding changes and putting .invalid classes on the element accordingly.

There's one trick, which is that the validate binding finds and is tied to enclosing the <data-form>. That's how it grabs the validation rules, or how it can find about fields that depend on each others and update when another field is modified. (It also does other services such as tracking if the entity is dirty or not, but again, beside the point).

Sorry I don't have access to the source code right now, and that was the most tricky part. I did it by having <data-form> be a template controller and put itself inside the scope/overrides chain under a known key such as $form. The behaviour would then walk up the scope chain until it finds or form (or nothing). I recall there were issues because of when the children are bound vs when the parent form is bound (basically it's the opposite of what you'd like).

You could also try to solve that problem by having <data-form> create a nested injection scope that can provide itself, and maybe have the behaviour inject the form. Haven't tried it, if it works it would be simpler than using the binding scope.

jasonhjohnson commented 4 years ago

@jods4 Super insightful, thanks!