Animations with Conductance

62 views
Skip to first unread message

pcxunl...@gmail.com

unread,
Aug 22, 2014, 5:46:22 PM8/22/14
to strati...@googlegroups.com
I can't seem to find anything about dealing with animation in either StratifiedJS or Conductance.

Even something as simple as "when adding an element, start it at opacity: 0 then fade it to opacity: 1" seems like a somewhat difficult task.

I like the way that React.js handles it: https://facebook.github.io/react/docs/animation.html

Basically, they give you a low-level hook that lets you do things when an element is added/removed, and then they build a nicer abstraction on top of the low-level stuff.

React.js uses CSS transitions which are supported by pretty much all modern browsers nowadays, and work well even on mobile. The way it works is that you specify a classname, like "foo". React will add a class "foo-enter" to the element when it's added to the DOM, and will then add "foo-enter-active" on the next tick (event loop cycle, DOM repaint, whatever). And it adds "foo-leave" and "foo-leave-active" when the DOM element is removed.

The neat thing about React's system is that, when removing a DOM element, it will wait until the animation is finished before actually removing the element.

Actually, React.js in general is a very awesome system, it's similar to Conductance/Surface in some ways, but the implementation is radically different.

Anyways, I don't think React.js's system should be copied directly into Conductance, but I think it does need *some* way of handling animations. I think there should be at least two layers: a low-level but powerful system that lets you do whatever kind of animation you want: CSS transitions, hooking up to jQuery or GreenSock GSAP, D3.js, whatever. And then a higher level system which handles the easy cases of "oh I just want to fade in/out this element when it's added/removed into the document".

I'll start experimenting with creating a widget to do this, but I figured I should post here to get any ideas or feedback. I'm very familiar with the DOM, but I'm pretty new to the StratifiedJS/Conductance way of doing things, so my implementation will probably end up being non-idiomatic and sub-optimal.

pcxunl...@gmail.com

unread,
Aug 22, 2014, 6:28:03 PM8/22/14
to strati...@googlegroups.com, pcxunl...@gmail.com
I just wrote a small app to test whether animations would be feasible in Conductance or not:

    @ = require(["mho:std", "mho:app"])

    var elements = @ObservableVar([])

    var i = 0

    function push() {
      return elements.modify(function (array) {
        return array.concat([++i])
      })
    }


    function Animate(elem) {
      return elem .. @Mechanism(function (elem) {
        // Force relayout
        getComputedStyle(elem).height
        elem.classList.add("anim-active")
      })
    }


    var css = @Style(`
      {
        display: block;
        transition: all 1s;
        background-color: red;
        height: 0px;
      }

      &.anim-active {
        height: 20px;
      }
    `)

    var dom = function (tab) {
      return `
        <li>${tab}</li>
      ` .. css .. Animate
    }

    var dom_elements = elements .. @observe(function (array) {
      return array .. @map(dom)
    })

    waitfor {
      while (i < 5) {
        hold(1500)
        push()
      }
    } and {
      @mainContent .. @appendContent(`
        <ul>
          ${dom_elements}
        </ul>
      `) { || hold() }
    }

Running the above confirms something I had already been suspecting: if an ObservableVar contains an array and it changes, Conductance will remove all the DOM elements from the old array, and then insert the elements of the new array.

This is a problem because it means if you have an array with 1,000 elements in it and then you add 1 element to the end, Conductance will remove 1,000 DOM elements, then add 1,001 DOM elements, rather than simply inserting 1 DOM element.

Not only is this bad for performance, but it's horrible for animations! If you try running the above, you'll see that adding a new item to the array will recreate the elements, so they *all* get animated. What you want is for only the new element to be animated, while the old elements stay the same.

React.js handles this by using DOM diffing: they compare the old and new versions of the DOM and only change what is necessary. In addition, if multiple changes happen within a single frame, React will batch them and use requestAnimationFrame to significantly reduce relayout/repaints.

I love lots of aspects of Conductance, but I think React.js does a better job of handling the DOM, so perhaps something could be gained by learning from React.js's approach.

pcxunl...@gmail.com

unread,
Aug 22, 2014, 8:35:51 PM8/22/14
to strati...@googlegroups.com, pcxunl...@gmail.com
To make the code compatible with the latest Conductance, just replace @Style with @CSS.

Alexander Fritze

