Packages
phoenix_live_view
0.17.9
1.2.8
1.2.7
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.2.0-rc.3
1.2.0-rc.2
1.2.0-rc.1
1.2.0-rc.0
1.1.32
1.1.31
1.1.30
1.1.29
1.1.28
1.1.27
1.1.26
1.1.25
1.1.24
1.1.23
1.1.22
1.1.21
1.1.20
1.1.19
1.1.18
1.1.17
1.1.16
1.1.15
1.1.14
1.1.13
1.1.12
1.1.11
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
retired
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.1.0-rc.4
1.1.0-rc.3
1.1.0-rc.2
1.1.0-rc.1
1.1.0-rc.0
1.0.18
1.0.17
1.0.16
1.0.15
1.0.14
1.0.13
1.0.12
1.0.11
1.0.10
1.0.9
1.0.8
retired
1.0.7
1.0.6
retired
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
1.0.0-rc.9
1.0.0-rc.8
1.0.0-rc.7
1.0.0-rc.6
1.0.0-rc.5
1.0.0-rc.4
1.0.0-rc.3
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
0.20.17
0.20.16
0.20.15
0.20.14
0.20.13
0.20.12
0.20.11
0.20.10
0.20.9
0.20.8
0.20.7
0.20.6
0.20.5
0.20.4
0.20.3
0.20.2
0.20.1
0.20.0
0.19.5
0.19.4
0.19.3
0.19.2
0.19.1
0.19.0
0.18.18
0.18.17
0.18.16
0.18.15
0.18.14
0.18.13
0.18.12
0.18.11
0.18.10
0.18.9
0.18.8
0.18.7
0.18.6
0.18.5
0.18.4
0.18.3
0.18.2
0.18.1
0.18.0
0.17.14
0.17.13
0.17.12
0.17.11
0.17.10
0.17.9
0.17.8
0.17.7
0.17.6
0.17.5
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.4
0.16.3
0.16.2
0.16.1
0.16.0
0.15.7
0.15.6
0.15.5
0.15.4
0.15.3
0.15.2
0.15.1
0.15.0
0.14.8
0.14.7
0.14.6
0.14.5
0.14.4
0.14.3
0.14.2
0.14.1
0.14.0
0.13.3
0.13.2
0.13.1
0.13.0
0.12.1
0.12.0
0.11.1
0.11.0
0.10.0
0.9.0
0.8.1
0.8.0
0.7.1
0.7.0
0.6.0
0.6.0-dev
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.1
0.3.0
0.2.1
0.2.0
0.1.1
0.1.0
Rich, real-time user experiences with server-rendered HTML
Current section
Files
Jump to
Current section
Files
assets/js/phoenix_live_view/rendered.js
import {
COMPONENTS,
DYNAMICS,
TEMPLATES,
EVENTS,
PHX_COMPONENT,
PHX_SKIP,
REPLY,
STATIC,
TITLE
} from "./constants"
import {
isObject,
logError,
isCid,
} from "./utils"
export default class Rendered {
static extract(diff){
let {[REPLY]: reply, [EVENTS]: events, [TITLE]: title} = diff
delete diff[REPLY]
delete diff[EVENTS]
delete diff[TITLE]
return {diff, title, reply: reply || null, events: events || []}
}
constructor(viewId, rendered){
this.viewId = viewId
this.rendered = {}
this.mergeDiff(rendered)
}
parentViewId(){ return this.viewId }
toString(onlyCids){
return this.recursiveToString(this.rendered, this.rendered[COMPONENTS], onlyCids)
}
recursiveToString(rendered, components = rendered[COMPONENTS], onlyCids){
onlyCids = onlyCids ? new Set(onlyCids) : null
let output = {buffer: "", components: components, onlyCids: onlyCids}
this.toOutputBuffer(rendered, null, output)
return output.buffer
}
componentCIDs(diff){ return Object.keys(diff[COMPONENTS] || {}).map(i => parseInt(i)) }
isComponentOnlyDiff(diff){
if(!diff[COMPONENTS]){ return false }
return Object.keys(diff).length === 1
}
getComponent(diff, cid){ return diff[COMPONENTS][cid] }
mergeDiff(diff){
let newc = diff[COMPONENTS]
let cache = {}
delete diff[COMPONENTS]
this.rendered = this.mutableMerge(this.rendered, diff)
this.rendered[COMPONENTS] = this.rendered[COMPONENTS] || {}
if(newc){
let oldc = this.rendered[COMPONENTS]
for(let cid in newc){
newc[cid] = this.cachedFindComponent(cid, newc[cid], oldc, newc, cache)
}
for(let cid in newc){ oldc[cid] = newc[cid] }
diff[COMPONENTS] = newc
}
}
cachedFindComponent(cid, cdiff, oldc, newc, cache){
if(cache[cid]){
return cache[cid]
} else {
let ndiff, stat, scid = cdiff[STATIC]
if(isCid(scid)){
let tdiff
if(scid > 0){
tdiff = this.cachedFindComponent(scid, newc[scid], oldc, newc, cache)
} else {
tdiff = oldc[-scid]
}
stat = tdiff[STATIC]
ndiff = this.cloneMerge(tdiff, cdiff)
ndiff[STATIC] = stat
} else {
ndiff = cdiff[STATIC] !== undefined ? cdiff : this.cloneMerge(oldc[cid] || {}, cdiff)
}
cache[cid] = ndiff
return ndiff
}
}
mutableMerge(target, source){
if(source[STATIC] !== undefined){
return source
} else {
this.doMutableMerge(target, source)
return target
}
}
doMutableMerge(target, source){
for(let key in source){
let val = source[key]
let targetVal = target[key]
if(isObject(val) && val[STATIC] === undefined && isObject(targetVal)){
this.doMutableMerge(targetVal, val)
} else {
target[key] = val
}
}
}
cloneMerge(target, source){
let merged = {...target, ...source}
for(let key in merged){
let val = source[key]
let targetVal = target[key]
if(isObject(val) && val[STATIC] === undefined && isObject(targetVal)){
merged[key] = this.cloneMerge(targetVal, val)
}
}
return merged
}
componentToString(cid){ return this.recursiveCIDToString(this.rendered[COMPONENTS], cid) }
pruneCIDs(cids){
cids.forEach(cid => delete this.rendered[COMPONENTS][cid])
}
// private
get(){ return this.rendered }
isNewFingerprint(diff = {}){ return !!diff[STATIC] }
templateStatic(part, templates){
if(typeof (part) === "number") {
return templates[part]
} else {
return part
}
}
toOutputBuffer(rendered, templates, output){
if(rendered[DYNAMICS]){ return this.comprehensionToBuffer(rendered, templates, output) }
let {[STATIC]: statics} = rendered
statics = this.templateStatic(statics, templates)
output.buffer += statics[0]
for(let i = 1; i < statics.length; i++){
this.dynamicToBuffer(rendered[i - 1], templates, output)
output.buffer += statics[i]
}
}
comprehensionToBuffer(rendered, templates, output){
let {[DYNAMICS]: dynamics, [STATIC]: statics} = rendered
statics = this.templateStatic(statics, templates)
let compTemplates = templates || rendered[TEMPLATES]
for(let d = 0; d < dynamics.length; d++){
let dynamic = dynamics[d]
output.buffer += statics[0]
for(let i = 1; i < statics.length; i++){
this.dynamicToBuffer(dynamic[i - 1], compTemplates, output)
output.buffer += statics[i]
}
}
}
dynamicToBuffer(rendered, templates, output){
if(typeof (rendered) === "number"){
output.buffer += this.recursiveCIDToString(output.components, rendered, output.onlyCids)
} else if(isObject(rendered)){
this.toOutputBuffer(rendered, templates, output)
} else {
output.buffer += rendered
}
}
recursiveCIDToString(components, cid, onlyCids){
let component = components[cid] || logError(`no component for CID ${cid}`, components)
let template = document.createElement("template")
template.innerHTML = this.recursiveToString(component, components, onlyCids)
let container = template.content
let skip = onlyCids && !onlyCids.has(cid)
let [hasChildNodes, hasChildComponents] =
Array.from(container.childNodes).reduce(([hasNodes, hasComponents], child, i) => {
if(child.nodeType === Node.ELEMENT_NODE){
if(child.getAttribute(PHX_COMPONENT)){
return [hasNodes, true]
}
child.setAttribute(PHX_COMPONENT, cid)
if(!child.id){ child.id = `${this.parentViewId()}-${cid}-${i}` }
if(skip){
child.setAttribute(PHX_SKIP, "")
child.innerHTML = ""
}
return [true, hasComponents]
} else {
if(child.nodeValue.trim() !== ""){
logError("only HTML element tags are allowed at the root of components.\n\n" +
`got: "${child.nodeValue.trim()}"\n\n` +
"within:\n", template.innerHTML.trim())
child.replaceWith(this.createSpan(child.nodeValue, cid))
return [true, hasComponents]
} else {
child.remove()
return [hasNodes, hasComponents]
}
}
}, [false, false])
if(!hasChildNodes && !hasChildComponents){
logError("expected at least one HTML element tag inside a component, but the component is empty:\n",
template.innerHTML.trim())
return this.createSpan("", cid).outerHTML
} else if(!hasChildNodes && hasChildComponents){
logError("expected at least one HTML element tag directly inside a component, but only subcomponents were found. A component must render at least one HTML tag directly inside itself.",
template.innerHTML.trim())
return template.innerHTML
} else {
return template.innerHTML
}
}
createSpan(text, cid){
let span = document.createElement("span")
span.innerText = text
span.setAttribute(PHX_COMPONENT, cid)
return span
}
}