CanJS Roadmap Discussion


#1

I’d like to start planning out the goals / vision of CanJS for the next year. These are ideas focused on just the “code”, and not ideas around evangelism/documentation/etc.

I haven’t thought it all through, but I see the following projects being important to keep pace with other OS tools, specifically React.

My high-level vision is to keep the benefits of CanJS (and some future CanJS plans) over react, while adopting the best of React.

I’d like to keep CanJS’s

  • very strong observables, which are necessary to solving a lot of structural problems with react applications (way too much code in top container)
  • ability to interface with other technologies (works great with normal jQuery widgets).
  • the performance benefits of can-derive with live-binding

With react, I’d like to adopt:

  • JSX style templating

Here’s what I think this means:

JSX style templating with live-binding

can-derive and live-binding can have algorithmically faster DOM updates than a react-style DIFF. A react style render and diff requires, at minimum a O(2n) cost. One loop of data to render the VDOM, another loop to do the DIFF. can-derive can do a filter in O( log(n)*log(n) ) = O( log^2 n) time. This time can become significant at 64 items (128 vs 36 operations) and quickly outweigh the usually dominant DOM operations.

Also, in big applications, if some shared bit of state is changed, the DIFF approach requires a re-render and diff of the entire page and all sub-components. Observables allow us to limit the scope of changes.

While diffing is great tech, I think we can provide functional programming and allow faster updates. Plus there are a lot of subtle things that observables make better, like list stores, that flux-like (observable-less) architectures make much more difficult.

What React really gets right, and what we abandoned from 1.1 to 2.0, is the use of “plain” JavaScript in its templates. This was done for two reasons:

  1. The use of plain JS in templates was painful in EJS. This was largely due to the need to use attr:

     <%= todo.attr("name") %>
    
  2. It was difficult to make data-bindings work.

#1 will be addressed in the next section. For #2, it will be possible using much of the stache core technology (can/view/parser, can/view/target, can/view/live) to make a new templating language similar to JSX, but uses live binding for the performance reasons listed above. For example, we can transpile:

function(viewModel) {
  <child-component value={viewModel.propertyName}/>
}

into:

function(viewModel) {
  return can.view.target([{
    tag: "child-component",
    attrs: {
      value: can.compute(function(){ return viewModel.propertyName  })
    }
 }]);
}

(note: the template binding syntax is not decided here … I’m using react-like syntax for an example)

Another example:

function(viewModel) {
  <ul>
  {
    viewModel.items.map((item) => {
      return <li><child-component value={item.value}></li>
    })
  }
  </ul>
}

Becomes:

function(viewModel) {
  return can.view.target([{
    tag: "ul",
    children: [
      function(){
        return viewModel.items.map((item) => {
          return can.view.target([{
           tag: "li", 
           children: [
             {
               tag: "child-component",
               attrs: {value: can.compute(function(){ return item.value  })}
             }
           ]
          }])
        })
      }
    ]
 }]);
}

Note: if you are wondering why we don’t just use react, please see my comments on performance. If you are still not convinced, I can walk through a long architecture discussion on the importance of observables.

_Note: I’m also unhappy that we would transition to ANOTHER template language, however, we would make sure this works well with stache. Also, building HTML is a HUGE part of web-apps, so it’s not entirely unsuprising that this is where CanJS would consistantly see the most amount of change.

Priority: #4.

.attr-less observables

Allow defining observables that work similar to can.Map and can.List, but can be used with the normal JS DOT (.) operator like:

var person = observe({});
person.first = "Justin";
person.last = "Meyer";

var fullName = can.compute(function(){
  return person.first+" "+person.last;
});

QUnit.stop();

fullName.bind("change", function(ev, newVal, oldVal){
	QUnit.start();
	QUnit.equal(newVal, "Vyacheslav Egorov");
	QUnit.equal(oldVal, "Justin Meyer");
});

// causes change event above
can.batch.start();
person.first = "Vyacheslav";
person.last = "Egorov";
can.batch.stop();

This project has a demo that works in browsers that support Proxies (Firefox / MS Edge). We can make all modern browsers work if we make people pre-define all their expected properties:

var Person = function(){}
can.define( Person.prototype, {
  first: "*",
  last: "string"
});

Progress on this can be found at can-define.

can-derive and lodash methods

can-derive is what enables algorthimically faster DOM updates than a diff. It uses a Red-Black tree version of can.List. But its use is pretty simple assuming .attr-less observables:

var list = can.observe([1, 2, 3, 4, 5]);
var filtered = list.dFilter(function(item){
  return item % 2
});

