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

Rendering of preserved content #1162

Closed
jvail opened this issue Sep 23, 2016 · 9 comments
Closed

Rendering of preserved content #1162

jvail opened this issue Sep 23, 2016 · 9 comments

Comments

@jvail
Copy link

jvail commented Sep 23, 2016

I have quite a hard time to understand why the RenderManager moves a node to the so called "preserved area". Could you elaborate on this feature a little bit?

The problem is that I am doing some parts of the initialization in onAfterRendering of the controller. I expect that the view is only rendered once i.e. added to the DOM. Unfortunately onAfterRendering is often called twice (e.g. when navigating back):
It is also called when a node is moved from the (hidden) "sap-ui-preserve" div to the top container if the view gets visible (upon navigation).
So far so good (? - why is it "re-rendered" if it is already in the DOM. It is just invisible)
Yet I can not find a rule when and why a node is moved to "sap-ui-preserve" and therefore later "re-rendered". Sometimes the node (the view) stays in the top container sometime s not. Is it possible to force a node NOT to be moved to "sap-ui-preserve"?

OpenUI5 version:
1.38.4

Browser/version (+device/version):
Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/53.0.2785.101 Safari/537.36

@codeworrior
Copy link
Member

Short answer:
For sap.ui.core.HTML controls, you can avoid DOM preservation, for XMLViews you currently can't.

Long version: Your observations touch several aspects of UI5 rendering. So it might be best to give an overview about UI5 rendering mechanisms before explaining the preserve mechanism:

  1. First of all, there's the default string-based rendering for all controls: when a control's state is changed via APIs (setting a property, adding / removing something to / from an aggregation), it invalidates itself. The framework collects such invalidated controls and registers them in a kind of "controls that need rendering" collection. In an asynchronous step (setTimeout), the RenderManager is called for all these controls and executes the following steps

    • call the renderer for the control to create an HTML string reflecting the current state of the control
    • remove the old DOM for the control (assuming there is any)
    • inject the new HTML at the place where the control was located before (basically using innerHTML)

    There's some additional handling for the initial rendering of controls. As no old DOM location is known initially, the RenderManager can't replace the old with the new DOM. Instead, the parent control is involved (rendered) and will render its children in the right order and layout.

    To allow controls to react on the creation / destruction of their DOM, the hooks onBeforeRendering ("string rendering is executed, any old DOM might be removed soon, so cleanup any DOM related stuff like event handler registration etc.") and onAfterRendering ("new DOM has been created, please adjust it if needed, e.g. register your event handlers") are available. For Views, the calls to those hooks are propagated to the controller to give views similar capabilities than controls. The hooks are called for each rendering, that's why they might be called multiple times for the same control / view.

  2. To avoid larger DOM trees being replaced when only small updates to the DOM are needed, many controls implement DOM patching in their mutator methods. When their state is change via mutator APIs and when they have been rendered before (have DOM), then they update their DOM synchronously according to the API call. This DOM patching is optional and done by the controls alone - the framework doesn't know about it. For that reason, no generic hooks are called.

  3. A few controls (esp. sap.ui.core.HTML, sap.ui.core.mvc.XMLView) allow to embed pure HTML. The HTML control is often used to integrate some 3rdparty, DOM based (rendering) libraries into UI5. Often, the HTML is heavily modified in onAfterRendering (or some other, later point in time) and re-doing the DOM modifications in each UI5 re-rendering might be too expensive or otherwise hard to achieve with those libraries. Therefore, the HTML control introduces the concept of DOM preservation:

    • Instead of removing the DOM during re-rendering, it is moved to some safe place (sap-ui-preserve area).
    • During string rendering, only some easy-to-find dummy is rendered
    • After rendering, this dummy is replaced with the preserved DOM.

    The first step and the 2nd and 3rd step can happen at different points in time. Means: an HTML control can be removed out of the control tree and its current DOM will be preserved. Several events later, the control can be moved back into the control tree, maybe at a different location and the preserved DOM will be restored in the new location (the mechanism is designed to work as long as the page lives, the preserved DOM is not stored in local storage or so). The DOM is only destroyed when the control is destroyed.

    Besides the handling of the DOM, an HTML control participate in the rendering like any other control. So onBeforeRendering and onAfterRendering are called as usual, although the DOM did not change. This makes sense, as the work of onAfterRendering might not only depend on the control's DOM but also on the location where it is embedded (e.g. when dealing with layouting, available space etc.). The HTML control provides an event afterRendering which even informs about whether this was the first or a later rendering.

    The XMLView also introduced the capability to add pure HTML around and inside UI5 controls (using the xhtml namespace). Therefore it participates in DOM preservation but in contrast to the HTML control, it can't be switched off for the XMLView (which I have to admit is a shortcoming).

You might ask (you indeed did), why DOM preservation is necessary at all. Why not leave the DOM alone, not touching it? The problem is with the first rendering variant, string based rendering. When some parent of the HTML control is modified and doesn't implement DOM patching, then the whole control tree starting with that parent will be re-rendered as a string and the old DOM will be lost, also for the HTML control.

What to do now?

Depending on what you want to achieve you might just accept the existing behavior (hopefully after getting a better understanding of it now), or you might move to the HTML control (as it allows you to use pure HTML but without preservation) or you might consider to write your own control (should be necessary only for very special use cases, but then please without DOM preservation, it's not a public feature for control development).

Last but not least: this is an explanation of the current state of the rendering. For sure there are other ways of implementing it. There've been experiments with a general DOM patching ( 26309b5 ) using the current renderers. And some other experiments with the use of virtual DOM. However, the huge amount of existing renderer code makes the introduction of new mechanisms quite challenging.

Well, I obviously violated your "a little bit" constraint a little bit, but hope this nevertheless helps to understand why DOM nodes might be moved around.

@jvail
Copy link
Author

jvail commented Sep 26, 2016

@codeworrior Many thanks for this thorough description.

@dfenerski
Copy link
Contributor

dfenerski commented Aug 12, 2020

<div data-sap-ui-preserve="__html0-container-2" id="__html0-container-2">...</div>
Is there a way to prevent changes to the value of the data-sap-ui-preserve, because in an aggregation binding this results in loss of preserved content: control bound to data-sap-ui-preserve='...3' will be bound after rerendering(deletion of previous element of the aggregation for example) now to data-sap-ui-preserve='...2' which is big error(html content is basically lost)
Is there a way around this , am i doing something wrong?

@codeworrior
Copy link
Member

codeworrior commented Aug 12, 2020

I think you have to explain your scenario a bit more. Is the deletion of the previous item in the aggregation done on model level and the list binding updates the content aggregation of your chart control? Then I could imagine that updateAggregation changes the relationship between ID and content.

But if you just remove an item via aggregation API, IDs should not change.

@codeworrior
Copy link
Member

codeworrior commented Aug 12, 2020

I see. I fear, the aggregation binding's standard behavior doesn't work together with the HTML control's constraint reg. the ID / content ID.

There's no such mechanism to decouple data-sap-ui-preserve and ID.

Is the model>idProp stable over time for the 'same' chart? If the aggregation would create an HTML control with that ID, this should work out. But the standard updateAggregation can't do that. Maybe using a factory function could help.

A factory function gets the binding context as 2nd parameter and could determine the idProp before creating the HTML control with the expected ID. Well, but that's not straight forward.

@dfenerski
Copy link
Contributor

dfenerski commented Aug 12, 2020

The aggregation API is good enough for me. I tried doing it with factory, but the result is the same (the content of the core.HTML is the initially provided string, not the preserved DOM)
factoryFN:

    fnFactory: function (sId, oContext) {
        const oControl = this.byId("templateItem").clone(sId);
        try {
          oControl.setContent(
            new sap.ui.core.HTML(oContext.getObject("globalId"), {
              content: "div id='{local>globalId}' /div",
            })
          );
        } catch (e) {
          console.log(e);
        }
        return oControl;
      },

core.HTML DOM node after follow-up re-rendering:
image

@codeworrior
Copy link
Member

Well, as I said, aggregation binding and HTML control don't work well together:

  • HTML control's preferDOM mechanism is designed to keep the DOM alive as long as the control instance exists AND as long as the content property is not changed
  • when the content property is changed (e.g. via data binding), this will replace the existing DOM with the new value of the content property. If DOM preservation is the goal, the content property should only be set once and should not be bound.
  • the standard update mechanism for bound control aggregations first destroys the old content and then creates it anew- There are optimised update mechanism that try to keep the same object for the same data instance, but model, control and application have to cooperate for this (check the topic Extended Change Detection if interested)
  • with the standard implementation, the HTML controls will be destroyed and their DOM with them

To demonstrate these problems' I've created a JSBin: https://jsbin.com/sewekisono/edit?html,output
It implements a modified update strategy that keeps the existing DOM controls alive and reuse them based on the ID in the model. For a productive solution, the cleanup would have to be addressed as well (HTML controls that are no longer needed after deletion need to be deleted explicitly, the JSBin keeps them forever).

@dfenerski
Copy link
Contributor

dfenerski commented Aug 13, 2020

Thank you.

Whilst implementing your solution I learned that 0..1 aggregations do not receive the named methods, so I had to
image
instead of
image

Nevertheless it worked. I still have one question, if you don't mind.
Is it ok to use sap.ui.getCore().applyChanges(); in prod? Because without it the async rendering step takes just a bit too long and my charts 'flicker' in the re-rendering process, which i was hoping to avoid in the first place.

@codeworrior
Copy link
Member

I used sap.ui.getCore().applyChanges() in my JSBin to be sure that rendering already happened before I apply the dynamic HTML.

Using it in a productive app is in general not recommended as sync rendering is a kind of de-optimization. Async rendering collects updates and handles them at once, e.g. avoiding multiple rendering of the same control. Sync rendering - when triggered by multiple components not knowing each other - might cause such redundant rendering or other side effects.

But when you see benefit in your scenario and do this only after deletion of a chart, you can IMO use it, it is a public API.

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

No branches or pull requests

4 participants