Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: disable links until JS properly loaded #352

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
24 changes: 22 additions & 2 deletions lib/phoenix_html/link.ex
Expand Up @@ -27,7 +27,7 @@ defmodule Phoenix.HTML.Link do

# If you supply a method other than `:get`:
link("delete", to: "/everything", method: :delete)
#=> <a href="/everything" data-csrf="csrf_token" data-method="delete" data-to="/everything">delete</a>
#=> <a href="/everything" data-csrf="csrf_token" data-method="delete" data-to="/everything" data-load-disabled="true">delete</a>

# You can use a `do ... end` block too:
link to: "/hello" do
Expand Down Expand Up @@ -111,6 +111,13 @@ defmodule Phoenix.HTML.Link do
## CSRF Protection

By default, CSRF tokens are generated through `Plug.CSRFProtection`.

## Disabled while loading

All links with methods other than `:get` are by default disabled until `Phoenix.HTML`
is loaded. This prevents them from being clicked (thus using the `:get` method).

If you already provide the `disabled` option, this feature is ignored.
"""
@valid_uri_schemes [
"http:",
Expand Down Expand Up @@ -153,7 +160,20 @@ defmodule Phoenix.HTML.Link do
else
{csrf_data, opts} = csrf_data(to, opts)
opts = Keyword.put_new(opts, :rel, "nofollow")
content_tag(:a, text, [data: csrf_data ++ [method: method, to: to], href: to] ++ opts)

{extra_data, extra_opts} =
if Keyword.has_key?(opts, :disabled) do
{[], []}
else
{[load_disabled: "true"], [disabled: "disabled"]}
end

content_tag(
:a,
text,
[data: csrf_data ++ [method: method, to: to] ++ extra_data, href: to] ++
opts ++ extra_opts
)
end
end

Expand Down
7 changes: 7 additions & 0 deletions priv/static/phoenix_html.js
Expand Up @@ -74,4 +74,11 @@
e.preventDefault();
}
}, false);

window.addEventListener("DOMContentLoaded", function () {
var links = document.querySelectorAll("a[data-load-disabled='true']");
for (var i = 0; i < links.length; i++) {
links[i].removeAttribute('disabled')
}
}, false);
})();
4 changes: 2 additions & 2 deletions test/phoenix_html/csrf_test.exs
Expand Up @@ -7,12 +7,12 @@ defmodule Phoenix.HTML.CSRFTest do

test "link with post using a custom csrf token" do
assert safe_to_string(link("hello", to: "/world", method: :post)) =~
~r(<a data-csrf="[^"]+" data-method="post" data-to="/world" href="/world" rel="nofollow">hello</a>)
~r(<a data-csrf="[^"]+" data-method="post" data-to="/world" data-load-disabled="true" disabled="disabled" href="/world" rel="nofollow">hello</a>)
end

test "link with put/delete using a custom csrf token" do
assert safe_to_string(link("hello", to: "/world", method: :put)) =~
~r(<a data-csrf="[^"]+" data-method="put" data-to="/world" href="/world" rel="nofollow">hello</a>)
~r(<a data-csrf="[^"]+" data-method="put" data-to="/world" data-load-disabled="true" disabled="disabled" href="/world" rel="nofollow">hello</a>)
end

test "button with post using a custom csrf token" do
Expand Down
13 changes: 10 additions & 3 deletions test/phoenix_html/link_test.exs
Expand Up @@ -8,7 +8,7 @@ defmodule Phoenix.HTML.LinkTest do
csrf_token = Plug.CSRFProtection.get_csrf_token()

assert safe_to_string(link("hello", to: "/world", method: :post)) ==
~s[<a data-csrf="#{csrf_token}" data-method="post" data-to="/world" href="/world" rel="nofollow">hello</a>]
~s[<a data-csrf="#{csrf_token}" data-method="post" data-to="/world" data-load-disabled=\"true\" disabled=\"disabled\" href="/world" rel="nofollow">hello</a>]
end

test "link with %URI{}" do
Expand All @@ -27,12 +27,19 @@ defmodule Phoenix.HTML.LinkTest do
csrf_token = Plug.CSRFProtection.get_csrf_token()

assert safe_to_string(link("hello", to: "/world", method: :put)) ==
~s[<a data-csrf="#{csrf_token}" data-method="put" data-to="/world" href="/world" rel="nofollow">hello</a>]
~s[<a data-csrf="#{csrf_token}" data-method="put" data-to="/world" data-load-disabled=\"true\" disabled=\"disabled\" href="/world" rel="nofollow">hello</a>]
end

test "link with put/delete without csrf_token" do
assert safe_to_string(link("hello", to: "/world", method: :put, csrf_token: false)) ==
~s[<a data-method="put" data-to="/world" href="/world" rel="nofollow">hello</a>]
~s[<a data-method="put" data-to="/world" data-load-disabled=\"true\" disabled=\"disabled\" href="/world" rel="nofollow">hello</a>]
end

test "link with post already disabled" do
csrf_token = Plug.CSRFProtection.get_csrf_token()

assert safe_to_string(link("hello", to: "/world", method: :post, disabled: "disabled")) ==
~s[<a data-csrf="#{csrf_token}" data-method="post" data-to="/world" disabled=\"disabled\" href="/world" rel="nofollow">hello</a>]
end

test "link with :do contents" do
Expand Down