I'm trying to figure out how to display up to 10 options from a suggestions array and then give the user the ability to select the option, with that option now becoming the search query?
Html and AngularJS:
<ul class="suggestions" ng-show="showAutocomplete">
<li ng-repeat="suggestion in autocomplete.suggestions" ng-show="suggestion.options.length > 0" ng-mousedown="searchForSuggestion()"><small>Searching —</small>
{{suggestion.options[0].text}}//how to display up to 10 options
</li>
</ul>
Suggestions array
$scope.autocomplete = {
suggestions: []
};
$scope.showAutocomplete = false;
JS:
$scope.searchForSuggestion = function() {
$scope.searchTerms = $scope.autocomplete.suggestions[0].options[0].text;
$scope.search();
$scope.showAutocomplete = false;
};
More JS:
var getSuggestions = function(query) {
searchService.getSuggestions(query).then(function(es_return) {
var suggestions = es_return.suggest.phraseSuggestion;
if (suggestions.length > 0) {
$scope.autocomplete.suggestions = suggestions;
}
else {
$scope.autocomplete.suggestions = [];
}
if (suggestions.length > 0) {
$scope.showAutocomplete = true;
}
else {
$scope.showAutocomplete = false;
}
});
};
Suggestions is an array mapped to the $scope.autocomplete object.
I'm using PhraseSuggestion from ES and options is an array within the PhraseSuggestion object. Text is the actual option from the options array within the PhraseSuggestion object.
I hope that makes it a bit clearer - I'm a bit new to AngularJS and still learning it.
UPDATE:
I should be able to include the functionality that I'm seeking in the getSuggestions function, right? Just adjusting the if statements a bit... then all it really comes down to is the display, how to display up to 10 suggestions in the options array
I think you might be looking to use a nested ng-repeat with a limitTo filter placed on it.
I've put together a plunker showing how you can use limitTo with an ng-click on your populated options here http://plnkr.co/edit/8Mh....
If selecting an option alters suggestions, that should be fine and update the view accordingly.
Hope that helps you out!
Related
Background: There's a table from which I can choose employees. I want to filter these employees by name.(I know name is not a good way to filter, this is just an example)
Basically I have a drop down from which I choose one of the filters.
My declaration: $scope.filters = null;.
I also have this deceleration to choose my filter $scope.chosenFilter= null;.
I use the following to retrieve the different filters I have $scope.filters = retrieveFilters(info.Names);
retrieveFilters looks like the following:
var retrieveFilters = function(rows) {
var filterWrapper = document.querySelector(".filterWrapper");
var datasetOptions = [];
$scope.predicate = 'Name';
if (rows) {
//Add Default option
datasetOptions.push({
name: $scope.data.Fnames.defaultFilterOptionLabel,
value: ''
});
$scope.chosenFilter = datasetOptions[0];
_.forEach(rows, function(ele) {
datasetOptions.push({
name: ele,
value: ele
});
});
} else {
filterWrapper.style.display = "none";
}
return datasetOptions;
};
I use the following to choose my filter:
$scope.$watch('chosenFilter', function() {
var filterSearchInput = document.querySelector(".filterWrapper input");
ng.element(filterSearchInput).triggerHandler('input');
});
Everything is fine and the display works on first load as I have set the default with
//Add Default option
datasetOptions.push({
name: $scope.data.Fnames.defaultFilterOptionLabel,
value: ''
});
From the default table whenever I click on an employees name hes details are displayed. However whenever I filter and click on the employees name, nothing is displayed. Whenever I click on a specific employees name at the default table and then filter in the same name the information also shows up, as I cache it each time.
I assume that you're displaying this data somewhere in your GUI using ng-repeat. Angular has a lot of great built-in features for this. Check out the answer here: AngularJS custom search data by writing custom filter for a way to approach this more from an Angular direction. You also might want to check out this question and answer: "Thinking in AngularJS" if I have a jQuery background?.
I have the following code:
<div ng-hide="items.length == 0">
<li ng-repeat="item in items" ng-hide="item.hide == true">
{{ item.name }} <hide-button />
</li>
</div>
Imagine that I have 10 items. When I click the button, I set the item.hide to true and the item disappear. When I hide all 10 items, I want to hide the main div. What's the best way to achieve this with AngularJS?
An approach might be to use a function like this:
$scope.checkItems = function () {
var total = $scope.items.length;
for (var i = 0; i < $scope.items.length; ++i) {
if ($scope.items[i].hide) total--;
}
return total === 0;
};
and change the ngHide attribute on the outer div:
ng-hide="checkItems()"
jsfiddle: http://jsfiddle.net/u6vkf6bt/
Another approach is to declare a $scope.allHidden variable and watch over the array like this:
$scope.$watch('items', function (newItems) {
var all = true;
for (var i = 0; i < newItems.length; ++i) {
if (newItems[i].hide !== true) all = false;
}
$scope.allHidden = all;
}, true);
This is checking when anything inside the array is changed, and check if all the hide attributes are set to true.
Then set it on the ngHide attribute:
ng-hide="allHidden"
jsfiddle: http://jsfiddle.net/u6vkf6bt/1/
Among the two, I would choose the first approach, because deep watching may cause performance issues:
This therefore means that watching complex objects will have adverse
memory and performance implications.
From here, under $watch(watchExpression, listener, [objectEquality]); when objectEquality is set to true.
Yet another approach would be updating the counter of hidden items whenever a single item is hidden. Of course this would require the hiding code to be placed within your controller, like this:
$scope.allHidden = false;
// if items can have hide = true property set already from the start
var hiddenItems = $scope.items.filter(function (item) {
return item.hide;
}).length;
// or if they are all initially visible, a straightforward approach:
// var hiddenItems = 0;
$scope.hideItem = function (item) {
item.hide = true;
hiddenItems++;
if (hiddenItems === $scope.items.length) {
$scope.allHidden = true;
}
}
and the hiding would need to be done like this:
<li ng-repeat="item in items" ng-hide="item.hide == true">{{ item.name }}
<button ng-click="hideItem(item)">hide</button>
</li>
jsfiddle: https://jsfiddle.net/723kmyou/1/
Pros for this approach
no need to iterate over the full items array when doing the "should main div be hidden?" check -> possibly better performance
Cons for this approach
you need to place the hideItem function inside your controller
A couple of reasons why I didn't like the other solutions:
Time complexity is always O(n). instead of counting how many items are visible/hidden - a number we do not care about at all in this problem - we should just ask if there's at least one visible. better performance.
Not reusable enough.
My preferred solution is to actually look for the first visible item:
http://plnkr.co/edit/FJv0aRyLrngXJWNw7nJC?p=preview
angular.module('MyApp',[]);
angular.module('MyApp').directive('myParent', function(){
return {
restrict: 'A',
link:function(scope, element){
scope.$watch(function(){
return element.find('>:visible:first').length > 0;
}, function( visible ){
if ( visible ){
element.show();
}else{
element.hide();
}
})
}
}
});
<div my-parent>
<div class="hidden">one child</div>
<div class="hidden">another child</div>
</div>
<div my-parent>
<div class="hidden">third child</div>
<div>another child</div>
</div>
You can even expand this by passing a selector to 'my-parent' attribute to be used in the directive. allows to better specify which items to look for.
I like this approach as it has almost no assumptions.
This can be used for any scenario where children might be hidden. not necessarily ng-repeat.
However the watch might be costly as we're using a :visible selector.
another - assuming ng-repeat is involved - is to watch the value on the scope - http://plnkr.co/edit/ZPVm787tWwx9nbJTNg1A?p=preview
app.directive('myParent', function(){
return{
restrict: 'A',
link: function(scope, element,attrs){
scope.$watch(function(){
try{
value = scope.$eval(attrs.myParent);
var i = value.length-1;
for ( ; i >= 0; i-- ){
if ( value[i].hide != false ){
return true;
}
}
}catch(e){
return false
}
return false;
}, function( newValue ){
if ( !!newValue ) { element.show(); } else { element.hide();}
})
}
}});
<ul my-parent="awesomeThings">
<li ng-repeat="thing in awesomeThings">{{thing.value}}</li>
</ul>
The watch here might be less costly, not sure, but it doesn't handle the dom, so there's a good chance for that..
While this is very similar to other solutions, it is implemented as a highly reusable directive, and looks for the first visible item.
In my solutions I also assume jquery is available.
There are many other ways to implement this if required:
AngularJS: element.show() in directive not working
regarding watch performance, not sure which way is better. I am using a function to evaluate the watched value and I am not sure if this is the best way to handle it.
I have a JSON structure which represents as hierarchical elements.
It looks like the following:
{
"url":"http://docsetups.json",
"partnerId":1,
"fieldDefs":
[
{"roleName":"Make","roleId":1,
"children":[{"roleName":"Invoice Number","roleId":11}]
},
{"roleName":"Model","roleId":2,
"children":[
{"roleName":"Manufacturer","roleId":21},
{"roleName":"EquipmentCode","roleId":22},
{"roleName":"EquipmentSSN","roleId":23}
]
}
]
}
Plunker
I've have created a plunker at: http://plnkr.co/edit/betBR2xLmcmuQR1dznUK?p=preview
I am using ng-repeat to display this in elements as a hierarchy of elements like the following:
When I click on either element the entire structure expands and looks like the following:
The code which renders the DOM is nice and easy and looks like the following:
<div class="headerItem"
ng-class="{focus: hover}"
ng-mouseenter="hover = true"
ng-mouseleave="hover = false"
data-ng-click="vm.onClick(item.roleName)"
data-ng-repeat="item in vm.documentSetups.fieldDefs">{{item.roleName}}
<div class="subItem" ng-show="vm.isVisible"
data-ng-repeat="subItem in item.children">[ ] {{subItem.roleName}}
</div>
</div>
vm.isVisible
The thing to focus on here is the subitem which has the ng-show="vm.isVisible" so that it only displays if that value is true.
Show Only The Subitem of the Clicked Parent
However, I'd like to only display the subitem when its parent item is clicked -- instead of showing all subitems like it does now. Can someone offer a good way to do this? I'm hoping to do it without a directive, because I am interested in whether or not this is possible without a directive or if the code is terribly convoluted in that case.
If you have a solution which includes creating a directive, please keep it as simple as possible. Thanks.
I think you should define a flag for every item which determine if the item is open.
Then you pass the item itself into handler:
data-ng-click="vm.onClick(item)
after that - you simply need to invert isOpen flag:
function onClick(item)
{
item.isOpen = !item.isOpen;
}
The whole view snippet:
<div class="headerItem"
ng-class="{focus: hover}"
ng-mouseenter="hover = true"
ng-mouseleave="hover = false"
data-ng-click="vm.onClick(item)" data-ng-repeat="item in vm.documentSetups.fieldDefs">{{item.roleName}}
<div class="subItem" ng-show="item.isOpen" data-ng-repeat="subItem in item.children">[ ] {{subItem.roleName}}</div>
</div>
The plunker: http://plnkr.co/edit/N8mUZaVfmLpnlW4kxzSr?p=preview
#Oleksii You're answer is very close and it did inspire me to develop the following answer so I appreciate your input and I did upvote you. However, there's a bit more to it than what you gave me.
View Solution at Plunker
I forked the previous plunker and you can see the final solution at:
http://plnkr.co/edit/QvyHlLh83bEyvlNkskYJ?p=preview
No Directive Required
Now I can click either or both element and they expand independently. Here's the sample output:
It took a bit of thinking, but what I did first was create a new type which holds a roleName (consider it unique) and a isVisible boolean. I call that type visibleItem and it looks like this:
var visibleItem = function (roleName){
this.isVisible = false;
this.roleName = roleName;
};
After that I created an array to hold all the visibleItems (1 for each node):
var visibleItems = [];
Now when I load the json I go ahead and create 1 visibleItem object for each node and push it into the visibleItems array.
$http.get('items.json')
.success(function(data, status, header, config) {
vm.documentSetups=data;
for (var x = 0; x < vm.documentSetups.fieldDefs.length; x++)
{
visibleItems.push(new visibleItem(vm.documentSetups.fieldDefs[x].roleName));
}
})
They are "keyed" by their roleName (consider it unique).
Next, I had to write two helper methods (setVisibleItem and getVisibleItem)
function setVisibleItem(roleName)
{
for (var x = 0; x < visibleItems.length;x++)
{
if (visibleItems[x].roleName == roleName)
{
visibleItems[x].isVisible = !visibleItems[x].isVisible;
}
}
}
function getVisibleItem(roleName)
{
for (var x = 0; x < visibleItems.length;x++)
{
if (visibleItems[x].roleName == roleName)
{
return visibleItems[x].isVisible;
}
}
return false;
}
Wire Up The Helper Methods
Finally, I wire up the setVisibleItem to the ng-click of the element and I wire up the getVisibleItem to the ng-show directive.
data-ng-click="vm.onClick(item.roleName)"
data-ng-repeat="item in vm.documentSetups.fieldDefs">{{item.roleName}}
<div class="subItem" ng-show="vm.getVisibleItem(item.roleName)"
data-ng-repeat="subItem in item.children">[ ] {{subItem.roleName}}</div>
</div>
Summary Of How It Works
Basically each of those just iterates through the list and checks to insure if the roleName sent in matches the roleName of the item. If it does it sets or gets the value.
Solved Without a Directive and Not Bad
It's a lot more work than you think it'll be, but I didn't have to implement a directive and the code is still fairly basic.
I'm looking for tutorial or example on how to implement a simple input text for searching
in the grid.
My attempt (but ng-keyup require angularjs > 1.1.3 and I've got
1.0.7)
<input type="text" ng-keyup="mySearch()" ng-model="searchText">
$scope.getPagedDataAsync = function (pageSize, page, searchText) {
setTimeout(function () {
var data;
if (searchText) {
var ft = searchText.toLowerCase();
$http.get('largeLoad.json?q='+encodeURIComponent(ft)).success(function (largeLoad) {
$scope.setPagingData(largeLoad,page,pageSize);
});
} else {
$http.get('largeLoad.json').success(function (largeLoad) {
$scope.setPagingData(largeLoad,page,pageSize);
});
}
}, 100);
};
$scope.mySearch = function(){
console.log($scope.searchText);
$scope.getPagedDataAsync($scope.pagingOptions.pageSize, $scope.pagingOptions.currentPage,$scope.searchText);
}
Bye
NB its a fake request against a json file just to make the example.
Update: I'm using ng-grid-1.3.2
Basically to solve this problem I think you can use a solution similar to what I've done below where I'm just watching the property of the model for changes and firing a function to do the filtering on the data set at that point.
The HTML for the text input
<input type="text" placeholder="Type to filter" ng-model="gardenModel.externalFilterText"/>
The JavaScript that filters the data set (also included the part I had a watch on a service to update the data in the first place too or if the data is refreshed to reapply the filter).
//This function is called every time the data is updated from the service or the filter text changes
$scope.filterGardens = function(filterText) {
//Basically everything in this function is custom filtering specific
//to the data set I was looking at if you want something closer to the
//real implementation you'll probably have to dig through the source (I believe they separated the search filter code into it's own file in the original project)
//Creating a temporary array so changes don't cause a bunch of firing of watchers
var tempToShow = [];
//doing case insensitive search so lower case the filter text
filterText = filterText.toLowerCase();
//If the filter text is blank just use the whole data set
if(!filterText || filterText == "")
{
$scope.gardenModel.shownGardens = $scope.gardenModel.gardens;
return;
}
//step through each entry in the main list and add any gardens that match
for (var i = 0; i < $scope.gardenModel.gardens.length; i++) {
var curEntry = $scope.gardenModel.gardens[i];
var curGarden = curEntry.curGarden;
if(curGarden["Garden Name"] && curGarden["Garden Name"].answer.toString().toLowerCase().indexOf(filterText)!=-1)
tempToShow.push(curEntry);
else if(curGarden["Address"] && curGarden["Address"].answer.toString().toLowerCase().indexOf(filterText)!=-1)
tempToShow.push(curEntry);
else if(curGarden["Ownership"] && curGarden["Ownership"].answer.toString().toLowerCase().indexOf(filterText)!=-1)
tempToShow.push(curEntry);
else if(curGarden.gardenId && curGarden.gardenId == filterText)
tempToShow.push(curEntry);
};
$scope.gardenModel.shownGardens = tempToShow;
}
//Watch for any changes to the filter text (this is bound to the input in the HTML)
$scope.$watch('gardenModel.externalFilterText', function(value) {
$scope.filterGardens(value);
});
//Watch for any changes on the service (this way if addition/edit are made and
//refresh happens in the service things stay up to date in this view, and the filter stays)
$scope.$watch( function () { return gardenService.gardens; }, function ( gardens ) {
$scope.gardenModel.gardens = gardens;
$scope.filterGardens($scope.gardenModel.externalFilterText);
});
Edit Cleaned up the code formatting a bit and added some comments.
I'm trying to create some tabs, one per profile the user chooses to save. Each profile is a ViewModel. So I thought I'd just create another ViewModel that contains an observableArray of objects of type: {name: profile_name, model: model_converted_to_json}.
I followed this example to create my code - but I get nothing bound, for some reason.
Here's my code:
-ViewModel (I use Requirejs, that explains the external wrapper):
"use strict";
// profiles viewmodel class
define(["knockout"], function(ko) {
return function() {
var self = this;
this.profilesArray = ko.observableArray();
this.selected = ko.observable();
this.addProfile = function(profile) {
var found = -1;
for(var i = 0; i < self.profilesArray().length; i++) {
if(self.profilesArray()[i].name == profile.name) {
self.profilesArray()[i].model = profile.model;
found = i;
}
}
if(found == -1) {
self.profilesArray().push(profile);
}
};
};
});
-The JS code (excerpt of larger file):
var profiles = new profilesViewMode();
ko.applyBindings(profiles, $("#profileTabs")[0]);
$("#keepProfile").on("click", function() {
var profile = {
name: $("#firstName").text(),
model: ko.toJSON(model)
};
profiles.addProfile(profile);
profiles.selected(profile);
console.log(profile);
$("#profileTabs").show();
});
-The HTML (Thanks Sara for correcting my HTML markup)
<section id="profileTabs">
<div>
<ul class="nav nav-tabs" data-bind="foreach: profilesArray">
<li data-bind="css: { active: $root.selected() === $data }">
</li>
</ul>
</div>
</section>
I have verified that the observableArray does get new, correct value on button click - it just doesn't get rendered. I hope it's a small thing that I'm missing in my Knockout data-bind syntax.
Thanks for your time!
You will want to call push directly on the observableArray, which will both push to the underlying array and notify any subscribers. So:
self.profilesArray.push(profile);
You are setting name using name: $('#firstName').text(); you may need to change that to .val() if this is referencing an input field (which I assumed here).
You are using .push() on the underlying array which bypasses ko's subscribers (the binding in this case)
Here is a working jsfiddle based on your code. I took some liberties with model since that wasn't included.