phoenix_live_view 0.17.2 → 0.17.3

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.17.3 (2021-10-28)
4
+
5
+ ### Enhancements
6
+ - Support 3-tuple for JS class transitions to support staged animations where a transition class is applied with a starting and ending class
7
+ - Allow JS commands to be executed on DOM nodes outside of the LiveView container
8
+
9
+ ### Optimization
10
+ - Avoid duplicate statics inside comprehension. In previous versions, comprehensions were able to avoid duplication only the content in their root. Now we recursively traverse all comprehension nodes and send the static only once for the whole comprehension. This should massively reduce the cost of sending comprehensions over the wire
11
+
12
+ ### Bug fixes
13
+ - Fix HTML engine bug causing expressions to be duplicated or not rendered correctly
14
+ - Fix HTML engine bug causing slots to not be re-rendered when they should have
15
+ - Fix form recovery being sent to wrong target
16
+
3
17
  ## 0.17.2 (2021-10-22)
4
18
 
5
19
  ### Bug fixes
@@ -63,7 +77,7 @@ atom `:default`.
63
77
  #### LEEx templates in stateful LiveComponents
64
78
 
65
79
  Stateful LiveComponents (where an `:id` is given) must now return HEEx templates
66
- (`~H` sigil or `.heex` extension). LEEx temlates (`~L` sigil or `.leex` extension)
80
+ (`~H` sigil or `.heex` extension). LEEx templates (`~L` sigil or `.leex` extension)
67
81
  are no longer supported. This addresses bugs and allows stateful components
68
82
  to be rendered more efficiently client-side.
69
83
 
@@ -302,6 +316,9 @@ Additionally on the client, the root LiveView element no longer exposes the
302
316
  LiveView module name, therefore the `phx-view` attribute is never set.
303
317
  Similarly, the `viewName` property of client hooks has been removed.
304
318
 
319
+ Codebases calling a custom function `component/3` should rename it or specify its module to avoid a conflict,
320
+ as LiveView introduces a macro with that name and it is special cased by the underlying engine.
321
+
305
322
  ### Enhancements
306
323
  - Introduce HEEx templates
307
324
  - Introduce `Phoenix.Component`
package/README.md CHANGED
@@ -17,9 +17,14 @@ steps:
17
17
  * Use a declarative model to render HTML on the server
18
18
  over WebSockets with optional LongPolling fallback
19
19
 
20
- * Smart templating and change tracking - after connected,
21
- LiveView sends only what changed to the client, skipping
22
- the template markup and reducing the payload
20
+ * A rich templating language, called HEEx, with support
21
+ for function components, slots, HTML validation, and more
22
+
23
+ * Smart change tracking - after connected, LiveView sends
24
+ only what changed to the client, skipping the template
25
+ markup and reducing the payload. This makes LiveView
26
+ payloads much smaller than server-rendered HTML and on
27
+ par with fine-tuned SPA applications
23
28
 
24
29
  * Live form validation with file upload support
25
30
 
@@ -27,9 +32,12 @@ steps:
27
32
  `phx-focus`, `phx-blur`, `phx-submit`, etc. `phx-hook` is
28
33
  included for the cases where you have to write JavaScript
29
34
 
30
- * Code reuse via components, which break templates, state, and
31
- event handling into reusable bits, which is essential in large
32
- applications
35
+ * Perform optimistic updates and transitions via JavaScript
36
+ commands (`Phoenix.LiveView.JS`)
37
+
38
+ * Code reuse via stateful components, which break templates,
39
+ state, and event handling into reusable bits, which is essential
40
+ in large applications
33
41
 
34
42
  * Live navigation to enrich links and redirects to only load the
35
43
  minimum amount of content as users navigate between pages
@@ -106,14 +114,6 @@ anywhere else:
106
114
  sent over the wire. This is achievable thanks to Elixir's
107
115
  immutability and its ability to treat code as data.
108
116
 
109
- * LiveView separates the static and dynamic parts of your templates.
110
- When you first render a page, Phoenix LiveView renders and sends
111
- the whole template to the browser. Then, for any new update, only
112
- the modified dynamic content is resent. This alongside diff tracking
113
- makes it so LiveView only sends a few bytes on every update, instead
114
- of sending kilobytes on every other user interaction - which would
115
- be detrimental to the user experience.
116
-
117
117
  ## Browser Support
118
118
 
119
119
  All current Chrome, Safari, Firefox, and MS Edge are supported.
@@ -74,3 +74,4 @@ export const COMPONENTS = "c"
74
74
  export const EVENTS = "e"
75
75
  export const REPLY = "r"
76
76
  export const TITLE = "t"
77
+ export const TEMPLATES = "p"
@@ -60,19 +60,21 @@ let JS = {
60
60
  }
61
61
  },
62
62
 
63
- exec_transition(eventType, phxEvent, view, sourceEl, {time, to, names}){
63
+ exec_transition(eventType, phxEvent, view, sourceEl, {time, to, transition}){
64
64
  let els = to ? DOM.all(document, to) : [sourceEl]
65
+ let [transition_start, running, transition_end] = transition
65
66
  els.forEach(el => {
66
- this.addOrRemoveClasses(el, names, [])
67
- view.transition(time, () => this.addOrRemoveClasses(el, [], names))
67
+ let onStart = () => this.addOrRemoveClasses(el, transition_start.concat(running), [])
68
+ let onDone = () => this.addOrRemoveClasses(el, transition_end, transition_start.concat(running))
69
+ view.transition(time, onStart, onDone)
68
70
  })
69
71
  },
70
72
 
71
73
  exec_toggle(eventType, phxEvent, view, sourceEl, {to, display, ins, outs, time}){
72
74
  if(to){
73
- DOM.all(document, to, el => this.toggle(eventType, view, el, display, ins || [], outs || [], time))
75
+ DOM.all(document, to, el => this.toggle(eventType, view, el, display, ins, outs, time))
74
76
  } else {
75
- this.toggle(eventType, view, sourceEl, display, ins || [], outs || [], time)
77
+ this.toggle(eventType, view, sourceEl, display, ins, outs, time)
76
78
  }
77
79
  },
78
80
 
@@ -95,37 +97,45 @@ let JS = {
95
97
  // utils for commands
96
98
 
97
99
  show(eventType, view, el, display, transition, time){
98
- let isVisible = this.isVisible(el)
99
- if(transition.length > 0 && !isVisible){
100
- this.toggle(eventType, view, el, display, transition, [], time)
101
- } else if(!isVisible){
102
- this.toggle(eventType, view, el, display, [], [], null)
100
+ if(!this.isVisible(el)){
101
+ this.toggle(eventType, view, el, display, transition, null, time)
103
102
  }
104
103
  },
105
104
 
106
105
  hide(eventType, view, el, display, transition, time){
107
- let isVisible = this.isVisible(el)
108
- if(transition.length > 0 && isVisible){
109
- this.toggle(eventType, view, el, display, [], transition, time)
110
- } else if(isVisible){
111
- this.toggle(eventType, view, el, display, [], [], time)
106
+ if(this.isVisible(el)){
107
+ this.toggle(eventType, view, el, display, null, transition, time)
112
108
  }
113
109
  },
114
110
 
115
- toggle(eventType, view, el, display, in_classes, out_classes, time){
116
- if(in_classes.length > 0 || out_classes.length > 0){
111
+ toggle(eventType, view, el, display, ins, outs, time){
112
+ let [inClasses, inStartClasses, inEndClasses] = ins || [[], [], []]
113
+ let [outClasses, outStartClasses, outEndClasses] = outs || [[], [], []]
114
+ if(inClasses.length > 0 || outClasses.length > 0){
117
115
  if(this.isVisible(el)){
118
- this.addOrRemoveClasses(el, out_classes, in_classes)
119
- view.transition(time, () => {
116
+ let onStart = () => {
117
+ this.addOrRemoveClasses(el, outStartClasses, inClasses.concat(inStartClasses).concat(inEndClasses))
118
+ window.requestAnimationFrame(() => {
119
+ this.addOrRemoveClasses(el, outClasses, [])
120
+ window.requestAnimationFrame(() => this.addOrRemoveClasses(el, outEndClasses, outStartClasses))
121
+ })
122
+ }
123
+ view.transition(time, onStart, () => {
124
+ this.addOrRemoveClasses(el, [], outClasses.concat(outEndClasses))
120
125
  DOM.putSticky(el, "toggle", currentEl => currentEl.style.display = "none")
121
- this.addOrRemoveClasses(el, [], out_classes)
122
126
  })
123
127
  } else {
124
128
  if(eventType === "remove"){ return }
125
- this.addOrRemoveClasses(el, in_classes, out_classes)
126
- DOM.putSticky(el, "toggle", currentEl => currentEl.style.display = (display || "block"))
127
- view.transition(time, () => {
128
- this.addOrRemoveClasses(el, [], in_classes)
129
+ let onStart = () => {
130
+ this.addOrRemoveClasses(el, inStartClasses, outClasses.concat(outStartClasses).concat(outEndClasses))
131
+ DOM.putSticky(el, "toggle", currentEl => currentEl.style.display = (display || "block"))
132
+ window.requestAnimationFrame(() => {
133
+ this.addOrRemoveClasses(el, inClasses, [])
134
+ window.requestAnimationFrame(() => this.addOrRemoveClasses(el, inEndClasses, inStartClasses))
135
+ })
136
+ }
137
+ view.transition(time, onStart, () => {
138
+ this.addOrRemoveClasses(el, [], inClasses.concat(inEndClasses))
129
139
  })
130
140
  }
131
141
  } else {
@@ -135,9 +145,11 @@ let JS = {
135
145
  },
136
146
 
137
147
  addOrRemoveClasses(el, adds, removes, transition, time, view){
138
- if(transition && transition.length > 0){
139
- this.addOrRemoveClasses(el, transition, [])
140
- return view.transition(time, () => this.addOrRemoveClasses(el, adds, removes.concat(transition)))
148
+ let [transition_run, transition_start, transition_end] = transition || [[], [], []]
149
+ if(transition_run.length > 0){
150
+ let onStart = () => this.addOrRemoveClasses(el, transition_start.concat(transition_run), [])
151
+ let onDone = () => this.addOrRemoveClasses(el, adds.concat(transition_end), removes.concat(transition_run).concat(transition_start))
152
+ return view.transition(time, onStart, onDone)
141
153
  }
142
154
  window.requestAnimationFrame(() => {
143
155
  let [prevAdds, prevRemoves] = DOM.getSticky(el, "classes", [[], []])
@@ -161,8 +173,8 @@ let JS = {
161
173
  return !(style.opacity === 0 || style.display === "none")
162
174
  },
163
175
 
164
- isToggledOut(el, out_classes){
165
- return !this.isVisible(el) || this.hasAllClasses(el, out_classes)
176
+ isToggledOut(el, outClasses){
177
+ return !this.isVisible(el) || this.hasAllClasses(el, outClasses)
166
178
  }
167
179
  }
168
180
 
@@ -232,8 +232,8 @@ export default class LiveSocket {
232
232
  this.transitions.after(callback)
233
233
  }
234
234
 
235
- transition(time, onDone = function(){}){
236
- this.transitions.addTransition(time, onDone)
235
+ transition(time, onStart, onDone = function(){}){
236
+ this.transitions.addTransition(time, onStart, onDone)
237
237
  }
238
238
 
239
239
  onChannel(channel, event, cb){
@@ -369,7 +369,7 @@ export default class LiveSocket {
369
369
  }
370
370
 
371
371
  owner(childEl, callback){
372
- let view = maybe(childEl.closest(PHX_VIEW_SELECTOR), el => this.getViewByEl(el))
372
+ let view = maybe(childEl.closest(PHX_VIEW_SELECTOR), el => this.getViewByEl(el)) || this.main
373
373
  if(view){ callback(view) }
374
374
  }
375
375
 
@@ -775,7 +775,8 @@ class TransitionSet {
775
775
  }
776
776
  }
777
777
 
778
- addTransition(time, onDone){
778
+ addTransition(time, onStart, onDone){
779
+ onStart()
779
780
  let timer = setTimeout(() => {
780
781
  this.transitions.delete(timer)
781
782
  onDone()
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  COMPONENTS,
3
3
  DYNAMICS,
4
+ TEMPLATES,
4
5
  EVENTS,
5
6
  PHX_COMPONENT,
6
7
  PHX_SKIP,
@@ -39,7 +40,7 @@ export default class Rendered {
39
40
  recursiveToString(rendered, components = rendered[COMPONENTS], onlyCids){
40
41
  onlyCids = onlyCids ? new Set(onlyCids) : null
41
42
  let output = {buffer: "", components: components, onlyCids: onlyCids}
42
- this.toOutputBuffer(rendered, output)
43
+ this.toOutputBuffer(rendered, null, output)
43
44
  return output.buffer
44
45
  }
45
46
 
@@ -66,7 +67,7 @@ export default class Rendered {
66
67
  newc[cid] = this.cachedFindComponent(cid, newc[cid], oldc, newc, cache)
67
68
  }
68
69
 
69
- for(var key in newc){ oldc[key] = newc[key] }
70
+ for(let cid in newc){ oldc[cid] = newc[cid] }
70
71
  diff[COMPONENTS] = newc
71
72
  }
72
73
  }
@@ -143,35 +144,46 @@ export default class Rendered {
143
144
 
144
145
  isNewFingerprint(diff = {}){ return !!diff[STATIC] }
145
146
 
146
- toOutputBuffer(rendered, output){
147
- if(rendered[DYNAMICS]){ return this.comprehensionToBuffer(rendered, output) }
147
+ templateStatic(part, templates){
148
+ if(typeof (part) === "number") {
149
+ return templates[part]
150
+ } else {
151
+ return part
152
+ }
153
+ }
154
+
155
+ toOutputBuffer(rendered, templates, output){
156
+ if(rendered[DYNAMICS]){ return this.comprehensionToBuffer(rendered, templates, output) }
148
157
  let {[STATIC]: statics} = rendered
158
+ statics = this.templateStatic(statics, templates)
149
159
 
150
160
  output.buffer += statics[0]
151
161
  for(let i = 1; i < statics.length; i++){
152
- this.dynamicToBuffer(rendered[i - 1], output)
162
+ this.dynamicToBuffer(rendered[i - 1], templates, output)
153
163
  output.buffer += statics[i]
154
164
  }
155
165
  }
156
166
 
157
- comprehensionToBuffer(rendered, output){
167
+ comprehensionToBuffer(rendered, templates, output){
158
168
  let {[DYNAMICS]: dynamics, [STATIC]: statics} = rendered
169
+ statics = this.templateStatic(statics, templates)
170
+ let compTemplates = rendered[TEMPLATES]
159
171
 
160
172
  for(let d = 0; d < dynamics.length; d++){
161
173
  let dynamic = dynamics[d]
162
174
  output.buffer += statics[0]
163
175
  for(let i = 1; i < statics.length; i++){
164
- this.dynamicToBuffer(dynamic[i - 1], output)
176
+ this.dynamicToBuffer(dynamic[i - 1], compTemplates, output)
165
177
  output.buffer += statics[i]
166
178
  }
167
179
  }
168
180
  }
169
181
 
170
- dynamicToBuffer(rendered, output){
182
+ dynamicToBuffer(rendered, templates, output){
171
183
  if(typeof (rendered) === "number"){
172
184
  output.buffer += this.recursiveCIDToString(output.components, rendered, output.onlyCids)
173
185
  } else if(isObject(rendered)){
174
- this.toOutputBuffer(rendered, output)
186
+ this.toOutputBuffer(rendered, templates, output)
175
187
  } else {
176
188
  output.buffer += rendered
177
189
  }
@@ -6,7 +6,10 @@ import EntryUploader from "./entry_uploader"
6
6
 
7
7
  export let logError = (msg, obj) => console.error && console.error(msg, obj)
8
8
 
9
- export let isCid = (cid) => typeof(cid) === "number"
9
+ export let isCid = (cid) => {
10
+ let type = typeof(cid)
11
+ return type === "number" || (type === "string" && /^(0|[1-9]\d*)$/.test(cid))
12
+ }
10
13
 
11
14
  export function detectDuplicateIds(){
12
15
  let ids = new Set()
@@ -190,8 +190,8 @@ export default class View {
190
190
  this.liveSocket.log(this, kind, msgCallback)
191
191
  }
192
192
 
193
- transition(time, onDone = function(){}){
194
- this.liveSocket.transition(time, onDone)
193
+ transition(time, onStart, onDone = function(){}){
194
+ this.liveSocket.transition(time, onStart, onDone)
195
195
  }
196
196
 
197
197
  withinTargets(phxTarget, callback){
@@ -199,7 +199,7 @@ export default class View {
199
199
  return this.liveSocket.owner(phxTarget, view => callback(view, phxTarget))
200
200
  }
201
201
 
202
- if(typeof(phxTarget) === "number" || /^(0|[1-9]\d*)$/.test(phxTarget)){
202
+ if(isCid(phxTarget)){
203
203
  let targets = DOM.findComponentNodeList(this.el, phxTarget)
204
204
  if(targets.length === 0){
205
205
  logError(`no component found matching phx-target of ${phxTarget}`)
@@ -716,9 +716,12 @@ export default class View {
716
716
  }
717
717
 
718
718
  targetComponentID(target, targetCtx){
719
- if(isCid(targetCtx)){
720
- return targetCtx
721
- } else if(target.getAttribute(this.binding("target"))){
719
+ if(isCid(targetCtx)){ return targetCtx }
720
+
721
+ let cidOrSelector = target.getAttribute(this.binding("target"))
722
+ if(isCid(cidOrSelector)){
723
+ return parseInt(cidOrSelector)
724
+ } else if(targetCtx && cidOrSelector !== null){
722
725
  return this.closestComponentID(targetCtx)
723
726
  } else {
724
727
  return null
@@ -1008,7 +1011,7 @@ export default class View {
1008
1011
  .map(form => {
1009
1012
  let newForm = template.content.querySelector(`form[id="${form.id}"][${phxChange}="${form.getAttribute(phxChange)}"]`)
1010
1013
  if(newForm){
1011
- return [form, newForm, this.componentID(newForm)]
1014
+ return [form, newForm, this.targetComponentID(newForm)]
1012
1015
  } else {
1013
1016
  return [form, null, null]
1014
1017
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_live_view",
3
- "version": "0.17.2",
3
+ "version": "0.17.3",
4
4
  "description": "The Phoenix LiveView JavaScript client.",
5
5
  "license": "MIT",
6
6
  "module": "./priv/static/phoenix_live_view.esm.js",
@@ -88,6 +88,7 @@ var COMPONENTS = "c";
88
88
  var EVENTS = "e";
89
89
  var REPLY = "r";
90
90
  var TITLE = "t";
91
+ var TEMPLATES = "p";
91
92
 
92
93
  // js/phoenix_live_view/entry_uploader.js
93
94
  var EntryUploader = class {
@@ -139,7 +140,10 @@ var EntryUploader = class {
139
140
 
140
141
  // js/phoenix_live_view/utils.js
141
142
  var logError = (msg, obj) => console.error && console.error(msg, obj);
142
- var isCid = (cid) => typeof cid === "number";
143
+ var isCid = (cid) => {
144
+ let type = typeof cid;
145
+ return type === "number" || type === "string" && /^(0|[1-9]\d*)$/.test(cid);
146
+ };
143
147
  function detectDuplicateIds() {
144
148
  let ids = new Set();
145
149
  let elems = document.querySelectorAll("*[id]");
@@ -1673,7 +1677,7 @@ var Rendered = class {
1673
1677
  recursiveToString(rendered, components = rendered[COMPONENTS], onlyCids) {
1674
1678
  onlyCids = onlyCids ? new Set(onlyCids) : null;
1675
1679
  let output = { buffer: "", components, onlyCids };
1676
- this.toOutputBuffer(rendered, output);
1680
+ this.toOutputBuffer(rendered, null, output);
1677
1681
  return output.buffer;
1678
1682
  }
1679
1683
  componentCIDs(diff) {
@@ -1699,8 +1703,8 @@ var Rendered = class {
1699
1703
  for (let cid in newc) {
1700
1704
  newc[cid] = this.cachedFindComponent(cid, newc[cid], oldc, newc, cache);
1701
1705
  }
1702
- for (var key in newc) {
1703
- oldc[key] = newc[key];
1706
+ for (let cid in newc) {
1707
+ oldc[cid] = newc[cid];
1704
1708
  }
1705
1709
  diff[COMPONENTS] = newc;
1706
1710
  }
@@ -1769,33 +1773,43 @@ var Rendered = class {
1769
1773
  isNewFingerprint(diff = {}) {
1770
1774
  return !!diff[STATIC];
1771
1775
  }
1772
- toOutputBuffer(rendered, output) {
1776
+ templateStatic(part, templates) {
1777
+ if (typeof part === "number") {
1778
+ return templates[part];
1779
+ } else {
1780
+ return part;
1781
+ }
1782
+ }
1783
+ toOutputBuffer(rendered, templates, output) {
1773
1784
  if (rendered[DYNAMICS]) {
1774
- return this.comprehensionToBuffer(rendered, output);
1785
+ return this.comprehensionToBuffer(rendered, templates, output);
1775
1786
  }
1776
1787
  let { [STATIC]: statics } = rendered;
1788
+ statics = this.templateStatic(statics, templates);
1777
1789
  output.buffer += statics[0];
1778
1790
  for (let i = 1; i < statics.length; i++) {
1779
- this.dynamicToBuffer(rendered[i - 1], output);
1791
+ this.dynamicToBuffer(rendered[i - 1], templates, output);
1780
1792
  output.buffer += statics[i];
1781
1793
  }
1782
1794
  }
1783
- comprehensionToBuffer(rendered, output) {
1795
+ comprehensionToBuffer(rendered, templates, output) {
1784
1796
  let { [DYNAMICS]: dynamics, [STATIC]: statics } = rendered;
1797
+ statics = this.templateStatic(statics, templates);
1798
+ let compTemplates = rendered[TEMPLATES];
1785
1799
  for (let d = 0; d < dynamics.length; d++) {
1786
1800
  let dynamic = dynamics[d];
1787
1801
  output.buffer += statics[0];
1788
1802
  for (let i = 1; i < statics.length; i++) {
1789
- this.dynamicToBuffer(dynamic[i - 1], output);
1803
+ this.dynamicToBuffer(dynamic[i - 1], compTemplates, output);
1790
1804
  output.buffer += statics[i];
1791
1805
  }
1792
1806
  }
1793
1807
  }
1794
- dynamicToBuffer(rendered, output) {
1808
+ dynamicToBuffer(rendered, templates, output) {
1795
1809
  if (typeof rendered === "number") {
1796
1810
  output.buffer += this.recursiveCIDToString(output.components, rendered, output.onlyCids);
1797
1811
  } else if (isObject(rendered)) {
1798
- this.toOutputBuffer(rendered, output);
1812
+ this.toOutputBuffer(rendered, templates, output);
1799
1813
  } else {
1800
1814
  output.buffer += rendered;
1801
1815
  }
@@ -1981,18 +1995,20 @@ var JS = {
1981
1995
  this.addOrRemoveClasses(sourceEl, [], names, transition, time, view);
1982
1996
  }
1983
1997
  },
1984
- exec_transition(eventType, phxEvent, view, sourceEl, { time, to, names }) {
1998
+ exec_transition(eventType, phxEvent, view, sourceEl, { time, to, transition }) {
1985
1999
  let els = to ? dom_default.all(document, to) : [sourceEl];
2000
+ let [transition_start, running, transition_end] = transition;
1986
2001
  els.forEach((el) => {
1987
- this.addOrRemoveClasses(el, names, []);
1988
- view.transition(time, () => this.addOrRemoveClasses(el, [], names));
2002
+ let onStart = () => this.addOrRemoveClasses(el, transition_start.concat(running), []);
2003
+ let onDone = () => this.addOrRemoveClasses(el, transition_end, transition_start.concat(running));
2004
+ view.transition(time, onStart, onDone);
1989
2005
  });
1990
2006
  },
1991
2007
  exec_toggle(eventType, phxEvent, view, sourceEl, { to, display, ins, outs, time }) {
1992
2008
  if (to) {
1993
- dom_default.all(document, to, (el) => this.toggle(eventType, view, el, display, ins || [], outs || [], time));
2009
+ dom_default.all(document, to, (el) => this.toggle(eventType, view, el, display, ins, outs, time));
1994
2010
  } else {
1995
- this.toggle(eventType, view, sourceEl, display, ins || [], outs || [], time);
2011
+ this.toggle(eventType, view, sourceEl, display, ins, outs, time);
1996
2012
  }
1997
2013
  },
1998
2014
  exec_show(eventType, phxEvent, view, sourceEl, { to, display, transition, time }) {
@@ -2010,37 +2026,45 @@ var JS = {
2010
2026
  }
2011
2027
  },
2012
2028
  show(eventType, view, el, display, transition, time) {
2013
- let isVisible = this.isVisible(el);
2014
- if (transition.length > 0 && !isVisible) {
2015
- this.toggle(eventType, view, el, display, transition, [], time);
2016
- } else if (!isVisible) {
2017
- this.toggle(eventType, view, el, display, [], [], null);
2029
+ if (!this.isVisible(el)) {
2030
+ this.toggle(eventType, view, el, display, transition, null, time);
2018
2031
  }
2019
2032
  },
2020
2033
  hide(eventType, view, el, display, transition, time) {
2021
- let isVisible = this.isVisible(el);
2022
- if (transition.length > 0 && isVisible) {
2023
- this.toggle(eventType, view, el, display, [], transition, time);
2024
- } else if (isVisible) {
2025
- this.toggle(eventType, view, el, display, [], [], time);
2034
+ if (this.isVisible(el)) {
2035
+ this.toggle(eventType, view, el, display, null, transition, time);
2026
2036
  }
2027
2037
  },
2028
- toggle(eventType, view, el, display, in_classes, out_classes, time) {
2029
- if (in_classes.length > 0 || out_classes.length > 0) {
2038
+ toggle(eventType, view, el, display, ins, outs, time) {
2039
+ let [inClasses, inStartClasses, inEndClasses] = ins || [[], [], []];
2040
+ let [outClasses, outStartClasses, outEndClasses] = outs || [[], [], []];
2041
+ if (inClasses.length > 0 || outClasses.length > 0) {
2030
2042
  if (this.isVisible(el)) {
2031
- this.addOrRemoveClasses(el, out_classes, in_classes);
2032
- view.transition(time, () => {
2043
+ let onStart = () => {
2044
+ this.addOrRemoveClasses(el, outStartClasses, inClasses.concat(inStartClasses).concat(inEndClasses));
2045
+ window.requestAnimationFrame(() => {
2046
+ this.addOrRemoveClasses(el, outClasses, []);
2047
+ window.requestAnimationFrame(() => this.addOrRemoveClasses(el, outEndClasses, outStartClasses));
2048
+ });
2049
+ };
2050
+ view.transition(time, onStart, () => {
2051
+ this.addOrRemoveClasses(el, [], outClasses.concat(outEndClasses));
2033
2052
  dom_default.putSticky(el, "toggle", (currentEl) => currentEl.style.display = "none");
2034
- this.addOrRemoveClasses(el, [], out_classes);
2035
2053
  });
2036
2054
  } else {
2037
2055
  if (eventType === "remove") {
2038
2056
  return;
2039
2057
  }
2040
- this.addOrRemoveClasses(el, in_classes, out_classes);
2041
- dom_default.putSticky(el, "toggle", (currentEl) => currentEl.style.display = display || "block");
2042
- view.transition(time, () => {
2043
- this.addOrRemoveClasses(el, [], in_classes);
2058
+ let onStart = () => {
2059
+ this.addOrRemoveClasses(el, inStartClasses, outClasses.concat(outStartClasses).concat(outEndClasses));
2060
+ dom_default.putSticky(el, "toggle", (currentEl) => currentEl.style.display = display || "block");
2061
+ window.requestAnimationFrame(() => {
2062
+ this.addOrRemoveClasses(el, inClasses, []);
2063
+ window.requestAnimationFrame(() => this.addOrRemoveClasses(el, inEndClasses, inStartClasses));
2064
+ });
2065
+ };
2066
+ view.transition(time, onStart, () => {
2067
+ this.addOrRemoveClasses(el, [], inClasses.concat(inEndClasses));
2044
2068
  });
2045
2069
  }
2046
2070
  } else {
@@ -2049,9 +2073,11 @@ var JS = {
2049
2073
  }
2050
2074
  },
2051
2075
  addOrRemoveClasses(el, adds, removes, transition, time, view) {
2052
- if (transition && transition.length > 0) {
2053
- this.addOrRemoveClasses(el, transition, []);
2054
- return view.transition(time, () => this.addOrRemoveClasses(el, adds, removes.concat(transition)));
2076
+ let [transition_run, transition_start, transition_end] = transition || [[], [], []];
2077
+ if (transition_run.length > 0) {
2078
+ let onStart = () => this.addOrRemoveClasses(el, transition_start.concat(transition_run), []);
2079
+ let onDone = () => this.addOrRemoveClasses(el, adds.concat(transition_end), removes.concat(transition_run).concat(transition_start));
2080
+ return view.transition(time, onStart, onDone);
2055
2081
  }
2056
2082
  window.requestAnimationFrame(() => {
2057
2083
  let [prevAdds, prevRemoves] = dom_default.getSticky(el, "classes", [[], []]);
@@ -2073,8 +2099,8 @@ var JS = {
2073
2099
  let style = window.getComputedStyle(el);
2074
2100
  return !(style.opacity === 0 || style.display === "none");
2075
2101
  },
2076
- isToggledOut(el, out_classes) {
2077
- return !this.isVisible(el) || this.hasAllClasses(el, out_classes);
2102
+ isToggledOut(el, outClasses) {
2103
+ return !this.isVisible(el) || this.hasAllClasses(el, outClasses);
2078
2104
  }
2079
2105
  };
2080
2106
  var js_default = JS;
@@ -2215,15 +2241,15 @@ var View = class {
2215
2241
  log(kind, msgCallback) {
2216
2242
  this.liveSocket.log(this, kind, msgCallback);
2217
2243
  }
2218
- transition(time, onDone = function() {
2244
+ transition(time, onStart, onDone = function() {
2219
2245
  }) {
2220
- this.liveSocket.transition(time, onDone);
2246
+ this.liveSocket.transition(time, onStart, onDone);
2221
2247
  }
2222
2248
  withinTargets(phxTarget, callback) {
2223
2249
  if (phxTarget instanceof HTMLElement || phxTarget instanceof SVGElement) {
2224
2250
  return this.liveSocket.owner(phxTarget, (view) => callback(view, phxTarget));
2225
2251
  }
2226
- if (typeof phxTarget === "number" || /^(0|[1-9]\d*)$/.test(phxTarget)) {
2252
+ if (isCid(phxTarget)) {
2227
2253
  let targets = dom_default.findComponentNodeList(this.el, phxTarget);
2228
2254
  if (targets.length === 0) {
2229
2255
  logError(`no component found matching phx-target of ${phxTarget}`);
@@ -2727,7 +2753,11 @@ var View = class {
2727
2753
  targetComponentID(target, targetCtx) {
2728
2754
  if (isCid(targetCtx)) {
2729
2755
  return targetCtx;
2730
- } else if (target.getAttribute(this.binding("target"))) {
2756
+ }
2757
+ let cidOrSelector = target.getAttribute(this.binding("target"));
2758
+ if (isCid(cidOrSelector)) {
2759
+ return parseInt(cidOrSelector);
2760
+ } else if (targetCtx && cidOrSelector !== null) {
2731
2761
  return this.closestComponentID(targetCtx);
2732
2762
  } else {
2733
2763
  return null;
@@ -3003,7 +3033,7 @@ var View = class {
3003
3033
  return dom_default.all(this.el, `form[${phxChange}]`).filter((form) => form.id && this.ownsElement(form)).filter((form) => form.elements.length > 0).filter((form) => form.getAttribute(this.binding(PHX_AUTO_RECOVER)) !== "ignore").map((form) => {
3004
3034
  let newForm = template.content.querySelector(`form[id="${form.id}"][${phxChange}="${form.getAttribute(phxChange)}"]`);
3005
3035
  if (newForm) {
3006
- return [form, newForm, this.componentID(newForm)];
3036
+ return [form, newForm, this.targetComponentID(newForm)];
3007
3037
  } else {
3008
3038
  return [form, null, null];
3009
3039
  }
@@ -3165,9 +3195,9 @@ var LiveSocket = class {
3165
3195
  requestDOMUpdate(callback) {
3166
3196
  this.transitions.after(callback);
3167
3197
  }
3168
- transition(time, onDone = function() {
3198
+ transition(time, onStart, onDone = function() {
3169
3199
  }) {
3170
- this.transitions.addTransition(time, onDone);
3200
+ this.transitions.addTransition(time, onStart, onDone);
3171
3201
  }
3172
3202
  onChannel(channel, event, cb) {
3173
3203
  channel.on(event, (data) => {
@@ -3303,7 +3333,7 @@ var LiveSocket = class {
3303
3333
  return view;
3304
3334
  }
3305
3335
  owner(childEl, callback) {
3306
- let view = maybe(childEl.closest(PHX_VIEW_SELECTOR), (el) => this.getViewByEl(el));
3336
+ let view = maybe(childEl.closest(PHX_VIEW_SELECTOR), (el) => this.getViewByEl(el)) || this.main;
3307
3337
  if (view) {
3308
3338
  callback(view);
3309
3339
  }
@@ -3713,7 +3743,8 @@ var TransitionSet = class {
3713
3743
  this.pushPendingOp(callback);
3714
3744
  }
3715
3745
  }
3716
- addTransition(time, onDone) {
3746
+ addTransition(time, onStart, onDone) {
3747
+ onStart();
3717
3748
  let timer = setTimeout(() => {
3718
3749
  this.transitions.delete(timer);
3719
3750
  onDone();