There are a number of areas for improvement in Bobtail, including but not limited to the following:
Asynchronous programming in JavaScript is the norm. All but the simplest applications require asynchronous
communication with backend services of one sort or another. There are some
draft ideas for an implementation of
an AjaxDepCell type (as well as a partially-reloadable bind, allowing one to refresh a part of a query), but
nothing set in stone. Settling on a clean, simple API for working with Promises is probably our highest priority.
And while we are not going to try to reimplement RxJS or Bacon.js, an API for subscribing to event streams seems highly desirable.
Currently, rx.transaction only delays events until the transaction
finishes. It still fires every event that was registered in the
transaction. It would instead be preferable if we could compress
multiple invocations of the same event. Consider:
x = rx.cell(0);
rx.transaction(() => {
x.set(1);
x.set(2);
x.set(3);
});
At the end of the transaction, x.onChange will fire three times, with arguments
[0, 1], [1, 2], and [2, 3]. For many if not most applications, we'd prefer for
these changes to be compressed into a single change event, with argument [0, 3].
The basic implementation would be fairly straightforward: Events would be rewritten to be called with variadic arguments. Each event would know how to com
While it is easy to envision how this could be accomplished for ObsCells, there
are wrinkles with the other data types. In particular, ObsMap's three events
are not amenable to squashing.
In addition, we need to ensure that, once we've squashed the events, that we fire them in topographical order, so that we do not end up refreshing cells more than once. The algorithm we're currently using to propagate refreshes calculates a topologically sorted DAG from a single source node, but a transaction inherently propagates events from multiple source nodes, and the topo sort algorithm we are currently using will not work in this case.
Finally, there is the question of whether we should change the default transaction
behavior, or introduce another function, such as digest, that will squash events.
Unlike the other data types, ObsMap's currently fire up to three events on a
mutation: onAdd, onRemove, and onChange. This seemed like a good idea at
the time. However, it's inconsistent with the other data structures, makes working
with ObsMaps unnecessarily complicated, and is standing in the way of implementing
transaction squashing (above).
Currently, the ObsArray.map callback signature only takes a single argument,
whereas Array.map's callback takes three: currentValue, index, and array.
To get a version with an indexed map function, it's necessary to construct an
IndexedArray with the .indexed method. This has led to convolution and code
duplication inside pur codebase, and is counterintuitive. Instead, the basic
ObsArray.map method should behave the way IndexedArray.map does today.
Currently, ObsArray.map is the only ObsArray method optimized to
minimally refresh. reduce, filter, et. al. all reference .all,
and thus any change to the parent array will force a full recalculation,
going over the entire array. It would be nice if we could instead perform
partial refreshes, for instance by having .every check that only
newly added elements satisfy the condition, rather than re-checking
the entire array.
One of the main sources of noise in the templating DSL is the need to liberally scatter bind calls throughout. One bit
of syntactic sugar that could be used to reduce this noise is to allow subscriptions via function. That is, if a
function is supplied as a child or an attribute, wrap that function in a bind, like so:
let x = rx.cell(5);
let cls = rx.cell('red');
let $div1 = tags.div({class: () => cls.get()}, () => x.get());
This would be equivalent to:
let $div1 = tags.div({class: rx.bind(() => cls.get())}, rx.bind(() => x.get()));
... just with a somewhat terser syntax.
There is a slight wrinkle to this, which is that the event fields already expect a function. In theory, one can have
the event handler bound to observables. There is, after all, no reason why the value of an ObsCell cannot be a
function. In practice this is rarely useful--any switching you want to do can be handled
inside the handler, so we could special case those fields to ignore the bind-wrapping.
Currently tag functions can take at most two arguments. The first argument is either an attributes object, a single child element, or an array, either primitive or reactive, of child elements. If the first argument is an attributes object, then the second is used to hold the child/children.
This was fine in CoffeeScript, with its terser syntax. However in Javascript, this leads to a proliferation of parentheses and brackets, like so:
tags.ul({style: {listStyle: 'list-unstyled'}}, [
tags.li('hello'),
tags.li('world')
]);
If however we children could be specified with spread or variadic arguments, then we could remove the square brackets, and write something like:
tags.ul({style: {listStyle: 'list-unstyled'}},
tags.li('hello'),
tags.li('world')
);
Currently, tag contents cannot be a nested array or nested reactive object. Nor, if the contents are a primitive array,
can any of its elements be an ObsCell. You can call rx.flatten to get around this, however it would be nice if
instead we auto-flattened tag contents before proceeding.
Consider the case where we want to have the class of an input dependent on its value.
Or, consider a case where I ran into recently, where I wanted to enforce that the sum of two numeric inputs could not be greater than 7.
There is no easy way to do this without resorting to imperative programming.
One partial solution would be to allow users to update element attributes with a reactive bind after they've been created:
let $in1 = tags.input.number({min: 1});
let $in2 = tags.input.number({min: 1});
let max1 = bind(() => 7 - $in1.rx('val').get()));
let max2 = bind(() => 7 - $in2.rx('val').get()));
$in1.attr('max', bind(() => max1.get())
$in2.attr('max', bind(() => max2.get())
This is imperative in style, however. A declarative approach would be greatly preferable.
Currently, it is possible to create a tag where we bind the entire style attribute to a cell, but not any of its sub-
attributes. That is:
let pixels = rx.cell(50);
// this is OK
tags.div({style: bind(() => {height: pixels.get()})});
// this is not OK
tags.div({style: {height: bind(() => pixels.get()}});
It would be nice if we could bind individual style attributes like that, and then if any of them were to update, to
only change that style attribute on the tag, rather than calling $.css.
Currently, Bobtail ships with functions for creating SVG tags in rxt.svg_tags, that are analagous to creating the
regular tags in rxt.tags. Unfortunately, it's not currently documented, and there's no example code that I know of.
It'd be good to bring this up to full support.
As mentioned, radio button change events only fire when a button is selected, not when it's deselected. Having an easy way to bind to the currently selected radio button in a group seems like a useful and valuable proposition.
There are a number of wrinkles with select elements. For one thing, when children are added or removed, the value of
the select can change without an event firing. A built-in select tag function that handles cases like this would be
very useful.
Ability to apply animations and effects to things like entrances, exits, and reorderings. See the d3 transitions API for possible inspiration.
jQuery is a large library, but we only use it for a few things. It would greatly reduce bobtail's minified size if we could make the jQuery dependency optional (one would of course then lose the jQuery extension function), or swappable with a lighter weight framework such as Zepto.
Currently, if a cell that a Bobtail template depends on changes, we will immediately attempt to recompute and redraw
the relevant DOM subtrees. This, however, is inefficient. The average screen only refreshes 60 times per second, and
indeed we could find ourselves attempting to redraw multiple times between screen refreshes. Instead of immediately
refreshing the DOM, we could instead mark the sections that need to be refreshed, and wait redraw those when the screen
next rerenders, using the
window.requestAnimationFrame API.
