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__