Forms-heavy application with auto-save

Yeah I have read your blog post and was happy someone did such a great work for all of us. I just don’t think it makes life easier for me in this particular scenario, where I have a simple list with no extra functionality needed. If I bind the table to an Array, and something causes the change of one item, I can just modify that item in the Array the table is bound to. The same way I would modify the DataView using your grid. Am I correct?

I’d use SlickGrid or similar if I would need more advanced features which are difficult to implement, like columns customization, grouping, inline editing with arrows navigation, stuff like that.

1 Like

Yes and No, because Aurelia-Slickgrid is wrapper of jQuery lib, there’s no direct binding to the array and most of the events are done the old fashion way with dispatch events. You understood correctly about the DataView in SlickGrid, but the main difference is that there are events which are published every time a DataView method is called (adding new row, updating a row, etc…) and from these events that we can subscribe, we can tell the grid to re-render but that’s kinda of a manual process, we need to subscribe and call a grid render, it won’t do it for you and like I said earlier it will only re-render what is shown in the screen (because of the Virtual Rendering), so it’s a deep binding… or at least I wouldn’t call that a deep binding and the Virtual Rendering is what makes this grid super fast and responsive.

1 Like

I don’t know if this will help you visualize better the state

If you look closely, you will see that almost every item uses the same pattern I mentioned above, current and list[].

I know the dox say you can have more than one state, but it seems to me that the whole point of aurelia-store is that everything is stored in one place only.

To give you a specific idea from the state you see in the picture …

  1. A purchase of goods is made from a supplier
  2. When the truck arrives at the warehouse an arrival is recorded
  3. After the goods are inspected (part of the arrival) the warehouse manager moves them into stock.

Say I am on the “arrival page” and I need to display some information about the purchase, - I already have state.purchase (I know an arrival can only come from that purchase). If I need the supplier’s full name, I already have a list of suppliers (supplier.list), so I can do a quick

const name = this.state.supplier.list.find(c=>c.id==this.state.purchase.supplierId).name;

Actually the way I really “learned” about store, was because this was the first app I’ve written that is mobile only - so I have to have the list of items in one view and the details in another view. I’m constantly hopping around between the list and the details. I don’t have the physical space on the screen to show the master/details. It forced me to completely rethink what I was doing - for example sending the supplier’s name as a parameter on the route so I could display it in the arrival view.

I don’t proclaim to be an expert on aurelia-store by any means - you need @zewa666 for that :smile:

2 Likes

@bigopon I have prepared a simple example of a form. I would like to be able to observe all “data” changes easily: https://codesandbox.io/s/complex-form-example-b63wo

1 Like

@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 https://codesandbox.io/s/complex-form-example-gu01h

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() {
    ...
  }
}
1 Like

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