NAME
Template::EmbeddedPerl::Cookbook::TypedViews - Refactor Contacts into typed views
DESCRIPTION
Experimental: Typed view support, including render_view, view, view_namespace, and view_factory, may change as real-world integration needs become clearer.
This document refactors the application from Template::EmbeddedPerl::Tutorial. It uses Moo to keep the examples concise, but Template::EmbeddedPerl accepts any blessed view object and does not require Moo at runtime.
The complete runnable refactor is in examples/contacts/typed. From the distribution root, run:
perl examples/contacts/typed/app.pl
It prints the same HTML as examples/contacts/untyped/app.pl. The checked-in t/cookbook_examples.t test runs both commands and verifies byte-for-byte output parity. The typed example uses Moo, so install Moo to run that example outside this distribution's development environment. Moo is optional and example-only for Template::EmbeddedPerl itself.
The files under examples/contacts/typed are the canonical complete example. The excerpts below are labeled fragments and should be read with those files.
WHY INTRODUCE A TYPED VIEW?
The untyped Contacts item has an explicit argument contract:
Fragment: examples/contacts/untyped/templates/contacts/item.epl
% args $contact, $root_title, $parent_title
<li data-root="<%= $root_title %>" data-parent="<%= $parent_title %>"><strong><%= $contact->{name} %></strong> <%= $contact->{email} %></li>
The caller must carry the contact and both identity values through every partial call. That is clear for a local template, but a growing list of related arguments is an object contract in disguise. The typed item instead puts its data and relationships in attributes:
Fragment: examples/contacts/typed/lib/Contacts/Typed/View/HTML/ContactItem.pm
has contact => (is => 'ro', required => 1);
has root => (is => 'ro', required => 1);
has parent => (is => 'ro', required => 1);
Typed templates receive their view as lexical $self, so the item can read $self->contact, $self->root, and $self->parent directly. This moves constructor validation and reusable behavior into ordinary Perl objects without changing the template engine's escaping or composition rules.
An object is not required for every template. Keep a small partial or layout untyped when its arguments are local to one composition site and an object would only add ceremony.
THE ROOT VIEW
Contacts::Typed::View::HTML::ContactList is the root object. Its title, contacts, and prebuilt badge are ordinary Moo attributes in the complete examples/contacts/typed/lib/Contacts/Typed/View/HTML/ContactList.pm file. The application builds that root and starts typed rendering explicitly:
Fragment: examples/contacts/typed/lib/Contacts/Typed/App.pm
$self->{root_view} = Contacts::Typed::View::HTML::ContactList->new(
title => 'Contacts',
contacts => $contacts,
prebuilt_badge => Contacts::Typed::View::HTML::Badge->new(
label => scalar(@$contacts) . ' contacts',
),
);
return $self->{engine}->render_view($self->{root_view});
render_view is the root entry point for a typed object and supplies that object as lexical $self in its template. Passing a blessed object to a legacy render call remains supported, but it remains in @_; it does not implicitly become $self.
The engine has view_namespace => 'Contacts::Typed::View'. For a root class below that namespace, it resolves the package suffix to a template identifier by converting each package segment independently to snake case:
Contacts::Typed::View::HTML::ContactList -> html/contact_list.epl
Thus the root uses examples/contacts/typed/templates/html/contact_list.epl. HTML becomes html, HTMLPage would become html_page, and ContactList becomes contact_list. The same ordered directories search path is used for typed views, untyped pages, partials, and layouts; the first matching template wins.
TYPED CHILD VIEWS
The root template has both kinds of child. Its complete source is examples/contacts/typed/templates/html/contact_list.epl. The relevant calls are:
Fragment: html/contact_list.epl
%= view $self->prebuilt_badge
% for my $contact (@{$self->contacts}) {
%= view 'HTML::ContactItem', contact => $contact
% }
The first call renders a preconstructed Contacts::Typed::View::HTML::Badge object. The second is a logical child name. The engine prefixes the configured view_namespace, loads Contacts::Typed::View::HTML::ContactItem, and constructs it with the values from the call.
Logical construction does not require Moo. Without a view_factory, the engine uses the class's ordinary new constructor and passes only explicitly supplied values. The HTML::ContactItem example uses a factory because its root and parent attributes need injection; a simple child with only a label can be constructed directly by its own new method.
ContactItem deliberately chooses a template that differs from its convention-derived path:
Fragment: examples/contacts/typed/lib/Contacts/Typed/View/HTML/ContactItem.pm
sub template { 'contacts/item' }
A nonempty template method has precedence over namespace convention lookup. Therefore this class renders contacts/item.epl, not its html/contact_item convention identifier. The Badge uses the same mechanism for contacts/badge.epl. Root ContactList and wrapper Page do not define a template method, so they use the convention paths.
WRAPPER VIEWS
Page is a typed wrapper. The root creates it with a final callback, which becomes the wrapper body:
Fragment: examples/contacts/typed/templates/html/contact_list.epl
%= view 'HTML::Page', title => $self->title, sub {
% my ($page) = @_;
<main>
<h1><%= display_heading $page->title %></h1>
%= view $self->prebuilt_badge
<ul>
% for my $contact (@{$self->contacts}) {
%= view 'HTML::ContactItem', contact => $contact
% }
</ul>
</main>
% }
Inside that callback, lexical $self remains the caller, the ContactList. The portable my ($page) = @_ unpacking receives the newly constructed Page. This endpoint uses $page->title for its heading; it is the same title as the ContactList, so the rendered HTML remains unchanged. The Page template sees a different $self: it is the Page object itself.
Fragment: examples/contacts/typed/templates/html/page.epl
% layout 'layouts/application', title => $self->title
%= content_replace 'head', sub {
<meta name="section" content="contacts">
% }
<section class="page">
%= yield
</section>
Here $self->title is Page's title and yield renders the body captured from the callback. The focused typed-view tests also cover nested wrappers: each wrapper gets its own body and yields it once.
INJECTING ROOT AND PARENT
The application configures view_factory when it creates the engine. Its three parameters have distinct roles: $class is the fully expanded logical class, $values is a hash reference containing exactly the arguments supplied by the template, and $context holds the current typed render context.
Fragment: examples/contacts/typed/lib/Contacts/Typed/App.pm
view_factory => sub {
my ($class, $values, $context) = @_;
my $view = $class->new(
%$values,
root => $context->root_view,
parent => $context->view,
);
push @{$self->{factory_calls}}, {
class => $class,
args => {%$values},
context => $context,
view => $view,
};
return $view;
},
$context->root_view is the original ContactList passed to render_view. $context->view is the caller at the logical view site. t/cookbook_examples.t checks the wrapper, then confirms its root and parent are the ContactList. It also confirms that each ContactItem in the Page callback receives the ContactList as parent because the callback keeps the caller scope. A logical child rendered inside a wrapper template would instead receive that wrapper as parent while retaining the original root.
The prebuilt_badge object was constructed by the application, not by a logical name. Rendering view $self->prebuilt_badge bypasses view_factory; t/cookbook_examples.t verifies that no Badge factory record was created. Preconstructed objects therefore let application code own special construction while logical names remain convenient for regular children.
COMPOSING THE VIEW TREE
Typed views use the existing composition helpers. In the complete Contacts refactor, display_heading is an application helper used from the typed root body, Page invokes the untyped layouts/application.epl layout, and content_replace 'head' contributes named content that the layout yields. The layout remains an ordinary untyped template with its own named argument contract:
Fragment: examples/contacts/typed/templates/layouts/application.epl
% args $title
<!doctype html>
<html>
<head>
<title><%= $title %></title>
%= yield 'head'
</head>
<body>
%= yield
</body>
</html>
This is intentional: a typed root or wrapper does not make every surrounding template typed. Partials, layouts, named content, helpers, automatic escaping, and ordered template lookup compose normally in the same render tree.
Use the complete examples/contacts/typed/app.pl endpoint when trying the tree. It constructs Contacts::Typed::App, renders the root, and prints the result. t/cookbook_examples.t checks that its output contains escaped <Ada> exactly once and matches the untyped application byte for byte.
CHOOSING BETWEEN BOTH DESIGNS
The final untyped application keeps data at its call sites: its root passes contact, root_title, and parent_title to the item partial, and its layout receives a title argument. This is direct and preferable for a small page, a one-off partial, or a layout whose behavior is only local composition.
The typed application moves the page, items, and wrapper into object contracts. Use that structure when constructor validation, reusable view methods, injected root/parent relationships, or framework-owned construction make the extra classes worthwhile. The examples use equivalent separate template trees and engine instances. They deliberately demonstrate the same helpers, layouts, named content, escaping, and byte-for-byte output while each endpoint remains independently runnable.
Moo is a concise implementation choice in the checked-in typed Contacts example, not a requirement of Template::EmbeddedPerl. Any blessed object that can serve as a view is valid. Typed view support remains experimental; keep the untyped design when it is the clearer fit.
FAILURES AND REUSE
Each top-level render, compiled-template render, or render_view call creates one render frame. Partials, layouts, nested views, and wrapper bodies share that frame. A cycle is rejected with the active render chain instead of recursing indefinitely.
If a nested template or constructor fails, the engine decorates the exception with one source-aware Render stack. It then cleans up the frame state, so a later top-level render on the same engine does not retain a body, named content, layout, or stack entry from the failed request. The typed-view tests cover wrapper-body restoration, render-stack diagnostics, cycle detection, and reuse after failure.
SEE ALSO
Start with Template::EmbeddedPerl::Tutorial for the untyped Contacts application, then use Template::EmbeddedPerl::Cookbook for independent recipes. The render_view, view, view_namespace, and view_factory API reference is in Template::EmbeddedPerl.