How to handle data binding on a dynamic set of checkboxes - javascript

I'm trying to create an angular directive making me able to select from a list of items group by category. Each item should be selectable using a checkbox.
The input data to the directive is something like
[
{
"id": "1",
"name": "category1",
"items": [
{
"id": "1",
"name": "item1"
},
{
"id": "2",
"name": "item2"
},
{
"id": "3",
"name": "item3"
},
{
"id": "4",
"name": "item4"
}
]
},
{
"id": "2",
"name": "category2",
"items": [
{
"id": "5",
"name": "item5"
},
{
"id": "6",
"name": "item6"
}
]
}
]
And the object of pre-checked items is:
{
"1": [
"2",
"4"
]
}
Here item with id 2 and 4 from category with 1 should be pre-checked.
My code results in this view:
The checked-state is handled using the ng-checked directive:
<input id="item-{{item.id}}" value="{{item.id}}" type="checkbox" ng-checked="isSelected(cat,item)">
When checking/unchecking an item, the selected items object should be updated to reflect the current state. How can I handle this? Should all this be structured in a different way?
See my plumber: http://plnkr.co/edit/6fbfZnQCq5fq1zDp8VIB.

As always, there is multiple ways to achieve this. My suggestion:
Use ng-model on your inputs:
<input ng-model="selected[cat.id][item.id]"
id="item-{{item.id}}"
value="{{item.id}}"
type="checkbox"
ng-checked="selected[cat.id][item.id]">
This will require a slight modification of your selectedItems property (it is now an object instead of an array):
$scope.selectedItems = {
"1": {
"2": true,
"4": true
}
};
The ng-checked in the HTML will automatically check the items which are marked true.
I'm not sure how you want to handle the selection of categories, but I hope this will give you an idea!
Check the updated Plunker.

Related

How to add or update items in this multidimensional JSON?

Let's say we have some houses represented as JSON. Something like this:
[
{
"id": "1",
"code": "1",
"name": "Smith's",
"children": [
{
"id": "",
"code": "11",
"name": "Kitchen",
"children": [
{
"id": "",
"code": "111",
"name": "Sink",
"children": []
}
]
},
{
"id": "",
"code": "12",
"name": "Living Room",
"children": [
{
"id": "",
"code": "121",
"name": "Television",
"children": [
{
"id": "",
"code": "1211",
"name": "Panel buttons",
"children": [
{
"id": "",
"code": "12111",
"name": "Power button",
"children": []
},
{
"id": "",
"code": "12112",
"name": "Colors adjust button",
"children": []
}
]
},
{
"id": "",
"code": "1221",
"name": "Screen",
"children": []
}
]
}
]
}
]
},
{
"id": "2",
"code": "2",
"name": "Taylor's",
"children": [
// Here goes all house places and items like the example above
]
},
{
"id": "1",
"code": "1",
"name": "Wilson's",
"children": [
// Here goes all house places and items like the example above
]
}
]
Take notice that the "code" property, found in each item, is something to represent the "path" until that item, carrying its parents "code" property concatenated with its own position by incremental order. So the code "11" means house 1 and child 1. And 212 would be house 2, child 1, child 2. Also take notice that all items follow the same type. In other words, every item has a children that follows its own type. So, it could be infinite.
Now, I'd like to maintain these structure. Adding items, updating items and so on. Let's say we want to add a carpet in Smith's living room. We would go deep in the structure 2 levels, which are Smith's house (index 0 of the array) and living room (index 1 of the children array). And then add a carpet.
The problem is it won't be 2 levels in all cases. What if I wanted to add a bathroom? It would be level 1, alongside with kitchen in living room (the first children). What if I'd like to add a microwave in the kitchen and add to it buttons, display, etc?
I think I'm a recursive scenario where I have to visit all items and, if it is the one I'm looking to reach at, add/updated it.
I've tried following this example
I couldn't figure it out how to bring it to my case. though.
I appreciate if your contribution is in JavaScript, but feel free to represent it in other language in case you are better in other language =).
There are indeed some questions, like for instance what happens if you have more than 10 items as child and why do you need it?
And what happens if you remove any item on any level? will you recursively start updating all codes?
Nevertheless I gave it a go. In essence what I do in the code is first search for the parent (example: Kitchen) where you want to add it to and then add the new child item (example: Carpet) to it.
The search is a typical recursive search.
The child addition is a typical addition to an array.
For argument's sake I assumed that the fields code always exist and that children is always an array.
// Actual code is underneath the declaration of this array
let houseList = [
{
"id": "1",
"code": "1",
"name": "Smith's",
"children": [
{
"id": "",
"code": "11",
"name": "Kitchen",
"children": [
{
"id": "",
"code": "111",
"name": "Sink",
"children": []
}
]
},
{
"id": "",
"code": "12",
"name": "Living Room",
"children": [
{
"id": "",
"code": "121",
"name": "Television",
"children": [
{
"id": "",
"code": "1211",
"name": "Panel buttons",
"children": [
{
"id": "",
"code": "12111",
"name": "Power button",
"children": []
},
{
"id": "",
"code": "12112",
"name": "Colors adjust button",
"children": []
}
]
},
{
"id": "",
"code": "1221",
"name": "Screen",
"children": []
}
]
}
]
}
]
},
{
"id": "2",
"code": "2",
"name": "Taylor's",
"children": [
// Here goes all house places and items like the example above
]
},
{
"id": "1",
"code": "1",
"name": "Wilson's",
"children": [
// Here goes all house places and items like the example above
]
}
]
addChild(houseList,"11",{name:"Carpet" });
addChild(houseList,"1211",{name: "Volume Up Button"});
addChild(houseList,"1211",{name: "Volume Down Button"});
console.log('new houselist', houseList);
// child is just what you want to add and the parentCode refers to where you want to add it to
function addChild(houseList, parentCode, child) {
let parent = findInHouseList(houseList,parentCode,child);
let amountOfChildren = parent.children.length;
let newCodeName = parentCode +""+ (amountOfChildren+1);
child = {...{id: "", code: newCodeName, children: []}, ...child};
console.log('adding child ', child);
parent.children = [...parent.children, child];
}
function findInHouseList(houseList,code) {
for (let house of houseList) {
let foundElement = findElement(house,code);
if ( foundElement)
return foundElement;
}
}
function findElement(currentElement, code) {
if ( currentElement.code === code)
return currentElement;
if (currentElement.children?.length > 0)
{
for (let child of currentElement.children) {
let foundElement = findElement(child,code);
if ( foundElement)
return foundElement;
}
}
return null;
}
I decided to let the code manage the code names for new children. It seems the easiest.
What you're trying to do is updating a JSON value at a dynamic path.
This function will append a child to the item which holds the specified code.
You may add conditions to check if the item at the code is defined
function appendChild(houses, code, item) {
let path = code.split('')
let o = houses
for (let i = 0; i < path.length; i++) {
let n = path[i] - 1
o = o[n]["children"]
}
o.push(item)
return houses
}
However, you should start your code indexes at 0 and storing them inside the JSON is useless since they are simply the path to reach the item.

Filter nested observable array RxJs Angular

I am trying to filter some observable nested array in angular with the filter function in combination pipe function of the RxJs library.
Question:
I only want to show the categories with surveys given by a specific date.
Simplified situation:
My angular component has 3 radiobuttons (values 1,2,3). If i click on one of them it goes to my 'FilterChanged($event.value)' function. In this function i would like to filter the data that is provided by an api. This api at first provides all the categories. After retrieving the data i would like to filter according to the radio-button.
This is the data i get back from the api:
[
{
"category": "A",
"surveys": [
{
"day": "1",
"answers": [
{
"name": "1",
"value": "a"
},
{
"name": "2",
"value": "b"
},
{
"name": "3",
"value": "c"
}
]
},
{
"day": "2",
"answers": [
{
"name": "1",
"value": "a"
},
{
"name": "2",
"value": "b"
},
{
"name": "3",
"value": "c"
}
]
},
{
"day": "3",
"answers": [
{
"name": "1",
"value": "a"
},
{
"name": "2",
"value": "b"
},
{
"name": "3",
"value": "c"
}
]
}
]
},
{
"category": "B",
"surveys": [
{
"day": "2",
"answers": [
{
"name": "1",
"value": "a"
},
{
"name": "2",
"value": "b"
},
{
"name": "3",
"value": "c"
}
]
},
{
"answers": [
{
"name": "1",
"value": "a"
},
{
"name": "2",
"value": "b"
},
{
"name": "3",
"value": "c"
}
]
},
{
"day": "2",
"answers": [
{
"name": "1",
"value": "a"
},
{
"name": "2",
"value": "b"
},
{
"name": "3",
"value": "c"
}
]
}
]
}
]
If radio button 1 is selected i would like to only show the category A and only show it has 1 survey because thats the only survey matching the filter.
Whyd doesn't this code works?? the update filter gets triggerd at the radiobox change event. For me this is much more readable than the reduce with spreader functions.
updateFilter(filterDays : number): void {
var filterDate = this.getFilterDate(filterDays);
this.surveyTypes$ = this.allSurveyTypes$.pipe(map((types) => this.filterSurveyTypes(types, filterDate)));
}
filterSurveyTypes(types : SurveyType[], filterDate : Date) : SurveyType[] {
return types.filter(type => type.surveys.filter(survey => moment(survey.filledInDate).isSameOrAfter(filterDate)).length);
}
and a lot more variations but it does not seem to work.
I think i should not need a map because i am not transforming any data so filter should be fine but is not working for me so far.
I appreciate any help. Thanks
not positive what you're looking for, but it seems like you want to filter the outer array based on what's in the inner array and also filter the inner array, this can be achieved in one pass with reduce:
function filterOuterByInner(array, value) {
return array.reduce((acc, v) => {
const tmp = { ...v }; // create shallow copy
tmp.surveys = tmp.surveys.filter(a => a.day === value); // filter surveys array by value
if (tmp.surveys.length)
acc.push(tmp); // add to acc array if any surveys after filter
return acc;
}, []);
}
then just use it in your map:
this.categories$ = combineLatest(this.allcategories$, this.value$).pipe(map(([categories, val]) => filterOuterByInner(categories, val)));
This would work for you too:
let teste = [];
allcategories.forEach( category => {
category.surveys.forEach( survey => {
if (survey.day == '1'){
teste.push(survey)
}
})
})
It depends how you are using the observable, here are 2 examples :
If you want to set the categories in a property, not as observable, you have to use the subscribe method like this:
this.subscription = this.categories$.subscribe({
next: categories => this.categories = categories
});
then use this.categories. In this case do no not forget to call this subscription.unsubscribe() when destroying the component.
If you are using it in your component template with the async pipe it should work fine.
Remark: an observable is not activated if there is no subscribe (the async pipe does the subscribe and unsubscribe)
The problem I am seeing here is: the filter function is not synchronous. This means, when you call the second filter, the first is answered with a Promise-like response. Since your code is not asynchronous, filter will respond with all the elements, or none.
To solve this problem, you would have to declare your function as an asynchronous function.
Here's some example on how to do so:
async function awesomeFilter(allcategories){
return await allcategories.filter(category =>
category.surveys.filter(survey => survey.day == '1').length
)
}
The survey.day would be your nested verification;
The .length would return to the first filter 0 if no correspondences are found or positive value if there are, telling the first filter where the correspondences are.
This way I was able to make it work. Hopefully it will help you.

MongoDb update with ElemMatch

I have a collection that has document structure like following:
Mongo PlayGround
{
"basicDetails": {
"id": "1",
"name": "xyz"
},
"tasks": [{
"id": "10",
"name": "task10",
"subtasks": [{
"id": "120",
"name": "subTask120",
"description": "ABC"
}]
}]
}
As you can see, each document has basicDetails object and a tasks array. Each task contains some properties of its own and a subtasks array.
I want to update subtasks's description from ABC to XYZ
where root level id is 1, task'id is 10 and subTasks.id =120
How do I do so?
I know I could find correct document via:
db.collection.find({
"basicDetails.id": "1",
"tasks": {
"$elemMatch": {
"id": "10",
"subtasks": {
"$elemMatch": {
"id": "120"
}
}
}
}
})
But how do I update it? I want to update only one single property of a single subtasks i.e description
To update nested arrays, the filtered positional operator $[identifier] identifies the array elements that match the arrayFilters conditions for an update operation.
Try the following query to $set in nested array:
db.collection.updateOne({
"basicDetails.id": "1"
},
{
"$set": {
"tasks.$[tasks].subtasks.$[subtasks].description": "XYZ"
}
},
{
"arrayFilters": [
{
"tasks.id": "10"
},
{
"subtasks.id": "120"
}
]
})
MongoDB Playground

React: trying to check/uncheck checkbox

I'm trying to make an obligatory Todo app to help with my React learning . The behavior I'm going for is multiple Todo lists, where you select a todo name and the list of todo items for that show. Select a different todo name and it's list todo items show, etc (like wunderlist/msft todo). For now it's using static json where each item has a child array.
I'm trying to check/uncheck a checkbox in order to mark the todo as done. My problem is that when a click the checkbox it doesn't update until a click away and then click back. What do I need to do to get it to update immediately?
code sandbox: https://codesandbox.io/s/github/cdubone/todo-react-bootstrap?file=/src/App.js
Here's the relevant code:
The function:
const updateChecked = todo => {
todo.IsChecked = !todo.IsChecked;
};
Properties on the component:
onChange={() => updateChecked(todo)}
isChecked={todo.IsChecked}
The input in the component:
<input
type="checkbox"
id={props.id}
name={props.id}
value={props.title}
checked={props.isChecked}
onChange={props.onChange}
/>
Here's the data -
JSON:
const TodoData = [
{
"Id": 1,
"Title": "Groceries",
"TodoList": [
{
"Id": 1,
"Title": "Apples",
"IsChecked": false
},
{
"Id": 2,
"Title": "Oranges",
"IsChecked": false
},
{
"Id": 3,
"Title": "Bananas",
"IsChecked": true
}
]
},
{
"Id": 2,
"Title": "Daily Tasks",
"TodoList": [{
"Id": 11,
"Title": "Clean Kitchen",
"IsChecked": false
},
{
"Id": 12,
"Title": "Feed Pets",
"IsChecked": false
},
{
"Id": 13,
"Title": "Do Stuff",
"IsChecked": false
}]
},
{
"Id": 3,
"Title": "Hardware Store",
"TodoList": []
},
{
"Id": 4,
"Title": "Costco",
"TodoList": [{
"Id": 21,
"Title": "Diapers",
"IsChecked": false
},
{
"Id": 22,
"Title": "Cat Food",
"IsChecked": false
},
{
"Id": 23,
"Title": "Apples",
"IsChecked": false
},
{
"Id": 24,
"Title": "Bananas",
"IsChecked": false
}]
},
{
"Id": 5,
"Title": "Work",
"TodoList": [
{
"Id": 34,
"Title": "TPS Reports",
"IsChecked": true
}
]
}
]
export default TodoData;
You're mutating state directly rather than using setTodoData to update (a lot of your functions are doing this). Also, you're mapping the the todo items of off ActiveTodoList rather than the todoData which creates a big issue. In react there should only be 1 source of truth, so it would be better to instead store the index of the active list and map "todoData[activeIndex].TodoList.", than to store an instance of the active list itself. Finally, because the data is so nested, you need the list index and the todo item index passed into your update function to reference its location in todoData. Map lets you pass index as a second parameter.
Something along the lines of the below:
todoData[activeIndex].TodoList.map((todo, todoIndex) => {
return (
<div onClick={()=> yourFunction(todo, todoIndex, activeIndex)}> </div>
)
}
yourFunction = (todo, todoIndex, listIndex) => {
let newTodo = {...todo, IsChecked: !todo.IsCheck};
setTodoData((todoData) => [
...todoData.slice(0,listIndex),
{
...todoData[listIndex],
TodoList: [
...todoData[listIndex].Todolist.slice(0,todoIndex)
,newTodo,
...todoData[listIndex].Todolist.slice(todoIndex + 1)
]
},
...todoData.slice(listIndex + 1)
]
}
The goal is not to change things directly, but make copies to send to state. Unfortunately with a nested object it can get confusing very fast.

Angular weirdness: how can one object attribute change an attribute on two different objects?

I'm building a website using Angularjs in which I've got a list of objects:
$scope.fieldsToShow = [
{
"fields": {},
"type": "LOGGED_IN"
},
{
"fields": {},
"type": "PERSONAL",
"user": 2,
"name": "Rick Astley"
}
];
I then select one of the objects into a variable:
var $scope.currentObject = $scope.fieldsToShow[1];
to let the user change it using the some checkboxes:
<input ng-model="currentObject.fields.a" type="checkbox">
which changes both $scope.currentObject:
{
"fields": {
"a": true
},
"type": "PERSONAL",
"user": 2,
"name": "Rick Astley"
}
and the original object in $scope.fieldsToShow:
$scope.fieldsToShow = [
{
"fields": {},
"type": "LOGGED_IN"
},
{
"fields": {
"a": true
},
"type": "PERSONAL",
"user": 2,
"name": "Rick Astley"
}
];
I then change the $scope.currentObject to the first object in the array:
$scope.currentObject = $scope.fieldsToShow[0];
and I click the checkbox again. As expected this also adds "a": true to the fields object of the first object in the $scope.fieldsToShow list. So far so good.
I now want to add an object within the fields object. So I created another checkbox:
<input ng-model="currentObject.fields.anInnerObject.val" type="checkbox">
I then change to the PERSONAL object again ($scope.currentObject = $scope.fieldsToShow[1];) and click the checkbox. As expected this changes both the $scope.currentObject:
{
"fields": {
"anInnerObject": {
"val": true
}
},
"type": "PERSONAL",
"user": 2,
"name": "Rick Astley"
}
and the original object in $scope.fieldsToShow:
$scope.fieldsToShow = [
{
"fields": {},
"type": "LOGGED_IN"
},
{
"fields": {
"anInnerObject": {
"val": true
}
},
"type": "PERSONAL",
"user": 2,
"name": "Rick Astley"
}
];
I then change to the LOGGED_IN object again ($scope.currentObject = $scope.fieldsToShow[0];) and click the checkbox again. And here is where it gets tricky. As expected it changes the $scope.currentObject:
{
"fields": {
"anInnerObject": {
"val": true
}
},
"type": "LOGGED_IN",
}
it also changes the original object in $scope.fieldsToShow (still as expected), BUT it ALSO changes the value of "anInnerObject" in the PERSONAL object to the boolean true:
$scope.fieldsToShow = [
{
"fields": {
"anInnerObject": {
"val": true // This changed, which I understand
}
},
"type": "LOGGED_IN"
},
{
"fields": {
"anInnerObject": true // BUT WHY this value also change? And why did it become true?
},
"type": "PERSONAL",
"user": 2,
"name": "Rick Astley"
}
];
How in the world can this happen?! I've been banging my head against the wall for hours, tried a million things and asked a colleague for help, but I simply can't find why this behaves like it does?
Does anybody know how this can happen? All tips are welcome!
You need to understand that both both $scope.currentObject and the original object in $scope.fieldsToShow the same object. Both are just references to the same memory location. This is how references to non-primitive types work in Javasript.
If you want to have truly separate different object you need to clone it before using:
var $scope.currentObject = angular.copy($scope.fieldsToShow[1]);

Categories