Forms-heavy application with auto-save

@bigopon Sorry for pinging you. Did you have a chance to look for something in Aurelia that would help me to deep observe such “complex” structure? I will have quite a lot of forms similar to this, and I would like to provide auto-save functionality.

Obviously, I can autosave on every “addSomething” and “removeSomething”, but I would also need to set up/release propertyObservers in these methods to observe text fields, selects etc. in the row. Sounds quite like a lot of messy code, which could/should be avoided.

Or is there any other recommended approach to achieve what I need?

1 Like

If the purpose is to get notified whenever there’s a modification deepdown, then maybe you don’t need to worry about setup/release the observer? You only need to recalculate the json data value

@bigopon Sorry, I don’t understand your point. I only need to recalculate the JSON data value. That’s correct. The problem is I don’t know when to do so, which is why I wanted to use observer.

Very ugly workaround would be to calculate JSON every few milliseconds, compare it to previous value, and save on change. But I hoped to avoid that.

1 Like

here is the POC of a deep observation for a computed property Complex form example - CodeSandbox

Usage would be like this:

class App {
  data = { ... }

  @deepComputedFrom('data')
  get jsonData() {
    return JSON.stringify(this.data);
  }
}

You can see that it: tracks changes deep within the object, and doesn’t do dirty checking. Pros & cons:

Pros:

  • simple change tracking

Cons:

  • if the object is big (contains array with many objects and a lot of properties), it’s costly upfront setup. (Though it needs to be quite a big object to be perceivable here)

It’s just a POC, need to properly release the observer and re-observe when array/object is mutated

Edit:

sorry for pinging …

No problems at all, i should say sorry that i missed your reply.

2 Likes

There’s also another way to solve it: declare a counter property, an on mutation, do an increment. If it’s a two way binding with an input field, then you can listen to input event and do the increment. That will be the simplest of all.

I may make the above @deepComputedFrom into a plugin, but no promise :stuck_out_tongue:

I’ve finished the base implementation here, missing map observation, it’ll be added later https://codesandbox.io/s/complex-form-example-t1r7g

I’ll turn this into a plugin probably

EDIT: I have made this into a plugin here https://www.npmjs.com/package/aurelia-deep-computed

2 Likes

From your readme

Besides observing for changes at data property of App, all properties of data will also be observed.

Does that mean you can do @observable data and dataChanged(data as any) as well?

1 Like

Yes, that is independent to the property with @deepComputedFrom.

Hi @bigopon ,

Sorry for being so late (life’s got in the way), but I would like to say big THANK YOU for this. To get rid of a bit of my shame and show some appreciation, I’ve at least doubled my mothly OpenCollective contributions :slight_smile:.

That said, similar to what @jeremyholt asked, what would be your recommended approach to observe changes using this plugin? I don’t think simple @observable would help here - if I understand it correctly, it cannot be used on computed properties and it is not “deep”.

Let’s say I have following in my VM:

class App {
  originalModel: ComplexModel;
  formModel: ComplexModel;

   activate() {
     this.originalModel = await loadFromApi();
     this.formModel = deepClone(this.originalModel);
   }

   @computedFrom('originalModel')
   @deepComputedFrom('formModel')
   get isDirtyComputed() {
     return deepEquals(this.originalModel, this.formModel);
   }

   // Q: How to call this method on change of isDirtyComputed?
   save() {
     await saveToApi(this.formModel);
     this.originalModel = this.formModel;
   }
}

This is probably more of a general question of how to observe changes in computed properties, but anyway. My hacky approach based on my limited Aurelia knowledge would be to:

  • bind something to    isDirtyComputed    from the View
  • create    @observable isDirty: boolean    on the VM
  • change    isDirtyComputed    implementation to
    return this.isDirty = deepEquals(this.originalModel, this.formModel);
  • implement    isDirtyChanged(newValue) { if (newValue) this.save(); }

But that is not very nice, and most importantly would not work if isDirtyComputed in not used on the View (correct me if I am wrong). Is there a cleaner way to do that?

Thank you again!

1 Like

Thank you very much @bigopon .
This does what the docs say about expressionObserver (“notify you when any property within the path of expressionString has been changed”).
I´m not a native speaker tough, maybe i got the docs wrong.

1 Like

You got it right. It behaves like an enhanced @computedFrom

@sousekd For the purpose of what you want to do, you can inject an observer locator, and use it to get the observer to subscribe to it:

@inject(ObserverLocator)
export class MyVm {

  constructor(observerLocator) {
    this.isDirtyObserver = this.observerLocator.getObserver(this, 'isDirty');
  }

  bind() {
    this.isDirtySubscription = this.isDirtyObserver.subscribe(isDirty => {
      if (isDirty) {
        this.save();
      }
    });
  }

  unbind() {
    this.isDirtySubscription.dispose();
    this.isDirtySubscription = null;
  }

  @deepComputedFrom('...')
  get isDirty() {
    ...
  }
}
2 Likes

Many thanks @bigopon, that looks perfect.

I’ve only used PropertyObserver and ExpressionObserver before. When talking about my usecase, ObserverLocator is some smart thing above these, returning the right observer for the target, supporting @computedFrom (and @deepComputedFrom) properties in addition to class fields, arrays, and expressions?

One last question: can I use both @deepComputedFrom and @computedFrom on a single property at the same time, to save few ticks on properties I know will never change “inside” (originalModel in my case)?

Once again, thank you for all your explanations and the plugin.

1 Like

Arr please don’t say that, it’s just the inner stuff that BindingEngine uses. You just reminded me that you can also use PropertyObserver too, so bindingEngine.propertyObserver(obj, 'isDirty').subscribe is the one.

One last question: can I use both @deepComputedFrom and @computedFrom on a single property at the same time, to save few ticks on properties I know will never change “inside” (originalModel in my case)?

@computedFrom will give hint to observer locator about how to get observer, so it will never reach @deepComputedFrom, so you can’t use them both together.

Glad I could help

1 Like

Hm, I didn’t realize it is possible to use propertyObserver on computed props :blush:. Is it also possible to use @observable decoractor on them, or they work differently?

1 Like

Probably not gonna work with @observable. How would you suggest the usage?

I’ve just learned that it is possible to use propertyObserver (or observerLocator) on a computed get x() property. So I wouldn’t be surprised if @observable decorator is usable on such properties too, internally using the same logic / components.

I suppose it is not the case, just asking…

1 Like

I just updated the plugin. Now you have 2 observation mode: deep/shallow https://www.npmjs.com/package/aurelia-deep-computed

2 Likes

@bigopon thanks, I use the plugin in my current project and it works great. One note: in the npm package description, I believe you did a mistake in the second example of “Rendered text will be:” :slight_smile:

1 Like