Skip to content

Instantly share code, notes, and snippets.

@JogoShugh
Last active December 12, 2015 00:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JogoShugh/4685389 to your computer and use it in GitHub Desktop.
Save JogoShugh/4685389 to your computer and use it in GitHub Desktop.
How to Build a Backbone-fortified User Story Editor for the VersionOne REST Data API
<html>
<head>
<title>Backbone-Fortified VersionOne Story Editor</title>
</head>
<body>
<h1>Backbone-Fortified VersionOne Story Editor</h1>
<div id="editor">
<form id="editorForm">
<h4>Story Details</h4>
<hr />
<div id="editorFields"></div>
</form>
<button id="storySave">Save Story</button> <span id="message"></span><span id="error"></span>
</div>
<h2>Enter a Story ID</h2>
<input type="text" id="storyId" value="1154" /> (Hint: use 1154 if don't know another...)
<br />
<button id="storyLoad">Load Story</button>
<hr/>
Visit the <a href="http://community.versionone.com/default.aspx">VersionOne Community</a> for more open source tools and APIs. Download code and <b>get involved</b> at <a href="http://www.github.com/VersionOne" target="_blank">VersionOne on GitHub</a>!
<br/>
</body>
</html>
<div id="editor">
<form id="editorForm" name="editorForm">
<h4>Story Details</h4>
<hr>
<div id="editorFields"></div>
</form>
<input id="storySave" type="button" value="Save Story">
</div>
<div id="editor">
<form id="editorForm" name="editorForm">
<label for="Name">Story Name:</label><br>
<input id="Name" name="Name" type="text"><br>
<label for="Name">Description:</label><br>
<textarea id="Description" name="Description"></textarea><br>
<label for="Estimate">Estimate:</label><br>
<input id="Estimate" name="Estimate" type="text"><br>
</form><input id="storySave" type="button" value="Save Story">
</div>
var StoryFormSchema = {
Name: { validators: [ "required" ] },
Description: "TextArea",
Benefits: "TextArea",
Estimate: "Number",
RequestedBy: {}
};
var storyForm = null;
var urlRoot = "http://eval.versionone.net/platformtest/rest-1.v1/Data/Story/";
var headers = { Authorization: "Basic " + btoa("admin:admin"), Accept: "haljson" };
Backbone.emulateHTTP = true;
var StoryModel = Backbone.Model.extend({
urlRoot: urlRoot,
url: function() {
if (this.hasChanged() && !this.isNew()) return this.urlRoot + this.id;
return this.urlRoot + this.id + "?sel=" + _.keys(storyForm.schema).join(",");
},
fetch: function(options) {
options || (options = {});
_.defaults(options, { dataType: "json", headers: headers });
return Backbone.Model.prototype.fetch.call(this, options);
},
save: function(attributes, options) {
options || (options = {});
_.defaults(options, { contentType: "haljson", patch: true, headers: headers });
return Backbone.Model.prototype.save.call(this, attributes, options);
}
});
var storyModel = new StoryModel;
function createForm(model) {
var settings = { schema: StoryFormSchema };
var finish = function() {
storyForm = new Backbone.Form(settings);
$("#editorFields").empty();
$("#editorFields").append(storyForm.render().el);
if (model) $("#editor").fadeIn();
};
if (model) {
model.fetch().done(function(data) {
settings.model = model;
finish();
});
} else finish();
}
function storyLoad() {
storyModel.id = $("#storyId").val();
if (storyModel.id === "") {
alert("Please enter a story id first");
return;
}
createForm(storyModel);
}
function storySave() {
if (storyForm.validate() != null) return;
storyForm.commit();
storyModel.save(storyForm.getValue()).done(function(data) {
$("#error").hide();
$("#message").text("Story saved!").fadeIn().delay(2500).fadeOut();
}).fail(function(jqXHR) {
$("#message").hide();
$("#error").text("Error during save! See console for details.").fadeIn().delay(5e3).fadeOut();
console.log(jqXHR);
});
}
$(function() {
createForm();
$("#storyLoad").click(storyLoad);
$("#storySave").click(storySave);
});
var StoryFormSchema = { // Backbone.Form will generate an HTML form based on this schema
Name: { validators: ['required'] }, // Name is required
Description: 'TextArea', // Since these next three are not required, we only need the data type
Benefits: 'TextArea',
Estimate: 'Number',
RequestedBy: {} // Defaults to 'Text'
};
var storyForm = null; // Instance of the schema declared above, created when we click 'Load Story'
var urlRoot = 'http://eval.versionone.net/platformtest/rest-1.v1/Data/Story/'; // V1 API URL base
var headers = { Authorization: 'Basic ' + btoa('admin:admin'), Accept: 'haljson' }; // Headers for auth and accept type format
Backbone.emulateHTTP = true; // Tells Backbone to issue a POST instead of a PUT HTTP method for updates
// Note that Models usually align with addressable HTTP resources, such as '/rest-1.v1/Data/Story/1154'
var StoryModel = Backbone.Model.extend({ // .extend comes from Underscore.js, to create an inherited 'class'
urlRoot: urlRoot, // Sets the root url to the V1 API URL base
url: function () { // Override the built in url() for two cases:
if (this.hasChanged() && !this.isNew()) return this.urlRoot + this.id; // In this case, just use the id -- used for save() via POST
return this.urlRoot + this.id + '?sel=' + _.keys(storyForm.schema).join(','); // Otherwise, fetch only the attributes our schema contains
}, // Note that _.keys is another Underscore goody that returns an array of key names from an object
fetch: function(options) { // Overrides the base fetch so we can customize behavior to be V1 API friendly
options || (options = {}); // When no options passed, default to an empty object
_.defaults(options, {dataType: 'json', headers: headers}); // Copies values from 2nd arg into the 1st if-and-only-if they don't exist already in the 1st
return Backbone.Model.prototype.fetch.call(this, options); // Delegate to the base implementation
},
save: function(attributes, options) { // Similar override of base save
options || (options = {});
_.defaults(options, {contentType: 'haljson', patch: true, headers: headers}); // See extended comment below...
return Backbone.Model.prototype.save.call(this, attributes, options);
} // patch: true tells Backbone.sync to send a partial representation, and makes it use the PATCH HTTP method,
}); // but, since we did Backbone.emulateHTTP = true, it uses POST and sets X-HTTP-Method: PATCH as a header
var storyModel = new StoryModel(); // Concrete instance of our StoryModel. Alive at last!
function createForm(model) { // Called to use Backbone.Form with our schema to build the form and add it to the DOM
var settings = {schema: StoryFormSchema}; // Gets passed to Backbone.Form constructor
var finish = function() { // Gets called below, either immediately if model is null, or asynchronously after fetch
storyForm = new Backbone.Form(settings); // Create concrete StoryForm instance
$('#editorFields').empty(); // Empty out the DOM element for our fields!
$('#editorFields').append(storyForm.render().el); // Construct the HTML for the form, and toss it into the DOM!
if (model) $('#editor').fadeIn(); // Oooo, ahhh animated fade in.
};
if (model) { // When called with a model instance:
model.fetch().done(function(data) { // Make the model fetch itself, and when done:
settings.model = model; // Assign a copy of the model into our settings hash, and:
finish(); // FINISH!
});
} else finish(); // When no model passed, just finish immediately WITHOUT a settings.model, resulting in an empty form
}; // Note that we don't have this case in the app, but if you'd like to make an 'Add' mode, you could rely on this
function storyLoad() { // Called when you click 'Load Story'
storyModel.id = $('#storyId').val(); // Extract the story id from the input field that we manually added
if (storyModel.id === '') { // If empty, then:
alert('Please enter a story id first'); // Warn, and:
return; // Get out of here...
}
createForm(storyModel); // Pass the model into createForm, causing the "if (model)" branch to run, causing
}; // model.fetch() to execute, causing Backbone.sync to fetch the model from the V1 API, and
// causing finish() to execute, causing Backbone.Form and friends to execute and presto!
function storySave() { // Called when you click 'Save Story'
if (storyForm.validate() != null) return; // Backbone Forms validates the form based on the schema we gave it,
storyForm.commit();
storyModel.save(storyForm.getValue()).done(function(data) { // storyForm.getValue() gets data from the Backbone.Form instance, and .save() returns a jQuery deferred object, so we can pass a 'done' handler:
$('#error').hide(); // done gets called on SUCCESS, so hide the errors element
$('#message').text('Story saved!').fadeIn().delay(2500).fadeOut(); // More ooo, ahh animation for the success message
}).fail(function(jqXHR) { // If the HTTP POST operation fails, this gets called to handle the error
$('#message').hide(); // Get rid of the success message this time.
$('#error').text('Error during save! See console for details.').fadeIn().delay(5000).fadeOut(); // Boooo, hiss!
console.log(jqXHR); // Dump the raw jQuery XML HTTP Request object to the console
});
};
$(function() { // Configure jQuery's document ready handler and GO!
createForm(); // Create the form, without a model. Not terribly useful, really, because it will be hidden still
$('#storyLoad').click(storyLoad); // Wire up the storyLoad click handler to its corresponding button
$('#storySave').click(storySave); // Wire up storySave the same way
});
body {
padding: 5px;
font-family: sans-serif;
}
#editor {
padding: 10px;
border: 1px solid #00008B;
background: #F5F5F5;
display: none;
}
h4 {
color: #666;
font-style: italic;
}
label {
color: #00008B;
}
textarea {
height: 100px;
}
#message {
margin-top: 5px;
color: #006400;
}
#storyIdLabel {
font-weight: 700;
}
#message {
display: none;
font-weight: 700;
color: #006400;
}
#error {
display: none;
font-weight: 700;
color: red;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment