added CHANGELOG.md
 
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0 (2021-11-13)
4
+
5
+ added preliminar support for Live View by calling `Phoenix.LiveView.Helpers.live_patch/2` instead of `Phoenix.HTML.Link.link/2` if *live* option is `true`
6
+
7
+ ## 0.2.1 (2021-11-11)
8
+
9
+ added option *display_if_single*
10
+
11
+ ## 0.2.0 (2021-09-11)
12
+
13
+ added option *merge_params*
14
+
15
+ ## 0.1.0 (2019-01-18)
16
+
17
+ public initial release
changed README.md
 
@@ -23,7 +23,7 @@ def deps do
23
23
[
24
24
# ...
25
25
{:scrivener_ecto, "~> 2.7"},
26
- {:scrivener_phoenix, "~> 0.2.1"},
26
+ {:scrivener_phoenix, "~> 0.3.0"},
27
27
]
28
28
end
29
29
```
 
@@ -40,6 +40,7 @@ config :scrivener_phoenix,
40
40
right: 0,
41
41
window: 4,
42
42
outer_window: 0,
43
+ live: false,
43
44
inverted: false,
44
45
param_name: :page,
45
46
merge_params: false,
 
@@ -53,6 +54,7 @@ config :scrivener_phoenix,
53
54
* right (default: `0`): display the *right* last pages
54
55
* window (default: `4`): display *window* pages before and after the current page (eg, if 7 is the current page and window is 2, you'd get: `5 6 7 8 9`)
55
56
* outer_window (default: `0`), equivalent to left = right = outer_window: display the *outer_window* first and last pages (eg valued to 2: `« First ‹ Prev 1 2 ... 5 6 7 8 9 ... 19 20 Next › Last »` as opposed to left = 1 and right = 3: `« First ‹ Prev 1 ... 5 6 7 8 9 ... 18 19 20 Next › Last »`)
57
+ * live (default: `false`): `true` to generate links with `Phoenix.LiveView.Helpers.live_patch/2` instead of `Phoenix.HTML.Link.link/2`
56
58
* inverted (default: `false`): see **Inverted pagination** above
57
59
* display_if_single (default: `false`): `true` to force a pagination to be displayed when there only is a single page of result(s)
58
60
* param_name (default: `:page`): the name of the parameter generated in URL (query string) to propagate the page number
 
@@ -110,7 +112,7 @@ defmodule MyAppWeb.BlogController do
110
112
def index(conn, params) do
111
113
posts =
112
114
params
113
- |> Map.get(:page, 1) # <= extract the page number from params if present else default to first page
115
+ |> Map.get("page", 1) # <= extract the page number from params if present else default to first page
114
116
|> Blog.posts_at_page()
115
117
116
118
conn
 
@@ -199,3 +201,63 @@ def blog_page_path(conn_or_endpoint, action = :index, page, query_params \\ [])
199
201
```
200
202
201
203
TL;DR: for arity, add 3 to the length of the list you pass as parameters if page number is a parameter to your route else 2 (and the page number will be part of the query string)
204
+
205
+ ## LiveView: dealing with live views
206
+
207
+ In order to avoid liveview reloading, we need to handle page changes with `handle_params/3` callback but without triggering a full (re)`mount/3`. To do so, pagination links have to be generated by calling `Phoenix.LiveView.Helpers.live_patch/2` instead of the "regular" `Phoenix.HTML.Link.link/2`. Since you may want to share a same template for dead and live views, a *live* option has been introduced to know which of these two has to be called.
208
+
209
+ So, comparatively to a dead view, only 2 changes are required:
210
+
211
+ 1. the first parameter of `Scrivener.PhoenixView.paginate/5`, usually `@conn`, becomes `@socket`
212
+ 2. add `live: true` as option to `Scrivener.PhoenixView.paginate/5`
213
+
214
+ Example:
215
+
216
+ ```elixir
217
+ defp to_tuple(socket = %Phoenix.LiveView.Socket{}, atom)
218
+ when is_atom(atom)
219
+ do
220
+ {atom, socket}
221
+ end
222
+
223
+ @impl Phoenix.LiveView
224
+ def mount(params, session, socket) do
225
+ # in mount, we load the first page by default
226
+ socket
227
+ |> assign(:posts, Blog.posts_at_page(1)) # see the module MyApp.Blog above if needed
228
+ # ...
229
+ |> to_tuple(:ok)
230
+ end
231
+
232
+ @impl Phoenix.LiveView
233
+ def handle_params(params, _uri, socket) do
234
+ # here, we fetch the page number from params to load and update the posts assign
235
+ posts =
236
+ params
237
+ |> Map.get("page", 1)
238
+ |> Blog.posts_at_page()
239
+
240
+ socket
241
+ |> assign(:posts, posts)
242
+ |> to_tuple(:noreply)
243
+ end
244
+
245
+ # NOTE: this callback can be replaced by a .html.heex template
246
+ @impl Phoenix.LiveView
247
+ def render(assigns) do
248
+ ~H"""
249
+ ...
250
+
251
+ <%#
252
+ For:
253
+
254
+ live "...", BlogPostLive, :index
255
+
256
+ In the router (lib/your_app_web/router.ex, report to the output of the command `mix phx.routes` if you are not sure about the path helper function's name).
257
+ %>
258
+ <%= paginate @socket, @posts, &Routes.blog_post_path/3, [:index], live: true %>
259
+
260
+ ...
261
+ """
262
+ end
263
+ ```
changed hex_metadata.config
 
@@ -14,7 +14,7 @@
14
14
<<"priv/gettext">>,<<"priv/gettext/scrivener_phoenix.pot">>,
15
15
<<"priv/gettext/fr">>,<<"priv/gettext/fr/LC_MESSAGES">>,
16
16
<<"priv/gettext/fr/LC_MESSAGES/scrivener_phoenix.po">>,<<"mix.exs">>,
17
- <<"README.md">>]}.
17
+ <<"CHANGELOG.md">>,<<"README.md">>]}.
18
18
{<<"licenses">>,[<<"BSD">>]}.
19
19
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/julp/scrivener_phoenix">>}]}.
20
20
{<<"name">>,<<"scrivener_phoenix">>}.
 
@@ -29,9 +29,9 @@
29
29
{<<"optional">>,false},
30
30
{<<"repository">>,<<"hexpm">>},
31
31
{<<"requirement">>,<<"~> 2.5">>}],
32
- [{<<"app">>,<<"phoenix_html">>},
33
- {<<"name">>,<<"phoenix_html">>},
32
+ [{<<"app">>,<<"phoenix_live_view">>},
33
+ {<<"name">>,<<"phoenix_live_view">>},
34
34
{<<"optional">>,false},
35
35
{<<"repository">>,<<"hexpm">>},
36
- {<<"requirement">>,<<"~> 2.11">>}]]}.
37
- {<<"version">>,<<"0.2.1">>}.
36
+ {<<"requirement">>,<<">= 0.16.0">>}]]}.
37
+ {<<"version">>,<<"0.3.0">>}.
changed lib/scrivener_phoenix/page.ex
 
@@ -5,12 +5,22 @@ defmodule Scrivener.Phoenix.Page do
5
5
6
6
defstruct ~W[no href]a
7
7
8
- @type t :: %__MODULE__{no: non_neg_integer(), href: String.t}
8
+ @type t :: %__MODULE__{
9
+ no: non_neg_integer,
10
+ href: String.t,
11
+ }
9
12
10
13
def create(no, href) do
11
- %__MODULE__{no: no, href: href}
14
+ %__MODULE__{
15
+ no: no,
16
+ href: href,
17
+ }
12
18
end
13
19
20
+ # <TODO: find a better place for these functions?>
21
+ def link_callback(%{live: true}), do: &Phoenix.LiveView.Helpers.live_patch/2
22
+ def link_callback(_options), do: &Phoenix.HTML.Link.link/2
23
+
14
24
def handle_rel(page = %__MODULE__{}, spage = %Scrivener.Page{}, attributes \\ []) do
15
25
cond do
16
26
page.no == spage.page_number + 1 ->
 