We should get all the common lodash methods working. Furthermore, the can-define plugin should be made to work with this nicely:

var TodosListViewModel = function(){}
can.define( TodosListViewModel.prototype, {
  items: {
   value: function(resolve){
     return Todo.getList().then(resolve);
   }
  },
  completedItems: {
    get: function(){
      return this.items ? this.items.dFilter(function(todos){
         return todos.complete;
      }) : []
    }
  }
});

tlvm = new TodosListViewModel();

// the template:
function(viewModel){
  <ul> {
        viewModel.completedItems.map((item) => {
          return <li><child-component value={item.value}></li>
        })
  }</ul>
}

Automatic Batching

Batching should be automatic. A batch should start as the last operation of the end of a requestAnimationFrame and end as the first operation of a requestOperationFrame. We should still allow can.batch.stop(true) to flush events so synchronous testing can be done more easily.

“class” inheritable

Things like components should be inheritable with ES6.

class TodoList extends Component {
  viewModel: TodosListViewModel
}

If this is not possible (because of the lack of extension hooks), we should might make those things simple functions:

TodoList = component(
  viewModel: TodosListViewModel
})

Structural Changes

I think everything in CanJS should be accessible as it’s own npm module, available to Browserify/CJS, RequireJS/AMD, ES Modules and the steal() syntax for legacy support. Each plugin will make itself available as a global so if people are still concating scripts, it’s still possible.

There will still be a can module that will include the core modules on a namespace object.

I think we drop support for everything other than Zepto / jQuery and standalone.

A plan for getting there

Here’s a “shooting from the hip” roadmap.

  • CanJS 3.0 - Basically CanJS 2.3, but with structural changes, stache/define plugin by default, Automatic Batching
  • CanJS 3.1 - can-derive and lodash methods
  • CanJS 3.2 - .attr-less observables
  • CanJS 3.3 - JSX style templating with live-binding

DoneJS Weekly Status - 2015-12-16
#2

JSX is fine.
What about done-component approach - define whole component in actually HTML file? How do these correlate?
JSX supports only one main root element, what are plans about it?


#3

I like all these ideas and the general theme of moving where the general JavaScript Community is going (JSX, Modular, ES classes, etc…)

Might I also suggest, that we begin to think about reconciling* our observables (can.compute, can.Map, can.List) with the ES2016 Proposal for Observables which is at stage 1 and championed by Kevin Smith & Jafar Husain. Of all the other stage 1 proposals I feel this has the most chance of landing in the 2016 spec.

We should consider having stash and the proposed JSX style templating work with ES Observables, so that they become a more generic use and valuable approach for the general js community.

*What reconciling means, well that’s up for discussion I guess


#4

Those observables are different in purpose from ours. It’s a general subscription interface more aligned to streams. While useful, our computes need a read notifier.

Also, we would have a very long time to adapt. I’m also not sure exactly how that API would fit with list/map (though I could see it apply to computes)


#5

I like the general roadmap. I have one comment and something I’d like to add to the roadmap.

First, I think pulling off jsx with computes will be difficult. How can you handle things like if statements:

function render(viewModel) {
    if(someCompute()) {
      return <div> ... </div>
    }
    return <span> ... </span>
}

You can make the render function a compute itself but then you wind up needing to do diffing. Don’t want to get too far into the weeds with the “how”, but I think this will be a difficult task.

FRP

I’d like to see an FRP pattern emerge as an alternative to MVVM. FRP is a nice fit in CanJS because computes are quite similar. However ViewModels are fundamentally stateful containers and therefore not a good fit if using functional reactive techniques.

I’m a fan of the model-view-intent pattern that lets you define a component as:

  • intent: events you are interested in
  • model: data derived from the intent
  • view: html derived from the model.

It could look something like this:

function counter(element) {
  return view(model(intent(element)));
}

function intent(element) {
  return {
    clicks: Rx.Observable.fromEvent(element, 'click')
  };
}

function model(actions) {
  return {
    counter: actions.clicks.scan(0, function(value) {
      return value + 1;
    }).toCompute()
  }
}

function view(model) {
  return <div>{model.counter}</div>
}

this pattern might not be a good fit, but i’d like to see some pattern with event streams as the focus and avoiding stateful viewmodels


#6

This is a limitation of React, not jsx. You could allow returning an array from a render function:

function render(viewModel){
  return [
    <div> ... </div>
    <span> ... </span>
  ];
}

#7

You can use different templates within a done-component already. So this should work fine with that approach.