Indico front-end
A culinary journey from spaghetti to lasagna
The spaghetti
Validation
(server-side)
wvars["usersLimit"] = i18nformat("""
<tr>
<td nowrap class="displayField"><b> _("Max No. of registrants"):</b></td>
<td width="100%%" align="left">%s</td>
</tr>
""") % regForm.getUsersLimit()
HTML
(python-embedded)
html = "<table bgcolor=\"gray\"><tr><td bgcolor=\"white\">\n<font size=\"+1\"" +
"color=\"red\"><b>%s</b></font>\n</td></tr></table>"%self._params["msg"]
Forms
(inconsistent)
Components
(inconsistent)
CSS
(messy)
ul.UIPeopleList li.listMessage:hover, ul.UISuggestedPeopleList li.listMessage:hover
{
background-color: transparent !important;
}
ul.UIPeopleList li .nameLink, ul.UIAuthorList li .nameLink {
padding: 0px 0px 4px 30px;
}
ul.UIPeopleList li .foundUserEmail, ul.UISuggestedPeopleList li .foundUserEmail {
font-style: italic;
font-size: 8pt;
padding-left: 5px;
line-height: 15px;
display:none;
}
ul.UIPeopleList li:hover > .foundUserEmail, ul.UISuggestedPeopleList li:hover > .foundUserEmail {
display:inline;
}
1 single file. +8000 LOC.
JavaScript
(unscoped)
<script type="text/javascript">
var recordingSwitch = false;
var toggleRecordingCapableRooms = function () {
IndicoUI.Effect.toggleAppearance($E('recordingCapableRoomsDiv'));
if (recordingSwitch) {
$E("recordingRoomsText").dom.innerHTML = $T("See list of record-able rooms.");
} else {
$E("recordingRoomsText").dom.innerHTML = $T("Hide list of record-able rooms.");
}
recordingSwitch = !recordingSwitch;
}
</script>
$E("recordingRoomsText").dom.innerHTML
Icons!
(image-based)
loading.png
meeting.png
slides.png
group.png
delete.png
diskettes.png
key.png
logo.png
admins.png
Ingredients for
front-end spaghetti
- 1tbps of server-side validations
- 1 whole HTML-stuffed Python
- 500g of inconsistent components
- 1l of hard-to-trace JS code
- 78kg of long, entangled CSS
- Assortment of image icons
A better recipe
Validation & Rendering
class UserForm(Form):
name = StringField("First name", [DataRequired()])
l_name = StringField("Last name", [DataRequired()])
enabled = BooleanField("Enabled")
Forms and field definition
form = UserForm()
form.name(class_="main")
<input type="text" name="name" value=""
class="main" id="name" required>
Rendering
class PersonForm(UserForm):
age = IntegerField("Event", [DataRequired()])
def validate_age(form, field):
if field.data < 13:
raise ValidationError("You must be >13")
Inheritance, validation
form = PersonForm()
if form.validate_on_submit():
# handle request
Template engine
{% extends "layout.html" %}
{% block body %}
<ul>
{% for user in users %}
<li>
<a href="{{ user.url }}">
{{ user.username }}
</a>
</li>
{% endfor %}
</ul>
{% endblock %}
Inheritance, loops, interpolation
{% macro input(name, value='', type='text', size=20) -%}
<input type="{{ type }}"
name="{{ name }}"
value="{{ value|e }}"
size="{{ size }}">
{%- endmacro %}
Macros
{{ input('jetons', 1, type='number') }}
CSS with superpowers
$font: Helvetica;
$color: #333;
h1 {
font: $font;
color: #fff;
}
a {
color: $color;
}
.button {
font: $font;
color: $color;
}
h1 {
font: Helvetica;
color: #fff;
}
a {
color: #333;
}
.button {
font: Helvetica;
color: #333;
}
Variables
nav {
ul {
margin: 0;
padding: 0;
list-style: none;
}
li {
display: inline-block;
}
a {
display: block;
padding: 6px 12px;
text-decoration: none;
}
}
nav ul {
margin: 0;
padding: 0;
list-style: none;
}
nav li {
display: inline-block;
}
nav a {
display: block;
padding: 6px 12px;
text-decoration: none;
}
Nesting
@mixin radius($r) {
-webkit-border-radius: $r;
-moz-border-radius: $r;
-ms-border-radius: $r;
border-radius: $r;
}
.box1 {
@include radius(4px);
}
.box2 {
@include radius(4px);
}
.box1 {
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
border-radius: 4px;
}
.box2 {
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
-ms-border-radius: 4px;
border-radius: 4px;
}
Mixins
.message, .success, .error {
border: 1px solid #ccc;
padding: 10px;
color: #333;
}
.success {
border-color: green;
}
.error {
border-color: red;
}
.message {
border: 1px solid #ccc;
padding: 10px;
color: #333;
}
.success {
@extend .message;
border-color: green;
}
.error {
@extend .message;
border-color: red;
}
Inheritance
The good ol' JS library
$('button.continue').html('Next Step...')
var hiddenBox = $('#banner-message');
$('#button-container button').on('click', function(event) {
hiddenBox.show();
});
DOM Traversal and Manipulation
Event Handling
$.ajax({
url: '/api/getWeather',
data: {
zipcode: 97201
},
success: function(result) {
var html = '<strong>' + result + '</strong> degrees';
$('#weather-temp').html(html);
}
});
AJAX
Pixel perfect icons
Icomoon
Icomoon App
The Noun Project
Ingredients for
front-end lasagna
- WTForms for preventing wtf
- Jinja for HTML templates
- Sass for manageable CSS
- jQuery for taming the DOM
- Icomoon for 🎉
Putting everything toghether
class TextListField(TextAreaField):
def process_formdata(self, valuelist):
lines = valuelist[0].split('\n')
self.data = [line for line in lines]
def _value(self):
return '\n'.join(self.data) if self.data else ''
Custom WTForms fields #1
class EmailListField(TextListField):
def process_formdata(self, valuelist):
v = valuelist
super(EmailListField, self).process_formdata(v)
self.data = map(sanitize_email, self.data)
Custom WTForms fields #2
class IndicoPasswordField(PasswordField):
"""Password field that shows/hides the password."""
widget = PasswordWidget()
Custom WTForms fields #3
class FeaturesForm(IndicoForm):
payment = BooleanField("Enabled",
widget=SwitchWidget())
Custom WTForms fields #4
{% for field in form %}
<tr>
{% if field.type == "BooleanField" %}
<td></td>
<td>{{ field }} {{ field.label }}</td>
{% else %}
<td>{{ field.label }}</td>
<td>{{ field }}</td>
{% end %}
</tr>
{% endfor %}
WTForms rendering in Jinja
{% macro message_box(type, message) %}
<div class="{{ type }}-message-box">
<div class="message-box-content">
<span class="icon"></span>
<div class="message-text">
{{ message }}
</div>
</div>
</div>
{% endmacro %}
UI components in Jinja
Color palette in Sass
$darker-blue: #005272;
$dark-blue: #007CAC;
$blue: #5D95EA;
$pastel-blue: #CDE1FF;
$light-blue: #EDF3FD;
$red: #AF0000;
$dark-red: #A76766;
$pastel-red: #A76766;
$light-red: #F2DEDE;
CSS magic with Sass
@mixin _semantic-outline($color) {
@include border-all($color);
color: $color;
}
%semantic-outline {
&.success {
@include _semantic-outline($green);
}
&.error {
@include _semantic-outline($red);
}
// ...
}
Configurable components in Sass
.i-button
.i-button.success
.i-button.danger.outline
.i-button.highlight.dashed
Sass partials
sass
├── _base.scss
├── _custom.scss
├── _modules.scss
├── _partials.scss
├── _widgets.scss
├── base
│ ├── _defaults.scss
│ ├── ...
├── modules
│ ├── _abstracts.scss
│ ├── ...
├── partials
│ ├── _buttons.scss
│ ├── ...
├── screen.scss
11 directories, 140 files
Everything has its place
partials
├── _boxes.scss
├── _buttons.scss
├── _dialogs.scss
├── _dropdowns.scss
├── _forms.scss
├── _icons.scss
├── _links.scss
├── _lists.scss
├── _qtips.scss
├── _sidebars.scss
├── _spinner.scss
├── _timelines.scss
├── _toolbars.scss
└── ...
jQueryUI widgets
declarative.js
Health benefits of
front-end lasagna
- Extensible, reusable forms
- Consistent HTML snippets
- Uniform UI with CSS
- Isolated, easy-to maintain JS
- Crisp, light-weight icons
The lasagna
Alejandro Avilés
@OmeGak
Indico front-end
By Alejandro Avilés (Ome Gak)
Indico front-end
The before and after of Indico front-end technologies
- 1,790