ember.js - define route with optional parameter and default model - javascript

I want to define a route in emberjs, that has an optional parameter
eg:
/video
and
/video/123
if no parameter is supplied, I want to use a default model/fixture.
if a parameter is supplied, then I want to obviously lookup the model using the parameter.
if I then go to a different route, and return to the route without the parameter, I want to use the previously loaded model.
eg:
startup app
/video - shows my default/fixture model
/video/123 - shows model 123
/another-route - shows new route
/video - shows model 123
is this possible?

I ended up using a different solution:
this.resource('video', function() {
this.route('index', {path: '/'});
this.route('item', {path: ':id'});
});
These routes support:
/video - shows my default/fixture model
/video/123 - shows model 123
When the user access to /video, the VideoIndexRoute must redirect to VideoItemRoute without any id.
var VideoIndexRoute = Em.Route.extend({
afterModel: function() {
// this is the tricky part
this.replaceWith('video.item', '');
}
});
Now, the VideoItemRoute must check if there is any model associated, and when it is missing, it should use the default fixtures or a new one.
var VideoItemRoute = Em.Route.extend({
model: function(param) {
if (param.id) {
return this.store.find('video', param.id);
}
},
setupController: function (controller, model) {
if (!model) {
model = this.store.createRecord('video',{
name: 'default Name'
});
// or use fixture...
}
this._super(controller, model);
}
});

There is a clean way to do this, although it is slightly "tricky". The idea is to use a nested route to hold the id, but not render it, instead having the parent route be responsible for rendering it using the render helper. When you do it this way, all the logic can live in VideoChoiceController, and it will be used for displaying either the default video or a specific one. When doing it this way, there is no need to explicitly "remember" the last video, the state machine that the route engine represents does it for you.
App.Router.map(function) {
this.resource('video', function(){
this.route('choice', {path: ':video_id'});
});
});
App.VideoRoute = Ember.Route.extend({
model: function(params) {
return App.get('defaultVideo');
},
setupController: function(controller, model) {
var video_choice = this.controllerFor('video.choice')
// this is necessary if you want, for example,
// to display the name or a link to the default video
// when a specific video is being displayed
controller.set('model', model);
if(Ember.isEmpty(video_choice.get('model'))){
video_choice.set('model', model);
}
}
});
App.VideoChoiceRoute = Ember.Route.extend({
model: function(params) {
return this.get('store').find('video', params.video_id);
},
renderTemplate: function() {
// if you don't override renderTemplate to do nothing,
// everything will still work but you will get an assertion
// error that render should only be used once with a
// singleton controller
}
});
<script type="text/x-handlebars">
<div>
{{outlet}}
</div>
</script>
<script type="text/x-handlebars" data-template-name='video'>
<div> attrributes here always refer to the default video: {{name}} </div>
{{render "video.choice"}}
</script>
<script type="text/x-handlebars" data-template-name='video'>
<div>
attrributes here always refer to specific or last video,
or default if a specific video has never been loaded: {{name}}
</div>
</script>

Definitely, you'll have to do something a little funky, like storing the last video in some global variable, but that's up to you.
http://emberjs.jsbin.com/uhoQozu/1/edit
http://emberjs.jsbin.com/uhoQozu/1#/video
http://emberjs.jsbin.com/uhoQozu/1#/video/32
App.Router.map(function() {
this.resource('videoModel', {path:'/video/:video_id'});
this.resource('video'); // this resource can be accessed at /video
});

Related

What are the right hooks in Ember.js to implement a "loading" state in a route?

While a Position model loads, I want to show a loading/spinner message in the template. I am not being able to achieve that.
{{#if loading}}
<div class="spinner"></div>
{{else}}
<div>Here goes the position</div>
{{/if}}
Given the router
App.Router.map(function() {
this.resource('article', { path: '/:id' }, function() {
this.route('position', { path: '*position_id' });
});
});
my approach in the ArticlePositionRoute is the following:
App.ArticlePositionRoute = Em.Route.extend({
actions: {
loading: function() {
var controller = this.get('controller');
if (controller) controller.set('loading', true);
return false;
}
},
setupController: function(controller, model) {
controller.set('loading', false); // BEFORE _super
this._super(controller, model);
controller.set('loading', false); // AFTER _super
},
model: function(params) {
return promiseThatTakesAWhile(); // slow fetch model
},
afterModel: function(model) {
var self = this;
return promiseThatTakesAWhile().then(function(result) {
model.set('result', result);
self.set('controller.loading', false); // THIS THROWS "Uncaught Error: Property set failed: object in path "controller" could not be found or was destroyed."
});
}
});
In afterModel, the controller is not available, throws Uncaught Error: Property set failed: object in path "controller" could not be found or was destroyed.
In setupController, when the set goes before the _super call, it blows with Error: Assertion Failed: Cannot delegate set('loading', false) to the 'content' property of object proxy <App.ArticlePositionController:ember726>: its 'content' is undefined., if I put it after the _super call, the view already calls didInsertElement and errs because it can't find the right div in the template (basically, it's too late).
What are the proper hooks to set/unset the 'loading' flag? I think I have the set loading true in the right place, but I don't know where to put set loading false.
When the Ember docs say actions: { loading: function(transition, originRoute) { // displayLoadingSpinner(); ..., if that example in the docs was fully developed, what would it look like?
You should use the built in loading route. You may think it needs to exist as a child route of the resource that is taking time to load, but that's incorrect, it needs to be a child route of the parent's resource (which will have already resolved). Or in your case the loading route needs to exist as ArticleLoadingRoute. It will be shown any time any resource/route immediately under it is taking time to load.
Here's an example where I've forced two resources to take a while to load
http://emberjs.jsbin.com/OxIDiVU/465/edit

Model reloading with Ember Data

I'm trying to poll for more data using the documented model.reload() function
App.ModelViewRoute = Ember.Route.extend({
actions: {
reload: function() {
this.get('model').reload();
}
}
});
But i'm getting an error message saying...
undefined is not a function TypeError: undefined is not a function
Is there a better way of doing this, it seems like I cannot access the model in this way from the route?
Here is the router
App.Router.map(function() {
this.route('video', { path: '/videos/:video_id' });
});
Here is the route
App.VideoRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('video', params.video_id);
},
actions: {
reloadModel: function() {
// PROBLEM HERE
// this.get('model').reload();
Ember.Logger.log('reload called!');
}
}
});
Here is the model
App.Video = DS.Model.extend({
title: DS.attr('string'),
status: DS.attr('string')
});
And the templates
<script type="text/x-handlebars" data-template-name="application">
<h1>Testing model reloading</h1>
{{#link-to "video" 1}}view problem{{/link-to}}
{{outlet}}
</script>
<script type="text/x-handlebars" data-template-name="video">
<h1>Video</h1>
<h2>{{title}}</h2>
{{model.status}}
<p><button {{action 'reloadModel'}}>Reload model</button></p>
</script>
I've made a jsbin of the issue here:
http://jsbin.com/wofaj/13/edit?html,js,output
I really can't understand why the reload gives me this error. Any advice would be much appreciated.
Thanks
Since model already exists as a hook on Ember.Route, you cannot get that as a property.
Instead you can do the following:
this.modelFor('video').reload();
Technically you could do this.get('currentModel').reload(); too, but that's undocumented and probably won't be available in the future.
The refresh method of the route would do what you're after
App.VideoRoute = Ember.Route.extend({
model: function(params) {
return this.store.find('video', params.video_id);
},
actions: {
reloadModel: function() {
this.refresh()
}
}
});
API docs
The route model function provides a hook to load your controller data. There is a specific section at the ember guide.
1) If you want to access your content, it would be like:
reload: function() {
this.controller.get('content');
}
2) reload is a method available of ember-data objects. In your example, you are loading a js object ({ id:2, title:"Test video title 2", status:"downloading"}).

Emberjs nothing handled this action

Error : Uncaught Error: Nothing handled the action 'rollDice'. If you did handle the action, this error can be caused by returning true from an action handler in a controller, causing the action to bubble.
I made sure that the method in the controller had the same name as the action.
???
HTML portion
<script type="text/x-handlebars">
{{outlet}}
</script>
<script type="text/x-handlebars" id="index">
{{#linkTo "roll"}}Lets roll dice!{{/linkTo}}
</script>
<script type="text/x-handlebars" id="roll">
<p class="centerme">A Dice Roller.</p>
<p> </p>
<p>Click to play!<br/>
<button id="play" {{action 'rollDice'}}>Roll Dice</button>
</p>
<section id="roll-wrap">Dice stuff</section>
<script>
Controller
DiceRoller.RollController = Ember.ObjectController.extend({
var diceModel = this.get('model');
actions: {
rollDice: function () {
var x=[270,1080,1440,810];
var rand1=Math.floor(Math.random()*4);
var rand2=Math.floor(Math.random()*4);
diceModel.set('rotateXvalue',x[rand1]+"deg");
diceModel.set('rotateYvalue',x[rand2]+"deg");
diceModel.save();
}.property('diceModel.rotateXvalue','diceModel.rotateYvalue')
}
});
Routing
DiceRoller.Router.map(function() {
this.resource("roll");
});
DiceRoller.IndexRoute = Ember.Route.extend({
redirect: function(){
this.transitionTo("roll");
}
});
DiceRoller.DiceRoute = Ember.Route.extend({
model: function() {
return this.store.find('Dice');
}
});
Model
DiceRoller.Dice = DS.Model.extend({
rotateXvalue: DS.attr('string'),
rotateYvalue: DS.attr('string')
});
DiceRoller.Dice.FIXTURES = [
{
rotateXvalue: '40deg',
rotateYvalue: '37deg'
}
];
http://jsbin.com/qosujasi/1/
My JS bin, so far it gives me an error about setting the content of an object proxy.
You've named your controller incorrectly. The correct controller for the roll route would be DiceRoller.RollController.
In the RollController, you should get the model inside the roleDice action and you don't need the list of properties. That's for computed properties, not actions.
DiceRoller.RollController = Ember.ObjectController.extend({
actions: {
rollDice: function () {
var diceModel = this.get('model');
var x=[270,1080,1440,810];
var rand1=Math.floor(Math.random()*4);
var rand2=Math.floor(Math.random()*4);
diceModel.set('rotateXvalue',x[rand1]+"deg");
diceModel.set('rotateYvalue',x[rand2]+"deg");
diceModel.save();
}
}
});
Check out this jsBin.
You need to create the model record to be able to set values on it in your route, like this:
DiceRoller.RollRoute = Ember.ObjectController.extend({
model:function() {
return this.store.createRecord('dice');
}
});
I am fresh new to Ember.js and also struggling, but for me it worked to either move actions: {...} from controller to route:
DiceRoller.DiceRoute = Ember.Route.extend({
model: function() {
return this.store.find('Dice');
},
actions: {...} // move actions here
});
OR to use ApplicationController instead of RollController:
DiceRoller.ApplicationController = Ember.ObjectController.extend({
var diceModel = this.get('model');
actions: {
rollDice: function () {
var x=[270,1080,1440,810];
var rand1=Math.floor(Math.random()*4);
var rand2=Math.floor(Math.random()*4);
diceModel.set('rotateXvalue',x[rand1]+"deg");
diceModel.set('rotateYvalue',x[rand2]+"deg");
diceModel.save();
}.property('diceModel.rotateXvalue','diceModel.rotateYvalue')
}
});
Not saying it is the correct way! Just saying it worked for me - still learning ;-)
When you follow Ember official tutorial, and get to the Templates->Actions chapter, you will probably run into this error on first example because this example uses Components that are explained later. I tried adding action to templates/about.hbs and creating component/about.js with action handler, but these two wouldn't work together. Im guessing the trick is to define hbs file in templates/components/ but before that I got the action working by creating
controllers/about.js like this:
import Ember from 'ember';
export default Ember.Controller.extend({
isBody: false,
actions: {
toggleBody() {
console.log("Look at me go!");
this.toggleProperty('isBody');
}
}
});
This is EmberCli environment, v2.0.0 and they say Controllers and Components will merge into one thing soon, so...

How to make Ember.js load RESTAdapter data before showing view?

I have a app with RestAdapter that takes proper data from server:
App.AFile= DS.Model.extend({
name: DS.attr( 'string' ),
...
App.Store = DS.Store.extend({
revision: 13,
adapter: DS.RESTAdapter.extend({url: "myapi"})
});
And a map like this:
App.Router.map(function() {
this.resource('allfiles', { path: '/allfiles' });
this.resource('onefile', {path: '/onefile/:onefile_id' });
And routes defined like this:
App.allfilesRoute = Ember.Route.extend({
model: function()
{
return App.AFile.find();
}
});
App.onefileRoute = Ember.Route.extend({
model : function(params)
{
return App.AFile.find(params.onefile_id);
}
});
And those templates:
<script type="text/x-handlebars" data-template-name="allfiles">
{{#each controller}}
{{#linkTo onefile this}}open{{/linkTo}}
{{/each}}
</script>
<script type="text/x-handlebars" data-template-name="onefile">
{{name}}
</script>
It works like this: user opens app and it displays allfiles template with a link called open. The link opens a template called onefile and passes the onefile_id to it.
When i open app and click open it works and displays proper name of one file. URL is set to #/onefile/1 where 1 is the onefile_id. So it works fine.
But when i refresh page (#/onefile/1) than name is not displayed anymore.
I've checked what is going on and in onefileRoute model function before i return the id and it occurs that App.AFile has null values for all fields defined. And after app loads those values are filled properly in the App.AFile object but are not displayed on the view.
So it looks like RestAdapter gets data after view display.
How to make it work?
App.onefileRoute = Ember.Route.extend({
model : function(params)
{
var m = App.AFile.find(params.onefile_id);
// at this point m might not be resolved, so name could be empty
return m;
},
afterModel: function(model, transition){
// at this point it should be resolved, what happens here?
console.log(model.get('name'));
}
});
My guess would be your endpoint is returning the wrong information for a model by id.

Loading data into root route of ember router when it's created outside of application.create()

When you want to use classes you created in Em.Application.create() in your router you need to specify the router outside of the application.create. But because the application is automatically initialized the router doesn't route to the / route.
You used to be able to defer the initialization by adding autoinit: false to the application.create. Now you are supposed to use App.deferReadiness() and App.advanceReadiness(). However this doesn't appear to work.
And I can't seem to escape the feeling that you are "supposed" to do it differently.
I added the minimal code to show the problem below. There is also a jsfiddle here
EDIT:
Apparently there is a new router in ember I kinda sorta overlooked that. I've changed the code to the new router, but guess what it still doesn't work :P
window.App = App = Em.Application.create({
ApplicationController: Em.Controller.extend({}),
ApplicationView: Em.View.extend({
template: Em.Handlebars.compile('{{outlet}}'),
}),
ExtendedPatientController: Em.ObjectController.extend({}),
ExtendedPatientView: Em.View.extend({
classNames: ['patient-view', 'extended'],
template: Em.Handlebars.compile('{{name}}')
}),
Patient: Em.Object.extend({
name: undefined,
}),
});
App.Router.map(function (match) {
match('/').to('application', function (match) {
match('/').to('extendedPatient');
})
});
App.deferReadiness();
App.ExtendedPatientRoute = Em.Route.extend({
setupController: function (controller) {
controller.set('', App.Patient.create({
name: "Bert"
}));
},
renderTemplates: function () {
this.render('extendedPatient', {
into: 'application'
});
}
});
App.advanceReadiness();
You're actually doing a lot more work than you need to here.
Here's all the code that you need to make your example work.
Template:
<script type="text/x-handlebars" data-template-name="index">
<div class="patient-view extended">
<p>Name: {{name}}</p>
</div>
</script>
App:
window.App = Em.Application.create();
App.Patient = Em.Object.extend({
name: null
});
App.IndexRoute = Em.Route.extend({
model: function() {
return App.Patient.create({
name: "Bert"
});
}
});
The working fiddle is at: http://jsfiddle.net/NXA2S/23/
Let me explain it a bit:
When you go to /, you are entering the automatic index route. All you need to do to show something on the screen for that route is to implement an index template. The easiest way to do that when you're getting up and running is to put your template in your index.html. Later, you will probably want to use build tools (see my answer here for more information).
You can control what model is displayed in a route's template by overriding the model hook in its route handler. In the case of index, the route handler is App.IndexRoute. In this case, the model is a brand new App.Patient.
You will probably want to implement controllers and events. You can learn more about the router on the Ember.js website
So the new router does solve this problem and does feel a bit shinier.
I finaly found out how to do this basic example this is what happens in the router:
App.Router.map(function (match) {
match('/').to('extendedPatient');
});
This what needs to happen in the views:
ExtendedPatientView: Em.View.extend({
classNames: ['patient-view', 'extended'],
//You need to specify the defaultTemplate because you extend the view class
//instead on initializing it.
defaultTemplate: Em.Handlebars.compile('{{name}}')
}),
You do not have to defer the readiness in the app the new router fixes that.
And in the route you do not need to specify the renderTemplates so the router now looks like:
App.ExtendedPatientRoute = Em.Route.extend({
setupController: function (controller) {
controller.set('content', App.Patient.create({
name: "Bert"
}));
},
});
http://jsfiddle.net/NXA2S/28/

Categories

Resources