Validate values of @bindable

I wander if there is a mechanism similar to PropTypes to validate values of @bindable. I think Angular has something similar too. From time to time, passing the wrong value or forgot to passing a value (maybe because of a typo) is a source of bugs and frustration to understand what is going on.

Is this possible? If so how?

1 Like

So educate me here, why use a bolt-on when it would seem you could use TypeScript to do that intrinsically? Is TS not an option?

1 Like
  1. My project is written in JS and I don’t intend to migrate to TypeScript soon. It would be nice to have some kind of check for this too (mostly to detect typos more easily).
  2. It would also be nice to have this for single file components.
  3. Bindable are passed to a component from the HTML like this my-bindable.bind="value" so the value is passed to the component at runtime. So I do know whether TS will be able to detect this since at this point, TS files are already compiled into JS files.
1 Like

Thanks for that. Every project always has different needs and constraints of course so the reason for asking.
Could you not perhaps use aurelia-validation as I think it can get fired when the binding changes to stay in the ecosystem?

1 Like

Maybe. I’ll try to look into that.

1 Like

I use TS, but here is an example of how I am using it and it is working for me. Might not be the best way though.

In attached() I call a method setValidationRules() that setup the property validation requirements.
Initially I was setting up a rules collection and running them against the property, but changed that over to use the on() method. I left the full function as it shows how I was able to use a few of the more advance methods.

setValidationRules()
    {
        const RENTAL_NOTSELECTED = 0;
        const NOINSTRUCTOR = 0;
        const RENTAL_DUAL = 3;

        //this.studentRules = 
        ValidationRules
            .ensure('wind').required()
            .ensure('expectedRunway').required()
            .ensure('fuelLeft').required()
            .ensure('fuelRight').required()
            .ensure('expectedFuelUsed').required()
            .ensure('weatherBriefed').satisfies((v, o: PaveTimecardData) =>
            {
                return o.weatherBriefed;
            })
            .on(this.paveTimecard.data);
            //.rules;

        //this.standardRules = 
        ValidationRules
            .ensure('airport').required()
            .ensure('aircraftId').required()
            .ensure('timecardDate').required()
            .ensure('hobbsStart').required()
            .ensure('hobbsEnd').required()
            .ensure('rentalTypeId').satisfies((val, o: PaveTimecardModel) =>
            {
                if (!o.rentalTypeId || o.rentalTypeId === RENTAL_NOTSELECTED)
                {
                    return false;
                }

                return true;
            })
            .withMessage("Rental Type needs to be selected.")
            .ensure('instructorId').satisfies((val, o: PaveTimecardModel) =>
            {
                if (o.instructorId === NOINSTRUCTOR)
                {
                    return false;
                }

                return true;
            })
            .withMessage("Rental Type [Dual] requires instructor to be selected.")
            .when((o: PaveTimecardModel) =>
            {
                return o.rentalTypeId === RENTAL_DUAL;
            })
            .ensure('instructorId').satisfies((val, o: PaveTimecardModel) =>
            {
                if (o.instructorId !== NOINSTRUCTOR)
                {
                    let msg: string = "Rental Type does not require an instructor to be selected and will be removed on save.";
                    if (this.warnings.indexOf(msg) === -1)
                    {
                        this.warnings.push(msg);
                    }
                    return true;
                }

                return true;
            })
            .when((o: PaveTimecardModel) =>
            {
                return o.rentalTypeId !== RENTAL_DUAL;
            })
            .ensure('fuelAmount').satisfies((val, o: PaveTimecardModel) =>
            {
                if (!o.fuelPurchased) 
                {
                    return true;
                }
                else if (o.fuelAmount && o.fuelAmount.length > 0)
                {
                    return true;
                }

                return false;
            })
            .when((o: PaveTimecardModel) => o.fuelPurchased)
            .withMessage("Purchased Fuel checked, no entry detected.")
            .on(this.paveTimecard);
            //.rules;
    }

I use it at various change events with the following code snippet:

        this.vcontroller.validate()
                .then(result => {
                    if(result.valid)
                        this.enableSaveTimecard = true;
                });

I have a repeat.for in the template that displays any errors in validation that require action:

            <div style="color: red;">
                <ul if.bind="vcontroller.errors">
                    <li repeat.for="error of vcontroller.errors">
                        ${error.message}
                    </li>
                </ul>
            </div>

Hope that helps somewhat.

2 Likes

@Jenselme there is this plugin: https://github.com/aurelia-contrib/aurelia-typed-observable-plugin that can help you to process the value before it’s assigned to a view model. You can use it to implement your assertion:

import { bindable } from 'aurelia-typed-observable-plugin';

export class MyEl {
  @bindable({
    coerce: val => {
      const realValue = Number(val);
      if (typeof realValue !== 'number') {
        throw new Error('Invalid value for "myProp", given: ' + val);
      }
      return realValue;
    }
  })
  myProp
}
2 Likes

This looks exactly like what I need, thanks.

1 Like