Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Explanation of tradeoffs compared to Reagent / Om, etc #44

Closed
GetContented opened this issue Jun 1, 2015 · 34 comments
Closed

Explanation of tradeoffs compared to Reagent / Om, etc #44

GetContented opened this issue Jun 1, 2015 · 34 comments

Comments

@GetContented
Copy link

More people will choose Freactive if they understand the tradeoffs of design choices and advantages / disadvantages of each compared to reagent, Om, etc.

@aaronc
Copy link
Owner

aaronc commented Jun 1, 2015

Hi Julian, yes this is probably something that will be useful, but I'm not planning to do much "promotion" until 0.2.0 is released - hopefully this summer.

Briefly, the pros and cons I would list are:

For freactive.dom:
Pros:

  • localized updates allows for high performance (esp. for attribute updates and therefore animation)
  • atom/cursor/rx implementation agnostic - 3rd party implementations like javelin shoud work out of the box
  • supports any DOM element (including Polymer)
  • supports custom DOM events
  • extensible via plugins
  • to be implemented: efficient management of keyed sequences of elements

Cons:

  • implementation not as mature as React (although there are plans for a React backend to support React Native)
  • no synthetic events (whether this is pro or con is I guess a matter of opinion)

For freactive.core:
Pros:

  • powerful atom/cursor implementation (the new one that isn't documented yet) that localizes change notifications and supports any backend storage
  • reactive computations with configurable laziness

Not sure what the cons are...

@GetContented
Copy link
Author

Nice work. Should pop this on the front page IMHO.

Would freactive allow one to do iframe content-diffing between changing srcDoc text? (in other words, when my ratom that contains the srcDoc of my iFrame changes, it currently will rerender the entire thing, causing masses of img reloads... but a DOM-diffing (freactive.dom ?) would allow this not to be the case.)

@GetContented
Copy link
Author

Another question - does freactive do atom-change updates automatically? Reagent is pretty fantastic at this, but it can get bogged down sometimes.

@aaronc
Copy link
Owner

aaronc commented Jun 1, 2015

Regarding srcDoc diffing, are you talking about diffing HTML text?

@GetContented
Copy link
Author

@aaronc Kind of, but not really. I'm talking about loading an iFrame's srcdoc initially with HTML source text, the browser obviously then renders that into the actual DOM, but then subsequent updates being done via comparing the iFrame's DOM with the modified to-be-rendered VDOM (VDOM diffing). Does that make any sense to you?

@GetContented
Copy link
Author

The reason I bring this up is that React and therefore Om & Reagent all have a problem where everything has to be wrapped... and this precludes doing things like this, amongst other things like "components" that return just text nodes, but if we use the actual DOM in freactive, it's possible to have a VDOM in an iFrame and do partial updates using "ordinary HTML" (such as my current project just happens to need).

@aaronc
Copy link
Owner

aaronc commented Jun 1, 2015

Okay, I think I understand... mostly. One of the long-term goals is to support server-side rendering in pure Clojure (no js-engine required), by interpreting freactive's syntax as a dialect of hiccup. Now there are some difficulties here.

First of all, if we show the user the pre-rendered HTML, no events will be wired up. One pretty straight-forward solution is to overlay the UI with a translucent loading indicator until the virtual DOM gets wired up.

To re-use the pre-rendered elements when freactive mounts is a little more difficult. We could diff against actual DOM elements via tagName, namespaceURI and attributes but IE support for attributes is somewhat flaky. For events, getEventListeners is not supported cross-browser. So, I'm thinking the way to support this would be via some "unsafe-reuse-nodes" flag which indicates that you have created those elements and even if they can't be diffed fully, as long tagName and namespaceURI match you "know" that it's safe.

Does that make sense? Would this address your issue?

@aaronc
Copy link
Owner

aaronc commented Jun 1, 2015

What do you mean by "do atom-change updates automatically"? freactive will automatically bind to anything that implements IDeref and IWatchable - i.e. an atom, cursor, rx, etc...

@GetContented
Copy link
Author

@aaronc Cool - that's what I thought (freactive will automatically...) great :) just wasn't sure, because I was reading some issue yesterday that made tme think freactive had different design goals in mind (made little sense, but all good). This was the issue: #16

@aaronc
Copy link
Owner

aaronc commented Jun 1, 2015

Yeah, right now it just doesn't support [my-component-fn ...] which imho is sort of syntax sugar. You need to explicitly write (rx ...) to create a "component". But there are reasons for this - freactive is agnostic to the data type you're binding with - you could create a "component" with javelin's defc= for all I care. Also freactive encourages you do bind more locally - directly to attributes if you like - why diff a whole node if all you want to change is one attribute. But like I said, I'll probably support reagent's syntax to make it easy for reagent users to transition...

@GetContented
Copy link
Author

re: "One of the long-term goals"

Sorry I think I've led you up the garden path a bit. I just got excited with the per-element DOM updating idea, but I don't think this is really something beneficial to either of us. The below is what I was going to say, but I don't think it'll work. I'm being far too ambitious. :)

...

Actually I have a different thing that I need to do in mind... my app is an HTML editor of sorts (editing happens in fragments of HTML), and as users make their changes, there's also a preview of those changes: an iFrame that is driven by a render function, which simply glues the correct model state together and is bound to the iFrame's srcDoc attr. On every keypress, the entire iFrame is re-rendered, which means all images are reloaded. I've experimented with doing delayed updates using a kind of slow debouncing algorithm, and even experimented with creating my own custom iFrame component that interpreted the HTML as React, in order to use the diffing. Trouble is, the HTML source (being edited) isn't under my program's direct control, it's under the control of a different user of the app. I hope this makes sense.

My thought was because freagent is able to wrap ANY HTML element, it would be able to do efficient diffing within the iFrame... only changing the (usually text) portions that a user is editing, but, yes, if they were editing portions of javascript or CSS, that would cause some pretty serious issues... especially if the thing they were editing was itself a full MVC JS application. Not really sure how to get around this. Perhaps I'll just look to more efficient caching as a better way to do this.

@aaronc
Copy link
Owner

aaronc commented Jun 1, 2015

If you stick with browsers that support Element.attributes and
getEventListeners you could probably do the diffing correctly.

On Mon, Jun 1, 2015 at 12:48 PM, JulianLeviston [email protected]
wrote:

re: "One of the long-term goals"

Sorry I think I've led you up the garden path a bit. I just got excited
with the per-element DOM updating idea, but I don't think this is really
something beneficial to either of us. The below is what I was going to say,
but I don't think it'll work. I'm being far too ambitious. :)

...

Actually I have a different thing that I need to do in mind... my app is
an HTML editor of sorts (editing happens in fragments of HTML), and as
users make their changes, there's also a preview of those changes: an
iFrame that is driven by a render function, which simply glues the correct
model state together and is bound to the iFrame's srcDoc attr. On every
keypress, the entire iFrame is re-rendered, which means all images are
reloaded. I've experimented with doing delayed updates using a kind of slow
debouncing algorithm, and even experimented with creating my own custom
iFrame component that interpreted the HTML as React, in order to use the
diffing. Trouble is, the HTML source (being edited) isn't under my
program's direct control, it's under the control of a different user of the
app. I hope this makes sense.

My thought was because freagent is able to wrap ANY HTML element, it would
be able to do efficient diffing within the iFrame... only changing the
(usually text) portions that a user is editing, but, yes, if they were
editing portions of javascript or CSS, that would cause some pretty serious
issues... especially if the thing they were editing was itself a full MVC
JS application. Not really sure how to get around this. Perhaps I'll just
look to more efficient caching as a better way to do this.


Reply to this email directly or view it on GitHub
#44 (comment).

@GetContented
Copy link
Author

re: "Yeah, right now it just doesn't" - excellent. This project is great! Love where it's going. Interestingly, this project (and reagent) consistently highlight some of the issues there seem to be with Clojure(script) in terms of them being not lazy enough, or data-driven enough, and also with the way the browser functions right now. (live rewriting of an application on the fly from wtihin itself anyone? figwheel FTW, but compiler needs to be within the browser). Anyway I'm rambling. Sorry!

As to my other issue, perhaps hipo can me help with its "live DOM node reconciliation" https://github.com/jeluard/hipo/ (ie VDOM diffing). Obviously, if the DOM node that is being manipulated is a script tag or somesuch, the entire page/frame would need to be reloaded. Ah I'll see how I go :)

I'll close this now. Thanks very much for your time. This stuff is great.

@sparkofreason
Copy link

Is there any advantage to having DOM diffing at all? Unwanted re-renders can be avoided by minimizing the scope of rx-expressions or using non-reactively, right? You currently have to do it by hand, but it doesn't seem to far-fetched that a macro could interrogate state derefs in the view function and figure out how to wrap things in these two macros.

The biggest advantage we've found with freactive is that bindings maintain the references to DOM nodes. This opens up the possibilities in working with 3rd-party libs without worrying about them playing nice with virtual DOM. We had a lot of headaches getting some Polymer elements to work correctly within virtual DOM. paper-dialog moves elements in the DOM to get "modal" overlay behavior, and the diff got confused. And we never did get the ACE editor to work correctly with vdom, but it was straightforward with freactive.

@aaronc
Copy link
Owner

aaronc commented Jun 2, 2015

In freactive's design diffing has always seemed to be the least important piece to me - it's what happens when all else fails to prevent unnecessary changes. I'm not sure there's no advantage to at all, but in an otherwise well-designed system you're right that diffing really wouldn't do that much. If a diffing algorithm is maintained, there would be a way to turn it off and I definitely wouldn't want it to interfere with using Polymer. In terms of features, I see management of keyed sequences of items being a much higher priority which would also eliminate a lot of use cases for diffing. Is this the virtual DOM lib that you're referring to: https://github.com/Matt-Esch/virtual-dom?

I'm curious, where are the cases that you need to use non-reactively frequently? Yes, ideally this would not need to be done by hand.

@sparkofreason
Copy link

I guess there might be cases where diff could be useful, but I have yet to run into them. I can contrive some examples, but the our common use-cases so far would be pretty well-served by some basic static analysis - just doing this by hand now. Part of the reason diff seems marginal might be that we're using HTML custom elements, and have them wired up so that they manage their own state and DOM subtree internally. The more likely case where we would encounter large pieces of DOM with small differences is going to be keyed sequences.

We were using that https://github.com/Matt-Esch/virtual-dom, via our first attempt at web components: https://github.com/allgress/web-components. That turned out to be kind of a mess, trying to add features to deal with aspects of working with other "uncooperative" libraries, including a half-baked version of Elm-style FRP. Some of the issues we saw might have been handled using virtual-dom's "widgets" and/or other methods, but it felt like we had too many different fiddly knobs to turn already. Our second attempt is https://github.com/allgress/cereus, leveraging freactive. The library itself is considerably smaller, and the consensus on our team is that the resulting code is much cleaner.

Most of our use of non-reactively was to prevent parent rx-expressions from registering dependencies of child rx-expressions. The latest changes to freactive have fixed that, so that use-case is gone. I also use non-reactively to avoid redundant state updates or possible race conditions on elements handling user input. For example, to make a validating text input we need to read the current text on each keypress, but I don't want to update the value attribute every time a key is pressed, as it's redundant at best. Finally, I think Cereus should probably wrap the custom-element lifecycle functions in non-reactively, to ensure that any internal deref's are not picked up by parent element rx-expressions. Haven't actually run into problems with this yet, but that may be due to the current aggressive use of non-reactively from the previous parent/child dependency issue.

@sparkofreason
Copy link

And using freactive and Polymer 1.0, I can't reproduce the race condition issue I saw before with text inputs, so that use of non-reactively is gone as well.

@aaronc
Copy link
Owner

aaronc commented Jun 3, 2015

Cool, glad that use of non-reactively is not needed as much. Like I said, ideally you would only need to use it in very special cases... its need in ordinary use cases I would probably treat as a bug.

@aaronc
Copy link
Owner

aaronc commented Jun 3, 2015

Btw, I just pushed a new DOM implementation to freactive's develop branch (and clojars). It builds on what was there but is overall significantly different and in my mind simpler... in a good way. Probably the biggest new thing is support for managing keyed sequences of elements (with any level of nesting)! There is no diffing support although it could be added - but again low priority. Overall, hopefully everything should work as before. I wonder if you could possibly test this against your Polymer code to make sure there are no glitches?

@sparkofreason
Copy link

Immediately hit this:

Uncaught #ExceptionInfo{:message "Can't safely do manual DOM manipulation within the managed element tree. Please do manual DOM manipulation only on top-level managed elements.", :data {:managed-element #<[object HTMLElement]>}}

I'll put together an isolated repro soon, but I think what's happening is I've got a parent custom element creating a child custom element. That occurs in the context of dom/mount!. The child also calls dom/mount! to render it's subtree, guessing somehow these nested calls are an issue.

@aaronc
Copy link
Owner

aaronc commented Jun 3, 2015

Yes, previously it would have been possible to have nested sub-trees that
could be manipulated manually, but in this implementation it really isn't.
The reason for this is that element sub-sequences are now allowed and this
requires element states to know where things are. Manual manipulation would
break that. However... a method could be added to DOMElement to do
append/insert safely. But I'm wondering what is the use case for nested
mounts?

On Wed, Jun 3, 2015 at 6:00 PM, Dave Dixon [email protected] wrote:

Immediately hit this:

Uncaught #ExceptionInfo{:message "Can't safely do manual DOM manipulation
within the managed element tree. Please do manual DOM manipulation only on
top-level managed elements.", :data {:managed-element #<[object
HTMLElement]>}}

