I'm working on ember.js tutorial now. I got a problem on "adding child routes" chapter. My todos list is not displayed. The "todos" template outputing just fine but "todos/index" template doesn't work at all. The console does not show any errors. I guess that this is some local problem or some bug. Maybe someone has already met with a similar problem. What could be the reason of this issue? How can i solve it?
Thanks.
HTML:
<html>
<head>
<meta charset="utf-8">
<title>Ember.js • TodoMVC</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<script type="text/x-handlebars" data-template-name="todos/index">
<ul id="todo-list">
{{#each itemController="todo"}}
<li {{bind-attr class="isCompleted:completed isEditing:editing"}}>
{{#if isEditing}}
{{edit-todo class="edit" value=title focus-out="acceptChanges" insert-newline="acceptChanges"}}
{{else}}
{{input type="checkbox" checked=isCompleted class="toggle"}}
<label {{action "editTodo" on="doubleClick"}}>{{title}}</label><button {{action "removeTodo"}} class="destroy"></button>
{{/if}}
</li>
{{/each}}
</ul>
</script>
<script type="text/x-handlebars" data-template-name="todos">
<section id="todoapp">
<header id="header">
<h1>todos</h1>
{{input type="text" id="new-todo" placeholder="What needs to be done?"
value=newTitle action="createTodo"}}
</header>
<section id="main">
{{outlet}}
<input type="checkbox" id="toggle-all">
</section>
<footer id="footer">
<span id="todo-count">
<strong>{{remaining}}</strong> {{inflection}} left</span>
<ul id="filters">
<li>
All
</li>
<li>
Active
</li>
<li>
Completed
</li>
</ul>
<button id="clear-completed">
Clear completed (1)
</button>
</footer>
</section>
<footer id="info">
<p>Double-click to edit a todo</p>
</footer>
</script>
<script src="js/libs/jquery-1.10.2.js"></script>
<script src="js/libs/handlebars-1.1.2.js"></script>
<script src="js/libs/ember-1.2.0.js"></script>
<script src="js/libs/ember-data.js"></script>
<script src="js/app.js"></script>
<script src="js/route.js"></script>
<script src="js/models/todo.js"></script>
<script src="js/controllers/todo_controller.js"></script>
<script src="js/controllers/todos_controller.js"></script>
<script src="js/views/edit_todo_view.js"></script>
</body>
</html>
JS:
window.Todos = Ember.Application.create();
Todos.ApplicationAdapter = DS.FixtureAdapter.extend();
Todos.Router.reopen({
rootURL: '/one/'
});
Todos.Router.map(function () {
this.resource('todos', { path: '/' });
});
Todos.TodosRoute = Ember.Route.extend({
model: function () {
return this.store.find('todo');
}
});
Todos.TodosIndexRoute = Ember.Route.extend({
model: function () {
return this.modelFor('todos');
}
});
Todos.Todo = DS.Model.extend({
title:DS.attr('string'),
isCompleted:DS.attr('boolean')
});
Todos.Todo.FIXTURES = [
{
id: 1,
title: 'Learn Ember.js',
isCompleted: true
},
{
id: 2,
title: '...',
isCompleted: false
},
{
id: 3,
title: 'Profit!',
isCompleted: false
}
];
Todos.TodosController = Ember.ArrayController.extend({
actions: {
createTodo: function () {
// Get the todo title set by the "New Todo" text field
var title = this.get('newTitle');
if (!title.trim()) { return; }
// Create the new Todo model
var todo = this.store.createRecord('todo', {
title: title,
isCompleted: false
});
// Clear the "New Todo" text field
this.set('newTitle', '');
// Save the new model
todo.save();
}
},
remaining: function () {
return this.filterBy('isCompleted', false).get('length');
}.property('#each.isCompleted'),
inflection: function () {
var remaining = this.get('remaining');
return remaining === 1 ? 'item' : 'items';
}.property('remaining'),
});
Todos.TodoController = Ember.ObjectController.extend({
actions:{
editTodo: function () {
this.set('isEditing', true);
},
acceptChanges: function () {
this.set('isEditing', false);
if (Ember.isEmpty(this.get('model.title'))) {
this.send('removeTodo');
} else {
this.get('model').save();
}
},
removeTodo: function () {
var todo = this.get('model');
todo.deleteRecord();
todo.save();
},
},
isEditing: false,
isCompleted: function(key, value){
var model = this.get('model');
if (value === undefined) {
// property being used as a getter
return model.get('isCompleted');
} else {
// property being used as a setter
model.set('isCompleted', value);
model.save();
return value;
}
}.property('model.isCompleted')
});
Technically this isn't a bug, the team figured there is no point in having an index route if you can't go any deeper in the router (this is due to the fact that the todos route and template will render, so there is no real need for the index route. That being said, if you really want it, add an anonymous function and you'll get it.
Todos.Router.map(function () {
this.resource('todos', { path: '/' },function () {});
});
https://github.com/emberjs/ember.js/issues/3995
I had this problem too. After much perplexity, the solution was to use a version of index.html that actually had the <script> wrapped <ul>, instead of the one where I was moving that block around to see if it made a difference and ended up having it nowhere.
Related
I need to render my template templates/page.html:
<section class="section">
<div class="container">
<h1 class="title"><%= title %></h1>
<div><%= content %></div>
<div><%= pages %></div>
</div>
</section>
into my div element with page-content id:
// Underscore mixins
_.mixin({templateFromUrl: function (url, data, settings) {
var templateHtml = "";
this.cache = this.cache || {};
if (this.cache[url]) {
templateHtml = this.cache[url];
} else {
$.ajax({
url: url,
method: "GET",
async: false,
success: function(data) {
templateHtml = data;
}
});
this.cache[url] = templateHtml;
}
return _.template(templateHtml, data, settings);
}});
// Models
var PageState = Backbone.Model.extend({
defaults: {
title: "",
subtitle: "",
content: "",
pages: "",
state: "games"
}
});
var pageState = new PageState();
// Routers
var PageRouter = Backbone.Router.extend({
routes: {
"": "games",
"!/": "games",
"!/games": "games",
"!/users": "users",
"!/add-user": "add_user"
},
games: function () {
select_item($("#games"));
console.log("games");
pageState.set({ state: "games" });
},
users: function () {
select_item($("#users"));
console.log("users");
pageState.set({ state: "users" });
},
add_user: function () {
console.log("add_user");
}
});
var pageRouter = new PageRouter();
// Views
var Page = Backbone.View.extend({
templates: {
"page": _.templateFromUrl("/templates/page.html")
},
initialize: function () {
this.render();
},
render: function () {
var template = this.templates["page"](this.model.toJSON());
this.$el.html(template);
return this;
}
});
$(function () {
var page = new Page({
el: '#page-content',
model: pageState
});
});
// Run backbone.js
Backbone.history.start();
// Close mobile & tablet menu on item click
$('.navbar-item').each(function(e) {
$(this).click(function() {
if($('#navbar-burger-id').hasClass('is-active')) {
$('#navbar-burger-id').removeClass('is-active');
$('#navbar-menu-id').removeClass('is-active');
}
});
});
// Open or Close mobile & tablet menu
$('#navbar-burger-id').click(function () {
if($('#navbar-burger-id').hasClass('is-active')) {
$('#navbar-burger-id').removeClass('is-active');
$('#navbar-menu-id').removeClass('is-active');
} else {
$('#navbar-burger-id').addClass('is-active');
$('#navbar-menu-id').addClass('is-active');
}
});
// Highlights an item in the main menu
function select_item(sender) {
$("a.v-menu-item").removeClass("is-active");
sender.addClass("is-active");
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.3.3/backbone-min.js"></script>
<body>
<section class="hero is-primary is-medium">
<div class="hero-head">
<nav id="navMenu" class="navbar">
<div class="container">
<div class="navbar-brand">
<a class="navbar-item is-size-2">Virto</a>
<span id="navbar-burger-id" class="navbar-burger burger" data-target="navbar-menu-id">
<span></span>
<span></span>
<span></span>
</span>
</div>
<div id="navbar-menu-id" class="navbar-menu">
<div class="navbar-end">
<a id="games" href="#!/games" class="navbar-item v-menu-item is-active">Games</a>
<a id="users" href="#!/users" class="navbar-item v-menu-item">Users</a>
<span class="navbar-item">
<a href="#!/add-user" class="button is-primary is-inverted">
<span class="icon">
<i class="fas fa-user-circle"></i>
</span>
<span>Add user</span>
</a>
</span>
<div>
</div>
</div>
</nav>
</div>
</section>
<div id="page-content"></div>
</body>
But my content doesn't load... I'm a noobie in backbone.js still so can someone explain what do I doing wrong and how to fix?
ADDED
I have checked my script in debugger. It loads a template and finds my div in the render() function but I can't see any result in browser after, my div is empty still.
I created a plunker with your code, and it works fine for me: link
I think you only forgot to initialize your model:
// Models
var PageState = Backbone.Model.extend({
defaults: {
title: "Title - 1",
subtitle: "Subtitle - 1",
content: "Content",
pages: "1",
state: "games"
}
});
I can't seem to get the selected item from an input checkbox.
<ul>
<li ng-repeat='shoe in shoebox'>
<input type='checkbox' ng-model='shoe.selected' id='shoe-{{shoe.name}}'>
<label for='shoe-{{shoe.name}}'>{{shoe.name}}</label>
</li>
<button ng-click='whatIsChecked(shoe.selected)'>Apply</button>
</ul>
Then in my controller:
$scope.whatIsChecked = function(selectedThing) {
console.log(selectedThing);
};
The above returns undefined.
The list items are displayed correctly, but the shoe.name or checked item doesn't seem to be stored by the ng-model.
the variable shoe is only valid in ng-repeat block.
If you want to got what is selected, you should try filter.
UPD:
Since you don't have the selected property, you should keep the selected items somewhere else by firing the ng-click event.
refer below code snippet.
angular.module("app", [])
.controller("myCtrl", function($scope) {
$scope.checkedShoes = [];
$scope.shoebox = [{
name: 'shoe1'
},
{
name: 'shoe2'
},
{
name: 'shoe3'
},
{
name: 'shoe4'
}
];
$scope.save = function(e, shoe) {
if (e.target.checked) {
$scope.checkedShoes.push(shoe);
} else {
var index = $scope.checkedShoes.indexOf(shoe);
if( index > -1) {
$scope.checkedShoes.splice(index, 1);
}
}
};
$scope.whatIsChecked = function() {
//var selected = $scope.shoebox.filter(function(item) {
// return item.selected === true;
//});
console.log($scope.checkedShoes);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
<div ng-app="app" , ng-controller="myCtrl">
<ul>
<li ng-repeat='shoe in shoebox'>
<input type='checkbox' ng-click="save($event, shoe)" id='shoe-{{shoe.name}}'>
<label for='shoe-{{shoe.name}}'>{{shoe.name}}</label>
</li>
<button ng-click='whatIsChecked()'>Apply</button>
</ul>
</div>
You can get the checked items using a angular.Foreach it becomes easy when you have multiple items checked.
$scope.checkedItems = [];
angular.forEach($scope.shoebox, function(value, key) {
if (value.selected)
$scope.checkedItems.push(value.name);
});
Demo
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope) {
$scope.shoebox = [{
name: 'Reboke',
selected: false
}, {
name: 'woodlands',
selected: false
}, {
name: 'Nike',
selected: false
}, {
name: 'Fila',
selected: false
}];
$scope.checkedItems = function() {
$scope.checkedItems = [];
angular.forEach($scope.shoebox, function(value, key) {
if (value.selected)
$scope.checkedItems.push(value.name);
});
}
});
<html>
<head>
<meta charset="utf-8" />
<title>AngularJS </title>
<link rel="stylesheet" href="style.css" />
<script src="https://code.angularjs.org/1.4.7/angular.js"></script>
</head>
<body ng-app="myApp" ng-controller="myCtrl">
<ul>
<li ng-repeat='shoe in shoebox'>
<input type='checkbox' ng-model='shoe.selected' id='shoe-{{shoe.name}}'>
<label for='shoe-{{shoe.name}}'>{{shoe.name}}</label>
</li>
<button ng-click='checkedItems()'>Apply</button>
Checked items are: {{checkedItems}}
</ul>
</body>
</html>
I am trying to convert the Meteor tutorial app, simple-todo, into a forum. I have a list that populates with Topics with a delete button and a radio button.
What I want to do, is when a Topic's radio button is selected, display all the comments associated with it. I am trying to accomplish this by putting the topicId(originally taskId), used in the tutorial, in a new Collection called Comments. I then query the Comments for that topicId.the comments are mostly copied and pasted topics, so the attributes and functionality of the two is mostly the same.
I, right now, don't know how to get to the topicId from my Template.body.events. If you can help me tie these two DB's together, that would be very helpful.
HTML:
<head>
<title>Forum</title>
</head>
<body>
<div class="login">
{{> loginButtons}}
</div>
<nav class="mainMenu">
<ul>
<li>Home</li>
<li>Forum</li>
<li>About Us</li>
</ul>
</nav>
<div class="container">
<header>
<h1>Forum</h1>
<h2>Post a topic</h2>
{{#if currentUser}}
<form class="new-topic">
<input type="topicalContent" name="topicalContent" placeholder="Type to add new topics" />
</form>
{{/if}}
</header>
<table>
<td class="topicsCol">
<h3>Topics</h3>
<ul class="topicsList">
{{#each topics}}
{{> topic}}
{{/each}}
</ul>
</td>
<td class="commentsCol">
<h3>Comments</h3>
<ul class="commentsList">
{{#each comments}}
{{> comment}}
{{/each}}
</ul>
<input type="commentContent" name="commentContent" placeholder="Type to Comment" />
</td>
</table>
</div>
</body>
<template name="topic">
<li class="{{#if selected}}select{{/if}}">
<button class="delete">×</button>
<span class="topicalContent">{{topicalContent}} -<strong>{{username}}</strong></span>
<input type="radio" name="curTopic" value="{{curTopic}}" />
</li>
</template>
<template name="commentsList">
<li class="comment">
<button class="delete">×</button>
<span class="responseContent">
<strong>
<!-- {{#if username === owner}}
<style="color:red;">OP
{{else}}-->
{{username}}
<!--{{/if}}-->
</strong>: {{responseContent}}</span>
</li>
</template>
JavaScript:
Topics = new Mongo.Collection("topics");
Comments = new Mongo.Collection("comments");
if(Meteor.isServer){
Meteor.publish("topics", function(){
return Topics.find({
$or: [
{ owner: this.userId }
]
});
});
Meteor.publish("comments", function(){
return Comments.find({
$or: [
{ parent: topicId },
{ owner: this.userId }
]
});
});
}
if (Meteor.isClient) {
// This code only runs on the client
Meteor.subscribe("topics");
Meteor.subscribe("comments");
Template.body.helpers({
topics: function() {
return Topics.find({}, {sort: {createdAt: -1}});
},
comments: function () {
return Comments.find({parent: {parent: topicId}}, {sort: {createdAt: -1}});
}
});
Template.body.events({
"submit .new-topic": function (event) {
//Prevent default browser form submit
event.preventDefault();
//Get value from form element
var topicalContent = event.target.topicalContent.value;
if (topicalContent == "") {
throw new Meteor.Error("Empty Input");
}
//Insert a topic into the collection
Meteor.call("addTopic", topicalContent);
//Clear form
event.target.topicalContent.value = "";
},
"submit .commentContent": function (event) {
event.preventDefault();
var commentContent = event.target.commentContent.value;
Meteor.call("addComment", this.topicId);
event.target.commentContent.value= "";
}
});
Template.topic.helpers({
isOwner: function(){
return this.owner === Meteor.userId();
}
});
Template.topic.events({
"click .curTopic": function() {
//Show Comments of selected radio button
Meteor.call("showComments", this._id);
},
"click .delete":function () {
Meteor.call("deleteTopic", this._id);
}
});
Template.comment.helpers({
isOwner: function(){
return this.owner === Meteor.userId();
}
});
Template.comment.events({
"click .delete":function () {
Meteor.call("deleteComment", this._id);
}
});
Accounts.ui.config({
passwordSignupFields: "USERNAME_ONLY"
});
}
Meteor.methods({
addTopic:function(topicalContent){
if (!Meteor.userId()) {
throw new Meteor.Error("not-authorized");
}
Topics.insert({
topicalContent,
createdAt: new Date(),
owner: Meteor.userId(),
username: Meteor.user().username
});
},
deleteTopic:function(topicId) {
var topic = Topics.findOne(topicId);
if (topic.owner !== Meteor.userId()) {
throw new Meteor.Error("not-authorized");
}
else {
Topics.remove(topicId);
}
},
showComments:function (topicId) {
Comments.find({"parent":topicId});
},
addComment:function(commentContent, topicId){
if (!Meteor.userId()) {
throw new Meteor.Error("not-authorized");
}
Comments.insert({
commentContent,
createdAt: new Date(),
owner: Meteor.userId(),
username: Meteor.user().username,
parent: topicId
});
},
deleteComment:function(commentId) {
var comment = Comments.findOne(commentId);
if (comment.owner !== Meteor.userId()) {
throw new Meteor.Error("not-authorized");
}
else {
Comments.remove(commentId);
}
}
});
One way (and there are many) is to use a session variable to track the current topic. In your topic event handler:
Template.topic.events({
"click .curTopic": function() {
Session.set('topicId',this._id); // set the Session variable
Meteor.call("showComments", this._id);
});
Now everywhere else you're looking for the current topicId you can just use Session.get('topicId');
How to use hogan templates with backbone code?How to combine the values of a model into the Hogan templates?
My HTML page:
<!doctype html>
<html lang="en">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>Backbone.js • TodoMVC</title>
<link rel="stylesheet" href="js/assets/base.css">
</head>
<body>
<section id="todoapp">
<header id="header">
<h1>todos</h1>
<input id="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<section id="main">
<input id="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list"></ul>
</section>
<footer id="footer"></footer>
</section>
<div id="info">
<p>Double-click to edit a todo</p>
<p>Written by Addy Osmani</p>
<p>Part of TodoMVC</p>
</div>
<script type="text/template" id="item-template">
<div class="view">
<input class="toggle" type="checkbox" {{ completed ? 'checked' : '' }}>
<label>{{ title }}</label>
<button class="destroy"></button>
</div>
<input class="edit" value="{{ title }}">
</script>
<script type="text/template" id="stats-template">
<span id="todo-count"><strong>{{ remaining }}</strong> {{ remaining === 1 ? 'item' : 'items' }} left</span>
<ul id="filters">
<li>
<a class="selected" href="#/">All</a>
</li>
<li>
Active
</li>
<li>
Completed
</li>
</ul>
{[ if (completed) { ]}
<button id="clear-completed">Clear completed ({{ completed }})</button>
{[ } ]}
</script>
<script src="js/assets/base.js"></script>
<script src="js/lib/jquery.js"></script>
<script src="js/lib/underscore.js"></script>
<script src="js/lib/backbone.js"></script>
<script src="js/lib/backbone.localStorage.js"></script>
<script src="js/models/todo.js"></script>
<script src="js/collections/todos.js"></script>
<script src="js/views/todos.js"></script>
<script src="js/views/app.js"></script>
<script src="js/routers/router.js"></script>
<script src="js/app.js"></script>
</body>
</html>
My js code
/*global Backbone, jQuery, _, ENTER_KEY */
var app = app || {};
(function ($) {
'use strict';
_.templateSettings = {
evaluate : /\{\[([\s\S]+?)\]\}/g,
interpolate : /\{\{([\s\S]+?)\}\}/g
};
// The Application
// ---------------
// Our overall **AppView** is the top-level piece of UI.
app.AppView = Backbone.View.extend({
// Instead of generating a new element, bind to the existing skeleton of
// the App already present in the HTML.
el: '#todoapp',
// Our template for the line of statistics at the bottom of the app.
statsTemplate: _.template($('#stats-template').html()),
// Delegated events for creating new items, and clearing completed ones.
events: {
'keypress #new-todo': 'createOnEnter',
'click #clear-completed': 'clearCompleted',
'click #toggle-all': 'toggleAllComplete'
},
// At initialization we bind to the relevant events on the `Todos`
// collection, when items are added or changed. Kick things off by
// loading any preexisting todos that might be saved in *localStorage*.
initialize: function () {
this.allCheckbox = this.$('#toggle-all')[0];
this.$input = this.$('#new-todo');
this.$footer = this.$('#footer');
this.$main = this.$('#main');
this.$list = $('#todo-list');
this.listenTo(app.todos, 'add', this.addOne);
this.listenTo(app.todos, 'reset', this.addAll);
this.listenTo(app.todos, 'change:completed', this.filterOne);
this.listenTo(app.todos, 'filter', this.filterAll);
this.listenTo(app.todos, 'all', this.render);
// Suppresses 'add' events with {reset: true} and prevents the app view
// from being re-rendered for every model. Only renders when the 'reset'
// event is triggered at the end of the fetch.
app.todos.fetch({reset: true});
},
// Re-rendering the App just means refreshing the statistics -- the rest
// of the app doesn't change.
render: function () {
var completed = app.todos.completed().length;
var remaining = app.todos.remaining().length;
if (app.todos.length) {
this.$main.show();
this.$footer.show();
this.$footer.html(this.statsTemplate({
completed: completed,
remaining: remaining
}));
this.$('#filters li a')
.removeClass('selected')
.filter('[href="#/' + (app.TodoFilter || '') + '"]')
.addClass('selected');
} else {
this.$main.hide();
this.$footer.hide();
}
this.allCheckbox.checked = !remaining;
},
// Add a single todo item to the list by creating a view for it, and
// appending its element to the `<ul>`.
addOne: function (todo) {
var view = new app.TodoView({ model: todo });
this.$list.append(view.render().el);
},
// Add all items in the **Todos** collection at once.
addAll: function () {
this.$list.html('');
app.todos.each(this.addOne, this);
},
filterOne: function (todo) {
todo.trigger('visible');
},
filterAll: function () {
app.todos.each(this.filterOne, this);
},
// Generate the attributes for a new Todo item.
newAttributes: function () {
return {
title: this.$input.val().trim(),
order: app.todos.nextOrder(),
completed: false
};
},
// If you hit return in the main input field, create new **Todo** model,
// persisting it to *localStorage*.
createOnEnter: function (e) {
if (e.which !== ENTER_KEY || !this.$input.val().trim()) {
return;
}
app.todos.create(this.newAttributes());
this.$input.val('');
},
// Clear all completed todo items, destroying their models.
clearCompleted: function () {
_.invoke(app.todos.completed(), 'destroy');
return false;
},
toggleAllComplete: function () {
var completed = this.allCheckbox.checked;
app.todos.each(function (todo) {
todo.save({
'completed': completed
});
});
}
});
})(jQuery);
it is from the ToDo example. I used Mustache templates here by changing the template settings. But i want to convert the template into Hogan.
my Edited js:
/*global Backbone, jQuery, _, ENTER_KEY, ESC_KEY */
var app = app || {};
(function ($) {
'use strict';
// Todo Item View
// --------------
_.templateSettings = {
evaluate : /\{\[([\s\S]+?)\]\}/g,
interpolate : /\{\{([\s\S]+?)\}\}/g
};
// The DOM element for a todo item...
app.TodoView = Backbone.View.extend({
tagName: 'li',
// Cache the template function for a single item.
template1: _.template($('#item-template').html()),
template:Hogan.compile(template1),
// The DOM events specific to an item.
events: {
'click .toggle': 'toggleCompleted',
'dblclick label': 'edit',
'click .destroy': 'clear',
'keypress .edit': 'updateOnEnter',
'keydown .edit': 'revertOnEscape',
'blur .edit': 'close'
},
initialize: function () {
this.listenTo(this.model, 'change', this.render);
this.listenTo(this.model, 'destroy', this.remove);
this.listenTo(this.model, 'visible', this.toggleVisible);
},
render: function () {
// Backbone LocalStorage is adding `id` attribute instantly after creating a model.
// This causes our TodoView to render twice. Once after creating a model and once on `id` change.
// We want to filter out the second redundant render, which is caused by this `id` change.
// It's known Backbone LocalStorage bug, therefore we've to create a workaround.
// https://github.com/tastejs/todomvc/issues/469
if (this.model.changed.id !== undefined) {
return;
}
console.log(this.template);
this.$el.html(this.template(this.model.toJSON()));
this.$el.toggleClass('completed', this.model.get('completed'));
this.toggleVisible();
this.$input = this.$('.edit');
return this;
},
toggleVisible: function () {
this.$el.toggleClass('hidden', this.isHidden());
},
isHidden: function () {
var isCompleted = this.model.get('completed');
return (// hidden cases only
(!isCompleted && app.TodoFilter === 'completed') ||
(isCompleted && app.TodoFilter === 'active')
);
},
toggleCompleted: function () {
this.model.toggle();
},
edit: function () {
this.$el.addClass('editing');
this.$input.focus();
},
close: function () {
var value = this.$input.val();
var trimmedValue = value.trim();
if (!this.$el.hasClass('editing')) {
return;
}
if (trimmedValue) {
this.model.save({ title: trimmedValue });
if (value !== trimmedValue) {
this.model.trigger('change');
}
} else {
this.clear();
}
this.$el.removeClass('editing');
},
updateOnEnter: function (e) {
if (e.which === ENTER_KEY) {
this.close();
}
},
revertOnEscape: function (e) {
if (e.which === ESC_KEY) {
this.$el.removeClass('editing');
}
},
clear: function () {
this.model.destroy();
}
});
})(jQuery);
I'm new to EmberJs and was following this post from Adam Hawkins. When I tried to run this in a browser it seems to work but not as expected. when I click a dj in the navigation bar (data-template-name="djs") the browser navigates to the detail of the choosen dj and shows me all his albums. e.g. embertest/index.html#/djs/djs/armin-van-buuren
But if I paste the url (embertest/index.html#/djs/djs/armin-van-buuren) directly in the browser without clicking a dj first in the navigation list I get the message "No Albums" from the handlebars template "djs/dj"
I would expect the same behavior in both scenario's. What am I missing here?
For completeness you can find my ember application and handlebar templates below.
app.js
var App = Ember.Application.create(
{ LOG_TRANSITIONS: true,
LOG_BINDINGS: true,
LOG_VIEW_LOOKUPS: true,
LOG_STACKTRACE_ON_DEPRECATION: true,
LOG_VERSION: true,
debugMode: true
});
window.App = App;
App.DJS = [
{
name: 'Armin van Buuren',
albums: [
{
name: 'A State of Trance 2006',
cover: 'http://upload.wikimedia.org/wikipedia/en/thumb/8/87/ASOT_2006_cover.jpg/220px-ASOT_2006_cover.jpg'
},
{
name: '76',
cover: 'http://upload.wikimedia.org/wikipedia/en/thumb/8/8a/Armin_van_Buuren-76.jpg/220px-Armin_van_Buuren-76.jpg'
},
{
name: 'Shivers',
cover: 'http://upload.wikimedia.org/wikipedia/en/thumb/a/a1/ArminvanBuuren_Shivers.png/220px-ArminvanBuuren_Shivers.png'
}
]
},
{
name: 'Markus Schulz',
albums: [
{
name: 'Without You Near',
cover: 'http://upload.wikimedia.org/wikipedia/en/9/92/Markus_Schulz_Without_You_Near_album_cover.jpg'
},
{
name: 'Progression',
cover: 'http://upload.wikimedia.org/wikipedia/en/thumb/8/81/Markus-schulz-progression_cover.jpg/220px-Markus-schulz-progression_cover.jpg',
},
{
name: 'Do You Dream?',
cover: 'http://upload.wikimedia.org/wikipedia/en/thumb/9/92/Doyoudream.jpg/220px-Doyoudream.jpg',
}
]
},
{
name: 'Christopher Lawrence',
albums: [
{
name: 'All or Nothing',
cover: 'http://s.discogss.com/image/R-308090-1284903399.jpeg',
},
{
name: 'Un-Hooked: The Hook Sessions',
cover: 'http://s.discogss.com/image/R-361463-1108759542.jpg'
}
]
},
{
name: 'Above & Beyond',
albums: [
{
name: 'Group Therapy',
cover: 'http://s.discogss.com/image/R-2920505-1345851845-3738.jpeg'
},
{
name: 'Tri-State',
cover: 'http://s.discogss.com/image/R-634211-1141297400.jpeg',
},
{
name: 'Tri-State Remixed',
cover: 'http://s.discogss.com/image/R-1206917-1200735829.jpeg'
}
]
}
];
App.Router.map(function() {
this.resource('djs', function() {
this.route('dj', { path: '/djs/:name' });
});
});
App.IndexRoute = Ember.Route.extend({
redirect: function() {
this.transitionTo('djs');
}
});
App.DjsRoute = Ember.Route.extend({
model: function() {
return App.DJS;
}
});
App.DjsDjRoute = Ember.Route.extend({
serialize: function(dj) {
return {
name: dj.name.dasherize()
}
}
});
Index.html
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<body>
<!-- application template -->
<script type="text/x-handlebars">
<div class="navbar navbar-static-top">
<div class="navbar-inner">
{{#linkTo 'djs' class="brand"}}On The Decks{{/linkTo}}
</div>
</div>
<div class="container-fluid">
<div class="row-fluid">
{{outlet}}
</div>
</div>
</script>
<script type="text/x-handlebars" data-template-name="djs">
<div class="span2">
<ul class="nav nav-list">
{{#each controller}}
<li>{{#linkTo 'djs.dj' this}}{{name}}{{/linkTo}}
{{/each}}
</ul>
</div>
<div class="span8">
{{outlet}}
</div>
</script>
<script type="text/x-handlebars" data-template-name="djs/dj">
<h2>{{name}}</h2>
<h3>Albums</h3>
{{#if albums}}
<ul class="thumbnails">
{{#each albums}}
<li>
<div class="thumbnail">
<img {{bindAttr src="cover" alt="name"}} />
</div>
</li>
{{/each}}
{{else}}
<p>No Albums</p>
{{/if}}
</script>
<script type="text/x-handlebars" data-template-name="djs/index">
<p class="well">Please Select a DJ</p>
</script>
<script src="js/libs/jquery-1.9.1.js"></script>
<script src="js/libs/handlebars-1.0.0.js"></script>
<script src="js/libs/ember-1.0.0.js"></script>
<script src="js/app.js"></script>
</body>
</html>
You are rigth, I try to explain you
Ember has severals ways to go to a Route, here we have two examples.
The linkTo helper, and directly writing the url.
With the linkTo we provide a model to the route, the this keyword
{{#linkTo 'djs.dj' this}}{{name}}{{/linkTo}}
For the url way, ember route needs to know the model to represent, and for this executes the model hook of the route (missing in your example), you can def the dj route like this.
App.DjsDjRoute = Ember.Route.extend({
serialize: function(dj) {
return {
name: dj.name.dasherize()
}
},
model:function(dj){
return App.DJS.find(function(item){
//The url param is the dasherized name
return item.name.dasherize() == dj.name;});
}
});
Also there is a typo defining the routes and this.route('dj', { path: '/djs/:name' }); should be this.route('dj', { path: '/:name' });
Complete JSFiddle http://fiddle.jshell.net/AM7sf/10/show/#/djs