In one application that I'm working with, the route system need to be integrated with i18n, like the example below:
$routeProvider.when('/:i18n/section', ...);
But I'm facing some issues due to, what I guess it is, the $digest cycle, which doesn't change the i18n param at the runtime.
Other issue that I'm facing is, if the location path is pointed to something like:
http://localhost:9000/section/...
not like:
http://localhost:9000/en/section/...
the i18n path param ends being associated with /section/, which means, on $routeParams service, $routeParams.i18n = 'section';. This is expected, but I need to be able to parse the /:i18n/ param, to avoid this conflicts, and change the URL, concatenating one locale, to contextualize the session, replacing the current route with the new one, i18n-ized, whithout refreshing the view/app automatically, yet selectivelly, because some features only need to be changed, not all.
Also, I've designed one service that evaluates, based on a list of possible language settings and its weights, the language that'll be selected to the current context:
var criteria = {
default: {
tag: 'en',
weight: 10
},
user: {
tag: 'pt',
weight: 20
},
cookie: {
tag: 'zh',
weight: 30
},
url: {
tag: 'ru',
weight: 40
},
runtime: {
tag: 'es',
weight: 50
}
};
function chooseLanguage (languages) {
var deferred = $q.defer();
var weights = [];
var competitors = {};
var choosen = null;
if (defineType(languages) === 'array') {
for (var i = languages.length - 1; i >= 0; i--) {
if (languages[i].tag !== null) {
weights.push(languages[i].weight);
competitors[languages[i].weight] = languages[i];
}
}
choosen = competitors[Math.max.apply(Math, weights)];
} else if (defineType(languages) === 'object') {
choosen = languages;
} else {
return;
}
setRuntimeLanguage(choosen.tag);
deferred.resolve(choosen);
return deferred.promise;
}
Explaining the code above, when angular bootstraps the app, the snippet is executed, selecting which language is defined and if its strong enough to be selected. Other methods are related to this operation, like de detection of the URL param, if there's one logged user, etc, and the process is executed not only on the bootstrap, but on several contexts: $routeChangeStart event, when the user autenticates its session, switching the languages on a select box, and so on.
So, in resume, i need to be able to:
Parse the URL and apply the locale param properly, if its not informed initialy;
Change the URL i18n param during the runtime, whithout reloading the whole view/app;
Deal with the language changes correctly, which means, if my approach based on weights isn't the better way to go, if you suggest me something else.
Edit 1:
A $watcher doesn't do the trick because the app needs the correct locale in every path, even before it instantiates all the elements. AngularJS is used in every step of this check, but if there's any clue to do this outside, before Angular instantiates, we can discuss about it.
For now, I'm using the accepted answer below, with a solution that I developed, but it has to be improved.
I've ended up doing a kind of preliminary parse on the URL. Using only ngRoute (ui-router wasn't an option...), I check if the path matches with the restrictions, if not, a redirect is triggered, defining correctly the path.
Below, follows a snippet of the solution, for the primary route, and a simple subsequent example, due to the quantity of the routes on the app, and their specific data, that doesn't belongs to the basis idea:
$routeProvider
.otherwise({
redirectTo: function () {
return '/404/';
}
})
.when('/:i18n/', {
redirectPath: '/:i18n/',
templateUrl: 'home.html',
controller: 'HomeCtrl',
resolve: {
i18n: [
'$location',
'$route',
'i18nService',
function ($location, $route, i18nService) {
var params = $route.current.params;
return i18nService.init(params.i18n)
.then(null, function (fallback) {
if (params.i18n === '404') {
return $location.path('/' + params.i18n + '/404/').search({}).replace();
}
$location.path('/' + fallback).search({}).replace();
});
}
],
data: [
'dataService',
function (dataService) {
return dataService.resolve();
}
]
}
})
.when('/:i18n/search/:search/', {
redirectPath: '/:i18n/search/:search/',
templateUrl: 'search.html',
controller: 'SearchCtrl',
resolve: {
i18n: [
'$location',
'$route',
'i18nService',
function ($location, $route, i18nService) {
var params = $route.current.params;
return i18nService.init(params.i18n)
.then(null, function (fallback) {
$location.path('/' + fallback + '/search/').search({
search: params.search
}).replace();
});
}
],
data: [
'$route',
'searchService',
function ($route, searchService) {
var params = $route.current.params;
return searchService.resolve({
'search': params.search
});
}
]
}
});
The redirectPath property is used in case of you need the pattern for that route, because ngRoute keeps one copy for private use, but doesn't give access to it with $route public properties.
Due to the required parse before any app request (except the i18nService, which loads the locale list and triggers angular-translate to load the translations), this methods causes several redirects, which leads to a significant delay on the instantiation.
If any improvements are possible, I'll thanks for, also, if I found a better way to do it, I'll update this answer with it.
Related
I inherited a mess. And I need a quick fix for some code that needs to be completely rewritten - but not enough time to do a complete rewrite yet.
Orig developer created an index.js file that has all kinds of global functions used all through an AngularJS/Ionic project. Quite often I am finding AngularJS functions in specific controllers actually passing $scope/$q out to the standard JS functions in the index.js file - its a mess.
One of these global functions is window.FirebasePlugin.onNotificationOpen - which is watching for any inbound pushNotification/messages. The original developer was simply using an "alert" for inbound messages with a data payload. We are now trying to redirect those messages to open up in proper popover or modal windows that belong in the controller in two specific pages.
So the question is, in the global watch JS function, how can I direct
the 'data/payload' to a controller to process in a proper $scope?
I have modified the .state to accept parameters - and then all the subsequent code is in place to pass the $stateParams into specific controllers:
.state('tab.clubs', {
cache: true,
url: '/clubs',
views: {
'tab-clubs': {
templateUrl: 'templates/tab-clubs.html',
controller: 'ClubCtrl',
params: {
'pushAction' : 0,
'pushCode' : 'default'
}
}
}
})
The problem I am having is trying to figure out how to pass URL data from standard JS into the AngularJS .state
The global JS function:
window.FirebasePlugin.onNotificationOpen(function(payload) {
// if there is a payload it will be in payload object
if (payload.action == 1) {
window.location("/clubs/", payload) ; // process message in ClubCtrl
} else if (payload.action == 2) {
window.location("/map/", payload) ; // process message in MapCtrl
}
}, function(error) {
console.error(error);
}) ;
But this method fails.
If your not going to use angulars router to navigate to the page you will need to declare the params in the URL somehow. You can use path params by doing something like /clubs/:pushAction/:pushCode or url params with something like /clubs?pushAction&pushCode
Example:
.state('tab.clubs', {
cache: true,
url: '/clubs/:pushAction/:pushCode',
views: {
'tab-clubs': {
templateUrl: 'templates/tab-clubs.html',
controller: 'ClubCtrl',
params: {
'pushAction' : 0,
'pushCode' : 'default'
}
}
}
})
Then navigate it with
location.href = `/clubs/${payload.action}/${payload.code}`
Additionally if you have alot of unknown params you could also pass in the whole payload as base64 encoded json. I wouldnt recommend this but it is... a solution
.state('tab.clubs', {
cache: true,
url: '/clubs?state',
views: {
'tab-clubs': {
templateUrl: 'templates/tab-clubs.html',
controller: 'ClubCtrl',
params: {
'state' : 0,
}
}
}
})
window.location(`/clubs?state=${btoa(JSON.stringify(payload))}`
Then in your controller reverse that operation
class ClubCtrl {
...
// parses the state out of the state params and gives you the full payload
parseState($stateParams) {
return JSON.parse(atob($stateParams.state));
}
...
}
It would give you all the payload, but its pretty gross
I currently Angular 1.6 and angular-ui/ui-router
I have a problem with dynamic url which I don't know a limit of parameter
example
www.example.com/name/hello/address/telephone/postcode/city/ .... (n)
or
www.example.com/name/hello/school/age/weight/height/...(n)
or
www.example.com/name/hello/friends/family/age/address
from the example url I need to using ui-route to manage that url and get value parameters from url and just implement only one state.
expected result
let result = [hello,address,telephone,postcode,city, .... (n)];
example route (*only one state handle this case)
.state('person', {
url: '/name/:param1/:param2', (which I want to dynamic)
component: 'person',
resolve: {
search:($transition$) {
return $transition$.params();
}
}
})
Please advice :)
Update :
I found a good solution from this link
Recursive ui router nested views
you can make a function which takes your array input and return an state object.
function generateState(array){
var url = a.reduce(function(url, item){ return url+":"+item+"/" }, "/name/");
return {
url: url,
component: 'person',
resolve: {
search:($transition$)=> {
return $transition$.params();
}
}
}
}
after which you can dynamically register the state using $stateProvider
I have attached data attribute in each .state to identify the user (authenticated or public) as following (one state example)
$stateProvider
.state('admin-panel.public.home', {
url: '/p',
templateUrl: 'app/public/home.tmpl.html',
controller: 'PublicHomeController',
controllerAs: 'ctrl',
data: {
requireLogin: false
}
});
I need to use the some state for both of user (authenticated and public) as an example
.state('403', {
url: '/403',
templateUrl: '403.tmpl.html',
controller: function($scope, $state, APP, Auth) {
$scope.app = APP;
$scope.goHome = function() {
if(Auth.isAuthenticated()){
$scope.requireLogin = true;
$state.go('admin-panel.default.home');
}
else{
$scope.requireLogin = false;
$state.go('admin-panel.public.home');
}
};
},
data: {
requireLogin: $scope.requireLogin
}
})
Here when the authenticated user access this state I need to pass the true value to requireLogin: true as well when public user access this state I need to pass the false value as requireLogin: false. I checked the current user status in the controller as above. How can I bind the $scope.requireLogin to data attribute?
Anyone in expert of ui-router please tell a way to solve???
You can solve your problem in a very cleaner way. Let's start with a global controller example GlobalCtrl which is added to the body or html tag like ng-controller="GlobalCtrl.
Doing this will enable us to keep the scope of this GlobalCtrl throughout your single page Angular app (as you are using ui-router) and we can avoid the usage of $rootScope (actually mimicking the usage of $rootScope).
Now, inside your GlobalCtrl define something like this:
// Using an object to avoid the scope inheritance problem of Angular
// https://github.com/angular/angular.js/wiki/Understanding-Scopes
$scope.globalData = {};
// Will be called everytime before you start navigating to any state
$scope.$on('$stateChangeStart', function(event, toState, toParams) {
$scope.globalData.requireLogin = false;
var statesToLoginCheck = ['403', 'foo', 'bar']; // and other states on which you want to check if user is logged in or not
// If the current state on which we are navingating is allowed to check for login
if (statesToLoginCheck.indexOf(toState.name) > -1) {
if (Auth.isAuthenticated()) {
$scope.globalData.requireLogin = true;
$state.go('admin-panel.default.home');
} else {
$scope.globalData.requireLogin = false;
$state.go('admin-panel.public.home');
}
event.preventDefault();
return;
}
});
Now, since $scope of GlobalCtrl is in body or html then every state or directive will inherit the scope of this GlobalCtrl and then you simply have to check in your any controller of variable $scope.globalData.requireLogin.
I'm trying to find out what the best practice is for dynamic content in an angular app. I have an array that contains a set of phone numbers and i want to create a page/view base on the country of the phone numbers. So all German phone numbers should be listed under #/app/numbers/germany for example.
The array that holds the phone numbers will be fetched at page load - so it's ready for use and filtration.
Normally I would create a filtration based on the url parameters like ?country=Germany, but I don't suppose this is the right way to do it.
I use a filter for removing duplicates from the view in order to create a list over all countries (which should hold the link to the numbers under each country):
.filter('unique', function(){
return function(collection, keynam){
var output = [];
keys = [];
angular.forEach(collection, function(item){
var key = item[keyname];
if(keys.indexOf(key) === -1) {
keys.push(key);
output.push(item);
}
});
return output;
};
})
So basically I want to know what the best practice in this scenario is - using (dynamic) routes, load data based on URL or something entirely different?
Solution
I've found a solution by using $stateParams from the routing. My dynamic state:
.state('app.single', {
url: '/numbers/:country',
views: {
'menuContent': {
templateUrl: 'templates/country.html',
controller: 'CountryCtrl'
}
}
})
In the controller I assign the $stateParams to a scope variable like this:
.controller('CountryCtrl', function($scope, $stateParams, Numbers) {
//Load Firbase ref and get data
$scope.numbers = Numbers;
$scope.currentCountry = $stateParams.country;
})
And finally in the view I use $scope.currentCountry to filter out the numbers that match the current state/route:
ng-repeat="item in numbers | filter:{Country:currentCountry}"
The great thing about this is that i don't need to load data more than once, but I can rely on controller logic.
If you have a service (PhoneNumberSvc) with the function "getNumbers(country)" that filters the phone numbers by country:
app.module('appName')
.service('PhoneNumberSvc', [
'$http',
function ( $http ) {
this.getNumbers = function ( country ) {
return $http.get('numbers.json')
.then(function ( response ) {
var return_data = [];
angular.forEach(response.data, function ( item ) {
if ( item.country = country ) {
return_data.push(item);
}
});
return return_data;
});
};
}
]);
Then you could do something like this in your config:
$routeProvider.when('/app/numbers/:country', {
templateUrl: 'yourview.html',
controller: 'YourController',
resolve: {
data: function ( $route, PhoneNumberSvc ) {
var country = $route.current.params.country;
return PhoneNumberSvc.getNumbers(country);
}
}
});
Then, in your controller, be sure to inject the parameter "data":
angular.module('appName')
.controller('YourController', [
'$scope',
'data',
function ( $scope, data ) {
$scope.numbers = data;
}
]);
I would load only the data you need:
At first let's declare an angular route
$routeProvider
.when('/numbers/:country',{
templateUrl : '/foo/bar/baz/numbers.html',
controller : 'NumbersController'
})
Then in the NumbersController you can use the country parameter to query the backend and fetch the array of numbers related to the requested country
app.controller("NumbersController",function($scope,$http,$routeParams){
$http({
url: "some_url",
method: "GET",
params: {country: $routeParams.country}
}).success(function(response){
//Handle the data here
}).error(function(response){
//Handle errors here
});
});
The first benefit of this approach is that you don't have to load the entire array, but only what you need.
Furthermore you don't have to do filtering and parsing and other more complex operations.
There is not one single way to solve your problem, but this is a common approach in the angularJS world
I'm trying to write a factory which exposes a simple users API. I'm new to AngularJS and I'm a bit confused about factories and how to use them. I've seen other topics but none that are a good match to my use case.
For the sake of simplicity, the only functionality I'd like to achieve is getting all users in an array and then pass them to a controller through the injected factory.
I stored the users in a json file (for now I only want to read that file, without modifying the data)
users.json:
[
{
"id": 1,
"name": "user1",
"email": "a#b.c"
},
{
"id": 2,
"name": "user2",
"email": "b#b.c"
}
]
The factory I'm trying to write should be something like this:
UsersFactory:
app.factory('usersFactory', ['$http', function ($http) {
return {
getAllUsers: function() {
return $http.get('users.json').then(
function(result) {
return result.data;
},
function(error) {
console.log(error);
}
);
}
};
}]);
And finally, the controller call would be like this:
UsersController
app.controller('UsersCtrl', ['$scope', 'usersFactory', function($scope, usersFactory){
usersFactory.getAllUsers().then(function (result) {
$scope.users = result;
});
}]);
I've searched the web and it seems like it is not really a good practice to use factories this way, and if I'd like to achieve some more functionality like adding/removing a new user to/from the data source, or somehow store the array within the factory, that wouldn't be the way to do it. I've seen some places where the use of the factory is something like new UsersFactory().
What would be the correct way to use factories when trying to consume APIs?
Is it possible to initialize the factory with an object containing the $http.get() result and then use it from the controller this way?
var usersFactory = new UsersFactory(); // at this point the factory should already contain the data consumed by the API
usersFactory.someMoreFunctionality();
I don't see anything wrong with your factory. If I understand correctly you want to add functionality. A few small changes would make this possible. Here's what I'd do (note that calling getAllUsers wipes out any changes):
app.factory('usersFactory', ['$http', function ($http) {
var users = [];
return {
getAllUsers: function() {
return $http.get('users.json').then(
function(result) {
users = result.data;
return users;
},
function(error) {
users = [];
console.log(error);
}
);
},
add: function(user) {
users.push(user);
},
remove: function(user) {
for(var i = 0; i < users.length; i++) {
if(users[i].id === user.id) { // use whatever you want to determine equality
users.splice(i, 1);
return;
}
}
}
};
}]);
Typically the add and remove calls would be http requests (but that's not what you're asking for in the question). If the request succeeds you know that your UI can add/remove the user from the view.
I like my API factories to return objects instead of only one endpoint:
app.factory('usersFactory', ['$http', function ($http) {
return {
getAllUsers: getAllUsers,
getUser: getUser,
updateUser: updateUser
};
function getAllUsers() {
return $http.get('users.json');
}
function getUser() {
...
}
function updateUser() {
...
}
}]);
That way if you have any other user-related endpoints you can consume them all in one factory. Also, my preference is to just return the $http promise directory and consume the then() in the controller or where ever you're injecting the factory.
I'm really a fan of route resolve promises. Here is John Papa's example. I will explain afterwards how to apply this to what you're doing:
// route-config.js
angular
.module('app')
.config(config);
function config($routeProvider) {
$routeProvider
.when('/avengers', {
templateUrl: 'avengers.html',
controller: 'Avengers',
controllerAs: 'vm',
resolve: {
moviesPrepService: moviesPrepService
}
});
}
function moviesPrepService(movieService) {
return movieService.getMovies();
}
// avengers.js
angular
.module('app')
.controller('Avengers', Avengers);
Avengers.$inject = ['moviesPrepService'];
function Avengers(moviesPrepService) {
var vm = this;
vm.movies = moviesPrepService.movies;
}
Basically, before your route loads, you get the request data you need (in your case, your "users" JSON.) You have several options from here... You can store all that data in a Users factory (by the way, your factory looks fine), and then in your controller, just call Users.getAll, which can just return the array of users. Or, you can just pass in users from the route resolve promise, much like John Papa does in his example. I can't do it as much justice as the article he wrote, so I would seriously recommend reading it. It is a very elegant approach, IMHO.
I typically use a factory something like this:
.factory('usersFactory', ['$resource',
function($resource){
return $resource('http://someresource.com/users.json', {}, {
query: {
method:'GET',
isArray:true
}
})
}])
Which you could call with:
usersFactory.query();
As this is a promise you can still use the .then method with it too
$http is a promise that means you have to check whether your get call worked or not.
so try to implement this type of architecture in your controller
$http.get('users.json')
.success(function(response) {
// if the call succeed
$scope.users = result;
})
.error(function(){console.log("error");})
.then(function(){
//anything you want to do after the call
});