AFASSoftware / maquette

Pure and simple virtual DOM library

Home Page:https://maquettejs.org

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

createDom produces intermediate DOM states

devpaul opened this issue · comments

commented

I am trying to combine Maquette and A-Frame to render a WebVR scene and am running into an issue with intermediate DOM states.

When Maquette creates a new DOM node it attaches the new DOM node to the parent node before adding attributes and properties: https://github.com/AFASSoftware/maquette/blob/master/src/maquette.ts#L789-L795. This causes an issue with A-Frame which uses Custom Elements that watch for and react to changes in the DOM. When Maquette attaches the new DOM node to the parent, it is incomplete and thus creates an intermediate and invalid DOM.

These intermediate DOM states are problematic because they're not rendering what the developer expects and they prematurely trigger observers and proxies.

As a comparison, A-Frame resolves these issues w/ React by explicitly updating attributes in componentDidUpdate(): https://github.com/aframevr/aframe-react/blob/master/src/index.js#L91. But I don't think Maquette offers a similar mechanism to update the DOM before it's attached.

I think the most straight-forward solution would be to call initPropertiesAndChildren() on domNode right before it's added to the DOM.

commented

After talking w/ some colleagues it seems like moving initPropertiesAndChildren() can cause issues since attaching to the DOM can change properties and attributes.

We still need a solution for Custom Elements, observers, and proxies that depend on a complete DOM node being added so it can analyze and maintain state. Maybe we can add an event like beforeAttach that fires immediately before the domNode is added to the DOM. This would give us a chance to make these modifications to accommodate the above cases.

Hello, Thanks for trying maquette to do cool new things with! The whole idea of attaching nodes to the DOM before initializing them is to give external libraries/widgets like google-maps, recaptcha and ckeditor an attached DOM node, otherwise they will not work.

It seems that A-Frame uses a kind of observer mechanism to determine when to start parsing the DOM. Maquette provides the afterCreate callback that would be the right moment to start. Do you have a repository or an online example that I can look at and debug?

commented

Sure, I have a repo up here: https://github.com/devpaul/derpymon

It would be nice if we had control over when A-Frame parses the DOM and could tell it to wait for afterCreate. Unfortunately, I don't think we can do that. I think this is a common pattern for custom elements.

For maquette, I was thinking that adding an event that is triggered before a node is inserted or attached might provide a solution.

beforeAttach?(parentNode: Node, element: Element, beforeNode?: Node): boolean;

In the above event, if it exists and if it returns false then the node wouldn't be attached to the DOM. This would allow anyone to cache the node and return false and wait for initPropertiesAndChildren() to run and the node could be attached later in the afterCreate event.

It looks like maquette causes the attachedCallback of custom elements to fire too soon for a-frame. This is probably not only the case for the for the root a-scene but also for all elements that are added dynamically into the scene afterwards.

One approach would be to test every Element for an 'attachedCallback' property (or check for dashes in the tagname) and if they have one, insert them into the DOM tree after initializing properties and children. But this will influence performance for people not using custom elements.

commented

I was thinking about something along those lines. On my machine I tested this by moving initPropertiesAndChildren() to come before nodes were attached to the DOM and A-Frame worked without issue.

Maybe something like this would work as more formal solution?
https://github.com/devpaul/maquette/blob/custom-elements/src/maquette.ts#L760-L775

This would allow us to handle the corner case where a complete DOM is necessary while creating the smallest impact on normal virtual-DOM approaches. In cases where we want to delay attaching a node to the DOM until after it's properties and children are initialized we could do this by creating a beforeAttach() handler and returning false and later handle afterCreate() and attach nodes then.

Let me know what you think of this solution. If it's viable, I'll build it out and test it against my A-Frame project, add tests and submit a PR.

Before making changes, lets have a look at the big picture here.

Maquette provides the afterCreate (and afterUpdate) callback for components/widgets that are not based on custom elements. AfterCreate guarantees that the Element's properties and children have been initialized and the DOM node is (as far as maquette can tell) connected to the DOM, so the component/widget can determine the size and position of the element. (This does cause an extra reflow, thus hurting performance, but it works).

Now lets enter the custom-components era (whenever that may be). Components based on the customComponents spec have a nice connectedCallback that will inform the custom component when it is added to the DOM. Determining size and position is guaranteed to succeed at this point. When we create a new tree containing multiple custom elements, it would be better if the tree was first created fully and attached/connected to the DOM at once. Determining size or position would not cause unneeded reflows (unless multiple trees were added at the same time).

So for custom elements in general it would be better (and for A-frame even required) if maquette would add elements to the document as late as possible. I am assuming that these custom elements will not use afterCreate to initialize pre-custom-component widgets which rely on the DOM being attached/connected, because they should use their connectedCallback capabilities to initialize these.

Adding a extra callback which returns a boolean is a last resort, because it makes the life of consumers of maquette harder. If maquette could do the logical/right thing automatically that would make things a lot easier/simpler.

commented

Maybe I'm getting ahead of myself. I was looking at the problem as "what could we add to Maquette that would provide a general solution for any of these situations in the future". I have some concerns implementing a specific solution for a feature that's still in draft, but I'd be happy to consider it.

From your previous responses it sounds like you recommend identifying custom elements based on their element name and waiting to attach them to the DOM until after initPropertiesAndChildren() run. Does this sound about right? Should we also add an option to the vnode that allows people to flag nodes that should only be added after their elements and properties are added?

I am not sure what the benefits would be for non-custom-element vnodes to delay being added to the DOM. Without a connectedCallback this would not help anyone and the afterCreate should just work fine, or do you maybe have a specific usecase in mind?

There is one more concern about A-Frame. I like the objectives of the library a lot, but it seems like they have chosen the DOM to be their API. This leads to a lot of overhead when using maquette or React, because the following steps are taken:

  1. The data for the view is represented as plain JS objects
  2. The data is used by the renderMaquette function to produce VNodes
  3. The VNodes are converted to real DOM nodes (custom elements)
  4. The changes to these custom elements are observed and synchronized to three.js objects by A-Frame (not sure here)
  5. A-Frame waits for the next animationFrame to occur
  6. A-Frame instructs three.js to render the objects
  7. Three.js draws each object using webGL

This not only causes 3 extra representations of the data (vnode, DOM Node, three.js objects) to remain synchronized, it also causes an extra delay of 1 animationFrame :-(.

I really like to see what it is like to create a 3d scene based on plain data and a pure render function. Coupling maquette with A-Frame would certainly be a good prototype for this, but I expect that a render function that draws directly to webgl would be the most performant.

I hope that my analysis is not stopping you from taking this experiment further.

I think that we should not burden maquette (size and performance) with my suggested approach for custom elements just yet. I think it is wise to wait for more people and more usecases. Please use your modified version of maquette for coupling with A-Frame and I am curious to see the results!

commented

There is one more concern about A-Frame. I like the objectives of the library a lot, but it seems like they have chosen the DOM to be their API. This leads to a lot of overhead when using maquette or React, because the following steps are taken

I understand your concerns about A-Frame being less efficient than other options. I think that efficiency can be important, but often a larger barrier can be reaching a critical mass of developers to make a technology viable. By using the DOM it simplifies using and sharing components by leveraging the lingua franca of the web.

As a contrast, ReactVR offers a solution that doesn't require custom elements. This is OK for React developers, but for other frameworks like Dojo, Vue.js, or Angular, which already know how to work w/ the DOM it adds an additional layer of complexity or forces a framework decision. If I were to make a framework with Maquette that rendered directly to a canvas it would be more efficient at the cost of less flexibility.

I hope that my analysis is not stopping you from taking this experiment further.

I'm happy to continue working on this as a demo and come back to this issue once we have some data points.

So far we've identified two possible solutions

  1. A flag that instructs Maquette to delay attaching to the DOM until after attributes have been added. This would add some complexity for the user to handle a corner case, but not affect size or performance.
  2. Add a parser that reads element names and tries to do the right thing by automatically delaying attaching the DOM for custom elements. This doesn't add complexity for the end user, but does add complexity to Maquette and decreases performance.

I'm still inclined to pursue the former solution because it has the least impact on Maquette and only a small amount on a user that is already using an advanced technology. I'll update this ticket when I have a working demo.

P.S. If you haven't seen it yet, A-Frame inspector is a great tool to play around w/ A-Frame and export a scene.

commented

I've created a working demo at https://devpaul.github.io/derpymon/ (source: https://github.com/devpaul/derpymon).

I modified maquette (devpaul@fd16bde) by adding a delayAttach property that instructs Maquette to wait to attach a node to the DOM until after attributes and properties have been added. It's only necessary in a couple areas like A-Frame assets when a custom element has required attributes. Overall rendering looks good on desktop and mobile.

A flag was the most straight-forward way to solve this particular issue, but I also added a beforeAttach event that lets you cancel attaching to the DOM as an alternative. Ultimately we should find a solution that works best for Maquette.

I looked at the example and the code. I think it looks good! I am still not convinced that there are usecases that would not be possible if element names which contain a dash were added to the DOM later.

commented

That's fair. It would be easy to introduce more functionality like the delayAttach property or a beforeAttach event if it becomes necessary. I've submitted a PR w/ your suggested approach!

Hi Paul, we want to address this issue in maquette 3.0.
We can address this issue further at #118