Trying to create a possibility for admins to change translations on
the current page without editing language files directly.
I'm using angular-translate and I want to catch all translated texts on a current page before interpolation. What I need:
translationId: original-value-without-interpolation
First I changed the translate filter and wrote a custom interpolator to return an object instead of just the interpolated value.
interpolate: function (string, interpolateParams) {
return {
string: string,
interpolateParams: interpolateParams,
translated: $translateDefaultInterpolation.interpolate(string, interpolateParams)
};
}
Then inside the translate filter I was able to access both the translationId and original-value.
Soon after that I remembered that I had forgotten everything about directives and who knows what else I might have forgotten that use the same interpolator.
Now it feels like it would be safer to find an alternative where I can catch the translationId and the original-value without editing filters and directives.
Is there perhaps a function that I can hook myself to that I haven't found yet?
Bear in mind that I only want to catch the translations on the current page and I need to get them before {{values}} are replaced.
angular 1.2.28
angular-translate 1.5.2
Edit: Apparently I can access to the translations table $translateProvider.translations() where I can get values before interpolation. Then I would only need to track down all the translations used on the current page. (Edit2: Unable to access the $translationTable at runtime)
I found a solution.
First thing I did was to create a custom loader. Which is based on the default one but it also stores all the loaded translations into an object. Which is later passed on.
Secondly I created a translate filter and directive. Which are also pretty much based on the provided one put with an addition of:
Filter
angular.module('app')
.filter('translate', ['$parse', '$translate', 'TranslationsHandler', function ($parse, $translate, TranslationsHandler) {
var translateFilter = function (translationId, interpolateParams, interpolation) {
if ( ! angular.isObject(interpolateParams)) {
interpolateParams = $parse(interpolateParams)(this);
}
TranslationsHandler.addViewTranslation($translate.use(), translationId);
return $translate.instant(translationId, interpolateParams, interpolation);
};
// Since AngularJS 1.3, filters which are not stateless (depending at the scope)
// have to explicit define this behavior.
translateFilter.$stateful = true;
return translateFilter;
}]);
Directive
angular.module('app')
.directive('translate', ['TranslationsHandler', '$translate', function (TranslationsHandler, $translate) {
return {
link: function (scope, el, attrs) {
TranslationsHandler.addViewTranslation($translate.use(), attrs.translate);
}
};
}]);
Both of these have the addition of TranslationsHandler.addViewTranslation
Since I mentioned that I'm trying to make it possible to change translations on page the translations object looks like this:
var stuff = {
file: {}, // Data loaded from translation files
server: {}, // Already changed & saved translations but not yet merged with files
changed: {}, // Currently changed translations
current: {}, // Current view translations
merged: {} // All together in this order: file + server + changed
}
// When in translation mode, the merged array is the one passed to angular-translate as the translation table
// Also on state change I clear the current object so that I can fill that up again
// And I use $injector.get('$translate').refresh() this after every blur event on the input to change the translation. This way I am able to display the change on the page.
Related
I have a simple AngularJS webpage, where a variable acts as a filter. That variable can be given in the URL as, e.g., http://example.org/?filter=foobar. That variable can also be given/selected in a form on the page.
What I want to do is, when the page is already loaded and in the url bar the filter term is changed, for the filter variable to be updated (and so the active filter changed) without the page reloading.
This was working for me before, but a recent update and change to html5 mode (for different reasons) seems to have broken this. Now the page is reloaded.
What I did, and still do, is add watchers:
$scope.$watch(
function () { return $location.search().filter },
function(filter) { $scope.filter = filter; }
);
$scope.$watch(
'filter',
function(filter) { $location.search('filter', filter); }
);
To be honest, I wrote that code based on examples, so I may not have full understanding.
The other similar questions on SO do not provide answers to my question; they use $state or ngRoute, which I don't and it worked before without those things.
I am trying to make an Angular 1.5 directive work within an ng-repeat for a table. A few things I need this to do (and please, if I am going at this the completely wrong way, let me know):
1) Update the html whenever new results are provided. Most importantly, the link function gets called once, with an empty result, and never called again. So, my table is blank. Always.
2) Inside the 'template' of the directive, I reference the controller's name and a method which is type coupling. Is the 'right' way to do this?
Here's my code snippets...
module-a.view.html:
<table>
<tr ng-repeat="(ii,result) in moduleACtrl.results">
<td>This is a normal TD</td>
<td-result result="{{result}}" ctrlName="moduleACtrl"></td-result>
</tr>
</table>
UPDATED
common.td-result.directive.js
(function(){
'use strict';
/* global angular */// ESLINT
angular.module('common').directive('tdResult',Directive);
Directive.$inject = ['$compile'];
function Directive($compile){
return {
restrict : 'E',
scope : {
result : '#',
ctrlName : '#'
},
template: '<td>{{result.prop1}}</td>\
<td>{{result.prop2}}</td>\
<td>{{result.prop3}}</td>\
<td><div ng-click="ctrlName.doSomething()">Something Goes Here {{ctrlName}}</div></td>'
};
}
})();
After reading a number of related questions on StackOverflow, and re-reviewing Angular's Directives documentation, I cannot figure out how to do what I want.
The overall context that got me here is that I have an OpenLayers 3 map, displaying result data from a query. I have the result data on the map interactively displaying result data when you click on it, like this: http://openlayers.org/en/latest/examples/getfeatureinfo-image.html
The difference though, is that I am using Angular 1.5. I have a ModuleA with a controller and view that house the map. I have another ModuleCommon that houses a service that handles creation of the map object and hands it to ModuleA.controller (or any controller who wants the map service). So, now, I have to create an interface for the map service that can generically handle displaying data to any client who wants to render said data.
I decided this was all a bad idea and that I need to step way back. First, the map object should not be responsible for displaying the data. The map is responsible for handling the 'singleclick' event and getting me a feature.
So, I decided that the controller, which instantiates the map object via the map service, will simply be responsible for creating and sending a callback to the map object so the map object knows what it's client wants on a singleclick. The getFeature is the map object's responsibility, and what to do with the feature's id is the client's (i.e. controller's) responsibility.
Whew!
Map code:
map.on('singleclick',function(event){
map.forEachFeatureAtPixel(event.pixel,function(feature){
callback(feature.getId());
});
});
Controller Code:
...
function mapCallback(someId){
var elem = angular.element('#info');
if ( someId === undefined ) {
elem.slideUp(); // Hide element
} else {
for (var data, ii = 0, len = vm.dataList.length; ii < len; ii++) {
data = vm.dataList[ii];
if ( someId === data.property ) {
vm.currentData = data;
$scope.$apply();
elem.slideDown(); // Show element
break;
} else {
elem.slideUp(); // Hide element
}
}
}
}
...
1) The Map is de-coupled from the data pertaining to the feature.
2) The Controller controls what happens when the map selects its data.
3) The HTML stays in the Controller's associated Component's templateUrl where it belongs!
I have 4 controllers in my Angular 1.x app:
houseController
apartmentController
addressController
contactInfoController
With addressController & contactInfoController I update the address and contact info of a house or an apartment.
I work in the following way:
<div ng-controller="houseController" ng-init="getHouseInformation()>
{{house.contact_info.email}}
{{house.contact_info.mobile_phone_number}}
<a ng-controller="contactInfoController" ng-click="showContactInfoEdit(house.contact_info.id)">Edit</a>
</div>
When the update of the contact information is successfull within the contactInfoController, I want to update the house information getHouseInformation() in the houseController (read: make a new call to the API to get the updated information).
Since I have more than one (and in the future even more) house/apartment/... controllers, I need to have a solid way on how to 'refresh' the scope of these apartments, houses, ... on the fly.
What would be the best solution in my case?
Edit:
My scopes look like this:
// in houseController
$scope.house = {
id : 1,
title : "House title",
contact_info : {
email: '',
mobile_phone_number : ''
}
}
// in apartmentController
$scope.apartment = {
id : 1,
title : "Apartment title",
contact_info : {
email: '',
mobile_phone_number : ''
}
}
// in contactInfoController
$scope.contact_info : {
email: '',
mobile_phone_number : ''
}
So when updating the contact information scope, I'm not directly changing the house scope... Hope this helps.
I disagree with the other advice you got here - directives help you intelligently manipulate the DOM with your data but not necessarily share it between scopes, which is what you're asking about. As I wrote to you on Slack, what you seem to be looking for is a Service which will contain all your data (either declared in it or linked to an external file or API), and then injected into every controller that needs access to that data. That's one of the main uses for services!
For posterity, here's what I wrote to you on Slack:
"...You’re currently creating unrelated objects that don’t communicate - why should they? They’re in different scopes. You’re actually not getting any use out of Angular and could use vanilla Javascript for that:)
The idea is to use persistent data that is shared across your web app. $scope.anything will never communicate outside its scope unless you bind it to something outside the scope, like a service. Whether the service draws data from an external API or really just an object model that lies outside of Angular, (on your server/file structure, for example), or even defined within the service itself doesn’t matter (although the latter is far from a best practice) - but you need something persistent outside of the local scopes that's shared between your controllers/views.
Then there are a few ways to connect the dots. A very common (and probably the best) design pattern is to create a service (NOT directive! I don’t know why they gave you that advice) that encapsulates that data. So, for example,
myApp.service(‘dataModel', function(){
$scope.houses = {}; //an object containing ALL the houses, each one containing ALL the apartments, each apt.containing ALL the contact info, etc. etc.
});
Then in your controllers you pass the dataModel service, and then declare and link the local scope 'reference' of the same object to it, for example:
myApp.controller(‘buildingsView’, [dataModel, function(dataModel){
//and link to that service’s object locally:
$scope.houses = dataModel.houses;
}]);
Then, once you affect that model in one view/controller, the data in the other views/controllers will magically change too!
And that is the angular way:)
Hope this makes sense!
(To clarify, there are two issues here: getting your data INTO angular and then SHARING that same object by injecting the same service to your various controllers. That’s sort of the exact idea of services - they’re singletons, which means only one copy of them exists at any time and if they’re referred to more than once, it’ll always be to the same object, which is why manipulating it in one place will change it in another.)"
Put your data within a $scope variable, and make your controllers watch this varaible from scope. When the event is triggered, you can then do what you want.
$scope.houses = [];
$scope.$watch('houses', function(newValue, oldValue) {
// This is triggered when $scope.houses changes directly
// Add true as param to this function if you want to watch properties nested within the houses object
});
If houses is within a controller, use the following:
(in controller)
var self = this;
self.houses = [];
// This tells angular to watch the controller property houses instead of a scope variable
$scope.$watch(function(){return self.houses}, function(newValue, oldValue) {
// This is triggered when $scope.houses changes directly
// Add true as param to this function if you want to watch properties nested within the houses object
});
I suggest to use directive, then it's easier to exchange data. And that is the reason why directive exists. I try to explain how to build your use case with directives. Assume that you have one controller (houseController) and for every sub requirements you make a directive. One for the contact, one for the appartment and one for the address. Then you define your whole object inside houseController. You pass all necessary data as a parameter to the directive and you can access them from the houseController or from inside the directive, because of the two way data binding. In this way you don't have to exchange something or to call update functions. You adjuste just the object.
your html
<div ng-controller="houseController">
<contact data="house.contact"></contact>
<apartment data="house.apartment"></apartment>
...
</div>
house controller
$scope.house = {
apartment:{
floor: 1,
number:34,
...
},
contact:{
id:2,
email:"peter#test.com",
mobile_phone_number:"123456789",
...
},
...
}
contact directive
.directive('contact', function() {
return {
restrict: 'E',
scope: {
data: '='
},
link: function(scope, element, attrs) {
scope.showContactInfoEdit = function(id){
// do your stuff here
scope.data.name = "Peter";
}
},
templateUrl: 'contact.html'
};
});
contact.html
{{data.email}}
{{data.mobile_phone_number}}
<a ng-click="showContactInfoEdit(data.id)">Edit</a>
I made a plunkr: http://plnkr.co/edit/8tRMtgztaXRC3EKyQhcH?p=preview
I created a nested directive structure with 2-way-object-databinding.
Everything worked like a charm on js side, until I connected it to my services which send the data to my api.
Because in the post request the object has old values, it always ignores the latest change. But the real problem is... outputting the object right before I pass it to the request it has the correct updated values.
service.updatePerson = function(person) {
console.log(person); //this outputs the correct up to date data
$http.post(Routing.generate('update_person'), person); // this works not
//After trying some more (see update below) - this works as well
$timeout(function() {
$http.post(Routing.generate('update_person'), person);
}, 1000);
};
(I am using the fos routing bundle for symfony)
Inspecting the request with my developer tools I can see that the submitted object has old values. But its not like the values never change, its always the last but not the current change.
I'm quite new to angular and might overlook something special about the $http service - I did not enable cache, so that should not be the problem.
Also the problem occurs only with the updated objects, not if I am sending a complete new entity.
The only difference I can see and I am suspecting to cause the issue is the depth of the nesting.
I have a directive called 'collapsedTable' which takes different data. Not just the data also the update, add and delete method are passed to the directive.
This is the usage of the directive:
<collapsed-table ng-if="formModel" ng-if="persons" view-data="persons" form-model="formModel" add-object="newPerson" add-item="addPerson(item)" update-item="updatePerson(item)" delete-item="deletePerson(item)"></collapsed-table>
Adding and deleting items happens directly in the directive:
<button href="#" class="btn btn-success pull-right" ng-click="addItem({item : newItem})">
Add
</button>
(delete button looks the same, but calls the delete function)
But updating data does not happen via one button, it is bound directly to the form fields and the form fields are its own directive:
<form-field ng-if="column.type" form-field-bind="item[column.name]" form-field-action="updateItem({item:item})" form-field-action-param="item" form-select-options="column.data" form-template="{{column.type}}"></form-field>
(I know, generic shit going on here)
And within the directive (editableTextarea for an example):
{{formFieldBind || 'eintragen'}}
(using the x-editable module for angular here)
I think posting the whole content of my directives is going too far. But I think the scope settings are relevant to unterstand how functions and variables are passed.
collapsedTableDirective.js
.directive('collapsedTable',['$filter',function($filter) {
return {
restrict: 'E',
templateUrl: 'templates/collapsedTableView.html',
scope: {
data: "=viewData",
newItem: '=addObject',
formModel: '=',
deleteItem: '&',
updateItem: '&',
addItem: '&'
}
}
}]);
formFieldDirective.js
.directive('formField',['formTemplateFactory', '$filter',function(formTemplateFactory, $filter) {
return {
restrict: 'E',
scope: {
formFieldBind: '=',
formFieldActionParam: '=',
formFieldAction: '&',
formTemplate: '#',
formSelectOptions: '='
},
transclude: true,
template: '<div ng-include="getTemplate()"></div>'
}
}]);
I'm also showing that form field templates are pulled via ng-include, which creates a new scope and which is the reason why the bound var is referenced with "parent" inside the form field template.
But with all questions that can be possibly discussed here, do not forget:
console.log outputs the correct data within the service. Only inside the request we have old data. Thats also the reason ist kinda difficult to debug... from the debugger side my objects always look fine.
Any help appreciated!
UPDATE - with $timeout it works
After trying some stuff out, I've got the idea to try a timeout (from a completely unrelated post). And yeah, a timeout does the trick.
But I am not answering the question because I really do not have an explanation for this behaviour. The console.log does not need a tiemout, though it should be executed before the post request.
Also there is probably a more apropriate solution than using a timeout.
person is not updated at the time when service.updatePerson runs, and it looks like it is triggered by non-Angular code which isn't adapted to digest cycles. In this case this
$scope.$apply();
// or $rootScope.$apply() if this is a service and not a controller
$http.post(Routing.generate('update_person'), person);
may help, though doing $apply in caller function and not callee is always a better habit, so external JS should trigger a wrapper instead:
scope.externalAction = function (obj) {
scope.$apply(function () {
scope.formFieldAction(obj);
};
};
console.log doesn't output the data that was actual at that moment because it was supplied with object reference, and it may reflect the changes that took place after console.log was called. This is a useful feature if taken into account, otherwise it's harmful.
To log the current object state always use
console.log(JSON.stringify(person));
When I use this it works:`
angular.module('app').service('DataService', function() {
return {theme: "amelia"}
});
But when I use this, there is no update? Can you tell me the difference?
angular.module('app').service('DataService', function() {
return {
theme: function() {
return {theme: "amelia"}
}
};
});
Controller
$scope.settings = DataService.theme();
Jade
select.form-control(ng-model="settings.theme", ng-options="theme for theme in themes")
Is it possible to get the second way working? Because I will share more data then one Object!
Thank you!
The first version of the code calls the function once to instantiate the service. After that, because services are singletons in angular the function isn't called again, but rather the return value (a "static" object) is accessed in every controller that uses the service after that.
The second version, each controller you inject the service into calls the theme function, which instantiates a brand new object each time. You have now effectively mitigated the fact that the service is a singleton. This is why data will not be shared with the second set of code.
If you put a break point on the function call in each case and run your code you should see the first version called once while the second version will be called many times.
"Get It Working"...
You can't really make it work with a function call but if you need to share multiple data objects there isn't any reason not to nest them. You could very easily do something like:
angular.module('app').service('DataService', function() {
return {
dataObjects: [
{"type":"theme", "theme":"amelia"},
{"type":"user", "id":123, "name":"ABC"}
]};
});
In the example I added a second object which is a user object to make shared "dataObjects" array. To find a specific object in the "dataObjects" array, you could loop till you find the correct type ("theme", for example). If necessary, you could even nest one level deeper if you needed the objects to be pristine (without the added type attribute).
Hope that helps!
It should be theme: function().... inside your service. Replace "=" with ":".