I am working on a todo type project using Ember-CLI. I used as a starting point the nifty todoMVC project, but built with Ember-CLI using this guide:
http://blaketv.com/2014/10/03/ember-cli-todo-mvc-tutorial-0-0-47//
My question is, how would I go about adding projects at the parent level. So we would have a master-detail type interface and in the sidebar we would have projects and you could CRUD project names, and then when you click on a project name, you see the todos in the detail pane.
I have gotten far enough defining the hasMany relationships to the models, but I cannot figure out if I need multiple {{outlets}} It is very difficult to get everything on the same page and working.
Here is my model for project:
export default DS.Model.extend({
title: DS.attr('string'),
isCompleted: DS.attr('boolean'),
description: DS.attr('string'),
todos: DS.hasMany('todo', {async: true})
});
and model for todos:
import DS from 'ember-data';
export default DS.Model.extend({
title: DS.attr('string'),
isCompleted: DS.attr('boolean')
});
and the main Router:
Router.map(function() {
this.resource('projects', function () {
this.route('new');
this.resource('project', { path: ':id' }, function () {
this.route('todos');
});
});
});
Project Route:
export default Ember.Route.extend({
model: function(params) {
return this.store.find('project', params.id);
}
});
Index Route:
export default Ember.Route.extend({
model: function() {
return this.store.find('project');
}
});
Todos Route:
export default Ember.Route.extend({
model: function() {
return this.modelFor('todos');
}
});
So for project.hbs this is where it gets tricky. I create the sidebar with bootsrap and then this outlet shows the todos....
<div class="projects-column col-md-3">
<div id="inbox-header"><span class="glyphicon glyphicon-inbox"></span> Inbox <span class="badge">42</span></div>
<div id="projects-header"><span class="glyphicon glyphicon-list-alt"></span> Projects</div>
<div id="forecast-header"><span class="glyphicon glyphicon-calendar"></span> Forecast</div>
<div id="log-header"><span class="glyphicon glyphicon-book"></span> Sessions Log</div>
</div>
<div>{{outlet}}</div>
Index.hbs:
<ul>
{{#each model}}
<li>{{link-to title "project.todos" this}}</li>
{{/each}}
So this above when you click on the project title link, it shows the associated todos.... but it renders in the left pane... it's probably just something about the CSS layout...but something tells me there is a very Ember-ish way to do this that I am missing.
Then in /project/todo.hbs we have the iteration
{{#each model.todos}}
<li>{{title}}</li>
{{/each}}
I haven't even really addressed making the CRUD for controllers or anything. Most likely this above is laughable and there is a much more elegant way to approach this...
Basically I want a projects parent route, that I do CRUD with... and then when you render a list of project links in the sidebard and click on one, you get in the right pane the rendered ToDoMVC working app.
Of course this is just a starting point for my application. Most likely if someone comes up with a elegant way to do this, we can turn it into an open source project on github for others to learn from.
I think a bunch of burgeoning ember developers are having a hard time with this type of thing because of the multiple ways to do it (outlets, partials, render, render into other templates, views, components, etc)
Don't really know how to get any further.
Not sure if you're still stuck, but I'd try it without bootstrap as a side bar, and just put an {{#each}} [full code here]
App = Ember.Application.create({
LOG_TRANSITIONS: true,
LOG_BINDINGS: true,
LOG_VIEW_LOOKUPS: true,
LOG_ACTIVE_GENERATION: true,
debugMode: true
});
App.Router.map(function() {
this.resource('projects', {
path: '/'
});
this.resource('project', {
path: '/projects/:project_id'
}, function() {
// URL = '/projects/:id/todos'
this.resource('project.todos', {
path: '/todos'
}, function() {
// URL = '/project/:id/todos/new'
this.route("new");
});
});
});
App.ApplicationAdapter = DS.FixtureAdapter.extend();
//App.Store = DS.Store.extend({adapter : DS.FixtureAdapter});
App.ProjectsRoute = Ember.Route.extend({
model: function() {
return this.store.findAll('project');
},
actions: {
addproject: function() {
var newproject = this.store.createRecord('project', {
name: "My New project"
});
},
removeproject: function(project) {
console.log(project);
console.log(this.controller.get("model"));
this.controller.get("model").removeObject(project);
}
}
});
App.ProjectRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('project', params.project_id).then(function(project) {
return project;
});
}
});
App.ProjectsIndexRoute = Ember.Route.extend({
model: function(params) {
return this.modelFor('project');
}
});
App.ProjectTodosRoute = Em.Route.extend({
model: function(params) {
return this.modelFor('project');
},
actions: {
addtodo: function() {
this.transitionTo("project.todos.new");
}
}
});
App.projecttodosNewRoute = Em.Route.extend({
model: function(params) {
parentprojectId = this.modelFor('project').get("id");
newtodo = this.store.createRecord('todo', {
id: "5",
name: "John Doe",
//project : parentprojectId
project: this.store.getById('project', parentprojectId)
});
console.log("new todo = " + newtodo);
return newtodo;
},
actions: {
save: function() {
//console.log(this.controllerFor('projecttodosNew').content);
//console.log('save of newtodo = '+this.controllerFor('projecttodosNew').get('newtodo'));
console.log('newtodo~ ' + newtodo.get('name') + ', ' +
newtodo.id + ', ' + newtodo);
newtodo.save()
//this.controllerFor('projecttodosNew').content.save()
.then(function() {
this.transitionTo("project.todos");
});
},
cancel: function() {
console.log("rollback for " + this.get("controller.model"));
this.get("controller.model").rollback();
this.set("controller.model", null);
this.transitionTo("project.todos");
}
}
});
//App.projecttodosNewController = Ember.ObjectController
// .extend({
// needs : [ 'application', 'project'],
// newtodo : null
// });
App.Project = DS.Model.extend({
name: DS.attr(),
todos: DS.hasMany('todo', {
async: true
})
});
App.Project.FIXTURES = Em.A([{
id: 1,
name: 'Monday',
todos: ['2']
}, {
id: 2,
name: 'Tuesday',
todos: ['1', '2']
}, {
id: 3,
name: 'Wednesday',
todos: ['4']
}]);
App.Todo = DS.Model.extend({
name: DS.attr('string'),
//project : DS.belongsTo('project')
});
App.Todo.FIXTURES = [{
id: 1,
name: 'shop',
project: 1
}, {
id: 2,
name: 'sell things',
project: 2
}, {
id: 4,
name: 'dance',
project: 3
}];
/* Put your CSS here */
html,
body {
margin: 20px;
}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Ember Starter Kit</title>
<link rel="stylesheet" href="http://cdnjs.cloudflare.com/ajax/libs/normalize/2.1.0/normalize.css">
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="http://builds.handlebarsjs.com.s3.amazonaws.com/handlebars-v1.3.0.js"></script>
<script src="http://builds.emberjs.com/tags/v1.6.1/ember.js"></script>
<script src="http://builds.emberjs.com/tags/v1.0.0-beta.10/ember-data.prod.js"></script>
</head>
<body>
<script type="text/x-handlebars">
<h2>Welcome to "The Project/TODO Demo"</h2>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="projects">
<ul>
{{#each item in model}}
<li>{{#link-to 'project.todos' item }}{{item.name}}, List of todos{{/link-to}} ,
<button {{action "removeproject" item}}>X</button>
</li>
{{/each}}
</ul>
<button type="button" {{action "addproject" this.id}}>Add a project</button>
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="project/index">
<br><b>Name of project:</b> {{name}}
</script>
<script type="text/x-handlebars" data-template-name="project">
{{#link-to "projects"}}Home{{/link-to}} {{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="project/todos/index">
<h1></h1>
<b>todos</b>
<br>
<ul>
{{#each todo in todos}}
<li>{{todo.name}}</li>
{{/each}}
</ul>
<button type="button" {{action "addtodo"}}>Add a todo</button>
<br>{{#link-to 'project' this}}project details page{{/link-to}} {{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="project/todos/new">
<h1></h1>
<b>New todos</b>
<br>
<ul>
<li>Name: {{input type='text' value=model.name}}</li>
<li>todo Id: {{input type='text' value=id}}</li>
<li>Parent project Id: {{project}}</li>
</ul>
<button type="button" {{action "save"}}>Save todo</button>
<button type="button" {{action "cancel"}}>cancel</button>
<br>{{outlet}}
</script>
</body>
</html>
on the top of your homepage / index. After you get that working you can start playing with the layout with bootstrap, and getting it looking cool.
similar to links doc. Hope that gets you passed your hurdle.
Related
I'm following the official Ember guide to do a Todo app.
Is it possible to do a persistent change to the fixtures? If so, how? Because with the code I have, clicking the check box doesn't save the value persistently to the fixtures.
index.html:
<script type="text/x-handlebars" data-template-name="todos">
<ul id="todo-list">
{{#each itemController="todo"}}
<li {{bind-attr class="isCompleted:completed"}}>
{{input type="checkbox" checked=isCompleted class="toggle"}}
<label>{{title}}</label><button class="destroy"></button>
</li>
{{/each}}
</ul>
</script>
application.js:
window.Todos = Ember.Application.create();
Todos.ApplicationAdapter = DS.FixtureAdapter.extend();
models/todo.js:
Todos.Todo = DS.Model.extend({
title: DS.attr('string'),
isCompleted: DS.attr('boolean')
});
Todos.Todo.FIXTURES = [
{
id: 1,
title: 'Learn Ember.js',
isCompleted: true
}
];
controllers/todo_controller.js:
Todos.TodoController = Ember.ObjectController.extend({
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')
});
I don't believe so. At the end of that tutorial, there's a section for replacing the fixture adapter with local storage so that you get real persistence. http://emberjs.com/guides/getting-started/using-other-adapters/
js/application.js
window.Todos = Ember.Application.create();
Todos.ApplicationAdapter = DS.LSAdapter.extend({
namespace: 'todos-emberjs'
});
index.html
<!--- ... additional lines truncated for brevity ... -->
<script src="js/libs/ember-data.js"></script>
<script src="js/libs/localstorage_adapter.js"></script>
<script src="js/application.js"></script>
<!--- ... additional lines truncated for brevity ... -->
How can I force Ember to update a template when a child record is added/removed to my model?
Customer model
Docket.Customer = DS.Model.extend({
name: DS.attr('string'),
initial: DS.attr('string'),
description: DS.attr('string'),
number: DS.attr('string'),
archived: DS.attr('boolean'),
projects: DS.hasMany('project',{ async: true })
});
Project model
Docket.Project = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
number: DS.attr('string'),
archived: DS.attr('boolean'),
customer: DS.belongsTo('customer', { async: true })
});
When a project is added/deleted, this template should be updated:
{{#each filteredProjects}}
<h2>Customer: {{customer.name}}</h2>
<ul class="entries">
{{#each projects}}
<li>
<div class="actions">
<button {{action "remove" id}} class="icon-close"></button>
</div>
<div class="link" {{action "edit" id}} data-uk-modal="{target:'#project-modal'}">
<span class="before">{{number}}</span>{{name}}
</div>
</li>
{{else}}
<li>No projects</li>
{{/each}}
</ul>
{{/each}}
Example actions (extract)
remove: function (id) {
this.get('store').find('project', id).then(function (data) {
data.deleteRecord();
data.save();
});
},
save: function() {
// create new record
var project = this.store.createRecord('project', _this.getProperties('name', 'number', 'description', 'archived'));
// set customer
project.set('customer', this.get('selectedCustomer'));
// validate and save if validation passes, otherwise show errors
project.save().then(function () {
_this.closeForm();
}, function (response) {
_this.set('errors', response.errors);
});
}
Update 2
I openend an issue here, but it hasn't been resolved until now.
Your problem is, because you are using map to group the data, the returned array isn't a DS.RecordArray instance, so when a item is added or removed, the content isn't updated.
I think the easy way to handle it, is to reload the data, when a item is added or removed. So extract the method that load the data and call it in the save and remove action. Here I created a loadData method:
route
Docket.OrganizationProjectsIndexRoute = Docket.AuthenticatedRoute.extend({
setupController: function() {
this.loadData();
},
loadData: function () {
var projectsController = this.controllerFor('organization.projects');
this.store.find('customer').then(function(customers) {
var promises = customers.map(function(customer) {
return Ember.RSVP.hash({
customer: customer,
projects: customer.get('projects').then(function(projects) {
return projects.filter(function(project) {
return !project.get('archived');
});
});
});
});
Ember.RSVP.all(promises).then(function(filteredProjects) {
projectsController.set('filteredProjects', filteredProjects);
});
});
},
actions: {
remove: function (project) {
var _this = this;
project.destroyRecord().then(function() {
_this.loadData();
});
},
save: function() {
// create new record
var project = this.store.createRecord('project', _this.getProperties('name', 'number', 'description', 'archived'));
// set customer
project.set('customer', this.get('selectedCustomer'));
// validate and save if validation passes, otherwise show errors
projects.save().then(function () {
_this.closeForm();
_this.loadData();
}, function (response) {
_this.set('errors', response.errors);
});
}
}
});
template
{{#each filteredProjects}}
<h2>Customer: {{customer.name}}</h2>
<ul class="entries">
{{#each projects}}
<li>
<div class="actions">
<button {{action "remove" this}} class="icon-close"></button>
</div>
<div class="link" {{action "edit" this}} data-uk-modal="{target:'#project-modal'}">
<span class="before">{{number}}</span>{{name}}
</div>
</li>
{{else}}
<li>No projects</li>
{{/each}}
</ul>
{{/each}}
Some tips:
You can use project.destroyRecord() instead of project.deleteRecord() project.save().
You can pass the project instance directlly to the action using {{action "remove" this}} instead of the id {{action "remove" id}} so no need to reload using:
this.get('store').find('project', id)...
I hope it helps
I am trying to generate click able links using emberjs framework. I have the model setup correctly and I have the following handlebar template:
<script type="text/x-handlebars" data-template-name="index" >
{{#each name in model.mymodules }}
{{#link-to name 'home' }}{{name}}{{/link-to}}
{{/each
</script>
The idea is to call modulename/home on each link.
For ex: say I have 3 modules: "abc", "xyz", "123"
I want three links:
abc <a href="/abc/home">, xyz <a href="/xyz/home">, 123 <a href="/123/home">
What controller/route do I need to define for this to work.
jsfiddle:
http://jsfiddle.net/spkRa/2/
You need to make use of ember resources for dealing with this problem
Read http://emberjs.com/guides/routing/defining-your-routes/
Example of application code should be something like this. JSfidle http://jsfiddle.net/NQKvy/291/
App = Ember.Application.create({
LOG_TRANSITIONS: true,
LOG_TRANSITIONS_INTERNAL: true,
LOG_VIEW_LOOKUPS: true
});
App.Router.map(function() {
this.resource('modules', { path: '/modules' }, function() {
this.route('home', {path: ':module_name/home'});
});
});
App.IndexRoute = Ember.Route.extend({
model:function(){
return App.Modules;
}
});
App.ModulesHomeRoute = Ember.Route.extend({
model: function(params) {
//returns an object from an ember array based on the property value
return App.Module.findProperty('name',params.module_name);
},
serialize: function(model, params) {
//updates the url with the param value
return { module_name: model.get('name') };
}
});
App.Modules = Ember.A([
Ember.Object.create({name:'aaa'}),
Ember.Object.create({name:'bbb'}),
Ember.Object.create({name:'ccc'})
]);
And hadlebars code
<script type="text/x-handlebars" data-template-name="index">
<ul>
{{#each}}
<li>{{name}}</li>
<li>{{#link-to 'modules.home' this}}{{name}}{{/link-to}}</li>
{{/each}}
</ul>
</script>
<script type="text/x-handlebars" data-template-name="modules/home">
This is the home of the module {{name}}
</script>
I want to simply render an Ember select view with the model defined in a route. Data is coming from fixtures adapter. When doing this, I receive the error: Ember.CollectionView's content must implement Ember.Array - You passed App.AuthorsController.
How can I solve this ?
See JSFIDDLE: http://jsfiddle.net/cyclomarc/frvJZ/4/
(after running the app, click on the 'Authors' link to goto the authors route with authorsController data.
CODE-HTML:
<script type="text/x-handlebars" data-template-name="application">
<h1>Ember select view</h1>
{{#linkTo 'authors'}}Authors{{/linkTo}}
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="authors">
{{view Ember.Select contentBinding="App.AuthorsController"}}
</script>
CODE-JS:
window.App = Ember.Application.create();
App.Router.map(function () {
this.resource('authors', { path: "/authors" });
});
App.AuthorsRoute = Ember.Route.extend({
model: function () {
return App.Author.find();
}
});
App.AuthorsController = Ember.ArrayController.extend({})
//DATA
//define model for category
App.Author = DS.Model.extend({
name: DS.attr('string'),
language: DS.attr('string')
});
App.Store = DS.Store.extend({
revision: 12,
adapter: 'DS.FixtureAdapter'
});
App.Author.FIXTURES = [
{
id: 1,
name: 'Luc Verschuren',
language: 'German'
},
{
id: 2,
name: 'Patrick Burms',
language: 'Dutch'
},
{
id: 3,
name: 'Jean Demeester',
language: 'French'
}
];
Try using the content property of your App.AuthorsController having the data:
<script type="text/x-handlebars" data-template-name="authors">
{{view Ember.Select
contentBinding="content"
optionLabelPath="content.name"}}
</script>
Working jsfiddle.
Hope it helps.
Using:
ember-1.0.0-pre.4.js
ember-data.js REVISION:11
handlebars-1.0.rc.2.js
Please have a look at this jsFiddle illustrating the described problem.
I have a list of items that are displayed in a template. The template contain a linkTo helper that let's the controller add an item to the collection and is shown as a text input on the page.
Adding the item to the collection is done by the controller:
App.TodoItem = DS.Model.extend({
title: DS.attr('string', { defaultValue: "unknown" })
});
App.Router.map(function () {
this.resource('todo_items')
});
App.TodoItemsRoute = Em.Route.extend({
model: function () {
return App.TodoItem.find();
}
});
App.TodoItemsController = Em.ArrayController.extend({
addTodoItem: function () {
App.TodoItem.createRecord();
}
});
If I want the new item to be shown is the list, I have to pass params to createRecord, otherwise the item is not visible. The same behaviour can be reproduced by using Chrome's inspector and then the item can be made visible as follows:
// Open the jsFiddle http://jsfiddle.net/bazzel/BkFYd/ and select 'result(fiddle.jshell.net) in the inspector, then:
var item = App.TodoItem.createRecord();
// Nothing visible yet.
item.set('title', 'Whatever');
// Now the text input appear with the title as its value.
Is this expected behaviour and if so, what am I missing here?
I took time to redo your example the way i feel things should be done properly with Emberjs. You should rather make sure of transaction and properly define your views and then all your issues get taken care of. So here's how i think you should do this
Define a view for the textfield to capture the value being entered or
just bind it to the model property.
Listing items and adding a new item to the list should be done in two different views and should not be mixed together
<script type="text/x-handlebars">
{{outlet}}
<div>
{{outlet 'addItem'}}
</div>
</script>
<script type="text/x-handlebars" data-template-name="todo_items">
{{#linkTo 'todo_items.new'}}Add Todo Item{{/linkTo}}
<ul>
{{#each item in controller}}
<li>
{{#unless item.isNew}}
{{item.title}}
{{/unless}}
</li>
{{/each}}
</ul>
</script>
Define different states for listing items and adding a new one
To benefit from automatic binding of your text field value to the
model property, you need to associate an ObjectController to the TodoItemsNew route
Finally, make use of transaction to create and commit records to the store
window.App = Em.Application.create();
App.TodoItem = DS.Model.extend({
title: DS.attr('string')
});
App.TodoItem.FIXTURES = [{
id: 1,
title: 'Lorem'
}, {
id: 2,
title: 'Ipsum'
}];
App.store = DS.Store.create({
revision: 11,
adapter: DS.FixtureAdapter.create()
});
App.Router.map(function () {
this.resource('todo_items',function(){
this.route('new');
})
});
App.IndexRoute = Em.Route.extend({
redirect: function () {
this.transitionTo('todo_items');
}
});
App.TodoItemsRoute = Em.Route.extend({
model: function () {
return App.TodoItem.find();
}
});
App.TodoItemsNewRoute = Em.Route.extend({
transaction: App.store.transaction(),
setupController:function(controller) {
console.info(controller.toString());
controller.set('content',this.transaction.createRecord(App.TodoItem));
},
renderTemplate: function() {
this.render('addItem',{
into:'application',
outlet:'addItem',
})
},
events: {
addItem: function() {
this.transaction.commit();
this.transitionTo('todo_items');
}
}
});
App.TodoItemsController = Em.ArrayController.extend();
App.TodoItemsNewController = Em.ObjectController.extend();
App.TextField = Ember.TextField.extend({
insertNewline: function () {
this.get('controller').send('addItem')
}
});
Here' is a working version of the example on jsfiddle. Hopefully, i helped with this example clarify some of your issues.
Thank you Ken for answering my question. It indeed feels like a more proper of way of doing this in Ember. However, I still think it's difficult to get the hang of which objects are accessible from where...
Your example inspired me to do a rewrite of my code. I also made some changes to your approach:
I'm not sure if it's the best practice, my I don't create a store instance. Instead I define a Store class.
The content for the TodoItemsNewController is set by calling the model property on the corresponding route.
renderTemplate in the TodoItemsNewRoute only needs the outlet key.
<script type="text/x-handlebars">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="todo_items">
{{#linkTo 'todo_items.new'}}Add Todo Item{{/linkTo}}
<ul>
{{outlet "addItem"}}
{{#each controller}}
<li>
{{#unless isNew}}
{{title}}
{{/unless}}
</li>
{{/each}}
</ul>
</script>
<script type="text/x-handlebars" data-template-name="todo_items/new">
{{view Ember.TextField valueBinding="title" placeholder="Enter title"}}
window.App = Em.Application.create();
App.TodoItem = DS.Model.extend({
title: DS.attr('string', {
defaultValue: "unknown"
})
});
App.TodoItem.FIXTURES = [{
id: 1,
title: 'Lorem'
}, {
id: 2,
title: 'Ipsum'
}];
App.Store = DS.Store.extend({
revision: 11,
adapter: DS.FixtureAdapter.create()
});
App.Router.map(function() {
this.resource('todo_items', function() {
this.route('new');
});
});
App.IndexRoute = Em.Route.extend({
redirect: function() {
this.transitionTo('todo_items');
}
});
App.TodoItemsRoute = Em.Route.extend({
model: function() {
return App.TodoItem.find();
}
});
App.TodoItemsNewRoute = Em.Route.extend({
model: function() {
return App.TodoItem.createRecord();
},
renderTemplate: function() {
this.render({
outlet: 'addItem'
});
}
});
App.TodoItemsNewView = Em.View.extend({
tagName: 'li'
});
The updated example is on jsFiddle.
Any reviews are welcome.