gnaeus/knockout-decorators

Implement "deep" @observable property

gnaeus opened this issue · 8 comments

Expected behaviour:

class ViewModel {
  @reactive
  deepObservable = {            // like @observable
    firstName: "Clive Staples", // like @observable
    lastName: "Lewis",          // like @observable

    array: [1, 2, 3],           // like @observableArray

    object: {                   // like @observable({ deep: true })
      foo: "bar",               // like @observable
      reference: null,          // like @observable({ deep: true })
    },
  }
}

const vm = new ViewModel();

vm.deepObservable.object.reference = {
  firstName: "Clive Staples", // make @observable
  lastName: "Lewis",          // make @observable
};

vm.deepObservable.array.push({
  firstName: "Clive Staples", // make @observable
  lastName: "Lewis",          // make @observable
});

Will the variable itself be observable?

class ViewModel {
  @observable({ deep: true }) obj = {
    firstName: "Clive Staples", // like @observable
    lastName: "Lewis",          // like @observable

    array: [1, 2, 3],           // like @observableArray

    object: {                   // like @observable({ deep: true })
      foo: "bar",               // like @observable
      reference: null,          // like @observable({ deep: true })
    },
  }
}

(async () => {
  const vm = new ViewModel();
  vm.obj.lastName = 'Foobar'; // Works, cool
  vm.obj = await fetch('some/api').then((response) => response.json()); // Will this work?
})();

Yes, I think it should be observable.
I update description.

Great.
There are 2 typos, it needs to be:

const vm = new ViewModel();

vm.obj.object.reference = {
  firstName: "Clive Staples", // make @observable
  lastName: "Lewis",          // make @observable
};

vm.obj.array.push({
  firstName: "Clive Staples", // make @observable
  lastName: "Lewis",          // make @observable
});

or

const vm = new ViewModel().obj;

vm.object.reference = {
  firstName: "Clive Staples", // make @observable
  lastName: "Lewis",          // make @observable
};

vm.array.push({
  firstName: "Clive Staples", // make @observable
  lastName: "Lewis",          // make @observable
});

How can I help?

Thank you! I fix description again.

You can create pull request if you want to implement this functionality yourself.
Or you can help with writing unit tests for this.

I would go with unit tests, since I'm not in the code of yours. Cool!

@krnlde, today I push first implementation of observable({ deep: true }) with few unit tests.
You can see it in src/__tests__/deep-observable-test.ts.

But the code needs refactoring and many more unit tests to cover edge cases.
So I don't update /dist/ files and NPM package.

And I think that @observable({ deep: true }) needs one-word shorthand alias, for example @reactive:

class ViewModel {
  @observable({ deep: true }) first = {};
  // will be equivalent to
  @reactive first = {};
}

Also an utility function for creating "deep observable" objects would be useful.
We can name it track() like in Steven Sanderson's knockout-es5 or again reactive()

class ViewModel {
  first = 123; // make @observable;
  second = "test"; // make @observable;
  constructor() {
    track(this);
    // or
    reactive(this);
  }
}
// and
let array = track([1, 2, 3]); // like observableArray
// or
let obj = reactive({
  foo: "bar", // make @observable
});

The new "deep" observable array wrapper can even track assignments
to some index (arr[i] = "something") definitelly like in MobX.

And also I create simple benchmark for "deep" observableArray in src/__tests__/deep-observable-benchmark.ts.
It seems that using our "deep" observableArray is about 2 times slower than native ko.observableArray
and about 100 times slower than plain JavaScript Array without change tracking.

Man you're fast! I could not yet find time to add tests and review the code. Sorry for that. I think I'll find some time near the end of the coming week. Will ping you back soon.

It's because I use my library in production. And our app is in active development now =)

During the last week I refactor many parts of the library and add about 30 new test.
At the next week I will try to release version 0.9.0 (and update readme) with our "deep oservable" functionality.

I decide not to add a new option to @observable decorator. Instead, I added new @reactive decorator.
So now @observable means "shallow observable" and @reactive means "deep observable".
You can find tests for that in src/__tests__/reactive-decorator-test.ts.
But I think, it may not cover all edge cases. So, your tests are welcome.

And also I drop arr[i] = "something" change tracking functionality. Because it slows down Knockout's foreach binding, and increases library size.