I'm having an issue with ko.mapping plugin for knockout.
I've been thoroughly searching for an answer but couldn't find anything.
I'm using knockout in conjunction with boilerplate js, but I think that's not the problem.
Here's the JS:
define(function(require){
var Boiler = require('Boiler'),
komapping = require('knockout_mapping');
ko.mapping = komapping;
var mapping = {
'observe': ['disciplina',
'numero',
'paraUsoEn',
'detalleCertificadoCalidad',
'comentariosGenerales']
};
var RequisicionViewModel = function(moduleContext, params, bindingCallback){
/* Propiedades del modelo */
var self = this;
this.disciplinas = ko.observableArray();
this.requisicion = ko.mapping.fromJS({});
/* Obtener los valores del WS */
// Obtener las disciplinas
moduleContext.requestor.get('/disciplina/').done(function(data){
self.disciplinas(data);
});
// Obtener la plantilla de la requisición
moduleContext.requestor.get('/requisicion/ZFN-5612').done(function(data){
ko.mapping.fromJS(data, mapping, self.requisicion);
self.requisicion.planos = ko.observable("Jola!")
// Aplicar el binding
bindingCallback();
});
/* Gestión de eventos */
this.onGuardarClicked = function(){
console.log(ko.mapping.toJSON(self.requisicion));
};
};
return RequisicionViewModel;
});
As you can see I define only the objects I want to be observable.
Here's the HTML
<div id="uso-planos-informacion" class="clearfix" data-bind="with:requisicion">
<div class="control-grp">
<label for="usarse-en" class="text-solid">{{nls.label_usarse_en}}</label>
<input id="usarse-en"
type="text"
data-bind="value:paraUsoEn">
</div>
<div class="control-grp">
<label for="planos" class="text-solid">{{nls.label_planos}}</label>
<input id="planos"
type="text">
</div>
<div class="control-grp">
<label for="certificado-calidad" class="text-solid">{{nls.certificado_calidad}}</label>
<input id="certificado-calidad"
type="text"
data-bind="value:detalleCertificadoCalidad">
</div>
</div><!-- Termina uso-planos-informacion -->
It's much longer, but for brevity I'll just paste 2 fields that show the error.
Finally when I run it, this is what happens:
http://i.stack.imgur.com/2Vasm.png
Here's what I've tried so far:
Use () after the variable name so that it evaluates and shows the value inside the observable.
This works, but the observable looses it's properties or something like that because it does not get updated again after this.
Defining a create function for the mapping.
Al ready tried this:
var mapping = {'paraUsoEn':{create:function(options){return ko.observable(options.data);}}}
And does not work. The value does not appear, neither it can be updated.
Hope Someone has solved this kind of problem, otherwise I'll have to do the mapping manually (which works!).
Thanks!
Well, after following the suggestion from #Salvador Dali, I was creating a more reproducible example, and by doing this I could find the solution.
Since I'm using require.js to load my libraries I found out that there was a problem with knockout.
Although knockout was included as a script in my index.jsp, it was not working properly.
The solution consists in configuring knockout inside main.js (boilertplatejs file) like the following:
paths:{
…
knockout : 'path_to_knockout js',
knockout_mapping : 'path_to_knockout_mapping js',
…
},
shim : {
…
'knockout_mapping' : {
deps : ['knockout'],
exports: 'ko.mapping'
}
…
}
You can leave knockout reference inside index.jsp if you want, and if you already have components or modules built it will prevent them from breaking.
Inside your component you'll need this:
var ko = require('knockout');
And inside your viewmodel.js, add this:
var komapping = require('knockout_mapping'),
ko = require('knockout');
ko.mapping = komapping;
This way I've been able to map json data (mapped as normal JS object by jquery), into observables.
Additional information.
Knockout version: 3.2.0 (version 3.1.0 produces an error about ko not
being defined).
KO Mapping version: 2.4.1
jquery version: 1.7.1
underscore-1.3.3_1
Thanks!
Related
I am a newcomer in Magento2 and face with a next task. I have to do custom validation for city and street inputs at Checkout page for both shipping and payment steps.
There are 2 issues I am misunderstanding totally.
As I could investigate templates for city and street are inserted by Knockout. To get cities and streets list I have to insert php method in script tag. This php method provides to me URL for next Ajax request. Because of general Knockout template with '.html' type I can't insert php code there.
So how can I call my js file from Knockout html template?
City and street inputs must offer coincidences for first entered letters (as a result of Ajax request) in their lists below. How this lists can be realized?
I have read Magento devdocs and a lot of communities but couldn't find intelligible explanation. Sorry for possible repeat.
app/design/frontend/Vendor/Theme/Magento_Checkout/web/template/shipping-address/form.html (inputs are inserted inside id="shipping-new-address-form")
<div class="amtheme-shipping-wrap">
<form class="form form-shipping-address amtheme-form-address"
id="co-shipping-form"
data-bind="attr: {'data-hasrequired': $t('* Required Fields')}">
<div class="step-title" data-bind="text: setAddressTitle" data-role="title"></div>
<!-- ko foreach: getRegion('before-fields') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!--/ko-->
<div id="shipping-new-address-form" class="fieldset address">
<!-- ko foreach: getRegion('additional-fieldsets') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!--/ko-->
<!-- ko if: (isCustomerLoggedIn) -->
<div class="field choice" data-bind="visible: !isFormInline">
<input type="checkbox" class="checkbox" id="shipping-save-in-address-book" data-bind="checked: saveInAddressBook" />
<label class="label" for="shipping-save-in-address-book">
<span data-bind="i18n: 'Save in address book'"></span>
</label>
</div>
<!-- /ko -->
<div class="amtheme-address-toolbar" data-bind="visible: !isFormInline && !isFormPopUpVisible()">
<button type="button"
class="action action-cancel"
click="hideNewAddress"
text="$t('Cancel')"
data-bind="attr: {title: $t('Cancel')}">
</button>
<button type="button"
class="action action-save"
click="saveNewAddress"
text="$t('Ship here')"
data-bind="attr: {title: $t('Ship here')}">
</button>
</div>
</div>
</form>
</div>
I was about to write inside form.html something like this:
<script>
require([
'jquery',
'Magento_Theme/js/govaddress-validation'
], function($) {
$(function () {
$('input[name="city"]').keyup(function () {
console.log('keyup event worked');
govAddressValidation.getCityList('<?php echo $this->getUrl("opgovaddress"); ?>');
});
})
})
</script>
My JS file is not matter as it is unreachable for now
app/design/frontend/Vendor/Theme/Magento_Theme/web/js/govaddress-validation.js
define([
'jquery'
], function ($) {
'use strict';
return {
url: '',
getCityList: function (url) {
var inputValue = $('input[name="city"]').val();
this.inputValue = inputValue;
this.url = url;
this.ajaxCall();
console.log('getCityList');
},
...
}
})
app/design/frontend/Vendor/Theme/Magento_Theme/requirejs-config.js
var config = {
map: {
'*': {
backTop: 'Magento_Theme/js/components/back-to-top',
amMenu: 'Magento_Theme/js/components/am-menu',
amQty: 'Magento_Theme/js/components/am-qty',
amSelect: 'Magento_Theme/js/components/am-select',
amFileUpload: 'Magento_Theme/js/components/am-file-upload',
amStickyHeader: 'Magento_Theme/js/components/am-sticky-header',
govAddressValidation: 'Magento_Theme/js/govaddress-validation'
}
},
config: {
mixins: {
'mage/validation': {
'Magento_Theme/js/lib/mage/validation-mixin': false
},
'mage/menu': {
'Magento_Theme/js/lib/mage/menu-mixin': true
}
}
}
};
Firstly, Magento's validation for checkout_index_index.xml is not working properly so, you have to insert the rules while you replace the street fields.
You can do this by creating a plugin and inserting the fields in Layout AfterProcessor.
namespace/module/etc/frontend/di.xml
...
<type name="Magento\Checkout\Block\Checkout\LayoutProcessor">
<plugin name="rewrite-street" type="Namespace\Module\Model\Checkout\LayoutProcessorPlugin" sortOrder="10"/>
</type>
...
And the LayoutProcessorPlugin should look like this: (Please see in my attachment)
https://pastebin.com/AE6AiLxk
You can insert in the validation array any validation rule you want.
So what you do is open your checkout and look for this field you want to validate. I will choose your first field, city. Opening dev tools gives me this:
<input class="input-text" type="text" data-bind=" ..." name="city" ...>
So we could do something with the name since we don't have a really useful id. I see in your question that you already have this part!
Since you say you want both shipping and payment step, I will go for the last. So to do this we need a custom validator on the payment step. I assume you already have a custom extension, something like Vendor_MyCheckoutValidator. First we will create a copy of checkout_index_index.xml (from Magento Checkout module) in Vendor/MyCheckoutValidator/view/frontend/layout and we will leave out all the stuff we don't need. Simply copy the steps down to payment:
<body>
<referenceBlock name="checkout.root">
<arguments>
[... all the way down to payment ...]
<item name="payment" xsi:type="array">
<item name="children" xsi:type="array">
<item name="additional-payment-validators" xsi:type="array">
<item name="children" xsi:type="array">
<item name="my-city-validator" xsi:type="array">
<item name="component" xsi:type="string">Vendor_MyCheckoutValidator/js/view/finalcheck-validation</item>
</item>
[... and all the way back down again ...]
So there you have it, I named it my-city-validator and it points to Vendor_MyCheckoutValidator/js/view/finalcheck-validation.
Let's create ths finalcheck-validation.js in your module under Vendor/MyCheckoutValidator/view/frontend/web/js/view:
define(
[
'uiComponent',
'Magento_Checkout/js/model/payment/additional-validators',
'Vendor_MyCheckoutValidator/js/model/final-address-check-validator'
],
function (Component, additionalValidators, finalAddressCheckValidator) {
'use strict';
additionalValidators.registerValidator(finalAddressCheckValidator);
return Component.extend({});
}
);
It looks more complicated then it is but simply said our define says it is a payment additinal validator, it can be found in js/model/final-address-check-validator and these are then used in the function. So we now we need our final bit, the final-address-check-validator.js that should be in Vendor/MyCheckoutValidator/view/frontend/web/js/model :
Note: the path mentioned in the checkout_index_index.xml is Vendor_MyCheckoutValidator/js/view/finalcheck-validation and in the above js it is Vendor_MyCheckoutValidator/js/model/final-address-check-validator but in reality you need the "view/frontend/web" bit as well (Magento knows where to find it but this can be confusing for new developers) so our path is Vendor/MyCheckoutValidator/view/frontend/web/js/view and Vendor/MyCheckoutValidator/view/frontend/web/js/model
Back to our last bit, the final-address-check-validator.js. Here we have this problem of not knowing the id but we do have the name of the input as seen at the beginning of my post, the name is "city". So let's check if it starts with a Z because we don't want to send things to cities that start with an Z.
We need some standard stuff such as mage/translate and the (error) messageList and of course jquery:
define(
['jquery', 'mage/translate', 'Magento_Ui/js/model/messageList'],
function ($, $t, messageList) {
'use strict';
return {
validate: function () {
var cityIsFine = true,
city = $('input[name="city"]').val(); // getting our city input by name!
if(city.match("^Z") {
messageList.addErrorMessage({ message: $t('We are sorry, your city starts with a Z.') });
cityIsFine = false;
}
// you could do some checks here and maybe a console.log
console.log(city);
if(!cityIsFine) {
return false;
}
return true;
}
}
}
);
Install your module, make sure to clear all cache! Especially frontend cache! Deploy your frontend, clear browser cache thoroughly and try with a city named Zen. You should be able to fill in everything fine and then you push the payment button and you get an error!
As you can see I did not make use of any templates or mixins or other things (I could but it simply isn't needed here). You only have to declare your custom validator on the payment step, add it to the additional validators as shown and do some jquery magic in it.
I concentrated on city but you could do the same for street. Have a look on your frontend what its name is. Say it has name="street[0]" then your jQuery value would look like street0 = $('input[name="street[0]"]').val();
You also could start with throwing an error right away and get that working before you add regex and other rules. Then you know you have this working :-)
define(
['jquery', 'mage/translate', 'Magento_Ui/js/model/messageList'],
function ($, $t, messageList) {
'use strict';
return {
validate: function () {
console.log('Sorry!');
messageList.addErrorMessage({ message: $t('We are sorry you will never get past the payment button') });
return false;
}
}
}
);
And this part I haven't tried but I think you can do an ajax call in the above validator. I'm not sure if you want a kind of auto-complete from your question so I concentrated on the validation of city in the above answer.
I have a View model, which has a loaddata function. It has no constructor. I want it to call the loadData method IF the ID field has a value.
That field is obtained via:
self.TemplateId = ko.observable($("#InputTemplateId").val());
Then, at the end of my ViewModel, I have a bit of code that checks that, and calls my load function:
if (!self.CreateMode()) {
self.loadData();
}
My load method makes a call to my .Net WebAPI method, which returns a slighly complex structure. The structure is a class, with a few fields, and an Array/List. The items in that list, are a few basic fields, and another List/Array. And then THAT object just has a few fields. So, it's 3 levels. An object, with a List of objects, and those objects each have another list of objects...
My WebAPI call is working. I've debugged it, and the data is coming back perfectly.
self.loadData = function () {
$.get("/api/PlateTemplate/Get", { id: self.TemplateId() }).done(function (data) {
self.Data(ko.mapping.fromJS(data));
});
}
I am trying to load the contents of this call, into an observable object called 'Data'. It was declared earlier:
self.Data = ko.observable();
TO load it, and keep everything observable, I am using the Knockout mapping plugin.
self.Data(ko.mapping.fromJS(data));
When I breakpoint on that, I am seeing what I expect in both data (the result of the API call), and self.Data()
self.Data seems to be an observable version of the data that I loaded. All data is there, and it all seems to be right.
I am able to alert the value of one of the fields in the root of the data object:
alert(self.Data().Description());
I'm also able to see a field within the first item in the list.
alert(self.Data().PlateTemplateGroups()[0].Description());
This indicates to me that Data is an observable and contains the data. I think I will later be able to post self.Data back to my API to save/update.
Now, the problems start.
On my View, I am trying to show a field which resides in the root class of my complex item. Something I alerted just above.
<input class="form-control" type="text" placeholder="Template Name" data-bind="value: Data.Description">
I get no error. Yet, the text box is empty.
If I change the code for the input box to be:
data-bind="value: Data().Description()"
Data is displayed. However, I am sitting with an error in the console:
Uncaught TypeError: Unable to process binding "value: function
(){return Data().Description() }" Message: Cannot read property
'Description' of undefined
I think it's due to the view loading, before the data is loaded from the WebAPI call, and therefore, because I am using ko.mapping - the view has no idea what Data().Description() is... and it dies.
Is there a way around this so that I can achieve what I am trying to do? Below is the full ViewModel.
function PlateTemplateViewModel() {
var self = this;
self.TemplateId = ko.observable($("#InputTemplateId").val());
self.CreateMode = ko.observable(!!self.TemplateId() == false);
self.IsComponentEditMode = ko.observable(false);
self.IsDisplayMode = ko.observable(true);
self.CurrentComponent = ko.observable();
self.Data = ko.observable();
self.EditComponent = function (data) {
self.IsComponentEditMode(true);
self.IsDisplayMode(false);
self.CurrentComponent(data);
}
self.loadData = function () {
$.get("/api/PlateTemplate/Get", { id: self.TemplateId() }).done(function (data) {
self.Data(ko.mapping.fromJS(data));
});
}
self.cancel = function () {
window.location.href = "/PlateTemplate/";
};
self.save = function () {
var data = ko.mapping.toJS(self.Data);
$.post("/api/PlateTemplate/Save", data).done(function (result) {
alert(result);
});
};
if (!self.CreateMode()) {
self.loadData();
}
}
$(document).ready(function () {
ko.applyBindings(new PlateTemplateViewModel(), $("#plateTemplate")[0]);
});
Maybe the answer is to do the load inside the ready() function, and pass in data as a parameter? Not sure what happens when I want to create a New item, but I can get to that.
Additionally, when I try save, I notice that even though I might change a field in the view (Update Description, for example), the data in the observed view model (self.Data) doesn't change.
Your input field could be this:
<div data-bind="with: Data">
<input class="form-control" type="text" placeholder="Template Name" data-bind="value: Description">
</div>
I prefer using with as its cleaner and should stop the confusion and issues you were having.
The reason that error is there is because the html is already bound before the data is loaded. So either don't apply bindings until the data is loaded:
$.get("/api/PlateTemplate/Get", { id: self.TemplateId() }).done(function (data) {
self.Data(ko.mapping.fromJS(data));
ko.applyBindings(self, document.getElementById("container"));
});
Or wrap the template with an if, therefore it won't give you this error as Data is undefined originally.
self.Data = ko.observable(); // undefined
<!-- ko if: Data -->
<div data-bind="with: Data">
<input class="form-control" type="text" placeholder="Template Name" data-bind="value: Description">
</div>
<!-- /ko -->
Also if you know what the data model is gonna be, you could default data to this.
self.Data = ko.observable(new Data());
Apply Bindings Method:
var viewModel = null;
$(document).ready(function () {
viewModel = new PlateTemplateViewModel();
viewModel.loadData();
});
I have spent some time trying to unscramble this issue. I have a page that I would like to be used by a beauty salon owner to manage two things:
Appointments
Days with special schedule
To select a 'view' I am using a toggle that is based on Knockout's visible binding:
So when a radio button is clicked a div is being rendered and another hidden:
<div id="selectingScopes" class="col-xs-6 col-md-4">
<fieldset>
<legend>View:</legend>
<div id="radioControls" class="switch-toggle well">
<input jsf:id="appointmentsRadio" name="appointmentsRadio" type="radio"
data-bind="checked: selectedScope" value="#{bigcopy.appointmentsLabel}"/>
<label for="appointmentsRadio" onclick="">#{bigcopy.appointmentsLabel}</label>
<input jsf:id="specialdaysRadio" name="specialdaysRadio" type="radio"
data-bind="checked: selectedScope" value="#{bigcopy.specialDaysLabel}"/>
<label for="specialdaysRadio" onclick="">#{bigcopy.specialDaysLabel}</label>
<a class="btn btn-primary"></a>
</div>
</fieldset>
</div>
<div id="appointmentsModel" class="row" data-bind="fadeVisible: selectedScope()==='#{bigcopy.appointmentsLabel}'">
<ui:include src="includes/appointmentsManagement.xhtml"/>
</div>
<div class="row" data-bind="fadeVisible: selectedScope()==='#{bigcopy.specialDaysLabel}'">
<ui:include src="includes/specdaysManagement.html"/>
</div>
In the same time I want to bind the "appointmentsModel" div above to a different view model that will be responsible only for managing the table of appointments that have to be uploaded from the server. This is the js file sifted of irrelevant clutter:
var restServiceRoot = "localhost:8080/RimmaNew/rest";
var scopeSelector = {
selectedScope: ko.observable()
};
ko.applyBindings(scopeSelector);
//appointments part
//Initial load
function appointmentsModel() {
var self = this;
self.serviceURL = restServiceRoot + "/appointments";
self.Appointments = ko.observableArray([]);
$.ajax({
url: self.serviceURL,
type: 'get',
data: null,
dataType: 'json',
success: function (appointments) {
var parsed = JSON.parse(appointments);
var appointmentsArray = parsed.current;
var mappedAppointments = $.map(appointmentsArray, function(item){
return new Appointment(item);
});
self.Appointments(mappedAppointments);
},
error: function (xhr, ajaxOptions, thrownError) {
var err = xhr.responseText;
alert(err);
}
});
}
var appointmentsModelImpl = new appointmentsModel();
ko.applyBindings(appointmentsModelImpl, document.getElementById('appointmentsModel'));
My code is inspired of two books: Knockout.js (O'Reilly - Jamie Munro) and Java EE and HTML 5 Enterprise Application Development (Oracle Press) that barely touch upon the topic of having cross-bindings in KO. I equally didn't find official KO documentation to be helpful in this endeavor.
There is a stackoverflow ticket that dealt with a similar issue, but in one it was the author who answered himself - his explanation made perfect sense to himself, but the mentions of 'stopBinding' and 'controlsDescendantBindings' seemed taken out of context to the two bindings that he uses below...I can't see how to apply this to my problem...
So you would make me a huuge favor if you could either
direct me to a resource where I could educate myself on how to use cross-bindings in KO
Or guide me in how can I mend my code to be able to control the visibility of these two divs and apply a viewmodel to each of them
Doing both would be fantastic.
1 The full code for the original page (xhtml) is here
2The js file
3The js 'classes' file
4The enclosed page with the 'appointments' table
Your best bet will probably be to think of your models a little bit differently. Instead of doing a separate model binding, it may make more sense to do a default appointmentsModel, then update the observables inside that.
What I would do is have a single parent binding to the body, or a containing div in the body, then submodels for the various children.
Sample JS:
var MyParentModel = function(){
var self = this;
self.selectingScopes = {
scope: ko.observable("");
};
self.appointmentModel = {
param1: ko.observable(),
param2: ko.observable()
}
};
ko.applyBindings(new MyParentModel());
Sample HTML:
<div id="appointmentsModel" data-bind="with: appointmentModel"></div>
I'm using knockout for a single page app that does some basic calculations based on several inputs to then populate the value of some html . In an attempt to keep my html concise I've used an array of objects in my viewModel to store my form. I achieved the basic functionality of the page however I wish to add a 'display' value to show on html that has formatted decimal points and perhaps a converted value in the future.
I'm not sure of a 'best practices' way of accessing the other values of the object that I'm currently 'in'. For example: If I want my display field to be a computed value that consists of the value field rounded to two decimal places.
display: ko.computed(function()
{
return Math.round(100 * myObj.value())/100;
}
I've been reading through the documentation for knockout and it would appear that managing this is a common problem with those new to the library. I believe I could make it work by adding the computed function outside of the viewmodel prototype and access the object by
viewModel.input[1].value()
However I would imagine there is a cleaner way to achieve this.
I've included a small snippet of the viewModel for reference. In total the input array contains 15 elements. The HTML is included below that.
var ViewModel = function()
{
var self = this;
this.unitType = ko.observable('imperial');
this.input =
[
{
name: "Test Stand Configuration",
isInput: false
},
{
name: "Charge Pump Displacement",
disabled: false,
isInput: true,
unitImperial: "cubic inches/rev",
unitMetric: "cm^3/rev",
convert: function(incomingSystem)
{
var newValue = this.value();
if(incomingSystem == 'metric')
{
//switch to metric
newValue = convert.cubicinchesToCubiccentimeters(newValue);
}
else
{
//switch to imperial
newValue = convert.cubiccentimetersToCubicinches(newValue);
}
this.value(newValue);
},
value: ko.observable(1.4),
display: ko.computed(function()
{
console.log(self);
}, self)
}
]
};
__
<!-- ko foreach: input -->
<!-- ko if: $data.isInput == true -->
<div class="form-group">
<div class="col-sm-6">
<label class="control-label" data-bind="text: $data.name"></label>
</div>
<div class="col-sm-6">
<div class="input-group">
<!-- ko if: $data.disabled == true -->
<input data-bind="value: $data.value" type="text" class="form-control" disabled>
<!-- /ko -->
<!-- ko if: $data.disabled == false -->
<input data-bind="value: $data.value" type="text" class="form-control">
<!-- /ko -->
<!-- ko if: viewModel.unitType() == 'imperial'-->
<span data-bind="text: $data.unitImperial" class="input-group-addon"></span>
<!-- /ko -->
<!-- ko if: viewModel.unitType() == 'metric' -->
<span data-bind="text: $data.unitMetric" class="input-group-addon"></span>
<!-- /ko -->
</div>
</div>
</div>
<!-- /ko -->
<!-- ko if: $data.isInput == false -->
<div class="form-group">
<div class="col-sm-6">
<h3 data-bind="text: $data.name"></h3>
</div>
</div>
<!-- /ko -->
If you want to read/ write to & from the same output, #Aaron Siciliano's answer is the way to go. Else, ...
I'm not sure of a 'best practices' way of accessing the other values of the object that > I'm currently 'in'. For example: If I want my display field to be a computed value that consists of the value field rounded to two decimal places.
I think there's a misconception here about what KnockoutJS is. KnockoutJS allows you to handle all your logic in Javascript. Accessing the values of the object you are in is simple thanks to Knockout's context variables: $data (the current context, and the same as JS's this), $parent (the parent context), $root(the root viewmodel context) and more at Binding Context. You can use this variables both in your templates and in your Javascript. Btw, $index returns the observable index of an array item (which means it changes automatically when you do someth. wth it). In your example it'd be as simple as:
<span data-bind="$data.display"></span>
Or suppose you want to get an observable w/e from your root, or even parent. (Scenario: A cost indicator that increases for every item purchased, which are stored separately in an array).
<span data-bind="$root.totalValue"></span>
Correct me if I'm wrong, but given that you have defined self only in your viewmodel, the display function should output the whole root viewmodel to the console. If you redefine a self variable inside your object in the array, self will output that object in the array. That depends on the scope of your variable. You can't use object literals for that, you need a constructor function (like the one for your view model). So you'd get:
function viewModel() {
var self = this;
self.inputs = ko.observableArray([
// this builds a new instance of the 'input' prototype
new Input({initial: 0, name: 'someinput', display: someFunction});
])
}
// a constructor for your 15 inputs, which takes an object as parameter
function Input(obj) {
var self = this; // now self refers to a single instance of the 'input' prototype
self.initial = ko.observable(obj.initial); //blank
self.name = obj.name;
self.display = ko.computed(obj.fn, this); // your function
}
As you mentioned, you can also handle events afterwards, see: unobtrusive event handling. Add your event listeners by using the ko.dataFor & ko.contextFor methods.
It appears as though KnockoutJS has an example set up on its website for this exact scenario.
http://knockoutjs.com/documentation/extenders.html
From reading that page it looks as though you can create an extender to intercept an observable before it updates and apply a function to it (to format it for currency or round or perform whatever changes need to be made to it before it updates the ui).
This would probably be the closest thing to what you are looking for. However to be completely honest with you i like your simple approach to the problem.
I'm following John Papa's jumpstart course about SPA's and trying to display a list of customers loaded via ASP.NET Web API the knockout foreach binding is not working. The Web API is working fine, I've tested it on it's own and it is returning the correct JSON, because of that I won't post the code for it. The get method simply returns one array of objects, each with properties Name and Email. Although not a good practice, knockout is exposed globaly as ko by loading it before durandal.
I've coded the customers.js view model as follows
define(['services/dataservice'], function(ds) {
var initialized = false;
var customers = ko.observableArray();
var refresh = function() {
return dataservice.getCustomers(customers);
};
var activate = function() {
if (initialized) return;
initialized = true;
return refresh();
};
var customersVM = {
customers: customers,
activate: activate,
refresh: refresh
};
return customersVM;
});
The dataservice module I've coded as follows (I've not wrote bellow the function queryFailed because I know it's not being used)
define(['../model'], function (model) {
var getCustomers = function (customersObservable) {
customersObservable([]);
var options = {url: '/api/customers', type: 'GET', dataType: 'json'};
return $.ajax(options).then(querySucceeded).fail(queryFailed);
function querySucceeded(data) {
var customers = [];
data.forEach(function (item) {
var c = new model.Customer(item);
customers.push(c);
});
customersObservable(customers);
}
};
return {
getCustomers: getCustomers
};
});
Finaly the model module was built as follows:
define(function () {
var Customer = function (dto) {
return mapToObservable(dto);
};
var model = {
Customer: Customer
};
return model;
function mapToObservable(dto) {
var mapped = {};
for (prop in dto)
{
if (dto.hasOwnProperty(prop))
{
mapped[prop] = ko.observable(dto[prop]);
}
}
return mapped;
}
});
The view is then simply a list, it is simply:
<ul data-bind="foreach: customers">
<li data-bind="text: Name"></li>
</ul>
But this doesn't work. Any other binding works, and I've looked on the console window, and it seems the observable array is being filled correctly. The only problem is that this piece of code doesn't show anything on screen. I've reviewed many times the files but I can't seem to find the problem. What's wrong with this?
You can use the knockout.js context debugger chrome extension to help you debug your issue
https://chrome.google.com/webstore/detail/knockoutjs-context-debugg/oddcpmchholgcjgjdnfjmildmlielhof
Well, I just spent a lot of time on an local issue to realize that the ko HTML comment format, if used, should be like this:
<!-- ko foreach: arrecadacoes -->
and NOT like this:
<!-- ko: foreach: arrecadacoes -->
: is NOT used after ko...
I know this question is a little old but I thought I'd add my response in case someone else runs into the same issue I did.
I was using Knockout JS version 2.1.0 and it seems the only way I can get the data to display in a foreach loop was to use:
$data.property
so in the case of your example it would be
$data.Name
Hope this helps
I don't see anywhere in your code that you've called ko.applyBindings on your ViewModel.
KO has a known issue while using foreach in a non-container element like the one above <ul> so you have to use containerless control flow syntax.
e.g.
<ul>
<!-- ko foreach: customers-->
<li data-bind="text: Name"></li>
<!-- /ko -->
</ul>
Ref: http://knockoutjs.com/documentation/foreach-binding.html