From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

use utf8;
$Mojolicious::Plugin::DevexpressHelpers::Helpers::VERSION = '0.152320';
#ABSTRACT: Helpers for Devexpress controls are defined here
use MojoX::AlmostJSON qw(encode_json);
use constant DEBUG => 0;
#Not sure why C<out> function have to decode from utf8,
#but it make my day!
our $OUT_DECODE = 'UTF-8';
our $INDENT_BINDING = 0;
sub out{
my $tag = shift;
my $bytes = Mojo::ByteStream->new($tag);
return $bytes->decode($OUT_DECODE) if defined $OUT_DECODE;
return $bytes;
}
sub new{
my $class = shift;
my $self = bless {
next_id => 1,
bindings => '',
}, $class;
return $self;
}
sub indent_binding{
my $self = shift;
$INDENT_BINDING = shift;
}
sub add_binding{
my $self = shift;
$self->{bindings} .= join "\n", @_;
}
sub next_id{
my $self = shift;
return "dxctl".($self->{next_id}++);
}
sub new_id{
my ($c, $attrs) = @_;
#should compute a new uniq id
$c->stash('dxHelper')->next_id;
}
sub dxbind{
my ($c, $control, $id, $attrs, $extensions, $befores, $afters) = @_;
#should return html code to be associated to the control
$befores //=[];
$afters //=[];
my $jquery_id = $id;
$jquery_id =~ s{\.}{\\\\.}g;
my $binding = '$("#'.$jquery_id.'").'.$control.'(';
my @options;
if (ref($attrs) eq 'HASH') {
$binding .= '{';
$binding .= "\n " if $INDENT_BINDING;
for my $k ( sort keys %$attrs){
my $v = $attrs->{$k};
if(ref($v) eq 'SCALAR'){
#unref protected scalar
$v = $$v;
}
elsif ($v!~/^\s*(?:function\s*\()/) {
$v = encode_json $v;
}
push @options, "$k: $v";
}
}
else{
push @options, $attrs;
}
$binding .= join ",\n".($INDENT_BINDING?' ':''), @options;
$binding .= '}' if ref($attrs) eq 'HASH';
$binding .= ');' . ($INDENT_BINDING?"\n":"");
#append some extensions (eg: dxdatagrid)
$binding .= join ";\n".($INDENT_BINDING?' ':''), @$extensions if defined $extensions;
$c->stash('dxHelper')->add_binding($binding);
out join('',@$befores).'<div id="'.$id.'"></div>'.join('',@$afters);
}
sub parse_attributs{
my $c = shift;
my @implicit_args = @{shift()};
my %attrs;
IMPLICIT_ARGUMENT:
while(@_ and @implicit_args){
my $ref = ref($_[0]);
my $implicit = shift @implicit_args || '';
last unless $ref =~ /^(?:|SCALAR)$/
or (substr($implicit,0,1) eq '@' and $ref eq 'ARRAY')
or (substr($implicit,0,1) eq '%' and $ref eq 'HASH')
or (substr($implicit,0,1) eq '\\' and $ref eq 'REF')
or (substr($implicit,0,1) eq '*');
$implicit =~ s/^[\\\*\%\@]//;
$attrs{ $implicit } = shift @_;
}
if(my $args = shift){
if(ref($args) eq 'HASH'){
NAMED_ARGUMENT:
while(my($k,$v)=each %$args){
$attrs{$k} = $v;
}
}
}
return \%attrs;
}
sub dxmenu {
my $c = shift;
my $attrs = parse_attributs( $c, [qw(id @items onItemClick)], @_ );
my $id = delete($attrs->{id}) // new_id( $c, $attrs );
dxbind( $c, 'dxMenu' => $id => $attrs);
}
sub dxloadpanel {
my $c = shift;
my $attrs = parse_attributs( $c, [qw(id message)], @_ );
my $id = delete($attrs->{id}) // new_id( $c, $attrs );
dxbind( $c, 'dxLoadPanel' => $id => $attrs);
}
sub dxbutton {
my $c = shift;
my $attrs = parse_attributs( $c, [qw(id text onClick type)], @_ );
my $id = delete($attrs->{id}) // new_id( $c, $attrs );
dxbind( $c, 'dxButton' => $id => $attrs);
}
sub dxdatagrid{
my $c = shift;
my $attrs = parse_attributs( $c, [qw(id dataSource)], @_ );
my $id = delete($attrs->{id}) // new_id( $c, $attrs );
my @extensions;
#dxbind( $c, 'dxDataGrid' => $id => $attrs, [ $dataSource ]);
if ($attrs->{dataSource} && ref($attrs->{dataSource}) eq '') {
my $dataSource = delete $attrs->{dataSource};
#push @extensions, '$.getJSON("' . $dataSource . '",function(data){$("#'.$id.'").dxDataGrid({ dataSource: data });});';
#$attrs->{dataSource} = \'[]'; #protect string to be "stringified" within dxbind
#\"" is to protect string to be "stringified" within dxbind
$attrs->{dataSource} = \"{store:{type:'odata',url:'$dataSource'}}";
}
if (exists $attrs->{options}) {
$attrs = $attrs->{options};
}
dxbind( $c, 'dxDataGrid' => $id => $attrs, \@extensions);
}
sub dxpopup{
my $c = shift;
my $attrs = parse_attributs( $c, [qw(id title contentTemplate)], @_ );
my $id = delete($attrs->{id}) // new_id( $c, $attrs );
dxbind( $c, 'dxPopup' => $id => $attrs );
}
sub dxswitch{
my $c = shift;
my $attrs = parse_attributs( $c, [qw(id value label)], @_ );
my $id = delete($attrs->{id});
if (my $name = $id) {
$attrs->{attr}{name}=$name;
}
$id //= new_id( $c, $attrs );
$attrs->{onText} //= 'On';
$attrs->{offText} //= 'Off';
my (@before, @after);
if(my $label = delete($attrs->{label})){
push @before, '<div class="dx-field">';
push @before, '<div class="dx-field-label">'.$label.'</div>';
push @before, '<div class="dx-field-value">';
push @after, '</div>';
push @after, '</div>';
}
dxbind( $c, 'dxSwitch' => $id => $attrs, undef, \@before, \@after );
}
sub dxtextbox{
my $c = shift;
my $attrs = parse_attributs( $c, [qw(id value label)], @_ );
my $id = delete($attrs->{id});
if (my $name = $id) {
$attrs->{attr}{name}=$name;
}
$id //= new_id( $c, $attrs );
my (@before, @after);
if(my $label = delete($attrs->{label})){
push @before, '<div class="dx-field">';
push @before, '<div class="dx-field-label">'.$label.'</div>';
push @before, '<div class="dx-field-value">';
push @after, '</div>';
push @after, '</div>';
}
dxbind( $c, 'dxTextBox' => $id => $attrs, undef, \@before, \@after );
}
sub dxlookup{
my $c = shift;
my $attrs = parse_attributs( $c, [qw(id value label)], @_ );
my $id = delete($attrs->{id});
if (my $name = $id) {
$attrs->{attr}{name}=$name;
}
$id //= new_id( $c, $attrs );
my (@before, @after);
if(my $label = delete($attrs->{label})){
push @before, '<div class="dx-field">';
push @before, '<div class="dx-field-label">'.$label.'</div>';
push @before, '<div class="dx-field-value">';
push @after, '</div>';
push @after, '</div>';
}
dxbind( $c, 'dxLookup' => $id => $attrs, undef, \@before, \@after );
}
sub dxselectbox{
my $c = shift;
my $attrs = parse_attributs( $c, [qw(id value label)], @_ );
my $id = delete($attrs->{id});
if (my $name = $id) {
$attrs->{attr}{name}=$name;
}
$id //= new_id( $c, $attrs );
my (@before, @after);
if(my $label = delete($attrs->{label})){
push @before, '<div class="dx-field">';
push @before, '<div class="dx-field-label">'.$label.'</div>';
push @before, '<div class="dx-field-value">';
push @after, '</div>';
push @after, '</div>';
}
dxbind( $c, 'dxSelectBox' => $id => $attrs, undef, \@before, \@after );
}
sub dxbuild {
my $c = shift;
my $dxhelper = $c->stash('dxHelper') or return;
if($dxhelper->{bindings}){
out '<script language="javascript">$(function(){'.$dxhelper->{bindings}.'});</script>';
}
}
sub require_asset{
my $c = shift;
my $dxhelper = $c->stash('dxHelper') or return;
push @{ $dxhelper->{required_assets} }, $_ for @_;
return $c;
}
sub required_assets{
my $c = shift;
my $dxhelper = $c->stash('dxHelper') or return;
my $required_assets = $dxhelper->{required_assets} // [];
my $results = Mojo::ByteStream->new();
ASSET:
for my $asset (@$required_assets){
#not sure about how to simulate " %= asset 'resource' " that we can use in template rendering,
#nor how to output multiple Mojo::ByteStream objets at a time (is returning required ?)
$$results .= ${ $c->asset($asset) };
}
return $results;
}
#Helper method to export without prepending a prefix
my @without_prefix = qw( dxbuild required_assets require_asset indent_binding );
#Helper method to export with prepending a prefix
my @with_prefix = qw( Button DataGrid Popup TextBox TextArea Switch
Menu LoadPanel
SelectBox NumberBox List DateBox CheckBox Calendar Box Lookup );
sub register {
my ( $self, $app, $args ) = @_;
my $tp = $args->{tag_prefix};
SUB_NO_PREFIX:
for my $subname ( @without_prefix ){
my $lc_name = lc $subname;
my $sub = __PACKAGE__->can( $lc_name );
unless($sub){
$app->log->debug(__PACKAGE__." helper '$lc_name' does not exists!");
next SUB_NO_PREFIX;
}
$app->helper( $lc_name => $sub );
}
SUB_WITH_PREFIX:
for my $subname ( @with_prefix ){
my $lc_name = lc $subname;
my $sub = __PACKAGE__->can( 'dx' . $lc_name );
unless($sub){
$app->log->debug(__PACKAGE__." helper 'dx$lc_name' does not exists!");
next SUB_WITH_PREFIX;
}
say STDERR "## adding helper '$tp$lc_name'" if DEBUG;
$app->helper( $tp . $lc_name => $sub );
say STDERR "## adding helper '$tp$subname'" if DEBUG and $args->{tag_camelcase};
$app->helper( $tp . $subname => $sub ) if $args->{tag_camelcase};
}
}
1;
__END__
=pod
=encoding UTF-8
=head1 NAME
Mojolicious::Plugin::DevexpressHelpers::Helpers - Helpers for Devexpress controls are defined here
=head1 VERSION
version 0.152320
=head1 SUBROUTINES/METHODS
=head2 out
Output string in template
out '<div id="'.$id.'"></div>';
=head2 new
Internal usage.
my $dxHelper = Mojolicous::Plugin::DevexpressHelpers::Helpers->new;
$c->stash( dxHelper => $dxHelper );
=head2 indent_binding
$dxHelper->indent_binding( 1 );
=head2 add_binding
Internal usage.
$dxHelper->add_binding($binding, [$binding2,...] );
=head2 next_id
Internal usage.
my $next_id_number = $dxHelper->next_id;
=head2 new_id
Internal usage.
my $new_id = $dxHelper->new_id;
=head2 dxbind
Internal usage.
dxbind( $c, 'dxButton' => $id => $attrs, \@extensions);
Produce a div tag with an computed id, which will be binded to
a dxButton with C<$attrs> attributs at call to dxbuild
=head2 parse_attributs
Internal usage
my $attrs = parse_attributs( $c, \@implicit_arguments, @attributs )
=head2 dxmenu C< $id, \@items ,[ \%options ]>
%= dxmenu mainMenu => [{ id => 1, text => 'Back', iconSrc => '/images/back.png', link => 'javascript:location.back();' }], \q% function(e){return e.itemData.link;} %
=head2 dxloadpanel C<[ $id, [ $message ] ], [ \%options ]>
%= dxloadpanel myLoadPanel => 'Please wait'
=head2 dxbutton C<[ $id, [ $text, [ $onclick ] ] ], [ \%options ]>
%= dxbutton myButtonId => 'My button' => q{ function (){ alert('onClick!'); } }
%= dxbutton undef, 'My button' => '/some/url'
%= dxbutton {
id => myId,
text => 'My button',
onClick => q{ function (){ alert('onClick!'); } },
type => 'danger',
icon => 'user'
};
=head2 dxdatagrid C<[ $id, [ $datasource, ] ] [ \%opts ]>
%= dxdatagrid 'myID' => '/products.json', { columns => [qw( name description price )] }
%= dxdatagrid undef, '/products.json'
%= dxdatagrid { id => myId, dataSource => '/products.json' }
The following syntaxe allow to specify all options from a javascript object.
B<Note: It will ignore all other options specified in the hash reference.>
%= dxdatagrid myId, { options => 'JSFrameWork.gridsOptions.myResource' }
=head2 dxpopup C<[ $id, [ $title, [ $contentTemplate, ] ] ], [\%opts]>
%= dxpopup myPopupID => 'Popup Title', \q{function(contentElement){
contentElement.append('<p>Hello!</p>');
}};
=head2 dxswitch C<[ $id [, $value [, $label] ] ], [\%opts]>
%= dxswitch 'mySwitch' => $boolean_value => 'Enabled: '
=head2 dxtextbox C<[ $id, [ $value, [ $label, ] ] ], [\%opts]>
%= dxtextbox 'name' => $value => 'Name: ', { placeHolder => 'Type a name' }
=head2 dxlookup C<[ $id, [ $value, [ $label, ] ] ], [\%opts]>
%= dxlookup 'name' => $value => 'Name: ', { dataSource => $ds, valueExpr=> $ve, displayExpr => $de }
=head2 dxselectbox C<[ $id, [ $value, [ $label, ] ] ], [\%opts]>
%= dxselectbox 'name' => $value => 'Name: ', { dataSource => $ds, valueExpr=> $ve, displayExpr => $de }
=for comment TextArea
NumberBox
List
DateBox
CheckBox
Calendar
Box
=head2 dxbuild
Build the binding between jQuery and divs generated by plugin helpers such as dxbutton.
It is should to be called in your template just before you close the body tag.
<body>
...
%= dxbuild
</body>
=head2 require_asset @assets
Used to specify one or more assets dependencies, that will be appended on call to required_assets.
This function need 'AssetPack' plugin to be configurated in your application.
in your template:
<body>
...
%= require_asset 'MyScript.js'
...
</body>
in your layout:
<head>
...
%= required_assets
...
</head>
=head2 required_assets
Add assets that was specified by calls to require_asset.
See require_asset for usage.
=head2 register
Register our helpers
=head1 AUTHOR
Nicolas Georges <xlat@cpan.org>
=head1 COPYRIGHT AND LICENSE
This software is Copyright (c) 2015 by Nicolas Georges.
This is free software, licensed under:
The MIT (X11) License
=cut