Skip to content

Instantly share code, notes, and snippets.

@derek
Created Jul 19, 2012
Embed
What would you like to do?
App example - Github Contributors
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example: GitHub Contributors - YUI Library</title>
<meta name="viewport" content="width=960" id="meta-viewport">
<script>
if (screen.width < 768) {
document.getElementById('meta-viewport').setAttribute('content', 'width=768');
}
</script>
<link rel="stylesheet" href="http://fonts.googleapis.com/css?family=PT+Sans:400,700,400italic,700italic">
<link rel="stylesheet" href="http://yui.yahooapis.com/3.5.1/build/cssgrids/cssgrids-min.css">
<link rel="stylesheet" href="http://yuilibrary.com/combo/css?main-min.css">
<link rel="stylesheet" href="http://yuilibrary.com/vendor/prettify/prettify-min.css">
<link rel="stylesheet" href="http://yuilibrary.com/css/docs-min.css">
<script>var YUI_config={"filter":"min","maxURLLength":1024,"groups":{"site":{"combine":true,"comboBase":"/combo/js?","root":"","modules":{"hoverable":{"path":"hoverable-min.js","requires":["event-hover","node-base","node-event-delegate"]},"search":{"path":"search-min.js","requires":["autocomplete","autocomplete-highlighters","node-pluginhost"]},"api-filter":{"path":"apidocs/api-filter-min.js","requires":["autocomplete-base","autocomplete-highlighters","autocomplete-sources"]},"api-list":{"path":"apidocs/api-list-min.js","requires":["api-filter","api-search","event-key","node-focusmanager","tabview"]},"api-search":{"path":"apidocs/api-search-min.js","requires":["autocomplete-base","autocomplete-highlighters","autocomplete-sources","escape"]}}}}};</script>
<script src="http://yui.yahooapis.com/3.5.1/build/yui/yui-min.js"></script>
</head>
<body class="yui3-skin-sam">
<div id="github-app"></div>
<style scoped>
.scrollable pre {
overflow-y: auto;
max-height: 40em;
-webkit-overflow-scrolling: touch;
}
.screenshot {
display: block;
width: 584px;
margin: 0 auto;
}
</style>
<section>
<div class="example">
<style scoped>
/*-- Override Styles ---------------------------------------------------------*/
.example {
padding: 0 !important;
}
/*-- Common Styles -----------------------------------------------------------*/
#github-app h1 {
padding: 0;
}
#github-app .avatar,
#github-app .avatar img {
display: block;
width: 76px;
height: 76px;
border: 0;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
#github-app .avatar {
display: inline-block;
*display: inline; zoom: 1;
height: 76px;
padding: 2px;
}
#github-app .avatar:hover,
#github-app .user-avatar {
-webkit-box-shadow: 0 0 2px rgba(0,0,0,0.25);
-moz-box-shadow: 0 0 2px rgba(0,0,0,0.25);
box-shadow: 0 0 2px rgba(0,0,0,0.25);
}
#github-app .back,
#github-app .view-on-github {
margin-top: 0;
}
#github-app .view-on-github {
text-align: right;
}
#github-app .info h1 {
margin: 0;
}
#github-app .info .user-avatar,
#github-app .info .user-login,
#github-app .info .user-name,
#github-app .info .repo-name {
vertical-align: middle;
}
#github-app .info .user-avatar {
margin-right: 10px;
}
#github-app .info .user-name {
font-weight: 400;
}
#github-app .stats ul {
margin: 0;
padding: 8px;
color: #30418C;
background: #F0F1F8;
border: 1px solid #D4D8EB;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
text-align: center;
list-style: none;
}
#github-app .stats b {
display: block;
font-size: 36px;
line-height: 44px;
}
/*-- HomePage Styles ---------------------------------------------------------*/
#github-app .home-page {
padding: 1em;
}
#github-app .home-page h1,
#github-app .home-page p {
display: block;
text-align: center;
}
#github-app .home-page label,
#github-app .home-page input,
#github-app .home-page button {
font-size: 20px;
}
#github-app .home-page label {
color: #A6A6A6;
}
#github-app .home-page input {
display: inline-block;
*display: inline; zoom: 1;
font-family: Helvetica, sans-serif;
line-height: normal;
margin: 5px auto 0;
padding: 6px;
width: 200px;
text-align: left;
}
/*-- UserPage Styles ---------------------------------------------------------*/
#github-app .user-page {
padding: 1em;
}
#github-app .repos {
padding-left: 0;
list-style: none;
border-top: 1px solid #E5E6F1;
margin: 0 -13px;
}
#github-app .repo {
margin: 1px 0;
padding: 0 13px 6px;
color: #30418C;
background: #F9F9FC;
cursor: pointer;
border-bottom: 1px solid #E5E6F1;
}
#github-app .repo:hover {
background: #F1F1F4;
}
#github-app .repo-name h3 {
margin: 6px 0 0 0;
}
#github-app .repo-stats {
margin-top: 6px;
background: url(../assets/app/arrow.gif) right center no-repeat;
}
#github-app .repo-stats ul {
list-style: none;
padding-left: 0;
height: 47px;
line-height: 47px;
}
#github-app .repo-stats b {
display: block;
}
#github-app .repo-stats span {
display: block;
font-size: 11px;
color: #B6BCD7;
color: rgba(48, 65, 140, 0.40);
}
#github-app .repo-stats .repo-watchers,
#github-app .repo-stats .repo-forks {
text-align: center;
line-height: 11px;
margin-top: 15px;
}
#github-app .repo-desc {
margin: 10px 20px 0 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #B6BCD7;
color: rgba(48, 65, 140, 0.40);
}
/*-- RepoPage Styles ---------------------------------------------------------*/
#github-app .repo-page {
padding: 1em;
}
#github-app .contributors {
margin-left: -5px;
margin-right: -5px;
padding-left: 0;
list-style: none;
}
#github-app .contributor {
margin: 5px;
display: inline-block;
*display: inline; zoom: 1;
}
#github-app .contributor .avatar {
position: relative;
display: block;
}
#github-app .contributor .avatar:link,
#github-app .contributor .avatar:visited,
#github-app .contributor .avatar:hover,
#github-app .contributor .avatar:active {
text-decoration: none;
}
#github-app .contributor-name,
#github-app .contributor-contributions {
position: absolute;
left: 0;
margin: 2px;
padding: 0 4px;
width: 68px;
font-size: 11px;
color: #fff;
background: #000;
background: rgba(0,0,0, 0.2);
text-shadow: 0 1px 0 rgba(0,0,0, 0.4);
}
#github-app .contributor-name {
bottom: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
-webkit-border-bottom-right-radius: 3px;
-webkit-border-bottom-left-radius: 3px;
-moz-border-radius-bottomright: 3px;
-moz-border-radius-bottomleft: 3px;
border-bottom-right-radius: 3px;
border-bottom-left-radius: 3px;
}
#github-app .contributor-contributions {
top: 0;
-webkit-border-top-left-radius: 3px;
-webkit-border-top-right-radius: 3px;
-moz-border-radius-topleft: 3px;
-moz-border-radius-topright: 3px;
border-top-left-radius: 3px;
border-top-right-radius: 3px;
}
</style>
<!--
HomePage Template:
Template HTML used by the <code>HomePageView</code> to render the app's home page.
-->
<script id="t-home" type="text/x-handlebars-template">
<h1>Contributors of GitHub Projects</h1>
<p>
<input id="github-app-username" type="text" value="{{login}}" placeholder="GitHub Username" />
<button class="yui3-button">Show Repos</button>
</p>
<p>
<label for="github-app-username">(e.g. yui, davglass, rgrove)</label>
</p>
</script>
<!--
User Template:
Template HTML used by the <code>UserPageView</code> to render the header section of a
user's page in the app; this displays info and stats about the GitHub user.
-->
<script id="t-user" type="text/x-handlebars-template">
<div class="yui3-g">
<p class="back yui3-u-1-2">
<a href="#/">« Choose Someone Different</a>
</p>
<p class="view-on-github yui3-u-1-2">
<a href="{{html_url}}">View on GitHub</a>
</p>
</div>
<div class="yui3-g">
<div class="info yui3-u-2-3">
<h1>
<span class="avatar user-avatar">
<img src="{{avatar_url}}" alt="{{login}}'s avatar" />
</span>
<span class="user-login">{{login}}</span>
{{#if name}}
<span class="user-name">({{name}})</span>
{{/if}}
</h1>
</div>
<div class="stats yui3-u-1-3">
<ul class="yui3-g">
<li class="user-repos yui3-u-1-2">
<b>{{public_repos}}</b> {{public_repos_label}}
</li>
<li class="user-followers yui3-u-1-2">
<b>{{followers}}</b> {{followers_label}}
</li>
</ul>
</div>
</div>
</script>
<!--
RepoList Template:
Template HTML used by the <code>UserPageView</code> to render the main section of a user's
page in the app; this displays the list of project repos for the GitHub user.
-->
<script id="t-repo-list" type="text/x-handlebars-template">
<h2 class="no-toc">Public Repositories</h2>
<ul class="repos">
{{#each repos}}
<li id="{{clientId}}" class="repo yui3-g">
<div class="repo-name yui3-u-2-3">
<h3 class="no-toc">
<a href="#/github/{{owner.login}}/{{name}}/">{{name}}</a>
</h3>
<p class="repo-desc">{{description}}</p>
</div>
<div class="repo-stats yui3-u-1-3">
<ul class="yui3-g">
<li class="repo-lang yui3-u-1-3">
<b>{{language}}</b>
</li>
<li class="repo-watchers yui3-u-1-3">
<b>{{watchers}}</b>
<span>{{watchers_label}}</span>
</li>
<li class="repo-forks yui3-u-1-3">
{{#if forks}}
<b>{{forks}}</b>
<span>{{forks_label}}</span>
{{/if}}
</li>
</ul>
</div>
</li>
{{/each}}
</ul>
</script>
<!--
Repo Template:
Template HTML used by the <code>RepoPageView</code> to render the header section of a
repo page in the app; this displays info and stats about a GitHub user's repo.
-->
<script id="t-repo" type="text/x-handlebars-template">
<div class="yui3-g">
<p class="back yui3-u-1-2">
<a href="#/github/{{owner.login}}/">« Select Repository</a>
</p>
<p class="view-on-github yui3-u-1-2">
<a href="{{html_url}}">View on GitHub</a>
</p>
</div>
<div class="yui3-g">
<div class="info yui3-u-2-3">
<h1>
<span class="avatar user-avatar">
<img src="{{owner.avatar_url}}" alt="{{owner.login}}'s avatar" />
</span>
<span class="user-login">{{owner.login}}</span> /
<span class="repo-name">{{name}}</span>
</h1>
</div>
<div class="stats yui3-u-1-3">
<ul class="yui3-g">
<li class="repo-watchers yui3-u-1-2">
<b>{{watchers}}</b> {{watchers_label}}
</li>
<li class="repo-forks yui3-u-1-2">
<b>{{forks}}</b> {{forks_label}}
</li>
</ul>
</div>
</div>
</script>
<!--
ContributorList Template:
Template HTML used by the <code>RepoPageView</code> to render the main section of a repo
page in the app; this displays the list of contributors to the GitHub project.
-->
<script id="t-contributor-list" type="text/x-handlebars-template">
<h2 class="no-toc">Project Contributors ({{num}})</h2>
<ul class="contributors">
{{#each contributors}}
<li class="contributor">
<a class="avatar" href="#/github/{{login}}/"
title="{{login}} has {{contributions}} {{contributions_label}}">
<img alt="{{login}}'s avatar" src="{{avatar_url}}" />
<span class="contributor-name">{{login}}</span>
<span class="contributor-contributions">{{contributions}}</span>
</a>
</li>
{{/each}}
</ul>
</script>
<script>
YUI().use('app', 'handlebars', 'jsonp', 'cssbutton', function (Y) {
var User, Repo, RepoList, Contributor, ContributorList,
UserView, RepoView, RepoListView, ContributorListView,
HomePageView, UserPageView, RepoPageView,
ContributorsApp;
// -- Utility Functions --------------------------------------------------------
// A helper function used by the Views to create labels for numerical fields
// which are correctly pluralized, and puts the label back on the <code>data</code> object.
function addLabel(data, field, label) {
var num = data[field];
data[field] || (data[field] = '0');
data[field + '_label'] = num === 1 ? label : (label + 's');
}
// -- GithubSync ---------------------------------------------------------------
function GithubSync() {}
GithubSync.API_ORIGIN = 'https://api.github.com';
GithubSync.prototype = {
// Intended to be overridden with a GitHub API endpoint URL.
githubURL: '/',
// Can be overridden to customize the actual URL used when making the
// request; this way the <code>githubURL</code> can be more of a template, and this
// method can substitute place-holders in the template with specific values.
buildURL: function () {
return this.githubURL;
},
// Provides an implementation for a Model/ModelList's <code>sync()</code> method which
// can read data from GitHub's API.
sync: function (action, options, callback) {
Y.Lang.isFunction(callback) || (callback = function () {});
// Enforce ready-only constraint.
if (action !== 'read') {
return callback('Read-only');
}
// Creates the API URL and adds params for 100 items per-page and a
// JSONP callback placeholder.
var url = this.buildURL() + '?per_page=100&callback={callback}';
// Performs JSONP request to GitHub's API.
Y.jsonp(GithubSync.API_ORIGIN + url, function (res) {
var meta = res.meta,
data = res.data;
// Check for a successful response, otherwise return the error
// message returned by the GitHub API.
if (meta.status >= 200 && meta.status < 300) {
callback(null, res);
} else {
callback(data.message, res);
}
});
},
// Provides an implementation for a Model/ModelList's <code>parse()</code> method which
// simply returns the <code>data</code> object or array from the response JSON.
parse: function (res) {
return res.data;
}
};
// -- User ---------------------------------------------------------------------
User = Y.Base.create('user', Y.Model, [GithubSync], {
githubURL: '/users/{user}',
buildURL: function () {
return Y.Lang.sub(this.githubURL, {
user: this.getAsURL('login')
});
}
}, {
// These attributes correspond to the GitHub user data we care about:
// http://developer.github.com/v3/users/
ATTRS: {
login : {value: null},
name : {value: null},
html_url : {value: null},
avatar_url : {value: null},
public_repos : {value: 0},
followers : {value: 0}
}
});
// -- Repo ---------------------------------------------------------------------
Repo = Y.Base.create('repo', Y.Model, [], {
// The <code>id</code> attribute for this Model will be an alias for <code>name</code>.
idAttribute: 'name'
}, {
// These attributes correspond to the GitHub repo data we care about:
// http://developer.github.com/v3/repos/
ATTRS: {
name : {value: null},
html_url : {value: null},
description: {value: null},
watchers : {value: 0},
forks : {value: 0},
language : {value: null},
owner : {value: null}
}
});
// -- RepoList -----------------------------------------------------------------
RepoList = Y.Base.create('repoList', Y.ModelList, [GithubSync], {
model : Repo,
githubURL: '/users/{user}/repos',
buildURL: function () {
// This list's <code>user</code> is used to build the API URL.
return Y.Lang.sub(this.githubURL, {
user: this.get('user').getAsURL('login')
});
},
// Override the default <code>comparator()</code> so items are sorted by most number of
// watchers to least number of watchers.
comparator: function (repo) {
// Note the minus sign.
return -repo.get('watchers');
}
}, {
ATTRS: {
// A <code>RepoList</code> has a <code>User</code> associated with it, this allows for a
// loose-coupling between a user and set of repositories.
user: {value: null}
}
});
// -- Contributor --------------------------------------------------------------
Contributor = Y.Base.create('contributor', User, [], {}, {
ATTRS: {
contributions: {value: 0}
}
});
// -- ContributorList ----------------------------------------------------------
ContributorList = Y.Base.create('contributorList', Y.ModelList, [GithubSync], {
model : Contributor,
githubURL: '/repos/{user}/{repo}/contributors',
buildURL: function () {
// This list's <code>repo</code> is used to build the API URL.
var repo = this.get('repo');
return Y.Lang.sub(this.githubURL, {
user: repo.getAsURL('owner.login'),
repo: repo.getAsURL('name')
});
},
// Override the default <code>comparator()</code> so items are sorted by most number of
// contributors to least number of contributors.
comparator: function (contributor) {
// Note the minus sign.
return -contributor.get('contributions');
}
}, {
ATTRS: {
// A <code>ContributorList</code> has a <code>Repo</code> associated with it, this allows for
// a loose-coupling between a repo and set of contributors.
repo: {value: null}
}
});
// -- User View ----------------------------------------------------------------
UserView = Y.Base.create('userView', Y.View, [], {
// Compiles the User Template into a reusable Handlebars template.
template: Y.Handlebars.compile(Y.one('#t-user').getHTML()),
render: function () {
// Retrieves all of the model instance's data as a simple JSON struct.
var user = this.get('model').toJSON(),
content;
// Add proper pluralized labels for numerical data fields.
addLabel(user, 'public_repos', 'Public Repo');
addLabel(user, 'followers', 'Follower');
// Applies the <code>User</code> model data to the User Template and sets the
// resulting HTML as the contents of this view's container.
content = this.template(user);
this.get('container').setHTML(content);
return this;
}
});
// -- Repo View ----------------------------------------------------------------
RepoView = Y.Base.create('repoView', Y.View, [], {
// Compiles the Repo Template into a reusable Handlebars template.
template: Y.Handlebars.compile(Y.one('#t-repo').getHTML()),
render: function () {
// Retrieves all of the model instance's data as a simple JSON struct.
var repo = this.get('model').toJSON(),
content;
// Add proper pluralized labels for numerical data fields.
addLabel(repo, 'watchers', 'Watcher');
addLabel(repo, 'forks', 'Fork');
// Applies the <code>Repo</code> model data to the Repo Template and sets the
// resulting HTML as the contents of this view's container.
content = this.template(repo);
this.get('container').setHTML(content);
return this;
}
});
// -- RepoList View ------------------------------------------------------------
RepoListView = Y.Base.create('repoListView', Y.View, [], {
// Compiles the RepoList Template into a reusable Handlebars template.
template: Y.Handlebars.compile(Y.one('#t-repo-list').getHTML()),
// Attach DOM events for the view. The <code>events</code> object is a mapping of
// selectors to an object containing one or more events to attach to the
// node(s) matching each selector.
events: {
'.repo': {
click: 'selectRepo'
}
},
initializer: function () {
// The <code>selectRepo</code> event is fired when the user chooses a GitHub repo
// to view its contributors. This event will bubble up to the
// <code>ContributorsApp</code> via the <code>UserPageView</code> view when it is the app's
// <code>activeView</code>.
this.publish('selectRepo', {preventable: false});
},
render: function () {
var repos = this.get('modelList'),
reposData, content;
// Iterates over all <code>Repo</code> models in the list and retrieves each model
// instance's data as a simple JSON structs and collects it in an array.
reposData = repos.map(function (repo) {
var data = repo.toJSON();
// Add <code>clientId</code> to the data, this is ignored by <code>toJSON()</code>. This
// will be used by the template and is an easy way to regain access
// to the associated Repo model.
data.clientId = repo.get('clientId');
// Add proper pluralized labels for numerical data fields.
addLabel(data, 'watchers', 'Watcher');
addLabel(data, 'forks', 'Fork');
return data;
});
// Applies the <code>RepoList</code> data to the RepoList Template and sets the
// resulting HTML as the contents of this view's container.
content = this.template({repos: reposData});
this.get('container').setHTML(content);
return this;
},
// Called when the user clicks anywhere on a repo line-item in our list
// of repos. This will retreive the corresponding Repo model instance
// from the RepoList via the <code>clientId</code>.
selectRepo: function (e) {
// Noop when the element clicked is an anchor, we'll let the link
// preform its default action.
if (e.target.test('a')) {
return;
}
// Because we've rendered our list of repos with the <code>clientId</code> as the
// <code>id</code> attribute for each repo node, we can use the client id to
// retreive the Repo model instance from the RepoList.
var repos = this.get('modelList'),
repo = repos.getByClientId(e.currentTarget.get('id'));
if (repo) {
this.fire('selectRepo', {repo: repo});
}
}
});
// -- ContributorList View -----------------------------------------------------
ContributorListView = Y.Base.create('contributorListView', Y.View, [], {
// Compiles the ContributorList Template into a reusable Handlebars template.
template: Y.Handlebars.compile(Y.one('#t-contributor-list').getHTML()),
render: function () {
var contributors = this.get('modelList'),
contributorsData, content;
// Iterates over all <code>Contributor</code> models in the list and retrieves a
// sub-set of each model instance's data as a simple JSON structs and
// collects it in an array.
contributorsData = contributors.map(function (contributor) {
// Only a few of the <code>Contributor</code> data attributes are needed.
var data = contributor.getAttrs(['login', 'avatar_url', 'contributions']);
// Add proper pluralized labels for numerical data fields.
addLabel(data, 'contributions', 'Contribution');
return data;
});
// Applies the <code>ContributorList</code> data to the ContributorList Template
// along with the total number of contributors for the repo.
content = this.template({
num : contributors.size(),
contributors: contributorsData
});
// Sets the resulting HTML from apply the data to the template as the
// contents of this view's container.
this.get('container').setHTML(content);
return this;
}
});
// -- HomePage View ------------------------------------------------------------
HomePageView = Y.Base.create('homePageView', Y.View, [], {
// Compiles the HomePage Template into a reusable Handlebars template.
template: Y.Handlebars.compile(Y.one('#t-home').getHTML()),
// Attach DOM events for the view. The <code>events</code> object is a mapping of
// selectors to an object containing one or more events to attach to the
// node(s) matching each selector.
events: {
'button': {
click: 'changeUser'
},
'input': {
keypress: 'enter'
}
},
initializer: function () {
// The <code>changeUser</code> event is fired when the user chooses a GitHub user
// to start browsing. This event will bubble up to the <code>ContributorsApp</code>
// when this view is the app's <code>activeView</code>.
this.publish('changeUser', {preventable: false});
},
render: function () {
// Retrieves just the <code>login</code> of the <code>User</code> model instance and applies
// it to the HomePage Template.
var user = this.get('model'),
content = this.template(user.getAttrs(['login']));
// Adds the "home-page" CSS class to aid styling and sets the resulting
// HTML as the contents of this view's container.
this.get('container').addClass('home-page').setHTML(content);
return this;
},
// Called when the user clicks the "Show Repos" button. This will retrieve
// the GitHub username from the text <code>&lt;input&gt;</code> and fire the <code>changeUser</code>
// event, passing on the username to the app.
changeUser: function () {
var user = this.get('container').one('input').get('value');
if (user) {
this.fire('changeUser', {user: user});
}
},
// Called when the user types inside the text <code>&lt;input&gt;</code>; when the "enter"
// key is pressed, this will call the <code>changeUser()</code> method.
enter: function (e) {
// Check for "enter" keypress.
if (e.keyCode === 13) {
this.changeUser();
}
}
});
// -- UserPage View ------------------------------------------------------------
UserPageView = Y.Base.create('userPageView', Y.View, [], {
initializer: function () {
var user = this.get('model'),
repos = this.get('modelList');
// This view serves as a "page"-level view containing two sub-views to
// which it delegates rendering and stitches together the resulting UI.
// Sub-views are created to render the <code>User</code> and <code>RepoList</code>.
this.userView = new UserView({model: user});
this.repoListView = new RepoListView({modelList: repos});
// This will cause the sub-views' custom events to bubble up to here.
this.userView.addTarget(this);
this.repoListView.addTarget(this);
},
// This destructor is specified so this view's sub-views can be properly
// destroyed and cleaned up.
destructor: function () {
this.userView.destroy();
this.repoListView.destroy();
delete this.userView;
delete this.repoListView;
},
render: function () {
// A document fragment is created to hold the resulting HTML created
// from rendering the two sub-views.
var content = Y.one(Y.config.doc.createDocumentFragment());
// This renders each of the two sub-views into the document fragment,
// then sets the fragment as the contents of this view's container.
content.append(this.userView.render().get('container'));
content.append(this.repoListView.render().get('container'));
// Adds the "user-page" CSS class to aid styling and sets the document
// fragment containing the two rendered sub-views as the contents of
// this view's container.
this.get('container').addClass('user-page').setHTML(content);
return this;
}
});
// -- RepoPage View ------------------------------------------------------------
RepoPageView = Y.Base.create('repoPageView', Y.View, [], {
initializer: function () {
var repo = this.get('model'),
contributors = this.get('modelList');
// This view serves as a "page"-level view containing two sub-views to
// which it delegates rendering and stitches together the resulting UI.
// Sub-views are created to render the <code>RepoView</code> and
// <code>ContributorListView</code>.
this.repoView = new RepoView({model: repo});
this.contributorListView = new ContributorListView({modelList: contributors});
// This will cause the sub-views' custom events to bubble up to here.
this.repoView.addTarget(this);
this.contributorListView.addTarget(this);
},
// This destructor is specified so this view's sub-views can be properly
// destroyed and cleaned up.
destructor: function () {
this.repoView.destroy();
this.contributorListView.destroy();
delete this.repoView;
delete this.contributorListView;
},
render: function () {
// A document fragment is created to hold the resulting HTML created
// from rendering the two sub-views.
var content = Y.one(Y.config.doc.createDocumentFragment());
// This renders each of the two sub-views into the document fragment,
// then sets the fragment as the contents of this view's container.
content.append(this.repoView.render().get('container'));
content.append(this.contributorListView.render().get('container'));
// Adds the "repo-page" CSS class to aid styling and sets the document
// fragment containing the two rendered sub-views as the contents of
// this view's container.
this.get('container').addClass('repo-page').setHTML(content);
return this;
}
});
// -- Contributors App ---------------------------------------------------------
ContributorsApp = Y.Base.create('contributorsApp', Y.App, [], {
// This is where we can declare our page-level views and define the
// relationship between the "pages" of our application. We can later use the
// <code>showView()</code> method to create and display these views.
views: {
homePage: {
type: HomePageView
},
userPage: {
type: UserPageView
},
repoPage: {
type : RepoPageView,
parent: 'userPage'
}
},
initializer: function () {
// Here we register a listener for the <code>HomePageView</code>'s <code>changeUser</code>
// event. When the <code>HomePageView</code> is the <code>activeView</code>, its events will
// bubble up to this app instance.
this.on('*:changeUser', this.navigateToUser);
// Here we register a listener for the <code>RepoListView</code>'s <code>selectRepo</code>
// event. The <code>RepoListView</code>'s events bubble up to the <code>UserPageView</code>,
// so when it is the <code>activeView</code>, its events will bubble up to this
// app instance.
this.on('*:selectRepo', this.navigateToRepo);
// Once our app is ready, we'll either dispatch to our route-handlers if
// the current URL matches one of our routes, or we'll simply show the
// <code>HomePageView</code>.
this.once('ready', function (e) {
if (this.hasRoute(this.getPath())) {
this.dispatch();
} else {
this.showHomePage();
}
});
},
// -- Event Handlers -------------------------------------------------------
// When called, this will navigate the application to the user-page for the
// GitHub username specified on the event facade. This will cause our app to
// dispatch to its route-handlers along with updating the URL.
navigateToUser: function (e) {
var activeView = this.get('activeView');
// We want to add a history entry for "/" when we're showing the home
// page, but the app's current path isn't already "/". This provides
// proper back button support and helps get around a side effect of
// using hash based URLs in this example.
if (activeView instanceof HomePageView && this.getPath() !== '/') {
// Adds history entry for "/" so we can get back to the home page
// via the back button.
this.save('/');
}
this.navigate('/github/' + e.user + '/');
},
// When called, this will navigate the application to the repo-page for the
// GitHub repository specified on the event facade. This will cause our app
// to dispatch to its route-handlers along with updating the URL.
navigateToRepo: function (e) {
var repo = e.repo;
this.navigate('/github/' + repo.get('owner.login') + '/' + repo.get('name') + '/');
},
// -- Route Handlers -------------------------------------------------------
// This will be called for all URLs which start with: "/github/:user/"; its
// job is to make sure a <code>User</code> model for the specified GitHub username is
// fully-loaded and placed on the request object for other route-handlers.
handleUser: function (req, res, next) {
var username = req.params.user,
user = this.get('user'),
self = this;
// When the current <code>User</code> model set on the app is new or the specified
// GitHub username from the URL is different, a new user model instance
// is created, loaded, and set on this app before adding it to the
// request object.
if (username === user.get('login') && !user.isNew()) {
// Places a reference to the user model on the request object before
// delegating to the next route-handler.
req.user = user;
next();
} else {
// Create a new <code>User</code> model instance using the specified GitHub
// username from the URL.
user = new User({login: username});
// Load the user's data from the GitHub API, then sent the user
// model on both this app and the request object before delegating
// to the next route-handler.
user.load(function () {
self.set('user', user);
req.user = user;
next();
});
}
},
// This will be called for all URLs which start with: "/github/:user/"; its
// job is to make sure a fully-loaded <code>RepoList</code> instance is placed on the
// request object. It is assumed that <code>handleUser()</code> route-handler has
// already placed the <code>User</code> model on the request object.
handleRepos: function (req, res, next) {
var user = req.user,
repos = this.get('repos');
// Adds a reference to this app's <code>RepoList</code> to the request object.
req.repos = repos;
// This makes sure the <code>RepoList</code> is loaded for the current user.
if (user === repos.get('user')) {
next();
} else {
// A fade transition is preferred when we've switched users, so it
// is added to the response object.
res.transition = 'fade';
// Sets the current user model on the <code>RepoList</code> instance and loads
// the repos for the given user before continuing.
repos.set('user', user).load(function () {
next();
});
}
},
// This will be called for URLs which match: "/github/:user/:repo/"; its job
// is to make sure a fully-loaded <code>ContributorList</code> instance is placed on
// the request object. It is assumed that the <code>handleUser()</code> and
// <code>handleRepos()</code> route-handlers have already placed the <code>User</code> model and
// <code>RepoList</code> model-list on the request object.
handleRepo: function (req, res, next) {
// This uses data from the request object to look for a <code>Repo</code> model
// instance in the <code>RepoList</code>.
var repoId = req.params.repo,
repos = req.repos,
repo = repos.getById(repoId),
contributors = this.get('contributors');
// We error-out when the specified repo name does not exist in the list
// of repos for the current GitHub user.
if (!repo) {
return next('GitHub repository was not found.');
}
// Adds a reference to the <code>Repo</code> model and <code>ContributorList</code> to the
// request object.
req.repo = repo;
req.contributors = contributors;
// This makes sure the <code>ContributorList</code> is loaded for the current repo.
if (repo === contributors.get('repo')) {
next();
} else {
// Sets the current repo model on the <code>ContributorList</code> instance and
// loads the contributors before continuing.
contributors.set('repo', repo).load(function () {
next();
});
}
},
// This is called when the URL is "/" and will show our app's home page.
showHomePage: function (req) {
this.showView('homePage', {
model: this.get('user')
});
},
// This is the final route for "/github/:user/" URLs and will show our app's
// user page which lists the user's repos.
showUserPage: function (req, res) {
this.showView('userPage', {
model : req.user,
modelList: req.repos
}, {
// Overrides the default transition with the preferred one, if set.
transition: res.transition
});
},
// This is the final route for "/github/:user/:repo/" URLs and will show our
// app's repo page which lists the contributors to the repo/project.
showRepoPage: function (req, res) {
this.showView('repoPage', {
model : req.repo,
modelList: req.contributors
}, {
// Overrides the default transition with the preferred one, if set.
transition: res.transition
});
}
}, {
ATTRS: {
// These attributes will be used by the app to hold its current state,
// and they will be accessed and modified by our route-handlers.
user : {value: new User()},
repos : {value: new RepoList()},
contributors: {value: new ContributorList()},
// Our app will use more advanced routing features where multiple
// route-handlers will be used to fulfill a "request", allowing us to
// encapsulate and reuse our data processing logic. Note: the order the
// route-handlers are defined in is significant.
routes: {
value: [
{path: '/', callback: 'showHomePage'},
{path: '/github/:user/*', callback: 'handleUser'},
{path: '/github/:user/*', callback: 'handleRepos'},
{path: '/github/:user/', callback: 'showUserPage'},
{path: '/github/:user/:repo/', callback: 'handleRepo'},
{path: '/github/:user/:repo/', callback: 'showRepoPage'}
]
}
}
});
// -- Go-go-gadget App! --------------------------------------------------------
// Create and render a new instance of our <code>ContributorsApp</code>!
new ContributorsApp({
// We force this to false for this example app because there is no server.
serverRouting: false,
// Here we set our app's rendering container, and restrict which links on
// the page should cause the app to navigate.
container : '#github-app',
linkSelector: '#github-app a',
// Enable view transitions when users are navigating around the app.
transitions: true,
// We'll define the default GitHub user to be "yui".
user: new User({login: 'yui'})
}).render();
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment