File Coverage

blib/lib/Dash.pm
Criterion Covered Total %
statement 195 360 54.1
branch 25 112 22.3
condition 8 38 21.0
subroutine 28 39 71.7
pod 3 4 75.0
total 259 553 46.8


line stmt bran cond sub pod time code
1             package Dash;
2              
3 6     6   320918 use strict;
  6         35  
  6         164  
4 6     6   32 use warnings;
  6         10  
  6         149  
5 6     6   138 use 5.020;
  6         24  
6              
7             our $VERSION = '0.09'; # VERSION
8              
9             # ABSTRACT: Analytical Web Apps in Perl (Port of Plotly's Dash to Perl)
10              
11             # TODO Enable signatures?
12              
13 6     6   1893 use Mojo::Base 'Mojolicious';
  6         727325  
  6         49  
14 6     6   1338160 use JSON;
  6         47498  
  6         35  
15 6     6   825 use Scalar::Util;
  6         14  
  6         561  
16 6     6   2604 use Browser::Open;
  6         3809  
  6         292  
17 6     6   2824 use File::ShareDir 1.116;
  6         135771  
  6         374  
18 6     6   4609 use Path::Tiny;
  6         64449  
  6         331  
19 6     6   3060 use Try::Tiny;
  6         7581  
  6         332  
20 6     6   2577 use Dash::Renderer;
  6         15  
  6         190  
21 6     6   2355 use Dash::Exceptions::NoLayoutException;
  6         20  
  6         30978  
22              
23             # TODO Add ci badges
24              
25             has app_name => __PACKAGE__;
26              
27             has external_stylesheets => sub { [] };
28              
29             has _layout => sub { {} };
30              
31             has _callbacks => sub { {} };
32              
33             has '_rendered_scripts' => "";
34              
35             has '_rendered_external_stylesheets' => "";
36              
37             sub layout {
38 13     13 1 2750 my $self = shift;
39 13         24 my $layout = shift;
40 13 100       37 if ( defined $layout ) {
41 11         23 my $type = ref $layout;
42 11 100 66     131 if ( $type eq 'CODE' || ( Scalar::Util::blessed($layout) && $layout->isa('Dash::BaseComponent') ) ) {
      66        
43 9         43 $self->_layout($layout);
44             } else {
45 2         17 Dash::Exceptions::NoLayoutException->throw(
46             'Layout must be a dash component or a function that returns a dash component');
47             }
48             } else {
49 2         9 $layout = $self->_layout;
50             }
51 11         97 return $layout;
52             }
53              
54             sub callback {
55 8     8 1 115 my $self = shift;
56 8         28 my %callback = $self->_process_callback_arguments(@_);
57              
58             # TODO check_callback
59             # TODO Callback map
60 8         19 my $output = $callback{Output};
61 8         25 my $callback_id = $self->_create_callback_id($output);
62 8         32 my $callbacks = $self->_callbacks;
63 8         20 $callbacks->{$callback_id} = \%callback;
64 8         21 return $self;
65             }
66              
67             sub _process_callback_arguments {
68 8     8   14 my $self = shift;
69              
70 8         12 my %callback;
71              
72             # 1. all refs: 1 blessed, 1 array, 1 code or 2 array, 1 code
73             # Hash with keys Output, Inputs, callback
74             # 2. Values content: hashref or arrayref[hashref], arrayref[hashref], coderef
75             # 3. Values content: blessed output or arrayref[blessed], arrayref[blessed], coderef
76              
77 8 50       19 if ( scalar @_ < 5 ) { # Unamed arguments, put names
78 0         0 my ( $output_index, $input_index, $state_index, $callback_index );
79              
80 0         0 my $index = 0;
81 0         0 for my $argument (@_) {
82 0         0 my $type = ref $argument;
83 0 0       0 if ( $type eq 'CODE' ) {
    0          
    0          
    0          
    0          
    0          
84 0         0 $callback_index = $index;
85             } elsif ( Scalar::Util::blessed $argument) {
86 0 0       0 if ( $argument->isa('Dash::Dependencies::Output') ) {
87 0         0 $output_index = $index;
88             }
89             } elsif ( $type eq 'ARRAY' ) {
90 0 0       0 if ( scalar @$argument > 0 ) {
91 0         0 my $first_element = $argument->[0];
92 0 0       0 if ( Scalar::Util::blessed $first_element) {
93 0 0       0 if ( $first_element->isa('Dash::Dependencies::Output') ) {
    0          
    0          
94 0         0 $output_index = $index;
95             } elsif ( $first_element->isa('Dash::Dependencies::Input') ) {
96 0         0 $input_index = $index;
97             } elsif ( $first_element->isa('Dash::Dependencies::State') ) {
98 0         0 $state_index = $index;
99             }
100             }
101             } else {
102 0         0 die "Can't use empty arrayrefs as arguments";
103             }
104             } elsif ( $type eq 'SCALAR' ) {
105 0         0 die
106             "Can't mix scalarref arguments with objects when not using named paremeters. Please use named parameters for all arguments or classes for all arguments";
107             } elsif ( $type eq 'HASH' ) {
108 0         0 die
109             "Can't mix hashref arguments with objects when not using named parameters. Please use named parameters for all arguments or classes for all arguments";
110             } elsif ( $type eq '' ) {
111 0         0 die
112             "Can't mix scalar arguments with objects when not using named parameters. Please use named parameters for all arguments or classes for all arguments";
113             }
114 0         0 $index++;
115             }
116 0 0       0 if ( !defined $output_index ) {
117 0         0 die "Can't find callback output";
118             }
119 0 0       0 if ( !defined $input_index ) {
120 0         0 die "Can't find callback inputs";
121             }
122 0 0       0 if ( !defined $callback_index ) {
123 0         0 die "Can't find callback function";
124             }
125              
126 0         0 $callback{Output} = $_[$output_index];
127 0         0 $callback{Inputs} = $_[$input_index];
128 0         0 $callback{callback} = $_[$callback_index];
129 0 0       0 if ( defined $state_index ) {
130 0         0 $callback{State} = $_[$state_index];
131             }
132             } else { # Named arguments
133             # TODO check keys ¿Params::Validate or similar?
134 8         42 %callback = @_;
135             }
136              
137             # Convert Output & input to hashrefs
138 8         32 for my $key ( keys %callback ) {
139 26         39 my $value = $callback{$key};
140              
141 26 100       89 if ( ref $value eq 'ARRAY' ) {
    50          
142 12         18 my @hashes;
143 12         24 for my $dependency (@$value) {
144 14 50       30 if ( Scalar::Util::blessed $dependency) {
145 0         0 my %dependency_hash = %$dependency;
146 0         0 push @hashes, \%dependency_hash;
147             } else {
148 14         27 push @hashes, $dependency;
149             }
150             }
151 12         40 $callback{$key} = \@hashes;
152             } elsif ( Scalar::Util::blessed $value) {
153 0         0 my %dependency_hash = %$value;
154 0         0 $callback{$key} = \%dependency_hash;
155             }
156             }
157              
158 8         40 return %callback;
159             }
160              
161             sub _create_callback_id {
162 8     8   13 my $self = shift;
163 8         13 my $output = shift;
164              
165 8 100       29 if ( ref $output eq 'ARRAY' ) {
166 2         6 return ".." . join( "...", map { $_->{component_id} . "." . $_->{component_property} } @$output ) . "..";
  4         28  
167             }
168              
169 6         25 return $output->{component_id} . "." . $output->{component_property};
170             }
171              
172             sub startup {
173 11     11 1 175690 my $self = shift;
174              
175 11         39 my $renderer = $self->renderer;
176 11         47 push @{ $renderer->classes }, __PACKAGE__;
  11         40  
177              
178 11         126 my $r = $self->routes;
179             $r->get(
180             '/' => sub {
181 1     1   13460 my $c = shift;
182 1         7 $c->stash( stylesheets => $self->_rendered_stylesheets,
183             external_stylesheets => $self->_rendered_external_stylesheets,
184             scripts => $self->_rendered_scripts,
185             title => $self->app_name
186             );
187 1         49 $c->render( template => 'index' );
188             }
189 11         145 );
190              
191 11         3115 my $dist_name = 'Dash';
192             $r->get(
193             '/_dash-component-suites/:namespace/*asset' => sub {
194              
195             # TODO Component registry to find assets file in other dists
196 1     1   15530 my $c = shift;
197 1         6 my $file = $self->_filename_from_file_with_fingerprint( $c->stash('asset') );
198              
199 1         8 $c->reply->file(
200             File::ShareDir::dist_file( $dist_name,
201             Path::Tiny::path( 'assets', $c->stash('namespace'), $file )->canonpath
202             )
203             );
204             }
205 11         80 );
206              
207             $r->get(
208             '/_favicon.ico' => sub {
209 1     1   7309 my $c = shift;
210 1         4 $c->reply->file( File::ShareDir::dist_file( $dist_name, 'favicon.ico' ) );
211             }
212 11         4844 );
213              
214             $r->get(
215             '/_dash-layout' => sub {
216 1     1   10103 my $c = shift;
217 1         4 $c->render( json => $self->layout() );
218             }
219 11         3018 );
220              
221             $r->get(
222             '/_dash-dependencies' => sub {
223 1     1   6880 my $c = shift;
224 1         5 my $dependencies = $self->_dependencies();
225 1         3 $c->render( json => $dependencies );
226             }
227 11         3134 );
228              
229             $r->post(
230             '/_dash-update-component' => sub {
231 2     2   24243 my $c = shift;
232              
233 2         9 my $request = $c->req->json;
234             try {
235 2         118 my $content = $self->_update_component($request);
236 1         4 $c->render( json => $content );
237             } catch {
238 1 50 33     1511 if ( Scalar::Util::blessed $_ && $_->isa('Dash::Exceptions::PreventUpdate') ) {
239 1         7 $c->render( status => 204, json => '' );
240             } else {
241 0         0 die $_;
242             }
243 2         1321 };
244             }
245 11         3384 );
246              
247 11         3618 return $self;
248             }
249              
250             sub run_server {
251 0     0 0 0 my $self = shift;
252              
253 0         0 $self->_render_and_cache_scripts();
254 0         0 $self->_render_and_cache_external_stylesheets();
255              
256             # Opening the browser before starting the daemon works because
257             # open_browser returns inmediately
258             # TODO Open browser optional
259 0 0       0 if ( not caller(1) ) {
260 0         0 Browser::Open::open_browser('http://127.0.0.1:8080');
261 0         0 $self->start( 'daemon', '-l', 'http://*:8080' );
262             }
263 0         0 return $self;
264             }
265              
266             sub _dependencies {
267 4     4   15 my $self = shift;
268 4         9 my $dependencies = [];
269 4         7 for my $callback ( values %{ $self->_callbacks } ) {
  4         11  
270 3         22 my $rendered_callback = { clientside_function => JSON::null };
271 3         13 my $states = [];
272 3         4 for my $state ( @{ $callback->{State} } ) {
  3         8  
273             my $rendered_state = { id => $state->{component_id},
274             property => $state->{component_property}
275 1         4 };
276 1         3 push @$states, $rendered_state;
277             }
278 3         7 $rendered_callback->{state} = $states;
279 3         15 my $inputs = [];
280 3         7 for my $input ( @{ $callback->{Inputs} } ) {
  3         5  
281             my $rendered_input = { id => $input->{component_id},
282             property => $input->{component_property}
283 3         11 };
284 3         16 push @$inputs, $rendered_input;
285             }
286 3         5 $rendered_callback->{inputs} = $inputs;
287 3         8 my $output_type = ref $callback->{Output};
288 3 100       15 if ( $output_type eq 'ARRAY' ) {
    50          
289 1         3 $rendered_callback->{'output'} .= '.';
290 1         2 for my $output ( @{ $callback->{'Output'} } ) {
  1         3  
291             $rendered_callback->{'output'} .=
292 2         7 '.' . join( '.', $output->{component_id}, $output->{component_property} ) . '..';
293             }
294             } elsif ( $output_type eq 'HASH' ) {
295             $rendered_callback->{'output'} =
296 2         11 join( '.', $callback->{'Output'}{component_id}, $callback->{'Output'}{component_property} );
297             } else {
298 0         0 die 'Dependecy type for callback not implemented';
299             }
300 3         9 push @$dependencies, $rendered_callback;
301             }
302 4         26 return $dependencies;
303             }
304              
305             sub _update_component {
306 6     6   141 my $self = shift;
307 6         30 my $request = shift;
308              
309 6 100       11 if ( scalar( values %{ $self->_callbacks } ) > 0 ) {
  6         16  
310 5         54 my $callbacks = $self->_search_callback( $request->{'output'} );
311 5 50       27 if ( scalar @$callbacks > 1 ) {
    50          
312 0         0 die 'Not implemented multiple callbacks';
313             } elsif ( scalar @$callbacks == 1 ) {
314 5         11 my $callback = $callbacks->[0];
315 5         8 my @callback_arguments = ();
316 5         8 my $callback_context = {};
317 5         9 for my $callback_input ( @{ $callback->{Inputs} } ) {
  5         14  
318 5         9 my ( $component_id, $component_property ) = @{$callback_input}{qw(component_id component_property)};
  5         13  
319 5         8 for my $change_input ( @{ $request->{inputs} } ) {
  5         17  
320 5         12 my ( $id, $property, $value ) = @{$change_input}{qw(id property value)};
  5         20  
321 5 50 33     26 if ( $component_id eq $id && $component_property eq $property ) {
322 5         9 push @callback_arguments, $value;
323 5         23 $callback_context->{inputs}{ $id . "." . $property } = $value;
324 5         14 last;
325             }
326             }
327             }
328 5         9 for my $callback_input ( @{ $callback->{State} } ) {
  5         12  
329 1         3 my ( $component_id, $component_property ) = @{$callback_input}{qw(component_id component_property)};
  1         3  
330 1         2 for my $change_input ( @{ $request->{state} } ) {
  1         3  
331 1         2 my ( $id, $property, $value ) = @{$change_input}{qw(id property value)};
  1         3  
332 1 50 33     7 if ( $component_id eq $id && $component_property eq $property ) {
333 1         2 push @callback_arguments, $value;
334 1         11 $callback_context->{states}{ $id . "." . $property } = $value;
335 1         4 last;
336             }
337             }
338             }
339              
340 5         11 $callback_context->{triggered} = [];
341 5         14 for my $triggered_input ( @{ $request->{changedPropIds} } ) {
  5         12  
342 5         27 push @{ $callback_context->{triggered} },
343             { prop_id => $triggered_input,
344 5         7 value => $callback_context->{inputs}{$triggered_input}
345             };
346             }
347 5         13 push @callback_arguments, $callback_context;
348              
349 5         11 my $output_type = ref $callback->{Output};
350 5 100       32 if ( $output_type eq 'ARRAY' ) {
    50          
351 1         4 my @return_value = $callback->{callback}(@callback_arguments);
352 1         8 my $props_updated = {};
353 1         2 my $index_output = 0;
354 1         3 for my $output ( @{ $callback->{'Output'} } ) {
  1         3  
355             $props_updated->{ $output->{component_id} } =
356 2         8 { $output->{component_property} => $return_value[$index_output] };
357 2         3 $index_output++;
358             }
359 1         4 return { response => $props_updated, multi => JSON::true };
360             } elsif ( $output_type eq 'HASH' ) {
361 4         15 my $updated_value = $callback->{callback}(@callback_arguments);
362 3         28 my $updated_property = ( split( /\./, $request->{output} ) )[-1];
363 3         9 my $props_updated = { $updated_property => $updated_value };
364 3         22 return { response => { props => $props_updated } };
365             } else {
366 0         0 die 'Callback not supported';
367             }
368             } else {
369 0         0 return { response => "There is no matching callback" };
370             }
371              
372             } else {
373 1         9 return { response => "There is no registered callbacks" };
374             }
375 0         0 return { response => "Internal error" };
376             }
377              
378             sub _search_callback {
379 5     5   7 my $self = shift;
380 5         8 my $output = shift;
381              
382 5         13 my $callbacks = $self->_callbacks;
383 5         25 my @matching_callbacks = ( $callbacks->{$output} );
384 5         12 return \@matching_callbacks;
385             }
386              
387             sub _rendered_stylesheets {
388 1     1   5 return '';
389             }
390              
391             sub _render_external_stylesheets {
392 0     0   0 my $self = shift;
393 0         0 my $stylesheets = $self->external_stylesheets;
394 0         0 my $rendered_external_stylesheets = "";
395 0         0 for my $stylesheet (@$stylesheets) {
396 0         0 $rendered_external_stylesheets .= '' . "\n";
397             }
398 0         0 return $rendered_external_stylesheets;
399             }
400              
401             sub _render_and_cache_external_stylesheets {
402 0     0   0 my $self = shift;
403 0         0 my $stylesheets = $self->_render_external_stylesheets();
404 0         0 $self->_rendered_external_stylesheets($stylesheets);
405             }
406              
407             sub _render_and_cache_scripts {
408 0     0   0 my $self = shift;
409 0         0 my $scripts = $self->_render_scripts();
410 0         0 $self->_rendered_scripts($scripts);
411             }
412              
413             sub _render_dash_config {
414             return
415 0     0   0 '';
416             }
417              
418             sub _dash_renderer_js_dependencies {
419 0     0   0 my $js_dist_dependencies = Dash::Renderer::_js_dist_dependencies();
420 0         0 my @js_deps = ();
421 0         0 for my $deps (@$js_dist_dependencies) {
422 0         0 my $external_url = $deps->{external_url};
423 0         0 my $relative_package_path = $deps->{relative_package_path};
424 0         0 my $namespace = $deps->{namespace};
425 0         0 my $dep_count = 0;
426 0         0 for my $dep ( @{ $relative_package_path->{prod} } ) {
  0         0  
427             my $js_dep = { namespace => $namespace,
428             relative_package_path => $dep,
429             dev_package_path => $relative_package_path->{dev}[$dep_count],
430 0         0 external_url => $external_url->{prod}[$dep_count]
431             };
432 0         0 push @js_deps, $js_dep;
433 0         0 $dep_count++;
434             }
435             }
436 0         0 \@js_deps;
437             }
438              
439             sub _dash_renderer_js_deps {
440 0     0   0 return Dash::Renderer::_js_dist();
441             }
442              
443             sub _render_dash_renderer_script {
444 0     0   0 return '';
445             }
446              
447             sub _render_scripts {
448 0     0   0 my $self = shift;
449              
450             # First dash_renderer dependencies
451 0         0 my $scripts_dependencies = $self->_dash_renderer_js_dependencies;
452              
453             # Traverse layout and recover javascript dependencies
454             # TODO auto register dependencies on component creation to avoid traversing and filter too much dependencies
455 0         0 my $layout = $self->layout;
456              
457 0         0 my $visitor;
458 0         0 my $stack_depth_limit = 1000;
459             $visitor = sub {
460 0     0   0 my $node = shift;
461 0         0 my $stack_depth = shift;
462 0 0       0 if ( $stack_depth++ >= $stack_depth_limit ) {
463              
464             # TODO warn user that layout is too deep
465 0         0 return;
466             }
467 0         0 my $type = ref $node;
468 0 0       0 if ( $type eq 'HASH' ) {
    0          
    0          
469 0         0 for my $key ( keys %$node ) {
470 0         0 $visitor->( $node->{$key}, $stack_depth );
471             }
472             } elsif ( $type eq 'ARRAY' ) {
473 0         0 for my $element (@$node) {
474 0         0 $visitor->( $element, $stack_depth );
475             }
476             } elsif ( $type ne '' ) {
477 0         0 my $node_dependencies = $node->_js_dist();
478 0 0       0 push @$scripts_dependencies, @$node_dependencies if defined $node_dependencies;
479 0 0       0 if ( $node->can('children') ) {
480 0         0 $visitor->( $node->children, $stack_depth );
481             }
482             }
483 0         0 };
484              
485 0         0 $visitor->( $layout, 0 );
486              
487 0         0 my $rendered_scripts = "";
488 0         0 $rendered_scripts .= $self->_render_dash_config();
489 0         0 push @$scripts_dependencies, @{ $self->_dash_renderer_js_deps() };
  0         0  
490 0         0 my $filtered_resources = $self->_filter_resources($scripts_dependencies);
491 0         0 my %rendered = ();
492 0         0 for my $dep (@$filtered_resources) {
493 0   0     0 my $dynamic = $dep->{dynamic} // 0;
494 0 0       0 if ( !$dynamic ) {
495 0         0 my $resource_path_part = join( "/", $dep->{namespace}, $dep->{relative_package_path} );
496 0 0       0 if ( !$rendered{$resource_path_part} ) {
497 0         0 $rendered_scripts .=
498             '' . "\n";
499 0         0 $rendered{$resource_path_part} = 1;
500             }
501             }
502             }
503 0         0 $rendered_scripts .= $self->_render_dash_renderer_script();
504              
505 0         0 return $rendered_scripts;
506             }
507              
508             sub _filter_resources {
509 0     0   0 my $self = shift;
510 0         0 my $resources = shift;
511 0         0 my %params = @_;
512 0   0     0 my $dev_bundles = $params{dev_bundles} // 0;
513 0   0     0 my $eager_loading = $params{eager_loading} // 0;
514 0   0     0 my $serve_locally = $params{serve_locally} // 1;
515              
516 0         0 my $filtered_resources = [];
517 0         0 for my $resource (@$resources) {
518 0         0 my $filtered_resource = {};
519 0         0 my $dynamic = $resource->{dynamic};
520 0 0       0 if ( defined $dynamic ) {
521 0         0 $filtered_resource->{dynamic} = $dynamic;
522             }
523 0         0 my $async = $resource->{async};
524 0 0       0 if ( defined $async ) {
525 0 0       0 if ( defined $dynamic ) {
526 0         0 die "A resource can't have both dynamic and async: " + to_json($resource);
527             }
528 0         0 my $dynamic = 1;
529 0 0       0 if ( $async eq 'lazy' ) {
530 0         0 $dynamic = 1;
531             } else {
532 0 0 0     0 if ( $async eq 'eager' && !$eager_loading ) {
533 0         0 $dynamic = 1;
534             } else {
535 0 0 0     0 if ( $async && !$eager_loading ) {
536 0         0 $dynamic = 1;
537             } else {
538 0         0 $dynamic = 0;
539             }
540             }
541             }
542 0         0 $filtered_resource->{dynamic} = $dynamic;
543             }
544 0         0 my $namespace = $resource->{namespace};
545 0 0       0 if ( defined $namespace ) {
546 0         0 $filtered_resource->{namespace} = $namespace;
547             }
548 0         0 my $external_url = $resource->{external_url};
549 0 0 0     0 if ( defined $external_url && !$serve_locally ) {
550 0         0 $filtered_resource->{external_url} = $external_url;
551             } else {
552 0         0 my $dev_package_path = $resource->{dev_package_path};
553 0 0 0     0 if ( defined $dev_package_path && $dev_bundles ) {
554 0         0 $filtered_resource->{relative_package_path} = $dev_package_path;
555             } else {
556 0         0 my $relative_package_path = $resource->{relative_package_path};
557 0 0       0 if ( defined $relative_package_path ) {
558 0         0 $filtered_resource->{relative_package_path} = $relative_package_path;
559             } else {
560 0         0 my $absolute_path = $resource->{absolute_path};
561 0 0       0 if ( defined $absolute_path ) {
562 0         0 $filtered_resource->{absolute_path} = $absolute_path;
563             } else {
564 0         0 my $asset_path = $resource->{asset_path};
565 0 0       0 if ( defined $asset_path ) {
566 0         0 my $stat_info = path( $resource->{filepath} )->stat;
567 0         0 $filtered_resource->{asset_path} = $asset_path;
568 0         0 $filtered_resource->{ts} = $stat_info->mtime;
569             } else {
570 0 0       0 if ($serve_locally) {
571 0         0 warn
572             'There is no local version of this resource. Please consider using external_scripts or external_stylesheets : '
573             + to_json($resource);
574 0         0 next;
575             } else {
576 0         0 die
577             'There is no relative_package-path, absolute_path or external_url for this resource : '
578             + to_json($resource);
579             }
580             }
581             }
582             }
583             }
584             }
585              
586 0         0 push @$filtered_resources, $filtered_resource;
587             }
588 0         0 return $filtered_resources;
589             }
590              
591             sub _filename_from_file_with_fingerprint {
592 1     1   13 my $self = shift;
593 1         1 my $file = shift;
594 1         7 my @path_parts = split( /\//, $file );
595 1         6 my @name_parts = split( /\./, $path_parts[-1] );
596              
597             # Check if the resource has a fingerprint
598 1 50 33     9 if ( ( scalar @name_parts ) > 2 && $name_parts[1] =~ /^v[\w-]+m[0-9a-fA-F]+$/ ) {
599 0         0 my $original_name = join( ".", $name_parts[0], @name_parts[ 2 .. ( scalar @name_parts - 1 ) ] );
600 0         0 $file = join( "/", @path_parts[ 0 .. ( scalar @path_parts - 2 ) ], $original_name );
601             }
602              
603 1         4 return $file;
604             }
605              
606             1;
607              
608             =pod
609              
610             =encoding UTF-8
611              
612             =head1 NAME
613              
614             Dash - Analytical Web Apps in Perl (Port of Plotly's Dash to Perl)
615              
616             =head1 VERSION
617              
618             version 0.09
619              
620             =head1 SYNOPSIS
621              
622             use Dash;
623             use aliased 'Dash::Html::Components' => 'html';
624             use aliased 'Dash::Core::Components' => 'dcc';
625            
626             my $external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'];
627            
628             my $app = Dash->new(
629             app_name => 'Basic Callbacks',
630             external_stylesheets => $external_stylesheets
631             );
632            
633             $app->layout(
634             html->Div([
635             dcc->Input(id => 'my-id', value => 'initial value', type => 'text'),
636             html->Div(id => 'my-div')
637             ])
638             );
639            
640             $app->callback(
641             Output => {component_id => 'my-div', component_property => 'children'},
642             Inputs => [{component_id=>'my-id', component_property=> 'value'}],
643             callback => sub {
644             my $input_value = shift;
645             return "You've entered '$input_value'";
646             }
647             );
648            
649             $app->run_server();
650              
651             use Dash;
652             use aliased 'Dash::Html::Components' => 'html';
653             use aliased 'Dash::Core::Components' => 'dcc';
654            
655             my $external_stylesheets = ['https://codepen.io/chriddyp/pen/bWLwgP.css'];
656            
657             my $app = Dash->new(
658             app_name => 'Random chart',
659             external_stylesheets => $external_stylesheets
660             );
661            
662             my $initial_number_of_values = 20;
663             $app->layout(
664             html->Div(children => [
665             dcc->Input(id => 'my-id', value => $initial_number_of_values, type => 'number'),
666             dcc->Graph(id => 'my-graph')
667             ])
668             );
669            
670             my $serie = [ map { rand(100) } 1 .. $initial_number_of_values];
671             $app->callback(
672             Output => {component_id => 'my-graph', component_property => 'figure'},
673             Inputs => [{component_id=>'my-id', component_property=> 'value'}],
674             callback => sub {
675             my $number_of_elements = shift;
676             my $size_of_serie = scalar @$serie;
677             if ($number_of_elements >= $size_of_serie) {
678             push @$serie, map { rand(100) } $size_of_serie .. $number_of_elements;
679             } else {
680             @$serie = @$serie[0 .. $number_of_elements];
681             }
682             return { data => [ {
683             type => "scatter",
684             y => $serie
685             }]};
686             }
687             );
688            
689             $app->run_server();
690              
691             =head1 DESCRIPTION
692              
693             This package is a port of L to Perl. As
694             the official Dash doc says: I.
695             So this Perl package is a humble atempt to ease the task of building data visualization web apps in Perl.
696              
697             The ultimate goal of course is to support everything that the Python version supports.
698              
699             The use will follow, as close as possible, the Python version of Dash so the Python doc can be used with
700             minor changes:
701              
702             =over 4
703              
704             =item * Use of -> (arrow operator) instead of .
705              
706             =item * Main package and class for apps is Dash
707              
708             =item * Component suites will use Perl package convention, I mean: dash_html_components will be Dash::Html::Components, although for new component suites you could use whatever package name you like
709              
710             =item * Instead of decorators we'll use plain old callbacks
711              
712             =item * Callback context is available as the last parameter of the callback but without the response part
713              
714             =item * Instead of Flask we'll be using L (Maybe in the future L)
715              
716             =back
717              
718             In the SYNOPSIS you can get a taste of how this works and also in L or directly in L. The full Dash tutorial is ported to Perl in those examples folder.
719              
720             =head2 Components
721              
722             This package ships the following component suites and are ready to use:
723              
724             =over 4
725              
726             =item * L as Dash::Core::Components
727              
728             =item * L as Dash::Html::Components
729              
730             =item * L as Dash::Table
731              
732             =back
733              
734             The plan is to make the packages also for L, L, L and L.
735              
736             =head3 Using the components
737              
738             Every component has a class of its own. For example dash-html-component Div has the class: L and you can use it the perl standard way:
739              
740             use Dash::Html::Components::Div;
741             ...
742             $app->layout(Dash::Html::Components::Div->new(id => 'my-div', children => 'This is a simple div'));
743              
744             But with every component suite could be a lot of components. So to ease the task of importing them (one by one is a little bit tedious) we could use two ways:
745              
746             =head4 Factory methods
747              
748             Every component suite has a factory method for every component. For example L has the factory method Div to load and build a L component:
749              
750             use Dash::Html::Components;
751             ...
752             $app->layout(Dash::Html::Components->Div(id => 'my-div', children => 'This is a simple div'));
753              
754             But this factory methods are meant to be aliased so this gets less verbose:
755              
756             use aliased 'Dash::Html::Components' => 'html';
757             ...
758             $app->layout(html->Div(id => 'my-div', children => 'This is a simple div'));
759              
760             =head4 Functions
761              
762             Many modules use the L & friends to reduce typing. If you like that way every component suite gets a Functions package to import all this functions
763             to your namespace.
764              
765             So for example for L there is a package L with one factory function to load and build the component with the same name:
766              
767             use Dash::Html::ComponentsFunctions;
768             ...
769             $app->layout(Div(id => 'my-div', children => 'This is a simple div'));
770              
771             =head3 I want more components
772              
773             There are L. So if you want to contribute I'll be glad to help.
774              
775             Meanwhile you can build your own component. I'll make a better guide and an automated builder but right now you should use L for all the javascript part (It's L based) and after that the Perl part is very easy (the components are mostly javascript, or typescript):
776              
777             =over 4
778              
779             =item * For every component must be a Perl class inheriting from L, overloaded the hash dereferencing %{} with the props that the React component has, and with this methods:
780              
781             =over 4
782              
783             =item DashNamespace
784              
785             Namespace of the component
786              
787             =item _js_dist
788              
789             Javascript dependencies for the component
790              
791             =item _css_dist
792              
793             Css dependencies for the component
794              
795             =back
796              
797             =back
798              
799             Optionally the component suite will have the Functions package and the factory methods for ease of using.
800              
801             As mentioned early, I'll make an automated builder but contributions are more than welcome!!
802              
803             Making a component for Dash that is not React based is a little bit difficult so please first get the javascript part React based and integrating it with Perl, R or Python will be easy.
804              
805             =head1 Missing parts
806              
807             Right now there are a lot of parts missing:
808              
809             =over 4
810              
811             =item * Prefix mount
812              
813             =item * Debug mode & hot reloading
814              
815             =item * Dash configuration (supporting environment variables)
816              
817             =item * Callback dependency checking
818              
819             =item * Clientside functions
820              
821             =item * Support for component properties data-* and aria-*
822              
823             =item * Dynamic layout generation
824              
825             =back
826              
827             And many more, but you could use it right now to make great apps! (If you need some inspiration... just check L)
828              
829             =head1 STATUS
830              
831             At this moment this library is experimental and still under active
832             development and the API is going to change!
833              
834             The intent of this release is to try, test and learn how to improve it.
835              
836             Security warning: this module is not tested for security so test yourself if you are going to run the app server in a public facing server.
837              
838             If you want to help, just get in contact! Every contribution is welcome!
839              
840             =head1 DISCLAIMER
841              
842             This is an unofficial Plotly Perl module. Currently I'm not affiliated in any way with Plotly.
843             But I think Dash is a great library and I want to use it with perl.
844              
845             If you like Dash please consider supporting them purchasing professional services: L
846              
847             =head1 SEE ALSO
848              
849             =over 4
850              
851             =item L
852              
853             =item L
854              
855             =item L
856              
857             =item L
858              
859             =item L
860              
861             =item L
862              
863             =back
864              
865             =head1 AUTHOR
866              
867             Pablo Rodríguez González
868              
869             =head1 COPYRIGHT AND LICENSE
870              
871             This software is Copyright (c) 2020 by Pablo Rodríguez González.
872              
873             This is free software, licensed under:
874              
875             The MIT (X11) License
876              
877             =cut
878              
879             __DATA__