Nice!
> It seems to work quite well, allowing you to add/remove/replace/move items
> anywhere in the array. I dislike that it requires you to use a custom API
> rather than the normal Array.prototype.push (etc) but that's the price you
> pay for having a custom data type.
If you *really* wanted to use normal arrays/object and implement
something similar to React's diffing, I don't think it would be *that*
hard to implement. Conductance ships with the jsondiffpatch library
(it's in the doc browser) which can be used for teasing out the
changes in an array/object. The basic idea would be to have a
component that keeps around the old version of the state and then
diffs it when a new value comes along. The component would then
translate the information in the diff to dom manipulations:
@jsondiffpatch = require('sjs:jsondiffpatch');
function ListWidget(array_observable) {
return @Ul() .. @Mechanism(function(list) {
var old = [];
array_observable .. @each {
|current|
var diff = @jsondiffpatch.diff(old, current);
old = current;
/* XXX code to translate diff into dom manipulations */
}
});
}
> This system is basically just as good as React's system: you can attach the
> mechanism to any element and then specify an "animation-start" or
> "animation-end" class. It will apply the "animation-start" class when the
> element is first created, then transition to the "animation-end" class. When
> removing an element, it will transition from "animation-end" to
> "animation-start", effectively reversing the animation. It will wait until
> the animation is complete before removing the DOM node.
>
> This makes it very easy to handle the common case of animating things, but
> the downside is that you have to use the special ArrayDiff stream in order
> for it to work. You can't just slap an array into any old stream and expect
> it to work.
Right, if you want to use a stream that contains copies of the whole
state, then either you need to live with the limitation that more DOM
gets updated (not an option in your case because of the animations) or
do something like the jsondiffpatch thing above.
>
> I also ran into a bit of an issue on line 184: if I use normal "each", it
> gets messed up if a new event happens while in mid-animation. So I have to
> use "each.par" or "each.track". I'd like to deeply understand the
> implications of that.
Yeah, the issue is that you wait for the animations. If you use normal
@each and changes arrive while your change-handling function is
currently busy, then those changes are lost (those are the semantics
of event streams).
Both @each.track or @each.par could be used to make this work, but
both require a little bit of rewriting of your code. There is also a
3rd possibility.
* @each.track
@each.track aborts the current change handling when a new change arrives.
This would be appropriate if you want only one animation to be
happening at any given time, with the animations of new incoming
changes taking precedence over any one currently executing.
The way your code is currently written now, the aborting can sometimes
cause elements from not being removed because the code is aborted
while busy in reverseAnimation, before reaching removeNode.
It is quite easy to fix the code so that @each.track works though. It
basically entails putting all the code that *must* be executed for
handling the change into the finally section of a try/finally block.
When code is aborted, any outstanding finally sections are guaranteed
to run. This interplay of aborting (also called 'retraction' in SJS) &
execution of finally blocks is a fundamental aspect of SJS that makes
the language very composable.
In your case you would e.g. need to change the code from
child .. reverseAnimation;
child .. @removeNode;
to
try {
child .. reverseAnimation;
}
finally {
child .. @removeNode;
}
* @each.par
@each.par calls the change-handling function concurrently each time a
change arrives... it there are already 10 invocations of the function
currently running, @each.par doesn't care, it will just call it again.
The problem is that you now might get a desynchronization of state: If
you e.g. in short succession drop the first two elements of the list
and communicate these changes as 'remove item 0', 'remove item 0',
then if these run concurrently, the both change handlers might operate
on the same element.
Therefore, to get @each.par to work, you'd need to find a stable way
to reference elements, e.g. giving them unique keys, and storing those
keys on the element.
* 3rd alternative: sequential handling & buffering.
If you know that changes are infrequent, but you just want to guard
against the occasional situation where changes bunch up, you can stick
with normal sequential @each, but add some buffering:
stream .. @tailbuffer(100) .. @each { ||... }
>
> Another thing I dislike: the DOM code relies on the implicit API that the
> ArrayDiff stream provides. This wouldn't be a problem if Conductance had a
> standard way of specifying changes to an object, but currently it does not
> have such a standard. So, in essence, the DOM code and ArrayDiff code
> *could* be separate... but they currently are not, which is what I dislike.
Yeah, it's difficult to come up with a standard way of specifying
changes... it really depends on the situation. And the way you want to
apply changes also depends on the situation. E.g. in a chat
application, you might want to animate single messages if they come
in, but if a whole lot come in at the same time, then you might want
to use a different animation on the whole group.
> I'm also not sure about the Mechanism API: from what I understand, the
> "dom_elements" mechanism will be attached once to the <ul> and will then
> remain there for the duration of the page. In other words, it will not be
> removed and reattached, which is better for performance.
It will be aborted & removed when the element that it is attached to
is being removed from the DOM.
If the mechanism has a try/finally block, then the finally code gets
executed when the element is removed from the DOM.
> What happens if you
> attach the same mechanism to multiple different elements? Does it create a
> new instance per element?
The mechanism will be executed for each element that it is attached to.
> What if you attach the same mechanism to the same
> element repeatedly?
>
> Also, is the Mechanism API the idiomatic/normal/expected way to do weird DOM
> manipulations that aren't handled by the built-in surface library?
>
Yes :-)
Another comment on your code:
I saw that you used
var parent = elem.children[0]
to access the list, and you were probably surprised by that. Why does
'elem' not point to your list directly?
That's because you're applying the dom_elements mechanism to a html
*fragment* rather than an html element: Conductance is not (yet)
clever enough to figure out that `<ul></ul>` is a single Html element,
therefore it wraps it into a dummy element (<surface-ui>: you can see
this if you inspect the DOM) before passing it to the mechanism.
Mechanisms and other wrappers such as CSS, On, Class, etc always do
this conversion. The way around this is to pass in an instance of
"Element". In your case:
@mainContent .. @appendContent(@Element('ul') .. dom_elements)
or
@mainContent .. @appendContent(@Ul() .. dom_elements)