Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

more work on repeatable js

  • Loading branch information...
commit 3ee7a54bc87db4d40e4cc0de24d0eb07684eb933 1 parent bf7de5c
@gshank authored
View
12 lib/HTML/FormHandler.pm
@@ -1059,6 +1059,17 @@ sub get_tag {
if ( $self->form && $self->form->block_exists($block_name) );
return '';
}
+has 'for_js' => (
+ isa => 'HashRef',
+ traits => ['Hash'],
+ is => 'rw',
+ default => sub { {} },
+ handles => {
+ set_for_js => 'set',
+ has_for_js => 'count',
+ clear_for_js => 'clear',
+ }
+);
has 'action' => ( is => 'rw' );
has 'posted' => ( is => 'rw', isa => 'Bool', clearer => 'clear_posted', predicate => 'has_posted' );
@@ -1256,6 +1267,7 @@ sub clear {
$self->clear_use_init_obj_over_item;
$self->clear_no_update;
$self->clear_info_message;
+ $self->clear_for_js;
}
sub values { shift->value }
View
7 lib/HTML/FormHandler/BuildFields.pm
@@ -346,10 +346,13 @@ sub _merge_updates {
$field_attr->{widget_wrapper} = $widget_wrapper;
}
# add widget and wrapper roles to field traits
- if( $widget ) {
+ if ( $widget ) {
my $widget_role = $self->get_widget_role( $widget, 'Field' );
+ push @{$field_attr->{traits}}, $widget_role;
+ }
+ if ( $widget_wrapper ) {
my $wrapper_role = $self->get_widget_role( $widget_wrapper, 'Wrapper' );
- push @{$field_attr->{traits}}, $widget_role, $wrapper_role;
+ push @{$field_attr->{traits}}, $wrapper_role;
}
}
return $field_attr;
View
61 lib/HTML/FormHandler/Field/AddElement.pm
@@ -0,0 +1,61 @@
+package HTML::FormHandler::Field::AddElement;
+use HTML::FormHandler::Moose;
+extends 'HTML::FormHandler::Field::Display';
+use HTML::FormHandler::Render::Util ('process_attrs');
+
+=head1 NAME
+
+HTML::FormHandler::Field::AddElement
+
+=head1 SYNOPSIS
+
+EXAMPLE field for rendering an AddElement field for
+doing javascript additions of repeatable elements.
+
+You probably want to make your own.
+
+The main requirements are that the button have 1) the
+'add_element' class, 2) a 'data-rep-id' attribute that
+contains the id of the repeatable to which you want to
+add an element.
+
+=head1 ATTRIBUTES
+
+ has_field 'add_element' => ( type => 'AddElement', repeatable => 'foo',
+ value => 'Add another foo',
+ );
+
+=head2 repeatable
+
+Requires the name of a Repeatable sibling field.
+
+=head2 value
+
+The value of the button that's rendered, 'Add Element' by default.
+
+=cut
+
+has 'repeatable' => ( is => 'rw', isa => 'Str', required => 1 );
+has '+do_wrapper' => ( default => 1 );
+has '+value' => ( default => 'Add Element' );
+
+sub build_render_method {
+ return sub {
+ my ( $self, $result ) = @_;
+ $result ||= $self->result;
+
+ my $rep_field = $self->parent->field($self->repeatable);
+ die "Invalid repeatable name in field " . $self->name unless $rep_field;
+ my $value = $self->html_filter($self->_localize($self->value));
+ my $attrs = $self->element_attributes($result);
+ push @{$attrs->{class}}, ( 'add_element', 'btn' );
+ $attrs->{'data-rep-id'} = $rep_field->id;
+ $attrs->{id} = $self->id;
+ my $attr_str = process_attrs($attrs);
+ my $output = qq{<div$attr_str>$value</div>};
+ $output = $self->wrap_field($self->result, $output);
+ return $output;
+ };
+}
+
+1;
View
69 lib/HTML/FormHandler/Field/Display.pm
@@ -41,51 +41,43 @@ or set the name of the rendering method:
....
}
-You can also supply an 'html' method with a trait or a custom field. See examples
-in t/field_traits.t and t/xt/display.t of the distribution.
+or provide a 'render_method':
+
+ has_field 'my_button' => ( type => 'Display', render_method => \&render_my_button );
+ sub render_my_button {
+ my $self = shift;
+ ....
+ return '...';
+ }
=cut
has 'html' => ( is => 'rw', isa => 'Str', builder => 'build_html', lazy => 1 );
sub build_html {''}
has 'set_html' => ( isa => 'Str', is => 'ro');
-sub _set_html_meth {
- my $self = shift;
- return $self->set_html if $self->set_html;
- my $name = $self->full_name;
- $name =~ s/\./_/g;
- $name =~ s/_\d+_/_/g;
- return 'html_' . $name;
-}
-sub _can_form_html {
- my $self = shift;
- my $set_html = $self->_set_html_meth;
- return
- unless $self->form &&
- $set_html &&
- $self->form->can( $set_html );
- return $set_html;
-}
-sub _form_html {
+has '+do_label' => ( default => 0 );
+
+has 'render_method' => (
+ traits => ['Code'],
+ is => 'ro',
+ isa => 'CodeRef',
+ lazy => 1,
+ predicate => 'does_render_method',
+ handles => { 'render' => 'execute_method' },
+ builder => 'build_render_method',
+);
+
+sub build_render_method {
my $self = shift;
- return unless (my $meth = $self->_can_form_html);
- if( $self->form->meta->has_attribute( $meth ) ) {
- return $self->form->$meth;
- }
- else {
- return $self->form->$meth($self);
- }
-}
-sub render {
- my $self = shift;
- if ( my $meth = $self->_can_form_html ) {
- return $self->form->$meth( $self );
- }
- elsif ( $self->html ) {
+ my $set_html = $self->set_html;
+ $set_html ||= "html_" . HTML::FormHandler::Field::convert_full_name($self->full_name);
+ return sub { my $self = shift; $self->form->$set_html($self); }
+ if ( $self->form && $self->form->can($set_html) );
+ return sub {
+ my $self = shift;
return $self->html;
- }
- return '';
+ };
}
sub _result_from_object {
@@ -96,11 +88,6 @@ sub _result_from_object {
return $result;
}
-after 'clear_data' => sub {
- my $self = shift;
- $self->clear_value;
-};
-
__PACKAGE__->meta->make_immutable;
use namespace::autoclean;
1;
View
35 lib/HTML/FormHandler/Field/Render.pm
@@ -1,35 +0,0 @@
-package HTML::FormHandler::Field::Render;
-# ABSTRACT: display only field
-
-use Moose;
-extends 'HTML::FormHandler::Field::NoValue';
-use namespace::autoclean;
-
-=head1 SYNOPSIS
-
-This is an alternative to the Display field. It allows
-you to provide a 'render_method', instead of using
-the Display field's method of providing html.
-
-=cut
-
-has 'render_method' => (
- traits => ['Code'],
- is => 'ro',
- isa => 'CodeRef',
- predicate => 'does_render_method',
- handles => { 'render' => 'execute_method' },
- default => sub { \&default_render },
-);
-
-sub default_render {
- my $self = shift;
- return $self->html;
-}
-
-has 'html' => ( is => 'rw', isa => 'Str', default => '' );
-
-
-__PACKAGE__->meta->make_immutable;
-use namespace::autoclean;
-1;
View
40 lib/HTML/FormHandler/Field/Repeatable.pm
@@ -139,7 +139,7 @@ has 'init_contains' => ( is => 'rw', isa => 'HashRef', traits => ['Hash'],
has 'num_when_empty' => ( isa => 'Int', is => 'rw', default => 1 );
has 'num_extra' => ( isa => 'Int', is => 'rw', default => 0 );
-has 'extra_for_js' => ( isa => 'Str', is => 'rw' );
+has 'setup_for_js' => ( isa => 'Bool', is => 'rw' );
has 'index' => ( isa => 'Int', is => 'rw', default => 0 );
has 'auto_id' => ( isa => 'Bool', is => 'rw', default => 0 );
has 'is_repeatable' => ( is => 'ro', default => 1 );
@@ -279,29 +279,28 @@ sub _result_from_input {
$index++;
}
}
- $self->_add_extra_for_js if $self->extra_for_js;
$self->index($index);
+ $self->_setup_for_js if $self->setup_for_js;
$self->result->_set_field_def($self);
return $self->result;
}
-sub _extra_for_js_id {
+sub _setup_for_js {
my $self = shift;
- return '' unless $self->extra_for_js;
- return $self->full_name . "." . $self->extra_for_js . ".wrp";
-}
-sub _add_extra_for_js {
- my $self = shift;
- $self->_add_extra($self->extra_for_js);
- my $field = $self->field($self->extra_for_js);
-
- # add an extra div around the field so that the html can be easily pulled out
- # need to clone tags, otherwise tags shared with all repeatable instances
- my $tags = data_clone( $field->tags );
- my $id = $self->_extra_for_js_id;
- $tags->{before_wrapper} = qq{<div class="for_js" id="$id">};
- $tags->{after_wrapper} = '</div>';
- $field->tags($tags);
+ return unless $self->form;
+ my $full_name = $self->full_name;
+ my $index_level =()= $full_name =~ /{index\d+}/g;
+ $index_level++;
+ my $field_name = "{index-$index_level}";
+ my $field = $self->_add_extra($field_name);
+ my $rendered = $field->render;
+ # remove extra result & field, now that it's rendered
+ $self->result->_pop_result;
+ $self->_pop_field;
+ # set the information in the form
+ # $self->index is the index of the next instance
+ $self->form->set_for_js( $self->full_name,
+ { index => $self->index, html => $rendered, level => $index_level } );
}
# this is called when there is an init_object or a db item with values
@@ -339,8 +338,8 @@ sub _result_from_object {
$index++;
}
}
- $self->_add_extra_for_js if $self->extra_for_js;
$self->index($index);
+ $self->_setup_for_js if $self->setup_for_js;
$values = \@new_values if scalar @new_values;
$self->_set_value($values);
$self->result->_set_field_def($self);
@@ -356,6 +355,7 @@ sub _add_extra {
$result = $field->_result_from_fields($result);
$self->result->add_result($result) if $result;
$self->add_field($field);
+ return $field;
}
sub add_extra {
@@ -394,8 +394,8 @@ sub _result_from_fields {
$index++;
$count--;
}
- $self->_add_extra_for_js if $self->extra_for_js;
$self->index($index);
+ $self->_setup_for_js if $self->setup_for_js;
$self->result->_set_field_def($self);
return $result;
}
View
1  lib/HTML/FormHandler/Fields.pm
@@ -57,6 +57,7 @@ has 'fields' => (
num_fields => 'count',
has_fields => 'count',
set_field_at => 'set',
+ _pop_field => 'pop',
}
);
# This is for updates applied via roles or compound field classes; allows doing
View
147 lib/HTML/FormHandler/Render/RepeatableJs.pm
@@ -0,0 +1,147 @@
+package HTML::FormHandler::Render::RepeatableJs;
+use Moose::Role;
+
+use JSON ('encode_json');
+
+=head1 NAME
+
+HTML::FormHandler::Render::RepeatableJs
+
+=head1 SYNOPSIS
+
+Creates jQuery javascript to add and delete repeatable
+elements.
+
+Note: This is still EXPERIMENTAL.
+This is an EXAMPLE.
+Changes are very likely to occur.
+Javascript is not guaranteed to be best practice.
+It will not work on all rendered repeatables (requires wrapper with id).
+It is strongly suggested that you make your own role if you use it.
+Then you can modify it as needed.
+Or just write out the rep_ data to javascript variables, and write the
+function in javascript.
+This function uses a plain javascript confirmation dialog.
+You almost certainly want to do something else.
+This javascript depends on the Bootstrap 'controls' div class
+in order to position the new elements. You will have to modify
+it to work if you don't use Bootstrap rendering.
+
+A role to be used in a Form Class:
+
+ package MyApp::Form::Test;
+ use HTML::FormHandler::Moose;
+ extends 'HTML::FormHandler';
+ with 'HTML::FormHandler::Render::RepeatableJs';
+ ...
+
+=head2 DESCRIPTION
+
+This contains one method, 'render_repeatable_js'. It's designed to be
+used in a template, something like:
+
+ [% WRAPPER "wrapper.tt" %]
+ [% form.render_repeatable_js %]
+ <h1>Editing Object .... </h1>
+ [% form.render %]
+ [% END -%]
+
+It will render javascript which can be used with the AddElement field,
+and setting the 'setup_for_js' flag in the Repeatable field to add
+the ability to dynamically add a new repeatable element in a form.
+
+Note: this code is provided as an example. You may need to write your
+own javascript function if your situation is different.
+
+Some of the extra information (level) in this function is in preparation for
+handling nested repeatables, but it's not supported yet.
+
+This function operates on HTML elements that have the id of the
+repeatable element. That requires that the wrapper have the repeatable
+instance ID (now rendered by default). If you don't have wrappers around
+your repeatable elements, this won't work.
+
+See HTML::FormHandler::Field::AddElement for an example of rendering
+an HTML element that can be used to provide the AddElement button.
+See that field for the requirements for the add HTML.
+
+There is no example of a remove button because it's very basic
+and there are too many different places to put it and ways to do it.
+The main requirements are that the button have a 'data-rep-elem-id'
+attribute that contains the id of the repeatable element to remove,
+and a class of 'rm_element'. It should be a child field of the
+repeatable.
+
+This one works:
+
+ has_field 'elements.rm_element' => (
+ type => 'Display', render_method => \&render_rm_element );
+ sub render_rm_element {
+ my $self = shift;
+ my $id = $self->parent->id;
+ return qq{<span class="btn rm_element" data-rep-elem-id="$id">Remove</span>};
+ }
+
+=cut
+
+sub render_repeatable_js {
+ my $self = shift;
+ return '' unless $self->has_for_js;
+
+ my $for_js = $self->for_js;
+ my %index;
+ my %html;
+ my %level;
+ foreach my $key ( keys %$for_js ) {
+ $index{$key} = $for_js->{$key}->{index};
+ $html{$key} = $for_js->{$key}->{html};
+ $level{$key} = $for_js->{$key}->{level};
+ }
+ my $index_str = encode_json( \%index );
+ my $html_str = encode_json( \%html );
+ my $level_str = encode_json( \%level );
+ my $js = <<EOS;
+<script>
+\$(document).ready(function() {
+ var rep_index = $index_str;
+ var rep_html = $html_str;
+ var rep_level = $level_str;
+ \$('.add_element').click(function() {
+ // get the repeatable id
+ var data_rep_id = \$(this).attr('data-rep-id');
+ // create a regex out of index placeholder
+ var level = rep_level[data_rep_id]
+ var re = new RegExp('\{index-' + level + '\}',"g");
+ // replace the placeholder in the html with the index
+ var index = rep_index[data_rep_id];
+ var html = rep_html[data_rep_id];
+ html = html.replace(re, index);
+ // escape dots in element id
+ var esc_rep_id = data_rep_id.replace(/[.]/g, '\\\\.');
+ // append new element in the 'controls' div of the repeatable
+ var rep_controls = \$('#' + esc_rep_id + ' > .controls');
+ rep_controls.append(html);
+ // increment index of repeatable fields
+ index++;
+ rep_index[data_rep_id] = index;
+ });
+
+ \$(document).on('click', '.rm_element', function() {
+ cont = confirm('Remove?');
+ if (cont) {
+ var id = \$(this).attr('data-rep-elem-id');
+ var esc_id = id.replace(/[.]/g, '\\\\.');
+ var rm_elem = \$('#' + esc_id);
+ rm_elem.remove();
+ }
+ event.preventDefault();
+ });
+
+});
+</script>
+EOS
+ return $js;
+}
+
+
+1;
View
1  lib/HTML/FormHandler/Result/Role.pm
@@ -39,6 +39,7 @@ has '_results' => (
clear_results => 'clear',
find_result_index => 'first_index',
set_result_at_index => 'set',
+ _pop_result => 'pop',
}
);
View
68 t/render/display.t
@@ -0,0 +1,68 @@
+use strict;
+use warnings;
+use Test::More;
+use HTML::FormHandler::Test;
+
+{
+ package Test::Field::Rendering;
+ use HTML::FormHandler::Moose;
+ extends 'HTML::FormHandler';
+
+ has_field 'my_html' => ( type => 'Display', html => '<h2>You got here!</h2>' );
+ has_field 'explanation' => ( type => 'Display' );
+ has_field 'between' => ( type => 'Display', set_html => 'between_html' );
+ has_field 'nolabel' => ( type => 'Text', do_label => 0 );
+
+ sub html_explanation {
+ my ( $self, $field ) = @_;
+ return "<p>I have an explanation somewhere around here...</p>";
+ }
+
+ sub between_html {
+ my ( $self, $field ) = @_;
+ return "<div>Somewhere, over the rainbow...</div>";
+ }
+
+}
+
+my $form = Test::Field::Rendering->new;
+is_html( $form->field('my_html')->render, '<h2>You got here!</h2>', 'display field renders with html attribute' );
+is_html( $form->field('explanation')->render, '<p>I have an explanation somewhere around here...</p>',
+ 'display field renders with form method' );
+is_html( $form->field('between')->render, '<div>Somewhere, over the rainbow...</div>',
+ 'set_html field renders' );
+is_html( $form->field('nolabel')->render, '
+<div><input type="text" name="nolabel" id="nolabel" value="" />
+</div>', 'do_label => 0 works');
+
+# test render_method
+{
+ package MyApp::Form::Test;
+ use HTML::FormHandler::Moose;
+ extends 'HTML::FormHandler';
+
+ has_field 'foo';
+ has_field 'bar' => (
+ type => 'Display',
+ render_method => \&render_bar,
+ );
+ sub render_bar {
+ my $self = shift; # $self is field
+ my $name = $self->name;
+ return "<p>This is field $name!</p>";
+ }
+ has_field 'moy' => (
+ type => 'Render',
+ html => '<p>From the html attribute...</p>',
+ );
+
+}
+
+$form = MyApp::Form::Test->new;
+
+my $rendered = $form->render;
+ok( $rendered, 'it rendered' );
+like( $rendered, qr/This is field bar/, 'rendered from render_method' );
+like( $rendered, qr/From the html attribute/, 'rendered from html attribute' );
+
+done_testing;
View
32 t/render/simple.t
@@ -226,36 +226,4 @@ ok( $output, 'get rendered output from form');
is_html( $form->render_field( $form->field('no_render')), '', 'no_render' );
-{
- package Test::Field::Rendering;
- use HTML::FormHandler::Moose;
- extends 'HTML::FormHandler';
-
- has_field 'my_html' => ( type => 'Display', html => '<h2>You got here!</h2>' );
- has_field 'explanation' => ( type => 'Display' );
- has_field 'between' => ( type => 'Display', set_html => 'between_html' );
- has_field 'nolabel' => ( type => 'Text', do_label => 0 );
-
- sub html_explanation {
- my ( $self, $field ) = @_;
- return "<p>I have an explanation somewhere around here...</p>";
- }
-
- sub between_html {
- my ( $self, $field ) = @_;
- return "<div>Somewhere, over the rainbow...</div>";
- }
-
-}
-
-$form = Test::Field::Rendering->new;
-is_html( $form->field('my_html')->render, '<h2>You got here!</h2>', 'display field renders' );
-is_html( $form->field('explanation')->render, '<p>I have an explanation somewhere around here...</p>',
- 'display field renders with form method' );
-is_html( $form->field('between')->render, '<div>Somewhere, over the rainbow...</div>',
- 'set_html field renders' );
-is_html( $form->field('nolabel')->render, '
-<div><input type="text" name="nolabel" id="nolabel" value="" />
-</div>', 'do_label => 0 works');
-
done_testing;
View
39 t/repeatable/extra_for_js.t
@@ -1,39 +0,0 @@
-use strict;
-use warnings;
-use Test::More;
-
-{
- package Test::User::Repeatable;
- use HTML::FormHandler::Moose;
- extends 'HTML::FormHandler';
-
- has_field 'user_name';
- has_field 'occupation';
- has_field 'employers' => ( type => 'Repeatable', extra_for_js => 'XXX' );
- has_field 'employers.employer_id' => ( type => 'PrimaryKey' );
- has_field 'employers.name';
- has_field 'employers.address';
-}
-my $form = Test::User::Repeatable->new;
-ok( $form->field('employers')->field('XXX'), 'extra field from field results');
-
-my $unemployed_params = {
- user_name => "No Employer",
- occupation => "Unemployed",
- 'employers.0.employer_id' => '', # empty string
- 'employers.0.name' => '',
- 'employers.0.address' => ''
-};
-$form->process( $unemployed_params);
-
-ok( $form->field('employers')->field('XXX'), 'extra field from input results' );
-
-my $rendered = $form->field('employers.XXX')->render;
-my $expected = qq{<div class="for_js" id="employers.XXX.wrp">};
-like($rendered, qr/$expected/, 'special wrapping div');
-
-$rendered = $form->field('employers.0')->render;
-unlike($rendered, qr/$expected/, 'no wrapping div on non-for_js element');
-
-
-done_testing;
View
37 t/repeatable/js.t
@@ -0,0 +1,37 @@
+use strict;
+use warnings;
+use Test::More;
+use HTML::FormHandler::Test;
+
+# an example of how to setup a form for adding repeatable elements.
+# not much to actually test here...
+{
+ package MyApp::Form::Test;
+ use HTML::FormHandler::Moose;
+ extends 'HTML::FormHandler';
+ with 'HTML::FormHandler::Render::RepeatableJs';
+
+ has_field 'foo' => ( type => 'Repeatable', setup_for_js => 1);
+ has_field 'foo.one';
+ has_field 'foo.two';
+ has_field 'add_element' => ( type => 'AddElement', repeatable => 'foo',
+ value => 'Add another foo',
+ );
+ has_field 'bar';
+
+}
+
+my $form = MyApp::Form::Test->new;
+ok( $form );
+
+ok( $form->has_for_js, 'for_js data is built');
+
+my $js = $form->render_repeatable_js;
+ok( $js, 'got some javascript' );
+
+my $expected = '<div><div class="add_element btn" data-rep-id="foo" id="add_element">Add another foo</div></div>';
+my $rendered = $form->field('add_element')->render;
+is_html( $rendered, $expected, 'add_element rendered ok' );
+
+
+done_testing;
View
18 t/template.t
@@ -0,0 +1,18 @@
+use strict;
+use warnings;
+use Test::More;
+
+{
+ package MyApp::Form::Test;
+ use HTML::FormHandler::Moose;
+ extends 'HTML::FormHandler';
+
+ has_field 'foo';
+ has_field 'bar';
+
+}
+
+my $form = MyApp::Form::Test->new;
+ok( $form );
+
+done_testing;

0 comments on commit 3ee7a54

Please sign in to comment.
Something went wrong with that request. Please try again.