-
Notifications
You must be signed in to change notification settings - Fork 959
/
Copy pathphoenix_live_view.ex
2228 lines (1712 loc) · 75.6 KB
/
phoenix_live_view.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
defmodule Phoenix.LiveView do
@moduledoc ~S'''
A LiveView is a process that receives events, updates
its state, and renders updates to a page as diffs.
To get started, see [the Welcome guide](welcome.md).
This module provides advanced documentation and features
about using LiveView.
## Life-cycle
A LiveView begins as a regular HTTP request and HTML response,
and then upgrades to a stateful view on client connect,
guaranteeing a regular HTML page even if JavaScript is disabled.
Any time a stateful view changes or updates its socket assigns, it is
automatically re-rendered and the updates are pushed to the client.
Socket assigns are stateful values kept on the server side in
`Phoenix.LiveView.Socket`. This is different from the common stateless
HTTP pattern of sending the connection state to the client in the form
of a token or cookie and rebuilding the state on the server to service
every request.
You begin by rendering a LiveView typically from your router.
When LiveView is first rendered, the `c:mount/3` callback is invoked
with the current params, the current session and the LiveView socket.
As in a regular request, `params` contains public data that can be
modified by the user. The `session` always contains private data set
by the application itself. The `c:mount/3` callback wires up socket
assigns necessary for rendering the view. After mounting, `c:handle_params/3`
is invoked so uri and query params are handled. Finally, `c:render/1`
is invoked and the HTML is sent as a regular HTML response to the
client.
After rendering the static page, LiveView connects from the client
to the server where stateful views are spawned to push rendered updates
to the browser, and receive client events via `phx-` bindings. Just like
the first rendering, `c:mount/3`, is invoked with params, session,
and socket state. However in the connected client case, a LiveView process
is spawned on the server, runs `c:handle_params/3` again and then pushes
the result of `c:render/1` to the client and continues on for the duration
of the connection. If at any point during the stateful life-cycle a crash
is encountered, or the client connection drops, the client gracefully
reconnects to the server, calling `c:mount/3` and `c:handle_params/3` again.
LiveView also allows attaching hooks to specific life-cycle stages with
`attach_hook/4`.
## Template collocation
There are two possible ways of rendering content in a LiveView. The first
one is by explicitly defining a render function, which receives `assigns`
and returns a `HEEx` template defined with [the `~H` sigil](`Phoenix.Component.sigil_H/2`).
defmodule MyAppWeb.DemoLive do
# In a typical Phoenix app, the following line would usually be `use MyAppWeb, :live_view`
use Phoenix.LiveView
def render(assigns) do
~H"""
Hello world!
"""
end
end
For larger templates, you can place them in a file in the same directory
and same name as the LiveView. For example, if the file above is placed
at `lib/my_app_web/live/demo_live.ex`, you can also remove the
`render/1` function altogether and put the template code at
`lib/my_app_web/live/demo_live.html.heex`.
## Async Operations
Performing asynchronous work is common in LiveViews and LiveComponents.
It allows the user to get a working UI quickly while the system fetches some
data in the background or talks to an external service, without blocking the
render or event handling. For async work, you also typically need to handle
the different states of the async operation, such as loading, error, and the
successful result. You also want to catch any errors or exits and translate it
to a meaningful update in the UI rather than crashing the user experience.
### Async assigns
The `assign_async/3` function takes the socket, a key or list of keys which will be assigned
asynchronously, and a function. This function will be wrapped in a `task` by
`assign_async`, making it easy for you to return the result. This function must
return an `{:ok, assigns}` or `{:error, reason}` tuple, where `assigns` is a map
of the keys passed to `assign_async`.
If the function returns anything else, an error is raised.
The task is only started when the socket is connected.
For example, let's say we want to async fetch a user's organization from the database,
as well as their profile and rank:
def mount(%{"slug" => slug}, _, socket) do
{:ok,
socket
|> assign(:foo, "bar")
|> assign_async(:org, fn -> {:ok, %{org: fetch_org!(slug)}} end)
|> assign_async([:profile, :rank], fn -> {:ok, %{profile: ..., rank: ...}} end)}
end
> ### Warning {: .warning}
>
> When using async operations it is important to not pass the socket into the function
> as it will copy the whole socket struct to the Task process, which can be very expensive.
>
> Instead of:
>
> ```elixir
> assign_async(:org, fn -> {:ok, %{org: fetch_org(socket.assigns.slug)}} end)
> ```
>
> We should do:
>
> ```elixir
> slug = socket.assigns.slug
> assign_async(:org, fn -> {:ok, %{org: fetch_org(slug)}} end)
> ```
>
> See: https://hexdocs.pm/elixir/process-anti-patterns.html#sending-unnecessary-data
The state of the async operation is stored in the socket assigns within an
`Phoenix.LiveView.AsyncResult`. It carries the loading and failed states, as
well as the result. For example, if we wanted to show the loading states in
the UI for the `:org`, our template could conditionally render the states:
```heex
<div :if={@org.loading}>Loading organization...</div>
<div :if={org = @org.ok? && @org.result}>{org.name} loaded!</div>
```
The `Phoenix.Component.async_result/1` function component can also be used to
declaratively render the different states using slots:
```heex
<.async_result :let={org} assign={@org}>
<:loading>Loading organization...</:loading>
<:failed :let={_failure}>there was an error loading the organization</:failed>
{org.name}
</.async_result>
```
### Arbitrary async operations
Sometimes you need lower level control of asynchronous operations, while
still receiving process isolation and error handling. For this, you can use
`start_async/3` and the `Phoenix.LiveView.AsyncResult` module directly:
def mount(%{"id" => id}, _, socket) do
{:ok,
socket
|> assign(:org, AsyncResult.loading())
|> start_async(:my_task, fn -> fetch_org!(id) end)}
end
def handle_async(:my_task, {:ok, fetched_org}, socket) do
%{org: org} = socket.assigns
{:noreply, assign(socket, :org, AsyncResult.ok(org, fetched_org))}
end
def handle_async(:my_task, {:exit, reason}, socket) do
%{org: org} = socket.assigns
{:noreply, assign(socket, :org, AsyncResult.failed(org, {:exit, reason}))}
end
`start_async/3` is used to fetch the organization asynchronously. The
`c:handle_async/3` callback is called when the task completes or exits,
with the results wrapped in either `{:ok, result}` or `{:exit, reason}`.
The `AsyncResult` module provides functions to update the state of the
async operation, but you can also assign any value directly to the socket
if you want to handle the state yourself.
## Endpoint configuration
LiveView accepts the following configuration in your endpoint under
the `:live_view` key:
* `:signing_salt` (required) - the salt used to sign data sent
to the client
* `:hibernate_after` (optional) - the idle time in milliseconds allowed in
the LiveView before compressing its own memory and state.
Defaults to 15000ms (15 seconds)
'''
alias Phoenix.LiveView.{Socket, LiveStream, Async}
@type unsigned_params :: map
@doc """
The LiveView entry-point.
For each LiveView in the root of a template, `c:mount/3` is invoked twice:
once to do the initial page load and again to establish the live socket.
It expects three arguments:
* `params` - a map of string keys which contain public information that
can be set by the user. The map contains the query params as well as any
router path parameter. If the LiveView was not mounted at the router,
this argument is the atom `:not_mounted_at_router`
* `session` - the connection session
* `socket` - the LiveView socket
It must return either `{:ok, socket}` or `{:ok, socket, options}`, where
`options` is one of:
* `:temporary_assigns` - a keyword list of assigns that are temporary
and must be reset to their value after every render. Note that once
the value is reset, it won't be re-rendered again until it is explicitly
assigned
* `:layout` - the optional layout to be used by the LiveView. Setting
this option will override any layout previously set via
`Phoenix.LiveView.Router.live_session/2` or on `use Phoenix.LiveView`
"""
@callback mount(
params :: unsigned_params() | :not_mounted_at_router,
session :: map,
socket :: Socket.t()
) ::
{:ok, Socket.t()} | {:ok, Socket.t(), keyword()}
@doc """
Renders a template.
This callback is invoked whenever LiveView detects
new content must be rendered and sent to the client.
If you define this function, it must return a template
defined via the `Phoenix.Component.sigil_H/2`.
If you don't define this function, LiveView will attempt
to render a template in the same directory as your LiveView.
For example, if you have a LiveView named `MyApp.MyCustomView`
inside `lib/my_app/live_views/my_custom_view.ex`, Phoenix
will look for a template at `lib/my_app/live_views/my_custom_view.html.heex`.
"""
@callback render(assigns :: Socket.assigns()) :: Phoenix.LiveView.Rendered.t()
@doc """
Invoked when the LiveView is terminating.
In case of errors, this callback is only invoked if the LiveView
is trapping exits. See `c:GenServer.terminate/2` for more info.
"""
@callback terminate(reason, socket :: Socket.t()) :: term
when reason: :normal | :shutdown | {:shutdown, :left | :closed | term}
@doc """
Invoked after mount and whenever there is a live patch event.
It receives the current `params`, including parameters from
the router, the current `uri` from the client and the `socket`.
It is invoked after mount or whenever there is a live navigation
event caused by `push_patch/2` or `<.link patch={...}>`.
It must always return `{:noreply, socket}`, where `:noreply`
means no additional information is sent to the client.
> #### Note {: .warning}
>
> `handle_params` is only allowed on LiveViews mounted at the router,
> as it takes the current url of the page as the second parameter.
"""
@callback handle_params(unsigned_params(), uri :: String.t(), socket :: Socket.t()) ::
{:noreply, Socket.t()}
@doc """
Invoked to handle events sent by the client.
It receives the `event` name, the event payload as a map,
and the socket.
It must return `{:noreply, socket}`, where `:noreply` means
no additional information is sent to the client, or
`{:reply, map(), socket}`, where the given `map()` is encoded
and sent as a reply to the client.
"""
@callback handle_event(event :: binary, unsigned_params(), socket :: Socket.t()) ::
{:noreply, Socket.t()} | {:reply, map, Socket.t()}
@doc """
Invoked to handle calls from other Elixir processes.
See `GenServer.call/3` and `c:GenServer.handle_call/3`
for more information.
"""
@callback handle_call(msg :: term, {pid, reference}, socket :: Socket.t()) ::
{:noreply, Socket.t()} | {:reply, term, Socket.t()}
@doc """
Invoked to handle casts from other Elixir processes.
See `GenServer.cast/2` and `c:GenServer.handle_cast/2`
for more information. It must always return `{:noreply, socket}`,
where `:noreply` means no additional information is sent
to the process which cast the message.
"""
@callback handle_cast(msg :: term, socket :: Socket.t()) ::
{:noreply, Socket.t()}
@doc """
Invoked to handle messages from other Elixir processes.
See `Kernel.send/2` and `c:GenServer.handle_info/2`
for more information. It must always return `{:noreply, socket}`,
where `:noreply` means no additional information is sent
to the process which sent the message.
"""
@callback handle_info(msg :: term, socket :: Socket.t()) ::
{:noreply, Socket.t()}
@doc """
Invoked when the result of an `start_async/3` operation is available.
For a deeper understanding of using this callback,
refer to the ["Arbitrary async operations"](#module-arbitrary-async-operations) section.
"""
@callback handle_async(
name :: term,
async_fun_result :: {:ok, term} | {:exit, term},
socket :: Socket.t()
) ::
{:noreply, Socket.t()}
@optional_callbacks mount: 3,
render: 1,
terminate: 2,
handle_params: 3,
handle_event: 3,
handle_call: 3,
handle_info: 2,
handle_cast: 2,
handle_async: 3
@doc """
Uses LiveView in the current module to mark it a LiveView.
use Phoenix.LiveView,
container: {:tr, class: "colorized"},
layout: {MyAppWeb.Layouts, :app},
log: :info
## Options
* `:container` - an optional tuple for the HTML tag and DOM attributes to
be used for the LiveView container. For example: `{:li, style: "color: blue;"}`.
See `Phoenix.Component.live_render/3` for more information and examples.
* `:global_prefixes` - the global prefixes to use for components. See
`Global Attributes` in `Phoenix.Component` for more information.
* `:layout` - configures the layout the LiveView will be rendered in.
This layout can be overridden by on `c:mount/3` or via the `:layout`
option in `Phoenix.LiveView.Router.live_session/2`
* `:log` - configures the log level for the LiveView, either `false`
or a log level
"""
defmacro __using__(opts) do
# Expand layout if possible to avoid compile-time dependencies
opts =
with true <- Keyword.keyword?(opts),
{layout, template} <- Keyword.get(opts, :layout) do
layout = Macro.expand(layout, %{__CALLER__ | function: {:__live__, 0}})
Keyword.replace!(opts, :layout, {layout, template})
else
_ -> opts
end
quote bind_quoted: [opts: opts] do
import Phoenix.LiveView
@behaviour Phoenix.LiveView
@before_compile Phoenix.LiveView.Renderer
@phoenix_live_opts opts
Module.register_attribute(__MODULE__, :phoenix_live_mount, accumulate: true)
@before_compile Phoenix.LiveView
# Phoenix.Component must come last so its @before_compile runs last
use Phoenix.Component, Keyword.take(opts, [:global_prefixes])
end
end
defmacro __before_compile__(env) do
opts = Module.get_attribute(env.module, :phoenix_live_opts)
on_mount =
env.module
|> Module.get_attribute(:phoenix_live_mount)
|> Enum.reverse()
live = Phoenix.LiveView.__live__([on_mount: on_mount] ++ opts)
quote do
@doc false
def __live__ do
unquote(Macro.escape(live))
end
end
end
@doc """
Defines metadata for a LiveView.
This must be returned from the `__live__` callback.
It accepts:
* `:container` - an optional tuple for the HTML tag and DOM attributes to
be used for the LiveView container. For example: `{:li, style: "color: blue;"}`.
* `:layout` - configures the layout the LiveView will be rendered in.
This layout can be overridden by on `c:mount/3` or via the `:layout`
option in `Phoenix.LiveView.Router.live_session/2`
* `:log` - configures the log level for the LiveView, either `false`
or a log level
* `:on_mount` - a list of tuples with module names and argument to be invoked
as `on_mount` hooks
"""
def __live__(opts \\ []) do
on_mount = opts[:on_mount] || []
layout =
Phoenix.LiveView.Utils.normalize_layout(Keyword.get(opts, :layout, false))
log =
case Keyword.fetch(opts, :log) do
{:ok, false} -> false
{:ok, log} when is_atom(log) -> log
:error -> :debug
_ -> raise ArgumentError, ":log expects an atom or false, got: #{inspect(opts[:log])}"
end
container = opts[:container] || {:div, []}
%{
container: container,
kind: :view,
layout: layout,
lifecycle: Phoenix.LiveView.Lifecycle.build(on_mount),
log: log
}
end
@doc """
Declares a module callback to be invoked on the LiveView's mount.
The function within the given module, which must be named `on_mount`,
will be invoked before both disconnected and connected mounts. The hook
has the option to either halt or continue the mounting process as usual.
If you wish to redirect the LiveView, you **must** halt, otherwise an error
will be raised.
Tip: if you need to define multiple `on_mount` callbacks, avoid defining
multiple modules. Instead, pass a tuple and use pattern matching to handle
different cases:
def on_mount(:admin, _params, _session, socket) do
{:cont, socket}
end
def on_mount(:user, _params, _session, socket) do
{:cont, socket}
end
And then invoke it as:
on_mount {MyAppWeb.SomeHook, :admin}
on_mount {MyAppWeb.SomeHook, :user}
Registering `on_mount` hooks can be useful to perform authentication
as well as add custom behaviour to other callbacks via `attach_hook/4`.
The `on_mount` callback can return a keyword list of options as a third
element in the return tuple. These options are identical to what can
optionally be returned in `c:mount/3`.
## Examples
The following is an example of attaching a hook via
`Phoenix.LiveView.Router.live_session/3`:
# lib/my_app_web/live/init_assigns.ex
defmodule MyAppWeb.InitAssigns do
@moduledoc "\""
Ensures common `assigns` are applied to all LiveViews attaching this hook.
"\""
import Phoenix.LiveView
import Phoenix.Component
def on_mount(:default, _params, _session, socket) do
{:cont, assign(socket, :page_title, "DemoWeb")}
end
def on_mount(:user, params, session, socket) do
# code
end
def on_mount(:admin, _params, _session, socket) do
{:cont, socket, layout: {DemoWeb.Layouts, :admin}}
end
end
# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
# pipelines, plugs, etc.
live_session :default, on_mount: MyAppWeb.InitAssigns do
scope "/", MyAppWeb do
pipe_through :browser
live "/", PageLive, :index
end
end
live_session :authenticated, on_mount: {MyAppWeb.InitAssigns, :user} do
scope "/", MyAppWeb do
pipe_through [:browser, :require_user]
live "/profile", UserLive.Profile, :index
end
end
live_session :admins, on_mount: {MyAppWeb.InitAssigns, :admin} do
scope "/admin", MyAppWeb.Admin do
pipe_through [:browser, :require_user, :require_admin]
live "/", AdminLive.Index, :index
end
end
end
"""
defmacro on_mount(mod_or_mod_arg) do
caller = %{__CALLER__ | function: {:on_mount, 1}}
# While we could pass `mod_or_mod_arg` as a whole to
# expand_literals, we want to also be able to expand only
# the first element, even if the second element is not a literal.
mod_or_mod_arg =
case mod_or_mod_arg do
{mod, arg} ->
{Macro.expand_literals(mod, caller), Macro.expand_literals(arg, caller)}
mod_or_mod_arg ->
Macro.expand_literals(mod_or_mod_arg, caller)
end
quote do
Module.put_attribute(
__MODULE__,
:phoenix_live_mount,
Phoenix.LiveView.Lifecycle.validate_on_mount!(__MODULE__, unquote(mod_or_mod_arg))
)
end
end
@doc """
Returns true if the socket is connected.
Useful for checking the connectivity status when mounting the view.
For example, on initial page render, the view is mounted statically,
rendered, and the HTML is sent to the client. Once the client
connects to the server, a LiveView is then spawned and mounted
statefully within a process. Use `connected?/1` to conditionally
perform stateful work, such as subscribing to pubsub topics,
sending messages, etc.
## Examples
defmodule DemoWeb.ClockLive do
use Phoenix.LiveView
...
def mount(_params, _session, socket) do
if connected?(socket), do: :timer.send_interval(1000, self(), :tick)
{:ok, assign(socket, date: :calendar.local_time())}
end
def handle_info(:tick, socket) do
{:noreply, assign(socket, date: :calendar.local_time())}
end
end
"""
def connected?(%Socket{transport_pid: transport_pid}), do: transport_pid != nil
@doc """
Configures which function to use to render a LiveView/LiveComponent.
By default, LiveView invokes the `render/1` function in the same module
the LiveView/LiveComponent is defined, passing `assigns` as its sole
argument. This function allows you to set a different rendering function.
One possible use case for this function is to set a different template
on disconnected render. When the user first accesses a LiveView, we will
perform a disconnected render to send to the browser. This is useful for
several reasons, such as reducing the time to first paint and for search
engine indexing.
However, when LiveView is gated behind an authentication page, it may be
useful to render a placeholder on disconnected render and perform the
full render once the WebSocket connects. This can be achieved with
`render_with/2` and is particularly useful on complex pages (such as
dashboards and reports).
To do so, you must simply invoke `render_with(socket, &some_function_component/1)`,
configuring your socket with a new rendering function.
"""
def render_with(%Socket{} = socket, component) when is_function(component, 1) do
put_in(socket.private[:render_with], component)
end
@doc """
Puts a new private key and value in the socket.
Privates are *not change tracked*. This storage is meant to be used by
users and libraries to hold state that doesn't require
change tracking. The keys should be prefixed with the app/library name.
## Examples
Key values can be placed in private:
put_private(socket, :myapp_meta, %{foo: "bar"})
And then retrieved:
socket.private[:myapp_meta]
"""
@reserved_privates ~w(
connect_params
connect_info
assign_new
live_async
live_layout
live_temp
lifecycle
render_with
root_view
)a
def put_private(%Socket{} = socket, key, value) when key not in @reserved_privates do
%{socket | private: Map.put(socket.private, key, value)}
end
def put_private(%Socket{}, bad_key, _value) do
raise ArgumentError, "cannot set reserved private key #{inspect(bad_key)}"
end
@doc """
Adds a flash message to the socket to be displayed.
*Note*: While you can use `put_flash/3` inside a `Phoenix.LiveComponent`,
components have their own `@flash` assigns. The `@flash` assign
in a component is only copied to its parent LiveView if the component
calls `push_navigate/2` or `push_patch/2`.
*Note*: You must also place the `Phoenix.LiveView.Router.fetch_live_flash/2`
plug in your browser's pipeline in place of `fetch_flash` for LiveView flash
messages be supported, for example:
import Phoenix.LiveView.Router
pipeline :browser do
...
plug :fetch_live_flash
end
In a typical LiveView application, the message will be rendered by the CoreComponents’ flash/1 component.
It is up to this function to determine what kind of messages it supports.
By default, the `:info` and `:error` kinds are handled.
## Examples
iex> put_flash(socket, :info, "It worked!")
iex> put_flash(socket, :error, "You can't access that page")
"""
defdelegate put_flash(socket, kind, msg), to: Phoenix.LiveView.Utils
@doc """
Clears the flash.
## Examples
iex> clear_flash(socket)
Clearing the flash can also be triggered on the client and natively handled by LiveView using the `lv:clear-flash` event.
For example:
```heex
<p class="alert" phx-click="lv:clear-flash">
{Phoenix.Flash.get(@flash, :info)}
</p>
```
"""
defdelegate clear_flash(socket), to: Phoenix.LiveView.Utils
@doc """
Clears a key from the flash.
## Examples
iex> clear_flash(socket, :info)
Clearing the flash can also be triggered on the client and natively handled by LiveView using the `lv:clear-flash` event.
For example:
```heex
<p class="alert" phx-click="lv:clear-flash" phx-value-key="info">
{Phoenix.Flash.get(@flash, :info)}
</p>
```
"""
defdelegate clear_flash(socket, key), to: Phoenix.LiveView.Utils
@doc """
Pushes an event to the client.
Events can be handled in two ways:
1. They can be handled on `window` via `addEventListener`.
A "phx:" prefix will be added to the event name.
2. They can be handled inside a hook via `handleEvent`.
Events are dispatched to all active hooks on the client who are
handling the given `event`. If you need to scope events, then
this must be done by namespacing them.
Events pushed during `push_navigate` are currently discarded,
as the LiveView is immediately dismounted.
## Hook example
If you push a "scores" event from your LiveView:
{:noreply, push_event(socket, "scores", %{points: 100, user: "josé"})}
A hook declared via `phx-hook` can handle it via `handleEvent`:
```javascript
this.handleEvent("scores", data => ...)
```
## `window` example
All events are also dispatched on the `window`. This means you can handle
them by adding listeners. For example, if you want to remove an element
from the page, you can do this:
{:noreply, push_event(socket, "remove-el", %{id: "foo-bar"})}
And now in your app.js you can register and handle it:
```javascript
window.addEventListener(
"phx:remove-el",
e => document.getElementById(e.detail.id).remove()
)
```
"""
defdelegate push_event(socket, event, payload), to: Phoenix.LiveView.Utils
@doc ~S"""
Allows an upload for the provided name.
## Options
* `:accept` - Required. A list of unique file extensions (such as ".jpeg") or
mime type (such as "image/jpeg" or "image/*"). You may also pass the atom
`:any` instead of a list to support to allow any kind of file.
For example, `[".jpeg"]`, `:any`, etc.
* `:max_entries` - The maximum number of selected files to allow per
file input. Defaults to 1.
* `:max_file_size` - The maximum file size in bytes to allow to be uploaded.
Defaults 8MB. For example, `12_000_000`.
* `:chunk_size` - The chunk size in bytes to send when uploading.
Defaults `64_000`.
* `:chunk_timeout` - The time in milliseconds to wait before closing the
upload channel when a new chunk has not been received. Defaults to `10_000`.
* `:external` - A 2-arity function for generating metadata for external
client uploaders. This function must return either `{:ok, meta, socket}`
or `{:error, meta, socket}` where meta is a map. See the Uploads section
for example usage.
* `:progress` - An optional 3-arity function for receiving progress events.
* `:auto_upload` - Instructs the client to upload the file automatically
on file selection instead of waiting for form submits. Defaults to `false`.
* `:writer` - A module implementing the `Phoenix.LiveView.UploadWriter`
behaviour to use for writing the uploaded chunks. Defaults to writing to a
temporary file for consumption. See the `Phoenix.LiveView.UploadWriter` docs
for custom usage.
Raises when a previously allowed upload under the same name is still active.
## Examples
allow_upload(socket, :avatar, accept: ~w(.jpg .jpeg), max_entries: 2)
allow_upload(socket, :avatar, accept: :any)
For consuming files automatically as they are uploaded, you can pair `auto_upload: true` with
a custom progress function to consume the entries as they are completed. For example:
allow_upload(socket, :avatar, accept: :any, progress: &handle_progress/3, auto_upload: true)
defp handle_progress(:avatar, entry, socket) do
if entry.done? do
uploaded_file =
consume_uploaded_entry(socket, entry, fn %{} = meta ->
{:ok, ...}
end)
{:noreply, put_flash(socket, :info, "file #{uploaded_file.name} uploaded")}
else
{:noreply, socket}
end
end
"""
defdelegate allow_upload(socket, name, options), to: Phoenix.LiveView.Upload
@doc """
Revokes a previously allowed upload from `allow_upload/3`.
## Examples
disallow_upload(socket, :avatar)
"""
defdelegate disallow_upload(socket, name), to: Phoenix.LiveView.Upload
@doc """
Cancels an upload for the given entry.
## Examples
```heex
<%= for entry <- @uploads.avatar.entries do %>
...
<button phx-click="cancel-upload" phx-value-ref={entry.ref}>cancel</button>
<% end %>
```
def handle_event("cancel-upload", %{"ref" => ref}, socket) do
{:noreply, cancel_upload(socket, :avatar, ref)}
end
"""
defdelegate cancel_upload(socket, name, entry_ref), to: Phoenix.LiveView.Upload
@doc """
Returns the completed and in progress entries for the upload.
## Examples
case uploaded_entries(socket, :photos) do
{[_ | _] = completed, []} ->
# all entries are completed
{[], [_ | _] = in_progress} ->
# all entries are still in progress
end
"""
defdelegate uploaded_entries(socket, name), to: Phoenix.LiveView.Upload
@doc ~S"""
Consumes the uploaded entries.
Raises when there are still entries in progress.
Typically called when submitting a form to handle the
uploaded entries alongside the form data. For form submissions,
it is guaranteed that all entries have completed before the submit event
is invoked. Once entries are consumed, they are removed from the upload.
The function passed to consume may return a tagged tuple of the form
`{:ok, my_result}` to collect results about the consumed entries, or
`{:postpone, my_result}` to collect results, but postpone the file
consumption to be performed later.
A list of all `my_result` values produced by the passed function is
returned, regardless of whether they were consumed or postponed.
## Examples
def handle_event("save", _params, socket) do
uploaded_files =
consume_uploaded_entries(socket, :avatar, fn %{path: path}, _entry ->
dest = Path.join("priv/static/uploads", Path.basename(path))
File.cp!(path, dest)
{:ok, ~p"/uploads/#{Path.basename(dest)}"}
end)
{:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
end
"""
defdelegate consume_uploaded_entries(socket, name, func), to: Phoenix.LiveView.Upload
@doc ~S"""
Consumes an individual uploaded entry.
Raises when the entry is still in progress.
Typically called when submitting a form to handle the
uploaded entries alongside the form data. Once entries are consumed,
they are removed from the upload.
This is a lower-level feature than `consume_uploaded_entries/3` and useful
for scenarios where you want to consume entries as they are individually completed.
Like `consume_uploaded_entries/3`, the function passed to consume may return
a tagged tuple of the form `{:ok, my_result}` to collect results about the
consumed entries, or `{:postpone, my_result}` to collect results,
but postpone the file consumption to be performed later.
## Examples
def handle_event("save", _params, socket) do
case uploaded_entries(socket, :avatar) do
{[_|_] = entries, []} ->
uploaded_files = for entry <- entries do
consume_uploaded_entry(socket, entry, fn %{path: path} ->
dest = Path.join("priv/static/uploads", Path.basename(path))
File.cp!(path, dest)
{:ok, ~p"/uploads/#{Path.basename(dest)}"}
end)
end
{:noreply, update(socket, :uploaded_files, &(&1 ++ uploaded_files))}
_ ->
{:noreply, socket}
end
end
"""
defdelegate consume_uploaded_entry(socket, entry, func), to: Phoenix.LiveView.Upload
@doc """
Annotates the socket for redirect to a destination path.
*Note*: LiveView redirects rely on instructing client
to perform a `window.location` update on the provided
redirect location. The whole page will be reloaded and
all state will be discarded.
## Options
* `:to` - the path to redirect to. It must always be a local path
* `:status` - the HTTP status code to use for the redirect. Defaults to 302.
* `:external` - an external path to redirect to. Either a string
or `{scheme, url}` to redirect to a custom scheme
## Examples
{:noreply, redirect(socket, to: "/")}
{:noreply, redirect(socket, to: "/", status: 301)}
{:noreply, redirect(socket, external: "https://example.com")}
"""
def redirect(socket, opts \\ []) do
status = Keyword.get(opts, :status, 302)
cond do
Keyword.has_key?(opts, :to) ->
do_internal_redirect(socket, Keyword.fetch!(opts, :to), status)
Keyword.has_key?(opts, :external) ->
do_external_redirect(socket, Keyword.fetch!(opts, :external), status)
true ->
raise ArgumentError, "expected :to or :external option in redirect/2"
end
end
defp do_internal_redirect(%Socket{} = socket, url, redirect_status) do
validate_local_url!(url, "redirect/2")
put_redirect(socket, {:redirect, %{to: url, status: redirect_status}})
end
defp do_external_redirect(%Socket{} = socket, url, redirect_status) do
case url do
{scheme, rest} ->
put_redirect(
socket,
{:redirect, %{external: "#{scheme}:#{rest}", status: redirect_status}}
)