unread,
Aug 22, 2014, 10:55:50 PM8/22/14
to strati...@googlegroups.com
Yeah, you're absolutely right, Observables have an opaque internal
structure (by design) and are therefore not appropriate for these
scenarios, where you a) have a lot of data, or b) want to communicate
*changes* to the structure in the UI - e.g. by animation, like you
want to do it here.
(We probably shouldn't have used Observables in our tutorial, because
it gives the wrong idea).

However, the problem is that DOM diffing isn't a cure-all answer
either. If your DOM is big and complicated, then it becomes
prohibitively expensive. In most real-world cases, you - the
programmer - will have more knowledge than the computer about the
structure of the changes you will want to make to the DOM, and you
can do better.

Think e.g. of the case where your "DOM" is a 1000x1000 canvas image or
(very) long document with 1 million characters, and you are changing 1
pixel or character at a time. To hunt down that 1 pixel or character,
the computer needs to make one million comparisons every time.

If you - as the programmer - don't have any insight *which* pixel or
character has changed, well then that's the best you can do. But in
many real-life scenarios you *will* have an idea... after all your
changes will be coming from somewhere, maybe as keyboard input from
the user, or in the form of data from a remote server. It is unlikely
that each one of those 'change notifications' contain the *whole*
state each time: If only a single pixel is changed, then it would be
wasteful for a server to send a complete new 1MB image.

So bearing that in mind, the philosophy of conductance is to give you
all the building blocks that allow you to smartly convert your change
notifications into DOM manipulations.

Getting back to your animated list. If the only thing you want to do
is to keep appending new items to the list, then an EventStream would
be an appropriate abstraction. In relatively idiomatic conductance
code it would look like this:

@ = require(["mho:std", "mho:app"])

var Animate = @Mechanism(function (elem) {
// Force relayout
getComputedStyle(elem).height;
elem.classList.add("anim-active");
});

var Css = @CSS(`
{
display: block;
transition: all 1s;
background-color: red;
height: 0px;
overflow:hidden;
}

&.anim-active {
height: 20px;
}
`);

var AccumulatingList = stream ->
@Ul() .. @Mechanism(function(list) {
stream .. @each {
|item|
list .. @appendContent(@Li(item) .. Css .. Animate);
}
});

var List = @Emitter();

@mainContent .. @appendContent(AccumulatingList(List));

for (var i=0; i<100000; ++i) {
List.emit(i);
hold(1500);
> --
> You received this message because you are subscribed to the Google Groups
> "StratifiedJS" group.
> To unsubscribe from this group and stop receiving emails from it, send an
> email to stratifiedjs...@googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

pcxunl...@gmail.com

unread,
Aug 22, 2014, 11:32:45 PM8/22/14
to strati...@googlegroups.com
The DOM diffing done by React.js is quite fast, given the way it works. Of course you can create scenarios where it wouldn't be a good idea (like having a million DOM elements, one for each character), but for normal apps I think it works quite well.

So, what if I want to insert at the beginning, or at arbitrary indices inside the list? What about removing elements? What about updating elements? What about updating elements by mutating DOM nodes, and what about updating via replacing DOM nodes? The app I want to create needs to do all of those things. React.js can handle that, and my own custom DOM library can handle that. I want to use Conductance, but it has to be able to handle those use-cases (+ animation and other things) before I can use it. Even if I have to write my own custom widget or whatever, that's fine, as long as it works. Is it possible to handle the above use cases, without needing to use __js to hack it?

Alexander Fritze

unread,
Aug 23, 2014, 10:32:43 AM8/23/14
to strati...@googlegroups.com
On Fri, Aug 22, 2014 at 10:32 PM, <pcxunl...@gmail.com> wrote:
> The DOM diffing done by React.js is quite fast, given the way it works. Of
> course you can create scenarios where it wouldn't be a good idea (like
> having a million DOM elements, one for each character), but for normal apps
> I think it works quite well.
>
> So, what if I want to insert at the beginning, or at arbitrary indices
> inside the list? What about removing elements? What about updating elements?
> What about updating elements by mutating DOM nodes, and what about updating
> via replacing DOM nodes? The app I want to create needs to do all of those
> things. React.js can handle that, and my own custom DOM library can handle
> that. I want to use Conductance, but it has to be able to handle those
> use-cases (+ animation and other things) before I can use it. Even if I have
> to write my own custom widget or whatever, that's fine, as long as it works.
> Is it possible to handle the above use cases, without needing to use __js to
> hack it?

Yes, of course, all of these things can be done in conductance. You
could even replicate something like react's system on top of
conductance.

It might be easiest to talk about a concrete example though. Do you
have some react example? Then I can show you the corresponding
conductance code.

pcxunl...@gmail.com

unread,
Aug 23, 2014, 2:52:38 PM8/23/14
to strati...@googlegroups.com
I don't have any React code, so instead I'll show an animation module I wrote in Conductance: http://pastebin.com/WfugmyzK

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.

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.

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.

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.

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. What happens if you attach the same mechanism to multiple different elements? Does it create a new instance per element? 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?

pcxunl...@gmail.com

unread,
Aug 23, 2014, 3:00:26 PM8/23/14
to strati...@googlegroups.com, pcxunl...@gmail.com
By the way, I found it incredibly easy to change the behavior of the animation, thanks to Conductance's way of handling async.

As an example, there's two basic ways to handle moving an element from one index in an array to a different index:

1) Fade the element out, move it to the correct index, then fade it in.

2) Fade the element out, and simultaneously create a copy of the element (at the correct index) and fade it in.

Either one is very easy to do in Conductance, whereas it would be much harder in normal JS (I speak from experience).

I also liked how easy it was to wait for a CSS transition to end:

    child ..reverseAnimation
    child ..@removeNode

The above code first animates the child and then when the animation is complete, removes it. Very clear and easy to understand. No need for callbacks or anything else: Conductance's model is well suited for animation work.

By the way, I noticed this pattern in my own code...

    waitfor {
      ... // handle logic code
    } and {
      @mainContent ..@appendContent(...) { || hold() }
    }

Is that the idiomatic way to render content in an app?

Alexander Fritze

unread,
Aug 23, 2014, 3:58:53 PM8/23/14
to strati...@googlegroups.com
I've been playing around a bit with react & conductance.
Here's a direct comparison of react & conductance code for the
examples on react home page:

https://conductance.io/examples/react-comparison/

pcxunl...@gmail.com

unread,
Aug 23, 2014, 9:54:14 PM8/23/14
to strati...@googlegroups.com
Neat, it might be a good idea to put a link to that page on the examples page, and maybe write one for Angular too. The way that Conductance handles async dramatically simplifies a lot of stuff, so it makes for a great comparison.

By the way, I'm *not* saying that Conductance is bad: it's very good, and I was able to easily write the animation module using Conductance. I think that Conductance provides excellent low-to-mid-level systems for managing DOM code (like mechanisms and observables). But I also think that Conductance is missing some higher-level features that React has, and especially Conductance is missing animation support.

So I'm not proposing to change Conductance, instead I'm proposing to add new modules that will be built on the lower-level modules and provide some conveniences for common use cases. It'll still be possible to write your own custom code to handle a particular use case, but the common cases should be accounted for. Whether the high-level modules are similar to React, or are completely different, I don't mind either way, just so long as there's some decent way to deal with this stuff in the standard library.

So, to get the discussion rolling, what does a decent animation library need? At the bare minimum, it needs some way of animating a DOM node, specifying which properties to change and what duration the animation should last, and a way to wait until the animation is finished. CSS transitions provide all that, so that's a good start, but CSS transitions are very limited. But for basic use cases, transitions are fine.

Second, it's common to have a list of stuff which you want to add/remove to, with snazzy animations when adding/removing elements. This part is harder, because Conductance doesn't have a standardized way of dealing with object changes/deltas (the jsondiffpatch module is a start, but there's currently no convenient way to use it with a data structure). One approach to this is a custom data structure (like my ArrayDiff) which exposes an API that lets you manipulate the data structure, with events to be notified on changes. A second approach is like what React does. There could be a Mechanism or DOM node which will automatically diff its children and make the minimal changes. The benefit of this is that you can continue to use existing data structures like arrays. The downside is you have potentially less control and performance.

Third, it's common to want to orchestrate animations, chaining them one after the other, playing some in parallel, waiting until some group of animations finishes, etc. This is trivial with Conductance's async model, so unlike other animation libraries, not much really needs to be done about this: the built-in primitives are probably enough.

Alexander Fritze

unread,
Aug 23, 2014, 9:58:31 PM8/23/14
to strati...@googlegroups.com
On Sat, Aug 23, 2014 at 1:52 PM, <pcxunl...@gmail.com> wrote:
> I don't have any React code, so instead I'll show an animation module I
> wrote in Conductance: http://pastebin.com/WfugmyzK

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)

pcxunl...@gmail.com

unread,
Aug 23, 2014, 11:09:28 PM8/23/14
to strati...@googlegroups.com
On Saturday, August 23, 2014 3:58:31 PM UTC-10, Alexander Fritze wrote:
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:

