Reseting variables and editing arrays with AngularJS - javascript

I'm building an app using AngularJS and LocalStorage. I've run into a problem that it's a tad too complex for me.
I have a list of people, and the idea is to be able to add arrays of names. I choose X names, click add, creates an object in an array, it resets the list, and I can start over, choose X names, click add, etc.
Here's how I create the temporary array that then I push into LocalStorage:
HTML:
<form>
<div class="col-md-3" ng-repeat="staff in stafflist | orderBy: 'name'">
<button class="btn form-control" ng-show="!staff.chosen" ng-click="pushStaff(staff)">{{staff.name}}</button>
<button class="btn btn-primary form-control" ng-show="staff.chosen" ng-click="unpushStaff(staff)">{{staff.name}}</button>
</div>
<button class="btn ng-click="addRecord()">Add passangers</button>
</form>
JS:
$scope.paxlist = [];
$scope.pushStaff = function (staff) {
staff.chosen = true;
$scope.paxlist.push(staff);
console.log($scope.paxlist);
};
$scope.unpushStaff = function (staff) {
staff.chosen = false;
var index=$scope.paxlist.indexOf(staff)
$scope.paxlist.splice(index,1);
console.log($scope.paxlist);
}
My problem is that I can create objects into the array, but when I add an object, the selected items of the list of names won't reset, so they will be pre-selected when adding the next object.
At the same time, it will also stay linked to the last object added, so when I modify the selection, the last object will also get modified.
This also messes with the possibility of adding an editing capability for each object of the array.
I've created a Plnkr that illustrates the issue.
If you could shed some light on the issue, that would be brilliant.

In addRecord you need reset property chosen
$scope.addRecord = function () {
$scope.recordlist.push({ pax: angular.copy($scope.paxlist) });
jsonToRecordLocalStorage($scope.recordlist);
$scope.editItem = false;
$scope.paxlist = [];
$scope.stafflist.forEach(function (el) {
el.chosen = false;
});
};
Demo: http://plnkr.co/edit/vV8OuKiTKYkFyy7SrjOS?p=preview

Related

KoJs: bind a dynamic number of text boxes to elements of an array

