Using select2.js with multiple select optgroup and knockoutJS - javascript

I am trying to achieve a databinding for multiple selection with optgroup using knockoutJS. In addition we would like to use select2 for its search and display capabilities.
Here is the fiddle sample.
Everything works well when the items are added directly using the html control. You may pickup some countries in the example above and click the view button to see that the code of the countries are well retrieved. However, I would like to populate the items another way. Precisely, I created a command to flush the observable array containing the selected items and force the first item in the list of available options to be selected (which is the country Laos in our example). This command is executed when clicking the second button.
After clicking this latter button, you can check that the observable selectedCountries contains the expected values by clicking the first button. Unfortunately, the UI control is not refreshed, do you have an idea how to do that? The html databiding for my view looks like
<select class="multi-select" data-bind="foreach: availableCountries,selectedOptions:selectedCountries" multiple="multiple">
<optgroup data-bind="attr: {label: label}, foreach: children">
<option data-bind="text: display, value: code"></option>
</optgroup>
</select>

The short answer is that Select2 doesn't know about changes you make to the underlying model.
I was able to make your sample work using a hack, see here: http://jsfiddle.net/bXPM6/
The changes made are:
<select id="foo" class="multi-select" data-bind="foreach: availableCountries, selectedOptions:selectedCountries" multiple="multiple">
(Note the added id=foo).
And I added a subscription to the observable:
function MyViewModel(){
var self = this;
self.availableCountries = ko.observableArray(app.availableCountries());
self.selectedCountries = ko.observableArray([]);
// added this bit
self.selectedCountries.subscribe(function (newValue) {
$('#foo').select2("val", newValue);
});
}
The better option is to make a custom knockout binding that can keep Select2 updated with changes to your model.
Something like this:
HTML:
<select class="multi-select" data-bind="foreach: availableCountries, selectedOptions:selectedCountries, select2: selectedCountries" multiple="multiple">
JavaScript:
ko.bindingHandlers.select2 = {
update: function (element, valueAccessor) {
$(element).select2("val", ko.unwrap(valueAccessor()) || "");
}
};
Hope this is of some help.

Related

Set Knockout dropdown value dynamically

I have a dropdown value which is proving very difficult to set in the console. I have tried using Jquery .val and using document.getEWlementById.value and both will not set the item. Does anyone know how I can set the value of the dropdown using the value? I think the problem is that it is using Knockout which makes it more difficult to set it dynamically.
Here is the HTML:
<select id="sourceShippingLocations" data-bind="options: $root.ShippingLocations, optionsText:'Name', optionsCaption:'Select location', value: $root.SelectedOriginShippingLocation" class="form-control" title="">
<option value="">Select location</option>
<option value="">doo Warehouse</option>
<option value="">moo</option>
<option value="">Manchester</option>
</select>
Knockout doesn't make it "more difficult to set dynamically". It just works differently.
In knockout, a viewmodel dictates what happens in the view. In other words: you don't set the value of the select through modifying the attribute directly, but you change the selected value of the underlying model and knockout manages the UI state for you.
Each of the <option> elements represents a value in an array named $root.ShippingLocations. The selected value is stored in $root.SelectedOriginShippingLocation.
In the viewmodel, you'd update the current selection by doing something like:
this.SelectedOriginShippingLocation(this.ShippingLocations()[0])
(this sets the selection to the first shipping location)
If you want to see this in action without having to modify the viewmodel, you can hack this in your console:
var root = ko.contextFor(document.getElementById("sourceShippingLocations")).$root;
root.SelectedOriginShippingLocation(ko.unwrap(root.ShippingLocations)[0]);
// change 0 for the index you like

AngularJS ng-repeat render once and reuse

A view of my AngularJS app makes heavy use of ng-repeat directive. It is done like this:
<div ng-repeat="branches in company">
<p>{{branches.name}}</p>
<p>{{branches.location}}</p>
<div>
<select ng-model="branches.officeInformationType">
<option ng-repeat="offices in branches">{{offices.type}}</option>
</select>
<select ng-model="branches.officeInformationMeters">
<option ng-repeat="offices in branches">{{offices.meters}}</option>
</select>
<select ng-model="branches.officeInformationColor">
<option ng-repeat="offices in branches">{{offices.color}}</option>
</select>
</div>
</div>
The fact is, the second ng-repeat and and the others after it (offices in branches) are actually the same everytime, so it wouldn't need to be recalculated for every branch. It would need to be binded to the row it belonges to, for saving it later, so the branches.officeInformation model should still be watched by angular, but I would like to make the whole program more performant.
I am using angular-ui-router and when I change the view between my "Choose your office" view and any other, the lag is tremendous, almost at a minute of wait time when you leave the "Choose your office" page. It renders fast enough, 2 seconds for the whole rendering, but when I leave the page it takes a ton of time to change to the other view.
Any ideas, taking into consideration that the ng-model binding "branches.officeInformation.." is of importance?
EDIT: I have tried remove the nested ng-repeats and for each ng-repeat that I removed, the transition between states got faster and faster. When I removed all the nested ng-repeats the transition became instantaneous, hence why I believe it has to do with the ng-repeats.
The ng-repeats are tracked by $index and where possible I used :: for one time binding.
Thanks.
We can lazy load a dropdown's options right before the user interacts with it.
First, we initialize each dropdown with only the selected option, so you can see it when the dropdown is closed.
Then we attach an ng-focus directive to each dropdown. When our callback fires we can:
fully populate the options for that dropdown
remove all but the selected option from the previously active dropdown
I wasn't entirely sure of the structure of your data (it looks like some arrays have additional properties on them). So I chose to create "view model" objects that represent the UI. You can adapt this to your own structure.
Controller:
// Set up some test office options (null for no selection)
var allOffices = [null];
for (var i = 0; i < 50; i++) {
allOffices.push(i);
}
// activeDropdown holds the dropdown that is currently populated with the full list
// of options. All other dropdowns are only populated with the selected option so
// that it shows when the dropdown is closed.
var activeDropdown;
$scope.company = [
// Branch 1
[
// These objects represent each dropdown
{
// Just the selected option until the user interacts with it
options: ["0"],
selected: "0"
}, {
// Just the selected option until the user interacts with it
options: ["1"],
selected: "1"
}, {
// Just the selected option until the user interacts with it
options: [null],
selected: null
}
],
// Branch 2
[
// These objects represent each dropdown
{
// Just the selected option until the user interacts with it
options: ["2"],
selected: "2"
}, {
// Just the selected option until the user interacts with it
options: ["3"],
selected: "3"
}, {
// Just the selected option until the user interacts with it
options: [null],
selected: null
}
]
];
// When the user interacts with a dropdown:
// - fully populate the array of options for that dropdown
// - remove all but the selected option from the previously active dropdown's
// options so that it still shows when the dropdown is closed
$scope.loadOffices = function (dropdown) {
if (activeDropdown === dropdown) {
return;
}
dropdown.options = allOffices;
if (activeDropdown) {
activeDropdown.options = [activeDropdown.selected];
}
activeDropdown = dropdown;
};
Template:
<div ng-repeat="branch in company">
<div ng-repeat="dropdown in branch">
Selected: {{ dropdown.selected }}
<select ng-focus="loadOffices(dropdown)" ng-model="dropdown.selected">
<option ng-repeat="o in dropdown.options">{{ o }}</option>
</select>
</div>
</div>
Note that ng-focus was the only directive I needed to apply to each dropdown when I tested this. But you may need to add ng-keydown, ng-mouseover, ng-click, or others to get it to work in all scenarios including mobile.
I also noticed a potential styling issue. When you focus on a dropdown, we load all of the options for that dropdown. This may cause the width of the dropdown to change, so if you can set the same width for all of them you should be good.
If the number of options in each dropdown is huge, we may be able to optimize even further by writing some custom directives that interact and allow the actual DOM element options to be shared. But I suspect we won't have to go that far for this example.
Have you tried 'track by $index' ? it will reduce angular watches overhead.
something like that:
div ng-repeat="branches in company track by $index">
<p>{{branches.name}}</p>
<p>{{branches.location}}</p>
<div>
<select ng-model="branches.officeInformationType">
<option ng-repeat="offices in branches track by $index">{{offices.type}}</option>
</select>
<select ng-model="branches.officeInformationMeters">
<option ng-repeat="offices in branches track by $index">{{offices.meters}}</option>
</select>
<select ng-model="branches.officeInformationColor">
<option ng-repeat="offices in branches track by $index">{{offices.color}}</option>
</select>
</div>
</div>
First and foremost, thanks to those that helped me find the answer.
The problem was that I nested too many ng-repeats with too many event handlers attached to each repeated element. ng-models, ng-changes and ng-clicks were really heavy, but the number of elements was also out of control.
I solved this by using a single select without any nested ng-repeats, this select (and the options) are in a modal view, so a different controller. From that controller I return the select results, having only one select for all the elements in the page. When the data is returned from the modal, I use it from the main controller of the view.
Thanks again.

One ng-options is dependent on what is selected in another - AngularJS

I have a form in AngularJS where the options for one of the dropdowns depends on what is selected in the first.
<select ng-options="obj.name for obj in institutions track by obj.id" ng-model="newUser.institution">
</select>
<input type="text" name="email" ng-model="newUser.email">
<!-- DEPENDENT on what is selected in the previous dropdown -->
<select ng-options="obj.name for obj in organizations track by obj.id" ng-model="newUser.associatedOrg">
</select>
Institutions are all loaded right off the bat from available institutions in the database. That is done in the controller like this:
function populate() {
return Institutions.all().then(function (institutions) {
$scope.institutions = institutions;
return institutions;
});
}
// at the end of the controller
populate();
However, those "organizations" in the second dropdown are based on their parent table institution, so I need to do something like $scope.organizations = institution.memberOrganizations; in the controller after an option from the first dropdown is selected.
For now, just to make sure things work, I have made a button called "Load Organizations" with an ng-click for this function:
$scope.getOrganizationsOnCampus = function(institution) {
$scope.organizations = institution.memberOrganizations;
};
That works, however that is a really bad user experience.
So my question is: How do I update $scope.organizations every time a new institution is selected?
I want to do this without listening to the DOM - like you would in jQuery, because I know that is way against the AngularJS best practices.
P.S. For further clarity, here is a screenshot of before, and after that "Load Organizations" button is clicked, to load the child organizations of the selected institution. This is what I want to automatically do every time a different institution is selected in the previous option.
BEFORE
AFTER
Would you use ng-change like this:
<select ng-options="obj.name for obj in institutions track by obj.id" ng-model="newUser.institution" ng-change"updateOrg(newUser.institution)">
</select>
and then in your controller, you have a function like this:
$scope.updateOrg = function(institution) {
$scope.organizations = institution.memberOrganizations;
};

Grails if checks html select value

I have a problem with checking the select/grails select tag value via grails if tag. My code looks like this,
html:
<select id="select1" name="select1">
<option value=2>2</option>
<option value=3>3</option>
</select>
or grails select:
<g:select id="select1" name="select1" from="${[2, 3] }" value="2"/>
and the grails if:
...
<g:each var="rowNumber" in = "${0..5}">
<g:if test="${rowNumber} < ${select1.value}"> <!-- or ${select1.val()} or select1.value -->
...
</g:if>
</g:each>...
Code throws an NullPointerException, and says that select1 is a null object and he cannot evaluate method val(), or cannot get attribute value.
Anyone have an idea what I should do to fix this problem?
Thanks for help!
EDIT
Inside my if statement I have a render template, and when I change the value of select I want to render this templates again, but with saving what I have already type there (e.g if I have a textfield in my template).
EDIT2
I messed up a little bit. I want to create a dynamic table, e.g at start it could have 2 rows & columns, then I want to be able to enlarge/decreasenumber of rows/columns (e.g. by clicking button/buttons) of course by clicking the button, I want to save already filled table in ajax, then render table with new number of rows/columns, and fill earlier filled cell with their previous values (new cells will be empty).
e.g.
filled table 2x2
a a
a a
when I enlarge this table to 3x2 I want the table looks like this:
a a
a a
_ _
where _ is an empty cell.
Since you want to work on the client side you need to work with Javascript only.
function checkRows(rowNumber)
{
var value= $('#select1').val();
return rowNumber < elem;
}
If you want to trigger the function when the select value changes use
$(document).ready(function () {
$('#select1').change(function() {
checkRows(34 /* use your value here */);
});
});

knockout using one option list for selectedOptions on multiple selectboxes

http://jsfiddle.net/E2AMX/ has the exact demonstration of the problem, which is:
I have multiple select boxes on the same page. All the options of the selectboxes are in the given form:
<option value="#id_num">StringVal</option>
and i have one observableArray (say idlist) of id_nums with no separation regarding selectboxes. For example,
idlist = ko.observableArray([1,2,3,4]);
and the selectboxes are as
<select name="first" data-bind="selectedOptions: idlist">
...
<option value="2">Blah</option>
<option value="3">Blah</option>
...
</select>
<select name="second" data-bind="selectedOptions: idlist">
...
<option value="1">Blah</option>
...
</select>
<select name="third" data-bind="selectedOptions: idlist">
...
<option value="4">Blah</option>
...
</select>
My problem is: when i select one option from a selectbox, other selectboxes return to their initial states. This is directly related to selectedOptions, for if i remove the selectedOptions directive, this problem does not occur.
Any suggestions will be very welcomed.
Thanks.
The selectedOptions binding is meant to be used on a single <select> tag with multi-select enabled. It will keep an array of each item in the options box selected.
The reason you are seeing the behavior you are is because when you you select a single value from one of the drop downs, the selectedOptions binding immediately fires. The logic goes something like this:
Update on target <select> fires.
Binding extracts the value from <option> and updates the underlying observable array.
Observable array fires update since values have changed.
Secondary drop downs respond to update, and update their selected value based on what is in the array.
Since no value exists in the set of <option> tags, the value is cleared.
This is why you are seeing this behavior. If you want to collect a composite from all selected options, then you will either need to write a new custom binding, or create a seperate array for each <select> you want to bind to.

Categories