use strict; package HTML::FormFu::Element::ComboBox; $HTML::FormFu::Element::ComboBox::VERSION = '2.07'; # ABSTRACT: Select / Text hybrid use Moose; use MooseX::Attribute::Chained; extends 'HTML::FormFu::Element::Multi'; with 'HTML::FormFu::Role::Element::ProcessOptionsFromModel'; use HTML::FormFu::Util qw( _filter_components _parse_args ); use List::Util 1.33 qw( any ); use Moose::Util qw( apply_all_roles ); our @DEFER_TO_SELECT = qw( empty_first empty_first_label values value_range ); for my $name (@DEFER_TO_SELECT) { has $name => ( is => 'rw', traits => ['Chained'] ); } has select => ( is => 'rw', traits => ['Chained'], default => sub { {} } ); has text => ( is => 'rw', traits => ['Chained'], default => sub { {} } ); *default = \&value; ## build get_Xs methods for my $method ( qw( deflator filter constraint inflator validator transformer ) ) { my $sub = sub { my $self = shift; my %args = _parse_args(@_); my $get_method = "get_${method}s"; my $accessor = "_${method}s"; my @x = @{ $self->$accessor }; push @x, map { @{ $_->$get_method(@_) } } @{ $self->_elements }; return _filter_components( \%args, \@x ); }; my $name = __PACKAGE__ . "::get_${method}s"; ## no critic (ProhibitNoStrict); no strict 'refs'; *{$name} = $sub; } after BUILD => sub { my ( $self, $args ) = @_; $self->multi_value(1); $self->empty_first(1); return; }; sub options { my ( $self, @args ) = @_; if (@args) { $self->{options} = @args == 1 ? $args[0] : \@args; return $self; } else { # we're being called as a getter! # are the child elements made yet? if ( !@{ $self->_elements } ) { # need to build the children, so we can return the select options $self->_add_elements; } return $self->_elements->[0]->options; } } sub value { my ( $self, $value ) = @_; if ( @_ > 1 ) { $self->{value} = $value; # if we're already built - i.e. process() has been called, # call default() on our children if ( @{ $self->_elements } ) { $self->_combobox_defaults; $self->_elements->[0]->default( $self->select->{default} ); $self->_elements->[1]->default( $self->text->{default} ); } return $self; } return $self->{value}; } sub _add_elements { my ($self) = @_; $self->_elements( [] ); $self->_add_select; $self->_add_text; $self->_combobox_defaults; return; } sub _combobox_defaults { my ($self) = @_; if ( defined( my $default = $self->default ) ) { if ( !$self->form->submitted || $self->render_processed_value ) { for my $deflator ( @{ $self->_deflators } ) { $default = $deflator->process($default); } } my $select_options = $self->_elements->[0]->options; if ( $default ne '' && any { $_->{value} eq $default } @$select_options ) { $self->select->{default} = $default; $self->text->{default} = undef; } else { $self->select->{default} = undef; $self->text->{default} = $default; } } return; } sub _add_select { my ($self) = @_; my $select = $self->select; my $select_name = _build_field_name( $self, 'select' ); my $select_element = $self->element( { type => 'Select', name => $select_name, } ); apply_all_roles( $select_element, 'HTML::FormFu::Role::Element::MultiElement' ); for my $method (@DEFER_TO_SELECT) { if ( defined( my $value = $self->$method ) ) { $select_element->$method($value); } } if ( !@{ $select_element->options } ) { # we need to access the hashkey directly, # otherwise we'll have a loop $select_element->options( $self->{options} ); } if ( defined( my $default = $select->{default} ) ) { $select_element->default($default); } return; } sub _add_text { my ($self) = @_; my $text = $self->text; my $text_name = _build_field_name( $self, 'text' ); my $text_element = $self->element( { type => 'Text', name => $text_name, } ); apply_all_roles( $text_element, 'HTML::FormFu::Role::Element::MultiElement' ); if ( defined( my $default = $text->{default} ) ) { $text_element->default($default); } return; } sub get_select_field_nested_name { my ($self) = @_; my $select_name = _build_field_name( $self, 'select' ); return $self->get_element( { name => $select_name } )->nested_name; } sub get_text_field_nested_name { my ($self) = @_; my $text_name = _build_field_name( $self, 'text' ); return $self->get_element( { name => $text_name } )->nested_name; } sub _build_field_name { my ( $self, $type ) = @_; my $options = $self->$type; my $name; if ( defined( my $default_name = $options->{name} ) ) { $name = $default_name; } else { $name = sprintf "%s_%s", $self->name, $type; } return $name; } sub process { my ( $self, @args ) = @_; $self->_process_options_from_model; $self->_add_elements; return $self->SUPER::process(@args); } sub process_input { my ( $self, $input ) = @_; my $select_name = $self->get_select_field_nested_name; my $text_name = $self->get_text_field_nested_name; my $select_value = $self->get_nested_hash_value( $input, $select_name ); my $text_value = $self->get_nested_hash_value( $input, $text_name ); if ( defined $text_value && length $text_value ) { $self->set_nested_hash_value( $input, $self->nested_name, $text_value, ); } elsif ( defined $select_value && length $select_value ) { $self->set_nested_hash_value( $input, $self->nested_name, $select_value, ); } return $self->SUPER::process_input($input); } sub render_data { return shift->render_data_non_recursive(@_); } sub render_data_non_recursive { my ( $self, $args ) = @_; my $render = $self->SUPER::render_data_non_recursive( { elements => [ map { $_->render_data } @{ $self->_elements } ], $args ? %$args : (), } ); return $render; } __PACKAGE__->meta->make_immutable; 1; __END__ =pod =encoding UTF-8 =head1 NAME HTML::FormFu::Element::ComboBox - Select / Text hybrid =head1 VERSION version 2.07 =head1 SYNOPSIS --- elements: - type: ComboBox name: answer label: 'Select yes or no, or write an alternative:' values: - yes - no =head1 DESCRIPTION Creates a L element containing a Select field and a Text field. A ComboBox element named C would result in a Select menu named C and a Text field named C. The names can instead be overridden by the C value in L and L. If a value is submitted for the Text field, this will be used in preference to any submitted value for the Select menu. You can access the submitted value by using the ComboBox's name: my $value = $form->param_value('foo'); =head1 METHODS =head2 default If the value matches one of the Select menu's options, that options will be selected. Otherwise, the Text field will use the value as its default. =head2 options See L for details. =head2 values See L for details. =head2 value_range See L for details. =head2 empty_first See L for details. =head2 empty_first_label See L for details. =head2 select Arguments: \%setting Set values effecting the Select menu. Known keys are: =head3 name Override the auto-generated name of the select menu. =head2 text Arguments: \%setting Set values effecting the Text field. Known keys are: =head3 name Override the auto-generated name of the select menu. =head1 CAVEATS Although this element inherits from L, its behaviour for the methods Lfilters|HTML::FormFu/filters>, Lconstraints|HTML::FormFu/constraints>, Linflators|HTML::FormFu/inflators>, Lvalidators|HTML::FormFu/validators> and Ltransformers|HTML::FormFu/transformers> is more like that of a L, meaning all processors are added directly to the date element, not to its child elements. This element's L and L are inherited from L, and so have the same behaviour. However, it overrides the C method, such that it returns both itself and its child elements. =head1 SEE ALSO Is a sub-class of, and inherits methods from L, L, L L =head1 AUTHOR Carl Franks, C =head1 LICENSE This library is free software, you can redistribute it and/or modify it under the same terms as Perl itself. =head1 AUTHOR Carl Franks =head1 COPYRIGHT AND LICENSE This software is copyright (c) 2018 by Carl Franks. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. =cut