angular custom multiselect list - javascript

I want to build a list that functions like a select or a multiselect given a param. eg. if the parameter is false, select A->select B, unselects A and selects B.
<ul>
<li>a,</li>
<li>b, and</li>
<li>c</li>
</ul>
My initial approach looks something like:
<ul myDirective>
<li ng-model="a">a,</li>
<li ng-model="b">b, and</li>
<li ng-model="c">c</li>
</ul>
Where mydirective watches for clicks on the LIs. On Click it determines if it needs to unselect or unset anything. Then selects the new value an and applies a selected class.
I don't think I'm follows the angular paradigms and I'm looking for some help finding the right path forward.
** Edit **
I'm looking to provide a user experience like: http://jsfiddle.net/qbt2myj8/

in my opinion a custom directive will make your code much clean since you already use AngularJS.
Below is a complete solution using AngularJS Custom Directive that is completely reusable for code organization.
HTML
custom directive default behavior
<ul data-my-directive class="list">
<li>a</li>
<li>b</li>
<li>c</li>
</ul>
(This is the screenshot preview only. See PLAYGROUND link below to play around)
HTML
with custom attribute
<ul data-my-directive data-multi-select="true" class="list">
<li>a</li>
<li>b</li>
<li>c</li>
</ul>
(This is the screenshot preview only. See PLAYGROUND link below to play around)
Javascript
angular.module('app',[])
.directive('myDirective', function(){
return {
transclude: true,
template: '<div ng-transclude></div>',
link: function(scope, element, attrs){
function addClickListener(i, callback){
selections[i].addEventListener('click', function(){
callback(i);
}, false);
}
// Set up attribute value. Goes to default if attribute is not set up
attrs.multiSelect = attrs.multiSelect || 'false';
scope.isSelected = [];
var selections = element[0].getElementsByTagName('li');
if(attrs.multiSelect === "true"){
for(var i = 0; i < selections.length; i++){
scope.isSelected[i] = false;
addClickListener(i, function(i){
if(scope.isSelected[i] === true){
// if previously it is selected (red color), now make it unselected (black color / default color)
angular.element(selections[i]).removeClass('selected');
scope.isSelected[i] = false;
} else {
// previously black color, so change it to red color
angular.element(selections[i]).addClass('selected');
scope.isSelected[i] = true;
}
});
}
} else {
var currentSelection = -1;
for(var i = 0; i < selections.length; i++){
scope.isSelected[i] = false;
addClickListener(i, function(i){
if(scope.isSelected[i] === true){
// do nothing
} else {
// previously black color, so change it to red color
angular.element(selections[i]).addClass('selected');
scope.isSelected[i] = true;
angular.element(selections[currentSelection]).removeClass('selected');
scope.isSelected[currentSelection] = false;
currentSelection = i;
}
});
}
}
}
}
});
And finally, the ultimate
PLAYGROUND
to play around!
I hope it clears up your requirements. Have a nice day :)

My approach would be (at first) not to make a directive at all, but a custom JavaScript data structure. Quick example out of my mind :
/**
* Makes a selector data structure
* #param items elements to be listed
* #param multi whether multiple selection is supported
* #returns {{cells: *, selectedItems: selectedItems, unselectAll: unselectAll}}
*/
function makeSelector (items, multi) {
var cells = items.map(function (item) {
// each cell wraps an item and a selected flag
return {
item: item,
selected: false
};
});
// returns an array of the currently selected items
function selectedItems() {
return cells
.filter(function (cell) {
return cell.selected;
})
.map(function (cell) {
return cell.item;
});
}
// unselects all items
function unselectAll() {
cells.forEach(function (cell) {
cell.selected = false;
})
}
// adding methods to cells
cells.forEach(function (cell) {
cell.selectMe = (multi
? function () {
cell.selected = true;
}
: function () {
unselectAll();
cell.selected = true;
});
cell.unselectMe = function () {
cell.selected = false;
};
cell.toggle = function () {
if (cell.selected) {
cell.unselectMe();
} else {
cell.selectMe();
}
}
});
return {
cells: cells,
selectedItems: selectedItems,
unselectAll: unselectAll
};
}
This will allow you to easily retrieve your list of selected items in your controller:
// ...
$scope.abcSelector = makeSelector(['A','B','C'],false);
var getSelectedItems = $scope.abcSelector.selectedItems; // to do something with them in your controller
// ...
Then use Angular data-binding to reflect this data-structure in your HTML:
<h3>List of items :</h3>
<ul>
<li ng-repeat="cell in abcSelector.cells" ng-click="cell.toggle()"
ng-class="{'active': cell.selected}">
{{cell.item}}
</li>
</ul>
<h3>List of selected items :</h3>
<ul>
<li ng-repeat="item in abcSelector.selectedItems()">
{{item}}
</li>
</ul>
and if you fell like it factor this out in a directive, which will typically bind the list of the selected items to an ng-model.

Related

With Angular - Don't Display Array Items based on other Array's Items for Ng-Repeat

I am assuming this has something to do with filter. I have two arrays: 1. that contains all areas possible to be an administer (called areas) and 2. that contains only the areas that are being administered by the user (called adminAreas which is in an array itself of user). I have a webpage that shows both in a two column display. When the user clicks to add them it adds them to the other column and removes itself from the current column. What I am trying to do is... When the page loads... don't repeat the items in the adminAreas div in the areas div. Also a cleaner way to do the back and forth adding/removing.
JS:
DataService.getAllAreas().then(function (data) {
$scope.areas = data;
});
DataService.getUser($scope.userId).then(function(data){
$scope.user = data;
});
$scope.assignArea = function (area) {
$scope.user.adminAreas.push(area.name);
$scope.areas.splice($scope.areas.indexOf(area), 1);
DataService.updateUser($scope.user).then(function () {
});
};
$scope.removeArea = function (areaName) {
$scope.user.adminAreas.splice($scope.user.adminAreas.indexOf(areaName), 1);
DataService.updateUser($scope.user).then(function () {
});
};
HTML:
<div class="sideOneAdd" ng-repeat="currentArea in user.adminAreas>
<a ng-click="assignArea()"><span>{{currentArea}}</span></a>
</div>
<div class="sideTwoRemove" ng-repeat="area in areas (assuming something with filter)>
<a ng-click="removeArea()"><span>{{area.name}}</span></a>
</div>
ng-repeat="area in areas" | filter:filterNotCurrent
$scope.filterNotCurrent = function (area) {
return !$scope.user.adminAreas.some(function (element) {
if (element === area)
return true;
});
};
Use this filter :
angular.module('myApp', []);
angular
.module('myApp')
.filter('filterAssignedAreas', filterAssignedAreas);
function filterAssignedAreas(){
return function(areas, assignedAreas){
return areas.filter(function(obj){
return !assignedAreas.some(function(obj2){
return obj2.name == obj.name;
})
})
}
}
And Then :
<div class="sideTwoRemove" ng-repeat="area in areas | filterAssignedAreas:user.adminAreas">

tabs not storing scope vars

My markup:
<li active="active.search">search</li>
<li active="active.lists">lists</li>
<li active="active.find">find</li>
My code:
$scope.active = {
search: true
};
$scope.activate = function(li) {
$scope.active = {};
$scope.active[li] = true;
}
First - the item I've set to be highlighted, doesn't actually get highlighted by default, however they do work when clicked.
Secondly - I'm trying to use what is actually active via:
if ($scope.active[0].search === true) {
... some values
}
Is this not correct? as I can't seem to make this work.
active isn't an angular directive so you need to enclose the variable in angular brackets {{ }} like
active="{{active.search}}"
for angular to parse it.

filter doesn't work properly with nested data?

I wish can group my data by hiding and showing them using filter.
I added filter: {tabs.tabId: currentTab} in ng-repeat but it returned blank. When I removed this line the data appear, it means the filter caused some problem.
demo http://jsfiddle.net/8Ub6n/4/
This would work with deep nested values.
I would recommend to make your own filter:
<ul ng-repeat="friend in user">
<li ng-repeat="relation in friend.relationship | RelationFilter:currentTab">{{relation.name}} ({{relation.points}}points)</li>
</ul>
[..]
app.filter("RelationFilter", function () {
return function (input, currentTab) {
var output = [];
for (var i in input) {
if (input[i].tabs.length >= 1
&& input[i].tabs[0].tabId == currentTab) {
output.push(input[i]);
}
}
return output;
};
});
check JSFiddle http://jsfiddle.net/8Ub6n/11/

How to handle pagination with Knockout

I have a div that is setup to bind to a observeableArray ,but I only want to show at most 50 items from that observeableArray at any given time. I want to handle this with pagination with a previous and next button along with indices on the page to allow users to cycle through pages of items from the collection. I know I could probably do this with a computedObservable and a custom data binding but I'm not sure how to do it (I'm still a Knockout neophyte). Can anyone point me in the right direction?
Here is my code (the JS is in TypeScript):
<div class="container-fluid">
<div class="row-fluid">
<div class="span12">
<%=
if params[:q]
render 'active_search.html.erb'
else
render 'passive_search.html.erb'
end
%>
<%= form_tag("/search", method: "get", :class => "form-search form-inline") do %>
<%= label_tag(:q, "Search for:") %>
<%= text_field_tag(:q, nil, class:"input-medium search-query") %>
<%= submit_tag("Search", :class=>"btn") %>
<% end %>
<div class="media" data-bind="foreach: tweetsArray">
<%= image_tag('twitter-icon.svg', :class=>"tweet_img", :style=>"display:inline;") %>
<div class="media-body" style="display:inline;">
<h4 class="media-heading" data-bind="text: user.screen_name" style="display:inline;"></h4>
<span data-bind="text:text" style="display:inline;"></span> <br />
<span data-bind="text:'Created at '+created_at"></span> <br />
</div>
</div>
<div class="pagination pagination-centered">
<ul>
<li>
Prev
</li>
<li>
1
</li>
<li>
Next
</li>
</ul>
</div>
</div>
</div>
</div>
<script>
var viewModel = new twitterResearch.TweetViewModel();
ko.applyBindings(viewModel);
//TODO: notes to self, use custom binding for pagination along with a computed observable to determine where at in the list you are
//document.onReady callback function
$(function() {
$.getJSON('twitter', {}, function(data) {
viewModel.pushTweet(data);
console.log(data.user);
});
});
</script>
declare var $: any;
declare var ko: any;
module twitterResearch {
class Tweet {
text: string;
created_at: string;
coordinates: string;
user: string;
entities: string;
id: number;
id_str: string;
constructor(_text: string, _created_at: string, _coordinates: any, _user: any,
_entities: any, _id_str: string, _id: number){
this.text = _text;
this.created_at = _created_at;
this.coordinates = _coordinates;
this.user = _user;
this.entities = _entities;
this.id_str = _id_str;
this.id = _id;
}
}
export class TweetViewModel{
tweetsArray: any;
constructor()
{
this.tweetsArray = ko.observableArray([]);
}
//tweet is going to be the JSON tweet we return
//from the server
pushTweet(tweet)
{
var _tweet = new Tweet(tweet.text, tweet.created_at, tweet.coordinates,
tweet.user, tweet.entities, tweet.id_str, tweet.id);
this.tweetsArray.push(_tweet);
this.tweetsArray.valueHasMutated();
}
}
}
Pagination is quite simple with Knockout. I would personally achieve it this way:
Have an observableArray containing all your elements
Have an observable containing the current page (initialized to 0)
Have a variable declaring the number of elements per page
Have a computed that returns the number of pages, calculated thanks to the number of elements per page and the total number of elements.
Finally, add a computed that slices the array containing all the elements.
Given that, you can now add a function that increments (next) or decrements (previous) the current page.
Here is a quick example:
var Model = function() {
var self = this;
this.all = ko.observableArray([]);
this.pageNumber = ko.observable(0);
this.nbPerPage = 25;
this.totalPages = ko.computed(function() {
var div = Math.floor(self.all().length / self.nbPerPage);
div += self.all().length % self.nbPerPage > 0 ? 1 : 0;
return div - 1;
});
this.paginated = ko.computed(function() {
var first = self.pageNumber() * self.nbPerPage;
return self.all.slice(first, first + self.nbPerPage);
});
this.hasPrevious = ko.computed(function() {
return self.pageNumber() !== 0;
});
this.hasNext = ko.computed(function() {
return self.pageNumber() !== self.totalPages();
});
this.next = function() {
if(self.pageNumber() < self.totalPages()) {
self.pageNumber(self.pageNumber() + 1);
}
}
this.previous = function() {
if(self.pageNumber() != 0) {
self.pageNumber(self.pageNumber() - 1);
}
}
}
You'll find a simple and complete example here: http://jsfiddle.net/LAbCv/ (might be a bit buggy, but the idea is there).
Actually I am working on a website, which has a lot of tables (most of them need paging).So actually, I needed some reusable-component for paging to use it in all the cases which I need paging.
Also, I needed more advanced features than which provided in the accepted answer to this question.
So I developed my own component to solving this issue, here it is.
Now on Github
JsFiddle
And for more details, continue reading (Please consider to take the code from GitHub, not from here, as the GitHub code was updated and enhanced since I put it here)
JavaScript
function PagingVM(options) {
var self = this;
self.PageSize = ko.observable(options.pageSize);
self.CurrentPage = ko.observable(1);
self.TotalCount = ko.observable(options.totalCount);
self.PageCount = ko.pureComputed(function () {
return Math.ceil(self.TotalCount() / self.PageSize());
});
self.SetCurrentPage = function (page) {
if (page < self.FirstPage)
page = self.FirstPage;
if (page > self.LastPage())
page = self.LastPage();
self.CurrentPage(page);
};
self.FirstPage = 1;
self.LastPage = ko.pureComputed(function () {
return self.PageCount();
});
self.NextPage = ko.pureComputed(function () {
var next = self.CurrentPage() + 1;
if (next > self.LastPage())
return null;
return next;
});
self.PreviousPage = ko.pureComputed(function () {
var previous = self.CurrentPage() - 1;
if (previous < self.FirstPage)
return null;
return previous;
});
self.NeedPaging = ko.pureComputed(function () {
return self.PageCount() > 1;
});
self.NextPageActive = ko.pureComputed(function () {
return self.NextPage() != null;
});
self.PreviousPageActive = ko.pureComputed(function () {
return self.PreviousPage() != null;
});
self.LastPageActive = ko.pureComputed(function () {
return (self.LastPage() != self.CurrentPage());
});
self.FirstPageActive = ko.pureComputed(function () {
return (self.FirstPage != self.CurrentPage());
});
// this should be odd number always
var maxPageCount = 7;
self.generateAllPages = function () {
var pages = [];
for (var i = self.FirstPage; i <= self.LastPage() ; i++)
pages.push(i);
return pages;
};
self.generateMaxPage = function () {
var current = self.CurrentPage();
var pageCount = self.PageCount();
var first = self.FirstPage;
var upperLimit = current + parseInt((maxPageCount - 1) / 2);
var downLimit = current - parseInt((maxPageCount - 1) / 2);
while (upperLimit > pageCount) {
upperLimit--;
if (downLimit > first)
downLimit--;
}
while (downLimit < first) {
downLimit++;
if (upperLimit < pageCount)
upperLimit++;
}
var pages = [];
for (var i = downLimit; i <= upperLimit; i++) {
pages.push(i);
}
return pages;
};
self.GetPages = ko.pureComputed(function () {
self.CurrentPage();
self.TotalCount();
if (self.PageCount() <= maxPageCount) {
return ko.observableArray(self.generateAllPages());
} else {
return ko.observableArray(self.generateMaxPage());
}
});
self.Update = function (e) {
self.TotalCount(e.TotalCount);
self.PageSize(e.PageSize);
self.SetCurrentPage(e.CurrentPage);
};
self.GoToPage = function (page) {
if (page >= self.FirstPage && page <= self.LastPage())
self.SetCurrentPage(page);
}
self.GoToFirst = function () {
self.SetCurrentPage(self.FirstPage);
};
self.GoToPrevious = function () {
var previous = self.PreviousPage();
if (previous != null)
self.SetCurrentPage(previous);
};
self.GoToNext = function () {
var next = self.NextPage();
if (next != null)
self.SetCurrentPage(next);
};
self.GoToLast = function () {
self.SetCurrentPage(self.LastPage());
};
}
HTML
<ul data-bind="visible: NeedPaging" class="pagination pagination-sm">
<li data-bind="css: { disabled: !FirstPageActive() }">
<a data-bind="click: GoToFirst">First</a>
</li>
<li data-bind="css: { disabled: !PreviousPageActive() }">
<a data-bind="click: GoToPrevious">Previous</a>
</li>
<!-- ko foreach: GetPages() -->
<li data-bind="css: { active: $parent.CurrentPage() === $data }">
<a data-bind="click: $parent.GoToPage, text: $data"></a>
</li>
<!-- /ko -->
<li data-bind="css: { disabled: !NextPageActive() }">
<a data-bind="click: GoToNext">Next</a>
</li>
<li data-bind="css: { disabled: !LastPageActive() }">
<a data-bind="click: GoToLast">Last</a>
</li>
</ul>
Features
Show on need When there is no need for paging at all (for example the items which need to display less than the page size) then the HTML component will disappear.
This will be established by statementdata-bind="visible: NeedPaging".
Disable on need
for example, if you are already selected the last page, why the last page or the Next button should be available to press?
I am handling this and in that case I am disabling those buttons by applying the following binding data-bind="css: { disabled: !PreviousPageActive() }"
Distinguish the Selected page
a special class (in this case called active class) is applied on the selected page, to make the user know in which page he/she is right now. This is established by the binding data-bind="css: { active: $parent.CurrentPage() === $data }"
Last & First
going to the first and last page is also available by simple buttons dedicated to this.
Limits for displayed buttons
suppose you have a lot of pages, for example, 1000 pages, then what will happen? would you display them all for the user? absolutely not you have to display just a few of them according to the current page. for example, showing 3 pages before and other 3 pages after the selected page.
This case has been handled here <!-- ko foreach: GetPages() -->
the GetPages function applying a simple algorithm to determine if we need to show all the pages (the page count is under the threshold, which could be determined easily), or to show just some of the buttons.
you can determine the threshold by changing the value of the maxPageCount variable
Right now I assigned it as the following var maxPageCount = 7; which mean that no more than 7 buttons could be displayed for the user (3 before the SelectedPage, and 3 after the Selected Page) and the Selected Page itself.
You may wonder, what if there were not enough pages after OR before the current page to display? do not worry I am handling this in the algorithm for example, if you have 11 pages and you have maxPageCount = 7 and the current selected page is 10, Then the following pages will be shown
5,6,7,8,9,10(selected page),11
so we always stratifying the maxPageCount, in the previous example showing 5 pages before the selected page and just 1 page after the selected page.
Selected Page Validation
All set operation for the CurrentPage observable which determine the selected page by the user, is going through the function SetCurrentPage. In only this function we set this observable, and as you can see from the code, before setting the value we make validation operations to make sure that we will not go beyond the available page of the pages.
Already clean
I use only pureComputed not computed properties, which means you do not need to bother yourself with cleaning and disposing of those properties. Although, as you will see in the example below, you need to dispose of some other subscriptions which are outside of the component itself
NOTE 1
You may notice that I am using some bootstrap classes in this component,
This is suitable for me, but , of course, you can use your own classes instead of the bootstrap classes.
The bootstrap classes which I used here are pagination, pagination-sm, active and disabled
Feel free to change them as you need.
NOTE 2
So I introduced the component for you, It is time to see how it could work.
You would integrate this component into your main ViewModel as like this.
function MainVM() {
var self = this;
self.PagingComponent = ko.observable(new Paging({
pageSize: 10, // how many items you would show in one page
totalCount: 100, // how many ALL the items do you have.
}));
self.currentPageSubscription = self.PagingComponent().CurrentPage.subscribe(function (newPage) {
// here is the code which will be executed when the user changes the page.
// you can handle this in the way you need.
// for example, in my case, I am requesting the data from the server again by making an ajax request
// and then updating the component
var data = /*bring data from server , for example*/
self.PagingComponent().Update({
// we need to set this again, why? because we could apply some other search criteria in the bringing data from the server,
// so the total count of all the items could change, and this will affect the paging
TotalCount: data.TotalCount,
// in most cases we will not change the PageSize after we bring data from the server
// but the component allows us to do that.
PageSize: self.PagingComponent().PageSize(),
// use this statement for now as it is, or you have to made some modifications on the 'Update' function.
CurrentPage: self.PagingComponent().CurrentPage(),
});
});
self.dispose = function () {
// you need to dispose the manual created subscription, you have created before.
self.currentPageSubscription.dispose();
}
}
Last but not least, Sure do not forget to change the binding in the HTML component according to your special viewModel, or wrap all the component with the with binding like this
<div data-bind="with: PagingComponent()">
<!-- put the component here -->
</div>
Cheers
I have created a blogpost with detailed explanation on how to create pagination with the help of a little JQuery plugin (here).
Basically, I have used normal knockout data binding with AJAX and after data has been retrieved from the server, I call the plugin. You can find the plugin here. It's called Simple Pagination.
This question is still one of the top searches for "knockout pagination", so the knockout extension knockout-paging (git) is worth mentioning.
It provides pagination by extending ko.observableArray. It is well documented and easy to use.
The usage example is here.

javascript prototype problem

So I have a rather basic javascript problem which I have been slamming my head into a wall over for awhile:
<div class='alist'>
<ul>
<li class='group_1'> An Item </li>
<li class='group_1'> An Item </li>
<li class='group_2'> An Item </li>
</ul>
</div>
<div class='alist'>
<ul>
<li class='group_1'> An Item </li>
<li class='group_1'> An Item </li>
<li class='group_2'> An Item </li>
</ul>
</div>
<script>
function toggle_item( num ){
$$( 'li.group_' + num ).invoke('toggle');
}
</script>
Basically, I need to create a sweeper that sets the div to display:none if all the li are display:none.
I think it would start like:
function sweep(){
$$('div.alist').each( function( s ) {
ar = s.down().children
});
}
Any suggestions for good tutorials would be welcome as well
Something like this might get you started. You'll need to iterate through the children and check if they're visible. If any of them aren't, set a flag and break from the loop. If the flag is false then you don't need to hide the div.
function sweep(){
$$('div.alist').each( function( s ) {
var shouldHide = true;
var children = s.down().childElements();
children.each(function(li) {
if(li.visible()) {
shouldHide = false;
throw $break;
}
});
if(shouldHide) {
s.hide();
}
});
}
You could use the select() method of Element to find all li descendants. And run a method Array.all for each li and check if all return true. Hide the div if all return true.
function sweep() {
// check each div
$$('div.alist').each(function(element) {
var listItems = element.select('li');
// are the list items of this div hidden?
var listItemsHidden = listItems.all(function(item) {
return item.visible();
});
// hide the div too if so
if(listIemsHidden) {
element.hide();
}
});
}
This code is untested.
This is jQuery solution (Prototype must be something similar):
$('div.alist').css('display', function () {
return $(this).find('li:visible').length < 1 ? 'none' : '';
});

Categories

Resources