Skip to content

Step 6 styling form

opensas edited this page Nov 3, 2011 · 11 revisions

Step 6 - styling form

Purpose: in this step we will add twitter bootstrap styles to the create and update form. We will also develop a couple of custom tags to avoid repetitive code.

cd ~/devel/apps
git clone git://github.com/opensas/play-demo.git
cd play-demo
git checkout origin/06-styling_form

Applying twitter styles

Applying twitter styles is just a matter of specifying all the divs and classes required by the css files. This is the structure we have to reproduce for each input:

		<div class="clearfix ${errors.forKey('event.name') ? 'error' : ''}">
			<label for="event.name">Nombre</label>
			<div class="input">
				<input class="xlarge ${errors.forKey('event.name') ? 'error' : ''}" 
					id="event.name" name="event.name" size="30" type="text" value="${event.name}"/>
				<span class="help-inline">#{error "event.name" /}</span>
				<div class="help-inline">ingrese la descripción del evento</div>
			</div>
		</div> <!-- /clearfix -->

So our form.html page will end up like this

views/Application/form.html

#{extends 'main.html' /}
#{set title:'Home' /}

<section id="forms">

<div class="page-header">
  <h1>${event.id ? "Update event": "Create new event"}</h1>
</div>

<div class="row">
    <div class="span4 columns">
        <p>Enter the data for the next events.</p>
    </div>

    <div class="span12 columns">

        #{ifErrors}
            <div class="alert-message error">
                <a class="close" href="#">&times;</a>
                <p><strong>Oops! </strong>Errors detected.</p>
            </div>
        #{/ifErrors}
    
        #{form @save()}
        
        <input type="hidden" name="event.id" id="name" value="${event.id}" />
        
        <div class="clearfix ${errors.forKey('event.name') ? 'error' : ''}">
            <label for="event.name">Name</label>
            <div class="input">
                <input class="xlarge ${errors.forKey('event.name') ? 'error' : ''}" 
                    id="event.name" name="event.name" size="30" type="text" value="${event.name}"/>
                <span class="help-inline">#{error "event.name" /}</span>
                <div class="help-inline">enter the event's description'</div>
            </div>
        </div> <!-- /clearfix -->
        
        <div class="clearfix ${errors.forKey('event.type') ? 'error' : ''}">
            <label for="event.type.id">Type</label>
            <div class="input">
                <select name="event.type.id">
                    <option value=""${event.type==null ? ' selected' : ''}>--select an option--</option>
                    #{list types, as:'type'}
                        <option value="${type.id}"${event.type?.id==type.id ? ' selected' : ' '}>${type.name}</option>
                    #{/list}
                </select><br />
                <span class="help-inline">#{error "event.name" /}</span>
                <div class="help-inline">select the event's type'</div>
            </div>
        </div> <!-- /clearfix -->
        
        <div class="clearfix ${errors.forKey('event.place') ? 'error' : ''}">
            <label for="event.place">Place</label>
            <div class="input">
                <input class="xlarge ${errors.forKey('event.place') ? 'error' : ''}" 
                    id="event.place" name="event.place" size="30" type="text" value="${event.place}"/>
                <span class="help-inline">#{error "event.place" /}</span>
                <div class="help-inline">enter the event's place'</div>
            </div>
        </div> <!-- /clearfix -->
        
        <div class="clearfix ${errors.forKey('event.date') ? 'error' : ''}">
            <label for="event.date">Date</label>
            <div class="input">
                <input class="xlarge ${errors.forKey('event.date') ? 'error' : ''}" 
                    id="event.date" name="event.date" size="30" type="text" value="${event.date?.format('dd-MM-yyyy HH:mm')}"/>
                <span class="help-inline">#{error "event.date" /}</span>
                <div class="help-inline">enter the event's date' </div>
            </div>
        </div> <!-- /clearfix -->
        
        <div class="actions">
            <button type="submit" class="btn primary">Save changes</button>
            #{a @list(), class:"btn"}Cancel#{/a}
        </div>
        
        #{/form}
    
    </div> <!-- /div class="span12 columns"> -->

</div> <!-- /div class="row"> -->

</section>

Now this works ok, and looks pretty good, but the code is too verbose and repetitive. So we can develop a custom tag in order to make things easier.

A custom combo tag

Create the following file

views/tags/combo.html

%{
    _items = _items ?: [];
    _value = _value ?: 0;
    _name = _name ?: "";
    _showEmpty = _showEmpty ?: true;
}%
<select name="${_name}">
#{if _showEmpty}
    <option value=""${_value==null ? " selected " : ""}>--select--</option>
#{/if}
#{list items:_items, as:'item'}
    <option value="${item.id}"${item.id==_value ? " selected " : ""}>${item.name}</option>
#{/list}
</select>

and use it

views/Application/form.html

[...]
        <div class="clearfix ${errors.forKey('event.type') ? 'error' : ''}">
            <label for="event.type.id">Type</label>
            <div class="input">
                #{combo items:types, name:'event.type.id', value:event.type?.id /}
                <span class="help-inline">#{error "event.name" /}</span>
                <div class="help-inline">select the event's type</div>
            </div>
        </div> <!-- /clearfix -->
[...]

A custom input tag

We will create a custom input tags tag will let us specify each property but providing us with nice defaults.

views/tags/input.html

%{
	_fieldClass = _fieldClass ?: 'clearfix';
	_errorClass = _errorClass ?: 'error';
	_inputClass = _inputClass ?: 'xlarge';
	_helpClass = _helpClass ?: 'help-inline';
	_errorMessageClass = _errorMessageClass ?: _helpClass;
	_controlClass = _controlClass ?: 'input';

	_name = _name ?: 'field.name';
	_id = _id ?: _name;
	_label = _label ?: _name;
	_eval = _eval ?: _name;
	
	if (_value==null) {
		try {
			_value = evaluate('_caller.' + _eval);
		} catch (Exception e) {
			_value = '';
		}
	}
	
	_help = _help ?: '';
	
	_isError = play.data.validation.Validation.errors().forKey(_name);
	
	_errorClass =  _isError ? ' ' + _errorClass : '';
}%

<div class="${_fieldClass}${_errorClass}">
	<label for="${_name}">${_label}</label>
	<div class="${_controlClass}">
#{if _body}
	#{doBody /}
#{/if}
#{else}
		<input class="${_inputClass}${_errorClass}" id="${_id}" name="${_name}" size="30" type="text" value="${_value}"/>
#{/else}
		#{if _isError}<span class="${_errorMessageClass}">#{error _name /}</span>#{/if}
		#{if _help}<div class="${_helpClass}">${_help}</div>#{/if}
	</div>
</div> <!-- /clearfix -->

As you can see, most part of the code just initializes values and provides sensitives defaults

Now modifiy form.html to use our new tag.

views/Application/form.html

#{extends 'main.html' /}
#{set title:'Home' /}

<section id="forms">

<div class="page-header">
  <h1>${event.id ? "Update event": "Create new event"}</h1>
</div>

<div class="row">
    <div class="span4 columns">
        <p>Enter the data for the next events.</p>
    </div>

    <div class="span12 columns">

        #{ifErrors}
            <div class="alert-message error">
                <a class="close" href="#">&times;</a>
                <p><strong>Oops! </strong>Errors detected.</p>
            </div>
        #{/ifErrors}
    
        #{form @save()}
        
        <input type="hidden" name="event.id" id="name" value="${event.id}" />

        #{input name:'event.name', label:'Name', help:"enter the event's description" /}

        #{input name:'event.type.id', label:'Tipo', help:"select the event's type"}
            #{combo items:types, name:'event.type.id', value:event.type?.id /}
        #{/input}
        
        #{input name:'event.place', label:'Place', help:"enter the event's place" /}

        #{input name:'event.date', label:'Date & time', 
            value:event.date?.format('MM-dd-yyyy HH:mm'), 
            help:'enter date and time of the event (mm-dd-aaaa hh:mm)' 
        /}        
        
        <div class="actions">
            <button type="submit" class="btn primary">Save changes</button>
            #{a @list(), class:"btn"}Cancel#{/a}
        </div>
        
        #{/form}
    
    </div> <!-- /div class="span12 columns"> -->

</div> <!-- /div class="row"> -->

</section>

Final touches

We will also show the success message that comes from save action, in case the event was successfully saved.

views/Application/list.html

[...]
<div class="page-header">
    <h1>Events</h1>
</div>

#{if flash.success}
    <div class="alert-message success">
        <a class="close" href="#">&times;</a>
        <p>${flash.success}</p>
    </div>
#{/if}
[...]

And a little javascript to close the success and error message box. At the end of the main template add:

views/main.html

[...]
<script type="text/javascript">
    $('a.close').click( function() {
        $(this).parent().fadeOut(1000);
    })
</script>

Adding a timepicker

Go ahead and download Trent Richardson's time picker jquery plugin, and place it in public/javascripts.

You might also need to download some localization file for date picker and for time picker. Save them to public/javascripts/localization.

Just as it says on the plugin source site To use this plugin you must include jQuery and jQuery UI with datepicker and slider, so go to jquery ui download site create a custom download, select datepicker and slider, and select a theme, I chose Redmond.

Unzip the jqueryui custom file, and copy the theme folder to /public/stylesheet. Also copy the jquery-ui-1.8.16.custom.min file to /public/javascripts.

Next update the main.html file to include all the needed files, like this

views/maint.html

<!DOCTYPE html>

<html>
    <head>
        <title>#{get 'title' /}</title>
        <meta charset="${_response_encoding}">
        <link href="@{'/public/stylesheets/bootstrap-1.2.0.css'}" rel="stylesheet">
        <link rel="stylesheet" href="@{'/public/stylesheets/main.css'}" media="screen" >
        <link href="@{'/public/stylesheets/jquery-ui-timepicker-addon.css'}" rel="stylesheet">
        <link href="@{'/public/stylesheets/redmond/jquery-ui-1.8.16.custom.css'}" rel="stylesheet">
        #{get 'moreStyles' /}
        <link rel="shortcut icon" type="image/png" href="@{'/public/images/favicon.png'}">
        <script src="@{'/public/javascripts/jquery-1.6.3.min.js'}" type="text/javascript" charset="${_response_encoding}"></script>
        <script src="@{'/public/javascripts/jquery.tablesorter.min.js'}"></script>
        <script src="@{'/public/javascripts/jquery-ui-1.8.16.custom.min.js'}"></script>
        <script src="@{'/public/javascripts/jquery-ui-timepicker-addon.js'}"></script>
        #{get 'moreScripts' /}
    </head>
    <body>
        #{include 'partials/topbar.html' /}
        <div class="container">    
            #{doLayout /}
        </div> <!-- /container -->
        #{include 'partials/footer.html' /}
    </body>
</html>
<script type="text/javascript">
    $('a.close').click( function() {
        $(this).parent().fadeOut(1000);
    })
</script>

finally, add the following code at the end of form template

views/Application/form.html

[...]
<script type="text/javascript">
    $.datepicker.setDefaults($.datepicker.regional['es']);
    $('#event\\.date').datetimepicker( {
        dateFormat: 'dd-mm-yy',
        hour: 18,
        numberOfMonths: 2,
        hourGrid: 1,
        minuteGrid: 10,
        stepHour: 1,
        stepMinute: 10
    });
</script>
[...]

One little detail, timepicker css didn't get along quite well with twitter bootstrap, so I had to add

div.ui-datepicker { margin-top: 50px; }

to public/stylsheets/jquery-ui-timepicker-addon.css


Our crud page form is ready, now you can move on to Step 7 - ajaxed countdown.