@@ -21,6 +31,7 @@ defmodule Scrivener.Phoenix.Page do
21
31
attributes
22
32
end
23
33
end
34
+ # </TODO: find a better place for these functions?>
24
35
25
36
@doc ~S"""
26
37
Is the given page the last one?
changed lib/scrivener_phoenix_web/template.ex
 
@@ -24,19 +24,19 @@ defmodule Scrivener.Phoenix.Template do
24
24
Example:
25
25
26
26
# this is the current page
27
- def page(page = %Scrivener.Phoenix.Page{no: no}, %Scrivener.Page{page_number: no}) do
27
+ def page(page = %Scrivener.Phoenix.Page{no: no}, %Scrivener.Page{page_number: no}, _options) do
28
28
content_tag(:li) do
29
29
content_tag(:span, no, class: "current")
30
30
end
31
31
end
32
32
33
- def page(page = %Scrivener.Phoenix.Page{}, _) do
33
+ def page(page = %Scrivener.Phoenix.Page{}, _, _options) do
34
34
content_tag(:li) do
35
35
link(page.no, to: page.href)
36
36
end
37
37
end
38
38
"""
39
- @callback page(Scrivener.Phoenix.Page.t | Scrivener.Phoenix.Gap.t, Scrivener.Page.t) :: Phoenix.HTML.safe
39
+ @callback page(Scrivener.Phoenix.Page.t | Scrivener.Phoenix.Gap.t, Scrivener.Page.t, Scrivener.PhoenixView.options) :: Phoenix.HTML.safe
40
40
41
41
@doc ~S"""
42
42
Callback to generate HTML of the first page or to skip it by returning `nil`.
 
@@ -52,7 +52,7 @@ defmodule Scrivener.Phoenix.Template do
52
52
end
53
53
end
54
54
"""
55
- @callback first_page(Scrivener.Phoenix.Page.t, Scrivener.Page.t, map) :: Phoenix.HTML.safe | nil
55
+ @callback first_page(Scrivener.Phoenix.Page.t, Scrivener.Page.t, Scrivener.PhoenixView.options) :: Phoenix.HTML.safe | nil
56
56
57
57
@doc ~S"""
58
58
Callback to generate HTML of the last page or to skip it by returning `nil`.
 
@@ -67,17 +67,17 @@ defmodule Scrivener.Phoenix.Template do
67
67
end
68
68
end
69
69
"""
70
- @callback last_page(Scrivener.Phoenix.Page.t, Scrivener.Page.t, map) :: Phoenix.HTML.safe | nil
70
+ @callback last_page(Scrivener.Phoenix.Page.t, Scrivener.Page.t, Scrivener.PhoenixView.options) :: Phoenix.HTML.safe | nil
71
71
72
72
@doc ~S"""
73
73
Callback to generate HTML of the previous page or to skip it by returning `nil`.
74
74
"""
75
- @callback prev_page(Scrivener.Phoenix.Page.t, map) :: Phoenix.HTML.safe | nil
75
+ @callback prev_page(Scrivener.Phoenix.Page.t, Scrivener.PhoenixView.options) :: Phoenix.HTML.safe | nil
76
76
77
77
@doc ~S"""
78
78
Callback to generate HTML of the next page or to skip it by returning `nil`.
79
79
"""
80
- @callback next_page(Scrivener.Phoenix.Page.t, map) :: Phoenix.HTML.safe | nil
80
+ @callback next_page(Scrivener.Phoenix.Page.t, Scrivener.PhoenixView.options) :: Phoenix.HTML.safe | nil
81
81
82
82
defmacro __using__(_options) do
83
83
quote do
changed lib/scrivener_phoenix_web/templates/bootstrap4.ex
 