I'll put together an isolated repro soon, but I think what's happening is
I've got a parent custom element creating a child custom element. That
occurs in the context of dom/mount!. The child also calls dom/mount! to
render it's subtree, guessing somehow these nested calls are an issue.


Reply to this email directly or view it on GitHub
#44 (comment).

@sparkofreason
Copy link

I'm not sure it really is necessary to nest dom/mount!. The first implementation of Cereus used it to mount to the custom element shadow DOM. But I've not made shadow DOM optional - by default the view mounts to the custom element itself. I would still like Cereus to support the use-case of using custom elements directly from HTML. So I think I need to mount in this case, but if I can get some info about the context where the element is being attached, maybe I can conditionally call mount! when the parent is not managed by freactive. Is there a way to do that?

The other thing I'll try now is to just defer the call to dom/mount! with js/setTimeout.

@aaronc
Copy link
Owner

aaronc commented Jun 3, 2015

I just added a managed? fn to freactive.dom. Nested manipulation may be
possible but it wouldn't use mount! and is somewhat complex (due to element
seqs) - ideally I'd prefer to avoid it.

On Wed, Jun 3, 2015 at 6:32 PM, Dave Dixon [email protected] wrote:

I'm not sure it really is necessary to nest dom/mount!. The first
implementation of Cereus used it to mount to the custom element shadow DOM.
But I've not made shadow DOM optional - by default the view mounts to the
custom element itself. I would still like Cereus to support the use-case of
using custom elements directly from HTML. So I think I need to mount in
this case, but if I can get some info about the context where the element
is being attached, maybe I can conditionally call mount! when the parent is
not managed by freactive. Is there a way to do that?

The other thing I'll try now is to just defer the call to dom/mount! with
js/setTimeout.


Reply to this email directly or view it on GitHub
#44 (comment).

@sparkofreason
Copy link

It isn't really "nested" manipulation per se. Rather I want to treat the sub-tree of a custom element in isolation, updated by the state atom owned by that element. The state in turn is updated via event-handlers from internal elements, or attribute changes from the outside world.

My current solution when attaching a custom element is to create a "root" via document.createDocumentFragment(), call dom/mount! on that to render the view, and then stuff the document fragment into the custom element instance. And all of that happens in setTimeout, with the thought of avoiding cross-talk between dom/mount! calls. Took a quick look at the code, and didn't see any obvious issues with this approach, but would be interested in your thoughts. So far seems to work fine.

@aaronc
Copy link
Owner

aaronc commented Jun 4, 2015

Well there's nothing to prevent you from using different atoms in different
sub-trees like in om. I still don't understand what the issue with doing a
single mount! would be.

By nesting mounts you risk that the child mount won't get disposed of
properly when its parent gets disposed. That said, if you pass an unmanaged
node to mount! in the current impl, that node is wrapped with the proper
on-dispose callback. For the use case where someone may want another
renderer as a child of freactive, I added wrap-unmanaged-node which takes
an on-dispose callback.

On Wed, Jun 3, 2015 at 9:23 PM, Dave Dixon [email protected] wrote:

It isn't really "nested" manipulation per se. Rather I want to treat the
sub-tree of a custom element in isolation, updated by the state atom owned
by that element. The state in turn is updated via event-handlers from
internal elements, or attribute changes from the outside world.

My current solution when attaching a custom element is to create a "root"
via document.createDocumentFragment(), call dom/mount! on that to render
the view, and then stuff the document fragment into the custom element
instance. And all of that happens in setTimeout, with the thought of
avoiding cross-talk between dom/mount! calls. Took a quick look at the
code, and didn't see any obvious issues with this approach, but would be
interested in your thoughts. So far seems to work fine.


Reply to this email directly or view it on GitHub
#44 (comment).

@sparkofreason
Copy link

That's good info, thank you. I believe you're correct, and my approach would not have allowed the mount to be properly disposed. Web components have a standard set of lifecycle methods, so I think the right approach is to wire up the disposedCallback function to handle this.

I don't know how you would handle shadow-DOM with a single mount. In full implementation of the (draft) web components standard like Chrome has, the shadow DOM is essentially hidden from the rest of the DOM, and is where you do the actual rendering for the component. The component can have "light DOM", which is what the user specifies as content, but the light DOM is really input for rendering. Example:

