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);
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'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;
}
}
}
};
I have a view that displays data from a foreach loop in different categories depending on the type. Each category will contain a number of users - I created an object that will check to see if the number of users in a category are more than 10 then the text for the visible bind will show. And for the category that doesn't have more than 10 it will not show the text.
My question: if the first category doesn't have 10 it won't show text does that mean that it won't also show text for the remaining categories?
Help with: the visible binding is not working even though a category would contain more than 10 and not sure why.
Here is my JSFiddle: http://jsfiddle.net/xNdJk/1/
JavaScript:
var userViewModel = function (data) {
var _self = this;
_self.Name = ko.observable(data.Name);
_self.Letter = ko.observable(data.Letter);
_self.ShowLetter = ko.computed(function () {
return (roleViewModel.UserCount > 13);
});
};
var typeViewModel = function (data) {
var _self = this;
_self.ContentType = ko.observable(data.ContentType);
_self.Name = ko.observable(data.Name);
_self.Rank = ko.observable(data.Rank);
_self.UserCount = ko.observable(data.UserCount);
_self.Users = ko.observableArray([]);
};
View:
<div class="collapse in" data-bind="template: { name: 'list', foreach: $data.Users }">
</div>
<div id="letter" data-bind="visible:ShowLetter, text: Letter"></div>
You are mixing classes and instances, you have created a secondModel class but you never instance it, here is a working example
http://jsfiddle.net/xNdJk/2/
var viewModel = function(){
this.Letter = ko.observable('Hello, World!');
this.secondModel = new secondModel();
this.shouldShowMessage = ko.computed(function() {
return (this.secondModel.UserCount() > 13);
}, this);
}
var secondModel = function(){
var self = this;
self.UserCount = ko.observable(153);
}
I have this div which displays a letter, but I want to add an if statement of when to show this div based on the following condition:
if usersCount() > 3 then show letter
<div class=" header" id="letter" data-bind="text: Letter">
....
</div>
How could i add the if statement along with the text - expression statement?
data-bind="if: UserCount() > 13 then text:Letter"` ....??
var userViewModel = function (data) {
var _self = this;
_self.Letter = ko.observable(data.Letter);
};
var roleViewModel = function (data) {
var _self = this;
_self.UserCount = ko.observable(data.UserCount);
};
Check out the Visible Binding. You'll want to create a property in you View Model to handle the logic of hiding/showing the div. Here is a JSFiddle to demonstrate.
<div data-bind="visible: shouldShowMessage, text: Letter">
</div>
<script type="text/javascript">
var viewModel = function(){
var self = this;
self.Letter = ko.observable('Hello, World!');
self.UserCount = ko.computed(function() {
return Math.floor((Math.random() * 20) + 1);
});
self.shouldShowMessage = ko.computed(function() {
return (self.UserCount() > 13);
});
};
</script>