@@ -17,31 +17,31 @@ defmodule Scrivener.Phoenix.Template.Bootstrap4 do
17
17
content_tag(:li, content, options)
18
18
end
19
19
20
- defp build_element(text, href, options, parent_options \\ []) do
20
+ defp build_element(text, href, options, child_html_attrs, parent_html_attrs \\ []) do
21
21
text
22
- |> link(Keyword.merge(options, [to: href, class: "page-link"]))
23
- |> li_wrap(parent_options)
22
+ |> link_callback(options).(Keyword.merge(child_html_attrs, [to: href, class: "page-link"]))
23
+ |> li_wrap(parent_html_attrs)
24
24
end
25
25
26
26
@impl Scrivener.Phoenix.Template
27
27
def first_page(_page, %Scrivener.Page{page_number: 1}, %{}), do: nil
28
28
29
29
def first_page(page = %Page{}, _spage, options = %{}) do
30
- build_element(options.labels.first, page.href, title: dgettext("scrivener_phoenix", "First page"))
30
+ build_element(options.labels.first, page.href, options, title: dgettext("scrivener_phoenix", "First page"))
31
31
end
32
32
33
33
@impl Scrivener.Phoenix.Template
34
34
if false do
35
35
def last_page(page = %Page{}, spage = %Scrivener.Page{}, options = %{}) do
36
36
unless Page.last_page?(page, spage) do
37
- build_element(options.labels.last, page.href, title: dgettext("scrivener_phoenix", "Last page"))
37
+ build_element(options.labels.last, page.href, options, title: dgettext("scrivener_phoenix", "Last page"))
38
38
end
39
39
end
40
40
else
41
41
def last_page(%Page{}, %Scrivener.Page{page_number: no, total_pages: no}, %{}), do: nil
42
42
43
43
def last_page(page = %Page{}, _spage, options = %{}) do
44
- build_element(options.labels.last, page.href, title: dgettext("scrivener_phoenix", "Last page"))
44
+ build_element(options.labels.last, page.href, options, title: dgettext("scrivener_phoenix", "Last page"))
45
45
end
46
46
end
47
47
 
@@ -49,37 +49,37 @@ defmodule Scrivener.Phoenix.Template.Bootstrap4 do
49
49
def prev_page(nil, %{}), do: nil
50
50
51
51
def prev_page(page = %Page{}, options = %{}) do
52
- build_element(options.labels.prev, page.href, title: dgettext("scrivener_phoenix", "Previous page"), rel: "prev")
52
+ build_element(options.labels.prev, page.href, options, title: dgettext("scrivener_phoenix", "Previous page"), rel: "prev")
53
53
end
54
54
55
55
@impl Scrivener.Phoenix.Template
56
56
def next_page(nil, %{}), do: nil
57
57
58
58
def next_page(page = %Page{}, options = %{}) do
59
- build_element(options.labels.next, page.href, title: dgettext("scrivener_phoenix", "Next page"), rel: "next")
59
+ build_element(options.labels.next, page.href, options, title: dgettext("scrivener_phoenix", "Next page"), rel: "next")
60
60
end
61
61
62
62
@impl Scrivener.Phoenix.Template
63
63
if false do
64
- def page(page = %Page{}, spage = %Scrivener.Page{}) do
64
+ def page(page = %Page{}, spage = %Scrivener.Page{}, options = %{}) do
65
65
if Page.current?(page, spage) do
66
- build_element(page.no, "#", [], class: "active")
66
+ build_element(page.no, "#", options, [], class: "active")
67
67
else
68
- build_element(page.no, page.href, handle_rel(page, spage))
68
+ build_element(page.no, page.href, options, handle_rel(page, spage))
69
69
end
70
70
end
71
71
else
72
- def page(page = %Page{no: no}, %Scrivener.Page{page_number: no}) do
73
- build_element(page.no, "#", [], class: "active")
72
+ def page(page = %Page{no: no}, %Scrivener.Page{page_number: no}, options = %{}) do
73
+ build_element(page.no, "#", options, [], class: "active")
74
74
end
75
75
76
- def page(page = %Page{}, spage = %Scrivener.Page{}) do
77
- build_element(page.no, page.href, handle_rel(page, spage))
76
+ def page(page = %Page{}, spage = %Scrivener.Page{}, options = %{}) do
77
+ build_element(page.no, page.href, options, handle_rel(page, spage))
78
78
end
79
79
end
80
80
81
- def page(%Gap{}, %Scrivener.Page{}) do
82
- build_element("…", "#", [], class: "disabled")
81
+ def page(%Gap{}, %Scrivener.Page{}, options = %{}) do
82
+ build_element("…", "#", options, [], class: "disabled")
83
83
end
84
84
85
85
@impl Scrivener.Phoenix.Template
changed lib/scrivener_phoenix_web/views/scrivener_phoenix_view.ex
 
@@ -11,6 +11,7 @@ defmodule Scrivener.PhoenixView do
11
11
@default_right 0
12
12
@default_window 4
13
13
@default_outer_window 0
14
+ @default_live false
14
15
@default_inverted false
15
16
@default_param_name :page
16
17
@default_merge_params false
 
@@ -23,6 +24,7 @@ defmodule Scrivener.PhoenixView do
23
24
right: @default_right,
24
25
window: @default_window,
25
26
outer_window: @default_outer_window,
27
+ live: @default_live,
26
28
inverted: @default_inverted, # NOTE: would be great if it was an option handled by (and passed from - part of %Scriver.Page{}) Scrivener
27
29
display_if_single: @default_display_if_single,
28
30
param_name: @default_param_name,
 
@@ -44,13 +46,14 @@ defmodule Scrivener.PhoenixView do
44
46
]
45
47
end
46
48
47
- @typep conn_or_endpoint :: Plug.Conn.t | module
49
+ @typep conn_or_socket_or_endpoint :: Plug.Conn.t | Phoenix.LiveView.Socket.t | module
48
50
#@typep options :: %{optional(atom) => any}
49
- @typep options :: %{
51
+ @type options :: %{
50
52
left: non_neg_integer,
51
53
right: non_neg_integer,
52
54
window: non_neg_integer,
53
55
outer_window: non_neg_integer,
56
+ live: boolean,
54
57
inverted: boolean,
55
58
display_if_single: boolean,
56
59
param_name: atom | String.t,
 
@@ -70,7 +73,7 @@ defmodule Scrivener.PhoenixView do
70
73
},
71
74
}
72
75
73
- @spec do_paginate(conn :: conn_or_endpoint, page :: Scrivener.Page.t, fun :: function, arguments :: list, options :: %{optional(atom) => any}) :: Phoenix.HTML.safe
76
+ @spec do_paginate(conn :: conn_or_socket_or_endpoint, page :: Scrivener.Page.t, fun :: function, arguments :: list, options :: %{optional(atom) => any}) :: Phoenix.HTML.safe
74
77
75
78
# skip pagination if:
76
79
# - there is zero entry (total)
 
@@ -144,8 +147,9 @@ defmodule Scrivener.PhoenixView do
144
147
* window (default: `#{inspect(@default_window)}`): display *window* pages before and after the current page (eg, if 7 is the current page and window is 2, you'd get: `5 6 7 8 9`)
145
148
* outer_window (default: `#{inspect(@default_outer_window)}`), equivalent to left = right = outer_window: display the *outer_window* first and last pages (eg valued to 2:
146
149
`« First ‹ Prev 1 2 ... 5 6 7 8 9 ... 19 20 Next › Last »` as opposed to left = 1 and right = 3: `« First ‹ Prev 1 ... 5 6 7 8 9 ... 18 19 20 Next › Last »`)
150
+ * live (default: `#{inspect(@default_live)}`): `true` to generate links with `Phoenix.LiveView.Helpers.live_patch/2` instead of `Phoenix.HTML.Link.link/2`
147
151
* inverted (default: `#{inspect(@default_inverted)}`): `true` to first (left side) link last pages instead of first
148
- * display_if_single (default: `#{inspect(@default_display_if_single)}`): TODO
152
+ * display_if_single (default: `#{inspect(@default_display_if_single)}`): `true` to force a pagination to be displayed when there only is a single page of result(s)
149
153
* param_name (default: `#{inspect(@default_param_name)}`): the name of the parameter generated in URL (query string) to propagate the page number
150
154
* merge_params (default: `#{inspect(@default_merge_params)}`): `true` to copy the entire query string between requests, `false` to ignore it or a list of the parameter names to only reproduce
151
155
* template (default: `#{inspect(@default_template)}`): the module which implements `Scrivener.Phoenix.Template` to use to render links to pages
 
@@ -153,7 +157,7 @@ defmodule Scrivener.PhoenixView do
153
157
* labels (default: `%{first: dgettext("scrivener_phoenix", "First"), prev: dgettext("scrivener_phoenix", "Prev"), next: dgettext("scrivener_phoenix", "Next"), last: dgettext("scrivener_phoenix", "Last")}`):
154
158
the texts used by links to describe the first, previous, next and last page
155
159
"""
156
- @spec paginate(conn :: conn_or_endpoint, spage :: Scrivener.Page.t, fun :: function, arguments :: list, options :: Keyword.t) :: Phoenix.HTML.safe
160
+ @spec paginate(conn :: conn_or_socket_or_endpoint, spage :: Scrivener.Page.t, fun :: function, arguments :: list, options :: Keyword.t) :: Phoenix.HTML.safe
157
161
def paginate(conn, page = %Scrivener.Page{}, fun, arguments \\ [], options \\ [])
158
162
when is_function(fun)
159
163
do
 
@@ -233,7 +237,7 @@ defmodule Scrivener.PhoenixView do
233
237
pages
234
238
|> Enum.reverse()
235
239
|> Enum.into(links, fn page ->
236
- options.template.page(page, spage)
240
+ options.template.page(page, spage, options)
237
241
end)
238
242
end
239
243
 
@@ -306,7 +310,7 @@ defmodule Scrivener.PhoenixView do
306
310
Map.take(params, which |> Enum.map(&to_string/1))
307
311
end
308
312
309
- @spec query_params(conn_or_endpoint :: conn_or_endpoint, options :: options) :: map
313
+ @spec query_params(conn_or_socket_or_endpoint :: conn_or_socket_or_endpoint, options :: options) :: map
310
314
defp query_params(%Plug.Conn{}, %{merge_params: false}) do
311
315
%{}
312
316
end
 
@@ -317,6 +321,10 @@ defmodule Scrivener.PhoenixView do
317
321
|> filter_params(options)
318
322
end
319
323
324
+ defp query_params(%Phoenix.LiveView.Socket{}, _options) do
325
+ %{}
326
+ end
327
+
320
328
defp query_params(endpoint, _options)
321
329
when is_atom(endpoint)
322
330
do
changed mix.exs
 
@@ -10,7 +10,7 @@ defmodule Scrivener.Phoenix.MixProject do
10
10
def project do
11
11
[
12
12
app: :scrivener_phoenix,
13
- version: "0.2.1",
13
+ version: "0.3.0",
14
14
elixir: "~> 1.7",
15
15
compilers: compilers(Mix.env()),
16
16
start_permanent: Mix.env() == :prod,
 
@@ -25,7 +25,7 @@ defmodule Scrivener.Phoenix.MixProject do
25
25
coveralls: :test,
26
26
"coveralls.detail": :test,
27
27
"coveralls.post": :test,
28
- "coveralls.html": :test
28
+ "coveralls.html": :test,
29
29
]
30
30
]
31
31
end
 
@@ -50,7 +50,7 @@ defmodule Scrivener.Phoenix.MixProject do
50
50
51
51
defp package do
52
52
[
53
- files: ["lib", "priv", "mix.exs", "README*"],
53
+ files: ~W[lib priv mix.exs CHANGELOG.md README.md],
54
54
licenses: ["BSD"],
55
55
links: %{"GitHub" => "https://github.com/julp/scrivener_phoenix"}
56
56
]
 
@@ -61,12 +61,13 @@ defmodule Scrivener.Phoenix.MixProject do
61
61
[
62
62
{:gettext, ">= 0.0.0"},
63
63
{:scrivener, "~> 2.5"},
64
- {:phoenix_html, "~> 2.11"},
65
- {:ex_doc, "~> 0.16", only: :dev, runtime: false},
64
+ #{:phoenix_html, "~> 2.11"}, # pulled by phoenix_live_view
65
+ {:phoenix_live_view, ">= 0.16.0"},
66
+ {:ex_doc, "~> 0.25", only: :dev, runtime: false},
66
67
# test
67
68
{:jason, "~> 1.2", only: :test},
68
- {:phoenix, "~> 1.5", only: :test},
69
- {:excoveralls, "~> 0.13", only: :test},
69
+ #{:phoenix, "~> 1.6", only: :test}, # pulled by phoenix_live_view
70
+ {:excoveralls, "~> 0.14", only: :test},
70
71
{:dialyxir, "~> 1.0", only: ~W[dev test]a, runtime: false},
71
72
]
72
73
end