Skip to content

Instantly share code, notes, and snippets.

@jjn1056
Last active April 27, 2025 14:15
Show Gist options
  • Save jjn1056/ecc4c1831f6997c60a55396421afabf8 to your computer and use it in GitHub Desktop.
Save jjn1056/ecc4c1831f6997c60a55396421afabf8 to your computer and use it in GitHub Desktop.
another form builder model example
Sketch to see if we can just use AUTOLOAD to inject into the
display object methods as needed ad hoc
# in this approach you have some named methods like $args
# and other methods use AUTOLOAD, defautl behavior is to call
# resolve_method_for_proxy($method) or if that doesn't exist
# dfaults to $obj->method.
# if you pass args to an AUTOLOADED method, that sets that
# method to have that value.
## THIS APPROACH MAKE NOT GREAT BECAUSE YOU ALSO NEED TO MAP ->errors
my $display_user = $user->proxy
->args($c)
->first_name
->last_name,
->dob
->state_id
->states($c->model('States')->alphabetical)
->default_state_id($c->model('States')->default_id);
my $fb = ::FormBuilder(model=>$display_user)
$fb->input('first_name')
$fb->collection_select('state_id', 'states', {default_selected=>$user=>default_state_id})
#same with this, the page object does title and form, uses AUTOLOAD on others
page
->title($user->naming->human)
->list_link($c->uri('../list', [$c->req->args]))
->form
->model($user)
->
Now slightly more complex, where instead of jsut making fiuelds we make objects to
more fully encapsulation the form display model. mY biggest issue here is that
this seems to be expensive to build it each time. Maybe better to do something like
with CatalystX::Request...
my $user_form_model = Valiant::HTML::FormModel
->new(model=>$user, edit_action=>..., create_action=>..., controller=>..., context=>$c) # active
->text('first_name', \%opts) # where %opts an be things like active/inactive or even html opts like disabled
->text('last_name') # should we allow adding validations here...? Also change the display name via { as=>... } ?
->date('dob', \%opts) # lets you do formatting etc
->collection_select('state_id', +{
collection => $c->model('States')->alphabetical->only([qw/id value/]),
default_selected => $c->model('States')->default_state_id,
})
# Or maybe this hangs on the view...
$c->view
->form(model=>$user, edit_action=>..., create_action=>..., controller=>..., sub ($view, $form) {
$form->text('first_name, ...)
->...
});
# and then in the formbuilder
$fb->input('first_name')
$fb->collection_select('state_id', { class=>'...})
# In this approach ->text creates an object that response to 'for_label', 'for_errors_for' and 'for_input'.
# so when you do $fb->input('first_name') you call $user_form_model->get('first_name')->value_for_input
# this way you can a hard error if you apply a text field to a select field (won't have the right methods)
# and you can display label and errors like ->text('first_name', {label=>0, errors=0}) if you don't want them
# to be permitted in the view.
# Then use %opts for tweaking that??? For example
my $user_form_model = $user->formbuilder_model
->text('first_name', \%opts)
# Is there a way to avoid needing to call $fb->input('field'....)? its a bit verbose. Would
# be cool if you could do something like:
my $fb = Valiant::HTML::Formbuilder->new(form_model=>$user_form_model);
my $user_form = FormModelToFormBuilderProxy->new(form_model=>$user_form_model);
<%= $user_form->first_name
->label
->input
->errors_for %>
# If you need complex layout:
%= $user_form->first_name(sub ($first_name_fb) {
<div>
%= $first_name_fb->label
%= $first_name_fb->input
%= $first_name_fb->errors_for
</div>
% })
## == Other stuff to think about =-
?? How does this work with the request body mapping? And then update or insert? Do
we make that separate or put that into this? If we merge it then maybe that argues for
a more CatalystX::Request approach
That might look like
package Example::Model::FormModels:User;
use Moose;
use Valiant::FormModel; # this will actually include Valiant::RepresentationModel::Form
# but that is also stand alone. Might need to autoload the representation
# model based on incomign content type...?
use Valiant::RepresentationModel::Form;
# use Valiant::Validates; # Can even have validations here
## It makes sense for the form model to know how to deserialize its POST body representation
## since this is the most common case for forms that round trip. But we break out Representation
## for those use cases when you need to grab incoming POST body and just work with that.
namespace 'user'; # Probably don't need this if there's a $model
has 'first_name' => (
is => 'rw',
text_field => \%opts,
from_model => 1,
to_model => 1, # maybe options around how to skip empty, etc
property => +{undef=>1, empty=>1}, # ren
);
has 'state_id' => (
is => 'rw',
collection_select => +{
collection => sub($self, $c) { $c->model('States')->alphabetical },
selected => sub($self, $c, $value) { return $value }, # probably the default right
default_selected => [ id => 'value', sub($self, $c) { $c->model('States')->default_state_id } ],
}
);
has 'credit_cards' => (
is=>'rw',
fields_from=>{
class =>':User::CreditCard',
populate=>sub($self, $item { ... }, # alter the items
finalize=>sub($self, @items) {
return $self->credit_cards->new, @item;
}, # some way to add an empty to end for 'new credit card'
},
);
package ::User::CreditCard;
has 'number' => (
is=>'rw',
text_field=>1,
);
has 'exp' => (
is=>'rw',
date_field=>1,
);
# in controller
# Will map $user and then overlap POST body
my $display_user = $c->model('DisplayUser', $user);
$display_user->save;
return if $display_user->invalid;
# Perhaps even
$display_user->email->changed->to_text_field(class=>...)->as_events; # would return the
##########
# maybe view (showing 'advanced interpolation mode, not yet done :)
has user => (is=>'ro');
has return_link (is=>'ro');
$self->page(sub ($page) {
$page->header(sub ($header) {
$header->title('User Page')
$header->add_script(sub ($script) {
$script->javascript('id', sub () {
alert('hello');
})
})
})
$page->body(sub ($body) {
$body->form('user', sub ($fb) {
<div>
$fb->first_name
->label
->input
->errors;
</div>
$fb->submit
})
$body->link_for('return_link');
})
})
## displaybuilder (for formattint dates, etc
$db->date('created', {strftime=>'%Y-%m-%d %H:%M %Z'})
$db->link_for('return_link');
## Wicket like ##
package Example::View::User;
use Moose;
use CatalystX::Lace;
has 'name' => (
is=>'ro',
required=>1,
label=>1, # could be a +{} like { visible=>0 }
);
has 'roles' => (
is => 'ro',
required => 1,
list => {
populate => sub($self, $role) {
return $self->label('role', $role, \%opts)
},
},
# or...
list => '::User::Role',
);
sub custom :Lace ($self, $node, $cb) {
return $cb->(
$self->label(\'foo'), # Valiant::HTML::Model::Label->new(value=>$val, ...)
$self->label(\'bar')
);
}
package Example::View::User::Role;
use Moose;
use CatalystX::Lace;
has 'role' => (
is=>'ro',
required=>1,
label=>1
);
use Moose;
use CatalystX::Lace;
__END__
# possible template
<h1 lace:id='name'></h1>
<div lace:id='roles'>
<p lace:id='role'></p>
</div>
<div lace='custom'>
<p lace:id='foo'></p>
<p lace:id='bar'></p>
</div>
####
Component
View
Page
ListView
#
If a controller inherits from Component::Page, it has a URL mount
If it inherits directly from Component or another subclass then it has an internal endpoint only
package MyApp::Controller::Welcome;
use Moose;
use CatalystX::Valiant::HTML::Component::Page;
use MyApp::Syntax;
sub mount($self, $app) {
$self->label('welcome', "Welcome to the homepage")
->link('about', sub() {...})
->
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment