ember: dynamic language in templates. use LanguageController? - ember.js

I have an ember app written in pre 1.0 ember. I want to re-write the app in ember 2.0.
In the old codebase I have an LanguageController that have english defintions of all langauge strings, and I have an ajax call to populate the language controller with strings from another language. I then have that controller available as a global variable available in all templates. An ember 2.0 template may look like this:
<label id="lblHomeNextAppointment">{{EEA.lang.home_next_appointment}}</label>
<div class="appointment">
{{model.nextAppointment.formattedDate}}
{{model.nextAppointment.title}}
</div>
I want to put the dynamic language string where it says EEA.lang.home_next_appointment.
How to structure controllers etc. to achieve this? Maybe use a service? Whats the fundamental difference between a controller and a service?
The old (pre 1.0) controller looks like this:
EEA.LanguageController = Ember.ArrayController.extend({
// Default values for language strings
"header_progress": 'Progress',
"header_planned": 'Planned',
"home_next_appointment": "Next Appointment",
...
init: function() {
this._super();
EEA.lang = this; // Shorter to write EEA.lang, than EEA.router.languageController
},
...

You should use library for internationalization. Ember-I18n does exactly what are you looking for.
In your template you can use {{t}} helper:
<label id="lblHomeNextAppointment">{{t 'lang.home_next_appointment'}}</label>
You just need file with translations:
export default {
lang: {
'home_next_appointment': 'whatever'
}
};
I have an ajax call to populate the language controller with strings
from another language.
With Ember-I18n you can define translations at runtime.
For example, you could setup dynamic loading of required translations in beforeModel() hook of application route:
i18n: Ember.inject.service(),
beforeModel() {
let lang = $.cookie('user-lang');
if (!lang) {
lang = 'pl';
}
this.set('lang', lang);
Ember.$.getJSON('/translations/' + lang + '.json', (json) => {
this.get('i18n').addTranslations(lang, json);
});
},
Of course, with approach above you need to have .json translation files under public translations URL path.

There are few approaches for solving this, however in my opinion it's best to use Ember.Service() for this, for instance by creating Translator service (See Service Guide). I would reason this using following arguments:
You could detach Translator service from app and use it in other one
You could connect such service on-demand when needed (components, controllers, other services etc.)
You could eagerly load such service globally by using object initializer if needed
While you still could do it, coupling Route with controller would not requirement anymore
In this scenario you could do something like this:
Translator service:
export default Ember.Service.extend({
lang: 't1', // For reference
current: {},
init: function() {
this.current = this.translations[this.lang];
},
changeLanguage: function(lang) {
this.set('lang', lang);
this.set('current', this.translations[lang]);
},
translations: {
t1: {a: 'T1:A', b: 'T1:B'},
t2: {a: 'T2:A', b: 'T2:B'}
}
});
Injecting in controller
export default Ember.Controller.extend({
translator: Ember.inject.service()
});
And usage in template:
<p>{{translator.current.a}}</p>
See full gist or live example.
Obviously, one could do better, for instance by splitting into Translator (for setup/configuration) and Dictionary (for exposing selected language translations), so that template use would be something like {{Dictionary.index.greeting}}, but it shows Service-based approach to the problem.

Related

EmberJS 2 Dynamic Routing

I'm trying to setup a way to define routes, their template and model dynamically. (Ember 2.16.0)
So far I have this:
// app/router.js
Router.map(function () {
let routes = [
{route: "page_one", templateName: "default.hbs"},
{route: "page_two", templateName: "default.hbs"},
{route: "page_three", templateName: "custom.hbs"}
];
routes.forEach(function (key) {
this.route(key.route);
}, this);
});
This works, and allows me to load pages if their template file exists.
I want to take this a step further and define the template name here, for example I want them all to use "default.hbs" as well as load the model for them dynamically.
Is this something that's possible in EmberJS? If so which documentation should I be looking at to do so.
I'm aware you can specify templates in their corresponding route file "app/routes/page_one.js" but this wouldn't be dynamic.
By default, Ember's approach to routing and templates is to use built-in templates that handle layout of data that comes from a backend. It's perfectly possible do something like this however and end up with your CMS pages being displayed at /page/:slug:
// app/router.js
Router.map(function () {
this.route('page', { path: '/:post_id' });
});
Doing the above would then allow you to set your page route to handle retrieving the appropriate data from your CMS and then display it in your page.hbs (with the option to wrap it with any additional HTML to make it work well in Ember).
Here's one example of an Ember add-on that does something like that. https://github.com/oskarrough/ember-wordpress It also has a working test app that is designed to work against a Wordpress backend that you can study here: https://github.com/oskarrough/ember-wordpress/tree/master/tests/dummy
There are other approaches you could take as well, but this is probably the simplest one. Does that help?

Clean Ember 1.13+ way of knowing if child route is activated

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);
}),
});

How do I add a CSS class to the application view in Ember 2.7 or higher?

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');

Access current route name from a controller or component

I needs to apply an "active" class to a bootstrap tab depending on the current route name. The route object contains "routeName" but how to I access this from a controller or component?
Use this this.controllerFor('application').get('currentRouteName');
In fact, you don't need to apply active class by yourself. A link-to helper will do it for you.
See here:
{{link-to}} will apply a CSS class name of 'active' when the application's current route matches the supplied routeName. For example, if the application's current route is 'photoGallery.recent' the following use of {{link-to}}:
{{#link-to 'photoGallery.recent'}}
Great Hamster Photos
{{/link-to}}
will result in
<a href="/hamster-photos/this-week" class="active">
Great Hamster Photos
</a>
In the absolutely desperate case, you can look up the router, or the application controller (which exposes a 'currentRouteName' property) via this.container.lookup("router:main") or this.container.lookup("controller:application") from within the component.
If it was a common trend for me, I would make a CurrentRouteService and inject it into my component(s) so that I can mock things more easily in my tests.
There may also be a better answer to come along - but the container.lookup() should knock down your current blocker.
Since Ember 2.15 you can do this through the public Router service.
router: service(),
myRouteName: computed('router.currentRouteName', function () {
return this.get('router.currentRouteName') + 'some modification';
}
https://www.emberjs.com/api/ember/release/classes/RouterService
Which worked really well for me since I wanted something computed off of the current route. The service exposes currentRouteName, currentURL, location, and rootURL.
currentURL has the query params, but you would need to parse them from the URL.
For Ember 2, from a controller you can try :
appController: Ember.inject.controller('application'),
currentRouteName: Ember.computed.reads('appController.currentRouteName')
Then you can pass it to component.
Try this.
export default Ember.Route.extend({
routeName: null,
beforeModel(transition){
//alert(JSON.stringify(transition.targetName) + 'typeof' + typeof transition.targetName);
this.set('routeName', transition.targetName);
},
model(){
// write your logic here to determine which one to set 'active' or pass the routeName to controller or component
}
`
Using insights from #maharaja-santhir's answer, one can think of setting the routeName property on the target controller to use, e.g., in the target's template. This way there's no need for defining the logic in multiple locations and hence code-reusability. Here's an example of how to accomplish that:
// app/routes/application.js
export default Ember.Route.extend({
...
actions: {
willTransition(transition) {
let targetController = this.controllerFor(transition.targetName);
set(targetController, 'currentRouteName', transition.targetName);
return true;
}
}
});
Defining this willTransition action in the application route allows for propagating the current route name to anywhere in the application. Note that the target controller will retain the currentRouteName property setting even after navigating away to another route. This requires manual cleanup, if needed, but it might be acceptable depending on your implementation and use case.

How to create a inject helper for something other than service?

https://github.com/emberjs/ember.js/blob/5fd2d035b30aa9ebfe73de824b3b283ec8e589cc/packages/ember-runtime/lib/system/service.js#L31
In the line I reference above the ember-core team imports this createInjectionHelper and uses it to add a clean/simple api for injecting services like so
App.ApplicationRoute = Ember.Route.extend({
authManager: Ember.inject.service('auth'),
model: function() {
return this.get('authManager').findCurrentUser();
}
});
How can I create something like this myself for a non service?
Your example usage will change slightly from what you have above. We will cover what the injectRepositories does in a little bit.
import injectRepositories from 'app/utils/inject';
export default Ember.Route.extend({
repository: injectRepositories('person'),
model: function() {
var repository = this.get('repository');
return repository.find();
}
});
The initializer can be improved with the following changes:
import registerWithContainer from "ember-cli-auto-register/register";
export function initialize(_, application) {
registerWithContainer("repositories", application);
application.inject("repositories", "store", "store:main");
}
export default {
name: "repositories",
after: "store",
initialize: initialize
};
Let's break down what is happening in each line.
registerWithContainer("repositories", application);
In the line above, we are deferring to the ember-addon ember-cli-auto-register. This addon will take a directory, in this situation, the repositories directory and register each object into the Ember container to able to be accessed via a lookup. They will be inserted as if doing the following:
application.register("repositories:person", PersonRepository);
Then we add a function to do the injection using the ember-addon ember-cli-injection:
// app/utils/inject.js
import inject from "ember-cli-injection/inject";
var injectRepositories = inject("repositories");
export default injectRepositories;
This then allows us the opportunity to use the newly created function above to access these objects with the code below:
import injectRepositories from 'app/utils/inject';
export default Ember.Route.extend({
repository: injectRepositories('person'),
model: function() {
var repository = this.get('repository');
return repository.find();
}
});
Since each object is now in the container, we can look it up and inject at runtime instead of during the initialization of the application. We register the repositories key in the function and this then returns a computed property (see code below from ember-cli-injection). We do this as a computed property to allow lazy loading. The object is not fetched from the container until the property is accessed.
import Ember from 'ember';
var injection = function(key) {
return function(name) {
return Ember.computed(function(propertyName) {
var objectName = name || propertyName;
return this.container.lookup(key + ':' + objectName);
});
};
};
export default injection;
We also allow for a name to passed to the repositories function, for example repository: injectRepositories('person'). This allows you to name your object whatever you would like when injecting it.
If you would like to just name the object the same as the name of the repository injected into the container you can alternatively do person: injectRepositories(). This will pass the person key to the computed property as the propertyName and since the name was left null when injecting, the objectName will instead be person. This matches the API produces similar results but is not the same as that of the Ember.inject.service and Ember.inject.controller API that is available as of Ember 1.10.
I don't think it's their intention for you to use it this way. The standard way is to use App.inject() if you're using plain ember, or do this in an initializer if you're using ember-cli.
In your case:
// Register the service
App.register('service:auth', {
findCurrentUser: function() {
// Do your magic
}
}, { instantiate: false });
App.inject('route', 'auth', 'service:auth');
Then in your model hook you can use this.auth.findCurrentUser();. You can also inject this into controllers and components if you need. Also note, to keep it clean, that you might want to include a separate module instead of defining your auth module in the service registration.
More info here:
http://emberjs.com/guides/understanding-ember/dependency-injection-and-service-lookup/#toc_dependency-injection-with-code-register-inject-code
NOTE
A service is also not a "special" thing. You can inject anything you want into pretty much anything you want using this method.