Skip to content

Commit 4b57e42

Browse files
committed
New Events and Utils modules moved from Surface
1 parent 268c69a commit 4b57e42

File tree

4 files changed

+372
-1
lines changed

4 files changed

+372
-1
lines changed

lib/surface/components/events.ex

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
defmodule Surface.Components.Events do
2+
@moduledoc false
3+
4+
defmacro __using__(_) do
5+
quote do
6+
@doc "Triggered when the component receives click"
7+
prop click, :event
8+
9+
@doc "Triggered when a click event happens outside of the element"
10+
prop click_away, :event
11+
12+
# TODO: Remove this when LV min is >= v0.20.15
13+
@doc "Triggered when the component captures click"
14+
prop capture_click, :event
15+
16+
@doc "Triggered when the component loses focus"
17+
prop blur, :event
18+
19+
@doc "Triggered when the component receives focus"
20+
prop focus, :event
21+
22+
@doc "Triggered when the page loses focus"
23+
prop window_blur, :event
24+
25+
@doc "Triggered when the page receives focus"
26+
prop window_focus, :event
27+
28+
@doc "Triggered when a key on the keyboard is pressed"
29+
prop keydown, :event
30+
31+
@doc "Triggered when a key on the keyboard is released"
32+
prop keyup, :event
33+
34+
@doc "Triggered when a key on the keyboard is pressed (window-level)"
35+
prop window_keydown, :event
36+
37+
@doc "Triggered when a key on the keyboard is released (window-level)"
38+
prop window_keyup, :event
39+
40+
@doc "List values that will be sent as part of the payload triggered by an event"
41+
prop values, :keyword, default: []
42+
end
43+
end
44+
end

lib/surface/components/utils.ex

+122
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
defmodule Surface.Components.Utils do
2+
@moduledoc false
3+
import Surface, only: [event_to_opts: 2]
4+
5+
@valid_uri_schemes [
6+
"http:",
7+
"https:",
8+
"ftp:",
9+
"ftps:",
10+
"mailto:",
11+
"news:",
12+
"irc:",
13+
"gopher:",
14+
"nntp:",
15+
"feed:",
16+
"telnet:",
17+
"mms:",
18+
"rtsp:",
19+
"svn:",
20+
"tel:",
21+
"fax:",
22+
"xmpp:"
23+
]
24+
25+
def valid_destination!(%URI{} = uri, context) do
26+
valid_destination!(URI.to_string(uri), context)
27+
end
28+
29+
def valid_destination!({:safe, to}, context) do
30+
{:safe, valid_string_destination!(IO.iodata_to_binary(to), context)}
31+
end
32+
33+
def valid_destination!({other, to}, _context) when is_atom(other) do
34+
[Atom.to_string(other), ?:, to]
35+
end
36+
37+
def valid_destination!(to, context) do
38+
valid_string_destination!(IO.iodata_to_binary(to), context)
39+
end
40+
41+
for scheme <- @valid_uri_schemes do
42+
def valid_string_destination!(unquote(scheme) <> _ = string, _context), do: string
43+
end
44+
45+
def valid_string_destination!(to, context) do
46+
if not match?("/" <> _, to) and String.contains?(to, ":") do
47+
raise ArgumentError, """
48+
unsupported scheme given to #{context}. In case you want to link to an
49+
unknown or unsafe scheme, such as javascript, use a tuple: {:javascript, rest}
50+
"""
51+
else
52+
to
53+
end
54+
end
55+
56+
def csrf_data(to, opts) do
57+
case Keyword.pop(opts, :csrf_token, true) do
58+
{csrf, opts} when is_binary(csrf) ->
59+
{[csrf: csrf], opts}
60+
61+
{true, opts} ->
62+
{[csrf: csrf_token(to)], opts}
63+
64+
{false, opts} ->
65+
{[], opts}
66+
end
67+
end
68+
69+
defp csrf_token(to) do
70+
{mod, fun, args} = Application.fetch_env!(:surface, :csrf_token_reader)
71+
apply(mod, fun, [to | args])
72+
end
73+
74+
def skip_csrf(opts) do
75+
Keyword.delete(opts, :csrf_token)
76+
end
77+
78+
def opts_to_phx_opts(opts) do
79+
for {key, value} <- opts do
80+
case key do
81+
:trigger_action -> {:"phx-trigger-action", value}
82+
_ -> {key, value}
83+
end
84+
end
85+
end
86+
87+
def events_to_opts(assigns) do
88+
[
89+
event_to_opts(assigns.capture_click, :"phx-capture-click"),
90+
event_to_opts(assigns.click, :"phx-click"),
91+
event_to_opts(assigns.click_away, :"phx-click-away"),
92+
event_to_opts(assigns.window_focus, :"phx-window-focus"),
93+
event_to_opts(assigns.window_blur, :"phx-window-blur"),
94+
event_to_opts(assigns.focus, :"phx-focus"),
95+
event_to_opts(assigns.blur, :"phx-blur"),
96+
event_to_opts(assigns.window_keyup, :"phx-window-keyup"),
97+
event_to_opts(assigns.window_keydown, :"phx-window-keydown"),
98+
event_to_opts(assigns.keyup, :"phx-keyup"),
99+
event_to_opts(assigns.keydown, :"phx-keydown"),
100+
values_to_opts(assigns.values)
101+
]
102+
|> List.flatten()
103+
end
104+
105+
defp values_to_opts([]) do
106+
[]
107+
end
108+
109+
defp values_to_opts(values) when is_list(values) do
110+
values_to_attrs(values)
111+
end
112+
113+
defp values_to_opts(_values) do
114+
[]
115+
end
116+
117+
defp values_to_attrs(values) when is_list(values) do
118+
for {key, value} <- values do
119+
{:"phx-value-#{key}", value}
120+
end
121+
end
122+
end

mix.lock

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
"plug_crypto": {:hex, :plug_crypto, "2.1.0", "f44309c2b06d249c27c8d3f65cfe08158ade08418cf540fd4f72d4d6863abb7b", [:mix], [], "hexpm", "131216a4b030b8f8ce0f26038bc4421ae60e4bb95c5cf5395e1421437824c4fa"},
3232
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
3333
"sourceror": {:hex, :sourceror, "1.0.3", "111711c147f4f1414c07a67b45ad0064a7a41569037355407eda635649507f1d", [:mix], [], "hexpm", "56c21ef146c00b51bc3bb78d1f047cb732d193256a7c4ba91eaf828d3ae826af"},
34-
"surface": {:git, "https://github.com/surface-ui/surface.git", "518a471189447ccea095fb34aa348d39c1fed690", []},
34+
"surface": {:git, "https://github.com/surface-ui/surface.git", "1f105ed5e00cfa58ff4f84bfa7f32256e5b728c4", []},
3535
"surface_catalogue": {:git, "https://github.com/surface-ui/surface_catalogue.git", "c3b79034a0d4a5b7378f1e9d510debd29e555f45", []},
3636
"telemetry": {:hex, :telemetry, "1.3.0", "fedebbae410d715cf8e7062c96a1ef32ec22e764197f70cda73d82778d61e7a2", [:rebar3], [], "hexpm", "7015fc8919dbe63764f4b4b87a95b7c0996bd539e0d499be6ec9d7f3875b79e6"},
3737
"websock": {:hex, :websock, "0.5.3", "2f69a6ebe810328555b6fe5c831a851f485e303a7c8ce6c5f675abeb20ebdadc", [:mix], [], "hexpm", "6105453d7fac22c712ad66fab1d45abdf049868f253cf719b625151460b8b453"},
+205
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
defmodule Surface.Components.EventsTest do
2+
@moduledoc """
3+
Tests available events in components.
4+
"""
5+
6+
use Surface.ConnCase, async: true
7+
8+
defmodule ComponentWithEvents do
9+
use Surface.Component
10+
use Surface.Components.Events
11+
12+
import Surface.Components.Utils, only: [events_to_opts: 1]
13+
14+
def render(assigns) do
15+
~F"""
16+
<div {...events_to_opts(assigns)} />
17+
"""
18+
end
19+
20+
def mount(_params, session, socket) do
21+
{:ok, assign(socket, changeset: session["changeset"])}
22+
end
23+
end
24+
25+
defmodule Parent do
26+
use Surface.LiveComponent
27+
28+
def render(assigns) do
29+
~F"""
30+
<div>
31+
<ComponentWithEvents click="my_click" />
32+
</div>
33+
"""
34+
end
35+
36+
def handle_event(_, _, socket) do
37+
{:noreply, socket}
38+
end
39+
end
40+
41+
# Click Events
42+
43+
test "click event with parent live view as target" do
44+
html =
45+
render_surface do
46+
~F"""
47+
<ComponentWithEvents click="my_click" />
48+
"""
49+
end
50+
51+
assert html =~ """
52+
<div phx-click="my_click"></div>
53+
"""
54+
end
55+
56+
test "click away event with parent live view as target" do
57+
html =
58+
render_surface do
59+
~F"""
60+
<ComponentWithEvents click_away="my_click_away" />
61+
"""
62+
end
63+
64+
assert html =~ """
65+
<div phx-click-away="my_click_away"></div>
66+
"""
67+
end
68+
69+
# Focus Events
70+
71+
test "blur event with parent live view as target" do
72+
html =
73+
render_surface do
74+
~F"""
75+
<ComponentWithEvents blur="my_blur" />
76+
"""
77+
end
78+
79+
assert html =~ """
80+
<div phx-blur="my_blur"></div>
81+
"""
82+
end
83+
84+
test "focus event with parent live view as target" do
85+
html =
86+
render_surface do
87+
~F"""
88+
<ComponentWithEvents focus="my_focus" />
89+
"""
90+
end
91+
92+
assert html =~ """
93+
<div phx-focus="my_focus"></div>
94+
"""
95+
end
96+
97+
test "window blur event with parent live view as target" do
98+
html =
99+
render_surface do
100+
~F"""
101+
<ComponentWithEvents window_blur="my_blur" />
102+
"""
103+
end
104+
105+
assert html =~ """
106+
<div phx-window-blur="my_blur"></div>
107+
"""
108+
end
109+
110+
test "window focus event with parent live view as target" do
111+
html =
112+
render_surface do
113+
~F"""
114+
<ComponentWithEvents window_focus="my_focus" />
115+
"""
116+
end
117+
118+
assert html =~ """
119+
<div phx-window-focus="my_focus"></div>
120+
"""
121+
end
122+
123+
# Key Events
124+
125+
test "keydown event with parent live view as target" do
126+
html =
127+
render_surface do
128+
~F"""
129+
<ComponentWithEvents keydown="my_keydown" />
130+
"""
131+
end
132+
133+
assert html =~ """
134+
<div phx-keydown="my_keydown"></div>
135+
"""
136+
end
137+
138+
test "keyup event with parent live view as target" do
139+
html =
140+
render_surface do
141+
~F"""
142+
<ComponentWithEvents keyup="my_keyup" />
143+
"""
144+
end
145+
146+
assert html =~ """
147+
<div phx-keyup="my_keyup"></div>
148+
"""
149+
end
150+
151+
test "window keydown event with parent live view as target" do
152+
html =
153+
render_surface do
154+
~F"""
155+
<ComponentWithEvents window_keydown="my_keydown" />
156+
"""
157+
end
158+
159+
assert html =~ """
160+
<div phx-window-keydown="my_keydown"></div>
161+
"""
162+
end
163+
164+
test "window keyup event with parent live view as target" do
165+
html =
166+
render_surface do
167+
~F"""
168+
<ComponentWithEvents window_keyup="my_keyup" />
169+
"""
170+
end
171+
172+
assert html =~ """
173+
<div phx-window-keyup="my_keyup"></div>
174+
"""
175+
end
176+
177+
test "click event with @myself as target" do
178+
html =
179+
render_surface do
180+
~F"""
181+
<Parent id="comp" />
182+
"""
183+
end
184+
185+
doc = parse_document!(html)
186+
187+
assert js_attribute(doc, "div > div", "phx-click") == [["push", %{"event" => "my_click", "target" => 1}]]
188+
end
189+
190+
test "event with values" do
191+
html =
192+
render_surface do
193+
~F"""
194+
<ComponentWithEvents click="my_click" values={hello: :world, foo: "bar", one: 2} />
195+
"""
196+
end
197+
198+
# Assert: <div phx-click="my_click" phx-value-foo="bar" phx-value-hello="world" phx-value-one="2"></div>
199+
doc = parse_document!(html)
200+
assert attribute(doc, "phx-click") == ["my_click"]
201+
assert attribute(doc, "phx-value-foo") == ["bar"]
202+
assert attribute(doc, "phx-value-hello") == ["world"]
203+
assert attribute(doc, "phx-value-one") == ["2"]
204+
end
205+
end

0 commit comments

Comments
 (0)