knockout / tko

🥊 Technical Knockout – The Monorepo for Knockout.js (4.0+)
http://www.tko.io
Other
275 stars 34 forks source link

Bundle "knockout-es5" ko.track(), etc. in main build #14

Closed davetropeano closed 6 years ago

davetropeano commented 7 years ago

With the alpha the knockout-es5 plugin seems to work fine and with tko I think that this should be part of the standard build. The syntax of declaring a viewmodel as a POJO and optionally making attributes observable is cleaner (and not that this matter too much, more in line with other view libraries). It also of course removes the need for () on observables.

Example:

class App {
  constructor() {
    this.loggedIn = false;
    ko.track(this);
  }

  login() {
    this.loggedIn = true;
  }

  logout() {
    this.loggedIn = false;
  }
}

let app = new App();
ko.applyBindings(app);
brianmhunt commented 7 years ago

Hi Dave - I was thinking either es5 type bindings - or the new "proxy" standard, which would be even better theoretically.

Mulling this - there are other issues that raise this, it's be nice to reference them here.

Cheers

davetropeano commented 7 years ago

At first glance it does seem to make sense to implement something like track() with es6 proxies... +1

davetropeano commented 7 years ago

Here's a nice article about implementing observables using es6 proxies:

Writing a JavaScript Framework - Data Binding with ES6 Proxies

codymullins commented 7 years ago

How would this work? Would all properties automatically be observable or only if they were bound/found in the UI?

codymullins commented 7 years ago

Additionally how would this work for backwards compatibility?

davetropeano commented 7 years ago

It could work like knockout and the knockout-es5 plugin does today (and other frameworks do as well). By default, nothing in the POJO is observable. There is an API ko.track(obj) that takes a reference to an object or an array of property name strings.

With such an approach, the ko.observable() and ko.observableArray() functions are are unchanged from the user's perspective

abram27 commented 7 years ago

It would be nice not to have to define the variable and again pass it to track when not making all the properties in the view model observable. I view the way in which observables are initialized and accessed as two different issues even though they are related. Something like this may work well:

ko.addObservables(this, 
     {
            loggedIn: false,
            userName: null,
            othersLoggedIn: []
     });

I guess this could happen currently with the function mapping.fromJS result passed into a merge function with this. It should potentially be a global setting to automatically get/set an observable vs using parentheses.

ryansolid commented 6 years ago

I think this is a tricky thing. I've spent a lot of time coming with patterns around using Proxies and Getters/Setters around observable properties and even toyed with using TC-39 Observable Streams. And after all of that I have come to appreciate the simplicity of Knockout's Observables. I think there are good patterns and solutions in that space but I'm not sure they are Knockout.

Approaching this from the Getter/Setter and Proxy side of things you are desiring basically a plain object that provides a very simple interface to track dependencies. The gotcha is that in this case the thing that is observable is the objects properties, not the item itself. In so while the object here can be considered the view model, the getter is always returning the value so you have to either use a helper function, property accessor, or secondary getter to get the observable.

vm.counter; // the value - ex 5
obsv(vm, 'counter'); // ko.observable via helper
vm.get('counter') ; // ko.observable via accessor
vm.counter$ // ko.observable via secondary getter ie.. $ appended gets the observable

Proxies are interesting since they bring nesting to the table which is arguably the biggest pain in knockout in the sense that if you want deep data to be observable you have to map it. Even more interesting is you only have to map it at first request time so if nothing depends on a certain property you don't even need to wrap it in an observable.

Each nested level is still a proxy itself so it can be passed around, generate children proxies and nested into other proxies. But the object becomes the thing. If you wish to pass around single values or computeds they end up having to belong to a parent object or having a completely different syntax. I've found that acceptable but it is a consideration when you just want to pass a counter between components or view models. Not to mention taking data and mapping it in specific ways for certain uses is a bit stranger since the mapping to observable is happening automatically. It actually takes some thought to figure out what has really changed when you are mapping nested structures between different view models/components and should you have a Computed that returns an object should that object be coming back wrapped up as a nested Proxy object.

Knockout Observables are very different than TC-39 Observables and intentionally so. I'm pretty sure RX was already floating around in the early days of KO and Steve made some really key decisions. The fact they carry their values with them is huge. I tried to make a KO view data-binding library using RX Subjects and quickly realized that once you get to transforming the data you lose the ability to find the latest value outside of the pipe made it really awkward to setup things like computeds. As assuming you don't need computeds with Observables because you have all the operators to merge all your pure streams the amount of overhead to reduce every binding to a single value was insane. While the operators provided very clear code in my View Models the second I changed focus to my view code all my gains were squashed. If you have to make new variables for every combination of variables explicitly you are losing a lot of the gains of Knockout. Not to mention 2 way binding is interesting challenge because every chain needs to start and end with a Subject.

So the next idea was reducing the interopt so I'd end every TC-39 Observable with a toKO that would give me a computed, but the syntax wasn't worth the effort. Transforming from KO and back again was too verbose. I actually ended up working on porting my favorite operators and a couple custom operators for arrays in pure knockout. In essence I treated knockout arrays as single values instead of streams and the rest worked the same (map, filter, scan, flatMap). In so today I have a decent amount of knockout code that looks like:

users = ko.observableArray()
minmumAge = ko.observable(18)
mappedUsers = users.arrayMap(user=>{
    return {
        firstName: ko.observable(user.firstName)
        age: ko.obserable(user.age)
    }
}).map(list => {
    return list.filter(user => user.age() >= minimumAge())
})

So this leaves me with 2 different situations. One place where I'm embracing POJO looking proxies and another where I'm embracing functional transformations. One wants to mostly ignore KO Observables exist under the hood and the other is constantly using them directly. I think there is probably a way to reconcile both sides but moving further in either direction makes it more difficult for the other mentality I think.

caseyWebb commented 6 years ago

I want to chime in before anyone puts work into a Proxy implementation that Proxies can not be polyfilled or transpiled. I believe that alone is enough to rule out their usage in KO.

I'd also like to throw out knockout-decorators, because I think it scratches the itch of using POJOs, while remaining explicit about it.

martonx commented 6 years ago

I think if somebody need es5 compatiblity, then he should use 3.X instead of 4.

caseyWebb commented 6 years ago

IE compat is the killer feature of Knockout. I'm sure I'm not alone in saying that dropping IE9 support is a deal-breaker. Dropping IE6-8, is one thing, but ES5 is still in use all over the place.

brianmhunt commented 6 years ago

Knockout proper should be es3 compt through at least version 4 and 5.

TKO on the other hand may need Es6; you can see ko.proxy built into master, now, for beta 1. (Check out the .md in tko.conputed/docs)

brianmhunt commented 6 years ago

Closing this as ko.proxy is now in tko, and will be exposed for beta-1