my hacker news
An introduction to building mobile apps with Firebase, AngularJS and Ionic
Jamie Sutherland
CTO @ Mallzee
Tweeter me @wedgybo
What are we going to build?
A group chat application based around hacker news posts
The battle plan
- Get posts from hacker news
- View links posted on hacker news
- Store some user info to use when chatting
- Share posts to the group
- Comment on the shared posts with your pals
- Improvements (If we have time)
Feel free to shout out with any questions
Install ionic
> npm install -g ionic
> ionic start myhackernews
> cd myhackernews
> ionic setup sass
> ionic serve --lab
Provides AngularJS and an awesome set of components to help quickly build cross platform mobile applications
Setup firebase
Build apps using their API to sync data in realtime.
Create a new app
My Hacker News
Blue Peter moment:
ANgularfire
> bower install firebase angularfire
<!-- ionic/angularjs js -->
<script src="lib/ionic/js/ionic.bundle.js"></script>
<script src="lib/firebase/firebase.js"></script>
<script src="lib/angularfire/dist/angularfire.min.js"></script>
angular.module('starter', ['ionic', 'starter.controllers', 'starter.services', 'firebase'])
www/index.html
www/js/app.js
Hacker news data SERVICE
Angular uses services as the standard way of interacting with data. Use these to keep your data logic code in once place.
.factory('HackerNews', function ($http) {
return {
newest: function () {
// Returns a promise to the data from the API
return $http.get('http://hn-api.ionic.io/new/1');
}
};
})
www/js/services.js
Hookup hacker news service and Feed YOur SCOPE
We want to get the latest posts and be able to view and share the ones of interest with our buddies
.controller('HackerNewsCtrl',
function($scope, $ionicLoading, HackerNews) {
$scope.posts = [];
$scope.loadPosts = function () {
HackerNews.newest().then(function (posts) {
$scope.posts = posts.data;
});
};
// Load the posts when this controller is loaded
$scope.loadPosts();
})
www/js/controllers.js
Hacker news posts
- ng-repeat: Looping through content
- ng-click: Fire a scope function when the item is clicked
<ion-view title="Hacker News">
<ion-content>
<ion-list>
<ion-item class="item-button-right" ng-repeat="post in posts" ng-click="viewPost(post)">
<h2>{{post.title}}</h2>
<p>{{post.submitted_by}} - {{post.submitted_on}}</p>
</ion-item>
</ion-list>
</ion-content>
</ion-view>
www/templates/tab-hackernews.html
POst Viewer service
.factory('PostViewer', function ($rootScope, $sce, $ionicModal) {
// Create a new scope for this viewer to bind to
var $scope = $rootScope.$new();
angular.extend($scope, {
post: null,
hideModal: function() {
return modal.hide();
},
trustedUrl: function (url) {
return $sce.trustAsResourceUrl(url);
}
});
$ionicModal.fromTemplateUrl(
'templates/post-detail.html',
{
scope: $scope
}
).then(function (ionicModal) {
modal = ionicModal;
});
return {
viewPost: function (post) {
// Update the scope with the latest post to view
$scope.post = post;
modal.show();
}
};
})
www/js/services.js
Post Viewer
<ion-modal-view>
<ion-header-bar align-title="left">
<h1 class="title">{{post.title}}</h1>
<div class="buttons">
<button class="button button-outline button-positive" ng-click="hideModal()">
Close
</button>
</div>
</ion-header-bar>
<ion-content scroll="false">
<iframe height="100%" width="100%" ng-src="{{trustedUrl(post.url)}}" border="0"></iframe>
</ion-content>
</ion-modal-view>
// Inject PostViewer and add this to HackerNewsCtrl
$scope.viewPost = function (post) {
PostViewer.viewPost(post);
};
www/js/controllers.js
www/templates/post-detail.html
USER service
We need a service to store the information about the user and which group they are participating in
.factory('User', function ($window) {
var user = {
name: '',
group: ''
};
// Decode the user in local storage
user = JSON.parse($window.localStorage.getItem('user')) || user;
return {
get: function () {
return user;
},
save: function (updatedUser) {
user = updatedUser;
// Save any changes back to local storage for next time
$window.localStorage.setItem('user', JSON.stringify(user));
}
};
});
www/js/services.js
User Account
ng-model: Maps data
ng-change: Automagical saving
<ion-view title="Account">
<ion-content class="padding">
<div class="list">
<label class="item item-input item-label">
<span class="input-label">Username</span>
<input type="text" placeholder="Username"
ng-model="user.name" ng-change="saveUser(user)">
</label>
<label class="item item-input item-label">
<span class="input-label">Group</span>
<input type="text" placeholder="Group name"
ng-model="user.group" ng-change="saveUser(user)">
</label>
</div>
</ion-content>
</ion-view>
$scope.saveUser = function (user) {
User.save(user);
}
www/js/controllers.js - AccountCtrl
www/templates/tab-account.html
Share a post with the group
Lets create a service to interact with the group
.factory('GroupNews', function ($q, $firebase, User) {
var ref = new Firebase("https://my-hacker-news.firebaseio.com/groups/" + User.get().group || '');
return {
posts: $firebase(ref).$asArray(),
getPosts: function () {
return this.posts.$loaded();
},
sharePost: function (post) {
return this.posts.$add(post);
},
get: function (postId) {
return $firebase(ref.child(postId)).$asObject().$loaded();
}
};
})
<button class="button button-clear" ion-stop-event="click" ng-click="sharePost(post)">
<i class="icon ion-ios7-upload-outline"></i>
</button>
$scope.sharePost = function (post) {
$ionicLoading.show({
template: 'Sharing post ' + post.title
});
GroupNews.sharePost(post).then($ionicLoading.hide);
};
www/js/controllers.js
www/templates/tab-hackernews.html
www/js/services.js
Viewing the shared posts in the group
.controller('ChatsCtrl', function($scope, GroupNews) {
GroupNews.getPosts().then(function (posts) {
$scope.posts = posts;
});
})
<ion-view title="Chats">
<ion-content>
<ion-list>
<ion-item ng-repeat="post in posts" type="item-text-wrap" href="#/chats/{{post.$id}}">
<h2>{{post.title}}</h2>
</ion-item>
</ion-list>
</ion-content>
</ion-view>
www/templates/tab-chats.html
www/js/controllers.js
Chat details
We'll setup for viewing information about the shared post
- Set the state
- Act on the state parameter
.controller('ChatDetailCtrl',
function($scope, $stateParams, $firebase, User, GroupNews, PostViewer) {
GroupNews.get($stateParams.postId).then(function (post) {
$scope.post = post;
});
$scope.viewPost = function (post) {
PostViewer.viewPost(post);
};
})
.state('tab.chat-detail', {
url: 'chats/:postId',
views: {
'tab-chats': {
templateUrl: 'templates/chat-detail.html',
controller: 'ChatDetailCtrl'
}
}
})
www/js/app.js
www/js/controllers.js
Chat details
We need to show some detail on the post
<ion-view title="Group Chat">
<ion-content padding="true">
<div class="list card">
<div class="item item-text-wrap">
<h2>{{post.title}}</h2>
</div>
<div class="item">
<p>
Post by: {{post.submitted_by}}
</p>
<p>
<span href="#" class="subdued">{{post.submitted_on}}</span> |
<span href="#" class="subdued">{{post.points}} Points</span> |
<span href="#" class="subdued">{{post.comments}} Comments</span>
</p>
</div>
<a class="item item-icon-left item-icon-right" ng-click="viewPost(post)" href="#">
<i class="icon ion-ios7-world"></i>
View post
<i class="icon ion-ios7-arrow-right"></i>
</a>
</div>
</ion-view>
www/templates/chat-detail.html
Comments
Lets get some commenting on the go
<ul class="comments">
<li class="comment" ng-repeat="comment in post.userComments">
<p>{{comment.text}}<span>{{comment.user}}</span></p>
</li>
</ul>
</ion-content>
<ion-footer-bar keyboard-attach class="bar-stable item-input-inset">
<label class="item-input-wrapper">
<input type="text" placeholder="Type your message" ng-model="comment.text" />
</label>
<button class="button button-clear" ng-click="addComment(post.$id, comment)">
Send
</button>
</ion-footer-bar>
www/templates/chat-detail.html
Commenting
We need to hookup the ng-click actions to the controller which then adds comments via the GroupNews service
addComment: function (postId, comment) {
$firebase(ref.child(postId + '/userComments')).$asArray().$add(comment);
},
www/js/services.js - GroupNews
$scope.comment = {
user: User.get().name
};
$scope.addComment = function (postId, comment) {
GroupNews.addComment(postId, comment);
};
$scope.viewPost = function (post) {
PostViewer.viewPost(post);
};
www/js/controllers.js - ChatDetailCtrl
Improvments
(Do we have time?)
Pull to refresh
Lets allow the user to refresh the latest list of Hacker News posts
<ion-refresher
pulling-text="Pull to refresh..."
on-refresh="loadPosts()">
</ion-refresher>
www/templates/tab-hackernews.html
HackerNews.newest(1).then(function (posts) {
$scope.posts = posts;
$scope.$broadcast('scroll.refreshComplete');
});
www/js/controllers.js - HackerNewsCtrl.loadPosts
Filtering sassy GOODNESS
Lets clean up those comments
.comments {
margin:0; padding:0;
list-style-type:none;
li {
display:block; clear:both;
max-width:50%;
margin:0 0 1rem 0; padding:0;
p {
border-radius:.75rem;
background:#e6e5eb; color:#383641;
padding:.6875rem; margin:0;
font-size:.875rem;
span {
display: block;
font-size: 10px;
font-weight: light;
}
}
}
li.mine {
float:right;
p {
background:#158ffe; color:#fff;
span {
text-align: right;
}
}
}
}
.filter('isMine', function (User) {
return function (input) {
return User.get().name === input;
};
})
ng-class="{'mine': {{comment.user | isMine}}}"
Test on a device
Use the ionic tools to create projects for your device of choice.
Live reload changes on the device is pretty damn awesome!
> ionic platform add [ios|android]
> ionic run -l [ios|android]
Let me play!
Point your mobile browser here and give it a whirl.
https://wedgybo.github.io/my-hacker-news
Remember to name yourself and set the group to gdgedinburgh in the account page.
If nothing shows. Tell your browser to load unsecure scripts
Thanks for listening
jamie@mallzee.com - +JamieSutherland - @wedgybo