I am preparing SPA website containing hundreds of article-like pages (apart from eCommerce, login etc.). Every article has its own URL. I want to realize it using Angular2.
The only solution I found so far is:
1. to prepare hundreds of Agular2 components, one component for every article...
...with templateUrl pointing to article markup. So I will need hundreds of components similar to:
#core.Component({
selector: 'article-1',
templateUrl: 'article1.html'
})
export class Article1 {}
2. to display an article using AsyncRoute
see Lazy Loading of Route Components in Angular2
#core.Component({
selector: 'article-wrapper',
template: '<router-outlet></router-outlet>'
})
#router.RouteConfig([
new router.AsyncRoute({
path: '/article/:id',
loader: () => {
switch (id) {
case 1: return Article1;
case 2: return Article2;
//... repeat it hundreds of times
}
},
name: 'article'
})
])
class ArticleWrapper { }
In Angular1 there was ngInclude directive, which is missing in Angular2 due to the security issues (see here).
[Edit 1] There is not only problem with the code itself. Problem is also with static nature of this solution. If I need website with sitemap and dynamic page structure - adding a single page needs recompilation of the whole ES6 JavaScript module.
[Edit 2] The concept "markup x html as data" (where markup is not only static HTML but also HTML with active components) is basic concept of whole web (every CMS has its markup data in database). If there does not exist Angular2 solution for it, it denies this basic concept. I believe that there must exist some trick.
All following solutions are tricky. Official Angular team support issue is here.
Thanks to #EricMartinez for pointing me to #alexpods solution:
this.laoder.loadIntoLocation(
toComponent(template, directives),
this.elementRef,
'container'
);
function toComponent(template, directives = []) {
#Component({ selector: 'fake-component' })
#View({ template, directives })
class FakeComponent {}
return FakeComponent;
}
And another similar (from #jpleclerc):
#RouteConfig([
new AsyncRoute({
path: '/article/:id',
component: ArticleComponent,
name: 'article'
})
])
...
#Component({ selector: 'base-article', template: '<div id="here"></div>', ... })
class ArticleComponent {
public constructor(private params: RouteParams, private loader: DynamicComponentLoader, private injector: Injector){
}
ngOnInit() {
var id = this.params.get('id');
#Component({ selector: 'article-' + id, templateUrl: 'article-' + id + '.html' })
class ArticleFakeComponent{}
this.loader.loadAsRoot(
ArticleFakeComponent,
'#here'
injector
);
}
}
A bit different (from #peter-svintsitskyi):
// Faking class declaration by creating new instance each time I need.
var component = new (<Type>Function)();
var annotations = [
new Component({
selector: "foo"
}),
new View({
template: text,
directives: [WordDirective]
})
];
// I know this will not work everywhere
Reflect.defineMetadata("annotations", annotations, component);
// compile the component
this.compiler.compileInHost(<Type>component).then((protoViewRef: ProtoViewRef) => {
this.viewContainer.createHostView(protoViewRef);
});
I am working on my first Ember app and got it to display the way I wanted with the route returning a static JSON object from model():
element: {
name: "First Element",
divisions: [{
name: "First Division",
sets: [{name: "Set 1"},{name: "Set 2"},{name: "Set 3"}]
}, {
name: "Second Division",
sets: [{name: "Set 1"},{name: "Set 2"},{name: "Set 3"}]
}]
}
Now I am trying to refactor to use Ember Data + Mirage and having an awful time.
Here’s my index.js route
export default Ember.Route.extend({
model() {
return this.store.find('element', 1);
},
If I set up my Mirage config.js like this:
this.get('/elements', function() {
return {
elements: [
{
id: 1,
name: 'First Element',
divisions: [1, 2]
}
]
}
});
then I get this error:
Your Ember app tried to GET '/elements/1', but there was no route defined to handle this request.
If I set up my Mirage config.js like this:
this.get('/elements/1', function() {
return {
id: 1,
name: 'First Element',
divisions: [1, 2]
}
});
then I get this error:
22:46:40.883 "Error while processing route: index" "Assertion Failed: normalizeResponse must return a valid JSON API document:
* One or more of the following keys must be present: "data", "errors", "meta"." "EmberError#http://localhost:4200/assets/vendor.js:25582:15
EDIT:
So this isn't a solution to the problem as stated but it got me past this. I gave up on Pretender and started again creating an actual Rails server according to this excellent tutorial: http://emberigniter.com/modern-bridge-ember-and-rails-5-with-json-api/
I was able to do everything I wanted this way and if I ever want to make this a production app, I'm a lot closer.
So the issue is that you aren't actually adhering to the JSON API specification. You can solve this by reading Mirage's page on how to conform.
Essentially you need to either be returning an object at the top level of your JSON response in the case of a GET /foo/1 call. You'll also need to change your "elements" attribute to "data" for GET /foo and that should do the trick. Right now there isn't a simple, re-usable way to do this Mirage out of the box. The best bet right now for both issues is to use the solution presented in this issue.
ember error normalizeResponse must return a valid JSON API document
can be fixed in three ways
return a valid JSONAPI response
see your error message:
normalizeResponse must return a valid JSON API document:
* One or more of the following keys must be present: "data", "errors", "meta".
this.get('/elements/1', function() {
return {
data: {
id: 1,
name: 'First Element',
divisions: [1, 2]
}
}
});
see also https://jsonapi.org/examples/
normalize all responses
// app/serializers/application.js
import EmberData from "ember-data";
export default EmberData.JSONAPISerializer.extend({
normalizeResponse() {
return {
data: this._super(...arguments),
};
},
//normalize(){},
//serialize(){},
// ...
});
problem: error handling
by wrapping all responses in { data: ... }, they never return errors
on errors, the response should be
this.get('/elements/1', function() {
return {
errors: [
{
id: 12345,
title: 'title for error #12345'
}
]
}
});
see also https://jsonapi.org/format/#error-objects
replace JSONAPI with REST
sed -i 's/JSONAPISerializer/RESTSerializer/g' app/serializers/*.js
sed -i 's/JSONAPIAdapter/RESTAdapter/g' app/adapters/*.js
ember docs: adapters and serializers
duplicate: How can ember application be integrated to use with json-server?
I have uses angular translate from here (http://pascalprecht.github.io/angular-translate/) and it's just work fine, but it break my controller's unit test whith Error:
Unexpected request: GET scripts/i18n/locale-en.json
I don't understant why?
I use yeoman and test with karma.
app.js:
'use strict';
(function() {
angular.module('wbApp', ['authService', 'authUserService', 'checkUserDirective', 'ui.bootstrap', 'pascalprecht.translate'])
.config(function($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/login.html',
controller: 'LoginCtrl',
access: {
isFree: true
}
})
.when('/main', {
templateUrl: 'views/main.html',
controller: 'MainCtrl',
access: {
isFree: false
}
})
.otherwise({
redirectTo: '/'
});
});
})();
configTranslate.js:
'use strict';
(function() {
angular.module('wbApp')
.config(['$translateProvider',
function($translateProvider) {
$translateProvider.useStaticFilesLoader({
prefix: 'scripts/i18n/locale-',
suffix: '.json'
});
$translateProvider.preferredLanguage('en');
}]);
})();
karma.conf.js:
files = [
...
'app/bower_components/angular-translate/angular-translate.js',
'app/bower_components/angular-translate-loader-static-files/angular-translate-loader-static-files.js',
...
];
controller test:
'use strict';
describe('Controller: LoginCtrl', function() {
// load the controller's module
beforeEach(module('wbApp'));
var LoginCtrl, scope, location, httpMock, authUser;
// Initialize the controller and a mock scope
beforeEach(inject(function($controller, $rootScope, $location, $httpBackend, AuthUser) {
authUser = AuthUser;
location = $location;
httpMock = $httpBackend;
scope = $rootScope.$new();
LoginCtrl = $controller('LoginCtrl', {
$scope: scope
});
httpMock.when('GET', 'scripts/i18n/locale-en.json').passThrough();
}));
it(...);
...
});
if i add this in test controller, product same error:
httpMock.when('GET', 'scripts/i18n/locale-en.json').respond(200);
httpMock.flush();
or
httpMock.when('GET', 'scripts/i18n/locale-en.json').passThrough();
httpMock.flush();
i find this post How do I test controllers with Angular Translate initialized in App Config? but not helped me :/
I extensively use $httpBackend in my tests and it works fine, but in this case it is ineffective. If I comment the line:
$translateProvider.preferredLanguage('en');
obviously an error, if I add on the runtime (in my controllers)
$translate.uses(local);
I end up with the same error?
So I turn to the translation configuration (configTranslate.js) or at runtime is the same result:
Unexpected request: GET scripts/i18n/locale-en.json
Here is the syntax that I tested, either in a "beforeEach(inject(function(...});"
or in a test "it('...', function() {...});"
httpMock.expectGET('scripts/i18n/locale-en.json');
httpMock.when('GET', 'scripts/i18n/locale-en.json').passThrough();
httpMock.when('GET', 'scripts/i18n/locale-en.json').respond(data);
with at end
httpMock.flush();
I also tried a $ apply
httpMock.expectGET('scripts/i18n/locale-fr.json');
scope.$apply(function(){
$translate.uses('fr');
});
httpMock.flush();
nothing happens, Still this error is driving me crazy ..
If you have any suggestion
it's a known issue, please follow the documentation here: unit testing angular
The solution
Unfortunately, this issue is caused by the design of
angular-translate. To get around these errors, all we can do is to
overwrite our module configuration in our test suite, that it doesn't
use asynchronous loader at all. When there's no asynchronous loader,
there's no XHR and therefore no error.
So how do we overwrite our module configuration at runtime for our
test suite? When instantiating an angular module, we can always apply
a inline function which is executed as configuration function. This
configuration function can be used to overwrite the modules
configuration since we have access to all providers.
Using the $provide provider, we can build a custom loader factory,
which should then be used instead of the static files loader.
beforeEach(module('myApp', function ($provide, $translateProvider) {
$provide.factory('customLoader', function () {
// loader logic goes here
});
$translateProvider.useLoader('customLoader');
}));
Please read more in the above link provided.
We took the approach of ignoring the translation loader in unit tests, rather than being forced to modify each of the spec files.
One way to do it could be by separating the loader configuration to a separate file and then exclude it in karma.
So for example you can create a file app-i18n-loader.js (all other module configurations takes place in a different file):
angular
.module('myApp')
.config(loaderConfig);
loaderConfig.$inject = ['$translateProvider', '$translatePartialLoaderProvider'];
function loaderConfig($translateProvider, $translatePartialLoaderProvider) {
$translateProvider.useLoader('$translatePartialLoader', {
urlTemplate: 'assets/i18n/{part}/{lang}.json'
});
$translatePartialLoaderProvider.addPart('myApp');
}
And in your karma.conf.js exclude the file:
files: [
'bower_components/angular/angular.js',
'bower_components/angular-mocks/angular-mocks.js',
//...
'bower_components/angular-translate/angular-translate.js',
'bower_components/angular-translate-loader-partial/angular-translate-loader-partial.js',
'app/**/*.mdl.js',
'app/**/*.js'
],
exclude: [
'app/app-i18n-loader.js'
],
(Note: Answer edited to a solution that does not require grunt/gulp).
I wanted a solution,
which was not too hacky
which didn't require me to change my actual application code,
which wouldn't interfere with the ability to load additional modules
and most importantly which wouldn't require me to change every
single test.
This is what I ended up with:
// you need to load the 3rd party module first
beforeEach(module('pascalprecht.translate'));
// overwrite useStaticFilesLoader to get rid of request to translation file
beforeEach(module(function ($translateProvider) {
$translateProvider.useStaticFilesLoader = function () {
};
}));
Assuming you don't need the actual translations for your unit tests, this works great. Just put the beforeEach on a global level, preferably in it's own file inside the test folder. It will be executed before every other test then.
I encountered this problem with protractor tests. My solution was to mock translations like this:
angular.module('app')
.config(function ($translateProvider) {
$translateProvider.translations('en', {});
$translateProvider.preferredLanguage('en');
})
Now no language files are downloaded, no strings get translated and I just test against the string keys in specifications:
expect(element(by.css('#title')).getText()).toEqual('TITLE_TEXT');
Try putting to test method:
it('should ...', function() {
httpMock.when('GET', 'scripts/i18n/locale-en.json').respond({});
httpMock.expectGET('scripts/i18n/locale-en.json');
scope.resetForm(); // Action which fires a http request
httpMock.flush(); // Flush must be called after the http request
}
See examples from Angular docs
Please have a look at https://github.com/PascalPrecht/angular-translate/blob/master/test/unit/service/loader-static-files.spec.js as a reference.
In general, I would recommend using a standard translation loader for unit tests (without the hassle of http loadings) which means you can provide the labels with $translateProvider.translations(). Why? Because you do not have to test the remote loading functionality which is part of angular-translate project.
None of the solutions worked for me but I came with these solutions:
1) If you need to use scope.$apply(), or should deal with states in your test (after the $apply() the 2nd approach won't work), override your app's translations with the $translateProvider.translations() method, using a plugin to load JSON files
beforeEach(module(function ($translateProvider) {
$translateProvider.translations('en', readJSON('scripts/i18n/locale-en.json'));
}));
2) If your tested controller depends on the $translate service you can use a plugin to load JSON files and combine it with $httpBackend to load your locale file when angular-translate requests it.
beforeEach(inject(function (_$httpBackend_) {
$httpBackend = _$httpBackend_;
$httpBackend.whenGET('scripts/i18n/locale-en.json').respond(readJSON('scripts/i18n/locale-en.json'));
$httpBackend.flush();
})));
Note this should be below your beforeEach(module('myApp')); or you will get an $injector error.
I made a simple mock service for $translate
$translate=function (translation) {
return {
then: function (callback) {
var translated={};
translation.map(function (transl) {
translated[transl]=transl;
});
return callback(translated);
}
}
};
Usage example here : https://gist.github.com/dam1/5858bdcabb89effca457
I use this pattern.
ApplicationModule set regular angular-translate config.
test code load 'testModule' instead of 'applicationModule'
// application module .js
(function() {
'use strict';
angular
.module('applicationModule', [
'ngAnimate',
'ngResource',
'ui.router',
'pascalprecht.translate'
])
.config(['$stateProvider', '$urlRouterProvider', '$translateProvider', '$translatePartialLoaderProvider', config]);
function config($stateProvider, $urlRouterProvider, $translateProvider, $translatePartialLoaderProvider) {
// set routing ...
$translateProvider.useStaticFilesLoader({
prefix: 'i18n/locale-',
suffix: '.json'
});
$translateProvider.useMessageFormatInterpolation();
$translateProvider.fallbackLanguage(['en']);
$translateProvider
.registerAvailableLanguageKeys(['en', 'ko'], {
'en_US': 'en',
'ko_KR': 'ko'
})
.determinePreferredLanguage(navigator.browserLanguage);
$translateProvider.addInterpolation('$translateMessageFormatInterpolation');
$translateProvider.useSanitizeValueStrategy('escaped');
}
})();
// test.module.js
(function() {
'use strict';
angular
.module('testModule', ['applicationModule'])
.config(['$translateProvider', '$translatePartialLoaderProvider', config])
.run(['$httpBackend', run]);
function config($translateProvider, $translatePartialLoaderProvider) {
$translateProvider.useLoader('$translatePartialLoader', {
urlTemplate: 'i18n/locale-en.json'
});
$translatePartialLoaderProvider.addPart('applicationModule');
}
function run($httpBackend) {
$httpBackend.when('GET', 'i18n/locale-en.json').respond(200);
}
})();
// someDirective.spec.js
describe("a3Dashboard", function() {
beforeEach(module("testModule"))
var element, $scope;
beforeEach(inject(function($compile, $rootScope) {
$scope = $rootScope;
element = angular.element("<div>{{2 + 2}}</div>");
$compile(element)($rootScope)
}))
it('should equal 4', function() {
$scope.$digest();
expect(element.html()).toBe("4");
})
})
Late to the table with this, but I got round this by specifying that Karma simply serve the files as per this entry in karma.conf.js:
files: [
...
{pattern: 'scripts/i18n/*.json', included: false, served: true},
...
]
The 2016 answer for this is to preprocess your json into your tests and properly test translations work on your directives.
I use karma-ng-json2js-preprocessor. Follow all the steps to setup your karma.conf then in your test file, prepend the relevant file as a module, then set that information in $translateProvider.
beforeEach(module('myApp', '/l10n/english-translation.json'));
// Mock translations for this template
beforeEach(module(function($translateProvider, englishTranslation) {
$translateProvider.translations('en_us', englishTranslation);
$translateProvider.useSanitizeValueStrategy(null);
$translateProvider.preferredLanguage('en_us');
}));
Note according to the plugin, it uses your filename to generate a camelcased module name. You can play with the function inside the module's /lib but basically it remove all dashes but KEEPS underscores in a camelCase. So en_us becomes En_us.
You'll also need to tell your test that it is expecting that file as a GEt.
$httpBackend.expect('GET', '/l10n/english-translation.json').respond(200);
I'm playing around with this demo app https://github.com/kagemusha/ember-rails-devise-demo that uses Ember to create a JavaScript implemenation of Devise registration for Rails. The app uses CoffeeScript. I converted it to JavaScript using the compiler on the coffeescript.org website, but now I'm getting these unexpected token and string errors.I'm very inexperienced with Ember and don't know how to make heads or tails of this error messages.
For example, at the top of the settings file, the code does this memoization check (syntax same for both the coffeescript and JavaScript version)
App.urls ||= {}
Which triggers this error when I load the app.
Uncaught SyntaxError: Unexpected token = :3000/assets/settings.js?body=1:1
Same thing happens with the same type of code in the authentications_helper file
App.Authentication ||= {}
Which triggers this error
Uncaught SyntaxError: Unexpected token = authentication_helper.js:1
However, it also obviously affects the rest of the code in the authentication_helper file. When I try to register, I click a link in a template
{{view App.MenuItem href="#/registration" label="Register"}}
which calls the register function in the App.RegistrationRoute
App.RegistrationRoute = Ember.Route.extend({
model: function() {
Ember.Object.create();
},
actions: {
register: function() {
log.info("Registering...");
App.Authentication.register(this);
},
which tries to trigger this register function in the authentication_helper file (the file with the App.Authentication ||= {} at the top), but I'm getting an
Uncaught TypeError: Cannot call method 'register' of undefined
I'm guessing this App.Authentication isn't defined because of the error relating to the unexpecting = in the caching function.
Have I given you enough code to help me make some progress on these errors?
App.Authentication.register = function(route) {
$.ajax({
url: App.urls.register,
type: "POST",
data: {
"user[name]": route.currentModel.name,
"user[email]": route.currentModel.email,
"user[password]": route.currentModel.password,
"user[password_confirmation]": route.currentModel.password_confirmation
},
success: function(data) {
App.currentUser = data.user;
App.LoginStateManager.transitionTo("authenticated");
route.transitionTo('home');
},
error: function(jqXHR, textStatus, errorThrown) {
route.controllerFor('registration').set("errorMsg", "That email/password combo didn't work. Please try again");
}
});
};
App.urls ||= {} is not valid JS, so I suspect that your coffee script has not been translated to JS correctly. You're right that App.Authentication is not defined and is causing your other problems. The JS translation of App.urls ||= {} would be App.urls = App.urls || {}.
I started playing a bit with Sencha Touch.
So I've built a really simple application based on one of the examples just to see how it goes.
Basically it creates a JSON Request which executes a Last.FM web service to get music events near the user's location.
Here's the JSON code:
var makeJSONPRequest = function() {
Ext.util.JSONP.request({
url: 'http://ws.audioscrobbler.com/2.0/',
params: {
method: 'geo.getEvents',
location: 'São+Paulo+-+SP',
format: 'json',
callback: 'callback',
api_key: 'b25b959554ed76058ac220b7b2e0a026'
},
callback: function(result) {
var events = result.data.events;
if (events) {
var html = tpl.applyTemplate(events);
Ext.getCmp('content').update(html);
}
else {
alert('There was an error retrieving the events.');
}
Ext.getCmp('status').setTitle('Events in Sao Paulo, SP');
}
})
};
But every time I try to run it, I get the following exception:
Uncaught SyntaxError: Unexpected token :
Anyone has a clue?
A couple of things. First of all the "Uncaught SyntaxError: Unexpected token :" means the browser javascript engine is complaining about a colon ":" that has been put in the wrong place.
The problem will most likely be in the returned JSON. Since whatever the server returns will be run though the eval("{JSON HTTP RESULT}") function in javascript, the most likely thing is that your problem is in there somewhere.
I've put your code on a little sencha test harness and found a couple of problems with it.
First: My browser was not too happy with the "squiggly ã" in location: 'São+Paulo+-+SP', so I had to change this to location: 'Sao+Paulo,+Brazil', which worked and returned the correct results from the audioscribbler API.
Second: I notice you added a callback: 'callback', line to your request parameters, which changes the nature of the HTTP result and returns the JSON as follows:
callback({ // a function call "callback(" gets added here
"events":{
"event":[
{
"id":"1713341",
"title":"Skank",
"artists":{
"artist":"Skank",
"headliner":"Skank"
},
// blah blah more stuff
"#attr":{
"location":"Sao Paulo, Brazil",
"page":"1",
"totalpages":"1",
"total":"2"
}
}
}) // the object gets wrapped with extra parenthesis here
Instead of doing that I think you should be using the callbackKey: 'callback' that comes with the example in http://dev.sencha.com/deploy/touch/examples/ajax/index.js.
Something like this for example:
Ext.util.JSONP.request({
url: 'http://ws.audioscrobbler.com/2.0/',
params: {
method: 'geo.getEvents',
location: 'Sao+Paulo,+Brazil',
format: 'json',
api_key: 'b25b959554ed76058ac220b7b2e0a026'
},
callbackKey: 'callback',
callback: function(result) {
// Output result to console (Firebug/Chrome/Safari)
console.log(result);
// Handle error logic
if (result.error) {
alert(result.error)
return;
}
// Continue your code
var events = result.data.events;
// ...
}
});
That worked for me so hopefully it'll work for you too. Cherio.