Getting a list of routes in Ember.js - javascript

On my main page (index.hbs), I need to display a list of links to each route which matches a given criteria, (e.g. has a certain attribute). So what I need is something like this:
// Define a route with some attribute
App.FirstRoute = Ember.Route.extend({
showOnIndex: true
});
// Get a list of visible routes in the IndexController
visibleRoutes: function() {
var routes = /* How to do this */
return routes.filter(function(route) {
route.get('showOnIndex'));
});
}.property()
The problem is how to get routes. I have found that I can get a list of route names by:
var router = this.get('target');
var names = router.router.recognizer.names;
But I can't find out how to translate them into Route objects. Presumably the Router has the information to do so, but AFAICS it is not publically exposed. Any suggestions?

what about using the container and lookups?
Ember.keys(App.Router.router.recognizer.names).map(function(name) {
return App.__container__.lookup('route:' + name);
}).compact();
I know you access the __container__ which is private. But normally you shouldn't access all the routes anyway.

Here is one way to do it although I think it is a little bit horrible and maybe even dangerous long term since it is using features of Javascript rather than the Ember API. We start by getting the routes through your 'App' object. You can get a list of all the classes it contains using Object.keys(App). Then you can go through each of these, check if it is a route with .superclass which will be Ember.Route if it is a Route. You can then check the showOnIndex property on the prototype of each of these.
var allRoutes = [];
Object.keys(App).forEach(function(key, index, allKeys) {
if (App[key].superclass === Ember.Route && App[key].prototype.showOnIndex === true) {
allRoutes.push(App[key]);
}
});
We now have an array of all the class names of the routes which have showOnIndex of true.
However, you still have the problem of aligning the names from the recognizer with the class names of the Route but since the Ember way is that a route like my/url will map to MyUrlIndexRoute and 'my.url.index' etc then you can split the Route by upper case, remove the last part ('Route'), join them together and convert it all to lower case.
var routesByName = [];
allRoutes.forEach(function(name, index, all) {
var names = name.match(/[A-Z][a-z]+/g);
names.pop();
var routeName = names.join('.').toLowerCase();
routesByName.push(routeName);
});
Then you can just loop through the routesByName in the template and create a {{#link-to}} for each of them.
I think a better way to achieve what you want may be to keep a map of all the routes to their showOnIndex value separately inside your app and just update it for each new one you want to add. It may be better safer long term since who knows how the Ember internals will change in the future which could prevent doing some of the things above.
Note that I checked all this in debugger mode in an Ember app in chrome but haven't run all the code fragments directly so apologies for any mistakes.

In ember v4.9 you can do the following in any component:
export default class myComponent extends Component {
#service router;
#computed
get routes() {
// load RouterService
let router_service = this.router;
// extract routes and format.
let routes = router_service._router._routerMicrolib.recognizer.names;
let detailed_routes = Object.keys(routes)
.filter((key) => {
console.log(key === "error" )
return ! (key.match(/error/) || key.match(/loading/)
|| key.match(/index/) || key.match(/application/))
})
.map((route) => router_service.urlFor(route));
console.log(detailed_routes);
return detailed_routes
}
}

Related

Access a route model from non nested route with Ember js

I am wondering the appropriate way to access a route model from a different non nested route controller.
If I have my routes set up like this: (this works however, not sure if its proper)
App.Router.map(function() {
this.route('admin');
this.route('page1');
}
And the Page 1 route has a model like this:
App.page1Model = {content:'Content of simple model'};
App.Page1Route = Ember.Route.extend({
model(){
return App.page1Model;
});
Then the admin controller wants to access the page1 route, I can only do it like this:
App.AdminController = Ember.Controller.extend({
page1Model:App.page1Model,
Now do stuff with page1Model.....
});
Ive tried to use Ember.inject.controller() however that only works for me when my routes are nested and I want to access Parent controller from child. Is there a way to use that syntax to get what I want, or is there a better way than what im doing?
Thanks
There's an inherent problem with what you're asking for: when the user is on the admin page, they're not on the page1 page, so there's no page1 context. Some questions you might want to ask:
what happens if the user goes to /admin having never gone to /page1?
what happens if the user goes to /page1 then /page2 then /admin?
I can think of two Ember-esque ways of doing what you want:
A Page1ModelService. Here, you create an Ember.Service that holds an instance of Page1Model. You inject the service into route:page1 and route:admin and let them each pull off the instance. Whether they can change which instance of the model is showing is up to you.
Return a Page1Model instance in the model hook for route:application. This route sits above both route:page1 and route:admin, so they can both look up the model as follows:
// route:application
model() {
return App.Page1Model.create();
}
// route:page1
model() {
return this.modelFor('application');
}
I was able to achieve my goal through using registers and injection. Can someone please take a look and let me know if this is 'proper' through Ember standards or if there is a better way ( #James A. Rosen :) )?
OH! If there is a better way to attach the model to the page1 route, please let me know. This worked though I am not sure if i like the .model after create().
JSBIN: http://jsbin.com/tikezoyube/1/edit?html,js,output
JS of that:
var App = Ember.Application.create();
var page1Model = {title:'Old Title'};
var page1ModelFactory = Ember.Object.extend({
model : page1Model
});
App.Router.map(function(){
this.route('page1');
this.route('admin');
});
App.register('model:page1', page1ModelFactory);
App.inject('controller:admin','page1Model','model:page1');
App.Page1Route = Ember.Route.extend({
model(){ return page1ModelFactory.create().model; }
});
App.AdminController = Ember.Controller.extend({
actions:{
updateTitle:function(){
console.log(this.get('page1Model').model.title);
this.get('page1Model').set('model.title','THE NEW TITLE!');
console.log(this.get('page1Model').model.title);
this.transitionToRoute('page1');
}
}
});
Thanks!

Backbone router, view & model cooperation

I have the View containing some button. When that View becomes activated it should take the some URL parameter (in my case -- site id) and set it to the button attribute "data-site_id". There is a router too in the app. But I don't know how to implement it with the best way. Till now I see 3 supposed solutions:
Extract site id from URL hash. URL is build by such a pattern:
"sites/edit/:id(/:tab)": "editSite",
the question is -- may I use here a router itself (and, if yes then how?) or can not, and should parse it with common JS way? Of course, router & view are two different objects and located in different files/scopes.
Save the site_id in model. But I'm not sure how to store it from router. I think I can create an instance of model, set it as variable under router scope and then treat it as usual, something like this:
(function(app, $, config, _) {
var Model = new app.modelName();
var Router = app.Router = Backbone.Router.extend({
routes: {
"": "start",
//....
"sites/edit/:id(/:tab)": "editSite",
//...
},
//....
editSite: function(id, tab){
Model.set('site_id', id);
}
//....
})(window.Application, jQuery, window.chatConfig, _);
after that I can extract the site id from model in any time I want
Assign a data-site_id attribute to the button just from the router. But this doesn't look as a best practice, right?
So what is your advice?
Your second suggested solution makes the most sense, you could then instantiate your view passing that model to the constructor.
See http://backbonejs.org/#View-constructor

Passing an _id and a search query in the string

Using meteor for a test project. Can't figure out how to pass an ID and a search parameter when playing with the sample todo app they have.
For the moment, I have in my iron router:
this.route('team', {
path: '/team/:_id',
onBeforeAction: function() {
this.todosHandle = Meteor.subscribe('todos', this.params._id);
// Then filter mongoDB to search for the text
}});
The thing is, I also want to pass an optional search parameter to search for todos. So something like path: '/team/:_id(/search/:search)?'
Any ideas how to do this?
From your explanation, it sounds like you would like to carefully control which documents are actually published to the client, rather than publishing all of them and narrowing down your result set on the client. In this case, I would suggest first defining a publication on the server like so:
Meteor.publish('todosByTeamIdAndSearch', function(todoTeamId, searchParameter) {
var todosCursor = null;
// Check for teamId and searchParameter existence and set
// todosCursor accordingly. If neither exist, return an empty
// cursor, while returning a subset of documents depending on
// parameter existence.
todosCursor = Todos.find({teamId: todoTeamId, ...}); // pass parameters accordingly
return todosCursor;
});
To read more about defining more granular publications, check this out.
With a publication like the one above defined, you can then setup your route like so:
Router.route('/team/:_id/search/:search', {
name: 'team',
waitOn: function() {
return Meteor.subscribe('todosByTeamIdAndSearch', this.params._id, this.params.search);
},
data: function() {
if(this.ready()) {
// Access your Todos collection like you normally would
var todos = Todos.find({});
}
}
});
As you can see from the example route definition, you can define the path for the route exactly as you would like to see it directly in the call to the Router.route() function and access the parameters directly passed in like in the waitOn route option. Since the publication has been defined like I suggested, you can simply pass those route parameters right to the Meteor.subscribe() function. Then, in the data route option, once you have checked that your subscription is ready, you can access the Todos collection like normal with no further narrowing of the result set if you do not need to do so.
In order to learn more about how to configure your routes, check these two links out: Iron Router Route Parameters and Iron Router Route Options
On the client, you would just use Meteor.subscribe('todos'); in top-level code. 'todos' here doesn't refer to the Collection, it's an arbitrary string. Subscriptions don't care about what route you're on.
On the server, you would have a publish function like this:
Meteor.publish('todos', function() {
if (!Meteor.userId()) return;
// return all todos (you could pass whatever query params)
return Todos({});
});
Then, on your route definition:
Router.route('team', {
path: '/team/:_id',
data: function() {
if (this.params.query) { //if there's a query string
return Todos.find(/* according to the query string */).fetch();
else {
// return all the user's todos
return Todos.find({ uid: this.params._id }).fetch();
}
}
});

Saving with angular $resource.save causes view to redraw/reflow due to collection watcher

I have a model which I load and store using $resource. The model is an aggregate and has nested collections inside, which are binded to an html view using ng-repeat.
Model:
{
someRootField: "blabla",
sectionCollection: [
{
name: "section1"
....
},
{
name: "section2",
....
}
]
}
html:
<div ng-repeat="section in myModel.sectionCollection">
...
</div>
controller:
MyModelResource = $resource(config.api4resource + 'models/:id', {id:'#_id'});
$scope.myModel = MyModelResource.get({id: xxxx});
The problem: when I use $save on this model, it causes a reload/redraw of some portions of the screen (seems not the root fields, but the collection related ones), if some binded elements within the sections are inputs, focus is lost too. I did some debugging and here is what I think is happening.
When I save the model, the results from the POST command mirror the body of the request, and myModel is being repopulated with it. Simple fields in the root of the model are pretty much the same, so the watch() mechanism doesn't detect a change there, however the the objects in the sectionCollection array are different, as they are compared not by their contents but by an equality of the references and fail, this causes the ui controls associated with the collection to be completely reloaded/redrawn.
There is this code in $watchCollectionWatch() in angular:
} else if (isArrayLike(newValue)) {
if (oldValue !== internalArray) {
// we are transitioning from something which was not an array into array.
oldValue = internalArray;
oldLength = oldValue.length = 0;
changeDetected++;
}
newLength = newValue.length;
if (oldLength !== newLength) {
// if lengths do not match we need to trigger change notification
changeDetected++;
oldValue.length = oldLength = newLength;
}
// copy the items to oldValue and look for changes.
for (var i = 0; i < newLength; i++) {
if (oldValue[i] !== newValue[i]) {
changeDetected++;
oldValue[i] = newValue[i];
}
}
}
in my case, I've definitely seen the oldValue[i] = newValue[i] comparison fail, the objects were different. One of the reason is oldValue contained variables prefixed with $ that were referring back to the scopes that were previously created for each item.
The question is, how can I prevent a reflow? Or how can I do it differently to avoid it. Keeping myself two copies of the model, one for $resource and another for binding to view and synchronizing between them manually does not seem right.
Thanks!
You can use $http service to avoid model updates that $save cause:
$scope.save2 = -> $http.get 'blah_new.json'
I used get in example but you can use whatever you need from this list of shortcut methods. And here is a simple example plunk.
Also it's simple to save elemen's focus after rerendering:
$scope.save = ->
active = document.activeElement.getAttribute 'id'
$scope.user1.$save ->
document.getElementById(active).focus()

Dynamically creating outlets

Let's say I have an application with two routes: "/services", and "/services/:service".
When a user visits the :service state, I'd like him to see a list of information pertaining to one particular service. So far, trivial.
When the same user visits /services, however, I'd like to show a list of services, plus the per-service information (for every service). I'd also like to do this without any template duplication.
The obvious answer seemed to be outlets, however it looks like all outlets have to be statically named, and I have a previously-undetermined number of them. They all have unique names, but of course I can't hardcode the names in the template. I'd like to determine the outlet names dynamically, so I can call this.render appropriately in the renderTemplate to fill them in, but I don't know how to do that.
What would you suggest?
UPDATE: It appears that I can handle my particular case using {{template}}, but that doesn't connect up another route, as such. I'd still like to know how to do that; I'll be doing more complex things later, where that would definitely fit better, and there's still duplication of code in the respective controllers.
UPDATE: I eventually made up my own 'dynOutlet' helper, as such:
// Define "dynamic outlets". These are just like normal outlets,
// except they dereference the outlet name.
Handlebars.registerHelper('dynOutlet', function(property, options) {
Ember.assert("Syntax: {{dynOutlet <property>}}", arguments.length === 2);
var context = (options.contexts && options.contexts[0]) || this,
normalized = Ember.Handlebars.normalizePath(
context, property, options.data),
pathRoot = normalized.root,
path = normalized.path,
value = (path === 'this') ? pathRoot : Ember.Handlebars.get(
pathRoot, path, options);
return Ember.Handlebars.helpers.outlet(value, options);
});
I've yet to figure out if it'll work the way I want, though.
I had also the need to define dynamic Outlets. This is the helper i implemented:
Ember.Handlebars.registerHelper('dynamicOutlet', function(property, options) {
var outletSource;
if (property && property.data && property.data.isRenderData) {
options = property;
property = 'main';
}
outletSource = options.data.view;
while (!(outletSource.get('template.isTop'))){
outletSource = outletSource.get('_parentView');
}
options.data.view.set('outletSource', outletSource);
var firstContext = options.contexts.objectAt(0);
options.hash.currentViewBinding = '_view.outletSource._outlets.' + firstContext.get(property);
var controller = App.__container__.lookup("controller:movies");
options.contexts.clear();
options.contexts.addObject(controller);
return Ember.Handlebars.helpers.view.call(controller, Ember.Handlebars.OutletView, options);
});
This is the way i use it in my templates:
<div>
{{title}} <!-- accessing title property of context -->
{{dynamicOutlet outletName}} <!-- accessing the property that should be used as name of the outlet -->
</div>
The helper makes it possible to use properties of models as outlet names.

Categories

Resources