Assume we have an Article model as follows:
export default DS.Model.extend({
author: DS.belongsTo('user'),
tagline: DS.attr('string'),
body: DS.attr('string'),
});
Assume also that we have a lot of pages, and on every single page we want a ticker that shows the taglines for brand new articles. Since it's on every page, we load all (new) articles at the application root level and have a component display them:
{{taglines-ticker articles=articles}}
{{output}}
That way we can visit any nested page and see the taglines (without adding the component to every page).
The problem is, we do not want to see the ticker tagline for an article while it's being viewed, but the root-level taglines-ticker has no knowledge of what child route is activated so we cannot simply filter by params.article_id. Is there a clean way to pass that information up to the parent route?
Note:
This is not a duplicate of how to determine active child route in Ember 2?, as it does not involve showing active links with {{link-to}}
Ember is adding a proper router service in 2.15; this exposes information about the current route as well as some methods that allow for checking the state of the router. There is a polyfill for it on older versions of Ember, which might work for you depending on what version you're currently using:
Ember Router Service Polyfill
Based on the RFC that introduced that service, there is an isActive method that can be used to check if a particular route is currently active. Without knowing the code for tagline-ticker it's hard to know exactly how this is used. However, I would imaging that you're iterating over the articles passed in, so you could do something like:
export default Ember.Component.extends({
router: Ember.inject.service(),
articles: undefined,
filteredArticles: computed('articles', 'router.currentRoute', function() {
const router = this.get('router');
return this.get('articles').filter(article => {
// Return `false` if this particular article is active (YMMV based on your code)
return !router.isActive('routeForArticle', article);
});
})
});
Then, you can iterate over filteredArticles in your template instead and you'll only have the ones that are not currently displayed.
You can still use the link-to component to accomplish this, and I think it is an easy way to do it. You aren't sharing your taglines-ticker template, but inside it you must have some sort of list for each article. Make a new tagline-ticker component that is extended from the link-to component, and then use it's activeClass and current-when properties to hide the tagline when the route is current. It doesn't need to be a link, or look like a link at all.
tagline-ticker.js:
export default Ember.LinkComponent.extend({
// div or whatever you want
tagName: 'div',
classNames: ['whatever-you-want'],
// use CSS to make whatever class you put here 'display: none;'
activeClass: 'hide-ticker',
// calculate the particular route that should hide this tag in the template
'current-when': Ember.computed(function() {
return `articles/${this.get('article.id')}`;
}),
init() {
this._super(arguments);
// LinkComponents need a params array with at least one element
this.attrs.params = ['articles.article'];
},
});
tagline-ticker being used in taglines-ticker.hbs:
{{#tagline-ticker}}
Article name
{{/tagline-ticker}}
CSS:
.hide-ticker {
display: none;
}
I tried to extend the LinkComponent, but I ran into several issues and have still not been able to get it to work with current-when. Additionally, if several components need to perform the same logic based on child route, they all need to extend from LinkComponent and perform the same boilerplate stuff just to get it to work.
So, building off of #kumkanillam's comment, I implemented this using a service. It worked perfectly fine, other than the gotcha of having to access the service somewhere in the component in order to observe it.
(See this great question/answer.)
services/current-article.js
export default Ember.Service.extend({
setId(articleId) {
this.set('id', articleId);
},
clearId() {
this.set('id', null);
},
});
routes/article.js
export default Ember.Route.extend({
// Prefer caching currently viewed article ID via service
// rather than localStorage
currentArticle: Ember.inject.service('current-article'),
activate() {
this._super(arguments);
this.get('currentArticle').setId(
this.paramsFor('articles.article').article_id);
},
deactivate() {
this._super(arguments);
this.get('currentArticle').clearId();
},
... model stuff
});
components/taglines-ticker.js
export default Ember.Component.extend({
currentArticle: Ember.inject.service('current-article'),
didReceiveAttrs() {
// The most annoying thing about this approach is that it
// requires accessing the service to be able to observe it
this.get('currentArticle');
},
filteredArticles: computed('currentArticle.id', function() {
const current = this.get('currentArticle.id');
return this.get('articles').filter(a => a.get('id') !== current);
}),
});
UPDATE:
The didReceiveAttrs hook can be eliminated if the service is instead passed through from the controller/parent component.
controllers/application.js
export default Ember.Controller.extend({
currentArticle: Ember.inject.service('current-article'),
});
templates/application.hbs
{{taglines-ticker currentArticle=currentArticle}}
... model stuff
});
components/taglines-ticker.js
export default Ember.Component.extend({
filteredArticles: computed('currentArticle.id', function() {
const current = this.get('currentArticle.id');
return this.get('articles').filter(a => a.get('id') !== current);
}),
});
Related
Views are gone since Ember 2.0.0, but you could do this:
// app/views/application.js or app/application/view.js
import Ember from 'ember';
export default Ember.Component.extend({
classNames: []
});
Since Ember CLI 2.7 this workaround no longer works, looks like the views folder is now being ignored. However, the Ember inspector still shows this for the application view:
view:foobar#view:toplevel
And the HTML is:
<div id="ember420" class="ember-view">
<h2>application</h2>
</div>
It still is a view, there must be a way to customize it.
You can use jQuery (via Ember.$) to manually add the class at some point in the application's startup. This will work:
// in app/routes/application.js
export default Ember.Route.extend({
// ...
activate() {
this._super(...arguments);
let root = Ember.getOwner(this).get('rootElement');
Ember.$(root).addClass('my-custom-class');
},
// ...
});
Since you are asking about the application route, there is no need to clean up after this in the deactivate hook.
This is a specialization of something I've done to facilitate tweaking of styles depending on the current route. Here's an instance initializer that will add a route--xyz class to the root element for the current route hierarchy:
import Ember from 'ember';
function toCssName(routeName) {
return `route--${routeName.dasherize().replace(/\./g, '-')}`;
}
export function initialize(appInstance) {
Ember.Route.reopen({
activate() {
this._super(...arguments);
Ember.$(appInstance.rootElement).addClass(toCssName(this.routeName));
},
deactivate() {
this._super(...arguments);
Ember.$(appInstance.rootElement).removeClass(toCssName(this.routeName));
}
});
}
export default {
name: 'css-route-name',
initialize
};
With this initializer, the root element will always have the class route--application in addition to any other active routes.
Problem can be solved via css:
body > .ember-view {
height: 100%;
}
As jquery selector 'body > .ember-view' should work too
Seems like the best option is to add a component to the application template. What makes it the best solution is that you don't need extra addons or hacks.
More context: https://github.com/emberjs/ember.js/issues/11486
// application template
{{#app-view-substitute}}
{{outlet}}
{{/app-view-substitute}}
Note: Not convenient for a large app where other developers have already made assumptions about the level of nesting of the elements. This adds one more level to every single element and CSS, even when carefully crafted, might break. An alternative is to: https://stackoverflow.com/a/40187809/7852
What I ended up doing is:
// top level component rendered in the application template
didInsertElement: function() {
this._super(...arguments);
this._addIdAndCSSClassToApplicationView();
},
_addIdAndCSSClassToApplicationView: function() {
let root = Ember.getOwner(this).get('rootElement'); // Ember >= 2.3
let applicationView = root.querySelector('.ember-view:first-child');
let idclass = 'myRequiredName';
applicationView.id = idclass;
let classes = applicationView.className;
applicationView.className = classes + ' ' + idclass;
}
Wrapping the outlet is cleaner, this is a hack, but it makes sense in a large app where other have already made assumptions about the levels of nesting in the app.
With Ember 2.2 I had to change the root line to something like this:
let root = Ember.$('.ember-application');
Hey I'm facing a problem with removing a view.
The view is used as navbar
{{view "inner-form-navbar" navbarParams=innerNavObject}}
Where params look like this
innerNavObject: {
...
routeToReturn: 'someroute.index',
...
},
On the navbar there's a small "back" button when it's clicked the parent index route is opened.
It currently works like this:
this.get('controller').transitionToRoute(routeToReturn);
But this won't work in a component and is sketchy anyways. Do i need to somehow inject router to component? Or has anyone gotten a solution for this? The navbar is used in so many places so adding a property to navbarObject to have certain action defined is not a really good solution imo.
Went for this solution :
export default {
name: 'inject-store-into-components',
after: 'store',
initialize: function(container, application) {
application.inject('component', 'store', 'service:store');
application.inject('component', 'router', 'router:main');
}
};
Now i can do
this.get('router').transitionTo('blah')
Well you can try to use a service that provides the routing capabilities and then inject into the component.
There's an addon that seems to do just that - ember-cli-routing-service
Example taken from the link, adapted for you scenario:
export default Ember.Component.extend({
routing: Ember.inject.service(),
someFunc () {
this.get('routing').transitionTo(this.get('innerNavObject'). routeToReturn);
}
});
Having a component control your route/controller is typically bad practice. Instead, you would want to have an action that lives on your route or controller. Your component can then send that action up and your route or controller will catch it (data down, actions up).
In your controller or route, you would have your transition action:
actions: {
transitionFunction(route) {
this.transitionTo(route);
}
}
You would also define the the current route name in your route or controller and pass that to your nav bar component. Controller could then look like:
export default Controller.extend({
application: inject.controller(),
currentRoute: computed('application.currentRouteName', function(){
return get(this, 'application.currentRouteName');
}),
actions: {
transitionFunction(route) {
this.transitionTo(route);
}
}
});
Then call your component and pass the currentRoute CP to it:
{{nav-bar-component currentRoute=currentRoute action='transitionFunction'}}
Then, in your component, you can have a function that finds the parent route from the currentRoute:
export default Component.extend({
click() { // or however you are handling this action
// current route gives us a string that we split by the . and append index
const indexRoute = get(this, currentRoute).split('.')[0] + '.index';
this.sendAction('action', indexRoute);
}
});
Extending a route
Per your comment, you may want to have this across multiple routes or controllers. In that case, create one route and have your others extend from it. Create your route (just as I created the Controller above) with the action. Then import it for routes you need:
import OurCustomRoute from '../routes/yourRouteName';
export default OurCustomRoute.extend({
... // additional code here
});
Then your routes will have access to any actions or properties set on your first route.
In a simple, standard ember.js route ...
this.resource('foo', function () {
this.route('show');
this.route('new');
});
I have a foo template that shows a new button (link to foo/new route) and the foo.bar data of the foo objects (with links to foo/show in a sorted way using the foo controller)
import Ember from 'ember';
export default Ember.Controller.extend({
sortProperties: [ 'bar:desc' ],
sortedList: Ember.computed.sort('model', 'sortProperties')
});
The routes foo/show and foo/new do the obvious, nothing special.
I don't need or want a "real" foo/index route, since all my navigation needs are catered for in the foo route and template. Therefore I want to transitionTo from foo/index whenever I enter that route to the first (as in given by the sorting above) foo/show route (or foo\new if there is nothing to show).
I've learned that "the ember way" is to transitionTo out of the route, but never ever out of the controller.
Now, my problem is, that I get access to the unsorted data very easily (it's the model of my parent route, that I can access via modelFor) and I can sort this "manually", but I'd rather use the already sorted data from my parent's controller. Now, I'm at a bit of a loss which hook to use in the route to be sure that my parent route's controller's computed properties are ready - and it doesn't feel right. So, what would be the ember way to do this?
Edit: Non-working example at http://emberjs.jsbin.com/hujoxigami/2/edit?html,js,output ...
Use the afterModel hook as you mention in one of your comments.
App.FooIndexRoute = Ember.Route.extend({
afterModel: function(posts, transition) {
if (posts.get('length') > 0) {
this.transitionTo('foo.show', posts.get('firstObject'));
}
}
And there is nothing wrong with sorting your model before you return it from the FooRoute. (Even better if you can receive it already sorted from your API/data storage)
App.FooRoute = Ember.Route.extend({
model: function() {
// not fully functional, just illustrative
return [{id:1, name:"Hans", detail:"german"},
{id:3, name:"Henri", detail:"french"},
{id:2, name:"Harry", detail:"english"}].sortBy('name');
}
});
The sorting properties in the controller remain relevant if you want to provide sorting controls for the user.
I have a scenario where there are "jobaids" - jobaids belong to "businessunits", "systemapplications", and "courses" & finally "businessunits" have many of all of the above.
When entering the project you start on the "businessunits" route (#/businessunits), where they are listed as "#link-to"s they link to a show view and when clicked move you to #/businessunits/:businessunits_id
When you arrive on this template there is a an option to view the current "Businessunit"s related "Jobaids" by "systemapplication", "course" or just "show all"
Since the easiest place to start in consideration of the model relationships (and because I'm new with ember), is the "show all" option, and that's where I've started.
So - what I need to do is get the :businessunit_id in...
#/businessunit/:businessunit_id
to be seen by the processing in the
#businessunit/:businessunit_id/jobaids
...to be able to apply to that route, a model consisting of something like
this.store.find('jobaid', {businessunit_id: ID-FROM-PARENT-ROUTE})
so far I have...
/app/router.js
import Ember from 'ember';
import config from './config/environment';
var Router = Ember.Router.extend({
location: config.locationType
});
Router.map(function() {
this.resource("businessunits", function() {
this.route("show", { path: ":businessunit_id" }, function(){
this.resource("jobaids", function() {});
});
});
});
export default Router;
...and the "show all" link leading to the route where I need the value of the parent id is an actual anchor tag with an {{action "getJobaids"}} which is handled with the show in the /app/routes/businessunits.js route
import Ember from 'ember';
export default Ember.Route.extend({
model: function(){
return this.store.find('businessunit');
},
actions: {
getJobaids: function(){
var url = location.href.split('/');
window.unitClicked = url[url.length-1];
this.transitionTo('jobaids');
}
}
});
then when you get to /app/routes/jobaids.js we're getting the needed id from the window...
import Ember from 'ember';
export default Ember.Route.extend({
model: function(){
return this.store.find('jobaid', {businessunit_id: unitClicked});
}
});
this is TERRIBLE but this works enough to get me to the next route (where there are some other problems I have yet to figure out relating to querying the fixture and or adapter) - HOWEVER this is NOT a desirable fix and will never keep state, basically eventually resulting in missing (necessary) variables when linking back
#/businessunits/undefined/jobaids
which as you can imagine breaks everything - so how do I do this in the genuine ember fashion without depending on a hacky use of bare bones js?
I ended up taking the development of this project in the direction of having an actual database back end to query against and let the relationships in ember's data library handle the nested object structure to access the relations returned in the JSON structure.
I have a very basic route setup that allows me to first show "all" records for some object. Then if the user selects a dropdown they can filter this down using a date.
I recently upgraded to RC2 and realized that "needs" has replaced or will soon replace controllerFor.
I'm curious how I can use "needs" in the below situation where I need the nested / inner route for "records.date" to change the content for the parent "records" route when a date is selected.
What is missing from below is that inside the App.RecordsDateRoute I need to change the content of the "records" controller to be a new filter (by date this time) and everything I seem to do just dumps the handlebars template and show nothing -even when I try to use something simple like
this.controllerFor("records").set('content', App.Record.find(new Date(model.loaded)))
from within the setupController method of the RecordsDateRoute
App.Router.map(function(match) {
return this.resource("records", { path: "/" }, function() {
return this.route("date", { path: "/:date_loaded" });
});
});
App.RecordsController = Ember.ArrayController.extend({
selected: 0,
dates: Ember.computed(function() {
return App.Date.find();
}).property()
});
App.RecordsIndexRoute = Ember.Route.extend({
model: function() {
this.controllerFor("records").set("selected", 0);
return App.Record.find();
}
});
App.RecordsDateRoute = Ember.Route.extend({
model: function(params) {
//the controllerFor below seems to be working great ... but what about needs?
this.controllerFor("records").set("selected", params.date_loaded);
return App.Date.create({ loaded: params.date_loaded });
}
});
With rc2, instances of other controllers can be retrieved via "controllers.controllerName", in you case it would be this.get('controllers.records').
The "needs" declaration makes the referencing controller sort of import the reference to the other controller; in your case, the date controller would be:
App.RecordsDateRoute = Ember.Route.extend({
needs: ['records'],
model: function(params) {
this.get("controllers.records").set("selected", params.date_loaded);
return App.Date.create({ loaded: params.date_loaded });
}
});
Regarding App.Record.find(new Date(model.loaded)), find() expects an id or an object whose keys and values will be used to filter the collection of models, but you're giving it a Javascript date.
Did you mean App.Record.find(new App.Date(model.loaded)), or maybe something like App.Record.find({ loaded: model.loaded }) /* assuming it's already a Date */?
There is also an initController(controller, model) method in the route called , maybe you could use that instead of "overloading" the model() method with too many responsibilities. http://emberjs.com/api/classes/Ember.Route.html#method_setupController
I recently upgraded to RC2 and realized that "needs" has replaced or will soon replace controllerFor.
To access another controller from route hooks you should continue to use controllerFor. Controller.needs is for communication between controllers, it replaces the now deprecated use of controllerFor method on controllers. AFAIK there is no plan to deprecate controllerFor on ember Routes.
I'm curious how I can use "needs" in the below situation where I need the nested / inner route for "records.date" to change the content for the parent "records" route when a date is selected.
For this use case it would be best to stick with controllerFor. It is possible to use needs this way, by specifying that App.RecordsDateController needs = ['records'] you could access the records controller via controller.get('controllers.records') from within your route's setupController hook.
What is missing from below is that inside the App.RecordsDateRoute I need to change the content of the "records" controller to be a new filter (by date this time) and everything I seem to do just dumps the handlebars template and show nothing -even when I try to use something simple like this.controllerFor("records").set('content', App.Record.find(new Date(model.loaded))) from within the setupController method of the RecordsDateRoute
App.RecordsDateRoute = Ember.Route.extend({
model: function(params) {
return App.Date.create({ loaded: params.date_loaded });
},
setupController: function(controller, model) {
var recordsController = this.controllerFor("records");
// Moved this from model hook, since here you are 'setting up a controller'
recordsController.set("selected", model.date_loaded);
// Set query based on current route's model
var query = { loaded: model.loaded };
recordsController.set("content", App.Record.find(query));
}
});