Right, I first tried using jsondiffpatch, but I ran into some difficulties. I'm sure I could have worked them out, but I just found it easier to use the ArrayDiff stream. I might go back and write a version that uses jsondiffpatch.


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).

I see... I figured that was what it was, but the docs gave the impression that it was only EventStreams that had that problem. In fact, it seems to be a more general problem: if you have a global variable and you change it while a function is suspended, things might break due to the function relying on the global variable. This same problem can occur with promises and generators. State is tricky, especially if you have mutable state (which JavaScript has plenty of). Not even Clojure completely protects you from state problems, so it's no surprise that Conductance doesn't magically do the right thing either.


  try {
    child .. reverseAnimation;
  }
  finally {
    child .. @removeNode;
  }

Aha, I knew that each.track could mess up the state of the DOM, and I knew about retractions, but I hadn't thought about using them in that particular situation.

That sounds like a decent solution, except that I believe that will mandate one running animation *globally* for the entire list of nodes. What you really want is to have it be one running animation per *element*, where multiple animations on the same element override eachother, but you can have multiple animations on different elements. That'll be trickier to accomplish.


@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.

I think I'm a bit confused. I was under the impression that at any given time, only a single stratum will be running. So even with each.par, it'll still be running one chunk of code at a time, so the "double remove" situation won't ever come up. Is my thinking wrong?


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.

Yes, that's not a bad idea: React does that. I've also thought about using things like linked lists to specify the order of DOM nodes (rather than an array). That way adding/removing elements is constant time rather than linear. It also means you're no longer relying on indices for order information, but instead the relative ordering of things.


* 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 { ||... }

This might be the best solution (at least for my use case), because as soon as an animation is done, it'll proceed to the next item, so as long as the animation is (on average) shorter than the interval between changes, it should be okay. Of course, which solution is best will vary depending on what you're trying to do...


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.

Right... it is pretty difficult. So perhaps just a simple animation module to start with, something that just lets you animate a single element with a particular duration and wait until it's finished. I'll quickly whip one up.


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.

Right, but the <ul> is never removed, so the mechanism will stay there forever.

Good to know about the try/finally blocks. Does that work even if you wrap the entire mechanism in a try/finally?

    try {
      @Mechanism(function (elem) {
        ...
      })
    } finally {
      ...
    }

My guess is no, and that the only way to make it work is to put the try/finally inside the mechanism:

    @Mechanism(function (elem) {
      try {
        ...
      } finally {
        ...
      }
    })

Is my intuition correct on this? Or is Conductance so advanced that it can handle even the first case?

 
The mechanism will be executed for each element that it is attached to.

Even if the mechanism is cached using `var foo = @Mechanism(...)`? If so, why is it more efficient to use cached mechanisms?

What about attaching the same (cached) mechanism to the same element repeatedly? Is it a noop, or does it actually execute the mechanism each time?


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?

Yup, I was surprised, and I did indeed see the <surface-ui> wrapping. 


  @mainContent .. @appendContent(@Ul() .. dom_elements) 

I see... I had wondered why so many of the examples used things like @Ul() rather than `<ul></ul>`. I think that's unfortunate, and should be fixed. It's convenient to use quasis, but the auto-wrapping limits their usefulness.


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

Why does Conductance do auto-wrapping? What situations is it trying to solve?

pcxunl...@gmail.com

unread,
Aug 25, 2014, 12:12:14 AM8/25/14
to strati...@googlegroups.com, pcxunl...@gmail.com
Okay, here's a small animate module I wrote:

    @ = require(["mho:std", "mho:app"])

    var animate = @Mechanism(function (elem) {
      try {
        console.log("STARTING")

        elem.classList.add("animation-start")
        elem.classList.add("animation-end")
        // TODO is this idiomatic?
        hold(0)
        // Force browser relayout
        getComputedStyle(elem).left
        elem.classList.remove("animation-start")

        // TODO does waiting forever like this have any kind of performance impact?
        hold()

      } retract {
        console.log("ENDING")

        elem.classList.add("animation-start")
        elem.classList.add("animation-end")
        // TODO is this idiomatic?
        hold(0)
        // Force browser relayout
        //getComputedStyle(elem).left
        elem.classList.remove("animation-end")

        // This doesn't work: even though it waits forever, the element still gets removed
        hold()
      }
    })

    var css = @CSS(`
      {
        transition-property: background-color;
        transition-duration: 2s;
        transition-timing-function: ease-in-out;
      }

      &.animation-start {
        background-color: red;
      }
    `)

    var dom_elements = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] ..@map(function (x) {
      return @Element("div", x) ..css ..animate
    })

    @mainContent ..@appendContent(@Element("div", dom_elements), function () {
      hold(3000)
    })

Animations work fine when an element is added to the DOM, but do *not* work when the element is removed. To be more specific, Conductance removes the DOM node regardless of what happens in the try/retract block: it does *not* wait for the retract block to finish before removing the DOM node.

Is there a particular reason for this? Can it be fixed? If not, can a new API be added that lets a mechanism be notified when a DOM node is removed (and then delay the removal)?

Alexander Fritze

unread,
Aug 25, 2014, 12:46:52 AM8/25/14
to strati...@googlegroups.com
On Sun, Aug 24, 2014 at 11:12 PM, <pcxunl...@gmail.com> wrote:
[...]
> Animations work fine when an element is added to the DOM, but do *not* work
> when the element is removed. To be more specific, Conductance removes the
> DOM node regardless of what happens in the try/retract block: it does *not*
> wait for the retract block to finish before removing the DOM node.
>
> Is there a particular reason for this? Can it be fixed? If not, can a new
> API be added that lets a mechanism be notified when a DOM node is removed
> (and then delay the removal)?

It was a deliberate design decision to remove nodes from the DOM
*before* aborting associated mechanisms.
retract and finally blocks are not really a good place for doing
anything that takes time (like animations), because the caller has to
wait for the retract/finally blocks to complete before it moves on.
Blocking in retract/finally can lead to all sorts of desyncronization
scenarios.

But stay tuned - I've been playing around a bit with getting a
D3.js-like animation system working in SJS. I'll have some code to
show soon.

Alexander Fritze

unread,
Aug 25, 2014, 1:09:51 AM8/25/14
to strati...@googlegroups.com
On Sat, Aug 23, 2014 at 2:00 PM, <pcxunl...@gmail.com> wrote:
[...]
> By the way, I noticed this pattern in my own code...
>
> waitfor {
> ... // handle logic code
> } and {
> @mainContent ..@appendContent(...) { || hold() }
> }
>
> Is that the idiomatic way to render content in an app?

It's perfectly ok to do it this way, but you could also just do:

@mainContent .. @appendContent(...);
// handle logic code

Or, if you want want to have the lifetime of the appended content
bounded by the logic code:

@mainContent .. @appendContent(...) {
||
// logic code
}
// content will automatically be removed here

pcxunl...@gmail.com

unread,
Aug 25, 2014, 3:32:50 AM8/25/14
to strati...@googlegroups.com
> It was a deliberate design decision to remove nodes from the DOM 
> *before* aborting associated mechanisms. 
> retract and finally blocks are not really a good place for doing 
> anything that takes time (like animations), because the caller has to 
> wait for the retract/finally blocks to complete before it moves on. 
> Blocking in retract/finally can lead to all sorts of desyncronization 
> scenarios. 

That makes sense. It seemed hacky to me to use try/retract to handle animations.

Unfortunately, that means you can't introduce animations in a way that they seamlessly integrate with the existing system (like the removeNode function, or the third argument to the appendContent function). Instead, you would have to use custom animation-aware functions. I'd rather have a solution that works well with the existing APIs (though I'll use custom APIs if I have to).

Even if try/retract is a bad way to handle this situation, perhaps more functionality could be added to the Mechanism function:

    Mechanism(element, {
      add: function (element) {
        ...
      },
      remove: function (element) {
        ...
      }
    })

Rather than passing in a function, you pass in an options object that can have an "add" and/or "remove" property.

If you pass in a function rather than an options object...

    Mechanism(element, function (element) {
      ...
    })

...then that will be equivalent to this:

    Mechanism(element, {
      add: function (element) {
        ...
      }
    })

In other words, it would have the same behavior that it currently has, so this change is backwards compatible.

If you pass in a "remove" function, it is run before the DOM node is removed, and the DOM node will not be removed until the "remove" function is finished.

Given Conductance's current behavior, that would mean that it would run before the "add" function is retracted... I'm not sure if that behavior is sound or not.

On the other hand, if this feature was added, there wouldn't be as much need to block in try/retract, so perhaps the order of execution could be changed so that it would go "retract add" -> "run remove" -> "remove DOM node".


> But stay tuned - I've been playing around a bit with getting a 
> D3.js-like animation system working in SJS. I'll have some code to 
> show soon. 

Sounds great! I'll be waiting.
Reply all
Reply to author
Forward
0 new messages