Knockout.Unobtrusive Example

An example of using KnockoutJS without the default data-bind HTML5 markup. Uses the Knockout.Unobtrusive plugin

by bsatrom

HTML

<script src="http://knockoutjs.com/js/jquery.tmpl.js"></script>
<script src="https://raw.github.com/SteveSanderson/knockout/master/build/output/knockout-latest.js"></script>
<script src="https://raw.github.com/bsatrom/Knockout.Unobtrusive/master/js/knockout.unobtrusive.min.js"></script>
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <title>Create</title>
        <meta name="author" content="Brandon Satrom" />
        <meta name="viewport" content="width=device-width; initial-scale=1.0" />     
    </head>
    <body>
        <div id="site-wrapper">          
            For a full writeup of this example, <a href="http://userinexperience.com/?p=633">click here</a>
            <h2>Create a Speaker</h2>
            <form data-bind="submit: addSpeaker">
                <fieldset>
                    <legend>Speaker Info</legend>
                    Name: <input type="text" id="name" /> <br />                                                       
                    Bio: <input type="text" id="bio" /> <br />
                    Twitter Handle: <input id="twitterHandle" type="text" /> <br />
                    Photo Url: <input id="photoUrl" type="text" /> <br />
                    Presented Before: <input type="checkbox" name="presentedBefore" />                    
                </fieldset>
                <fieldset>
                    <legend>Favorite Languages</legend>
                    New item:
                    <input type="text" id="languageToAdd" />
                    <button type="submit" id="addLanguage">Add</button>
                    <p>Your choices:</p>
                    <select multiple="multiple" width="50" id="languages"> </select>
                </fieldset>
                <input type="submit" value="Create Speaker" />
                <!-- Profile Preview -->
                <article id="profilePreview">
                    <header>
                        <div><img id="image" /></div>
             ...

CSS

/* 
   Reset
------------------------------------------------------------------- */

html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, code, del, dfn, em, img, q, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, textarea, input, select {margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline;}
table {border-collapse: collapse; border-spacing: 0;}
caption, th, td {text-align: left; font-weight: normal;}
table, td, th {vertical-align: middle;}
blockquote:before, blockquote:after, q:before, q:after {content: "";}
blockquote, q {quotes: "" "";}
a img {border: none;}
:focus {outline: 0;}


/* 
   General 
------------------------------------------------------------------- */

html {
    height: 100%;
    padding-bottom: 1px; /* force scrollbars */
}

body {
    background: #FFF;
    color: #444;
    font: normal 75% sans-serif;
    line-height: 1.5;
}

/* 
   Typography 
------------------------------------------------------------------- */

/* Headings */

h1,h2,h3,h4,h5,h6 {
    color: #444;
    font-weight: normal;
    line-height: 1;
    margin-bottom: 0.3em;
}
h4,h5,h6 {font-weight: bold;}

h1 {font-size: 2.6em;}
h2 {font-size: 2em;}
h3 {font-size: 1.5em;}
h4 {font-size: 1.25em;}
h5 {font-size: 1.1em;}
h6 {font-size: 1em;}

h1 img, h2 img, h3 img, h4 img, h5 img, h6 img {margin: 0;}


/* Links */

a:focus,a:hover {color: #039;}
a {
    color: #456;
    text-decoration: none;
}
a:hover {text-decoration: underline;}

a.feed {
    background: url('img/icon-feed.gif') no-repeat left center;
    padding-left: 18px;
}
a.more {
    color: #579;
    font-weight: bold;
}
a.more:hover {color: #234;}


/* Text elements */

p {margin-bottom: 1em;}

abbr, acronym {border-bottom: 1px dotted #666;}
address {margin-bottom: 1.5em;}
blockquote {margin: 1.5em;}
del, blockquote {...

JavaScript

var viewModel = {
    id: '',
    name: ko.observable(''),
    bio: ko.observable(''),
    twitterHandle: ko.observable(''),
    state: ko.observable(''),
    photoUrl: ko.observable(''),
    languages: ko.observableArray([]),
    averageRating: ko.observable(''),
    languageToAdd: ko.observable(''),
    presentedBefore: ko.observable(true)
};
viewModel.twitterUrl = ko.dependentObservable(function() {
    return "http://www.twitter.com/" + viewModel.twitterHandle();
});
viewModel.addLanguage = function() {
    if (viewModel.languageToAdd() !== '') {
        viewModel.languages.push(viewModel.languageToAdd());
        viewModel.languageToAdd('');
    }
};
viewModel.addTopic = function() {
    if (viewModel.topicToAdd() !== '') {
        viewModel.favoriteTopics.push(viewModel.topicToAdd());
        viewModel.topicToAdd('');
    }
};
viewModel.addSpeaker = function() {
    $.ajax({
        url: "/speakers/create/",
        type: 'post',
        data: ko.toJSON(this),
        contentType: 'application/json',
        success: function(result) {
            alert(result);
        }
    });
};

var bindings = {
    value: [ 'languageToAdd', 'name', 'bio', 'twitterHandle', 'state', 'photoUrl' ],
    text: [ { displayName: 'name' }, { displayState: 'state' }, { displayBio: 'bio' }, { displayTwitterHandle: 'twitterHandle' }, { languageList: 'languages' }, { displayPresentedBefore: 'presentedBefore' } ],
    options: [ 'languages', 'favoriteTopics' ],
    checked: [ 'presentedBefore' ],
    custom: {
        languageToAdd: 'valueUpdate: "afterkeydown"',
        addLanguage: 'enable: languageToAdd().length > 0, click: addLanguage',
        photo: 'attr: {src: photoUrl, alt: name}',
        twitterUrl: 'attr: {href: twitterUrl}'
    }
};

ko.unobtrusive.createBindings(bindings);
ko.applyBindings(viewModel);

$('#profilePreview').hide();
$('input[type=text]').blur(function () {
    $('#profilePreview').show();
});
$('#profilePreview').draggable();