From 531544605414c89553e9eb4483e0eea4378a05b2 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Fri, 22 Feb 2019 13:53:45 +0000 Subject: [PATCH] Avoid UI flickering during transition from prerendered to live content --- .../src/Rendering/BrowserRenderer.ts | 19 ++++++++++++++++++- .../src/Rendering/LogicalElements.ts | 9 ++++++--- .../Browser.JS/src/Rendering/Renderer.ts | 8 -------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Components/Browser.JS/src/Rendering/BrowserRenderer.ts b/src/Components/Browser.JS/src/Rendering/BrowserRenderer.ts index 0499a45e60..3d2317668f 100644 --- a/src/Components/Browser.JS/src/Rendering/BrowserRenderer.ts +++ b/src/Components/Browser.JS/src/Rendering/BrowserRenderer.ts @@ -7,6 +7,7 @@ const selectValuePropname = '_blazorSelectValue'; const sharedTemplateElemForParsing = document.createElement('template'); const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g'); const preventDefaultEvents: { [eventType: string]: boolean } = { submit: true }; +const rootComponentsPendingFirstRender: { [componentId: number]: Element } = {}; export class BrowserRenderer { private eventDelegator: EventDelegator; @@ -19,7 +20,9 @@ export class BrowserRenderer { } public attachRootComponentToElement(componentId: number, element: Element) { - this.attachComponentToElement(componentId, toLogicalElement(element)); + // 'allowExistingContents' to keep any prerendered content until we do the first client-side render + this.attachComponentToElement(componentId, toLogicalElement(element, /* allowExistingContents */ true)); + rootComponentsPendingFirstRender[componentId] = element; } public updateComponent(batch: RenderBatch, componentId: number, edits: ArraySegment, referenceFrames: ArrayValues) { @@ -28,6 +31,13 @@ export class BrowserRenderer { throw new Error(`No element is currently associated with component ${componentId}`); } + // On the first render for each root component, clear any existing content (e.g., prerendered) + const rootElementToClear = rootComponentsPendingFirstRender[componentId]; + if (rootElementToClear) { + delete rootComponentsPendingFirstRender[componentId]; + clearElement(rootElementToClear); + } + this.applyEdits(batch, element, 0, edits, referenceFrames); } @@ -368,3 +378,10 @@ function raiseEvent(event: Event, browserRendererId: number, eventHandlerId: num eventDescriptor, JSON.stringify(eventArgs.data)); } + +function clearElement(element: Element) { + let childNode: Node | null; + while (childNode = element.firstChild) { + element.removeChild(childNode); + } +} diff --git a/src/Components/Browser.JS/src/Rendering/LogicalElements.ts b/src/Components/Browser.JS/src/Rendering/LogicalElements.ts index 826628ee2e..28d153f37d 100644 --- a/src/Components/Browser.JS/src/Rendering/LogicalElements.ts +++ b/src/Components/Browser.JS/src/Rendering/LogicalElements.ts @@ -28,9 +28,12 @@ const logicalChildrenPropname = createSymbolOrFallback('_blazorLogicalChildren'); const logicalParentPropname = createSymbolOrFallback('_blazorLogicalParent'); -export function toLogicalElement(element: Element) { - if (element.childNodes.length > 0) { - throw new Error('New logical elements must start empty'); +export function toLogicalElement(element: Element, allowExistingContents?: boolean) { + // Normally it's good to assert that the element has started empty, because that's the usual + // situation and we probably have a bug if it's not. But for the element that contain prerendered + // root components, we want to let them keep their content until we replace it. + if (element.childNodes.length > 0 && !allowExistingContents) { + throw new Error('New logical elements must start empty, or allowExistingContents must be true'); } element[logicalChildrenPropname] = []; diff --git a/src/Components/Browser.JS/src/Rendering/Renderer.ts b/src/Components/Browser.JS/src/Rendering/Renderer.ts index fa303f9f99..fd82e54568 100644 --- a/src/Components/Browser.JS/src/Rendering/Renderer.ts +++ b/src/Components/Browser.JS/src/Rendering/Renderer.ts @@ -16,7 +16,6 @@ export function attachRootComponentToElement(browserRendererId: number, elementS if (!browserRenderer) { browserRenderer = browserRenderers[browserRendererId] = new BrowserRenderer(browserRendererId); } - clearElement(element); browserRenderer.attachRootComponentToElement(componentId, element); } @@ -57,10 +56,3 @@ export function renderBatch(browserRendererId: number, batch: RenderBatch) { browserRenderer.disposeEventHandler(eventHandlerId); } } - -function clearElement(element: Element) { - let childNode: Node | null; - while (childNode = element.firstChild) { - element.removeChild(childNode); - } -}