Added gui_basic example
parent
5306c3b819
commit
1c268976bb
@ -0,0 +1,5 @@
|
|||||||
|
/database.db
|
||||||
|
/web/webperl.js
|
||||||
|
/web/emperl.*
|
||||||
|
/gui_basic
|
||||||
|
/gui_basic.exe
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
|
||||||
|
WebPerl Basic GUI Example
|
||||||
|
=========================
|
||||||
|
|
||||||
|
This is a demo of a very basic GUI using WebPerl. It consists of a
|
||||||
|
local web server, which includes code to access an SQLite database,
|
||||||
|
and this web server also serves up WebPerl code to a browser, where
|
||||||
|
the GUI is implemented as HTML with Perl.
|
||||||
|
|
||||||
|
To get this to work, you will need to copy the `webperl.js` and three
|
||||||
|
`emperl.*` files from the main `web` directory to the `web`
|
||||||
|
subdirectory in this project.
|
||||||
|
|
||||||
|
Note that this should not be considered production-ready, as there
|
||||||
|
are several key features missing, such as HTTPS or access control.
|
||||||
|
|
||||||
|
Also, a limitation is that the server does not know when the browser
|
||||||
|
window is closed, so it must be stopped manually.
|
||||||
|
|
||||||
|
You can pack this application into a single executable using:
|
||||||
|
|
||||||
|
DOING_PAR_PACKER=1 pp -o gui_basic -z 9 -x -a gui_basic_app.psgi -a web gui_basic.pl
|
||||||
|
|
||||||
|
Note: I'm not yet sure why, but sometimes this fails with errors such
|
||||||
|
as *"error extracting info from -c/-x file"*, in that case just try
|
||||||
|
the above command again.
|
||||||
|
|
||||||
|
|
||||||
|
Author, Copyright, and License
|
||||||
|
==============================
|
||||||
|
|
||||||
|
**WebPerl - <http://webperl.zero-g.net>**
|
||||||
|
|
||||||
|
Copyright (c) 2019 Hauke Daempfling (haukex@zero-g.net)
|
||||||
|
at the Leibniz Institute of Freshwater Ecology and Inland Fisheries (IGB),
|
||||||
|
Berlin, Germany, <http://www.igb-berlin.de>
|
||||||
|
|
||||||
|
This program is free software; you can redistribute it and/or modify
|
||||||
|
it under the same terms as Perl 5 itself: either the GNU General Public
|
||||||
|
License as published by the Free Software Foundation (either version 1,
|
||||||
|
or, at your option, any later version), or the "Artistic License" which
|
||||||
|
comes with Perl 5.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful, but
|
||||||
|
**WITHOUT ANY WARRANTY**; without even the implied warranty of
|
||||||
|
**MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE**.
|
||||||
|
See the licenses for details.
|
||||||
|
|
||||||
|
You should have received a copy of the licenses along with this program.
|
||||||
|
If not, see <http://perldoc.perl.org/index-licence.html>.
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
use warnings;
|
||||||
|
use 5.018;
|
||||||
|
use FindBin;
|
||||||
|
use File::Spec::Functions qw/catdir/;
|
||||||
|
use Plack::Runner ();
|
||||||
|
use Starman ();
|
||||||
|
use Browser::Open qw/open_browser/;
|
||||||
|
|
||||||
|
# This just serves up gui_basic_app.psgi in the Starman web server.
|
||||||
|
# You can also say "plackup gui_basic_app.psgi" instead.
|
||||||
|
|
||||||
|
BEGIN {
|
||||||
|
my $dir = $ENV{PAR_TEMP} ? catdir($ENV{PAR_TEMP},'inc') : $FindBin::Bin;
|
||||||
|
chdir $dir or die "chdir $dir: $!";
|
||||||
|
}
|
||||||
|
|
||||||
|
my $SERV_PORT = 5000;
|
||||||
|
my $THE_APP = 'gui_basic_app.psgi';
|
||||||
|
|
||||||
|
# AFAICT, both Plack::Runner->new(@args) and ->parse_options(@argv) set
|
||||||
|
# options, and these options are shared between "Starman::Server"
|
||||||
|
# (documented in "starman") and "Plack::Runner" (documented in "plackup").
|
||||||
|
my @args = (
|
||||||
|
server => 'Starman', loader => 'Delayed', env => 'development',
|
||||||
|
version_cb => sub { print "Starman $Starman::VERSION\n" } );
|
||||||
|
my @argv = ( '--listen', "localhost:$SERV_PORT", $THE_APP );
|
||||||
|
my $runner = Plack::Runner->new(@args);
|
||||||
|
$runner->parse_options(@argv);
|
||||||
|
$runner->set_options(argv => \@argv);
|
||||||
|
die "loader shouldn't be Restarter" if $runner->{loader} eq 'Restarter';
|
||||||
|
|
||||||
|
if ($ENV{DOING_PAR_PACKER}) {
|
||||||
|
require Plack::Util;
|
||||||
|
Plack::Util::load_psgi($THE_APP); # for dependency resolution
|
||||||
|
# arrange to have the server shut down in a few moments
|
||||||
|
my $procpid = $$;
|
||||||
|
my $pid = fork();
|
||||||
|
if (!defined $pid) { die "fork failed" }
|
||||||
|
elsif ($pid==0) { sleep 5; kill 'INT', $procpid; exit; } # child
|
||||||
|
print "====> Please wait a few seconds...\n";
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
# There's a small chance here that the browser could open before the server
|
||||||
|
# starts up. In that case, a reload of the browser window is needed.
|
||||||
|
print "Attempting to open in browser: http://localhost:$SERV_PORT/\n";
|
||||||
|
open_browser("http://localhost:$SERV_PORT/");
|
||||||
|
}
|
||||||
|
|
||||||
|
$runner->run;
|
||||||
@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env perl
|
||||||
|
use warnings;
|
||||||
|
use 5.018;
|
||||||
|
use Plack::MIME;
|
||||||
|
use Plack::Builder qw/builder enable mount/;
|
||||||
|
use Plack::Request ();
|
||||||
|
use Plack::Response (); # declare compile-time dependency
|
||||||
|
use Cpanel::JSON::XS qw/decode_json encode_json/;
|
||||||
|
use DBI ();
|
||||||
|
use DBD::SQLite (); # declare compile-time dependency
|
||||||
|
use HTML::Tiny ();
|
||||||
|
|
||||||
|
# This is the server-side code.
|
||||||
|
|
||||||
|
# note we rely on gui_basic.pl to set the working directory correctly
|
||||||
|
my $SERV_ROOT = 'web';
|
||||||
|
my $DB_FILE = 'database.db';
|
||||||
|
|
||||||
|
my $dbh = DBI->connect("DBI:SQLite:dbname=$DB_FILE",
|
||||||
|
undef, undef, { RaiseError=>1, AutoCommit=>1 });
|
||||||
|
|
||||||
|
$dbh->do(q{ CREATE TABLE IF NOT EXISTS FooBar (
|
||||||
|
foo VARCHAR(255), bar VARCHAR(255) ) });
|
||||||
|
|
||||||
|
# This sends HTML to the browser, but we could also send JSON
|
||||||
|
# and build the HTML table dynamically in the browser.
|
||||||
|
my $app_select = sub {
|
||||||
|
state $html = HTML::Tiny->new;
|
||||||
|
state $sth_select = $dbh->prepare(q{ SELECT rowid,foo,bar FROM FooBar });
|
||||||
|
$sth_select->execute;
|
||||||
|
my $data = $sth_select->fetchall_arrayref;
|
||||||
|
my $out = $html->table(
|
||||||
|
[ \'tr',
|
||||||
|
[ \'th', 'rowid', 'foo', 'bar' ],
|
||||||
|
map { [ \'td', @$_ ] } @$data
|
||||||
|
] );
|
||||||
|
return [ 200, [ "Content-Type"=>"text/html" ], [ $out ] ];
|
||||||
|
};
|
||||||
|
|
||||||
|
# This is an example of one way to communicate with JSON.
|
||||||
|
my $app_insert = sub {
|
||||||
|
my $req = Plack::Request->new(shift);
|
||||||
|
state $sth_insert = $dbh->prepare(q{ INSERT INTO FooBar (foo,bar) VALUES (?,?) });
|
||||||
|
my $rv = eval { # catch errors and return as 500 Server Error
|
||||||
|
my $content = decode_json( $req->content );
|
||||||
|
$sth_insert->execute($content->{foo}, $content->{bar});
|
||||||
|
{ ok=>1 }; # return value from eval, sent to client as JSON
|
||||||
|
}; my $e = $@||'unknown error';
|
||||||
|
my $res = $req->new_response($rv ? 200 : 500);
|
||||||
|
$res->content_type($rv ? 'application/json' : 'text/plain');
|
||||||
|
$res->body($rv ? encode_json($rv) : 'Server Error: '.$e);
|
||||||
|
return $res->finalize;
|
||||||
|
};
|
||||||
|
|
||||||
|
Plack::MIME->add_type(".js" => "application/javascript");
|
||||||
|
Plack::MIME->add_type(".data" => "application/octet-stream");
|
||||||
|
Plack::MIME->add_type(".mem" => "application/octet-stream");
|
||||||
|
Plack::MIME->add_type(".wasm" => "application/wasm");
|
||||||
|
|
||||||
|
builder {
|
||||||
|
enable 'SimpleLogger';
|
||||||
|
enable 'Static',
|
||||||
|
path => sub { s#\A/\z#/index.html#; /\.(?:html?|js|css|data|mem|wasm|pl)\z/i },
|
||||||
|
root => $SERV_ROOT;
|
||||||
|
mount '/select' => $app_select;
|
||||||
|
mount '/insert' => $app_insert;
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en-us">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<title>WebPerl GUI Demo</title>
|
||||||
|
<script src="webperl.js"></script>
|
||||||
|
<script type="text/perl" src="web.pl"></script>
|
||||||
|
</head>
|
||||||
|
<body style="font-family:sans-serif;">
|
||||||
|
<h1>WebPerl GUI Demo</h1>
|
||||||
|
|
||||||
|
<div id="datatable"><i>No data loaded yet...</i></div>
|
||||||
|
<div><button id="reload_data">Reload Data</button></div>
|
||||||
|
|
||||||
|
<div style="margin-top:1em">
|
||||||
|
<div>
|
||||||
|
<label for="input_foo">foo</label>
|
||||||
|
<input type="text" id="input_foo">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="input_bar">bar</label>
|
||||||
|
<input type="text" id="input_bar">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button id="do_insert">Insert Data</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>Powered by <a href="http://webperl.zero-g.net" target="_blank">WebPerl</a> (beta)</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -0,0 +1,69 @@
|
|||||||
|
#!perl
|
||||||
|
use warnings;
|
||||||
|
use 5.028;
|
||||||
|
use WebPerl qw/js js_new sub1 encode_json/;
|
||||||
|
|
||||||
|
# This is the code that WebPerl runs in the browser. It is loaded by index.html.
|
||||||
|
|
||||||
|
sub do_xhr {
|
||||||
|
my %args = @_;
|
||||||
|
die "must specify a url" unless $args{url};
|
||||||
|
$args{fail} ||= sub { js('window')->alert(shift) };
|
||||||
|
my $xhr = js_new('XMLHttpRequest');
|
||||||
|
$xhr->addEventListener("error", sub1 {
|
||||||
|
$args{fail}->("XHR Error on $args{url}: ".(shift->{textContent}||"unknown"));
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
$xhr->addEventListener("load", sub1 {
|
||||||
|
if ($xhr->{status}==200) {
|
||||||
|
$args{done}->($xhr->{response}) if $args{done};
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$args{fail}->("XHR Error on $args{url}: ".$xhr->{status}." ".$xhr->{statusText});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
$xhr->addEventListener("loadend", sub1 {
|
||||||
|
$args{always}->() if $args{always};
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
# when given data, default to POST (JSON), otherwise GET
|
||||||
|
if ($args{data}) {
|
||||||
|
$xhr->open($args{method}||'POST', $args{url});
|
||||||
|
$xhr->setRequestHeader('Content-Type', 'application/json');
|
||||||
|
$xhr->send(encode_json($args{data}));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$xhr->open($args{method}||'GET', $args{url});
|
||||||
|
$xhr->send();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
my $document = js('document');
|
||||||
|
|
||||||
|
my $btn_reload = $document->getElementById('reload_data');
|
||||||
|
sub do_reload {
|
||||||
|
state $dtbl = $document->getElementById('datatable');
|
||||||
|
$btn_reload->{disabled} = 1;
|
||||||
|
do_xhr(url => 'select',
|
||||||
|
done => sub { $dtbl->{innerHTML} = shift; },
|
||||||
|
always => sub { $btn_reload->{disabled} = 0; } );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$btn_reload->addEventListener("click", \&do_reload);
|
||||||
|
|
||||||
|
my $btn_insert = $document->getElementById('do_insert');
|
||||||
|
sub do_insert {
|
||||||
|
state $txt_foo = $document->getElementById('input_foo');
|
||||||
|
state $txt_bar = $document->getElementById('input_bar');
|
||||||
|
$btn_insert->{disabled} = 1;
|
||||||
|
do_xhr(url => 'insert',
|
||||||
|
data => { foo=>$txt_foo->{value}, bar=>$txt_bar->{value} },
|
||||||
|
always => sub { $btn_insert->{disabled} = 0; do_reload; } );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$btn_insert->addEventListener("click", \&do_insert);
|
||||||
|
|
||||||
|
do_reload; # initial load
|
||||||
|
|
||||||
Loading…
Reference in New Issue