<paper-button>
  <span>Button</span>
</paper-button>

The <span> as shown isn't rendered. Instead it gets moved into the shadow DOM and stuffed inside <paper-material> to give it a particular look (Polymer 1.0 has an option to efficiently emulate some aspects of the shadow DOM for browsers without a native implementation, hence the need for the funky DOM API's). So each shadow DOM needs to be a separate mount.

@aaronc
Copy link
Owner

aaronc commented Jun 4, 2015

So there's nothing wrong with doing multiple mounts on a single page - nesting is the only issue and can probably be avoided with good planning. But if you need to nest you can do it - just make sure you mount! on a node that you created with document.createElement, etc. - not one created and managed by freactive. The main reason for the restriction is really that the mount point should have no other children so that freactive can fully manage its children.

@sparkofreason
Copy link

Got this working fine.

@aaronc
Copy link
Owner

aaronc commented Jun 5, 2015

Cool... There's a possibility I may loosen some restrictions later down the road, but glad you have this working how it is.

@sparkofreason
Copy link

Okay, but that's definitely in the category of "nice to have". For browsers supporting shadow DOM, I'll just mount to the shadow root (haven't tested this yet, but can't see why it wouldn't work). Everybody else just gets an unmanaged container

as a child of the managed custom element. That
serves as the mount point. The only very minor downside is that there's an extra element in the rendered DOM tree of the custom element, not a big deal.

Related question: what will happen when Polymer moves managed nodes around in the DOM? Cereus doesn't really support this yet (no use-cases in our current application), but at some point probably will need to do the same thing.

@aaronc
Copy link
Owner

aaronc commented Jun 5, 2015

How will Polymer be moving managed nodes around the DOM? freactive provides
specific interfaces for moving nodes (haven't documented this yet but the
recommended way is to use IProjectionTarget's which can manage all sorts of
reorderings including reordering of nested sequences). The tradeoff for
this support is that using standard DOM API's to move nodes will definitely
break things.

On Fri, Jun 5, 2015 at 2:21 PM, Dave Dixon [email protected] wrote:

Okay, but that's definitely in the category of "nice to have". For
browsers supporting shadow DOM, I'll just mount to the shadow root (haven't
tested this yet, but can't see why it wouldn't work). Everybody else just
gets an unmanaged container
as a child of the managed custom element. That
serves as the mount point. The only very minor downside is that there's an
extra element in the rendered DOM tree of the custom element, not a big
deal.

Related question: what will happen when Polymer moves managed nodes around
in the DOM? Cereus doesn't really support this yet (no use-cases in our
current application), but at some point probably will need to do the same
thing.


Reply to this email directly or view it on GitHub
#44 (comment).

@sparkofreason
Copy link

Polymer 1.0 seems to have done away with one scenario, where <paper-dialog> would move itself in the DOM to give modal behavior. But other Polymer elements or custom element libraries could conceivably do this, and freactive's proper handling of this was one of the main attractions for us.

For Polymer 1.0, when using "shady DOM", they move elements to emulate the light/shadow/composed DOM behavior of the web components spec. So markup like this:

<paper-button>
  <span>Submit</span>
</paper-button>

winds up as this in the DOM:

<paper-button role="button" tabindex="0" aria-disabled="false" class="x-scope paper-button-0" pressed="">
  <paper-ripple class="style-scope paper-button" animating="">
    <div id="background" class="style-scope paper-ripple" style="opacity: 0; background-color: rgb(0, 0, 0);"></div>
    <div id="waves" class="style-scope paper-ripple"><div class="wave-container style-scope paper-ripple" style="top: -20.4296875px; left: 0px; width: 82.234375px; height: 82.234375px; -webkit-transform: translate(0.2578125px, 0.3125px); transform: translate3d(0.2578125px, 0.3125px, 0px);"><div class="wave style-scope paper-ripple" style="opacity: 0.25; -webkit-transform: scale(2.58437067390754, 2.58437067390754); transform: scale3d(2.58437067390754, 2.58437067390754, 1); background-color: rgb(0, 0, 0);"></div></div></div>
  </paper-ripple>
  <paper-material animated="" class="content  style-scope paper-button x-scope paper-material-0" elevation="0">
    <span>Submit</span>
  </paper-material>
</paper-button>

@sparkofreason
Copy link

And Polymer uses either the Polymer.dom APIs (discussed in Issue #45) or the raw DOM APIs to actually manipulate the DOM.

@aaronc
Copy link
Owner

aaronc commented Jun 5, 2015

Let's continue this discussion in issue #45.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants