We're typically quite conservative about adding significant new features to Knockout core, since we highly value keeping it as a small focused library, and not a big heavy framework.
This is good, but web development in 2014 as a whole is now converging on a newer way of building applications. Inspired by future standards including Web Components (with HTML imports and custom elements), plus experimental libraries such as Polymer.js, you can see that developers will soon want and expect to build highly modular applications, composed from reusable encapsulated pieces, with extremely clean markup.
Knockout in 2014
How should Knockout.js respond? One possible response would be to do nothing, and say that people targetting really new browsers can find their own ways of combining those new standards with KO. Possibly using polyfills, even though it's hard work. But if we do that then I think the relevance of Knockout will decline.
A different response, which I favour, is to embrace this direction and provide really first-class support for 2014-style web development in Knockout by means of a few carefully-chosen core features.
And among all the major JavaScript libraries, Knockout is probably the only one that can deliver this in a way that works seamlessly right back to IE 6, because KO already has exceptional backward compatibility.
Proposed new features
I've given this a fair bit of thought and built some prototypes, and here are the three interrelated features that I think are sufficient and necessary:
- Components - an easy way for your view to asynchronously pull in separate things that encapsulate their own viewmodel and view
- Custom elements - a simple and satisfying syntax for referencing components without all the
data-bind
- Text interpolation - a.k.a.,
{{ curly }}
expressions inside text and element attributes
These things don't turn Knockout into a framework - it's still a library - but they are hugely flexible tools that encourage and support more modular architectures.
Prototype: More about this later, but if you're impatient, here it is.
Components
A component is a combination of view and viewmodel. You register a component with some name before you can use it:
ko.components.register("cart-summary", {
template: ..., // Element name, or source URL, or a function that asynchronously returns a node array,
viewModel: ... // Object, or AMD module name, or a function that asynchronously returns an object
})
Now you wouldn't often use the component
binding directly (since you'd probably use a custom element instead - see below), but if you wanted to:
<!-- ko component: { name: "cart-summary", data: someData } --><!-- /ko -->
Of course, that looks ugly, hence the custom elements feature below. But what this does is pass the someData
value to the template
and viewModel
callbacks for the component, then when it's got the results, renders the template with the viewmodel. Simple, but also powerful.
Implementation
This is pretty much just a variation on the template
binding, except it supports retrieving both a view and a viewmodel asynchronously before it renders. We could even implement it by extending template
, but I suspect it might be cleaner to keep it separate.
Custom elements
For a nice way of consuming a component, use custom elements:
<cart-summary user="{{ currentUser }}" max-items="10"></cart-summary>
Given that you've registered a component named cart-summary
, this just works. The data
value it passes to the underlying component in this case is { user: currentUser, maxItems: 10 }
.
And here's the whole point of this: now your application doesn't have to be a monolith any more. Instead of one master viewmodel that is directly coupled to child viewmodels which are directly coupled to more child viewmodels, i.e., a monolith, you can now instead have many completely independent viewmodels that have no knowledge of each other, and exchange data only via custom elements in their views.
This makes code easier to reason about, and pieces easier to reuse. And if people build really general-purpose components (grids, etc.), they can be packaged as independent open source projects.
Implementation
In case you're wondering about the {{ ... }}
syntax, and how it's possible for components to know when the values change (and write back changes) even if they aren't just observable instances, well that actually works out quite nicely. KO knows when evaluating them whether they have any dependencies, and hence might change in the future, and in that case can wrap the accessors in writable computeds. Sounds complex but the result is that it just does what you'd want and expect.
Text interpolation
This is way overdue. In a sense it's separate from components and custom elements, but the lack of this feature in core is kind of embarassing already.
It just means this:
<a data-bind="attr: { href: '/items/' + id() }">
See also: <span data-bind="text: name"></span>
</a>
... can be written:
<a href="/items/{{ id }}">See also: {{ name }}</a>
That's a bit better :)
Having this just means that all your components' views become not annoying at all. It does not eliminate data-bind
; it just makes it unnecessary for inserting text and setting simple, write-only attributes.
Implementation
We could already do this easily as a node preprocessor. However since this involves walking the text inside every attribute inside every element, performance is pretty vital. We probably want to be sure that we only go looking for the curly braces once per DOM fragment, not every time the same DOM fragment is inserted into the document (e.g., in a foreach
). Maybe this fits into "template rewriting", but I haven't actually dug into this yet. I'd be especially interested in whether @mbest has ideas for making this super-fast.
And BTW - the curly braces used for custom element parameters would be implemented separately, because on custom elements you're not just assigning attributes.
A prototype
I've spiked up rough versions of components and custom elements here: https://github.com/SteveSanderson/ko-custom-elems-test/. There's a live demo too, though the end result is not as interesting as the code.
This proof-of-concept includes:
- Incremental loading. When you first arrive, it only loads code related to what's on the screen. As you navigate around, it automatically pulls in any additional JS/HTML needed for what you're seeing. This is thanks to components supporting asynchronous loading. The actual JS-file-loading works via require.js in this demo, but you could also use any other way of fetching viewmodels.
- Routing via crossroads.js. I'm not proposing this somehow becomes part of KO; I'm just showing it works cleanly with components.
- Browser support right back to IE 6, even with all the custom elements
For some of the APIs, I've put in a lot of thought already. For other APIs, it's pretty arbitrary. All of the implementation is rough. No doubt we (and especially Michael) can come up with more streamlined implementations.
This prototype does not yet include text interpolation. It would be easy to add it in a simple way, but I'm interested in nailing the performance on that first.
Why not a plugin?
We could totally do this as a plugin. That might even be a good way to start.
However, the goal here is not just to provide some new features that some people might like. It's about aligning the Knockout project with the direction of the industry as a whole, making the core library relevant and appealing to web developers in the coming years.
Feedback requested
Please let me know what you think!
Please try to keep replies focused on the features described here. For discussion of any other possible features (or existing bugs, etc.), many other great GitHub issues are available :)
type: meta