I'm trying to somehow dynamically use i18next translations together with Knockout.js, but I cant figure out how.
Neither a custom Knockout binding or the i18next jQuery plugin seems to work with observable values.
A demo of what I'm trying to achieve can be found here: http://jsfiddle.net/rdfx2/1/
A workaround is something like this, but I'd rather avoid that, if possible:
<div data-bind="text: translate('key', observable)"></div>
self.translate = function (key, value) {
return i18next.t(key, {
"var": value
});
};
Thanks,
I'm not very familiar with i18next, so I might be using i18next incorrectly, but you could easily achieve this by creating a bindingHandler. A very simple version of such a bindingHandler, which supports passing optional options, could look like:
ko.bindingHandlers['translatedText'] = {
update: function(element, valueAccessor, allBindings){
var key = ko.unwrap(valueAccessor());
var options = ko.toJS(allBindings.get('translationOptions') || {});
var translation = i18n.t(key, options);
element.innerText = translation;
}
};
Given the following i18next initialization code:
i18n.init({
lng: "en",
debug: true,
resStore: {
en: {
translation: {
'myTextKey': 'My translated value is "__value__"',
'otherTextKey': 'This is just a text which does not use options'
}
}
}
});
You could use it with the following HTML:
<input type="text" data-bind="value: input, valueUpdate: 'afterkeydown'"/>
<div data-bind="translatedText: 'myTextKey', translationOptions: { value: input }"></div>
<div data-bind="translatedText: 'otherTextKey'"></div>
And the following view model:
function ViewModel(){
this.input = ko.observable();
}
ko.applyBindings(new ViewModel);
I have saved the above code to a jsfiddle which you can find at http://jsfiddle.net/md2Hr/
I've updated the code to support translating HTML attributes as well.
Here is a working demo: http://jsfiddle.net/remisture/GxEGe/
HTML
<label>HTML/text</label>
<textarea data-bind="i18n: 'key', i18n-options: {var: input}"></textarea>
<label>Attribute</label>
<input type="text" data-bind="i18n: '[placeholder]key', i18n-options: {var: input}" />
JS
define(['knockout', 'i18next'], function (ko, i18n) {
ko.bindingHandlers.i18n = {
update: function (element, valueAccessor, allBindings) {
var key = ko.unwrap(valueAccessor()),
options = ko.toJS(allBindings.get('i18n-options') || {}),
translation,
parts,
attr;
// Check whether we are dealing with attributes
if (key.indexOf('[') === 0) {
parts = key.split(']');
key = parts[1];
attr = parts[0].substr(1, parts[0].length - 1);
}
translation = i18n.t(key, options);
if (attr === undefined) {
// Check whether the translation contains markup
if (translation.match(/<(\w+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/)) {
//noinspection InnerHTMLJS
element.innerHTML = translation;
} else {
// Check whether the translation contains HTML entities
if (translation.match(/&(?:[a-z]+|#x?\d+);/gi)) {
//noinspection InnerHTMLJS
element.innerHTML = translation;
} else {
// Treat translation as plain text
element.innerText = translation;
}
}
} else {
// Add translation to given attribute
element.setAttribute(attr, translation);
}
}
};
});
KO config:
var language = ko.observable('');
ko.i18n = function(key) {
return ko.computed(function() {
if (language() != null) {
return i18n.t(key, {
lng : language()
});
} else {
return "";
}
}, key);
};
view-model:
var labels = {
aboutUs: ko.i18n('app:labels.aboutUs'),
contactUsBtn: ko.i18n('app:labels.contactUsBtn') }
view:
<span data-bind="text: labels.aboutUs">
Thanks for a great example, #robert.westerlund!
I slightly modified your example to better fit my needs:
ko.bindingHandlers['i18n'] = {
update: function (element, valueAccessor, allBindings) {
var key = ko.unwrap(valueAccessor()),
options = ko.toJS(allBindings.get('i18n-options') || {}),
translation = i18next.t(key, options);
// Check whether the translation contains markup
if (translation.match(/<(\w+)((?:\s+\w+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/)) {
element.innerHTML = translation;
} else {
// Check whether the translation contains HTML entities
if (translation.match(/&(?:[a-z]+|#x?\d+);/gi)) {
element.innerHTML = translation;
} else {
// Treat translation as plain text
element.innerText = translation;
}
}
}
};
Related
I have a model called student. I also have form view, tree view for the student model. What I want to do is call my custom javascript file only when the form view of the student model is loaded. Is it possible? How to achieve this? Thanks.
What I tried is .....
openerp.student= function (instance) {
instance.web.FormView.include({
load_form: function(data) {
var self = this;
if (data.model === "student") {
altert('HELLO');
console.log('BLAH BLAH');
}
return this._super(data);
},
});
};
You can override the load_form method of FormView.
openerp.module_name= function (instance) {
instance.web.FormView.include({
load_form: function(data) {
var self = this;
if (data.model === "student") {
// Your custom code
}
return this._super(data);
},
});
};
To add the above code check this link inherit-or-override-js
It is possible to add a new view mode by extending FormFiew as Odoo did with account_move_line_quickadd.
openerp.your_module_name = function (instance) {
var _t = instance.web._t,
_lt = instance.web._lt;
var QWeb = instance.web.qweb;
instance.web.your_module_name = instance.web.your_module_name || {};
instance.web.views.add('student_form', 'instance.web.StudentFormView');
instance.web.StudentFormView = instance.web.FormView.extend({
load_form: function(data) {
var self = this;
// Add your custom code here
return this._super(data);
},
});
};
You just need to add the new mode to window action.
<record id="student_action" model="ir.actions.act_window">
<field name="name">student.action</field>
<field name="res_model">student</field>
<field name="view_mode">student_form,tree</field>
...
I've got a complex object in a JSON format. I'm using Knockout Mapping, customizing the create callback, and trying to make sure that every object that should be an observable - would actually be mapped as such.
The following code is an example of what I've got:
It enables the user to add cartItems, save them (as a JSON), empty the cart, and then load the saved items.
The loading part fails: It doesn't display the loaded option (i.e., the loaded cartItemName). I guess it's related to some mismatch between the objects in the options list and the object bounded as the cartItemName (see this post), but I can't figure it out.
Code (fiddle):
var cartItemsAsJson = "";
var handlerVM = function () {
var self = this;
self.cartItems = ko.observableArray([]);
self.availableProducts = ko.observableArray([]);
self.language = ko.observable();
self.init = function () {
self.initProducts();
self.language("english");
}
self.initProducts = function () {
self.availableProducts.push(
new productVM("Shelf", ['White', 'Brown']),
new productVM("Door", ['Green', 'Blue', 'Pink']),
new productVM("Window", ['Red', 'Orange'])
);
}
self.getProducts = function () {
return self.availableProducts;
}
self.getProductName = function (product) {
if (product) {
return self.language() == "english" ?
product.productName().english : product.productName().french;
}
}
self.getProductValue = function (selectedProduct) {
// if not caption
if (selectedProduct) {
var matched = ko.utils.arrayFirst(self.availableProducts(), function (product) {
return product.productName().english == selectedProduct.productName().english;
});
return matched;
}
}
self.getProductColours = function (selectedProduct) {
selectedProduct = selectedProduct();
if (selectedProduct) {
return selectedProduct.availableColours();
}
}
self.addCartItem = function () {
self.cartItems.push(new cartItemVM());
}
self.emptyCart = function () {
self.cartItems([]);
}
self.saveCart = function () {
cartItemsAsJson = ko.toJSON(self.cartItems);
console.log(cartItemsAsJson);
}
self.loadCart = function () {
var loadedCartItems = ko.mapping.fromJSON(cartItemsAsJson, {
create: function(options) {
return new cartItemVM(options.data);
}
});
self.cartItems(loadedCartItems());
}
}
var productVM = function (name, availableColours, data) {
var self = this;
self.productName = ko.observable({ english: name, french: name + "eux" });
self.availableColours = ko.observableArray(availableColours);
}
var cartItemVM = function (data) {
var self = this;
self.cartItemName = data ?
ko.observable(ko.mapping.fromJS(data.cartItemName)) :
ko.observable();
self.cartItemColour = data ?
ko.observable(data.cartItemColour) :
ko.observable();
}
var handler = new handlerVM();
handler.init();
ko.applyBindings(handler);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<script src="https://rawgit.com/SteveSanderson/knockout.mapping/master/build/output/knockout.mapping-latest.js
"></script>
<div>
<div data-bind="foreach: cartItems">
<div>
<select data-bind="options: $parent.getProducts(),
optionsText: function (item) { return $parent.getProductName(item); },
optionsValue: function (item) { return $parent.getProductValue(item); },
optionsCaption: 'Choose a product',
value: cartItemName"
>
</select>
</div>
<div>
<select data-bind="options: $parent.getProductColours(cartItemName),
optionsText: $data,
optionsCaption: 'Choose a colour',
value: cartItemColour,
visible: cartItemName() != undefined"
>
</select>
</div>
</div>
<div>
<button data-bind="text: 'add cart item', click: addCartItem" />
<button data-bind="text: 'empty cart', click: emptyCart" />
<button data-bind="text: 'save cart', click: saveCart" />
<button data-bind="text: 'load cart', click: loadCart" />
</div>
</div>
What needs to be changed to fix it?
P.S.: I've got another piece of code (see it here) that demonstrates a persistance of the selected value even after changing the options - though there optionsValue is a simple string, while here it's an object.
EDIT:
I figured out the problem: the call ko.mapping.fromJS(data.cartItemName) creates a new productVM object, which is not one of the objects inside availableProducts array. As a result, none of the options corresponds to the productVM contained in the loaded cartItemName, so Knockout thereby clears the value altogether and passes undefined.
But the question remains: how can this be fixed?
In the transition from ViewModel -> plain object -> ViewModel you loose the relation between the products in your cart and the ones in your handlerVM.
A common solution is to, when loading a plain object, manually search for the existing viewmodels and reference those instead. I.e.:
We create a new cartItemVM from the plain object
Inside its cartItemName, there's an object that does not exist in handlerVM.
We look in handlerVM for a product that resembles this object, and replace the object by the one we find.
In code, inside loadCart, before setting the new viewmodels:
loadedCartItems().forEach(
ci => {
// Find out which product we have:
const newProduct = ci.cartItemName().productName;
const linkedProduct = self.availableProducts()
.find(p => p.productName().english === newProduct.english());
// Replace the newProduct by the one that is in `handlerVM`
ci.cartItemName(linkedProduct)
}
)
Fiddle: https://jsfiddle.net/7z6010jz/
As you can see, the equality comparison is kind of ugly. We look for the english product name and use it to determine the match. You can also see there's a difference in what is observable and what isn't.
My advice would be to use unique id properties for your product, and start using those. You can create a simpler optionsValue binding and matching new and old values happens automatically. If you like, I can show you an example of this refactor as well. Let me know if that'd help.
I am new to Knockout and have been trying to follow code examples and the documentation, but keep running into an issue. My data bindings printing the Knockout observable function, not the actual values held by my observable fields. I can get the value if I evaluate the field using (), but if you do this you do not get any live data-binding / updates.
Below are some code snippets from my project that are directly related to the issue I am describing:
HTML
<div class="col-xs-6">
<div data-bind="foreach: leftColSocialAPIs">
<div class="social-metric">
<img data-bind="attr: { src: iconPath }" />
<strong data-bind="text: name"></strong>:
<span data-bind="text: totalCount"></span>
</div>
</div>
</div>
Note: leftColSocialAPIs contains an array of SocialAPIs. I can show that code too if needed.
Initializing the totalcount attribute
var SocialAPI = (function (_super) {
__extends(SocialAPI, _super);
function SocialAPI(json) {
_super.call(this, json);
this.totalCount = ko.observable(0);
this.templateName = "social-template";
}
SocialAPI.prototype.querySuccess = function () {
this.isLoaded(true);
appManager.increaseBadgeCount(this.totalCount());
ga('send', 'event', 'API Load', 'API Load - ' + this.name, appManager.getRedactedURL());
};
SocialAPI.prototype.toJSON = function () {
var self = this;
return {
name: self.name,
isActive: self.isActive(),
type: "social"
};
};
return SocialAPI;
})(API);
Updating totalcount attribute for LinkedIn
var LinkedIn = (function (_super) {
__extends(LinkedIn, _super);
function LinkedIn(json) {
json.name = "LinkedIn";
json.iconPath = "/images/icons/linkedin-16x16.png";
_super.call(this, json);
}
LinkedIn.prototype.queryData = function () {
this.isLoaded(false);
this.totalCount(0);
$.get("http://www.linkedin.com/countserv/count/share", { "url": appManager.getURL(), "format": "json" }, this.queryCallback.bind(this), "json").fail(this.queryFail.bind(this));
};
LinkedIn.prototype.queryCallback = function (results) {
if (results != undefined) {
results.count = parseInt(results.count);
this.totalCount(isNaN(results.count) ? 0 : results.count);
}
this.querySuccess();
};
return LinkedIn;
})(SocialAPI);
In the <span data-bind="text: totalCount"></span>, I expect to see a number ranging from 0-Integer.MAX. Instead I see the following:
As you can see, its outputting the knockout function itself, not the value of the function. Every code example I've seen, including those in the official documentation, says that I should be seeing the value, not the function. What am I doing wrong here? I can provide the full application code if needed.
Not sure, but KO view models obviously tend to bind own (not inherited through prototypes) observable properties only. So you should rewrite your code to supply totalCount observable for every social network separately.
I am building a JS framework to simulate AngularJS models, only for educational purposes.
The thing is: I assign a callback to run when the models(inputs) are updated, but after they run by the first time, they "disappear".
Before trying to do this using jQuery, I was trying with querySelectorAll and got stuck on the same problem.
Here it is: http://jsfiddle.net/YmY2w/
HTML
<div class="container" jd-app="test">
<input type="text" jd-model="name" value="foo" /><br />
<input type="text" jd-model="email" value="foo#foo" />
<hr />
<p>Name: {{ name }}</p>
<p>Email: {{ email }}</p>
</div>
JAVASCRIPT
(function($, window, document, undefined) {
'use strict';
// registering models
(function() {
var $app = $('[jd-app]');
var $models = $('[jd-model]', $app);
$models.each(function(index) {
UpdateModel.apply(this);
});
function UpdateModel() {
var model = { name: $(this).attr('jd-model'), value: this.value }
var re = new RegExp('{{\\s+('+ model.name +')\\s+}}', 'gm');
$('*', $app).each(function(index) {
var $this = $(this);
$this.text( $this.text().replace(re, model.value) );
});
$(this).on('keyup', UpdateModel);
}
})();
})(jQuery, window, document);
What am I doing wrong? Is there a better way to accomplish this?
The problem you're having is that when your script first sees {{ name }} it converts it to the value of the model "name". But then in your html, you have the text "foo" instead of {{ name }} (the thing you're trying to search/replace), so you aren't able to update the displayed text.
What you have to do is keep track of the individual DOM nodes (as attached to particular models) which contain the text that initially has the "{{ }}" in them with the proper model name.
I came up with a kind of ugly prototype here:
http://jsfiddle.net/YmY2w/11/
I'm not sure how flexible it is in terms of identifying the proper templated locations, but it serves as a demo for the kind of implementation I'm talking about. Also - not very efficient I would guess. (But then neither is angularjs really...)
(function($, window, document, undefined) {
'use strict';
// From http://stackoverflow.com/questions/298750/how-do-i-select-text-nodes-with-jquery
function getTextNodesIn(node, includeWhitespaceNodes) {
var textNodes = [], nonWhitespaceMatcher = /\S/;
function getTextNodes(node) {
if (node.nodeType == 3) {
if (includeWhitespaceNodes || nonWhitespaceMatcher.test(node.nodeValue)) {
textNodes.push(node);
}
} else {
for (var i = 0, len = node.childNodes.length; i < len; ++i) {
getTextNodes(node.childNodes[i]);
}
}
}
getTextNodes(node);
return textNodes;
}
// registering models
(function() {
var $app = $('[jd-app]');
var $models = $('[jd-model]', $app);
var models = [];
$models.each(function(index) {
registerModel.apply(this);
});
function registerModel() {
var model = { name: $(this).attr('jd-model'), value: this.value, domElements: [] };
var re = new RegExp('{{\\s+('+ model.name +')\\s+}}', 'gm');
$app.contents().each(function(index) {
if ($(this).text().match(re)) {
var textNodes = getTextNodesIn(this, false);
console.log(textNodes);
$.each(textNodes, function(index, elem) {
if (elem.nodeValue.match(re)) {
var text = elem.nodeValue;
var myArray = re.exec(text);
var match = myArray[0];
var firstIndex = text.indexOf(match);
var newElem = elem.splitText(firstIndex);
newElem.splitText(match.length);
model.domElements.push(newElem);
}
});
}
});
models.push(model);
$(this).on('keyup', updateModel);
$(this).trigger('keyup');
}
function updateModel() {
var $input = $(this);
$.each(models, function(index, model) {
if (model.name === $input.attr('jd-model')) {
var newVal = $input.val();
$.each(model.domElements, function(index, elem) {
elem.nodeValue = newVal;
});
}
});
}
})();
})(jQuery, window, document);
I have the following html:
<div ng-show=showMarketingNav>
...
</div>
<div ng-show=showProductsNav>
...
</div>
<div ng-show=showSettingsNav>
...
</div>
What I want to do is to easily be able to hide all but one of the divs from another controller. I thought I could be clever and do the following:
var subNavMenuDisplays = {
Marketing: $scope.showMarketingNav,
Products: $scope.showProductsNav,
Settings: $scope.showSettingsNav
}
$rootScope.hideContextMenu = function () {
for (var category in subNavMenuDisplays) {
subNavMenuDisplays[category] = false;
}
}
$rootScope.setContextMenu = function (name) {
$rootScope.hideContextMenu();
subNavMenuDisplays[name] = true;
}
but this obviously does not work as $scope.showMarketingNav etc. will be passed as value, not reference.
The following works, but is not exactly nice to work with:
$rootScope.hideContextMenu = function () {
$scope.showMarketingNav = false;
$scope.showProductsNav = false;
$scope.showSettingsNav = false;
}
$rootScope.setContextMenu = function (name) {
$rootScope.hideContextMenu();
if (name == "Marketing") {
$scope.showMarketingNav = true;
}
if (name == "Products") {
$scope.showProductsNav = true;
}
if (name == "Settings") {
$scope.showSettingsNav = true;
}
}
Is there a way to grab $scope.showMarketingNav by reference, or another clever way around this?
I'd prefer not using eval to concatenate variable names.
You can place an object on the $scope and then toggle it dynamically:
$scope.show = {};
$rootScope.setContextMenu = function (name) {
$scope.show = {};
$scope.show[name] = true;
}
And the Html:
<div ng-show="show.Marketing">
...
</div>
<div ng-show="show.Products">
...
</div>
<div ng-show="show.Settings">
...
</div>
Here's a plunker demonstrating the change.
You can assign simple updater functions in that object:
Marketing: function(val) { $scope.showMarketingNav = val },
Products: function(val) { $scope.showProductsNav = val},
Settings: function(val) { $scope.showSettingsNav = val}
Then call it:
subNavMenuDisplays[name](true);