Open justinbmeyer opened 6 years ago
parentNode
One idea might be for parentNode
to create the VM and render. I'm not sure if we can trap this, but it would allow setup like:
var myElement = document.createElement("my-element");
myElement.property = "value";
disconnectedElement.appendChild( myElement ) //<-- here is where the VM is created and the view rendered.
Cons:
can-view-target
's problem. But that can be solved other ways.parentNode
?
get parentNode(){
return this.pNode;
}
set parentNode(pNode) {
console.log("set");
this.pNode = pNode;
}
In this model, elements would render right away.
Pros:
clientWidth
will be 0), and we can more easily step in and tell people to do reads at a safe time.Cons:
In this design, elements would not render right away, but register themselves to be "activated".
var myElement = document.createElement("my-element");
myElement.property = "value";
Component.activate(); // all components have VM created and rendered.
myElement.children //-> [ ... ]
Every time stache renders or mutates the DOM, it would call activate()
.
Pros:
activate
is normally being called by stache during render, it will avoid "thrashing" in the same way that Render during Element Creation does.
Cons:
activate
method. Boo.We could provide a lifecycle hook that gets called in its own rAF after rendering occurs. This would be in its own queue that could take place in a rAF after the DOM_UI queue (or after the Mutate queue possibly).
This would be nice because it could be called ever time after its template gets updated. Something like:
Component.extend({
tagName: "hello-world",
mutatedCallback: function(){
// Reads are safe here
}
});
The side-effect problem is not limited to only a few small cases. Many view libraries work by creating DOM that never gets inserted. This is what CanJS does. Here's another that does the same thing. Creating DOM, and then later cloning + hydrating is common pattern.
Additionally, almost all (maybe all?) canjs apps do XHR requests when their ViewModel is bound, which happens in can-view-live before insertion. Here's where bitballs does. Here's where donejs-chat does.
Within the boundaries of can-view-callbacks and can-stache this isn't so bad because it's a mostly controlled environment. But once you support document.createElement
as your API to create components you have to be much more careful.
@matthewp to clarify what you are saying ... doing XHR requests is only a problem with Element creation
if people make custom elements and use them outside CanJS.
While this is a problem limited within CanJS applications. I agree that's a deal-breaker. The custom elements should work in as many use cases as possible.
This is why I think something like activate
might make the most sense; even more so because we could design these components such that if activate
isn't called and the element is inserted, it immediately complete all lifecycle hooks. This should work well for 3rd party use.
But for CanJS, or integrations that want the benefits of detached DOM creation (performance enforcement), they can simply call this method after rendering content intended for inserting in the page.
Regarding mutatedCallback
, I think if we are going to go down that road, we should solve all this more generally. The problem is that you sometimes want to write-read-write.
A bad (but most simple I can come up with) example might be keeping some absolute div (coverDiv
) over another div backgroundDiv
that's layout is changing box-sizing.
Naively, you'd want to:
backgroundDiv.style.boxSizing = "border-box";
var width = coverDiv.offsetWidth,
height = coverDiv.offsetHeight;
coverDiv.style.width = width +"px";
coverDiv.style.height = height +"px";
If you had multiple of these happening at the same time, you'd want something more like:
queues.domMutate.enqueue(function(){
backgroundDiv.style.boxSizing = "border-box";
queues.domRead.enqueue(function(){
var width = coverDiv.offsetWidth,
height = coverDiv.offsetHeight;
queues.domMutate.enqueue(function(){
coverDiv.style.width = width +"px";
coverDiv.style.height = height +"px";
})
})
})
One critical thing about the domMutate <->
domReadrelationship ... is that once we start flushing
domRead, we finish all of them before we "back up" to earlier queues, including
domMutate`.
Currently, if a task is enqueued in an earlier queue, all queues pause and start flushing tasks in the earliest queue. This wouldn't be the case here. We want all domRead
to finish, then we could back-up into domMutate
and start changing things and creating more domMutate
if necessary.
I think this discussion should be moved to can-element because can-component's automount capability has 2 implementations; custom elements and mutation observers. Since the MO method can only render after insertion the CE implementation should do the same, for consistency.
can-element won't have an MO implementation (you can use MO as a partial polyfill, though) so we have more freedom to do things differently. I'm starting to favor moving all rendering into a rAF task so we can more tightly control that queues are processed only every 16ms at a time.
@matthewp it's relevant here because of how we are going to prompt people to test can-component in #199. We need to give people a way of testing components (ideally without requiring them to actually insert the component into the page).
Regarding the rAF
... I'm uneasy about adding async until we have very solid debugging tools around our queues and very good patterns too. It's my understanding that the async queue system of ember is one of it's least liked parts.
ideally without requiring them to actually insert the component into the page
Is it really a big deal to require elements be inserted into the page in order to test components?
If you want to test things that don't require the element to be in the page, you should probably just be testing the ViewModel.
@phillipskevin we do this all the time in can-stache
. Tests are much faster because of it.
Changing the DOM and measuring the DOM should happen a two separate times. The difference in performance can be profound:
can-component
and CanJS was built with this performance problem in mind. This is why you render a template "disconnected" and then useinserted
and other events to know when the DOM is connected. It's currently very difficult to cause layout thrashing as seen in the above example.This issue is to discuss how to translate this to
customElements
which have two lifecycle hooks:new MyElement()
ordocument.createElement('my-element')
.Understanding thrashing
This article is a good start: http://kellegous.com/j/2013/01/26/layout-performance/
The way I think about it is that you want to do writes in a big batch, and then reads in a big batch.
For example, we want people to build all of their HTML, update CSS properties, etc, but not read any values like
clientWidth
.Once you begin reading values, read as many values as you'd like. Then you can return to updating the DOM.
Considerations besides performance
childNodes
of the element (maybe if custom elements have an existing light-dom and the element is updating itschildNode
)function mixin(Element) { let tag = new Element().localName; ... }
Is this the only way?can-view-target
- wants to be able to create a representation of the element without actually creating the VM and rendering the V.