-
Notifications
You must be signed in to change notification settings - Fork 436
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Persist data-turbo-permanent element within frames
Related to #64 Introduce the `FrameRenderer` (as a descendant of the `Renderer` interface, inspired by the `SnapshotRenderer` implementation) to serve as an abstraction for the behavior the `FrameController` was previously responsible for. Extract `turbo-frame` content extraction from the `FrameController` class and combine it with code copied from `SnapshotRenderer` responsible for cloning permanent elements into the new HTML. It's likely that there is a concept awaiting extraction, or a less duplicative way of re-using the permanent element extraction logic, but this is a start. Note, this commit does not resolve the `<turbo-stream>` element rendering shortcomings, but might blaze a trail for a `StreamRenderer` implementation.
- Loading branch information
1 parent
21e1769
commit 80466d4
Showing
6 changed files
with
181 additions
and
64 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import { FrameElement } from "../../elements/frame_element" | ||
import { RenderCallback, RenderDelegate, Renderer } from "../drive/renderer" | ||
import { Snapshot } from "../drive/snapshot" | ||
|
||
export { RenderCallback, RenderDelegate } from "../drive/renderer" | ||
|
||
export type PermanentElement = Element & { id: string } | ||
|
||
export type Placeholder = { element: Element, permanentElement: PermanentElement } | ||
|
||
export class FrameRenderer extends Renderer { | ||
readonly delegate: RenderDelegate | ||
readonly frameElement: FrameElement | ||
readonly currentSnapshot: Snapshot | ||
readonly newSnapshot: Snapshot | ||
readonly newBody: HTMLBodyElement | ||
|
||
static render(delegate: RenderDelegate, callback: RenderCallback, frameElement: FrameElement, newSnapshot: Snapshot) { | ||
return new this(delegate, frameElement, newSnapshot).renderView(callback) | ||
} | ||
|
||
constructor(delegate: RenderDelegate, frameElement: FrameElement, newSnapshot: Snapshot) { | ||
super() | ||
this.delegate = delegate | ||
this.frameElement = frameElement | ||
this.currentSnapshot = Snapshot.wrap(frameElement.innerHTML) | ||
this.newSnapshot = newSnapshot | ||
this.newBody = newSnapshot.bodyElement | ||
} | ||
|
||
async renderView(callback: RenderCallback) { | ||
const newFrameElement = await this.findFrameElement(this.newBody) | ||
|
||
if (newFrameElement) { | ||
super.renderView(() => { | ||
const placeholders = this.relocateCurrentBodyPermanentElements() | ||
replaceElementWithElement(this.frameElement, newFrameElement) | ||
this.replacePlaceholderElementsWithClonedPermanentElements(placeholders) | ||
this.focusFirstAutofocusableElement() | ||
callback() | ||
}) | ||
} else { | ||
this.invalidateView() | ||
} | ||
} | ||
|
||
private async findFrameElement(container: ParentNode): Promise<FrameElement | undefined> { | ||
let element | ||
|
||
if (element = container.querySelector(`turbo-frame[id="${this.id}"]`)) { | ||
if (element instanceof FrameElement) { | ||
return element | ||
} | ||
} | ||
|
||
if (element = container.querySelector(`turbo-frame[src][recurse~=${this.id}]`)) { | ||
if (element instanceof FrameElement) { | ||
await element.loaded | ||
} | ||
return await this.findFrameElement(element) | ||
} | ||
|
||
console.error(`Response has no matching <turbo-frame id="${this.id}"> element`) | ||
return new FrameElement() | ||
} | ||
|
||
private relocateCurrentBodyPermanentElements() { | ||
return this.currentBodyPermanentElements.reduce((placeholders, permanentElement) => { | ||
const newElement = this.newSnapshot.getPermanentElementById(permanentElement.id) | ||
if (newElement) { | ||
const placeholder = createPlaceholderForPermanentElement(permanentElement) | ||
replaceElementWithElement(permanentElement, placeholder.element) | ||
replaceElementWithElement(newElement, permanentElement) | ||
return [ ...placeholders, placeholder ] | ||
} else { | ||
return placeholders | ||
} | ||
}, [] as Placeholder[]) | ||
} | ||
|
||
private replacePlaceholderElementsWithClonedPermanentElements(placeholders: Placeholder[]) { | ||
for (const { element, permanentElement } of placeholders) { | ||
const clonedElement = permanentElement.cloneNode(true) | ||
replaceElementWithElement(element, clonedElement) | ||
} | ||
} | ||
|
||
private get currentBodyPermanentElements(): PermanentElement[] { | ||
return this.currentSnapshot.getPermanentElementsPresentInSnapshot(this.newSnapshot) | ||
} | ||
|
||
private focusFirstAutofocusableElement() { | ||
const element = this.newSnapshot.findFirstAutofocusableElement() | ||
if (elementIsFocusable(element)) { | ||
element.focus() | ||
} | ||
} | ||
|
||
private get id(): string { | ||
return CSS.escape(this.frameElement.id) | ||
} | ||
} | ||
|
||
function createPlaceholderForPermanentElement(permanentElement: PermanentElement) { | ||
const element = document.createElement("meta") | ||
element.setAttribute("name", "turbo-permanent-placeholder") | ||
element.setAttribute("content", permanentElement.id) | ||
return { element, permanentElement } | ||
} | ||
|
||
function replaceElementWithElement(fromElement: Element, toElement: Element) { | ||
const parentElement = fromElement.parentElement | ||
if (parentElement) { | ||
return parentElement.replaceChild(toElement, fromElement) | ||
} | ||
} | ||
|
||
function elementIsFocusable(element: any): element is { focus: () => void } { | ||
return element && typeof element.focus == "function" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
<turbo-frame id="frame"> | ||
<h2>Frames: Without Layout</h2> | ||
|
||
<div id="permanent-in-frame" data-turbo-permanent>Permanent element</div> | ||
</turbo-frame> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters