File Coverage

blib/lib/Mojolicious/Plugin/HTMX.pm
Criterion Covered Total %
statement 97 142 68.3
branch 19 38 50.0
condition 2 7 28.5
subroutine 22 38 57.8
pod 1 1 100.0
total 141 226 62.3


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::HTMX;
2 2     2   1371638 use Mojo::Base 'Mojolicious::Plugin';
  2         8701  
  2         16  
3              
4 2     2   1877 use Mojo::ByteStream;
  2         175600  
  2         157  
5 2     2   919 use Mojo::JSON qw(encode_json decode_json);
  2         14336  
  2         192  
6 2     2   20 use Mojo::Util qw(xml_escape);
  2         5  
  2         296  
7              
8             our $VERSION = '1.03';
9              
10             my @HX_RESWAPS = (qw[
11             innerHTML
12             outerHTML
13             beforebegin
14             afterbegin
15             beforeend
16             afterend
17             delete
18             none
19             ]);
20              
21 2     2   16 use constant HX_TRUE => 'true';
  2         5  
  2         201  
22 2     2   15 use constant HX_FALSE => 'false';
  2         4  
  2         156  
23              
24 2     2   15 use constant HTMX_STOP_POLLING => 286;
  2         4  
  2         169  
25 2     2   14 use constant HTMX_CDN_URL => 'https://unpkg.com/htmx.org';
  2         6  
  2         161  
26 2     2   16 use constant HTMX_V1_CDN_URL => 'https://unpkg.com/htmx.org@1.9.12';
  2         4  
  2         7021  
27              
28              
29             sub register {
30              
31 1     1 1 56 my ($self, $app) = @_;
32              
33 1     0   15 $app->helper('is_htmx_request' => sub { _header(shift, 'HX-Request', HX_TRUE) });
  0         0  
34              
35 1         223 $app->helper('htmx.asset' => \&_htmx_js);
36 1     0   596 $app->helper('htmx.stop_polling' => sub { shift->rendered(HTMX_STOP_POLLING) });
  0         0  
37              
38 1     0   497 $app->helper('htmx.req.boosted' => sub { _header(shift, 'HX-Boosted', HX_TRUE) });
  0         0  
39 1     0   936 $app->helper('htmx.req.current_url' => sub { Mojo::URL->new(_header(shift, 'HX-Current-URL')) });
  0         0  
40 1     0   1031 $app->helper('htmx.req.history_restore_request' => sub { _header(shift, 'HX-History-Restore-Request', HX_TRUE) });
  0         0  
41 1     0   1104 $app->helper('htmx.req.prompt' => sub { _header(shift, 'HX-Prompt') });
  0         0  
42 1     0   1177 $app->helper('htmx.req.request' => sub { _header(shift, 'HX-Request', HX_TRUE) });
  0         0  
43 1     0   1328 $app->helper('htmx.req.target' => sub { _header(shift, 'HX-Target') });
  0         0  
44 1     0   1367 $app->helper('htmx.req.trigger_name' => sub { _header(shift, 'HX-Trigger-Name') });
  0         0  
45 1     0   1383 $app->helper('htmx.req.trigger' => sub { _header(shift, 'HX-Trigger') });
  0         0  
46              
47             $app->helper(
48             'htmx.req.triggering_event' => sub {
49 0 0   0   0 eval { decode_json(_header(shift, 'Triggering-Event')) } || {};
  0         0  
50             }
51 1         1511 );
52              
53             $app->helper(
54             'htmx.req.to_hash' => sub {
55              
56 0     0   0 my $c = shift;
57              
58 0         0 my %hash = ();
59              
60             my @helpers
61 0         0 = (
62             qw[boosted current_url history_restore_request prompt request target trigger_name trigger triggering_event]
63             );
64              
65 0         0 for my $helper (@helpers) {
66 0 0       0 if (my $value = $c->helpers->htmx->req->$helper()) {
67 0         0 $hash{$helper} = "$value";
68             }
69             }
70              
71 0         0 return \%hash;
72              
73             }
74 1         1590 );
75              
76 1         1634 $app->helper('htmx.res.location' => \&_res_location);
77 1         2067 $app->helper('htmx.res.push_url' => \&_res_push_url);
78 1         2227 $app->helper('htmx.res.redirect' => \&_res_redirect);
79 1         2336 $app->helper('htmx.res.refresh' => \&_res_refresh);
80 1         2298 $app->helper('htmx.res.replace_url' => \&_res_replace_url);
81 1         2376 $app->helper('htmx.res.reswap' => \&_res_reswap);
82 1         2522 $app->helper('htmx.res.reselect' => \&_res_reselect);
83 1         2579 $app->helper('htmx.res.retarget' => \&_res_retarget);
84              
85 1     4   2704 $app->helper('htmx.res.trigger' => sub { _res_trigger('default', @_) });
  4         30713  
86 1     3   2758 $app->helper('htmx.res.trigger_after_settle' => sub { _res_trigger('after_settle', @_) });
  3         23592  
87 1     3   2783 $app->helper('htmx.res.trigger_after_swap' => sub { _res_trigger('after_swap', @_) });
  3         32417  
88              
89             $app->helper(
90             'hx' => sub {
91              
92 0     0   0 my ($c, %attrs) = @_;
93 0         0 my $hx = {};
94              
95 0         0 @$hx{map { y/_/-/; "hx-$_" } keys %attrs} = values %attrs;
  0         0  
  0         0  
96 0         0 return %{$hx};
  0         0  
97              
98             }
99 1         2917 );
100              
101             $app->helper(
102             'hx_attr' => sub {
103              
104 0     0   0 my ($c, %attrs) = @_;
105              
106 0         0 my %hx = $c->hx(%attrs);
107 0         0 my $result = '';
108              
109 0         0 for my $attr (sort keys %hx) {
110 0         0 my $value = $hx{$attr};
111 0         0 $result .= qq{ $attr="} . xml_escape($value) . '"';
112             }
113              
114 0         0 return Mojo::ByteStream->new($result);
115              
116             }
117 1         176 );
118              
119             }
120              
121             sub _htmx_js {
122              
123 0     0   0 my ($self, %params) = @_;
124 0   0     0 my $url = delete $params{url} || HTMX_CDN_URL;
125 0         0 my $ext = delete $params{ext};
126              
127 0 0       0 if ($ext) {
128 0         0 $url .= "/dist/ext/$ext.js";
129             }
130              
131 0         0 return Mojo::ByteStream->new(Mojo::DOM::HTML::tag_to_html('script', 'src' => $url));
132              
133             }
134              
135             sub _header {
136              
137 0     0   0 my ($c, $header, $check) = @_;
138 0         0 my $value = $c->req->headers->header($header);
139              
140 0 0 0     0 if ($value && $check) {
141 0 0       0 return !!1 if ($value eq $check);
142 0         0 return !!0;
143             }
144              
145 0         0 return $value;
146              
147             }
148              
149             sub _res_location {
150              
151 3     3   55466 my $c = shift;
152 3 100       21 my $location = (@_ > 1) ? {@_} : $_[0];
153              
154 3 50       12 return undef unless $location;
155              
156 3 100       14 if (ref $location eq 'HASH') {
157 2         12 $location = encode_json($location);
158             }
159              
160 3         51 return $c->res->headers->header('HX-Location' => $location);
161              
162             }
163              
164             sub _res_push_url {
165              
166 2     2   29849 my ($c, $push_url) = @_;
167 2 50       10 return undef unless $push_url;
168              
169 2         25 return $c->res->headers->header('HX-Push-Url' => $push_url);
170              
171             }
172              
173             sub _res_redirect {
174              
175 2     2   31076 my ($c, $redirect) = @_;
176 2 50       9 return undef unless $redirect;
177              
178 2         23 return $c->res->headers->header('HX-Redirect' => $redirect);
179              
180             }
181              
182             sub _res_refresh {
183 1     1   22941 my ($c) = @_;
184 1         7 return $c->res->headers->header('HX-Refresh' => HX_TRUE);
185             }
186              
187             sub _res_replace_url {
188              
189 2     2   17248 my ($c, $replace_url) = @_;
190 2 50       7 return undef unless $replace_url;
191              
192 2         6 return $c->res->headers->header('HX-Replace-Url' => $replace_url);
193              
194             }
195              
196             sub _res_reswap {
197              
198 16     16   124685 my ($c, $reswap) = @_;
199 16 50       42 return undef unless $reswap;
200              
201 16         31 my $is_reswap = grep {/^$reswap$/} @HX_RESWAPS;
  128         551  
202 16 50       38 Carp::croak "Unknown reswap value" if (!$is_reswap);
203              
204 16         50 return $c->res->headers->header('HX-Reswap' => $reswap);
205              
206             }
207              
208             sub _res_reselect {
209              
210 2     2   28159 my ($c, $reselect) = @_;
211 2 50       9 return undef unless $reselect;
212              
213 2         10 return $c->res->headers->header('HX-Reselect' => $reselect);
214              
215             }
216              
217             sub _res_retarget {
218              
219 2     2   21757 my ($c, $retarget) = @_;
220 2 50       8 return undef unless $retarget;
221              
222 2         20 return $c->res->headers->header('HX-Retarget' => $retarget);
223              
224             }
225              
226             sub _res_trigger {
227              
228 10     10   27 my ($type, $c) = (shift, shift);
229 10 100       39 my $trigger = (@_ > 1) ? {@_} : $_[0];
230              
231 10 50       26 return undef unless $trigger;
232              
233 10         33 my $trigger_header = {after_settle => 'HX-Trigger-After-Settle', after_swap => 'HX-Trigger-After-Swap'};
234              
235 10 100       31 if (ref $trigger eq 'HASH') {
236 6         26 $trigger = encode_json($trigger);
237             }
238              
239 10 100       89 if (ref $trigger eq 'ARRAY') {
240 1         1 $trigger = join ', ', @{$trigger};
  1         4  
241             }
242              
243 10   100     38 my $header = $trigger_header->{$type} || 'HX-Trigger';
244              
245 10         32 return $c->res->headers->header($header => $trigger);
246              
247             }
248              
249             1;
250              
251             =encoding utf8
252              
253             =head1 NAME
254              
255             Mojolicious::Plugin::HTMX - Mojolicious Plugin for htmx
256              
257             =head1 SYNOPSIS
258              
259             # Mojolicious
260             $self->plugin('Mojolicious::Plugin::HTMX');
261              
262             # Mojolicious::Lite
263             plugin 'Mojolicious::Plugin::HTMX';
264              
265             get '/trigger' => 'trigger';
266             post '/trigger' => sub ($c) {
267              
268             state $count = 0;
269             $count++;
270              
271             $c->htmx->res->trigger(showMessage => 'Here Is A Message');
272             $c->render(text => "Triggered $count times");
273              
274             };
275              
276             @@ template.html.ep
277            
278            
279             %= app->htmx->asset
280            
281            
282             %= content
283            
284            
285              
286             @@ trigger.html.ep
287             % layout 'default';
288            

Trigger

289              
290            
291            
292              
293            
294             %= tag 'button', hx(post => '/trigger'), 'Click Me'
295              
296            
297            
298              
299            
304              
305              
306             =head1 DESCRIPTION
307              
308             L is a L plugin to add htmx in your Mojolicious application.
309              
310              
311             =head1 HELPERS
312              
313             L implements the following helpers.
314              
315              
316             =head2 GENERIC HELPERS
317              
318             =head3 htmx->asset
319              
320             # Load htmx from "https://unpkg.com/htmx.org"
321             %= htmx->asset
322              
323             # Load htmx from a provided URL
324             %= htmx->asset(src => '/assets/js/htmx.min.js')
325              
326             # Load an extension from "/dist/ext" directory
327             %= htmx->asset(ext => 'debug')
328              
329             Generate C