I have a front-end which allows for adding and removing of text boxes suing the foreach binding. A text box looks something like this
<div id="dynamic-filters" data-bind="foreach: filterList">
<p>
<input type="text" data-bind="textInput: $parent.values[$index()], autoComplete: { options: $parent.options}, attr: { id : 'nameInput_' + $index() }"/>
</p>
</div>
What I want to do, as shown in the code above is to bind each of these dynamically generated text boxes to an element in the array using the $index() context provided by knockout.js
However it doesn't work for me, my self.values=ko.observableArray([]) doesn't change when the text boxes change.
My question is, if I want to have a way to bind these dynamically generated text boxes, is this the right way to do it? If it is how do I fix it? If it's not, what should I do instead?
Thanks guys!
EDIT 1
the values array is an observable so I thought I should unwrap it before use. I changed the code to
<input type="text" data-bind="textInput: $parent.values()[$index()], autoComplete: { options: $parent.options}, attr: { id : 'nameInput_' + $index() }"/>
This works in a limited way. When I add or change the content of text boxes, the array changes accordingly. However when I delete an element it fails in two ways:
If I delete the last item, the array simply doesn't change
If I delete an item in between, everything is shifted back
I suppose I have to add a function that changes the text-input value before destroying the text box itself.
Any help or advice on how to do this?
I would suggest taking the array of values and mapping it to some kind of model first, then dumping it into the filterList ko.observableArray. It can be as complex or as simple as need be.
That way you have direct access to those properties at the ko foreach: level instead of having to do the goofy index access.
I've added a simple knockout component example as well to show you what can be achieved.
var PageModel = function() {
var self = this;
var someArrayOfValues = [{label: 'label-1', value: 1},{label: 'label-2', value: 2},{label: 'label-3', value: 3},{label: 'label-4', value: 4}];
this.SimpleInputs = ko.observableArray(_.map(someArrayOfValues, function(data){
return new SimpleInputModel(data);
}));
this.AddSimpleInput = function(){
self.SimpleInputs.push(new SimpleInputModel({value:'new val', label:'new label'}));
};
this.RemoveSimpleInput = function(obj){
self.SimpleInputs.remove(obj);
}
}
var SimpleInputModel = function(r) {
this.Value = ko.observable(r.value);
this.Label = r.label;
};
var SimpleInputComponent = function(params){
this.Id = makeid();
this.Label = params.label;
this.Value = params.value;
function makeid() {
var text = "";
var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (var i = 0; i < 5; i++)
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
}
}
ko.components.register('input-component', {
viewModel: SimpleInputComponent,
template: '<label data-bind="text: Label, attr: {for: Id}"></label><input type="text" data-bind="textInput: Value, attr: {id: Id}" />'
})
window.model = new PageModel();
ko.applyBindings(model);
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.4/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<!-- ko if: SimpleInputs -->
<h3>Simple Inputs</h3>
<!-- ko foreach: SimpleInputs -->
<input-component params="value: Value, label: Label"></input-component>
<button data-bind="click: $parent.RemoveSimpleInput">X</button>
<br>
<!-- /ko -->
<!-- /ko -->
<button data-bind="click: AddSimpleInput">Add Input</button>
EDIT (7/16/2020):
Mind explaining this without requiring lodash? I literally googled "how to lodash map using plain javascript". Excellent answer otherwise! – CarComp
In this scenario the lodash _.map method could be overkill unless you are executing the script in an environment that does not have native support for the vanilla array map method. If you have support for the vanilla method, go ahead and use that. The map method essentially iterates over each array using the method it is handed to return a transformed array of the original items. Implementation of vanilla code would look like so.
this.SimpleInputs = ko.observableArray(someArrayOfValues.map(function(data) {
return new SimpleInputModel(data);
}));
Here we are taking the values of someArrayOfValues and telling it to use each item to build a new SimpleInputModel and return it using that item data. [SimpleInputModel, SimpleInputModel, SimpleInputModel, SimpleInputModel] is what the new array turns into after mapping. Each of these items has all the functionality described in the SimpleInputModel class, Value and Label.
So with the new array you could, if you wanted, access the values like this as well self.SimpleInputs[0].Value() or self.SimpleInputs[0].Label
Hope that helps to clarify.

Can I make one object have the same order as another object?

I know that the title of this question already doesn't make sense because objects are unordered by their nature. BUT, I think if you take a look at this screen shot that's linked here, it'll make more sense.
Picture of the two objects in my console.log
Here is what's happening. I am creating an object called $scope.gameConfigs, which is itself created from data I receive from a server call (a provisions object). This $scope.gameConfigs creates a group of dropdown menus.
Basically what I'm trying to do is make the dropdown menus display previously saved data IF the data exists. When saved data exists, the server returns an object that has all the saved data. This data is ordered numerically. My problem is, this saved data is not always ordered the same as the data in the $scope.gameConfigs I create, which results in blank fields appearing in my dropdowns.
If you look at the screenshot I linked to, you can see that the keys in $scope.gameConfigs (the object with line 30 written next to it) are Map, Server, and Mode.
The second object, which is the saved data returned from the server, is a group of objects that each have a name property (which is the name of the dropdown menu). If you take a look at that, the order is Server, Map, and Mode.
My question is, how can I make the Saved Data object copy the order of my $scope.gameConfigs object? Is that even possible? If not, what would be the best way to proceed?
Here is the HTML and the controller I have for my page/controller, too, if that helps at all:
<form class="form-horizontal" ng-controller="GamePreferenceCtrl">
<div ng-repeat="(name, section) in gameConfigs">
<label ng-bind="name"></label>
<select class="form-control" ng-model="formData.settings[$index].value" ng-change="dropdownItemClicked(name, formData.settings[$index].value)">
<option value="" disabled="disabled">---Please Select Option---</option>
<option ng-repeat="item in section" value="{{item.value}}" ng-bind="item.value"></option>
</select>
</div>
<div class="col-md-12" ng-include="gametemp"></div>
<div class="row">
<div class="hr-line-dashed"></div>
<div class="text-center col-md-12 padding-15">
<button class="btn btn-primary btn-lg" ng-click="saveGameSetting()" formnovalidate translate>
<i class='fa fa-circle-o-notch fa-spin' ng-if="showBusy"></i> Save
</button>
</div>
</div>
</form>
and the controller:
function GamePreferenceCtrl($scope, $filter, Tournament, Notification) {
$scope.$parent.child.game = $scope;
$scope.selectedItems = [];
$scope.formData = {};
$scope.loadConfig = function () {
Tournament.loadGameConfig($scope.id).then(function (response) {
$scope.gameConfigs = {};
if (response.data.tournamentPrefs) {
_.each(response.data.tournamentPrefs, function (val) {
if ($scope.formData.settings === undefined) {
$scope.formData.settings = [];
}
$scope.formData.settings.push({section: val.name, value: val.value});
});
$scope.formData = {};
};
$scope.dropdownItemClicked = function(name, value) {
if($scope.formData.settings === undefined) {
$scope.formData.settings = {};
$scope.formData.settings[name] = value;
} else {
console.log('inside else');
$scope.formData.settings[name] = value;
}
};
_.each(response.data.provisions, function (val) {
if ($scope.gameConfigs[val.section] === undefined) {
$scope.gameConfigs[val.section] = [];
}
$scope.gameConfigs[val.section].push({name: val.key, value: val.value});
});
});
};
I know this is a long read, and I truly appreciate anyone who might be able to help me out with this. Thank you!
You may do like
var o = {z:1, f:2, a:7}
p = Object.assign({},o),
q = Object.assign(o)
console.log(o, p, o === p);
console.log(o, q, o === q);

Remove item from array by pressing button

I'm using angularJS to build a SPA. I am trying to delete an object from an array in my controller. I am using ng-repeat and can't seem to get my head around this. Here is the related html:
<div class="cat-button" ng-repeat="category in cats" category="category">
<button class=" close-button" ng-click="removeCat()">
<span class="glyphicon glyphicon-remove-sign" aria-hidden=true> </span> </button>{{category.name}}
</div>
This created a div with a button for every object that gets saved to my $scope.cats array. It works fine but I cant figure out how do I use the button in each div to delete that specific object.
When I click on the button , the function on my controller gets called, but this is where I get lost, how do I delete the specific object created dynamically by the user.
This is the related code on my controller:
//Function to delete category
$scope.removeCat = function () {
//I know I have to use splice on my array but how do I Identify the object that needs to be deleted from my array?
};
You can either pass on $index like so:
<button class=" close-button" ng-click="removeCat($index)">
and in your function:
$scope.removeCat = function (index) {
$scope.cats.splice(index,1);
}
or pass the whole item and use indexOf (the saver way)
<button class=" close-button" ng-click="removeCat(category)">
$scope.removeCat = function (item) {
$scope.cats.splice(myArray.indexOf(item), 1);
}
You can pass the index of the item you want to delete in the ng-click function:
<div class="cat-button" ng-repeat="category in cats" category="category">
<button class=" close-button" ng-click="removeCat($index)">
<span class="glyphicon glyphicon-remove-sign" aria-hidden=true> </span> </button>{{category.name}}
</div>
Then you can use this in your Angular controller like this:
$scope.removeCat = function (index) {
$scope.cats.splice(index, 1);
};
Update
Incase you don't want to pass in the index, instead you can also pass in the entire object and locate the index in your controller. The code below is setup to work on all browsers. (Just haven't tested it ;) )
$scope.removeCat = function (cat) {
// Using underscore
var index = _.indexOf($scope.cats, cat);
// Or using a for loop
for(var i = 0; i < $scope.cats.length; i++) {
//Assuming your cat object has an id property
if($scope.cats.id === cat.id) {
index = i;
break;
}
}
};
Or any other way to locate the index of an object in an array.
ng-click="removeCat(category)"
$scope.removeCat = function (categoryToDelete) {
var index = $scope.cats.indexOf(categoryToDelete);
$scope.cats.splice(index, 1);
};

Sorting alphabetically in JQuery with two groups

I've got a todo list. Each row has a star icon that you can click, exactly like gmail. The difference here is that if you click a star it should sort to the top (higher priority), but also re-sort within the starred group by ascending alpha. Unstarred items sort below, also sorted by ascending alpha. Everything is working as expected except for the alpha sorting. Below is the sort function where I'm doing that. I've verified that everything works below except the //sort the arrays by alpha bit...
Sort fail:
function sortTasks(currList) {
var starredTasks = [];
var unstarredTasks = [];
//create arrays
$('li.task').each(function(){
if ($(this).children('img.star').attr('src') == "images/star_checked.gif") {
starredTasks.push($(this));
} else {
unstarredTasks.push($(this));
}
});
//sort the arrays by alpha
starredTasks.sort( function(a,b){ ($(a).children('p.task-name').text().toUpperCase() > $(b).children('p.task-name').text().toUpperCase()) ? 1 : -1;});
unstarredTasks.sort( function(a,b){ ($(a).children('p.task-name').text().toUpperCase() > $(b).children('p.task-name').text().toUpperCase()) ? 1 : -1;});
//draw rows starred first, unstarred second
$(currList).empty();
for (i=0; i < starredTasks.length; i++) {
$(currList).append(starredTasks[i]);
}
for (i=0; i < unstarredTasks.length; i++) {
$(currList).append(unstarredTasks[i]);
}
}
This array has been populated with the task rows in the order they were originally drawn. The data renders fine, but basically stays in the same order.
Example task row:
<div id="task-container" class="container">
<form name="enter-task" method="post" action="">
<input id="new-task" name="new-task" type="text" autofocus>
</form>
<h2 id="today">today</h2>
<ul id="today-list">
<li id="457" class="task">
<img class="star" src="images/star_checked.gif">
<p class="task-name" contenteditable>buy milk</p>
<p class="task-date"> - Wednesday</p>
</li>
</ul>
<h2 id="tomorrow">tomorrow</h2>
<ul id="tomorrow-list">
</ul>
<h2 id="future">future</h2>
<ul id="future-list">
</ul>
<h2 id="whenever">whenever</h2>
<ul id="whenever-list">
</ul>
</div>
Each item in the starredTasks array is an entire task row. I'm assuming that $(a) is the same level as $(li)?
and here's the function that triggers the sort:
$('body').on('click', 'img.star', function(){
var thisList = '#' + $(this).parent('li').parent('ul').attr('id');
if ($(this).attr('src') == 'images/star_checked.gif') {
$(this).attr('src', 'images/star_unchecked.gif');
} else {
$(this).attr('src', 'images/star_checked.gif');
}
sortTasks(thisList);
});
Also, I doubt it's worth mentioning, but the data is stored in mySQL and prepopulated via php.
I wasn't sure of a way to use .sort() directly on the $('li') without splitting it into separate arrays...
Anybody see my goof?
I don't see where you're adding the sorted list back into the DOM. If you're not, then that's the problem. Sorting an array of elements doesn't update the DOM at all.
Furthermore, your sorting is very expensive. It's better to map an array of objects that have the elements paired with the actual values to sort.
Finally, you appear to be using the same ID multiple times on a page. That's just wrong. it may work with jQuery's .children(selector) filter, but it's still wrong. You need to change that.
Here I map an array of objects that contain a text property holding the text to sort and a task property that holds the element.
I changed p#task-name to p.task-name, so you should change that to class="task-name" on the elements.
Then I do the sort using .localeCompare(), which returns a numeric value.
Finally, the .forEach() loop appends the elements to the DOM.
var data = starredTasks.map(function(t) {
return { task: t,
text: $(t).children('p.task-name').text().toUpperCase()
};
}).sort(function(obj_a, obj_b) {
obj_a.text.localeCompare(obj_b.text);
}).forEach(function(obj) {
original_container.append(obj.task);
});
This assumes starredTasks is an actual Array. If it's a jQuery object, then do starredTasks.toArray().map(func....
The original_container represents a jQuery object that is the direct parent of the task elements.

Find specific attribute inside a list with JavaScript/KnockoutJS

1. The goal
Find an attribute inside a list using JavaScript, KnockoutJS or jQuery.
2. The scenario
I have a store application working with KnockoutJS to dynamize its UI.
3. The problem
Each product available to add to shopping cart of my store has an add button, but it is only available if the product isn't on shopping cart already.
I need to iterate with the shopping cart to discover if each product of my available products list is already on it.
4. A few code ago...
Each product of my available products to add to list is using this fragment to toggle between the buttons:
<!-- ko if: Summary.hasItem($element) -->
<button class="btn btn-small action remove">
<i class="icon-minus"></i>
</button>
<!-- /ko -->
<!-- ko ifnot: Summary.hasItem($element) -->
<button class="btn btn-small action add">
<i class="icon-plus"></i>
</button>
<!-- /ko -->
As you can see, I'm using the hasItem() function to check if the product is already on the shopping cart or not — but I have to implement it, and I need your help to do this.
5. What I've already tried
As you can see below, I tried to make a loop to check product by product inside my shopping cart until...
self.hasItem = function (element) {
var $productId = $(element).closest("li").data("productid"),
products = self.products();
if (products.length > 0) {
for (var product in products) {
if (products[product].id() == $productId) {
return true;
} else {
return false;
}
}
}
};
... something went wrong! Continues in the next chapter.
6. Something goes wrong
My brain can not compute the logical, but as I have this loop for each button, it seems that when one runs, the other does not run, or if there is an item in the list, the other does not add.
7. Playground
Play with this!
8. I need to ask...
My loop/logic is right?
In your case this will work :
self.hasItem = function (element) {
var $productId = $(element).closest("li").data("productid"),
products = self.products();
return ko.utils.arrayFirst(products, function(p){
return (p.id() == $productId);
}) != null;
}
ko.utils.arrayFirst returns the first item that match the given predicate; otherwise null.
The code inside the for loop only executes for the first product.
You should have something like this:
for (var product in products) {
if (products[product].id() == $productId) {
return true;
}
}
return false;
Beside the loop, where you should probably use arrayFirst from ko.utils anyway, there seems to be a problem with the add function.
The ng-click binding provides the DOM event as the second parameter, so you probably want to change the signature of the method add to
function(data,event) {
var element = event.target;
}
See http://codepen.io/anon/pen/rcwil
self.add = function(model, event) {
console.log($(event.target));
var $productId = $(event.target).closest("li").data("productid"),
$productName = $(event.target).closest("h1");
self.products.push(new Product($productId, $productName));
};
You need to look at this a little differently, instead of calling a "hasItem", you should have something like "isSelected" within your Product object.
Then, you can loop around your Products and say (pseudo-code) if is not "isSelected" then show the add button. When this add button is clicked, then the "isSelected" property of your Product object will be set to true.
Make Knockout JS work with your self-contained objects where possible.
Such as:
<!-- ko foreach: Products -->
<!-- ko if: IsSelected --> // Automatically inherited from parent
<button class="btn btn-small action remove">
<i class="icon-minus"></i>
</button>
<!-- /ko -->
<-- /ko -->

Categories

Resources