defmodule Phoenix.WebComponent.Link do @moduledoc """ Conveniences for working with links and URLs in HTML. """ import Phoenix.HTML.Tag alias Phoenix.LiveView.Socket @doc """ Generates a link to the given URL. ## Examples wc_link("hello", to: "/world") #=> hello wc_link("hello", to: URI.parse("https://elixir-lang.org")) #=> hello wc_link("", to: "/world") #=> <hello> wc_link("", to: "/world", class: "btn") #=> <hello> wc_link("delete", to: "/the_world", data: [confirm: "Really?"]) #=> delete # If you supply a method other than `:get`: wc_link("delete", to: "/everything", method: :delete) #=> delete # You can use a `do ... end` block too: link to: "/hello" do "world" end #=> world ## Options * `:to` - the page to link to. This option is required * `:method` - the method to use with the link. In case the method is not `:get`, the link is generated inside the form which sets the proper information. In order to submit the form, JavaScript must be enabled * `:csrf_token` - a custom token to use for links with a method other than `:get`. All other options are forwarded to the underlying `` tag. ## Data attributes Data attributes are added as a keyword list passed to the `data` key. The following data attributes are supported: * `data-confirm` - shows a confirmation prompt before generating and submitting the form when `:method` is not `:get`. ## CSRF Protection By default, CSRF tokens are generated through `Plug.CSRFProtection`. """ def wc_link(text, opts) def wc_link(opts, do: contents) when is_list(opts) do wc_link(contents, opts) end def wc_link(_text, opts) when not is_list(opts) do raise ArgumentError, "wc_link/2 requires a keyword list as second argument" end def wc_link(text, opts) do {to, opts} = pop_required_option!(opts, :to, "expected non-nil value for :to in wc_link/2") {method, opts} = Keyword.pop(opts, :method, :get) if method == :get do # Call link attributes to validate `to` [data: data] = Phoenix.WebComponent.link_attributes(to, []) {linkOpts, opts} = pop_link_attr(Keyword.delete(opts, :csrf_token)) content_tag(:a, [href: data[:to]] ++ linkOpts) do content_tag(:"mwc-button", text, opts) end else {csrf_token, opts} = Keyword.pop(opts, :csrf_token, true) opts = Keyword.put_new(opts, :rel, "nofollow") [data: data] = Phoenix.WebComponent.link_attributes(to, method: method, csrf_token: csrf_token) {linkOpts, opts} = pop_link_attr(opts) content_tag(:a, [data: data, href: data[:to]] ++ linkOpts) do content_tag(:"mwc-button", text, opts) end end end @doc """ Generates a button tag that uses the Javascript function handleClick() (see phoenix_html.js) to submit the form data. Useful to ensure that links that change data are not triggered by search engines and other spidering software. ## Examples button("hello", to: "/world") #=> button("hello", to: "/world", method: :get, class: "btn") #=> ## Options * `:to` - the page to link to. This option is required * `:method` - the method to use with the button. Defaults to :post. All other options are forwarded to the underlying button input. When the `:method` is set to `:get` and the `:to` URL contains query parameters the generated form element will strip the parameters in accordance with the [W3C](https://www.w3.org/TR/html401/interact/forms.html#h-17.13.3.4) form specification. ## Data attributes Data attributes are added as a keyword list passed to the `data` key. The following data attributes are supported: * `data-confirm` - shows a confirmation prompt before generating and submitting the form. """ def wc_button(opts, do: contents) do wc_button(contents, opts) end def wc_button(text, opts) do {to, opts} = pop_required_option!(opts, :to, "option :to is required in wc_button/2") {link_opts, opts} = opts |> Keyword.put_new(:method, :post) |> Keyword.split([:method, :csrf_token]) link_attributes = Phoenix.WebComponent.link_attributes(to, link_opts) content_tag(:"mwc-button", text, link_attributes ++ opts) end defp pop_required_option!(opts, key, error_message) do {value, opts} = Keyword.pop(opts, key) unless value do raise ArgumentError, error_message end {value, opts} end defp pop_link_attr(opts) do list = [ :data, :download, :href, :hreflang, :media, :ping, :referrerpolicy, :rel, :target, :type ] Enum.reduce(list, {[], opts}, fn name, {pop, list} -> case Keyword.pop(list, name) do {val, list} when is_nil(val) -> {pop, list} {val, list} -> {Keyword.put(pop, name, val), list} end end) end @doc false def wc_live_patch(opts) when is_list(opts) do wc_live_link("patch", Keyword.fetch!(opts, :do), Keyword.delete(opts, :do)) end @doc """ Generates a link that will patch the current LiveView. When navigating to the current LiveView, `c:Phoenix.LiveView.handle_params/3` is immediately invoked to handle the change of params and URL state. Then the new state is pushed to the client, without reloading the whole page while also maintaining the current scroll position. For live redirects to another LiveView, use `wc_live_redirect/2`. ## Options * `:to` - the required path to link to. * `:replace` - the flag to replace the current history or push a new state. Defaults `false`. All other options are forwarded to the anchor tag. ## Examples <%= wc_live_patch "home", to: Routes.page_path(@socket, :index) %> <%= wc_live_patch "next", to: Routes.live_path(@socket, MyLive, @page + 1) %> <%= wc_live_patch to: Routes.live_path(@socket, MyLive, dir: :asc), replace: false do %> Sort By Price <% end %> """ def wc_live_patch(text, opts) def wc_live_patch(%Socket{}, _) do raise """ you are invoking wc_live_patch/2 with a socket but a socket is not expected. If you want to wc_live_patch/2 inside a LiveView, use push_patch/2 instead. If you are inside a template, make the sure the first argument is a string. """ end def wc_live_patch(opts, do: block) when is_list(opts) do wc_live_link("patch", block, opts) end def wc_live_patch(text, opts) when is_list(opts) do wc_live_link("patch", text, opts) end @doc false def wc_live_redirect(opts) when is_list(opts) do wc_live_link("redirect", Keyword.fetch!(opts, :do), Keyword.delete(opts, :do)) end @doc """ Generates a link that will redirect to a new LiveView of the same live session. The current LiveView will be shut down and a new one will be mounted in its place, without reloading the whole page. This can also be used to remount the same LiveView, in case you want to start fresh. If you want to navigate to the same LiveView without remounting it, use `wc_live_patch/2` instead. *Note*: The live redirects are only supported between two LiveViews defined under the same live session. See `Phoenix.LiveView.Router.live_session/3` for more details. ## Options * `:to` - the required path to link to. * `:replace` - the flag to replace the current history or push a new state. Defaults `false`. All other options are forwarded to the anchor tag. ## Examples <%= wc_live_redirect "home", to: Routes.page_path(@socket, :index) %> <%= wc_live_redirect "next", to: Routes.live_path(@socket, MyLive, @page + 1) %> <%= wc_live_redirect to: Routes.live_path(@socket, MyLive, dir: :asc), replace: false do %> Sort By Price <% end %> """ def wc_live_redirect(text, opts) def wc_live_redirect(%Socket{}, _) do raise """ you are invoking wc_live_redirect/2 with a socket but a socket is not expected. If you want to wc_live_redirect/2 inside a LiveView, use push_redirect/2 instead. If you are inside a template, make the sure the first argument is a string. """ end def wc_live_redirect(opts, do: block) when is_list(opts) do wc_live_link("redirect", block, opts) end def wc_live_redirect(text, opts) when is_list(opts) do wc_live_link("redirect", text, opts) end defp wc_live_link(type, block_or_text, opts) do {uri, opts} = pop_required_option!(opts, :to, "option :to is required in wc_live_link/2") replace = Keyword.get(opts, :replace, false) kind = if replace, do: "replace", else: "push" data = [phx_link: type, phx_link_state: kind] opts = opts |> Keyword.update(:data, data, &Keyword.merge(&1, data)) |> Keyword.put(:href, uri) {linkOpts, opts} = pop_link_attr(opts) content_tag(:a, linkOpts) do content_tag(:"mwc-button", opts, do: block_or_text) end end end