06 - Template Formatting
As previously discussed, all templating logic takes place in the controllers. This includes standard placeholder population, form population and repeating elements such as table rows.
Basic HTML document manipulation
The simplest way to alter the template from the controller is by targeting the document's elements by their named id like this:
html.admin_email = 'foo@bar.com'
This will replace the content of the template element which looks like this:
<div id="admin-email"></div>
Underscores and Dashes
In the example above, html.admin_email was interpreted to mean the html element with the id of "admin-email". Since this conversion is automatic and enabled by default, it's important to stick with the ZenTools convention and always use dashes instead of underscores for ids -- or at least for any ids which will be targeted by the controller for that template.
Implicit _content attribute and HTML attributes
In the simple example above, we see that setting an element to a value populates the content of that element. That's because the _content attribute of that element is assumed to be implicit. In other words, this:
html.admin_email = 'foo@bar.com'
...is a shortcut for this:
html.admin_email._content = 'foo@bar.com'
Any other supplied attributes are assumed to be HTML attributes. This means that from your controller code, you can arbitrarily set HTML attributes. For example:
html.logo_image.src = '/images/logo.png'
or:
html.google_link.href = 'http://google.com'
One special attribute to be aware of is the HTML class attribute. Since class is a reserved keyword in Python, you can't do this:
WRONG
html.order_total.class = 'large-price' # This won't work
RIGHT
html.order_total._class = 'large-price'
So just remember to use "_class" when setting the html class attribute of an element.
Targeting elements without IDs
You can also target elements by the tag itself. For example:
html['form'].action = '/other-page'
Of course this will only be useful for pages on which there is only one form element.
Here's another example of this syntax:
html.shopping_cart['h1']._class = 'big-red-headline'
Notice that this last example is a chain of three elements. So in this case, only an h1 tag within the shopping-cart block is targeted. You can use this syntax for more specific targeting of elements within the DOM tree.
Now in the corresponding controller method for this template, we might do something like this:
Formatting placeholders using format_with()
While the above techniques are very useful for many situations, it's not ideal for all situations. For example, if we want to display a page of customer data consisting of a couple dozen attributes (first name, last name, address1, address2, etc, etc), then after fetching the customer record from the database, we'd have to arduously write out each line like this:
The arduous template
<div id="customer-info">
<p id="customer_first_name"></p>
<p id="customer_last_name"></p>
<p id="customer_address1"></p>
<p id="customer_address2"></p>
<!-- ...and so on -->
</div>
The arduous controller
def customer_info():
this_customer = models.Customer.get()
html.customer_first_name = this_customer.first_name
html.customer_last_name = this_customer.last_name
html.customer_address1 = this_customer.address1
html.customer_address2 = this_customer.address2
# ...and so on
Alternatively, we could use placeholders in our template and format them all at once using the format_with() method.
The easier template
<div id="customer-info">
<p>(customer.first_name)</p>
<p>(customer.last_name)</p>
<p>(customer.address1)</p>
<p>(customer.address2)</p>
<!-- ...and so on -->
</div>
The easier controller
def customer_info():
this_customer = models.Customer.get()
html.customer_info.format_with(customer = this_customer )
The format_with() method takes one or more named arguments and matches the named attributes to the template. In the above example, the returned Model object is assigned to customer so that it can be clearly seen how the format_with() method marries the object and the template. But a common idiom is to use the same name for both like this:
def customer_info():
customer = models.Customer.get(1)
html.customer_info.format_with(customer = customer)
You can also condense this down to just one line if you prefer:
def customer_info():
html.customer_info.format_with(customer = models.Customer.get())
Template placeholders are quite natural and understandable even to non-programmers who might need to occasionally make simple tweaks to the template. You can use as many (foo.bar) formatted placeholders as you want without fear of breaking anything. If there's no controller method trying to format those placeholders, they are simply ignored and treated as normal text.
Formatting rows
What if you want to display a list of customers? This means creating a template row and duplicating that row for as many customers as we find in the database. For this, we can use the append_clone() and replace_with_clones() methods.
In our controller, what we want to do is:
- Collect all the customers from the database
-
Grab the
customer-rowfrom the template -
Format each customer record as a clone of
customer-row - Replace the original template row with our clones
Don't worry, it's not as complicated as it sounds. Here's the controller:
def customers_list():
row = html.customer_row
customers = models.Customer.collect()
for customer in customers:
row.append_clone(customer = customer)
row.replace_with_clones()
And as in the template formatting example above, you can condense lines 3 and 4 into one line like this:
for customer in models.Customer.collect():
The append_clone() method accepts arguments in exactly the same way as the format_with() method. Using this technique, you're able to manipulate the data at each iteration and really do pretty much anything you might need to do as you iterate through the objects.
But if you just need to do something as simple as formatting the customer row with the customer data, then you can use the shortcut method duplicate_with() like this:
def customers_list():
html.customer_row.duplicate_with(customer = models.Customer.collect())
That single line of code will replace your template row with fully populated clones. So not only are your HTML templates sparkling clean and easy to read, so is your controller code.
It should be mentioned while probably the most common use of these methods is for duplicating rows in a table, you can actually use them to duplicate any element of your document. It doesn't have to be a <tr> element.
Populating forms
This is often one of the most laborious tasks of web development. For a failed form submission for example, you want to populate the form items with the values submitted by the user (otherwise the user has to re-enter all of the fields). Especially troublesome is the fact that each type of form field is populated differently. Text input fields are handled differently from select menus or radio buttons for example. This last problem is handled elegantly with the set() method:
Template for set() example
<form id="customer-form" method="post" action="">
<input name="name" type="text" />
<input name="gender" type="radio" value="male" />
<input name="gender" type="radio" value="female" />
</form>
Controller method using set()
def customer_form():
if post:
html.customer_form.set(name = post.name)
html.customer_form.set(gender = post.gender)
The set() method is very clever and can intelligently populate any named form element whether it's an input field, or a select menu or any other standard html form element.
Another even simpler way is to populate the entire form all at once is using the populate_with() method like this:
def customer_form():
if post:
html.customer_form.populate_with(post)
This works because the attributes of post will certainly match the names of the form elements since that's where they came from in the first place. Using populate_with(post) is a common idiom. Here's how the process works:
-
Form is submitted with
<input name="first_name" type="text" /> -
Submitted value is available to the controller as
post.first_name -
Form is populated with post:
html.my_form.populate_with(post) -
For each named form element which has a corresponding attribute in the specified object, the
set()method is automatically applied. In other words, since the form contains an element namedfirst_name, and sincepostalso has an attribute offirst_name, the form element will have its value set to the same value which was posted.
Next: 07 - Models