canjs / can-observe

Observable objects
https://canjs.com/doc/can-observe.html
MIT License
20 stars 2 forks source link

Expand can-observe to work with nested data structures #21

Closed justinbmeyer closed 7 years ago

justinbmeyer commented 7 years ago
var observe = require("can-observe")
MyClass {
  constructor(){
    return observe(this)
  },
  method(){}
}
can.Component.extend({
  tag: "some-thing"
  view: stache(`{{expandedProp}}`)
  ViewModel: MyClass
})
<some-thing expandedProp:from="123">
justinbmeyer commented 7 years ago
MyClassA {

}

MyClassB {
  method() {
    this.myClassA = new MyClass()
  }
}
var b = new MyClassB();
canReflect.onKeyValue(b,"myClassA", function(newValue){

});

b.method()
justinbmeyer commented 7 years ago
Person {
  get fullName(){ ... }
}

{{person.fullName}} {{person.fullName}}
justinbmeyer commented 7 years ago

make sure cycles work

justinbmeyer commented 7 years ago
bgando commented 7 years ago

Tests from the erased data

QUnit.test("Should not duplicate proxies #21", function(){
    var a = {who: 'a'},
    b = {who: 'b'},
    c = {who: 'c'}

    aproxy = observe(a)
    cproxy = observe(c)

    aproxy.b = b;
    cproxy.b = b;

    QUnit.equal(aproxy.b, cproxy.b, "proxied objects should not be duplicated");
});

QUnit.test("Should not duplicate proxies in a cycle #21", function(){
    var a = {};
    var b = {};
    var c = {};
    a.b = b;
    b.c = c;
    c.a = a;

    observe(a);

    QUnit.equal(c.a, a, "proxied objects should not be duplicated");
});

QUnit.test("Should convert nested objects to observables in a lazy way #21", function(){
    var nested = {};
    var obj = {nested: nested};
    var obs = observe(obj);
    QUnit.equal(!!canReflect.isObservableLike(nested), false) //-> nested is not converted yet
    QUnit.equal(canReflect.isObservableLike(obs.nested), true) //-> nested is converted to a proxy and the proxy returned
});

QUnit.test("Should convert properties if bound", function() {
    var nested = {};
    var obj = {};
    var obs = observe(obj);
    canReflect.getKeyValue(obs, "nested", function(newVal){
        QUnit.equal(canReflect.isObservableLike(newVal), true) //-> is a proxied nested
    })

    obs.nested = nested;
});

QUnit.test("Nested objects should be observables #21", function(){
    expect(1);
    var obj = {nested: {}, primitive: 2};
    var obs = observe(obj);
    obs.nested.prop = 1;
    canReflect.onKeyValue(obs.nested, "prop", function(newVal){
        console.log('hi')
        assert.ok(true, "change is triggered on a nested property");
    });
    var x = obs.nested;
    x.prop = "abc";
});
bgando commented 7 years ago

This test passes, but I'm not sure where to put this:

var Component = require("can-component");
var stache = require("can-stache");
var observe = require("can-observe");

QUnit.test('Works with can-observe', function () {

    var vm = observe({
        firstName: 'Chris',
        lastName: 'Gomez',
        fullName: function() {
            return this.firstName + ' ' + this.lastName;
        }
    });

    Component.extend({
        tag: 'can-observe-component',
        viewModel: vm,
        view: stache('Name: {{fullName}}')
    });

    var frag = stache('<can-observe-component firstName:bind="firstName" lastName:bind="lastName" />')(vm);

    QUnit.equal(frag.firstChild.innerHTML, 'Name: Chris Gomez', 'Rendered fullName');

    vm.firstName = 'Justin';
    vm.lastName = 'Meyer';

    QUnit.equal(frag.firstChild.innerHTML, 'Name: Justin Meyer', 'Rendered fullName after change');
});
bmomberger-bitovi commented 7 years ago

This test passes, but I'm not sure where to put this:

Assuming this test also passes with MockComponent, let's break MockComponent out of can-stache-bindings and into can-test-helpers, which would make it available to both.

bmomberger-bitovi commented 7 years ago

For historical reference, a more expressive version of the above test is pasted below (it will not be included in the published version):

QUnit.test('Works with can-observe', function () {
    var stache = require("can-stache");
    var observe = require("can-observe");

    var parentVm = observe({
        firstName: 'Chris',
    lastName: 'Gomez',
    });

    var childVm = observe({
      fullName: function() {
          return this.firstName + ' ' + this.lastName;
      }
    });

    Component.extend({
        tag: 'can-observe-component',
        viewModel: function(initialData) {
            Object.keys(initialData).forEach(function(key) {
                childVm[key] = initialData[key];
            });
            return childVm;
        },
        view: stache('Name: {{fullName}}')
    });

    var frag = stache('<can-observe-component firstName:bind="firstName" lastName:bind="lastName" />')(parentVm);

    QUnit.equal(frag.firstChild.innerHTML, 'Name: Chris Gomez', 'Rendered fullName');

    parentVm.firstName = 'Justin';
    parentVm.lastName = 'Meyer';

    QUnit.equal(frag.firstChild.innerHTML, 'Name: Justin Meyer', 'Rendered fullName after change');
});
bmomberger-bitovi commented 7 years ago
  • Make sure it works with can-component. Force in can-component?

I'm not in favor of forcing this just yet, because can-component does this one weirdly. If you pass a can-observe in as a viewModel, it's treated as a singleton viewModel (which is correct) but does not receive any properties from initial values of bindings (which is confusing). This means that you have to write a generator function and manually copy props because can-observe isn't a constructor, and so the symmetry that we've had in can-component up to this point (prototype object <-> constructor class) isn't available. Let's get a consensus on how to approach this, and maybe make it official in docs.

justinbmeyer commented 7 years ago
class Foo(){
    constructor() {
        return observe(this);
    },
    method: function(){
        this.prop = "zed";
    }
}

can.Component.extend({
  ViewModel: Foo
})

can.Component.extend({
    ViewModel: function(){
        return observe({
            method: function(){
                this.prop = [1,2,3]
            },
            pop: function(){
                this.prop.pop();
            },
            get foo() {

            }
        })
    }
})