I'm having a strange issue where the value found in Vue DevTools is correct. It's declared in my data as expected. The first time I click on "Edit" an item, the correct value shows up in my browser window as well.
However, if I click on "Edit" an item that has a different quantity, the same value shows up again even if it is incorrect (it should be prepopulating from the database).
Then, if I click back on the first "Edit" item again that value will get updated with the previous value!
The craziest part is that while my browser window is not showing the correct value, the correct result is showing up in Vue DevTools at all times! The circled item in the image below is the UUID for the "Quantity" of 100, which is the correct value. Yet 700 is showing up (the previous Edit item's value). Anybody ever had this happen before and know what gives?
Here's some snippets of relevant code (it's from a Vue component using vue-resource, and this is taking place in a bootstrap modal in a Laravel project):
Vue JS
data() {
return {
selected_options: {},
attributes: [],
}
},
methods: {
editLineItem: function (line_item) {
this.getProductOptionsWithAttributes(line_item.product_id);
this.getPrepopulatedOptionsForLineItem(line_item.id);
},
getProductOptionsWithAttributes: function (product_id) {
var local_this = this;
var url = '/api/v1/products/' + product_id + '/options';
this.$http.get(url).then(function (response) {
local_this.attributes.$set(0, response.data);
}, function (response) {
// error handling
});
},
getPrepopulatedOptionsForLineItem: function (id) {
var local_this = this;
var url = '/api/v1/line_items/' + id + '/options';
this.$http.get(url).then(function (response) {
Object.keys(response.data).forEach(function (key) {
Vue.set(local_this.selected_options, key, response.data[key]);
});
}, function (response) {
//#TODO Implement error handling.
});
},
}
HTML
<div v-for="(key, attribute) in attributes[0]" class="col-md-12 selectbox_spacing">
<label for="option_{{$index}}">{{key}}</label><br/>
<select class="chosen-select form-control" v-model="selected_options[key]" v-chosen="selected_options[key]" id="option_{{$index}}">
<option v-for="option in attribute" value="{{option.id}}">{{option.name}}</option>
</select>
</div>
<button v-on:click="editLineItem(line_item)">
Main.js vue-directive:
Vue.directive('chosen', {
twoWay: true, // note the two-way binding
bind: function () {
$(this.el)
.change(function(ev) {
// two-way set
//this.set(this.el.value);
var i, len, option, ref;
var values = [];
ref = this.el.selectedOptions;
if(this.el.multiple){
for (i = 0, len = ref.length; i < len; i++) {
option = ref[i];
values.push(option.value)
}
this.set(values);
} else {
this.set(ref[0].value);
}
}.bind(this));
},
update: function(nv, ov) {
// note that we have to notify chosen about update
$(this.el).trigger("chosen:updated");
}
});
var vm = new Vue({
el : '#wrapper',
components: {
LineItemComponent
}
});
Script in edit.blade.php file:
<script>
$(document).ready(function() {
$('#lineItemModal').on('shown.bs.modal', function () {
$('.chosen-select', this).chosen('destroy').chosen();
});
}
</script>
by default, custom directives have a priority of 1000. v-model has a priority of 800 meaning it's evaluated after v-chosen when the template is compiled.
My Assumption is now: this is also affecting the update.
What I mean by that: I think $(this.el).trigger("chosen:updated"); in the v-chosen update method is called before v-model did refresh the selected attribute on the list of <option> elements - and that's where chosen checks for the new selected value.
Long story short: try this:
Vue.directive('chosen', {
priority: 700, // Priority lower than v-model
twoWay: true, // note the two-way binding
bind: function () {
....
Related
I added an Autocomplete feature to a form on a HTML template, i would like to perform some actions when an hint is selected, is there any way to do it? I'm using Jquery-Typeahead. Here is my actual code:
$(document).ready(function(){
// Defining the local dataset
$.getJSON('http://127.0.0.1:8000/myapi', function(data) {
console.log(data)
var dt = data
$(() => {
$('#myform').typeahead({
source: {
data: dt.results.map(record => record.item)
},
callback: {
onInit: function($el) {
console.log(`Typeahead initiated on: ${$el.prop('tagName')}#${$el.attr('id')}`);
},
onClick: function() {
console.log(); //How can i console.log() the selected value here, for example?
}
}
});
});
});
});
Try defining an onClickAfter callback, it's called right after user clicks on an item. Something like this:
onClickAfter: function(node, a, item, event) {
// item will be the item you selected
console.log(item);
}
You can also define the onClickBefore callback the same way, and it will be called immediately before "normal" typeahead behaviour kicks in
I have an MVC Control for a KendoUI ComboBox that does NOT setup the Change Event ahead of time. Upon rendering, a page controller sets-up & shims-in its' own Change Event.
Oddly, this event gets called TWICE:
When I change the Selected Item
When I click away from the control
Q: What am I doing wrong?
Q: Is this HOW we should over-write the change event on an existing Kendo ComboBox?
MVC CONTROL:
As you can see, I am NOT defining any client-side events here...
#(Html.Kendo().ComboBox()
.Name("ddlTechnician")
.Filter("contains")
.Placeholder("Select Technician...")
.DataTextField("Text")
.DataValueField("Value")
.BindTo(new List<SelectListItem>() {
new SelectListItem() { Text = "Frank", Value = "1" },
new SelectListItem() { Text = "Suzie", Value = "2" },
new SelectListItem() { Text = "Ralph", Value = "3" }
})
.Suggest(true)
.HtmlAttributes(new { style = "width:300px;" }))
PAGE CONTROLLER:
And, I am only defining the event ONCE here. I have also confirmed the event isn't already firing BEFORE setting it in the Page Controller
$(document).ready(function () {
var PageController = (function ($) {
function PageController(options) {
var that = this,
empty = {},
dictionary = {
elements: {
form: null
},
instances: {
ddlTechnician: null
},
selectors: {
form: 'form',
ddlTechnician: '#ddlTechnician'
}
};
var initialize = function (options) {
that.settings = $.extend(empty, $.isPlainObject(options) ? options : empty);
dictionary.elements.form = $(dictionary.selectors.form);
// Objects
dictionary.instances.ddlTechnician = $(dictionary.selectors.ddlTechnician, dictionary.elements.form).data('kendoComboBox');
// Events
dictionary.instances.ddlTechnician.setOptions({ change: that.on.change.kendoComboBox });
};
this.settings = null;
this.on = {
change: {
kendoComboBox: function (e) {
// This is getting called MULTIPLE TIMES
console.log('kendoComboBox RAN');
}
}
}
};
initialize(options);
}
return PageController;
})(jQuery);
var pageController = new PageController({});
});
I was able to reproduce your problem on a Kendo JQuery Combobox when I set the event handler through setOptions, which is not the recommended way after the widget has been rendered. Instead you should use the "bind" method as shown in the documentation's example for change events.
Try changing the line of code where you set your event handler to this:
dictionary.instances.ddlTechnician.bind("change", that.on.change.kendoComboBox);
Here's a dojo that shows the difference: http://dojo.telerik.com/iyEQe
Hope this helps.
I am building a chat application and on my "new chats" page I have a list of contacts, which you can select one by one by tapping them (upon which I apply a CSS selected class and push the user id into an array called 'newChatters'.
I want to make this array available to a helper method so I can display a reactive list of names, with all users who have been added to the chat.
The template that I want to display the reactive list in:
<template name="newChatDetails">
<div class="contactHeader">
<h2 class="newChatHeader">{{newChatters}}</h2>
</div>
</template>
The click contactItem event triggered whenever a contact is selected:
Template.contactsLayout.events({
'click #contactItem': function (e) {
e.preventDefault();
$(e.target).toggleClass('selected');
newChatters.push(this.username);
...
The newChatters array is getting updated correctly so up to this point all is working fine. Now I need to make {{newChatters}} update reactively. Here's what I've tried but it's not right and isn't working:
Template.newChatDetails.helpers({
newChatters: function() {
return newChatters;
}
});
How and where do I use Deps.autorun() to make this work? Do I even need it, as I thought that helper methods auto update on invalidation anyway?
1) Define Tracker.Dependency in the same place where you define your object:
var newChatters = [];
var newChattersDep = new Tracker.Dependency();
2) Use depend() before you read from the object:
Template.newChatDetails.newChatters = function() {
newChattersDep.depend();
return newChatters;
};
3) Use changed() after you write:
Template.contactsLayout.events({
'click #contactItem': function(e, t) {
...
newChatters.push(...);
newChattersDep.changed();
},
});
You should use the Session object for this.
Template.contactsLayout.events({
'click #contactItem': function (e) {
//...
newChatters.push(this.username);
Session.set('newChatters', newChatters);
}
});
and then
Template.newChatDetails.helpers({
newChatters: function() {
return Session.get('newChatters');
}
});
You could use a local Meteor.Collection cursor as a reactive data source:
var NewChatters = new Meteor.Collection("null");
Template:
<template name="newChatDetails">
<ul>
{{#each newChatters}}
<li>{{username}}</li>
{{/each}}
</ul>
</template>
Event:
Template.contactsLayout.events({
'click #contactItem': function (e) {
NewChatters.insert({username: this.username});
}
});
Helper:
Template.newChatDetails.helpers({
newChatters: function() { return NewChatters.find(); }
});
To mimick the behaviour of Session without polluting the Session, use a ReactiveVar:
Template.contactsLayout.created = function() {
this.data.newChatters = new ReactiveVar([]);
}
Template.contactsLayout.events({
'click #contactItem': function (event, template) {
...
template.data.newChatters.set(
template.data.newChatters.get().push(this.username)
);
...
Then, in the inner template, use the parent reactive data source:
Template.newChatDetails.helpers({
newChatters: function() {
return Template.parentData(1).newChatters.get();
}
});
for people who is looking for a workaround for this in the year 2015+ (since the post is of 2014).
I'm implementing a posts wizard pw_module where I need to update data reactively depending on the route parameters:
Router.route('/new-post/:pw_module', function(){
var pwModule = this.params.pw_module;
this.render('post_new', {
data: function(){
switch (true) {
case (pwModule == 'basic-info'):
return {
title: 'Basic info'
};
break;
case (pwModule == 'itinerary'):
return {
title: 'Itinerary'
};
break;
default:
}
}
});
}, {
name: 'post.new'
});
Later in the template just do a:
<h1>{{title}}</h1>
Changing routes
The navigation that updates the URL looks like this:
<nav>
Basic info
Itinerary
</nav>
Hope it still helps someone.
I have a sortable accordion loaded with a foreach-template loop over a ko.observableArray() named "Tasks".
In the accordion I render the TaskId, the TaskName, and a task Description - all ko.observable().
TaskName and Description is rendered in input/textarea elements.
Whenever TaskName or Description is changed, an item is de-selected, or another item is clicked on, I want to call a function saveEdit(item) to send the updated TaskName and Description to the database via an ajax request.
I need to match the TaskId with the Tasks-array to fetch the actual key/value-pair to send to the saveEdit().
This is the HTML:
<div id="accordion" data-bind="jqAccordion:{},template: {name: 'task-template',foreach: Tasks,afteradd: function(elem){$(elem).trigger('valueChanged');}}"></div>
<script type="text/html" id="task-template">
<div data-bind="attr: {'id': 'Task' + TaskId}" class="group">
<h3><b><span data-bind="text: TaskId"></span>: <input name="TaskName" data-bind="value: TaskName /></b></h3>
<p>
<label for="Description" >Description:</label><textarea name="Description" data-bind="value: Description"></textarea>
</p>
</div>
</script>
This is the binding:
ko.bindingHandlers.jqAccordion = {
init: function(element, valueAccessor) {
var options = valueAccessor();
$(element).accordion(options);
$(element).bind("valueChanged",function(){
ko.bindingHandlers.jqAccordion.update(element,valueAccessor);
});
},
update: function(element,valueAccessor) {
var options = valueAccessor();
$(element).accordion('destroy').accordion(
{
// options put here....
header: "> div > h3"
, collapsible: true
, active: false
, heightStyle: "content"
})
.sortable({
axis: "y",
handle: "h3",
stop: function (event, ui) {
var items = [];
ui.item.siblings().andSelf().each(function () {
//compare data('index') and the real index
if ($(this).data('index') != $(this).index()) {
items.push(this.id);
}
});
// IE doesn't register the blur when sorting
// so trigger focusout handlers to remove .ui-state-focus
ui.item.children("h3").triggerHandler("focusout");
if (items.length) $("#sekvens3").text(items.join(','));
ui.item.parent().trigger('stop');
}
})
.on('stop', function () {
$(this).siblings().andSelf().each(function (i) {
$(this).data('index', i);
});
})
.trigger('stop');
};
};
My first thought was to place the line
$root.SelectedTask( ui.options.active );
in an .on('click') event function where SelectedTask is a ko.observable defined in my viewModel. However, the .on('click') event seems to be called a lot and it's generating a lot of traffic. Also, I canĀ“t quite figure out where to put the save(item) call that sends the selected "item" from Tasks via an ajax-function to the database.
Any help is highly appreciated. Thanks in advance! :)
Whenever TaskName or Description is changed, an item is de-selected, or another item is clicked on, I want to call a function saveEdit(item) to send the updated TaskName and Description to the database via an ajax request.
This sounds like the core of what you want to do. Let's start out with a Task model
function Task (data) {
var self = this;
data = data || {};
self.id = ko.observable(data.id);
self.name = ko.observable(data.name);
self.description = ko.observable(data.description);
}
And then we need our View Model:
function ViewModel () {
var self = this;
self.tasks = ko.observableArray();
self.selectedTask = ko.observable();
self.saveTask = function (task) {
$.ajax({ ... });// ajax call that sends the changed data to the server
};
var taskSubscription = function (newValue) {
self.saveTask(self.selectedTask());
};
var nameSubscription, descriptionSubscription;
self.selectedTask.subscribe(function (newlySelectedTask) {
if (newlySelectedTask instanceof Task) {
nameSubscription =
newlySelectedTask.name.subscribe(taskSubscription);
descriptionSubscription =
newlySelectedTask.description.subscribe(taskSubscription);
self.saveTask(newlySelectedTask);// But why?
}
});
self.selectedTask.subscribe(function (currentlySelectedTask) {
if (currentlySelectedTask instanceof Task) {
nameSubscription.dispose();
descriptionSubscription.dispose();
self.saveTask(currentlySelectedTask);// But why?
}
}, null, 'beforeChange');
}
So what's going on here? Most of this should be pretty self explanatory so I'm just going to focus on the subscriptions. We created a taskSubscription function so we're not constantly having it defined every time the self.selectedTask changes.
We have two subscriber functions. The first fires after the selectedTask's value has changed and the second fires before it changes. In both, we verify that the new value is an instance of a Task object. In the after change subscription, we set up two subscriptions on the name and description properties. Then I capture the return value from the subscription function into two private variables. These are used in the before change function to dispose of those subscriptions so that if those Tasks are ever updated when they're not currently selected, then we don't continue to fire off the saveTask function.
I've also added self.saveTask in each of the subscriptions to the selectedTask observable. I asked why in here because, why save it if we don't know if the value has changed or not? You may be making ajax requests needlessly here.
Also, as demonstrated by this code, you can set up these subscriptions to make ajax requests every time the value changes but that may end up making a LOT of requests. A better option might be to set up functionality in your Task model that can track whether or not it is 'dirty' or not. Meaning one or more of its values have changed that requires updating.
function Task (data) {
var self = this;
// Make a copy of the data object coming in and use this to save previous values
self._data = data = $.extend(true, { id: null, name: null, description: null }, data);
self.id = ko.observable(data.id);
self.name = ko.observable(data.name);
self.description = ko.observable(data.description);
for (var prop in data) {
if (ko.isSubscribable(self[prop])) {
self[prop].subscribe(function (oldValue) {
data[prop] = oldValue;
}, null, 'beforeChange');
}
}
}
Task.prototype.isDirty = function () {
var self = this;
for (var prop in self._data) {
if (ko.isSubscribable(self[prop])) {
if (self._data[prop] !== self[prop]())
return true;
}
}
return false;
};
And of course you need a way to save it, or make it not dirty
Task.prototype.save = function () {
var self = this;
for (var prop in self._data) {
if (ko.isSubscribable(self[prop])) {
self._data[prop] = self[prop]();
}
}
};
Using the same concept you can also create Task.prototype.revert that does the opposite of what .save does. With all this in place, you could forego setting up the subscriptions on the individual name and description properties. I wanted to show that option to just demonstrate how one might want to use the .dispose method on a subscription. But now you can just subscribe to the selectedTask observable ('beforeChange') and see if the currently selected task that you're about to swap out isDirty. If it is, call the saveTask function, and when that completes, call the .save function on the Task so that it is no longer dirty.
This is probably the route I would go in implementing something like this. The beauty of it is, I haven't written a single line of code that has anything to do with the manipulating the View. You can set the selectedTask any way you see fit. What I would do is, bind the selectedTask observable to a click binding on the <h3> element inside of the accordion. That way, every time a user clicks on any of the accordions, it will potentially save the previously selected task (if any of the property values had changed).
Hopefully that addresses your scenario here of trying to save a Task when certain events are triggered.
I've got two RequireJS modules, one for fetching data from an external service, one in charge of passing a callback to the first module.
Here is the first very basic module:
define(["jquery"], function($) {
return {
/**
* Retrieves all the companies that do not employs the provided employee
* #param employeeId ID of the employee
* #param successCallback callback executed on successful request completion
* #return matching companies
*/
fetchCompanies: function(employeeId, successCallback) {
var url = '/employees/' + employeeId + '/nonEmployers';
return $.getJSON(url, successCallback);
}
};
});
And the most interesting one, that will generate a new drop-down and inject it into the specified DOM element (this is the one under test):
define([
'jquery',
'vendor/underscore',
'modules/non-employers',
'text!tpl/employeeOption.tpl'], function($, _, nonEmployers, employeeTemplate) {
var updateCompanies = function(selectedEmployeeId, companyDropDownSelector) {
nonEmployers.fetchCompanies(selectedEmployeeId, function(data) {
var template = _.template(employeeTemplate),
newContents = _.reduce(data, function(string,element) {
return string + template({
value: element.id,
display: element.name
});
}, "<option value='-1'>select a client...</option>\n");
$(companyDropDownSelector).html(newContents);
});
};
return {
/**
* Updates the dropdown identified by companyDropDownSelector
* with the companies that are non employing the selected employee
* #param employeeDropDownSelector selector of the employee dropdown
* #param companyDropDownSelector selector of the company dropdown
*/
observeEmployees: function(employeeDropDownSelector, companyDropDownSelector) {
$(employeeDropDownSelector).change(function() {
var selectedEmployeeId = $(employeeDropDownSelector + " option:selected").val();
if (selectedEmployeeId > 0) {
updateCompanies(selectedEmployeeId, companyDropDownSelector);
}
});
}
};
});
I'm trying to test this last module, using Jasmine-fixtures and using waitsFor, to asynchronously check that the set-up test DOM structure has been modified. However, the timeout is always reached.
If you can spot what's wrong in the following test, I'd be most grateful (gist:https://gist.github.com/fbiville/6223bb346476ca88f55d):
define(["jquery", "modules/non-employers", "modules/pages/activities"], function($, nonEmployers, activities) {
describe("activities test suite", function() {
var $form, $employeesDropDown, $companiesDropDown;
beforeEach(function() {
$form = affix('form[id=testForm]');
$employeesDropDown = $form.affix('select[id=employees]');
$employeesDropDown.affix('option[selected=selected]');
$employeesDropDown.affix('option[value=1]');
$companiesDropDown = $form.affix('select[id=companies]');
$companiesDropDown.affix('option');
});
it("should update the company dropdown", function() {
spyOn(nonEmployers, "fetchCompanies").andCallFake(function(employeeId, callback) {
callback([{id: 42, name: "ACME"}, {id: 100, name: "OUI"}]);
});
activities.observeEmployees('#employees', '#companies');
$('#employees').trigger('change');
waitsFor(function() {
var companiesContents = $('#companies').html(),
result = expect(companiesContents).toContain('<option value="42">ACME</option>');
return result && expect(companiesContents).toContain('<option value="100">OUI</option>');
}, 'DOM has never been updated', 10000);
});
});
});
Thanks in advance!
Rolf
P.S.: replacing $(employeeDropDownSelector).change by $(employeeDropDownSelector).on('change', and/or wrapping the activities.observeEmployees call (and $('#employees').trigger('change');) with a domReady yields the same result
P.P.S.: this error is the cause -> SEVERE: runtimeError: message=[An invalid or illegal selector was specified (selector: '[id='employees'] :selected' error: Invalid selector: *[id="employees"] *:selected).] sourceName=[http://localhost:59811/src/vendor/require-jquery.js] line=[6002] lineSource=[null] lineOffset=[0].
P.P.P.S.: it seems HtmlUnit doesn't support CSS3 selectors (WTF?), and even forcing the latest published version as jasmine-maven-plugin dependency won't change anything...
Is there any way to change jasmine plugin runner ?
OK guys.
Solution found:
upgrade (if not already) to jasmine-maven-plugin v1.3.1.1 (or later)
configure phantomjs instead of this crappy HtmlUnit (add PhantomJS binaries to your project)
if you've got use of ':focus' selector in your code, beware of this bug, replace it with $(mySelector).get(0) == document.activeElement
also, do not forget to wrap your code blocks by run(function() { /* expect */ }) if they are positioned after and depend on your waitsFor condition.
Finally, all should be well.
See how is the test now:
define(["jquery",
"modules/nonEmployers",
"modules/pages/activities"], function($, nonEmployers, activities) {
describe("activities test suite", function() {
var $form, $employeesDropDown, $companiesDropDown;
beforeEach(function() {
$form = affix('form[id=testForm]');
$employeesDropDown = $form.affix('select[id=employees]');
$employeesDropDown.affix('option[selected=selected]');
$employeesDropDown.affix('option[value=1]');
$companiesDropDown = $form.affix('select[id=companies]');
$companiesDropDown.affix('option');
spyOn(nonEmployers, "fetchCompanies").andCallFake(function(employeeId, callback) {
callback([{id: 42, name: "ACME"}, {id: 100, name: "OUI"}]);
});
});
it("should update the company dropdown", function() {
$(document).ready(function() {
activities.observeEmployees('#employees', '#companies');
$('#employees option[selected=selected]').removeAttr("selected");
$('#employees option[value=1]').attr("selected", "selected");
$('#employees').trigger('change');
waitsFor(function() {
var dropDown = $('#companies').html();
return dropDown.indexOf('ACME') > 0 && dropDown.indexOf('OUI') > 0;
}, 'DOM has never been updated', 500);
runs(function() {
var dropDown = $('#companies').html();
expect(dropDown).toContain('<option value="42">ACME</option>');
expect(dropDown).toContain('<option value="100">OUI</option>');
});
});
});
});
});
Creating modules this way is really difficult. I'd recommend not using fixtures and not rendering anywhere actually. Instead using detached DOM elements to do all the work is much easier.
Imagine if your code looked closer to this:
define([
'jquery',
'vendor/underscore',
'modules/non-employers',
'text!tpl/employeeOption.tpl'], function($, _, nonEmployers, employeeTemplate) {
return {
init: function() {
this.$companies = $('<select class="js-companies"></select>');
},
render: function(data) {
var template = _.template(employeeTemplate),
newContents = _.reduce(data, function(string,element) {
return string + template({
value: element.id,
display: element.name
});
}, "<option value='-1'>select a client...</option>\n");
this.$companies.empty().append(newContents);
return this;
});
observeEmployees: function(employeeDropDownSelector) {
$(employeeDropDownSelector).change(function() {
var selectedEmployeeId = $(employeeDropDownSelector + " option:selected").val();
if (selectedEmployeeId > 0) {
nonEmployers.fetchCompanies(selectedEmployeeId, function(data) {
this.render(data);
}
}
});
}
};
});
The above is not complete. It is just to give you an idea of another way to approach your problem. Now instead of a fixture all you need to do is inspect this.$companies and you will be done. I think the main problem though is that your functions are not simple enough. The concern of each function should be extremely specific. Your updateCompanies function is doing things like creating a template, fetching data then passing it to an anonymous function, which can't be spied on, that anonymous function iterates on an object, then you change some already existing DOM element. That sounds exhausting. All that function should do is look at some precompiled template send it an object. The template should loop on the object using {{each}} then return. Your function then empties and append the newContents and returns it self so the next function down can choose what it should do with this.$companies. Or if this.$companies has already been append to the page nothing needs to be done at all.