Route not seeing URL slug - ember.js

I have an app that allows management of orders. The default view is a split view with orders on the left and selected order details on the right like so:
/Orders /Orders/:order_id
|-----------| |-------------------------------------|
| | | |
| | | |
| | | |
| | | |
| | | |
| List of | | Selected Item |
| Items | | Details |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
|-----------| |-------------------------------------|
I'd like to be able to edit an order in "full-screen" mode so that the URL and template looks like this:
/Orders/:order_id/edit/
|---------------------------------------------------|
| |
| |
| |
| |
| |
| |
| Order Edit Interface |
| |
| |
| |
| |
| |
| |
| |
|---------------------------------------------------|
My routes are currently setup as follows:
this.resource('Orders.edit', { path: 'Orders/:order_id/edit' } , function () {
this.route('customer-details');
this.route('vendor-details');
this.route('shipping-details');
}
this.resource('Orders', { path: 'Orders' }, function () {
this.resource('Order', { path: ':order_id' }, function () {
this.route('customer-details');
this.route('vendor-details');
this.route('shipping-details');
}
}
And my order routes look like this:
// Orders Route
App.OrdersRoute = Em.Route.extend({
model: function() {
return this.store.find('order');
},
afterModel: function (orders) {
this.transitionTo('orders.order', orders.get('firstObject') );
}
});
// Order Detail
App.OrdersOrderRoute = Em.Route.extend({
model: function(params) {
return this.store.find('order', params.order);
},
setupController: function (controller, model) {
controller.set('content', model);
}
});
// Order Edit Route
App.OrdersEditRoute = Em.Route.extend({
model: function(params) {
if (typeof params.order_id !== 'undefined') {
this.store.find('order', params.order_id).then(function (order) {
this.controllerFor('orders.edit').set('content', order);
});
} else if (typeof params.order !== 'undefined') {
this.store.find('order', params.order).then(function (order) {
this.controllerFor('orders.edit').set('content', order);
});
}
},
afterModel: function(eo) {
this.transitionTo('orders.edit.customer-details', order);
}
});
// Order Edit - Customer Details Route
App.OrdersEditCustomerDetailsRoute = Em.Route.extend({
model: function() {
try {
var order = this.get('order');
return order;
} catch (e) {
console.log('ERROR: ' + e);
}
},
beforeModel: function() {
this.set('order', this.modelFor('orders.edit'));
},
});
This setup works if I'm in the Orders/:order_id route/template and click the edit button which then sends me to Orders/:order_id/edit with the desired interface and data loaded. But if I try to refresh Orders/:order_id/edit in the browser window nothing loads and I get the following errors. I also don't hit any breakpoints set inside of the Orders/:order_id/edit route when accessing the URL this way.
Uncaught Error: Assertion Failed: Cannot delegate set('classification', N/A) to the 'content' property of object proxy <Synapse.EngineeringOrdersEditDetailsController:ember1242>: its 'content' is undefined. libs.js:2870
Ember.assert libs.js:2870
EmberObject.extend.setUnknownProperty libs.js:23933
set libs.js:9229
setPath libs.js:9289
set libs.js:9209
trySet libs.js:9306
(anonymous function) libs.js:3416
tryable libs.js:5964
tryFinally libs.js:10524
suspendListener libs.js:5967
_suspendObserver libs.js:8311
Binding._sync libs.js:3415
DeferredActionQueues.invoke libs.js:11346
DeferredActionQueues.flush libs.js:11398
Backburner.end libs.js:10861
Backburner.run libs.js:10916
apply libs.js:10745
run libs.js:9378
runInitialize libs.js:45596
n.Callbacks.j libs.js:2
n.Callbacks.k.fireWith libs.js:2
n.extend.ready libs.js:2
I libs.js:2
TypeError: undefined is not a function
at http://localhost:1337/js/app.js:27936:22
at invokeCallback (http://localhost:1337/js/libs.js:13310:19)
at publish (http://localhost:1337/js/libs.js:12980:9)
at publishFulfillment (http://localhost:1337/js/libs.js:13400:7)
at http://localhost:1337/js/libs.js:18818:9
at DeferredActionQueues.invoke (http://localhost:1337/js/libs.js:11348:18)
at Object.DeferredActionQueues.flush (http://localhost:1337/js/libs.js:11398:15)
at Object.Backburner.end (http://localhost:1337/js/libs.js:10861:27)
at Object.Backburner.run (http://localhost:1337/js/libs.js:10916:20)
at executeTimers (http://localhost:1337/js/libs.js:11241:12) libs.js:6663
logToConsole libs.js:6663
RSVP.onerrorDefault libs.js:49435
__exports__.default.trigger libs.js:12274
Promise._onerror libs.js:12998
publishRejection libs.js:13405
(anonymous function) libs.js:18818
DeferredActionQueues.invoke libs.js:11348
DeferredActionQueues.flush libs.js:11398
Backburner.end libs.js:10861
Backburner.run libs.js:10916
executeTimers libs.js:11241
(anonymous function) libs.js:11231
Uncaught Error: Assertion Failed: TypeError: undefined is not a function libs.js:2870
Ember.assert libs.js:2870
RSVP.onerrorDefault libs.js:49436
__exports__.default.trigger libs.js:12274
Promise._onerror libs.js:12998
publishRejection libs.js:13405
(anonymous function) libs.js:18818
DeferredActionQueues.invoke libs.js:11348
DeferredActionQueues.flush libs.js:11398
Backburner.end libs.js:10861
Backburner.run libs.js:10916
executeTimers libs.js:11241
(anonymous function)
I suspect it has something to do with having the Orders/order/edit route outside the hierarchy of the Orders resource but I was unable to get the outlets to play nicely to render the desired interface.
TLDR - How do I get the Orders/:order_id/edit to load the model properly from the URL slug? Using Ember 1.6.1 and Ember-data Fixture Adapter

Two approaches to achieve what you describe are,
Separate resource for editing (basically along the lines if what you've tried)
Nested resource and maintain a property based on which the template will only render the outlet part or not.
Example of these two approaches,
http://emberjs.jsbin.com/wupiwoculoxi/1/edit
hbs
<script type="text/x-handlebars" id="orders">
{{#unless showOnlyEditDetail}}
orders
<br/>
{{#each order in model}}
{{#link-to "order" order.id}}
{{order.name}}
{{/link-to}}
<br/> <br/>
{{/each}}
{{/unless}}
{{!--the following lines before the outlet are shown on purpose for demonstration reasons, the unless helper in this example can hide anything on this template--}}
<hr/>
<i>the value of showOnlyEditDetail:</i><b>{{showOnlyEditDetail}}</b>
<hr/>
{{outlet}}
</script>
<script type="text/x-handlebars" id="order">
the order
<br/>
<br/>
{{this.id}}
<br/>
<br/>
{{this.name}}
<br/>
<br/>
{{#link-to "orderEdit" this.id }}edit1{{/link-to}}
<i>(separate resource)</i>
<br/>
<br/>
{{#link-to "orderEdit2" this.id }}edit2{{/link-to}}
<i>(nested resource and maintaining property)</i>
</script>
<script type="text/x-handlebars" id="orderEdit">
edit the order
<br/>
{{this.id}}
<br/>
{{this.name}}
<br/>
{{#link-to "order" this.id}}back to the order{{/link-to}}
</script>
<script type="text/x-handlebars" id="orderEdit2">
edit2 the order
<br/>
{{this.id}}
<br/>
{{this.name}}
<br/>
{{#link-to "order" this.id}}back to the order{{/link-to}}
</script>
js
App = Ember.Application.create();
App.Router.map(function() {
this.resource("orderEdit",{path:"orders/:order_id/edit"}, function(){
});
this.resource('orders', { path: 'orders' }, function () {
this.resource('order', { path: ':order_id' }, function () {
this.route('customer-details');
this.route('vendor-details');
this.route('shipping-details');
});
this.resource("orderEdit2",{path:":order_id/edit2"}, function(){});
});
});
App.IndexRoute = Ember.Route.extend({
redirect: function() {
this.transitionTo("orders");
}
});
var ordersData=[
{id:1,name:"order1"},
{id:2,name:"order2"},
{id:3,name:"order3"}
];
App.OrdersRoute = Ember.Route.extend({
model: function() {
return ordersData;
},
setupController:function(c,m){
c.set("showOnlyEditDetail",false);
this._super(c,m);
}
});
/*if the second approach is used then the controller with the specific property
(i.e. showOnlyEditDetail) must be defined.*/
App.OrdersController=Em.ArrayController.extend({
showOnlyEditDetail:false
});
App.OrderRoute = Ember.Route.extend({
model: function(params) {
return ordersData.findBy("id",parseInt(params.order_id,10));
}
});
App.OrderEditRoute = Ember.Route.extend({
model: function(params) {
return ordersData.findBy("id",parseInt(params.order_id,10));
}
});
App.OrderEdit2Route = Ember.Route.extend({
model: function(params) {
return ordersData.findBy("id",parseInt(params.order_id,10));
},
setupController:function(c,m){
this._super(c,m);
this.controllerFor("orders").set("showOnlyEditDetail",true);
},
actions:{
willTransition:function(){
this.controllerFor("orders").set("showOnlyEditDetail",false);
}
}
});

Related

carousel for each product(route) ember js

Hi I have a categories route and then product route. Each product has many images.
I want to show this images in carousel. the first product i click i get carousel with images but when i click on second product no carousel is displayed
MyApp.ShowCarouselComponent = Ember.Component.extend({
content: [],
templateName: 'show-carousel',
classNames: ['carousel', 'slide'],
init: function () {
this._super.apply(this, arguments);
// disable the data api from boostrap
$(document).off('.carousel.data-api');
// at least one item must have the active class, so we set the first here, and the class will be added by class binding
//var cdata = this.get('controller').get('carouselData');
var obj = this.get('content').get('firstObject');
Ember.set(obj, 'isActive', true);
console.log('this is what obj is ');
console.log(obj);
},
previousSlide: function () {
this.$().carousel('prev');
},
nextSlide: function () {
this.$().carousel('next');
},
didInsertElement: function () {
this.$().carousel();
},
willDestroyElement: function () {
this.$('.carousel').remove();
this._super();
},
indicatorsView: Ember.CollectionView.extend({
tagName: 'ol',
classNames: ['carousel-indicators'],
contentBinding: 'parentView.content',
itemViewClass: Ember.View.extend({
click: function () {
var $elem = this.get("parentView.parentView").$();
$elem.carousel(this.get("contentIndex"));
},
template: Ember.Handlebars.compile(''),
classNameBindings: ['content.isActive:active']
})
}),
itemsView: Ember.CollectionView.extend({
classNames: ['carousel-inner'],
contentBinding: 'parentView.content',
itemViewClass: Ember.View.extend({
classNames: ['item'],
classNameBindings: ['content.isActive:active'],
template: Ember.Handlebars.compile('\
{{view.content}}\
<img {{bind-attr src="view.content.product_url"}} alt="dfdds"/>\
<div class="carousel-caption">\
<h4>{{view.content}}</h4>\
<p>{{view.content.image_description}}</p>\
</div>')
})
})
});
show-carousel component
{{view view.indicatorsView}}
{{view view.itemsView}}
<a class="left carousel-control" {{action previousSlide target="view"}}>‹</a>
<a class="right carousel-control" {{action nextSlide target="view"}}>›</a>
router.js
this.resource('categories', {
path: '/'
}, function () {
this.resource('category', {
path: '/:category_id'
}, function () {
this.resource('product', {
path: '/:product_id'
});
});
});
In case of Ember views and Ember Components case accessing dom in init() method is a bad idea because it might happen that element that you are trying to access is not yet inserted into dom. so putting code from init method into didInsertElement() might solve your problem.

Ember.js clear controller on transitionToRoute call

I have a route that displays a list of parcels, and an Ember.Select that allows the user to select which state's parcels to show.
Model
App.Parcel = DS.Model.extend({
addresses: DS.attr('array')
});
Route
App.ParcelsRoute = Ember.Route.extend({
state: null,
renderTemplate: function () {
this.render({ outlet: 'parcels' });
},
model: function (params) {
state = params.state;
App.ParcelAdapter.state = state;
App.ImageAdapter.state = state;
return Ember.RSVP.hash({
props: this.store.findAll('parcel'),
states: this.store.findAll('state'),
});
},
setupController: function (controller, model) {
controller.set('states', model.states);
controller.set('props', model.props);
controller.set('selectedState', state);
}
});
Controller
App.ParcelsController = Ember.ObjectController.extend({
selectedState: null,
props: null,
states: null,
first: true,
modelReloadNeeded: function () {
if (this.get('selectedState') != undefined && !this.get('first')) {
this.transitionToRoute('/parcels/' + this.get('selectedState'));
}else{
this.set('first', false);
}
}.observes('selectedState')
});
Handlebars
<script type="text/x-handlebars" id="parcels">
{{view Ember.Select content=states optionValuePath="content.id" optionLabelPath="content.id" value=selectedState}}
<input class="search" placeholder="Search"/>
<ul class="list nav">
{{#each props}}
<li>{{#link-to 'parcel' this}}<h3 class="name">{{addresses.0.street_address}}</h3>{{/link-to}}</li>
{{/each}}
</ul>
</script>
When the select transitions to the new route, both the old routes data and new routes are in the model, but if I reload the page, only the current routes data is loaded. Is there a way to clear the DS.RecordArray for props in the controller without a location.reload() call?

Template not updating when controller property changes

Caveat: This is part of my first ember app.
I have an Ember.MutableArray on a controller. The corresponding view has an observer that attempts to rerender the template when the array changes. All the changes on the array (via user interaction) work fine. The template is just never updated. What am I doing wrong?
I'm using Ember 1.2.0 and Ember Data 1.0.0-beta.4+canary.7af6fcb0, though I guess the latter shouldn't matter for this.
Here is the code:
var ApplicationRoute = Ember.Route.extend({
renderTemplate: function() {
this._super();
var topicsController = this.controllerFor('topics');
var topicFilterController = this.controllerFor('topic_filter');
this.render('topics', {outlet: 'topics', controller: topicsController, into: 'application'});
this.render('topic_filter', {outlet: 'topic_filter', controller: topicFilterController, into: 'application'});
},
});
module.exports = ApplicationRoute;
var TopicFilterController = Ember.Controller.extend({
topicFilters: Ember.A([ ]),
areTopicFilters: function() {
console.log('topicFilters.length -> ' + this.topicFilters.length);
return this.topicFilters.length > 0;
}.property('topicFilters'),
getTopicFilters: function() {
console.log('getTopicFilters....');
return this.store.findByIds('topic', this.topicFilters);
}.property('topicFilters'),
actions: {
addTopicFilter: function(t) {
if(this.topicFilters.indexOf(parseInt(t)) == -1) {
this.topicFilters.pushObject(parseInt(t));
}
// this.topicFilters.add(parseInt(t));
console.log('topicFilters -> ' + JSON.stringify(this.topicFilters));
},
removeTopicFilter: function(t) {
this.topicFilters.removeObject(parseInt(t));
console.log('topicFilters -> ' + JSON.stringify(this.topicFilters));
}
}
});
module.exports = TopicFilterController;
var TopicFilterView = Ember.View.extend({
topicFiltersObserver: function() {
console.log('from view.... topicFilters has changed');
this.rerender();
}.observes('this.controller.topicFilters.[]')
});
module.exports = TopicFilterView;
// topic_filter.hbs
{{#if areTopicFilters}}
<strong>Topic filters:</strong>
{{#each getTopicFilters}}
<a {{bind-attr href='#'}} {{action 'removeTopicFilter' id}}>{{topic}}</a>
{{/each}}
{{/if}}
var TopicsController = Ember.ArrayController.extend({
needs: ['topicFilter'],
all_topics: function() {
return this.store.find('topic');
}.property('model', 'App.Topic.#each'),
actions: {
addTopicFilter: function(t) {
App.__container__.lookup('controller:topicFilter').send('addTopicFilter', t);
}
}
});
module.exports = TopicsController;
// topics.hbs
<ul class="list-group list-unstyled">
{{#each all_topics}}
<li class="clear list-group-item">
<span class="badge">{{entryCount}}</span>
<a {{bind-attr href="#"}} {{action 'addTopicFilter' id}}>{{topic}}</a>
</li>
{{/each}}
</ul>
your observes should just be controller.topicFilters.[]
And honestly this is a very inefficient way of doing this, rerendering your entire view because a single item changed on the array. If you show your template I can give you a much better way of handling this.
Here's an example, I've changed quite a few things, and guessed on some others since I don't know exactly how your app is.
http://emberjs.jsbin.com/uFIMekOJ/1/edit

2 dimensional table in Ember.js

We're evaluating Ember.js (against Angular) for a complex app by building a few "toy apps". One of them is to present data in a table.
I've been through countless SO postings, the Ember Website, and other sites but can't quite find the key to making it work. The closest examples were at xeqtit and this fiddle.
Quite stuck.
Any pointers to how to set this up? Been reading web postings for days just don't see the answer out there...
The Problem Statement
To simplify the problem: imagine a list of routers, each router can have a variable number of interfaces, those interfaces have an address, status, etc.
The final table would look like:
__________________________________________________
Machine | Interfaces
rt1.rp.ps.com | UP | UP | UP | NG | UP | UP | NG |
rt2.rp.ps.com | UP | UP | |
rt3.rp.ps.com | UP | UP | UP | |
rt4.rp.ps.com | UP | UP | UP | NG | UP | UP | NG |
rt5.rp.ps.com | UP | UP | UP | NG | |
--------------------------------------------------
Note the variable number of columns.
The Objects:
App.Machine = Ember.Object.extend(
{
nickname: '',
address: '',
ifaces: []
});
App.Interface = Ember.Object.extend(
{
num: '',
status: '',
address: ''
});
The Markup
<script type="text/x-handlebars" data-template-name="machinelist">
<p>List of Machines</p>
<table>
{{#each App.MachinelistController }}
<tr>
<td>{{nickname}}</td>
<td>{{address}}</td>
<td>
<table>
{{#each p in App.MachinelistController.getInterfaces}}
<tr><td>{{p}}</td></tr>
{{/each}}
</table>
</td>
</tr>
{{/each}}
</table>
</script>
The Controller
The Controller first reads a database to get a list of machines and their addresses. It then queries each machine to fetch the list of interfaces. [I've simplified the code to show the core of the issue ... excuse any typos]
App.MachinelistController = Ember.ArrayController.create(
{
content: [],
getInterfaces: function(x, y, z)
{
// This didn't work
return this.getPath('interfaces.status');
}.property('#each.read'),
polling: false,
machinePollTime: 5000,
detailPollTime: 3000,
The list of machines is retrieved from a database via PHP. It populates the 'content' of the Controller with Machine objects, but no details on the interfaces are filled in yet:
fetch: function()
{
console.log('machine fetch');
var self = this;
$.get("be/getDVStorList.php", function(data, textStatus)
{
self.set('content', []);
var statusReport = jQuery.parseJSON(data);
statusReport.machineList.forEach(function(v)
{
var machine = App.Machine.create(
{
nickname: v['machineName'],
address: v['machineIpAddr']
});
self.pushObject( machine );
})
});
if (self.polling)
setTimeout( function() { self.fetch(); }, self.machinePollTime);
return this;
},
In a separate polling loop (still in the ArrayController), each machine in the content list is polled to get the info about its interfaces:
fetchDetails: function ()
{
console.log("fetch details");
var self = this;
self.forEach(function(item, index, self)
{
console.log(item.get('address'));
var addr = item.get('address');
var base = "http://"+ addr;
var slug = "/cgi-bin/DvStorGetStatus.cgi?Recording=1&Playback=1&Structured=1";
$.ajax(
{
url: base+slug,
timeout: 10000,
cache: false,
success: buildMachineCallback(addr),
});
});
if (self.polling)
setTimeout( function () { self.fetchDetails(); }, self.detailPollTime);
return true;
function buildMachineCallback(addr)
{
return function(data, textStatus, jqXHR) { updateDetailsCallback(data, textStatus, jqXHR, addr); };
};
This function is called when the poll to each machine returns. It adds the 'interfaces' into the data structure:
// Update *data structure* based on return values in XML
function updateDetailsCallback(data, textStatus, jqXHR, addr)
{
// might be more than one with this address
var theMachines = self.filterProperty('address')
var interfaceList = $(data).find('Interface');
var interfaces = [];
$(playInterfaceerList).each(function()
{
var anInterface = App.Interface.create();
var num = $(this).find('InterfaceNum').text();
anInterface.set('num', num);
anInterface.set('status', $(this).find('InterfaceStatus').text());
interfaces[num-1] = anInterface;
})
// update all machines sharing this IP address
theMachines.forEach(function (m, idx, tm)
{
tm[idx].set('alias', $(data).find('Generic').find('Alias').text());
tm[idx].set('health', $(data).find('Generic').find('SystemHealth').text());
interfaces.forEach(function(p)
{
tm[idx].get('ifaces').pushObject( App.Interface.create(p) );
})
});
}
}
});
There are two solutions that should works.
Ember-table, an Ember plugin: http://addepar.github.io/#/ember-table/overview
jQuery-datatable, a jQuery plugin: https://datatables.net/
Using the jQuery plugin will be more complicated because it is not directly linked to the ember render process.

{{action}} link with transitionTo using relationship id

Given a view with a context like { id: 1, form_id: 5}, I want to create an {{action}} link to the form using the form_id.
My view code looks like:
<script type="text/x-handlebars" data-template-name="group">
{{action showForm form_id href=true}}
</script>
And the action in my router looks like:
showForm: function(router, event) {
var form_id = event.context;
router.transitionTo('root.form', { id: form_id });
},
I get an error that reads:
Uncaught Error: assertion failed: You must specify a target state for event 'showForm' in order to link to it in the current state 'root.index'.
I'm guessing that the problem is with the way I'm setting up the context for transitionTo, but I haven't been able to figure out the correct solution.
Here is the full code to reproduce the problem:
<script type="text/x-handlebars" data-template-name="application">
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="group">
{{action showForm form_id href=true}}
</script>
MyApp = Ember.Application.create({
autoinit: false
});
MyApp.router = Ember.Router.create({
root: Ember.Route.extend({
index: Ember.Route.extend({
route: '/',
// Throws error:
// You must specify a target state for event 'showForm' in
// order to link to it in the current state 'root.index'
//
showForm: function(router, event) {
var form_id = event.context;
router.transitionTo('root.form', { id: form_id });
},
// Won't work because form deserialize finds id, not form_id
//showForm: Em.Route.transitionTo('root.form'),
// This won't work either
// showForm: Em.Route.transitionTo('root.form', { id: this.form_id }),
connectOutlets: function( router, context ){
var group = Em.Object.create({ id:1, form_id: 5 });
router.get( 'applicationController' ).connectOutlet( 'group', group );
}
}),
form: Ember.Route.extend({
route: '/form/:id',
serialize: function( router, context ){
return { id: context.id }
},
deserialize: function( router, context ){
var form = Em.Object.create({ id: 5, name: 'my form' });
return MyApp.Form.find( context.id );
},
connectOutlets: function( router, context ){
// left out for fiddle example
}
})
})
});
MyApp.ApplicationController = Ember.Controller.extend({});
MyApp.GroupController = Em.ObjectController.extend({});
MyApp.GroupView = Em.View.extend({ templateName: 'group' });
MyApp.initialize(MyApp.router);​
And the cooresponding fiddle:
http://jsfiddle.net/jefflab/LJGCz/
I was able to come up with an ugly solution to this problem using a computed property as the context of my action. The key snippets are:
<script type="text/x-handlebars" data-template-name="group">
<a {{action showForm view.formHash href=true}}>go to form</a>
</script>
MyApp.GroupView = Em.View.extend({
templateName: 'group',
formHash: function() {
return { id: this.get('controller').get('form_id') };
}.property('form_id')
});
And the working fiddle is here:
http://jsfiddle.net/jefflab/pGv8u/
However, after talking to Tom Dale, it is clear that the "right way" to solve to solve this problem is by using materialized objects instead of id values. If you are using Ember data, this is a great use case for the "sideloading" belongsTo feature.