Change handler for ALL form elements in component

I’m using aurelia v1 for an app. We’re trying to add a “global” change handler for a component where we have a form and about a dozen radio/select/text elements. What I want to do is add a isModified property to the component and to use this to prevent navigation via canDeactivate.

Is there a straightforward way to make this happen in a component? IE when any input element changes, set isModified = true ?

Edit:

A little more detail - my current solution I can wrap my head around is to use change.delegate handlers on every single form element.

<input change.delegate="isModified = true">
...

This is just very repetitive though and thought there might be an easier way, something like a global “change” handler to capture bubbling input change events.

I don’t have a specific answer, but here are a few concepts you may end up using in your solution.

First, you can do change detection on a specific form field:

view.component.ts:

@inject(CanDeactiveFormService)
export class SomeViewComponent {

  @bindable({ defaultBindingMode: bindingMode.twoWay })
  firstName: string;

  public constructor(private canService: CanDeactiveFormService) {}

  firstNameChanged(newValue, oldValue) {
      this.canService.isModified = true;
  }
}

Then your canDeactivate functionality can check the service.

Of course, if you have your form fields in an object, that won’t work since Aurelia v1 doesn’t track deep changes. Instead, you’ll need to use a custom deep watch on your object.

multi-properties-binding-engine.js

import { inject, ObserverLocator, TaskQueue } from 'aurelia-framework';

@inject(ObserverLocator, TaskQueue)
export class MultiPropertiesBindingEngine {

    constructor(private observerLocator: ObserverLocator,
        private taskQueue: TaskQueue) {

    }

    /**
     * @param {object} obj
     * @param {string[]} props
     */
    propertiesObserver(obj: {}, props: string[]) {

        return {
            subscribe: (callback: Function) => {
                let isCallbackQueued = false;
                const unmarkQueued = () => {
                    isCallbackQueued = false;
                };
                const handler = () => {
                    if (!isCallbackQueued) {
                        this.taskQueue.queueMicroTask(unmarkQueued);
                        this.taskQueue.queueMicroTask(callback);
                        isCallbackQueued = true;
                    }
                };
                const observers = props.map(prop => this.observerLocator.getObserver(obj, prop));
                observers.forEach(observer => {
                    observer.subscribe(handler);
                });
                return {
                    observers: observers,
                    dispose: () => {
                        observers.forEach(observer => {
                            observer.unsubscribe(handler);
                        });
                    }
                };
            }
        }

    }
}

import { MultiPropertiesBindingEngine } from 'multi-properties-binding-engine';

@inject(Router, CanDeactiveFormService, MultiPropertiesBindingEngine)
export class AdminUserEditView {

    formData: SomeModel;

    private subscriptions: Disposable[] = [];

    constructor(
        private router: Router,
        private canService: CanDeactiveFormService,
        private bindingEngine: MultiPropertiesBindingEngine) {

    }

    async activate(params: any, routeConfig: RouteConfig): Promise<any> {
        this.subscriptions.push(this.bindingEngine
            .propertiesObserver(this.formData, ['status', 'text', 'category', 'dateFrom', 'dateTo'])
            .subscribe(this.formDataChanged.bind(this)));
    }

    formDataChanged(newValue, oldValue) {
      this.canService.isModified = true;
    }

    detached() {
        if (this.subscriptions && this.subscriptions.length) {
            this.subscriptions.forEach((disposable) => disposable.dispose());
            while (this.subscriptions.length) {
                this.subscriptions.pop();
            }
        }
    }
}

I haven’t looked into any type of reactive forms with Aurelia, but you should be able to subscribe to a form-level change event. This is an interesting case, and I’d be interested in learning how you solve it. There must be an elegant solution instead of such a hack above.

create an object to hold your bounded properties.
and use How To Observe Objects In Aurelia Using The Binding Engine
in order to obsever all properties of the object using the same change handler.

i briefly looked at @manks answer.
and it seemed too complicated.
but you should compare the solutions and check what works better for you.

Both of these solutions seem a little complicated I guess ( I don’t mean any offense by the way, I’m just trying to find a simple solution to what would seem to me to be a common situation).

Here’s another workaround I found:

class FormComponent {
   // object we are editing
   @bindable object = {};
   objectClone;
    activate(){ 
        this.objectClone = {...this.object};
    }
    
    canDeactivate(){
        if(deepEqual(this.object, this.objectClone)){
            return true;
        }
        return confirm('Form was modified but not saved, are you sure you want to continue?');
    }
    
    save(){
        // do validation , save to api, etc....
       .then(() => {
            this.objectClone = {...object};
        });
    }

Does this make sense ?

Certainly seems like a valid way to do it.

1 Like

You can add a change handler at the root of the form…
https://gist.dumber.app/?gist=1cf631925453d40cbcd7f43c122bd324

2 Likes

Whoa - I didn’t know you could add event handlers to parent elements! That is good to know!

:exploding_head: