Splice removing wrong object from ng-repeat in AngularJS - javascript

I'm listing an array of names in my view like this:
<div class="checkbox col-md-3" ng-repeat="staff in stafflist | orderBy: 'name'">
<div class="checkboxinner">
<button class="btn btn-staff form-control"
ng-show="!staff.chosen"
ng-click="pushStaff(staff)">
{{staff.name}}
</button> // visible when unselected, invisible when selected
<button class="btn btn-primary form-control"
ng-show="staff.chosen"
ng-click="unpushStaff(staff, $index)">
{{staff.name}}
</button> // visible when selected, invisible when unselected
</div>
</div>
The first button triggers this function, adding the object into the array and being replaced with another button (different color, same content) that is supposed to act as a toggle. This function works perfectly.
$scope.paxlist = [];
$scope.pushStaff = function (staff) {
staff.chosen = true;
$scope.paxlist.push(
{
name: staff.name
}
);
console.log($scope.paxlist);
};
Basically, when I click I add the object, when I click again, I remove it. Here's the remove function:
$scope.unpushStaff = function (staff, $index) {
staff.chosen = false;
var index=$scope.paxlist.indexOf(staff)
$scope.paxlist.splice(index,1);
console.log($scope.paxlist);
}
My problem is that the unpushStaff() will indeed remove an item, but not the item I clicked to remove, but another one.
What am I missing?
Maybe the ng-show is messing with the $index?

Your staff entry in stafflist and the entry in paxlist are not identical. Based on your template below:
<button class="btn btn-staff form-control"
ng-show="!staff.chosen"
ng-click="pushStaff(staff)">
{{staff.name}}
</button> // visible when unselected, invisible when selected
It is clear that each staff entry in stafflist is some sort of object that has at least one attribute name and another chosen.
When you push onto paxlist, you are creating a new object that looks like:
$scope.paxlist.push(
{
name: staff.name
}
);
This is fine. But when you then come to remove it, you are looking for it by:
var index=$scope.paxlist.indexOf(staff)
where staff is the object in stafflist! Of course, that object does not exist in paxlist - a separate object you derived above in paxlist.push() is - and so indexOf() is returning -1, leading splice() to remove the last item on paxlist.

Related

Vuejs and HTML creating a complex JSON Object dynamically and displaying the same to user using V-for

I am developing a Vuejs application within which I have a field. For this field, users can provide the values and this field expands dynamically based on the user-provided values.
The field name is extensions, initially an Add Extension button will be displayed. With on click of the button, a bootstrap modal will be displayed which has 3 fields: namespace (text), localname (text), datatype(dropdown: string/complex). If the datatype is string then a simple text field will be displayed. However, if the datatype is complex then another button should be displayed and with on click of the button again the same modal is displayed with fields and the process continues. So the created JSON based on this will expand horizontally and vertically.
I am able to complete the first iteration and display the elements to users on the front end. However, for further iteration, I am not understanding how to achieve it using the recursive approach. Since I don't know how many times users may create the extensions I need to have an approach that dynamically does this.
Can someone please help me how to create and display the JSONarray using Vuejs which expands horizontally and vertically?
Following is the code I have so far:
<template>
<div class="container-fluid">
<div class="row">
<div class="col-md-3">
<span>Extensions</span>
<button class="form-control" #click="createExtensions()">
Add Another
</button>
</div>
</div>
<div v-for="extension in extensionList" :key="extension.ID" class="form-inline">
<span>{{ extension.namespace+ ":"+extension.localName }}</span>
<input v-if="extension.dataType == 'string'" type="text" #input="AddExtensionText($event,extension.ID)">
<button v-if="extension.dataType == 'complex'" #click="AddComplextExtension(extension.ID)">
Add another
</button>
</div>
<b-modal
id="Extension"
title="Add Another Element"
size="lg"
width="100%"
:visible="showModal"
>
<b-form id="AddExtension" #submit.prevent="submitExtension">
<div class="form-group">
<label for="message-text" class="col-form-label">Namespace URI:</label>
<input
v-model="extension.namespace"
type="text"
class="form-control"
required
>
</div>
<div class="form-group">
<label for="message-text" class="col-form-label">Local Name:</label>
<input
v-model="extension.localName"
type="text"
class="form-control"
required
>
</div>
<div class="form-group">
<label for="AddExtensionDataType" class="col-form-label">Data Type:</label>
<b-form-select v-model="extension.dataType" class="form-control">
<b-form-select-option value="string">
String
</b-form-select-option>
<b-form-select-option value="complex">
Complex
</b-form-select-option>
</b-form-select>
</div>
</b-form>
<template #modal-footer="{ cancel }">
<b-btn #click="cancel">
Cancel
</b-btn>
<b-btn variant="primary" type="submit" form="AddExtension">
OK
</b-btn>
</template>
</b-modal>
</div>
</template>
<script>
export default {
data () {
return {
extensionList: [],
extensionCount: 0,
extension: {
namespace: '',
localName: '',
dataType: 'string'
},
showModal: false
}
},
methods: {
// Method to create extensions and add
createExtensions () {
this.showModal = true
},
// Function to submit the each extension
submitExtension () {
this.showModal = false
const extensionObj = {}
extensionObj.ID = this.extensionCount
extensionObj.namespace = this.extension.namespace
extensionObj.localName = this.extension.localName
extensionObj.dataType = this.extension.dataType
this.extensionList.push(extensionObj)
this.extensionCount++
},
// On addition of the input field value update List
AddExtensionText (event, extensionID) {
const extension = this.extensionList.filter(ex => ex.ID === extensionID)[0]
if (typeof extension !== 'undefined') {
extension.text = (typeof event.target.value !== 'undefined') ? event.target.value : ''
}
},
// Add complex extension
AddComplextExtension (extensionID) {
this.showModal = true
}
}
}
</script>
<style>
</style>
This is the initial field I have:
This is what I want to achieve where everything is created dynamically and JSON expands both horizontally and vertically:
Can someone please let me know how to create such dynamic JSON using Vuejs and display the same in the frontend.
To display data recursively, you need to use recursive components.
Abstract your v-for code into another component file (let's call it NodeComponent.vue). Pass your extensionList to this component, then inside this component, add another NodeComponent for each extension which has type complex.
Since your extension would be another array if it is complex, you can pass it directly into this NodeComponent as a prop and let recursion work its magic.
NodeComponent.vue
<template>
<div>
<div
v-for="extension in extensionList"
:key="extension.ID"
class="form-inline"
>
<span>{{ extension.namespace + ":" + extension.localName }}</span>
<input
v-if="extension.dataType == 'string'"
type="text"
#input="$emit('AddExtensionText', {$event, id: extension.ID}"
/>
<NodeComponent v-if="extention.dataType == 'complex'" :extentionList="extension" #AddExtensionText="AddExtensionText($event)"/>
<button
v-if="extension.dataType == 'complex'"
#click="AddComplextExtension(extension.ID)"
>
Add another
</button>
</div>
</div>
</template>
<script>
export default {
props: {
extensionList: Array,
extension: Object,
},
methods: {
AddComplextExtension(extensionID) {
// Emit event on root to show modal, or use this.$bvModal.show('modal-id') or create a dynamic modal, see: https://bootstrap-vue.org/docs/components/modal#message-box-advanced-usage
}
AddExtensionText({ value, id }) {
const i = this.extensionList.findIndex((el) => el.ID === id);
this.$set(extensionList, i, value);
}
}
};
</script>
Note that I emit a custom event from child NodeComponents on changing input text so that the parent can make this change in its extensionList array, using this.$set to maintain reactivity.
EDIT: If you want to add new Node components:
You need to have a parent component that holds the first NodeComponent in it. In here you'll define the modal (if you define it inside NodeComponent, you'll have a separate modal reference for each NodeComponent. Judging from your code you're probably using Bootstrap-Vue, it injects modals lazily when shown, so I don't think this will affect your performance too much, but it still doesn't feel like good code.). You need to emit event on root to show the modal. You need to send the extensionList as payload with this event like this: this.$root.emit('showModal', extensionList). In you parent component you can listen to the event and show the modal. Now inside your submitExtension function, you can use this extensionList and push a new object to it. The corresponding NodeComponent will update itself since arrays are passed by reference.
this.$root.on('showModal`, (extensionList) => {
this.editExtensionList = extensionList;
showModal = true;
}
submitExtension() {
this.showModal = false
const extensionObj = {}
extensionObj.ID = this.extensionCount
extensionObj.namespace = this.extension.namespace
extensionObj.localName = this.extension.localName
extensionObj.dataType = this.extension.dataType
this.editExtensionList.push(extensionObj)
this.extensionCount++
}
All being said, at this point it might be worthwhile to invest in implementing a VueX store where you have a global extentionList and define mutations to it.

Filter dropdown not working when selected an item

My project is trying to do a dropdown filter and when I select a certain item like soccer in the dropdown it doesn't come up showing that athlete in the specific item selected. Below is the code for my javascript filtering and [my image is supposed to show the soccer player since that was the filter selected][1]. and my HTML is below also for how it is displayed
//returns the value of selected value and filters the athleteList by the selected dropdown and value
filterObj.selectedValue = function(anchorObj) {
console.log("Selected Value: " + anchorObj.id);
//This is used to set the initial value of valueLabel
//If (anchorObj.id === undefined) is true the operator returns the value of the first expression
//If it's false it returns the value of the second expression
valueLabel = (anchorObj.id === undefined) ? "" : (": " + anchorObj.id);
//Calls the filterLabel function and sets it's parameters to the user selected dropdown and filter value
filterLabel(dropdownLabel, valueLabel);
//The switch takes the activeFilter as a parameter which was assigned a value in the selectedDropDown function
//The if statement parameters check if the user selected "All-Sports" if thats the value the user
//selected filterFn remains null since those options are made to display everything
//The function will only return true when the object in the array has a key/value pair that matches the filter
//For example if we select the Sport and then Soccer, Sport is key and Soccer is our value so the object
//has a Sport key equal to Soccer it will return true
var filterFn = null;
switch (activeFilter) {
case "Sport":
if (anchorObj.id === "All-Sports") {
filterFn = null;
break
}
filterFn = function (c) {
return anchorObj.id === c.sport;
};
}
//The switch statement returns two possible types of values. It's either set to null or to the value of a function that returns/sets a value of true
//or false for every elements in the array
//The ternary operator is used to set the value of filteredAthletes
//If filterFn is not null is true the first statment executes, otherwise the second statement executes
//The first statement takes the athletesList, which is the object list, applies the filter method to it with the parameter filterFn
//The second statement returns the unfiltered athleteList
//The filter method takes every object in filterFn that returned true and those are the only objects stored in the filterAthletes array
var filteredAthletes = filterFn ? athleteList.filter(filterFn) : athleteList;
console.log(filteredAthletes);
//The final step is to display the filteredAthletes
//We do this by setting the innerHTML value of our div with id listHere to ${filteredAthletes.map(athleteTemplate).join("") which
//returns a new array (maps) with the specific elements referenced in the athleteTemplate function and joins them to a string seperated by a space
document.getElementById("listHere").innerHTML = `
${filteredAthletes.map(athleteTemplate).join("")}
`;
};
//Called to display unfiltered list
filterObj.selectedValue({});
//Athlete template (keep in this file because its referenced by the function above) pulic method
//This is a basic template for displaying array of objects on your HTML page
//This can be manipulated and changed to fit the requirements/parameters of your array of objects
//This template requires two things to be displayed, a div in the HTML code and a to be added to the document using the div's id
//The code snippet that adds this template to its designated div on the HTML page is in the function below
//<div class="column"><div class = "content"><a href="${athlete.url}" target="_blank">
function athleteTemplate(athlete) {
return `
<div class = "content">
<img src="${athlete.image}">
<div class="overlay">
<div class="text">
<h3>${athlete.athleteName}</h3>
Born: ${athlete.birthday} <br><br>
Age: ${athlete.age} <br><br>
Sport: ${athlete.sport} <br><br>
Learn More
</div>
</div>
</div>
`;
}```
//html
<body>
<div class ="dropContainer">
<div class="dropdown">
<button title="Sport" onclick="filter.myFunction(); filter.selectedDropDown()" class="dropbtn">Sports</button>
<div id="Sport" class="dropdown-content">
<input type="text" placeholder="Search..." id="myInput2" onkeyup="filter.filterFunction('myInput2')">
<a id="All-Sports" onclick="filter.selectedValue(this)" href="#all">Show All</a>
<a id="Soccer" onclick="filter.selectedValue(this)" href="#Soccer">Soccer</a>
<a id="Basketball" onclick="filter.selectedValue(this)" href="#Basketball">Basketball</a>
<a id="Football" onclick="filter.selectedValue(this)" href="#Football">Football</a>
<a id="Baseball" onclick="filter.selectedValue(this)" href="#Baseball">Baseball</a>
<a id="Golf" onclick="filter.selectedValue(this)" href="#Golf">Golf</a>
<a id="Tennis" onclick="filter.selectedValue(this)" href="#Tennis">Tennis</a>
</div>
</div>
</div>
<center>
<div class="header">
<h1>Dropdown Filter with Image Overlay</h1>
<h2>Select the 'Sports' menu and select an option to filter the list by. <br>
Use the Search Bar to filter the dropdown. <br>
Hover over the images for more information on the athlete. <br>
Click 'Learn More' to go to their Wiki page.</h2>
<h2>Current Filter:</h2>
<div class="filter" id='filteredBy'></div>
</div>
<div class="wrapper" id="listHere"></div>
</center>
<script src="js/athletes.js"></script>
<script src="js/MakeFilter.js"></script>
<script>
var athleteList = getAthleteList();
var filter = MakeFilter("Sport", "Filter", "myInput2", athleteList);
</script>
</body>
[1]: https://i.stack.imgur.com/YIHKA.jpg

knockout observableArray remove causes errors and fails to remove

I have an issue that I have spent two days on trying to figure out, I will try to put everything here to explain the background without unnecessary information but please ask and I shall provide the info.
The problem
The process is, that the user selects the team they want to add to the competition, clicks add, the team selected will then be removed from the main teams list, and added to the competition.teams list. To remove a team, the user selects the team from the options box, and clicks remove. This will remove the team from the competition.teams array, and re-added to the teams array.
I have a list of "teams" in a drop down box, with a button to "add team". When clicked, it will add the team selected from the drop-down box to the select options box. When I remove the Team from the box, it will fail to remove it from the parent list that is data bound to the options box.
When the team is then readded, both entries in the select are the same team name.
The desired outcome
I want the code to work as described above, its possible I have over-engineered the solution due to my limited knowledge of knockout/javascript. I am open to other solutions, I havent got to the stage of submitting this back to the server yet, i predict this will not be as easy as a normal form submit!
The exception
The error in the chrome console is:
Uncaught TypeError: Cannot read property 'name' of undefined
at eval (eval at parseBindingsString (knockout-min.3.4.2.js:68), :3:151)
at f (knockout-min.3.4.2.js:94)
at knockout-min.3.4.2.js:96
at a.B.i (knockout-min.3.4.2.js:118)
at Function.Uc (knockout-min.3.4.2.js:52)
at Function.Vc (knockout-min.3.4.2.js:51)
at Function.U (knockout-min.3.4.2.js:51)
at Function.ec (knockout-min.3.4.2.js:50)
at Function.notifySubscribers (knockout-min.3.4.2.js:37)
at Function.ha (knockout-min.3.4.2.js:41)
The Code
The HTML for the screenshot:
<div class="form-group">
<div class="col-md-3">
<label for="selectedTeams" class="col-md-12">Select your Teams</label>
<button type="button" data-bind="enable:$root.teams().length>0,click:$root.addTeam.bind($root)"
class="btn btn-default col-md-12">Add Team</button>
<button type="button" data-bind="enable:competition().teams().length>0,click:$root.removeTeam.bind($root)"
class="btn btn-default col-md-12">Remove Team</button>
<a data-bind="attr:{href:'/teams/create?returnUrl='+window.location.pathname+'/'+competition().id()}"class="btn btn-default">Create a new Team</a>
</div>
<div class="col-md-9">
<select id="teamSelectDropDown" data-bind="options:$root.teams(),optionsText:'name',value:teamToAdd,optionsCaption:'Select a Team to Add..'"
class="dropdown form-control"></select>
<select id="selectedTeams" name="Teams" class="form-control" size="5"
data-bind="options:competition().teams(),optionsText:function(item){return item().name;},value:teamToRemove">
</select>
</div>
</div>
The addTeam button click code:
self.addTeam = function () {
if ((self.teamToAdd() !== null) && (self.competition().teams().indexOf(self.teamToAdd()) < 0)){// Prevent blanks and duplicates
self.competition().teams().push(self.teamToAdd);
self.competition().teams.valueHasMutated();
}
self.teams.remove(self.teamToAdd());
self.teamToAdd(null);
};
the removeTeam button click code:
self.removeTeam = function () {
self.teams.push(self.teamToRemove());
self.competition().teams.remove(self.teamToRemove());
self.competition().teams.valueHasMutated();
self.teamToRemove(null);
};
the Competition object (some properties removed for brevity):
function Competition(data) {
var self = this;
self.id = ko.observable(data.id);
self.name = ko.observable(data.name);
self.teams = ko.observableArray(
ko.utils.arrayMap(data.teams, function (team) {
return ko.observable(new Team(team));
}));
};
the team object:
function Team(data) {
var self = this;
self.id = ko.observable(data.id);
self.name = ko.observable(data.name);
}
Anything missing or unclear? Please ask and I will add to the materials on the question.
The Solution
As suggested by #user3297291
The problem was that the objects being added to competition.teams were observable in some places and not observable in others. This was causing a binding error in some places where it would try to access the observable property inside the observable object.
Changed Competition Object
function Competition(data) {
var self = this;
self.id = ko.observable(data.id);
self.name = ko.observable(data.name);
self.teams = ko.observableArray(
ko.utils.arrayMap(data.teams, function (team) {
return new Team(team);
}));
};
Revised HTML binding (only simplified the optionsText binding)
<div class="form-group">
<div class="col-md-3">
<label for="selectedTeams" class="col-md-12">Select your Teams</label>
<button type="button" data-bind="enable:$root.teams().length>0,click:$root.addTeam.bind($root)"
class="btn btn-default col-md-12">Add Team</button>
<button type="button" data-bind="enable:competition().teams().length>0,click:$root.removeTeam.bind($root)"
class="btn btn-default col-md-12">Remove Team</button>
<a data-bind="attr:{href:'/teams/create?returnUrl='+window.location.pathname+'/'+competition().id()}"class="btn btn-default">Create a new Team</a>
</div>
<div class="col-md-9">
<select id="teamSelectDropDown" data-bind="options:$root.teams(),optionsText:'name',value:teamToAdd,optionsCaption:'Select a Team to Add..'"
class="dropdown form-control"></select>
<select id="selectedTeams" name="Teams" class="form-control" size="5"
data-bind="options:competition().teams(),optionsText:'name',value:teamToRemove">
</select>
</div>
</div>
Revised Add Team function
self.addTeam = function () {
if ((self.teamToAdd() !== null) && (self.competition().teams().indexOf(self.teamToAdd()) < 0)){
self.competition().teams().push(self.teamToAdd());
self.competition().teams.valueHasMutated();
}
self.teams.remove(self.teamToAdd());
self.teamToAdd(null);
};
Revised Remove Team Function
pretty sure I don't need the valueHasMutated() call anymore but at least it works..
self.removeTeam = function () {
self.teams.push(self.teamToRemove());
self.competition().teams.remove(self.teamToRemove());
self.competition().teams.valueHasMutated();
self.teamToRemove(null);
};
You're filling an observableArray with observable instances. This is something you generally should not do:
// Don't do this:
self.teams = ko.observableArray(
ko.utils.arrayMap(data.teams, function(team) {
return ko.observable(new Team(team));
})
);
Instead, include the Team instances without wrapping them:
// Do this instead:
self.teams = ko.observableArray(
ko.utils.arrayMap(data.teams, function(team) {
return new Team(team);
})
);
Now, you can use the "simple" optionsText binding, like you did earlier:
data-bind="optionsText: 'name', /* ... */"
Personal preference: you don't need the utils.arrayMap helper when we have .map in every browser. I'd personally write:
Team.fromData = data => new Team(data);
// ...
self.teams = ko.observableArray(data.teams.map(Team.fromData));

Angular JS: Add and remove terms from checkboxes in ng-repeat?

I have a miller column constructed in Angular and Bootstrap.
http://codepen.io/smlombardi/pen/WGwGbY
In the second column, clicking the word (link) opens the third column, but I need to have the checkbox add that word to an array of search terms.
If the checkbox is UN-checked, I need to remove that word from the array of search terms. As you can see in the pen, I have the adding part working, but un-checking the box adds the word again.
I realize what I need to do is somehow check the state of the checkbox and if it's true add the word and if it's false check the array for the word (string) and pop it out of the array.
I can't figure out how to check only the checkbox that was clicked.
<div class="col-xs-3 inner-column">
<div class="panel panel-default">
<div class="list-group">
<div class="list-group-item" ng-class="{active: $index === pmt.millercolumn.level1Selected }" ng-repeat="level1 in pmt.millercolumn.level1 track by $index">
<input type="checkbox" ng-model="activeSearchTerm" ng-change="pmt.change($index)" id="ng-change-example1" />
<a href="" ng-click="pmt.getSublevel2($index)" >
{{level1.name}}
<i class="pull-right fa fa-angle-right fa-lg"></i>
</a>
</div>
</div>
</div>
the ng-change on the checkbox calls:
_this.change = function (index) {
var searchTerm = _this.millercolumn.level1[index].name;
_this.searchTerms.push(searchTerm);
};
It looks like you're thinking in a jquery mindset where you need to handle events when something changes. An easier way would be to make each checkbox correspond to an item in the array, so the ng-model would be something like level1.isSelected. Then, to construct your search terms array, use scope.$watch and pass true as the 3rd argument to deep watch your array of items. When a checkbox is checked, your watch will be called and you can reconstruct the search terms array by plucking the terms of the list items that are selected.
Add this code in place of your _change function it works for sure
_this.change = function (index) {
console.log('Clicked on', _this.millercolumn.level1[index].name);
var searchTerm = _this.millercolumn.level1[index].name;
var searchIndex = _this.searchTerms.indexOf(searchTerm);
if (searchIndex == -1) { // If new push
_this.searchTerms.push(searchTerm);
}
else { // Else remove item
_this.searchTerms.splice(searchIndex, 1);
}
console.log(_this.searchTerms);
};
Working codepen demo : http://codepen.io/krishcdbry/pen/EgKgBv
You're running the same code no matter if the checkbox is checked or not. Try something like this:
_this.change = function (index, checked) {
var searchTerm = _this.millercolumn.level1[index].name;
if(checked){
_this.searchTerms.push(searchTerm);
}
if(!checked){
_this.searchTerms.splice(searchTerm);
}
};
FWIW, this is what I did, which works:
<input type="checkbox" ng-model="level1.isSelected" ng-change="pmt.change($index, level1)" id="mycb" />
_this.change = function (index, item) {
if (item.isSelected) {
_this.searchTerms.push(item.name);
} else {
var termToRemove = item.name;
for (var i = _this.searchTerms.length - 1; i >= 0; i--) {
if (_this.searchTerms[i] === termToRemove) {
_this.searchTerms.splice(i, 1);
}
}
}
};

Angular.js: Update (data-bind) only one item of ng-repeat

I have the following html:
<li ng-repeat="friend in friends">
<span ng-repeat="(key, value) in friend.aktuell">
<input type="text" ng-model="friend.aktuell[key]">
</span>
<quick-datepicker ng-model="auditDate"></quick-datepicker>
<button ng-click="audit(auditDate, friend._id)" class="btn btn-info">Audit</button>
</li>
Now how can I update only the input fields of the friend[index] - so to speak - that has been clicked via the audit button?
e.g.
$scope.audit = function(auditDate, id){
$scope.friends[1].aktuell = {someData:someValues}; // this works if the index is hard coded
});
};
Above works if the index '1' is hard coded into 'friends[1]', but of course I want to no update this in the second input row, but in the one that has been clicked.
Idea: Can I pass the current clicked "indexifier" to my audit function or alternatively can I alter the input fields where "friend._id = friend._id"?
Screenshot:
There is $index variable available inside ng-repeat:
ng-click="audit(auditDate, $index)"
Why don't you pass the friend reference instead of passing its index?
<button ng-click="audit(auditDate, friend)" class="btn btn-info">Audit</button>
And on the controller:
$scope.audit = function(auditDate, friend){
friend.aktuell = {someData:someValues};
});
};

Categories