Forms
Basic Inputs
Examples of standard form inputs used throughout the Control Portal. Add the .form-control
class to each input type to get proper formatting and alignment.
Text
<input type="text" class="form-control" placeholder="Text Input">
Password
Examples of standard form inputs used throughout the Control Portal.
<input type="password" class="form-control" placeholder="Password">
Textarea
<textarea class="form-control" rows="3"></textarea>
Checkbox and Radios
<div class="checkbox">
<label>
<input type="checkbox" value="" /> Option 1</a>
</label>
</div>
<div class="checkbox disabled">
<label>
<input type="checkbox" value="" disabled>
Option two is disabled
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="optionsRadios" id="optionsRadios1" value="option1" checked>
Option one is this and that—be sure to include why it's great
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="optionsRadios" id="optionsRadios2" value="option2">
Option two can be something else and selecting it will deselect option one
</label>
</div>
<div class="radio disabled">
<label>
<input type="radio" name="optionsRadios" id="optionsRadios3" value="option3" disabled>
Option three is disabled
</label>
</div>
Select Dropdowns
- If there is only one item in the select options, than that item should automatically be selected for the user.
- If there are multiple items in the list but there is a context for how the user has arrived, select the logical choice. For example, a user selects the create server button on the WA1 - US West (Seattle) Data Center Overview page, the logical choice for the Data Center dropdown would be WA1 - US West (Seattle).
- If there is no logical defaults then the user should be presented with a contextual message that tells them what to do as the first item in the list (e.g. ‘Select a Data Center...’)
<select class="form-control">
<option>1</option>
<option>2</option>
<option>3</option>
<option>4</option>
<option>5</option>
</select>
Hierarchy Picker
When it is important to display the hierarchy of a collection (e.g. servers, groups, or organizations/accounts) in the selection process, use a hierarchy picker.
Single-Select Picker
If you want to allow the user to only pick one item use radio buttons.
<div class="picker">
<ol>
<li>
<label class="item-hover">
<input type="radio" name="group"/>
<span class="content">
<svg class="cyclops-icon"><use xlink:href='#icon-folder'></svg> <span>Level 1a</span>
</span>
</label>
</li>
<li>
<label class="item-hover">
<input type="radio" name="group"/>
<span class="content">
<svg class="cyclops-icon"><use xlink:href='#icon-folder'></svg> <span>Level 1b</span>
</span>
</label>
<ol>
<li>
<label class="item-hover">
<input type="radio" name="group"/>
<span class="content">
<svg class="cyclops-icon"><use xlink:href='#icon-folder'></svg> <span>Level 2a</span>
</span>
</label>
</li>
</ol>
</li>
</ol>
</div>
Multi-Select Picker
If you want to allow the user pick many items use checkboxes.
<div class="picker">
<ol>
<li>
<label class="item-hover">
<span class="content">
<svg class="cyclops-icon"><use xlink:href='#icon-folder'></svg> <span>Default Group</span>
</span>
</label>
<ol>
<li>
<label class="item-hover">
<input type="checkbox"/>
<span class="content">
<svg class="cyclops-icon"><use xlink:href='#icon-play'></svg> <span>QA1DEVTEST01</span>
</span>
</label>
</li>
<li>
<label class="item-hover">
<input type="checkbox"/>
<span class="content">
<svg class="cyclops-icon"><use xlink:href='#icon-play'></svg> <span>QA1DEVTEST02</span>
</span>
</label>
</li>
</ol>
</li>
<li>
<label class="item-hover">
<span class="content">
<svg class="cyclops-icon"><use xlink:href='#icon-folder'></svg> <span>Web Servers</span>
</span>
</label>
<ol>
<li>
<label class="item-hover">
<input type="checkbox" disabled/>
<span class="content">
<svg class="cyclops-icon"><use xlink:href='#icon-stop'></svg> <span>QA1DEVSRVR01</span>
</span>
</label>
</li>
<li>
<label class="item-hover">
<input type="checkbox"/>
<span class="content">
<svg class="cyclops-icon"><use xlink:href='#icon-play'></svg> <span>QA1DEVSRVR02</span>
</span>
</label>
</li>
<li>
<label class="item-hover">
<input type="checkbox"/>
<span class="content">
<svg class="cyclops-icon"><use xlink:href='#icon-play'></svg> <span>QA1DEVSRVR03</span>
</span>
</label>
</li>
</ol>
</li>
</ol>
</div>
One Label with Stacked Inputs
By default, inputs will automatically stack when placed in a single column.
<form class="form-horizontal">
<fieldset>
<div class="form-group">
<label class="col-sm-3 control-label">nodes</label>
<div class="col-sm-6">
<input type="text" id="node1" class="form-control" placeholder="ip address">
<input type="text" id="node2" class="form-control" placeholder="ip address">
<input type="text" id="node3" class="form-control" placeholder="ip address">
<input type="text" id="node4" class="form-control" placeholder="ip address">
</div>
</div>
</fieldset>
</form>
One Label with Adjacent Inputs
When it is more appropriate to adjacently align inputs within a .col-*
, add a .w*
to the .form-control
set the width based on a percentage of the parent. Widths are avaialbe in increments of 10, starting at 10%.
<form class="form-horizontal">
<fieldset>
<div class="form-group">
<label class="col-sm-3 control-label">nodes</label>
<div class="col-sm-6">
<input type="text" class="form-control w70" placeholder="ip address">
<input type="text" class="form-control w30" placeholder="port">
</div>
</div>
</fieldset>
</form>
Custom Controls
For more complex controls that require more than simple HTML markup we use Knockout Components. This means that in order for these to work the element, or a parent element, must have bindings applied. These self contained "controls" makes them easy to consume but provides a rich user experience.
Toggle
<toggle params="value: value, disabled: disabled, affirmativeText: 'yes', negativeText: 'no'"></toggle>
Sliders
<slider params="value: 6, min: 1, max: 16"></slider> <!-- CPU -->
<slider params="value: 4, min: 1, max: 128"></slider> <!-- Memory -->
<slider params="value: 50, min: 1, max: 1024"></slider> <!-- Disk -->
Group Picker
<group-picker params="{ selectedGroup: selectedGroup, isLoading: isLoading, groups:groups}"></group-picker>
Server Picker
<server-picker params="{ selectedServers: selectedServers, multiSelect: multiSelect, isLoading: isLoading, groups: groups}"></group-picker>
Disabled Inputs
Add the disabled
attribute on an input to prevent user interactions. Disabled inputs appear lighter and add a not-allowed
cursor.
<input type="text" class="form-control" id="disabledInput" placeholder="Disabled Text Input" disabled>
Disabled Fieldset
<form>
<fieldset disabled>
<div class="form-group">
<label for="disabledTextInput">Disabled input</label>
<input type="text" id="disabledTextInput" class="form-control" placeholder="Disabled input">
</div>
<div class="form-group">
<label for="disabledSelect">Disabled select menu</label>
<select id="disabledSelect" class="form-control">
<option>Disabled select</option>
</select>
</div>
<div class="checkbox">
<label>
<input type="checkbox"> Can't check this
</label>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</fieldset>
</form>
Basic Form
Individual form controls automatically receive some global styling. All textual <input>
, <textarea>
, and <select>
elements with .form-control
are set to width: 100%; by default. Wrap labels and controls in .form-group
for optimum spacing.
- Labels should have a
for
attribute that points to theid
of an input type. It is only allowed to point to input types not any element - The fieldset legend name should be useful not just
edit
orcreate
- Optional fields are denoted not required fields.
- Tab order should be logical and visit ever valid field (e.g. do not tab to disabled items)
- Items that are not editable are presented as paragraph tags not disabled inputs
<form>
<div class="form-group">
<label for="basicFormEmail">Email address</label>
<input type="email" class="form-control" id="basicFormEmail" placeholder="Email">
</div>
<div class="form-group">
<label for="basicFormPassword">Password</label>
<input type="password" class="form-control" id="basicFormPassword" placeholder="Password">
</div>
<div class="form-group">
<label for="basicFormFileInput">File input</label>
<input type="file" id="basicFormFileInput">
<p class="help-block">Example block-level help text here.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox"> Check me out
</label>
</div>
<button type="submit" class="btn btn-default">Submit</button>
<button class="btn btn-link">cancel</button>
</form>
Inline Forms
Use .inline-form
to display labels and form inputs on a single row. This is useful for confirmation dialogs that offer additional options (e.g. delete a database, but create a backup before it's deleted).
<form class="form-inline">
<div class="form-group">
<label for="inputEmail">email</label>
<input type="text" class="form-control" id="inputEmail" placeholder="email">
</div>
<div class="checkbox">
<label>
<input type="checkbox"> remember me
</label>
</div>
<div class="radio">
<label>
<input type="radio"> radio me
</label>
</div>
<button type="submit" class="btn btn-primary">sign in</button>
</form>
Horizontal Form
Horizontal forms rely on the Grid System to align form labels and inputs horizontally. These forms used throughout the Control Portal to create new entities, such as servers, appfog apps, and policies.
Add a visual separator to the form submission buttons by adding .form-submit
to the .form-group
container.
<form class="form-horizontal">
<div class="form-group">
<label for="horizontalFormEmail" class="col-sm-3 control-label">Email</label>
<div class="col-sm-9">
<input type="email" class="form-control" id="horizontalFormEmail" placeholder="Email">
</div>
</div>
<div class="form-group">
<label for="horizontalFormPassword" class="col-sm-3 control-label">Password</label>
<div class="col-sm-9">
<input type="password" class="form-control" id="horizontalFormPassword" placeholder="Password">
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
<div class="checkbox">
<label>
<input type="checkbox"> Remember me
</label>
</div>
</div>
</div>
<div class="form-group form-submit">
<div class="col-sm-offset-3 col-sm-9">
<button type="submit" class="btn btn-default">sign in</button>
<button class="btn btn-link">cancel</button>
</div>
</div>
</form>
Async Forms
Async calls can be used to fet values for input on calls that can take a long time. Doing this give the effect of faster render times since the user starts seeing UI faster than just watching the browser spin.
In the bellow example server name is not dependent on the data center choice so it is always enabled, but the avaliblity of hyperscale does change based on data center so it is disabled until the data centers are loaded.
Dynamic Forms
Sometimes when you are creating a form a user selection may influence the form fields themselves. For instance depending on what data center is chosen the user may have the option to provision a server as bare metal.
In the example bellow GB1 supports bare metal and the other two do not.
Static Text
When you need to place plain text next to a form label within a horizontal form, use the .form-control-static
class on a <p>
.
<p class="form-control-static">email@example.com</p>
Form Submission
When submitting a form we need to give the user some feedback that something is taking place. In some cases it can take several seconds for the request to return from the server. During that time we want to indicate something is happening and prevent the form from being changed or submitted again.
Bellow is a simple example that uses one observable to disable the form and also change the text of the button once the user has submitted the form.
var formModel = function(){ var self = this; self.submitting = ko.observable(false); self.submitButtonText = ko.pureComputed(function(){ return self.submitting() ? "submitting": "submit"; }); self.submit = function(){ self.submitting(true); setTimeout(function(){ // This simulates a long running AJAX call. When using jQuery xhr requests you can use // the deferred objects (https://api.jquery.com/category/deferred-object/) always handler // to set sumitting back to false all the time. self.submitting(false); }, 5000); }; return self; }
<form> <fieldset data-bind="disable: submitting"> <div class="form-group"> <label for="input">Input</label> <input type="text" id="input" class="form-control" placeholder="some text"> </div> <button type="submit" class="btn btn-primary" data-bind="text: submitButtonText, click: submit">submit</button> </fieldset> </form>
Validation
We suggest using knockout-validation and have created several bindings and validators that will make creating an awesome user experience simple and painless.
Getting Started
Becuase we use knockout-validation we suggest heading over to there wiki and reading the Getting Started Guide. Bellow is the set of options, validators, and custom validators.
Demo
A demo that includes all the options and configuration needed. See the list of native and custom validators for more complex validation needs. As a rule of thumb only optional fields are marked.
ValidationOptions
is used to set some common configuration that is needed to
adhere to out ux guidelines. This can be set on a global level but is not in control due to
the need for backwards compatablity with "old ui" pages.
validationMsg
is a custom binding that we created to show error messages that
follow our ux guidelines without requiring additional work from the developer. Use this
binding on a p
tag to display error messages
ko.validation.group
is used to select which properties on your
model will be validated. In general our models tend to have a lot of properties
(computeds, etc.) that don't need to be tracked for validation so we use validations groups.
more information about validation groups can be found on the
knockout-validation page
showAllMessages
is required when using validation groups to tigger the showing
of error messages for the tracked properties.
self.header = ko.observable(); self.text = ko.observable().extend({ required: { params: true, message: "some text is required." }, }); self.errors = ko.validation.group([self.text]) self.submit = function() { if (self.errors().length < 1) { alert("The Form is Valid!"); } else { self.errors.showAllMessages(); } };
<form class="form-horizontal" data-bind="validationOptions: { insertMessages: false, allowHtmlMessages: true }"> <div class="form-group"> <label class="col-sm-3 control-label" for="header">Header (optional)</label> <div class="col-sm-6"> <input class="form-control" type="text" id="header" data-bind="value: header" > </div> </div> <div class="form-group"> <label class="col-sm-3 control-label" for="requiredText">Message</label> <div class="col-sm-6"> <input class="form-control" type="text" id="requiredText" data-bind="value: text, validationElement: text" > <p class="hide" data-bind="validationMsg: text"></p> </div> </div> <div class="form-group form-submit"> <div class="col-sm-offset-3 col-sm-9"> <button class="btn btn-primary" data-bind="click: submit">Submit</button> </div> </div> </form>
Native Validators
A full list of navtive validators and their documentation can be found on the knockout-validation wiki page.
Custom Validators
A full list of custom validators and their documentation can be found on the validators page.
Password Strength
In addition to the passwordStrength
validator we have created a widget (named
passwordStrength
) that should be used in conjuction with the validator to provide
the end user with realtime information about the strength of their password. More information
about the options of this widget can be found on the widgets page.
self.password = ko.observable().extend({ required: { params: true, message: "A password is required." }, passwordStrength: { requiredStrength: 4, dissallowedCharacters: "[}" } }); self.errors = ko.validation.group([self.password]) self.submit = function() { if (self.errors().length < 1) { alert("The Password meets the requires strength!"); } else { self.errors.showAllMessages(); } };
<form class="form-horizontal" data-bind="validationOptions: { insertMessages: false, allowHtmlMessages: true }"> <div class="form-group"> <label class="col-sm-3 control-label" for="password">password</label> <div class="col-sm-6"> <input class="form-control" type="password" id="password" data-bind="value: password, validationElement: password, widget: 'passwordStrength'" > <p class="hide" data-bind="validationMsg: password"></p> </div> </div> <div class="form-group form-submit"> <div class="col-sm-offset-3 col-sm-9"> <button class="btn btn-primary">Submit</button> </div> </div> </form>
Remote / Async
Remote validation is done using
inline async validators. The bellow code example fakes out an AJAX call using
setTimeout. asyncMessage
is a custom binding that was writen to display
notification to the end user that validation is occuring. This is to be used in conjuction
with validationMsg
. For More information about either of these bindings please
consult the bindings documentation.
Additionally it is of note that your validator may be called again before the AJAX call has returned so you will need to properly abort the previous call and issue a new one.
Compare (confirm password)
When validating a confirm password field it should only show and error message if there is a
password typed. So you will want to use the onlyIf
parameter.
self.confirmPassword.extend({
required: { message: "Retype the password to confirm.", onlyIf: self.password },
equal: { params: self.password, message: "Passwords do not match." }
});