Filippos Vasilakis
Web Dev
We will go through an Ember app and we will touch the following:
EmberJS adds some sugar on top of Javascript
Person = Ember.Object.extend({
say(thing) {
alert(thing);
}
});
Developer = Person.extend({
isDeveloper: true.
say(thing) {
return this._super(thing) + 'world';
}
});
var person = Person.create();
person.say('Hello'); // alerts " says: Hello"
Person = Ember.Object.extend({
init() {
var name = this.get('name');
alert(`${name}, reporting for duty!`);
}
});
var person = Person.create();
var name = person.get('name');
person.set('name', 'Tobias Fünke');
If you don't use those methods, computed properties won't recalculate, observers won't fire, and templates won't update
A micro Twitter
It's the same app that Michael Hartl has build for the Rails tutorial
Built in JSONAPI
GET | /api/v1/session |
POST | /api/v1/session |
GET | /api/v1/users |
POST | /api/v1/users |
GET | /api/v1/users/:id |
PUT | /api/v1/users/:id |
DELETE | /api/v1/users/:id |
GET | /api/v1/microposts |
GET (feed) | /api/v1/microposts?feed_for_user_id=:id |
POST | /api/v1/microposts |
GET | /api/v1/microposts/:id |
PUT | /api/v1/microposts/:id |
DELETE | /api/v1/microposts/:id |
GET | /api/v1/users/:user_id/followers |
POST | /api/v1/users/:user_id/followers/:id |
DELETE | /api/v1/users/:user_id/followers/:id |
GET | /api/v1/users/:user_id/followings |
POST | /api/v1/users/:user_id/followings/:id |
DELETE | /api/v1/users/:user_id/followings/:id |
npm install -g ember-cli
npm install -g bower
ember new micro-twitter
Ember CLI
ember | Prints out a list of available commands. |
ember new <app-name> | Creates a directory called <app-name> and in it, generates an application structure. If git is available the directory will be initialized as a git repository and an initial commit will be created. Use --skip-git flag to disable this feature. |
ember init | Generates an application structure in the current directory. |
ember build | Builds the application into the dist/ directory (customize via the --output-path flag). Use the--environment flag to specify the build environment (defaults to development). Use the --watch flag to keep the process running and rebuilding when changes occur. |
ember server | Starts the server. The default port is 4200. Use the --proxy flag to proxy all ajax requests to the given address. For example, ember server --proxy http://127.0.0.1:8080 will proxy all ajax requests to the server running at http://127.0.0.1:8080. Aliases: ember s, ember serve |
ember generate <generator-name> <options> | Runs a specific generator. To see available generators, run ember help generate. Alias: ember g |
ember destroy <generator-name> <options> | Removes code created by the generate command. If the code was generated with the --pod flag, you must use the same flag when running the destroy command. Alias: ember d |
ember test | Run tests with Testem in CI mode. You can pass any options to Testem through a testem.json file. By default, Ember CLI will search for it under your project’s root. Alternatively, you can specify a config-file. Alias:ember t |
ember install <addon-name> | Installs the given addon into your project and saves it to the package.json file. If provided, the command will run the addon’s default blueprint. |
Ember CLI
(taken from http://ember-cli.com/user-guide/)
app/ | Contains your Ember application’s code. Javascript files in this directory are compiled through the ES6 module transpiler and concatenated into a file called <app-name>.js. See the table below for more details. |
dist/ | Contains the distributable (optimized and self-contained) output of your application. Deploy this to your server! |
public/ | This directory will be copied verbatim into the root of your built application. Use this for assets that don’t have a build step, such as images or fonts. |
tests/ | Includes your app’s unit and integration tests, as well as various helpers to load and run the tests. |
tmp/ | Temporary application build-step and debug output. |
bower_components/ | Bower dependencies (both default and user-installed). |
node_modules/ | npm dependencies (both default and user-installed). |
vendor/ | Your external dependencies not installed with Bower or npm. |
.jshintrc | JSHint configuration. |
.gitignore | Git configuration for ignored files. |
ember-cli-build.js | Contains the build specification for Broccoli. |
bower.json | Bower configuration and dependency list. See Managing Dependencies. |
package.json | npm configuration and dependency list. Mainly used to list the dependencies needed for asset compilation. |
Ember CLI folder structure
(taken from http://ember-cli.com/user-guide/)
app/app.js | Your application’s entry point. This is the first executed module. |
app/index.html | The only page of your single-page app! Includes dependencies, and kickstarts your Ember application. See app/index.html. |
app/router.js | Your route configuration. The routes defined here correspond to routes in app/routes/. |
app/styles/ | Contains your stylesheets, whether SASS, LESS, Stylus, Compass, or plain CSS (though only one type is allowed, see Asset Compilation). These are all compiled into <app-name>.css. |
app/templates/ | Your HTMLBars templates. These are compiled to /dist/assets/<app-name>.js. The templates are named the same as their filename, minus the extension (i.e. templates/foo/bar.hbs -> foo/bar). |
app/controllers/app/models/, etc. |
Ember CLI app/ folder structure
(taken from http://ember-cli.com/user-guide/)
The first step is to define the basic 'routes' of our app
But let'se see what 'routes' are in EmberJS first..
When we say Ember Routes we mean the URLs structure but also the UI structure.
(the directory structure of templates/routes/controllers follow the routing as well!)
# config/routes.rb
resources :artist do
resources :albums do
resources :album
end
end
// config/router.js
App.Router.map(function() {
this.route('artist', { path: 'artists/:artist_id' }, function() {
this.route('albums', function() {
// since we passed a function, index is generated for us
this.route('album', { path: '/:album_id' });
});
});
});
≠
At every level of nesting (including the top level), Ember automatically provides a route for the / path named index
Each template will be rendered into the {{outlet}} of its parent route's template.
<header class="navbar navbar-fixed-top navbar-inverse">
<!-- html(bars) code -->
</header>
<div class="container">
{{outlet}} <!-- The child(nested) template -->
<footer class="footer">
<!-- html(bars) code -->
</footer>
</div>
<h1> Welcome to our wonderful website! </h1>
Application Root Template
Index Template
This loads everytime when a suburl is accessed
This loads only when the root url is accessed (which happens to be the Application Template)
+
<header class="navbar navbar-fixed-top navbar-inverse">
<!-- html(bars) code -->
</header>
<div class="container">
<h1> Welcome to our wonderful website! </h1>
<footer class="footer">
<!-- html(bars) code -->
</footer>
</div>
http://localhost:4200/
<header class="navbar navbar-fixed-top navbar-inverse">
<!-- html(bars) code -->
</header>
<div class="container">
{{outlet}} <!-- The child(nested) template -->
<footer class="footer">
<!-- html(bars) code -->
</footer>
</div>
Application Root Template
<h2> {{artistName}} </h2>
{{outlet}} <!-- index or albums/songs/bio template -->
Artist Root Template
This loads everytime when a suburl is accessed
+
This loads only when the root url of the Artist Route is accessed
<header class="navbar navbar-fixed-top navbar-inverse">
<!-- html(bars) code -->
</header>
<div class="container">
<h2> Bob Dylan </h2>
<p> This artist is well known for ....
<footer class="footer">
<!-- html(bars) code -->
</footer>
</div>
<p> This artist is well know for .... </p>
Artist Index Template
This loads only when the root url of the ArtistAlbums Route is accessed
+
http://localhost:4200/artists/bobdylan
<header class="navbar navbar-fixed-top navbar-inverse">
<!-- html(bars) code -->
</header>
<div class="container">
{{outlet}} <!-- The child(nested) template -->
<footer class="footer">
<!-- html(bars) code -->
</footer>
</div>
Application Root Template
<h2> {{artistName}} </h2>
{{outlet}} <!-- albums/songs/bio template -->
Artist Root Template
This loads everytime when a suburl is accessed
+
This loads only when the root url of the Artist Route is accessed
<header class="navbar navbar-fixed-top navbar-inverse">
<!-- html(bars) code -->
</header>
<div class="container">
<h2> Bob Dylan </h2>
<ul>
<li>Bob Dylan</li>
<li>The Freewheelin' Bob Dylan</li>
<li>The Times They Are a-Changin'</li>
</ul>
<footer class="footer">
<!-- html(bars) code -->
</footer>
</div>
<h3> Albums </h3>
<!-- albums html(bars) code -->
Albums Index Template
This loads only when the root url of the ArtistAlbums Route is accessed
+
http://localhost:4200/artists/bobdylan/albums
Routes in Ember play a crucial role
A route in Ember:
Router Request Lifecycle
enter()
(private)
activate()
executed when entering the route
Model hooks
beforeModel(transition)
model(params)
afterModel(resolvedModel, transition)
setupController(controller, model)
renderTemplate(controller, model)
Usually model hooks and setupController() are used
Route model hooks
setupController(controller, model)
renderTemplate(controller, model)
A controller in Ember:
A template in Ember:
A component in Ember:
//app/config.js
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: config.locationType
});
Router.map(function() {
this.route('help');
this.route('about');
this.route('contact');
this.route('sessions.new', {path: '/login'});
this.route('users.new', {path: '/signup'});
this.route('users', function() {
});
this.route('user', {path: '/users/:user_id'}, function() {
this.route('edit');
this.route('following');
this.route('followers');
});
});
export default Router;
Back to our micro-twitter app
Having ready the app/router.js, we have defined the basic specifications of our app
We nest 'edit', 'followings' and 'followers' routes under user to reuse 'user' route model
Back to our micro-twitter app
Implementing our first route/template
//app/templates/application.hbs
<header class="navbar navbar-fixed-top navbar-inverse">
<div class="container">
{{#link-to 'index' id='logo'}}sample app{{/link-to}}
<nav>
<ul class="nav navbar-nav pull-right">
<li>{{link-to 'Home' 'application'}}</li>
<li>{{link-to 'Help' 'help'}}</li>
</ul>
</nav>
</div>
</header>
<div class="container">
{{outlet}}
<footer class="footer">
<nav>
<ul>
<li>{{link-to 'About' 'about'}}</li>
<li>{{link-to 'Contact' 'contact'}}</li>
</ul>
</nav>
</footer>
</div>
Our basic Application Template
//app/templates/help.hbs
<h1>Help</h1>
<p>
Do you need help? Open a gihub issue in the repository
<a href="https://github.com/vasilakisfil/rails_tutorial_ember">
<em>rails_tutorial_ember</em>
</a>.
</p>
Back to our micro-twitter app
http://localhost:4200/help
//app/routes/help.js
import Ember from 'ember';
export default Ember.Route.extend({});
If your route is empty, you don't need to define it (ember does that automatically)
Implementing our first route/template
Back to our micro-twitter app
Implementing our first route/template
{{#if isAtWork}}
Ship that code!
{{else if isReading}}
You can finish War and Peace eventually...
{{/if}}
{{#each people as |person|}}
Hello, {{person.name}}!
{{else}}
Sorry, nobody is here.
{{/each}}
<div id="logo">
<img src={{logoUrl}} alt="Logo">
</div>
Conditionals
Iterating lists
Binding attributes
{{input type="text" value=firstName disabled=entryNotAllowed size="50"}}
Input helpers
<input type="text" value={{firstName}} disabled={{entryNotAllowed}} size="50">
Personally I would like to see inputs like that
But that's not possible, yet.
After Ember 2.0 and htmlbars Templates are very powerful and they will never cause you a problem.
(before Ember 1.12 we had many issues though )
{{#link-to "photos.edit" 1}}First Photo Ever{{/link-to}}
link-to helper
Ember actions are events that are propagated from template to controller and then to the routes until one of them catches the event.
<h3><button {{action "toggleBody"}}>{{title}}</button></h3>
{{#if isShowingBody}}
<p>{{{body}}}</p>
{{/if}}
export default Ember.Route.extend({
actions: {
toggleBody() {
this.toggleProperty('isShowingBody');
}
}
});
Data Down, Actions Up (DDAU)
Simpler than what it seems
It's common sense that if you try to change the app's state from the low level (component) you will increase the complexity it in the long run leading into more bugs
But if the change is local (only inside a component) and not app-wide don't afraid to break the axiom :)
Always handle actions in the Route when the change is app-wide because routes have access to other routes,controllers and models!
//app/routes/session/new.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return Ember.Object.create({
identifier: null,
password: null
});
}
});
Let's build the login form! (http://localhost:4200/login)
Back to our micro-twitter app...
We create the object to be passed down to controller automatically by setupController()
The route
//app/templates/sessions/new.hbs
<h1>Log in</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
{{#if loginError}}
<div id="error_explanation">
<div class="alert alert-danger">
Invalid email/password combination
</div>
</div>
{{/if}}
<form class="form-basic" role="form" {{action (route-action 'createSession' model) on='submit'}}>
<label for="user_email">Email</label>
{{input class="form-control" type="email" value=model.identifier size="50"}}
<label for="user_password">Password</label> {{link-to '(forgot password)' 'users.new'}}
{{input class="form-control" type="password" value=model.password size="50"}}
<label class="checkbox inline" for="session_remember_me">
{{input type="checkbox" name="session_remember_me" checked=isAdmin}}
<span>Remember me on this computer</span>
</label>
<input type="submit" value="Log in" class="btn btn-primary">
</form>
<p>New user? {{link-to 'Sign up now!' 'users.new'}}</p>
</div>
</div>
The template
Back to our micro-twitter app...
Building the login form! (http://localhost:4200/login)
//app/routes/session/new.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return Ember.Object.create({
identifier: null,
password: null
});
},
actions: {
createSession(session) {
var _this = this;
//handle session creation
Ember.$.ajax({
url: 'http://localhost:3000/api/v1/session',
type: 'POST',
data: session.toJson(),
contentType: 'application/json;charset=utf-8',
dataType: 'json'
}).then(
function(response) {
//handle the token etc
},
function(error) {
_this.controllerFor('login').set('loginError', true);
});
},
},
});
Back to our micro-twitter app...
We handle the action in the route
Building the login form! (http://localhost:4200/login)
//app/routes/session/new.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return Ember.Object.create({
identifier: null,
password: null
});
},
actions: {
createSession(session) {
var _this = this;
this.get('session').authenticate(
'authenticator:devise',
session.get('identifier'),
session.get('password')
).catch((reason) => {
_this.controllerFor('login').set(
'errorMessage',
reason.error || reason
);
});
},
destroySession() {
this.get('session').invalidate();
},
},
});
Ember has 2500 addons
Usually you will always find an addon for every common problem
Here we use ember-simple-auth one of the most popular authentication addons for Ember
You can create an authenticator/authorizer for any API in no time
If you own the API, you can structure it in such a way to use an already implemented authenticator/authorizer
Here we use Devise authenticator
Implementing the user edit form
Back to our micro-twitter app...
But let's talk about Ember Data first that help us to sync data to our backend
It provides an ORM over the data, regadless the transfer medium that is used below (HTTP API, WebSockets, IndexDB etc)
//app/models/user.js
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default Model.extend({
name: attr('string'),
email: attr('string'),
password: attr('string'),
updatedAt: attr('date'),
createdAt: attr('date')
});
{
"data": {
"id": "1",
"type": "users",
"attributes": {
"name": "Example User",
"email": "example@embertutorial.org",
"created-at": "2016-05-26T02:57:38Z",
}
}
}
{
"user": {
"id": 1,
"name": "Example User",
"email": "example@railstutorial.org",
"created_at": "2016-05-26T02:57:38Z"
}
}
JSONAPI spec
regular JSON
Mapping to Ember Data
The serializer and adapter define the way EmberData will interact with the API
Since Ember 2.0, JSONAPI is the default
But regular (AMS) JSON is very popular as well
Creating your own serializer and adapter is easy which means any API can work with EmberData if you have right adapter/serializer
The serializer is responsible for the data(json) format
The adapter is responsible for the endpoints, headers and anything else not related the data itself
It provides an ORM over the data, regadless the transfer medium that is used below (HTTP API, WebSockets, IndexDB etc)
//app/models/user.js
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default Model.extend({
name: attr('string'),
email: attr('string'),
password: attr('string'),
updatedAt: attr('date'),
createdAt: attr('date')
});
var user = this.store.findRecord('user', 1); // => GET /users/1
var user = this.store.peekRecord('user', 1); // => no network request
var users = this.store.findAll('user'); // => GET /users
var users = this.store.peekAll('user'); // => no network request
// GET to /users?filter[name]=Peter
this.store.query('users', { filter: { name: 'Peter' } }).then(function(peters) {
// Do something with `peters`
});
//if you know that your filter is unique
//this picks up the only one resource from the collection (if there is any)
this.store.queryRecord('user', { filter: { email: 'tomster@example.com' } }).then(function(tomster) {
// do something with `tomster`
});
Querying records..
It provides an ORM over the data, regadless the transfer medium that is used below (HTTP API, WebSockets, IndexDB etc)
//app/models/user.js
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default Model.extend({
name: attr('string'),
email: attr('string'),
password: attr('string'),
updatedAt: attr('date'),
createdAt: attr('date')
});
var user = store.createRecord('user', {
name: 'Tom Dale',
email: 'tomdale@email.com',
password: '123123123'
});
user.save() //POST /users
store.findRecord('user', 1).then(function(user) {
user.get('name'); // => "Tom Dale"
user.set('updatedAt', new Date());
user.save(); // => PATCH to '/users/1'
});
store.findRecord('user', 2).then(function(user) {
user.destroyRecord(); // => DELETE to /users/2
});
Creating, Updating and Deleting
Keep in mind that Ember Data is separate from Ember, which means you can even build you own ORM library if you want and work with that in your Ember app!
Implementing the user edit form
Back to our micro-twitter app...
//app/models/user.js
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default Model.extend({
name: attr('string'),
email: attr('string'),
password: attr('string'),
updatedAt: attr('date'),
createdAt: attr('date'),
passwordConfirmation: null,
valid() {
if (!this.get('password')) { return true; }
this.get('errors')._clear();
if (this.get('password') === this.get('passwordConfirmation')) { return true;}
this.get('errors')._add(
'passwordConfirmation',
'is not the same with the password'
);
return false;
}
});
//app/routes/user.js
import Ember from 'ember';
export default Ember.Route.extend({
model(params) {
return this.store.findRecord('user', params.user_id);
}
});
//app/routes/user/edit.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.modelFor('user');
}
});
Remember our routes definition in app/router.js ?
Implementing the user edit form
Back to our micro-twitter app...
//app/templates/user/edit.hbs
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
{{#if model.errors}}
<div id="error_explanation">
<div class="alert alert-danger">
The form contains {{model.errors.length}} errors.
</div>
<ul>
{{#each model.errors as |error|}}
<li>{{error.attribute}} {{error.message}}</li>
{{/each}}
</ul>
</div>
{{/if}}
<form class="form-basic" role="form" {{action (route-action 'updateUser' model) on='submit'}}>
<label for="user_name">Name</label>
{{input class="form-control" type="text" value=model.name size="50"}}
<label for="user_email">Email</label>
{{input class="form-control" type="text" value=model.email size="50"}}
<label for="user_password">Password</label>
{{input class="form-control" type="password" value=model.password size="50"}}
<label for="user_password_confirmation">Confirmation</label>
{{input class="form-control" type="password" value=model.passwordConfirmation size="50"}}
<input type="submit" value="Save changes" class="btn btn-primary">
</form>
</div>
</div>
The template
Don't forget that this is rendered inside ApplicationTemplate
Implementing the user edit form
Back to our micro-twitter app...
//app/templates/user/edit.hbs
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
{{resource-errors errors=model.errors}}
<form class="form-basic" role="form" {{action (route-action 'updateUser' model) on='submit'}}>
<label for="user_name">Name</label>
{{input class="form-control" type="text" value=model.name size="50"}}
<label for="user_email">Email</label>
{{input class="form-control" type="text" value=model.email size="50"}}
<label for="user_password">Password</label>
{{input class="form-control" type="password" value=model.password size="50"}}
<label for="user_password_confirmation">Confirmation</label>
{{input class="form-control" type="password" value=model.passwordConfirmation size="50"}}
<input type="submit" value="Save changes" class="btn btn-primary">
</form>
</div>
</div>
Moving the errors in a component
//app/templates/components/resource-errors.hbs
{{#if model}}
<div id="error_explanation">
<div class="alert alert-danger">
The form contains {{model.length}} errors.
</div>
<ul>
{{#each model as |error|}}
<li>{{error.attribute}} {{error.message}}</li>
{{/each}}
</ul>
</div>
{{/if}}
Implementing the user edit form
Back to our micro-twitter app...
We need to format the text a bit
We have 3 options:
Implementing the user edit form
Back to our micro-twitter app...
//app/components/resource-errors.js
import Ember from 'ember';
export default Ember.Component.extend({
errorsText: Ember.computed('model.length', function() {
var inflector = new Ember.Inflector(Ember.Inflector.defaultRules);
var word = 'error';
if (this.get('model.length') > 1) {
return inflector.pluralize(word);
} else {
return inflector.singularize(word);
}
}),
});
Using the component itself
//app/templates/components/resource-errors.hbs
{{#if model}}
<div id="error_explanation">
<div class="alert alert-danger">
The form contains {{model.length}} {{errorsText}}.
</div>
<ul>
{{#each model as |error|}}
<li>{{error.attribute}} {{error.message}}</li>
{{/each}}
</ul>
</div>
{{/if}}
You can think components class like a small controller
To humanize the list of attributes is tricky though: use Ember Helpers instead
//app/helpers/pluralize.js
import Ember from 'ember';
export function pluralize([word], {amount}) {
var inflector = new Ember.Inflector(Ember.Inflector.defaultRules);
if (amount > 1) {
return inflector.pluralize(word);
} else {
return inflector.singularize(word);
}
}
export default Ember.Helper.helper(pluralize);
Helpers are most useful for transforming raw values from models and components into a format more appropriate for your users
//app/helpers/humanize.js
import Ember from 'ember';
export function humanize([word]) {
return word.decamelize().split('_').join(' ').capitalize();
}
export default Ember.Helper.helper(humanize);
Usually helpers are used when component is a bit too much
Helper names don't need to have a dash like components
Implementing the user edit form
Back to our micro-twitter app...
//app/templates/components/resource-errors.hbs
{{#if model}}
<div id="error_explanation">
<div class="alert alert-danger">
The form contains {{model.length}} {{pluralize 'error' amount=model.length}}.
</div>
<ul>
{{#each model as |error|}}
<li>{{humanize error.attribute}} {{error.message}}</li>
{{/each}}
</ul>
</div>
{{/if}}
The resource-errors template with the helpers
//app/templates/user/edit.hbs
<h1>Update your profile</h1>
<div class="row">
<div class="col-md-6 col-md-offset-3">
{{resource-errors errors=model.errors}}
<form class="form-basic" role="form" {{action (route-action 'updateUser' model) on='submit'}}>
<label for="user_name">Name</label>
{{input class="form-control" type="text" value=model.name size="50"}}
<label for="user_email">Email</label>
{{input class="form-control" type="text" value=model.email size="50"}}
<label for="user_password">Password</label>
{{input class="form-control" type="password" value=model.password size="50"}}
<label for="user_password_confirmation">Confirmation</label>
{{input class="form-control" type="password" value=model.passwordConfirmation size="50"}}
<input type="submit" value="Save changes" class="btn btn-primary">
</form>
</div>
</div>
Implementing the user edit form
Back to our micro-twitter app...
Much nicer :)
Implementing the user edit form
Back to our micro-twitter app...
//app/routes/user.js
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return this.modelFor('user');
},
actions: {
updateUser(user) {
var _this = this;
if (!user.valid()) { return false; }
user.save().then(function() { //PUT /api/v1/users/:id
user.setProperties({
'password': null,
'passwordConfirmation': null
});
//notify the user that the profile has been saved
});
}
}
});
The action handler
You can define the action in any parent route in the route hierarchy
Action will bubble until one of them handles it
Implementing the index page
Back to our micro-twitter app...
The models we need:
Built in JSONAPI
GET | /api/v1/session |
POST | /api/v1/session |
GET | /api/v1/users |
POST | /api/v1/users |
GET | /api/v1/users/:id |
PUT | /api/v1/users/:id |
DELETE | /api/v1/users/:id |
GET | /api/v1/microposts |
GET (feed) | /api/v1/microposts?feed_for_user_id=:id |
POST | /api/v1/microposts |
GET | /api/v1/microposts/:id |
PUT | /api/v1/microposts/:id |
DELETE | /api/v1/microposts/:id |
GET | /api/v1/users/:user_id/followers |
POST | /api/v1/users/:user_id/followers/:id |
DELETE | /api/v1/users/:user_id/followers/:id |
GET | /api/v1/users/:user_id/followings |
POST | /api/v1/users/:user_id/followings/:id |
DELETE | /api/v1/users/:user_id/followings/:id |
Restructuring our models
Back to our micro-twitter app...
//app/models/person.js
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export default Model.extend({
email: attr('string'),
name: attr('string'),
password: attr('string'),
created_at: attr('moment'),
updated_at: attr('moment'),
size: Ember.computed('meta', function() {
return this.get('meta.total-count');
})
/*
//old style
size: function() {
return this.get('meta.total-count');
}).property('meta')
*/
});
//app/models/user.js
import Person from './person';
export default Person.extend({
passwordConfirmation: null,
valid() {
if (!this.get('password')) { return true; }
this.get('errors')._clear();
if (this.get('password') === this.get('passwordConfirmation')) { return true;}
this.get('errors')._add(
'passwordConfirmation',
'is not the same with the password'
);
return false;
}
});
//app/models/follower.js
import Person from './person';
export default Person.extend({
});
//app/models/following.js
import Person from './person';
export default Person.extend({
});
Implementing the index page
Back to our micro-twitter app...
import Ember from 'ember';
export default Ember.Route.extend({
model() {
if (this.get('session.isAuthenticated')) {
return Ember.RSVP.hash({
user: this.store.findRecord(
'user', this.get('session.data.authenticated.id')
),
userFollowing: this.store.query('following', {
user_id: this.get('session.data.authenticated.id'),
per_page: 1 //optimization, we only need the meta info
}),
userFollowers: this.store.query('follower', {
user_id: this.get('session.data.authenticated.id'),
per_page: 1 //optimization, we only need the meta info
}),
microposts: this.store.query('feed', {
feed_for_user_id: this.get('session.data.authenticated.id')
}),
micropost: this.store.createRecord('micropost', {})
});
}
}
});
Ember.RSVP.hash doesn't resolve until all promises have been resolved
Implementing the index page
Back to our micro-twitter app...
{{#if session.isAuthenticated}}
<div class="row">
<aside class="col-md-4">
{{user-profile user=model.user followingsCount=model.followings.size followersCount=model.followers.size}}
{{new-micropost model=model.micropost createMicropost="createMicropost"}}
</aside>
<div class="col-md-8">
{{microposts-index title="Micropost Feed" model=model.microposts}}
</div>
</div>
{{else}}
<div class="center jumbotron">
<h1>Welcome to the Sample App</h1>
<h2>
This is the home page for the
<a href="https://github.com/vasilakisfil/rails_tutorial_ember">Ember on Rails Tutorial</a>
sample application.
</h2>
{{link-to "Sign up now!" 'users.new' class="btn btn-lg btn-primary"}}
</div>
<a href="http://emberjs.com/">
<img src="https://pbs.twimg.com/profile_images/2326095089/3s1seyc0csl75btyw1vl.png" alt="Ember logo" class="ember-logo">
</a>
{{/if}}
It's a good practice to break down our template in a number of components
Implementing the user-profile component
Back to our micro-twitter app...
//app/templates/components/user-profile.hbs
<section class="user_info">
<h1>
<!-- an ember addon for gravatar -->
{{gravatar-image email=model.user.email alt=model.user.name size=80 class='gravatar'}}
{{user.name}}
</h1>
</section>
<section class="stats">
<div class="stats">
{{#link-to 'user.following' model.user}}
<strong id="following" class="stat">
{{followingsCount}}
</strong>
following
{{/link-to}}
{{#link-to 'user.followers' model.user}}
<strong id="followers" class="stat">
{{followersCount}}
</strong>
followers
{{/link-to}}
</div>
</section>
Implementing the microposts-index component
Back to our micro-twitter app...
//app/templates/components/microposts-index.hbs
{{#if model.length}}
<h3>{{title}}</h3>
<ol class="microposts">
{{#each model as |micropost|}}
<li class={{micropost.id}}>
{{gravatar-image email=micropost.user.email alt=micropost.user.name size=50 class='gravatar'}}
<span class="user">{{link-to micropost.user.name 'user.index' micropost.user.id}}</span>
<span class="content">
{{micropost.content}}
{{#if micropost.picture}}
<img src={{micropost.pictureUrl}}>
{{/if}}
</span>
<span class="timestamp">
<!-- Ember addon that reports the datetime in "3 minutes ago"-style -->
Posted about {{moment-from-now micropost.createdAt interval=10000}}.
</span>
</li>
{{/each}}
</ol>
<!-- Ember addon for pagination -->
{{page-numbers content=model numPagesToShow=4}}
{{/if}}
Implementing the new-micropost component
Back to our micro-twitter app...
//app/templates/components/new-micropost.hbs
<section class="micropost_form">
<form class="form-basic" role="form" {{action (route-action 'createMicropost' model) on='submit'}}>
<div class="field">
{{textarea cols="80" value=model.content rows="4" placeholder="Compose new micropost..."}}
</div>
<input type="submit" class="btn btn-primary">
</form>
</section>
Implementing the index page
Back to our micro-twitter app...
{{#if session.isAuthenticated}}
<div class="row">
<aside class="col-md-4">
{{user-profile user=model.user followingsCount=model.followings.size followersCount=model.followers.size}}
{{new-micropost model=model.micropost createMicropost="createMicropost"}}
</aside>
<div class="col-md-8">
{{microposts-index title="Micropost Feed" model=model.microposts}}
</div>
</div>
{{else}}
<div class="center jumbotron">
<h1>Welcome to the Sample App</h1>
<h2>
This is the home page for the
<a href="https://github.com/vasilakisfil/rails_tutorial_ember">Ember on Rails Tutorial</a>
sample application.
</h2>
{{link-to "Sign up now!" 'users.new' class="btn btn-lg btn-primary"}}
</div>
<a href="http://emberjs.com/">
<img src="https://pbs.twimg.com/profile_images/2326095089/3s1seyc0csl75btyw1vl.png" alt="Ember logo" class="ember-logo">
</a>
{{/if}}
It's a good practice to break down our template in a number of components
Implementing the index page
Back to our micro-twitter app...
import Ember from 'ember';
export default Ember.Route.extend({
model() {
if (this.get('session.isAuthenticated')) {
return Ember.RSVP.hash({
user: this.store.findRecord(
'user', this.get('session.data.authenticated.id')
),
userFollowing: this.store.query('following', {
user_id: this.get('session.data.authenticated.id'),
per_page: 1 //optimization, we only need the meta info
}),
userFollowers: this.store.query('follower', {
user_id: this.get('session.data.authenticated.id'),
per_page: 1 //optimization, we only need the meta info
}),
microposts: this.store.query('feed', {
feed_for_user_id: this.get('session.data.authenticated.id')
}),
micropost: this.store.createRecord('micropost', {})
});
}
},
actions: {
createMicropost(micropost) {
var _this = this;
//this.model() will run the model hook again!
micropost.set('user', this.modelFor('index').user);
micropost.save().then(function(/*micropost*/) {
_this.refresh();
});
}
}
});
We make the final touches to the model before saving it to the server
Once the HTTP request has finished (server has responded) we reload the model so that we see the new micropost in the feed
Implementing the index page
Back to our micro-twitter app...
import Ember from 'ember';
export default Ember.Route.extend({
model() {
if (this.get('session.isAuthenticated')) {
return Ember.RSVP.hash({
user: this.store.findRecord(
'user', this.get('session.data.authenticated.id')
),
userFollowing: this.store.query('following', {
user_id: this.get('session.data.authenticated.id'),
per_page: 1 //optimization, we only need the meta info
}),
userFollowers: this.store.query('follower', {
user_id: this.get('session.data.authenticated.id'),
per_page: 1 //optimization, we only need the meta info
}),
microposts: this.store.query('feed', {
feed_for_user_id: this.get('session.data.authenticated.id')
}),
micropost: this.store.createRecord('micropost', {})
});
}
},
actions: {
createMicropost(micropost) {
var _this = this;
//this.model() will run the model hook again!
micropost.set('user', this.modelFor('index').user);
micropost.save().then(function(micropost) {
Ember.set(
this.modelFor('index'),
'micropost',
this.store.createRecord('micropost', {})
);
_this.modelFor('index').microposts.unshiftObject(micropost._internalModel);
});
}
}
});
Instead of running the model hook again, we can just push the new object in the feed array
Implementing the index page
Back to our micro-twitter app...
I could go on but I am pretty sure you are already tired of me :)
Summary
Get started now!
(slides contain sometimes slightly different code from the repo for the sake of simplicity)
If you have any question or need help ping me on twitter (https://twitter.com/vasilakisfil)
or comment in the talk page so that other users see it as well :)
Questions ?
Thanks!