Last active
December 12, 2015 00:39
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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