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.
Related
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;
};
I'm trying to remove options from drop downs based on the selections made in other drop downs.
For instance, on page load, I have one drop down, and a button which allows the creation of another drop down.
code for my operation:
<div class="form-inline" ng-repeat="setting in sensor.settings">
<select class="form-control" ng-model="setting.port" ng-options="item.name group by item.type for item in ports track by item.name">
<option value="">-- Module Port --</option>
</select>
</div>
<a href ng-click="newItem($event, sensor.settings)">New Port</a>
Expected Result:
If only one drop down exists, a user can select any option in it and no additional logic needs to be executed.
However, if the "new port" button is clicked, it'll create additional drop downs with the same options as the first, second, etc...
What I'm looking to do is remove the options which are selected in any of the drop downs from those which the option wasn't selected from to prevent duplicate selections.
So for instance, if I have 3 drop downs, and the available selections to all drop downs are D1, D2, and D3; the user selects D1 for the first, which removes option D1 from drop downs 2 & 3. D2 is selected in the second, which removes D2 from drop down 1 & 3, leaving drop down 3 with a single selection of D3.
Here's a link to my code:
I have forked your plunker, and I think I have something that should work for you here.
The important part is that I changed ng-options to use a filtered list of ports, based on the index you're currently looking at. (I changed the ng-repeat to also track the index):
<div class="form-inline" ng-repeat="(index, setting) in sensor.settings">
<select class="form-control" ng-model="setting.port" ng-options="item.name group by item.type for item in filteredPorts(index) track by item.name" ng-change="updateAll(index)">
<option value="">-- Module Port --</option>
</select>
</div>
I then implemented filteredPorts as a function that takes an index and filters out all other selected ports and returns only those as the possible options:
$scope.filteredPorts = function(index) {
// Filter out anything that's currently selected elsewhere
var currentSelections = [];
$scope.sensor.settings.forEach(function(value, idx) {
if (index !== idx) { currentSelections.push(value.port); } // if statement enforces "elsewhere"
});
Note that I also defined an array diff method, used in the above code:
Array.prototype.diff = function(a) {
return this.filter(function(i) {return a.indexOf(i) < 0;});
}; // from http://stackoverflow.com/questions/1187518/javascript-array-difference
return $scope.ports.diff(currentSelections);
}
I also changed the newItem function to just create a new item and not do any filtering:
$scope.newItem = function($event, sensorSetting) {
$scope.sensor.settings.push({
port: '',
room: '',
device: ''
});
$event.preventDefault();
}
Finally, just in case, I added an ng-click handler that deals with any duplicates, but that shouldn't be possible so can probably be ignored.
To explain why it didn't work in the first place and what the main difference here is, you were changing the $scope variable from which all of the <select>s were pulling -- you want to change it for each one individually, which I accomplish here by passing in the index variable to a function.
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.
Here is the current code (I didn't write it just trying to work with it)-
<select type="text" id="{{for}}" name="{{for}}" ng-model="model.value" class="form-control input-sm" placeholder="{{placeholder}}" ng-options="c.name as c.name for c in countries track by c.code">
<option value="">— Select —</option>
</select>
and the countries array seems to be set up like so
$http.get('services/dictionary/countries').then(function(response) {
_.chain(response.data).map(function(country) {
return {
code: country.id,
digraph: country.digraph,
trigraph: country.trigraph,
name: country.name
};
}).sortBy('name').each(function(country) {
dictionary.countries.push(country);
});
deferred.resolve(dictionary.countries);
}, function(error) {
$log.error('dictionary:', error);
});
We are pulling the list of countries from some dictionary service, and trying to display them as select options. (we aren't really using digraph and trigraph here, but it's needed for other areas that use the same dictionary call).
Problem 1 is, we can set the choice and it saves, but the select list will not show the object I saved, especially after I store and refresh it.I assume this is because we aren't properly setting the value="" with the ng-options we've set up, but I can't get it to work properly after trying many iterations (don't really understand the documentation).
Problem 2 is, on this and all other select dropdowns we have, I can't figure out a way to revert to a null choice. I have a value="" default option but it doesn't blank out the ng-model when selected. we need this to allow for user screw ups, 'oops i didn't even mean to set that field.' type things.
Much appreciated for the help gang.
Problem 1: Are you setting the ngModel value to the saved value on refresh? Only then will it be selected on the refresh.
Problem 2: Set the model to an empty object in your controller:
model = {};
I don't know if what I'm experiencing is a bug, but I can't seem to reset a select box in Angular JS 1.0.2 (also tested with 1.1.5) where there is only one option. This is for a iPad app wrapped in Phonegap. I've tested in the browser (Safari, Chrome) though and the issue is still there.
I'm working on an app that has many products that are in different categories and sub-categories. When you select a category the route changes and normally resets the select box. And it looks like so:
However, if you were to choose an option and then decide to choose another sub-category when there is only one option in the select box for a sub-category (when the user clicks one of the images where it says "Other Products") the select box doesn't properly reset. The userthen can't advance from this point to the next select box. It looks like this:
I've almost gotten it to work by coming up with this function from the various resources out there, but seems Angular is being quirky. It looks like this with where I've got so far:
The problem is that I want the blank space to be before the option, not after. Then the user has to click the second blank option and then click the option again in order to activate the second select box.
Here is the JS:
$scope.reset = function() {
$scope.variants.selectedIndex = 0;
};
Here is the JSON. Notice that these set of variants have the same size:
1: {
name: 'Super Awesome Product',
description: 'Cool description',
category: 'viewers',
variants: {
1: {
color: 'Gold',
size: '55-62mm',
productCode: 'FCSTVG',
price: 0,
image: [path + 'FCSTVG-T.png', path + 'FCSTVG.png']
},
2: {
color: 'Silver',
size: '55-62mm',
productCode: 'FCSTVS',
price: 0,
image: [path + 'FCSTVS-t.png', path + 'FCSTVS.png']
}
}
}
};
And the HTML for the select box:
<select ng-model="selectedVariant" ng-show="variants != null">
<option ng-repeat="size in variants" value="{{size}}">
{{size[0].size}}
</option>
</select>
And the HTML for the where my reset() is clicked. Like I had said, these are the "Other Products" of images below:
<div class="other-products">
<h2>Other products</h2>
<div class="slide-container">
<ul ng-show="products != null" style="width: {{products.length * 112}}px">
<li ng-repeat="p in products" ng-show="findDefaultImage(p) != null">
<a href="#" eat-click ng-click="selectProduct(p.id);reset()">
<img ng-src="{{findDefaultImage(p)}}" alt="" />
</a>
</li>
</ul>
</div>
</div>
I've tried everything, like adding different values to this line $scope.variants.selectedIndex = 0; like -1 or 1.
Any help would be appreciated!
UPDATE: I solved the issue by hardcoding it. Didn't know why I didn't do it before, but I'm still endorsing #Shawn Balestracci answer as it answers the question.
Angular has a tendency to empty out the "index 0" of a select box pushing all of the options back 1 in the select box, but in actuality the option that a user selects is actually the next option in the drop down list. I don't know if this is a bug or a feature.
Here's how I hardcoded the HTML:
<select ng-model="selectedVariant" required="required" ng-show="variants != null">
<option style="display:none" value="">PICK ONE:</option>
<option ng-repeat="size in variants" value="{{size}}">
{{size[0].size}}
</option>
</select>
This stops Angular from pushing back the options in the drop down.
In your reset function just change $scope.selectedVariant to a value that isn't in the list. It's the value stored in that model that will determine which option is selected (and vice-versa.)
You should also be able to take advantage of the ngOptions directive isntead of creating them via ng-repeat.
http://docs.angularjs.org/api/ng.directive:select
$scope.reset = function() {
$scope.selectedVariant = {};
};
So if there is only one option, why use a select box? Or should the second option be "none".
In anycase, if you could put together a simple jsfiddle, that would help folks better see the problem and be able to play with it and hopefully find you an answer.
I disagree with resetting the select model to an empty object. That will blank out the list of options you may have there. Instead you can set the selected option to zero using the model:
$scope.reset = function() {
$scope.selectedVariant = 0;
};