I have an observable extended with the following extender:
ko.extenders.e2mElementName = function (target, options) {
var result = ko.dependentObservable({
read: target,
write: function (newValue) {
var current = target.peek();
if (newValue != current) {
newValue = newValue.replace(/[^0-9A-Za-z_]/g, "").replace(" ", "_");
//check if name already exists
if (!document._editor.elementNames)
document._editor.elementNames = [];
if ($.inArray(newValue, document._editor.elementNames)) {
alert("The name '" + newValue + "' was already assigned to an element on this page");
target.notifySubscribers(current)
} else {
document._editor.elementNames.remove(current);
document._editor.elementNames.push(newValue);
target(newValue);
}
}
}
}).extend({ notify: 'always' });
//result(target());
return result;
};
When i extend the observable at construction everything works fine, in my object construction i have:
this.Name = ko.observable("").extend({ e2mElementName: true });
but after i make a call to mapping.fromJs my observable gets changed to a string ex: "My Name" ...
ko.mapping.fromJS(rawData, mapping, this);
if I remove the extender everything works fine and i have the observable wrapping the string....
what am I doing wrong?
you can add the extender after calling the ko.mapping.fromJS
var applyMapping = function(rawData) {
ko.mapping.fromJS(rawData, {}, this);
this.name = this.name.extend({e2ElementName: true});
}
or you can use the mapping plugin options for this
var mappingOptions = {
// customize the creation of the name property
name: {
create: function(data) {
return ko.observable(data.name).extend( {e2ElementName: true} );
}
}
};
ko.mapping.fromJS(data, mappingOptions, this));
Related
I've been trying to create an autocomplete dropdown based on the accepted response in this post but the autocomplete dropdown simply isn't showing up. It could be because the response is 9 years old or perhaps I'm doing something wrong. I have tried all of the suggestions that I've come across. Is there an updated way to create this combobox using jquery version 1.12.3, jquery-ui version 1.12.1, and knockoutjs version 3.4.1?
To me is seems like the bindings aren't really taking place because I could rename the custom binding to "jqAuto1" instead of "jqAuto" and there would be no errors, even though "jqAuto1" isn't defined anywhere. Why isn't that being picked up?
Here is my code. Note that the JS script is in a separate, parent solution from the CSHTML and TS files. The browser still finds and executes the JS script.
CSHTML
<input class="form-control form-control-xs" data-bind="value: companyName, jqAuto: { autoFocus: true }, jqAutoSource: myComp, jqAutoValue: mySelectedGuid, jqAutoSourceLabel: 'displayName', jqAutoSourceInputValue: 'coname', jqAutoSourceValue: 'id'" placeholder="Type Company Name and select from list" />
TS
// For list of Companies
class Comp {
_id: KnockoutObservable<string>;
_coname: KnockoutObservable<string>;
_coid: KnockoutObservable<string>;
constructor(id: string, coname: string, coid: string) {
this._id = ko.observable(id);
this._coname = ko.observable(coname);
this._coid = ko.observable(coid);
}
}
myComp: KnockoutObservableArray<Comp>;
mySelectedGuid: KnockoutObservable<string>;
displayName: KnockoutComputed<string>;
...
this.myComp = ko.observableArray([
new Comp("1", "Company 1", "CO1"),
new Comp("2", "Company 2", "CO2"),
new Comp("3", "Company 3", "CO3"),
new Comp("4", "Company 4", "CO4"),
new Comp("5", "Company 5", "CO5")
]);
this.companyName = ko.validatedObservable<string>("");
this.displayName = ko.computed(function () {
return this.myComp.coname + " [" + this.myComp.coid + "]";
}, this);
this.mySelectedGuid = ko.observable("5");
JS
Pretty much what's in the linked post
(function () {
var global = this || (0, eval)('this'),
document = global['document'],
moduleName = 'knockout-binding-jqauto',
dependencies = ['knockout', 'jquery'];
var moduleDance = function (factory) {
// Module systems magic dance.
if (typeof define === "function" && define["amd"]) {
define(moduleName, dependencies.concat('exports'), factory);
} else {
// using explicit <script> tags with no loader
global.CPU = global.CPU || {};
factory(global.ko, global.Globalize);
}
};
var factory = function (ko, $) {
ko.bindingHandlers.jqauto = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
var options = valueAccessor() || {},
allBindings = allBindingsAccessor(),
unwrap = ko.utils.unwrapObservable,
modelValue = allBindings.jqAutoValue,
source = allBindings.jqAutoSource,
valueProp = allBindings.jqAutoSourceValue,
inputValueProp = allBindings.jqAutoSourceInputValue || valueProp,
labelProp = allBindings.jqAutoSourceLabel || valueProp;
//function that is shared by both select and change event handlers
function writeValueToModel(valueToWrite) {
if (ko.isWriteableObservable(modelValue)) {
modelValue(valueToWrite);
} else { //write to non-observable
if (allBindings['_ko_property_writers'] && allBindings['_ko_property_writers']['jqAutoValue'])
allBindings['_ko_property_writers']['jqAutoValue'](valueToWrite);
}
}
//on a selection write the proper value to the model
options.select = function (event, ui) {
writeValueToModel(ui.item ? ui.item.actualValue : null);
};
//on a change, make sure that it is a valid value or clear out the model value
options.change = function (event, ui) {
var currentValue = $(element).val();
alert(currentValue);
var matchingItem = ko.utils.arrayFirst(unwrap(source), function (item) {
return unwrap(inputValueProp ? item[inputValueProp] : item) === currentValue;
});
if (!matchingItem) {
writeValueToModel(null);
}
}
//handle the choices being updated in a DO, to decouple value updates from source (options) updates
var mappedSource = ko.dependentObservable(function () {
mapped = ko.utils.arrayMap(unwrap(source), function (item) {
var result = {};
result.label = labelProp ? unwrap(item[labelProp]) : unwrap(item).toString(); //show in pop-up choices
result.value = inputValueProp ? unwrap(item[inputValueProp]) : unwrap(item).toString(); //show in input box
result.actualValue = valueProp ? unwrap(item[valueProp]) : item; //store in model
return result;
});
return mapped;
}, null, { disposeWhenNodeIsRemoved: element });
//whenever the items that make up the source are updated, make sure that autocomplete knows it
mappedSource.subscribe(function (newValue) {
$(element).autocomplete("option", "source", newValue);
});
options.source = mappedSource();
//initialize autocomplete
$(element).autocomplete(options);
},
update: function (element, valueAccessor, allBindings, viewModel) {
//update value based on a model change
var allBindings = allBindingsAccessor(),
unwrap = ko.utils.unwrapObservable,
modelValue = unwrap(allBindings.jqAutoValue) || '',
valueProp = allBindings.jqAutoSourceValue,
inputValueProp = allBindings.jqAutoSourceInputValue || valueProp;
//if we are writing a different property to the input than we are writing to the model, then locate the object
if (valueProp && inputValueProp !== valueProp) {
var source = unwrap(allBindings.jqAutoSource) || [];
var modelValue = ko.utils.arrayFirst(source, function (item) {
return unwrap(item[valueProp]) === modelValue;
}) || {}; //probably don't need the || {}, but just protect against a bad value
}
//update the element with the value that should be shown in the input
$(element).val(modelValue && inputValueProp !== valueProp ? unwrap(modelValue[inputValueProp]) : modelValue.toString());
}
}
};
moduleDance(factory);
})();
I have not fully understood your question. But knockout is not relevant to UIComplete. Please see a simple example using UI complete.
async function autocomplete() {
const sthings= await getSthings(); //gets json array, or ajax call, this is a promise
$("#sthHighlightSearch").autocomplete({
source: sthings
});
//This is an extension method for autocomplete
//Should filter the list with starts with characters written in the autocomplete
$.ui.autocomplete.filter = function (array, term) {
var matcher = new RegExp("^" + $.ui.autocomplete.escapeRegex(term), "i");
return $.grep(array, function (value) {
return matcher.test(value.label || value.value || value);
});
};
}
I'm using gmail.js for some project. In the library, there is a function like this :
api.dom.compose = function(element) {
// stuff
}
api.dom.email = function(element) {
this.id = element;
var message_class_id = 'm' + this.id;
this.id_element = $('div.ii.gt div.a3s.aXjCH.' + message_class_id);
element = this.id_element.closest('div.adn');
this.$el = element;
return this;
};
$.extend(api.dom.email.prototype, {
body: function(body) {
var el = this.dom('body');
if (body) {
el.html(body);
}
return el.html();
},
from: function(email, name) {
var el = this.dom('from');
if (email) {
el.attr('email',email);
}
if (name) {
el.attr('name',name);
el.html(name);
}
return {
email: el.attr('email'),
name: el.attr('name'),
el: el
};
},
// more extended functions
});
// more functions on the api.dom object
return api;
In my code I'm using it like so :
var email = provider.dom.email(mId);
console.log(email);
The console.log is really surprising. I was expecting to see the functions from the $.extend section. In that place, the functions showing are those registered on the api.dom object ! email() itself, compose, and more.
I don't get at all why this is happening. Thanks ahead for any help.
It was the prototype that has been extended. The functions are available when creating an instance with new. So do a console.log(api.dom.email.prototype); or create a new instance with new.
var email = new provider.dom.email(mId);
console.log(email);
After much research and trail and error, I haven't come up with a solution yet. Please help! The SearchCustomer method in the code has comments on the scenarios that work and don't work.
Situation
I use knockoutjs with the mapping plugin. I take a view model which contains a Workorder from the server and it contains some properties about it along with a Customer model underneath it and a Contact model underneath Customer.
On the workorder screen the user can search for a customer which pops up a modal search window. They select that customer and the customer's id and customer model comes back to the workorder. I update the workorder's customerID no problem, but when I try to update the customer data (including contact) I get the Function Expected error.
Code
function WorkorderViewModel(data) {
var self = this;
data = data || {};
mapping = {
'Workorder': {
create: function (options) {
return new Workorder(options.data, self);
}
}
}
ko.mapping.fromJS(data, mapping, self);
self.ViewCustomer = function () {
self.Workorder.Customer.View();
}
self.SearchCustomer = function () {
self.Workorder.Customer.Search(function (customerID, customer) {
self.Workorder.CustomerID(customerID); //Works
self.Workorder.Customer(customer) //Function Expected, I feel this should work! Please help!
self.Workorder.Customer = new Customer(customer, self.Workorder); //No Error doesn't update screen
self.Workorder.Customer.Contact.FirstName(customer.Contact.FirstName); //Works, updates screen, but I don't want to do this for every property.
self.Workorder.SaveAll(); //Works, reload page and customer data is correct. Not looking to reload webpage everytime though.
})
}
}
function Workorder(data, parent) {
var self = this;
data = data || {};
mapping = {
'Customer': {
create: function (options) {
return new Customer(options.data, self);
}
}
}
ko.mapping.fromJS(data, mapping, self);
}
function Customer(data, parent) {
var self = this;
data = data || {};
mapping = {
'Contact': {
create: function (options) {
return new Contact(options.data, self);
}
}
}
ko.mapping.fromJS(data, mapping, self);
}
function Contact(data, parent) {
var self = this;
data = data || {};
mapping = {};
ko.mapping.fromJS(data, mapping, self);
self.AddedOn = ko.observable(moment(data.AddedOn).year() == 1 ? '' : moment(data.AddedOn).format('MM/DD/YYYY'));
self.FullName = ko.computed(function () {
var fullName = '';
if (self.FirstName() != null && self.FirstName() != '') {
fullName = self.FirstName();
}
if (self.MiddleName() != null && self.MiddleName() != '') {
fullName += ' ' + self.MiddleName();
}
if (self.LastName() != null && self.LastName() != '') {
fullName += ' ' + self.LastName();
}
return fullName;
})
}
Thanks Everyone!
Since self.Workorder.Customer is originally populated using ko.mapping, when you want to repopulate it, you should use ko.mapping again, like:
ko.mapping.fromJS(customer, self.Workorder.Customer)
Try changing:
self.Workorder.Customer(customer);
to:
self.Workorder.Customer = customer;
My guess is that the Customer property of the Workorder is not an observable.
I am trying to return an object method on the event jQuery.change() of a text field,
here is the code:
var Utente = function(indice){
this.Indice = indice;
this.Dati = new Array();
this.initialize = function() {
this.Dati['stato_civile'] = this.getField('stato_civile').val();
this.onChange('stato_civile',this.checkObbligatorieta);
}
this.getField = function(name) {
return $('#'+indice+name);
}
this.onChange = function(field, func) {
this.getField(field).live('change',function() {
return func.apply();
});
}
this.checkObbligatorieta = function() {
this.Dati['stato_civile'] = this.getField('stato_civile').val();
[...]
}
this.initialize();
}
Using this I get the field "#stato_civile" returns the function this.checkObbligatorieta correctly but it gives me an error:
** this.getField('stato_civile').val() is not a function
I think it's something strictly related with the scope, but I can't figure it out.
That's because you're not invoking func() in the same context as the caller, so this is not bound to the same object.
You can fix the problem by passing this to apply():
this.onChange = function(field, func) {
this.getField(field).live("change", function() {
return func.apply(this);
});
};
I'm writing a lightweight jQuery plugin to detect dirty forms but having some trouble with events. As you can see in the following code, the plugin attaches an event listener to 'beforeunload' that tests if a form is dirty and generates a popup is that is the case.
There is also another event listener attached to that form's "submit" that should in theory remove the 'beforeunload' listener for that specific form (i.e. the current form I am submitting should not be tested for dirt, but other forms on the page should be).
I've inserted a bunch of console.log statements to try and debug it but no luck. Thoughts?
// Checks if any forms are dirty if leaving page or submitting another forms
// Usage:
// $(document).ready(function(){
// $("form.dirty").dirtyforms({
// excluded: $('#name, #number'),
// message: "please don't leave dirty forms around"
// });
// });
(function($) {
////// private variables //////
var instances = [];
////// general private functions //////
function _includes(obj, arr) {
return (arr._indexOf(obj) != -1);
}
function _indexOf(obj) {
if (!Array.prototype.indexOf) {
Array.prototype.indexOf = function (obj, fromIndex) {
if (fromIndex == null) {
fromIndex = 0;
} else if (fromIndex < 0) {
fromIndex = Math.max(0, this.length + fromIndex);
}
for (var i = fromIndex, j = this.length; i < j; i++) {
if (this[i] === obj)
return i;
}
return -1;
};
}
}
////// the meat of the matter //////
// DirtyForm initialization
var DirtyForm = function(form, options) {
// unique name for testing purposes
this.name = "instance_" + instances.length
this.form = form;
this.settings = $.extend({
'excluded' : [],
'message' : 'You will lose all unsaved changes.'
}, options);
// remember intial state of form
this.memorize_current();
// activate dirty tracking, but disable it if this form is submitted
this.enable();
$(this.form).on('submit', $.proxy(this.disable, this));
// remember all trackable forms
instances.push(this);
}
// DirtyForm methods
DirtyForm.prototype = {
memorize_current: function() {
this.originalForm = this.serializeForm();
},
isDirty: function() {
var currentForm = this.serializeForm();
console.log("isDirty called...")
return (currentForm != this.originalForm);
},
enable: function() {
$(window).on('beforeunload', $.proxy(this.beforeUnloadListener, this));
console.log("enable called on " + this.name)
},
disable: function(e) {
$(window).off('beforeunload', $.proxy(this.beforeUnloadListener, this));
console.log("disable called on " + this.name)
},
disableAll: function() {
$.each(instances, function(index, instance) {
$.proxy(instance.disable, instance)
});
},
beforeUnloadListener: function(e) {
console.log("beforeUnloadListener called on " + this.name)
console.log("... and it is " + this.isDirty())
if (this.isDirty()) {
e.returnValue = this.settings.message;
return this.settings.message;
}
},
setExcludedFields: function(excluded) {
this.settings.excluded = excluded;
this.memorize_current();
this.enable();
},
serializeForm: function() {
var blacklist = this.settings.excludes
var filtered = [];
var form_elements = $(this.form).children();
// if element is not in the excluded list
// then let's add it to the list of filtered form elements
if(blacklist) {
$.each(form_elements, function(index, element) {
if(!_includes(element, blacklist)) {
filtered.push(element);
}
});
return $(filtered).serialize();
} else {
return $(this.form).serialize();
}
}
};
////// the jquery plugin part //////
$.fn.dirtyForms = function(options) {
return this.each(function() {
new DirtyForm(this, options);
});
};
})(jQuery);
[EDIT]
I ended up fixing this by using jQuery's .on() new namespace feature to identify the handler. The problem was that I was passing new anonymous functions as the handler argument to .off(). Thanks #FelixKling for your solution!
this.id = instances.length
[...]
enable: function () {
$(window).on('beforeunload.' + this.id, $.proxy(this.beforeUnloadListener, this));
},
disable: function () {
$(window).off('beforeunload.' + this.id);
},
Whenever you are calling $.proxy() it returns a new function. Thus,
$(window).off('beforeunload', $.proxy(this.beforeUnloadListener, this));
won't have any effect, since you are trying to unbind a function which was not bound.
You have to store a reference to the function created with $.proxy, so that you can unbind it later:
enable: function() {
this.beforeUnloadListener = $.proxy(DirtyForm.prototype.beforeUnloadListener, this);
$(window).on('beforeunload', this.beforeUnloadListener);
console.log("enable called on " + this.name)
},
disable: function(e) {
$(window).off('beforeunload', this.beforeUnloadListener);
console.log("disable called on " + this.name)
},