phoenix_live_view 0.20.0 → 0.20.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -9
- package/assets/js/phoenix_live_view/aria.js +1 -1
- package/assets/js/phoenix_live_view/constants.js +3 -1
- package/assets/js/phoenix_live_view/dom.js +9 -4
- package/assets/js/phoenix_live_view/dom_patch.js +35 -45
- package/assets/js/phoenix_live_view/entry_uploader.js +1 -0
- package/assets/js/phoenix_live_view/js.js +10 -0
- package/assets/js/phoenix_live_view/live_socket.js +9 -7
- package/assets/js/phoenix_live_view/live_uploader.js +1 -0
- package/assets/js/phoenix_live_view/rendered.js +216 -71
- package/assets/js/phoenix_live_view/upload_entry.js +2 -1
- package/assets/js/phoenix_live_view/view.js +23 -10
- package/assets/js/phoenix_live_view/view_hook.js +4 -2
- package/assets/package.json +2 -2
- package/package.json +1 -1
- package/priv/static/phoenix_live_view.cjs.js +266 -131
- package/priv/static/phoenix_live_view.cjs.js.map +2 -2
- package/priv/static/phoenix_live_view.esm.js +266 -131
- package/priv/static/phoenix_live_view.esm.js.map +2 -2
- package/priv/static/phoenix_live_view.js +266 -131
- package/priv/static/phoenix_live_view.min.js +5 -10
package/README.md
CHANGED
|
@@ -29,10 +29,10 @@ model while keeping your code closer to your data (and ultimately your source of
|
|
|
29
29
|
|
|
30
30
|
* **Rich templating language:** Enjoy HEEx: a templating language that supports function components, slots, HTML validation, verified routes, and more.
|
|
31
31
|
|
|
32
|
-
* **
|
|
32
|
+
* **Diffs over the wire:** Instead of sending "HTML over the wire", LiveView knows exactly which parts of your templates change, sending minimal diffs over the wire after the initial render, reducing latency and bandwidth usage. The client leverages this information and optimizes the browser with 5-10x faster updates, compared to solutions that replace whole HTML fragments.
|
|
33
33
|
|
|
34
34
|
* **Live form validation:** LiveView supports real-time form validation out of the box. Create rich user interfaces with features like uploads, nested inputs, and [specialized recovery](https://hexdocs.pm/phoenix_live_view/form-bindings.html#recovery-following-crashes-or-disconnects).
|
|
35
|
-
|
|
35
|
+
|
|
36
36
|
* **File uploads:** Real-time file uploads with progress indicators and image previews. Process your uploads on the fly or submit them to your desired cloud service.
|
|
37
37
|
|
|
38
38
|
* **Rich integration API:** Use the rich integration API to interact with the client, with `phx-click`, `phx-focus`, `phx-blur`, `phx-submit`, and `phx-hook` included for cases where you have to write JavaScript.
|
|
@@ -47,9 +47,13 @@ model while keeping your code closer to your data (and ultimately your source of
|
|
|
47
47
|
|
|
48
48
|
* **Robust test suite:** Write tests with confidence alongside Phoenix LiveView built-in testing tools. No more running a whole browser alongside your tests.
|
|
49
49
|
|
|
50
|
-
##
|
|
50
|
+
## Learning
|
|
51
|
+
|
|
52
|
+
Check our [comprehensive docs](https://hexdocs.pm/phoenix_live_view) to get started.
|
|
53
|
+
|
|
54
|
+
The Phoenix framework documentation also keeps a list of [community resources](https://hexdocs.pm/phoenix/community.html), including books, videos, and other materials, and some include LiveView too.
|
|
51
55
|
|
|
52
|
-
|
|
56
|
+
Also follow these announcements from the Phoenix team on LiveView for more examples and rationale:
|
|
53
57
|
|
|
54
58
|
* [LiveBeats: Building a Social Music App With Phoenix LiveView](https://fly.io/blog/livebeats/)
|
|
55
59
|
|
|
@@ -57,15 +61,11 @@ News from the Phoenix team on LiveView:
|
|
|
57
61
|
|
|
58
62
|
* [Initial announcement](https://dockyard.com/blog/2018/12/12/phoenix-liveview-interactive-real-time-apps-no-need-to-write-javascript)
|
|
59
63
|
|
|
60
|
-
## Learning
|
|
61
|
-
|
|
62
|
-
See our existing comprehensive [docs](https://hexdocs.pm/phoenix_live_view) and [guides](https://hexdocs.pm/phoenix_live_view/api-reference.html) for more information.
|
|
63
|
-
|
|
64
64
|
## Installation
|
|
65
65
|
|
|
66
66
|
LiveView is included by default in all new Phoenix v1.6+ applications and
|
|
67
67
|
later. If you have an older existing Phoenix app and you wish to add
|
|
68
|
-
LiveView, see [the installation guide
|
|
68
|
+
LiveView, see [the previous installation guide](https://github.com/phoenixframework/phoenix_live_view/blob/v0.20.1/guides/introduction/installation.md).
|
|
69
69
|
|
|
70
70
|
## What makes LiveView unique?
|
|
71
71
|
|
|
@@ -167,6 +167,7 @@ $ mix test
|
|
|
167
167
|
```
|
|
168
168
|
|
|
169
169
|
Running the Javascript tests:
|
|
170
|
+
|
|
170
171
|
```bash
|
|
171
172
|
$ cd assets
|
|
172
173
|
$ npm run test
|
|
@@ -17,7 +17,7 @@ let ARIA = {
|
|
|
17
17
|
(el instanceof HTMLAreaElement && el.href !== undefined) ||
|
|
18
18
|
(!el.disabled && (this.anyOf(el, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement, HTMLButtonElement]))) ||
|
|
19
19
|
(el instanceof HTMLIFrameElement) ||
|
|
20
|
-
(el.tabIndex > 0 || (!interactiveOnly && el.
|
|
20
|
+
(el.tabIndex > 0 || (!interactiveOnly && el.getAttribute("tabindex") !== null && el.getAttribute("aria-hidden") !== "true"))
|
|
21
21
|
)
|
|
22
22
|
},
|
|
23
23
|
|
|
@@ -21,6 +21,7 @@ export const PHX_DROP_TARGET = "drop-target"
|
|
|
21
21
|
export const PHX_ACTIVE_ENTRY_REFS = "data-phx-active-refs"
|
|
22
22
|
export const PHX_LIVE_FILE_UPDATED = "phx:live-file:updated"
|
|
23
23
|
export const PHX_SKIP = "data-phx-skip"
|
|
24
|
+
export const PHX_MAGIC_ID = "data-phx-id"
|
|
24
25
|
export const PHX_PRUNE = "data-phx-prune"
|
|
25
26
|
export const PHX_PAGE_LOADING = "page-loading"
|
|
26
27
|
export const PHX_CONNECTED_CLASS = "phx-connected"
|
|
@@ -79,9 +80,10 @@ export const DEFAULTS = {
|
|
|
79
80
|
// Rendered
|
|
80
81
|
export const DYNAMICS = "d"
|
|
81
82
|
export const STATIC = "s"
|
|
83
|
+
export const ROOT = "r"
|
|
82
84
|
export const COMPONENTS = "c"
|
|
83
85
|
export const EVENTS = "e"
|
|
84
86
|
export const REPLY = "r"
|
|
85
87
|
export const TITLE = "t"
|
|
86
88
|
export const TEMPLATES = "p"
|
|
87
|
-
export const STREAM = "stream"
|
|
89
|
+
export const STREAM = "stream"
|
|
@@ -285,10 +285,15 @@ let DOM = {
|
|
|
285
285
|
}
|
|
286
286
|
},
|
|
287
287
|
|
|
288
|
-
maybeHideFeedback(container,
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
if(
|
|
288
|
+
maybeHideFeedback(container, inputs, phxFeedbackFor){
|
|
289
|
+
let feedbacks = []
|
|
290
|
+
inputs.forEach(input => {
|
|
291
|
+
if(!(this.private(input, PHX_HAS_FOCUSED) || this.private(input, PHX_HAS_SUBMITTED))){
|
|
292
|
+
feedbacks.push(input.name)
|
|
293
|
+
if(input.name.endsWith("[]")){ feedbacks.push(input.name.slice(0, -2)) }
|
|
294
|
+
}
|
|
295
|
+
})
|
|
296
|
+
if(feedbacks.length > 0){
|
|
292
297
|
let selector = feedbacks.map(f => `[${phxFeedbackFor}="${f}"]`).join(", ")
|
|
293
298
|
DOM.all(container, selector, el => el.classList.add(PHX_NO_FEEDBACK_CLASS))
|
|
294
299
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
PHX_ROOT_ID,
|
|
7
7
|
PHX_SESSION,
|
|
8
8
|
PHX_SKIP,
|
|
9
|
+
PHX_MAGIC_ID,
|
|
9
10
|
PHX_STATIC,
|
|
10
11
|
PHX_TRIGGER_ACTION,
|
|
11
12
|
PHX_UPDATE,
|
|
@@ -76,7 +77,7 @@ export default class DOMPatch {
|
|
|
76
77
|
})
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
perform(){
|
|
80
|
+
perform(isJoinPatch){
|
|
80
81
|
let {view, liveSocket, container, html} = this
|
|
81
82
|
let targetContainer = this.isCIDPatch() ? this.targetCIDContainer(html) : container
|
|
82
83
|
if(this.isCIDPatch() && !targetContainer){ return }
|
|
@@ -96,21 +97,19 @@ export default class DOMPatch {
|
|
|
96
97
|
|
|
97
98
|
let externalFormTriggered = null
|
|
98
99
|
|
|
99
|
-
let diffHTML = liveSocket.time("premorph container prep", () => {
|
|
100
|
-
return this.buildDiffHTML(container, html, phxUpdate, targetContainer)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
100
|
this.trackBefore("added", container)
|
|
104
101
|
this.trackBefore("updated", container, container)
|
|
105
102
|
|
|
106
103
|
liveSocket.time("morphdom", () => {
|
|
107
104
|
this.streams.forEach(([ref, inserts, deleteIds, reset]) => {
|
|
108
105
|
Object.entries(inserts).forEach(([key, [streamAt, limit]]) => {
|
|
109
|
-
this.streamInserts[key] = {ref, streamAt, limit}
|
|
106
|
+
this.streamInserts[key] = {ref, streamAt, limit, resetKept: false}
|
|
110
107
|
})
|
|
111
108
|
if(reset !== undefined){
|
|
112
109
|
DOM.all(container, `[${PHX_STREAM_REF}="${ref}"]`, child => {
|
|
113
|
-
if(
|
|
110
|
+
if(inserts[child.id]){
|
|
111
|
+
this.streamInserts[child.id].resetKept = true
|
|
112
|
+
} else {
|
|
114
113
|
this.removeStreamChildElement(child)
|
|
115
114
|
}
|
|
116
115
|
})
|
|
@@ -121,10 +120,14 @@ export default class DOMPatch {
|
|
|
121
120
|
})
|
|
122
121
|
})
|
|
123
122
|
|
|
124
|
-
morphdom(targetContainer,
|
|
123
|
+
morphdom(targetContainer, html, {
|
|
125
124
|
childrenOnly: targetContainer.getAttribute(PHX_COMPONENT) === null,
|
|
126
125
|
getNodeKey: (node) => {
|
|
127
|
-
|
|
126
|
+
if(DOM.isPhxDestroyed(node)){ return null }
|
|
127
|
+
// If we have a join patch, then by definition there was no PHX_MAGIC_ID.
|
|
128
|
+
// This is important to reduce the amount of elements morphdom discards.
|
|
129
|
+
if(isJoinPatch){ return node.id }
|
|
130
|
+
return node.id || (node.getAttribute && node.getAttribute(PHX_MAGIC_ID))
|
|
128
131
|
},
|
|
129
132
|
// skip indexing from children when container is stream
|
|
130
133
|
skipFromChildren: (from) => { return from.getAttribute(phxUpdate) === PHX_STREAM },
|
|
@@ -185,6 +188,27 @@ export default class DOMPatch {
|
|
|
185
188
|
}
|
|
186
189
|
added.push(el)
|
|
187
190
|
},
|
|
191
|
+
onBeforeElChildrenUpdated: (fromEl, toEl) => {
|
|
192
|
+
// before we update the children, we need to set existing stream children
|
|
193
|
+
// into the new order from the server if they were kept during a stream reset
|
|
194
|
+
if(fromEl.getAttribute(phxUpdate) === PHX_STREAM){
|
|
195
|
+
let toIds = Array.from(toEl.children).map(child => child.id)
|
|
196
|
+
Array.from(fromEl.children).filter(child => {
|
|
197
|
+
let {resetKept} = this.getStreamInsert(child)
|
|
198
|
+
return resetKept
|
|
199
|
+
}).sort((a, b) => {
|
|
200
|
+
let aIdx = toIds.indexOf(a.id)
|
|
201
|
+
let bIdx = toIds.indexOf(b.id)
|
|
202
|
+
if(aIdx === bIdx){
|
|
203
|
+
return 0
|
|
204
|
+
} else if(aIdx < bIdx){
|
|
205
|
+
return -1
|
|
206
|
+
} else {
|
|
207
|
+
return 1
|
|
208
|
+
}
|
|
209
|
+
}).forEach(child => fromEl.appendChild(child))
|
|
210
|
+
}
|
|
211
|
+
},
|
|
188
212
|
onNodeDiscarded: (el) => this.onNodeDiscarded(el),
|
|
189
213
|
onBeforeNodeDiscarded: (el) => {
|
|
190
214
|
if(el.getAttribute && el.getAttribute(PHX_PRUNE) !== null){ return true }
|
|
@@ -273,9 +297,7 @@ export default class DOMPatch {
|
|
|
273
297
|
})
|
|
274
298
|
}
|
|
275
299
|
|
|
276
|
-
|
|
277
|
-
DOM.maybeHideFeedback(targetContainer, input, phxFeedbackFor)
|
|
278
|
-
})
|
|
300
|
+
DOM.maybeHideFeedback(targetContainer, trackedInputs, phxFeedbackFor)
|
|
279
301
|
|
|
280
302
|
liveSocket.silenceEvents(() => DOM.restoreFocus(focused, selectionStart, selectionEnd))
|
|
281
303
|
DOM.dispatchEvent(document, "phx:update")
|
|
@@ -363,7 +385,7 @@ export default class DOMPatch {
|
|
|
363
385
|
isCIDPatch(){ return this.cidPatch }
|
|
364
386
|
|
|
365
387
|
skipCIDSibling(el){
|
|
366
|
-
return el.nodeType === Node.ELEMENT_NODE && el.
|
|
388
|
+
return el.nodeType === Node.ELEMENT_NODE && el.hasAttribute(PHX_SKIP)
|
|
367
389
|
}
|
|
368
390
|
|
|
369
391
|
targetCIDContainer(html){
|
|
@@ -376,37 +398,5 @@ export default class DOMPatch {
|
|
|
376
398
|
}
|
|
377
399
|
}
|
|
378
400
|
|
|
379
|
-
// builds HTML for morphdom patch
|
|
380
|
-
// - for full patches of LiveView or a component with a single
|
|
381
|
-
// root node, simply returns the HTML
|
|
382
|
-
// - for patches of a component with multiple root nodes, the
|
|
383
|
-
// parent node becomes the target container and non-component
|
|
384
|
-
// siblings are marked as skip.
|
|
385
|
-
buildDiffHTML(container, html, phxUpdate, targetContainer){
|
|
386
|
-
let isCIDPatch = this.isCIDPatch()
|
|
387
|
-
let isCIDWithSingleRoot = isCIDPatch && targetContainer.getAttribute(PHX_COMPONENT) === this.targetCID.toString()
|
|
388
|
-
if(!isCIDPatch || isCIDWithSingleRoot){
|
|
389
|
-
return html
|
|
390
|
-
} else {
|
|
391
|
-
// component patch with multiple CID roots
|
|
392
|
-
let diffContainer = null
|
|
393
|
-
let template = document.createElement("template")
|
|
394
|
-
diffContainer = DOM.cloneNode(targetContainer)
|
|
395
|
-
let [firstComponent, ...rest] = DOM.findComponentNodeList(diffContainer, this.targetCID)
|
|
396
|
-
template.innerHTML = html
|
|
397
|
-
rest.forEach(el => el.remove())
|
|
398
|
-
Array.from(diffContainer.childNodes).forEach(child => {
|
|
399
|
-
// we can only skip trackable nodes with an ID
|
|
400
|
-
if(child.id && child.nodeType === Node.ELEMENT_NODE && child.getAttribute(PHX_COMPONENT) !== this.targetCID.toString()){
|
|
401
|
-
child.setAttribute(PHX_SKIP, "")
|
|
402
|
-
child.innerHTML = ""
|
|
403
|
-
}
|
|
404
|
-
})
|
|
405
|
-
Array.from(template.content.childNodes).forEach(el => diffContainer.insertBefore(el, firstComponent))
|
|
406
|
-
firstComponent.remove()
|
|
407
|
-
return diffContainer.outerHTML
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
|
|
411
401
|
indexOf(parent, child){ return Array.from(parent.children).indexOf(child) }
|
|
412
402
|
}
|
|
@@ -26,6 +26,16 @@ let JS = {
|
|
|
26
26
|
return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length > 0)
|
|
27
27
|
},
|
|
28
28
|
|
|
29
|
+
isInViewport(el){
|
|
30
|
+
const rect = el.getBoundingClientRect()
|
|
31
|
+
return (
|
|
32
|
+
rect.top >= 0 &&
|
|
33
|
+
rect.left >= 0 &&
|
|
34
|
+
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
35
|
+
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
36
|
+
)
|
|
37
|
+
},
|
|
38
|
+
|
|
29
39
|
// private
|
|
30
40
|
|
|
31
41
|
// commands
|
|
@@ -522,7 +522,7 @@ export default class LiveSocket {
|
|
|
522
522
|
if(!dead){ this.bindNav() }
|
|
523
523
|
this.bindClicks()
|
|
524
524
|
if(!dead){ this.bindForms() }
|
|
525
|
-
this.bind({keyup: "keyup", keydown: "keydown"}, (e, type, view, targetEl, phxEvent,
|
|
525
|
+
this.bind({keyup: "keyup", keydown: "keydown"}, (e, type, view, targetEl, phxEvent, phxTarget) => {
|
|
526
526
|
let matchKey = targetEl.getAttribute(this.binding(PHX_KEY))
|
|
527
527
|
let pressedKey = e.key && e.key.toLowerCase() // chrome clicked autocompletes send a keydown without key
|
|
528
528
|
if(matchKey && matchKey.toLowerCase() !== pressedKey){ return }
|
|
@@ -530,13 +530,13 @@ export default class LiveSocket {
|
|
|
530
530
|
let data = {key: e.key, ...this.eventMeta(type, e, targetEl)}
|
|
531
531
|
JS.exec(type, phxEvent, view, targetEl, ["push", {data}])
|
|
532
532
|
})
|
|
533
|
-
this.bind({blur: "focusout", focus: "focusin"}, (e, type, view, targetEl, phxEvent,
|
|
534
|
-
if(!
|
|
533
|
+
this.bind({blur: "focusout", focus: "focusin"}, (e, type, view, targetEl, phxEvent, phxTarget) => {
|
|
534
|
+
if(!phxTarget){
|
|
535
535
|
let data = {key: e.key, ...this.eventMeta(type, e, targetEl)}
|
|
536
536
|
JS.exec(type, phxEvent, view, targetEl, ["push", {data}])
|
|
537
537
|
}
|
|
538
538
|
})
|
|
539
|
-
this.bind({blur: "blur", focus: "focus"}, (e, type, view, targetEl,
|
|
539
|
+
this.bind({blur: "blur", focus: "focus"}, (e, type, view, targetEl, phxEvent, phxTarget) => {
|
|
540
540
|
// blur and focus are triggered on document and window. Discard one to avoid dups
|
|
541
541
|
if(phxTarget === "window"){
|
|
542
542
|
let data = this.eventMeta(type, e, targetEl)
|
|
@@ -619,7 +619,7 @@ export default class LiveSocket {
|
|
|
619
619
|
}
|
|
620
620
|
|
|
621
621
|
bindClicks(){
|
|
622
|
-
window.addEventListener("
|
|
622
|
+
window.addEventListener("mousedown", e => this.clickStartedAtTarget = e.target)
|
|
623
623
|
this.bindClick("click", "click", false)
|
|
624
624
|
this.bindClick("mousedown", "capture-click", true)
|
|
625
625
|
}
|
|
@@ -661,7 +661,7 @@ export default class LiveSocket {
|
|
|
661
661
|
if(!(el.isSameNode(clickStartedAt) || el.contains(clickStartedAt))){
|
|
662
662
|
this.withinOwners(e.target, view => {
|
|
663
663
|
let phxEvent = el.getAttribute(phxClickAway)
|
|
664
|
-
if(JS.isVisible(el)){
|
|
664
|
+
if(JS.isVisible(el) && JS.isInViewport(el)){
|
|
665
665
|
JS.exec("click", phxEvent, view, el, ["push", {data: this.eventMeta("click", e, e.target)}])
|
|
666
666
|
}
|
|
667
667
|
})
|
|
@@ -703,7 +703,9 @@ export default class LiveSocket {
|
|
|
703
703
|
let type = target && target.getAttribute(PHX_LIVE_LINK)
|
|
704
704
|
if(!type || !this.isConnected() || !this.main || DOM.wantsNewTab(e)){ return }
|
|
705
705
|
|
|
706
|
-
|
|
706
|
+
// When wrapping an SVG element in an anchor tag, the href can be an SVGAnimatedString
|
|
707
|
+
let href = target.href instanceof SVGAnimatedString ? target.href.baseVal : target.href
|
|
708
|
+
|
|
707
709
|
let linkState = target.getAttribute(PHX_LINK_STATE)
|
|
708
710
|
e.preventDefault()
|
|
709
711
|
e.stopImmediatePropagation() // do not bubble click to regular phx-click bindings
|
|
@@ -51,6 +51,7 @@ export default class LiveUploader {
|
|
|
51
51
|
entry.relative_path = file.webkitRelativePath
|
|
52
52
|
entry.type = file.type
|
|
53
53
|
entry.size = file.size
|
|
54
|
+
if(typeof(file.meta) === "function"){ entry.meta = file.meta() }
|
|
54
55
|
fileData[uploadRef].push(entry)
|
|
55
56
|
})
|
|
56
57
|
return fileData
|