How to disable other divs coming from loop except the clicked one - javascript

I have some li tags whose data is coming from a loop. When I click any li tag it will become active by changing its image and will store into localstorage so that on refresh the clicked one is still active. Here when we click, an active object is adding to json the clicked li tag and stores it in localstorage. Now the problem is when I click again on the clicked li tag or outside it, it's toggling the earlier image, which should not be happening. Again, other li tags should be disabled except the clicked one. Here is the code below https://stackblitz.com/edit/angular-skzgno?file=src%2Fapp%2Fapp.component.ts
app.component.html
<hello name="{{ name }}"></hello>
<p>
Start editing to see some magic happen :)
</p>
<div>
<pre>
</pre>
<ul>
<li *ngFor="let item of statusdata" (click)="toggleActive(item, !item.active)">
<span>{{item.id}}</span>
<span>{{item.name}}</span>
<span>
<img *ngIf="!item?.active || item?.active === false" src ="https://dummyimage.com/qvga" />
<img *ngIf="item?.active === true" src ="https://dummyimage.com/300.png/09f/fff" />
</span>
</li>
</ul>
</div>
app.component.ts
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
statusdata: any;
ngOnInit() {
this.statusdata = [
{ id: 1, name: "Angular 2" },
{ id: 2, name: "Angular 4" },
{ id: 3, name: "Angular 5" },
{ id: 4, name: "Angular 6" },
{ id: 5, name: "Angular 7" }
];
this.statusdata.forEach(item => {
this.getCacheItemStatus(item);
});
}
toggleActive(item, activeStatus = true) {
item.active = activeStatus;
localStorage.setItem(`item:${item.id}`, JSON.stringify(item));
}
getCacheItemStatus(item) {
const cachedItem = localStorage.getItem(`item:${item.id}`);
if (cachedItem) {
const parse = JSON.parse(cachedItem); // Parse cached version
item.active = parse.active; // If the cached storage item is active
}
}
}

It looks to me like you just need to add some logic to your toggleActive method. You could check if any items are active before deciding whether or not to do anything about the click. Does something like this solve your problem?
toggleActive(item, activeStatus = true) {
if(item.active || !this.statusdata.some(d => d.active)){
item.active = activeStatus;
localStorage.setItem(`item:${item.id}`, JSON.stringify(item));
}
}

Related

React JS Custom Sidebar Navigation

I have Created a toggled navigation in React APP. Everything working fine except the toggle. Below is the code of toggle. I am using the check of parentId if parentId exist then include the children id's and it will open the toggle. If not exist then add the main Id which one is parentId in children.
Problem is I parentId already exist and if I click the toggle it is not closing the old one and displaying the new one along with old.
const handleArrowClick = ( data: CoursesNav ) => {
const { id, parentId } = data;
let newtoggledMenus = [...toggledMenus];
if( parentId && newtoggledMenus.includes(parentId)){
if (newtoggledMenus.includes(id)) {
var index = newtoggledMenus.indexOf(id);
if (index > -1) {
newtoggledMenus.splice(index, 1);
}
} else {
newtoggledMenus.push(id);
}
settoggledMenus(newtoggledMenus);
}else{
settoggledMenus([id]);
}
};
I have created the nav structure given below in which under the main item children are added.
export const CourseMenu = ( book: Book ): CoursesNav[] => {
if( course ){
// Chapters from book
return book.chapters.map( c => {
return {
id: c.id,
name: c.name,
link: `/my-book/${course.id}/chapter/${c.id}/edit`,
parentId: c.id,
// get topics and put under children
children: c.topics.map( m => {
return {
id: m.id,
name: m.name,
link: `/my-book/${course.id}/chapter/${c.id}/topics/${m.id}/edit`,
parentId: c.id,
//get blocks and put under blocks
children: m.blocks.map( b => {
return {
id: b.id,
name: b.title,
link: `/my-book/${course.id}/chapter/${c.id}/topics/${m.id}/block/${b.id}/edit`,
parentId: m.id
}
})
}
})
}
});
}
}
Nav logic is added just to show you that what I am trying to create.
Every parent has button for toggle to show the sub items. If any sub item has children again the toggle will displayed.
Only I have the problem in opening and closing the correct toggle. Right now under a main children if I open its all child previous one are not closing because they are getting the parentId in toggledMenu state which is a numeric array.
yow broh, why don't you use aria. don't search for the parent of no body.
The user clicks your button/link whatever the user clicks, but you need to set an aria attribute call aria-controls in which you need to put the value of which eva you want to hide/show.
start by closing any open menu or so, then wait 150 milliseconds or so. just to prevent that your loop closes the one that needs to be opened
function App () {
/**
* togging menus
*/
function onClickHandler (e) {
e.preventDefault();
const btnPressed = e.target;
// first you need to check if there is a item toggled and stuff.
const openMenus = document.querySelectorAll("[aria-expanded]");
if (openMenus.length) {
Object.keys(openMenus).forEach(button => {
const toggler = openMenus[button];
const menuId = toggler.getAttribute("aria-controls");
const menuToClose = document.getElementById(menuId);
if (menuToClose && menuToClose !== null) {
menuToClose.classList.remove("show");
menuToClose.setAttribute("aria-hidden", "true");
menuToClose.classList.add("hidden");
// now the button
toggler.setAttribute("aria-expanded", "false");
}
})
}
//wait a minute to prevent conflicts
setTimeout(()=>{
const toOpen = document.getElementById(btnPressed.getAttribute("aria-controls"));
if (!toOpen) {return;}
btnPressed.setAttribute("aria-expanded", "true");
toOpen.classList.add("show");
toOpen.setAttribute("aria-hidden", "false");
toOpen.classList.remove("hidden");
},[150])
}
return (
<div className="accordion-example">
<ul aria-label="Accordion Control Group Buttons" className="accordion-controls">
<li>
<button onClick={onClickHandler} aria-controls="content-1" aria-expanded="false" id="accordion-control-1">Apples</button>
<div className="menu hidden" aria-hidden="true" id="content-1">
<p>Apples are a fine fruit often associated with good health, and fewer doctor's appointments.</p>
<p>Example. An apple a day keeps the doctor away.</p>
</div>
</li>
<li>
<button onClick={onClickHandler} aria-controls="content-2" aria-expanded="false" id="accordion-control-2">Lemons</button>
<div className="menu hidden" aria-hidden="true" id="content-2">
<p>Lemons are good with almost anything, yet are often have a negative connotation when used in conversation.</p>
<p>Example. The bread from the french bakery is normally very good, but the one we bought today was a lemon.</p>
</div>
</li>
<li>
<button onClick={onClickHandler} aria-controls="content-3" aria-expanded="false" id="accordion-control-3">Kiwis</button>
<div className="menu hidden" aria-hidden="true" id="content-3">
<p>Kiwis are a fun, under-appreciated fruit.</p>
</div>
</li>
</ul>
</div>)
}
ReactDOM.render( < App / > , document.getElementById("page"));
.hidden {
display: none;
}
.show {
display: blocK
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="page"></div>
I am guessing you have a 3 level hierarchy inside a Book -> chapter, topic & block. On click of (let's say) right button, you want that specific item's children to be toggled. This is evidently at chapter and topic level.
One solution is maintain the entire hierarchy which you shared (book.chapters) as a state, and have a showChildren flag at each level.
return book.chapters.map( c => {
return {
id: c.id,
name: c.name,
link: `/my-book/${course.id}/chapter/${c.id}/edit`,
parentId: c.id,
showChildren: false,
// get topics and put under children
children: c.topics.map( m => {
return {
id: m.id,
name: m.name,
link: `/my-book/${course.id}/chapter/${c.id}/topics/${m.id}/edit`,
parentId: c.id,
showChildren: false,
//get blocks and put under blocks
children: m.blocks.map( b => {
return {
id: b.id,
name: b.title,
link: `/my-book/${course.id}/chapter/${c.id}/topics/${m.id}/block/${b.id}/edit`,
parentId: m.id
}
})
}
})
}
});
}
Based on the click on the right arrow, toggle that particular state.
With the above approach, you can keep multiple levels in the nav hierarchy open to toggle.

How to change object's properties without having infinite loop in Vue JS watcher?

I have an an Array of Objects like this :
let tree = [
{
task: "Some Task",
spentTime : 2,
subTasks: {
task: "Some Sub Task",
spentTime: 1,
subTasks:{
task:"Some sub sub task",
spentTime:30
}
}
}
]
As you can see here i have this type of tree structure and i am displaying that some kind of nested accordion. So every node has a input box which has 2 way binding with spentTime property ( using v-model ).
Now if i type in any of the node's input. i need to do some operation on these spentTime values and re-populate Or insert different values in the same object.
Here i was thinking doing deep watch. But i think this will cause infinite loop because i am changing the same object and assigning value back and it triggers watch again :)
What i can do if i want to trigger a function on input change and put different values back in the same object.
Thanks!
I had similar reactivity issues with Vue.js
Try to use Vue.set or this.$set to save any changes to your array :
this.$set(this.someObject, 'b', 2)
You can read more about Vue set here.
You can read more information about Vue reactivity here.
tl;dr
The clean solution, based on #djiss suggestion, and which correctly bubbles up to top parent, using $set and watch, is here:
https://codesandbox.io/s/peaceful-kilby-yqy9v
What's below is the initial answer/logic, which uses $emit and the task 'key' to move the update in the parent.
In Vue you can't modify the child directly. I mean, you can, but you shouldn't. When you do it, Vue warns you about it informing you the change you just made will be overridden as soon as the parent changes.
The only options are to use state to manage the single source of trouth for your app (Vuex or a simple Vue object), or you call the parent telling it: "Change this particular child with this particular value". And you simply listen to changes coming from parent.
Which is what I did here:
const task = {
task: "Some Task",
spentTime: 2,
subTasks: [{
task: "Some Sub Task",
spentTime: 1,
subTasks: [{
task: "Some sub sub task",
spentTime: 30
}, {
task: "Some other sub sub task",
spentTime: 12
}]
}]
};
Vue.config.productionTip = false;
Vue.config.devtools = false;
Vue.component('Task', {
template: `
<div>
<h2>{{task.task}} ({{spentTime}})</h2>
<div v-if="hasTasks">
<Task v-for="(t, k) in task.subTasks" :key="k" :task="t" #fromChild="fromChild" :tid="k"/>
</div>
<input v-else v-model="localTime" type="number" #input="updateParent(localTime)">
</div>
`,
props: {
task: {
type: Object,
required: true
},
tid: {
type: Number,
default: 0
}
},
data: () => ({
localTime: 0
}),
mounted() {
this.updateParent(this.spentTime);
},
computed: {
spentTime() {
return this.hasTasks ? this.subtasksTotal : this.task.spentTime;
},
subtasksTotal() {
return this.task.subTasks.map(t => t.spentTime).reduce(this.sum, 0)
},
hasTasks() {
return !!(this.task.subTasks && this.task.subTasks.length);
}
},
methods: {
fromChild(time, task) {
this.task.subTasks[task].spentTime = time;
this.updateParent(this.spentTime);
},
updateParent(time) {
this.$emit("fromChild", Number(time), this.tid);
this.localTime = this.spentTime;
},
sum: (a, b) => a + b
},
watch: {
"task.spentTime": function() {
this.localTime = this.task.spentTime;
}
}
});
new Vue({
el: "#app",
data: () => ({
task
}),
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<Task :task="task" :tid="0" />
</div>
It will consume any tree you throw at it, provided it has the same structure. The logic is: show the input if no subtasks or calculate from subtasks otherwise.
Obviously, you can change that to fit your needs.
I ran into this headache before, and I did solve/ cheat on it with deep watch and Lodash _.cloneDeep and _.isEqual.
Inside your child component, create your own data componentTask. You will watch your componentTask and your prop. Every time they change, compare them using _.isEqual. When componentTask changes, emit an event to its parent.
SubTask:
<template>
<div>
<input type="text" v-model="componentTask.task">
<input type="number" min="0" v-model.number="componentTask.spentTime">
<SubTask v-if="task.subTasks" #task-change="handleTaskChange" :task="task.subTasks" />
</div>
</template>
<script lang="ts">
import {Vue, Component, Prop, Watch} from 'vue-property-decorator'
import {Task} from "#/components/Test/Test";
import _ from "lodash";
#Component
export default class SubTask extends Vue {
#Prop() task!: Task;
componentTask: Task | undefined = this.task;
#Watch('task', {deep: true, immediate: true})
onTaskChange(val: Task, oldVal: Task) {
if (_.isEqual(this.componentTask, val))
return;
this.componentTask = _.cloneDeep(val);
}
#Watch('componentTask', {deep: true, immediate: true})
onComponentTaskChange(val: Task, oldVal: Task) {
if (_.isEqual(val, this.task))
return;
this.$emit("task-change");
}
handleTaskChange(subTasks: Task){
this.componentTask = subTasks;
}
}
</script>
Parent class:
<template>
<div style="margin-top: 400px">
<h1>Parent Task</h1>
<br>
<div style="display: flex;">
<div style="width: 200px">
<h4>task</h4>
<p>{{task.task}}</p>
<p>{{task.spentTime}}</p>
<br>
</div>
<div style="width: 200px">
<h4>task.subTasks</h4>
<p>{{task.subTasks.task}}</p>
<p>{{task.subTasks.spentTime}}</p>
<br>
</div>
<div style="width: 200px">
<h4>task.subTasks.subTasks</h4>
<p>{{task.subTasks.subTasks.task}}</p>
<p>{{task.subTasks.subTasks.spentTime}}</p>
<br>
</div>
</div>
<SubTask :task="task" #task-change="handleTaskChange"/>
</div>
</template>
<script lang="ts">
import {Vue, Component, Prop} from 'vue-property-decorator'
import SubTask from "#/components/Test/SubTask.vue";
import {defaultTask, Task} from "#/components/Test/Test";
#Component({
components: {SubTask}
})
export default class Test extends Vue {
task: Task = defaultTask;
handleTaskChange(task: Task) {
this.task = task;
}
}
</script>
Defined interface:
export interface Task {
task: string;
spentTime: number;
subTasks?: Task;
}
export const defaultTask: Task = {
task: "Some Task",
spentTime : 2,
subTasks: {
task: "Some Sub Task",
spentTime: 1,
subTasks:{
task:"Some sub sub task",
spentTime:30
}
}
};

How to capture/console the value of checked/uncheked checkbox into an array of object in angular 7

I have some checkboxes with parent-child structure whose values are coming from loop.Here when I click submit button I need to capture the selected/unselected value into below format(mentioned as commented output). When I click submit for preselected value is working fine, but if I remove checked from html(unselected on page load) and click submit that time it shows empty array.As per my requirement in project sometimes all checkboxes will be preselected,some times few selected/few unselected and some times all will be unselected based on condition and I need to capture selected/unselected value(same as output) on submit. Here is the code below
Demo - https://stackblitz.com/edit/angular-ar5apb?file=src%2Fapp%2Fapp.component.html
app.component.html
Checkbox -
<div class="col-md-3" id="leftNavBar">
<ul *ngFor="let item of nestedjson">
<li class="parentNav">{{item.name}}</li>
<li class="childData">
<ul>
<li *ngFor="let child of item.value; let i = index">{{child}}<span class="pull-right"><input checked type="checkbox" (change)="item.checked[i] = !item.checked[i]" ></span></li>
</ul>
</li>
</ul>
<div><button type="submit" (click)="getit()">submit</button></div>
</div>
app.component.ts
import { Component, OnInit } from "#angular/core";
#Component({
selector: "my-app",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent implements OnInit {
data: any;
nestedjson: any;
message = "";
test: any;
constructor() {}
ngOnInit() {
this.nestedjson = [
{ name: "parent1", value: ["child11", "child12"] },
{ name: "parent2", value: ["child2"] },
{ name: "parent3", value: ["child3"] }
];
this.nestedjson.forEach(
v => (v.checked = Array(v.value.length).fill(true))
);
}
getit() {
var duplicatePushArray = [];
this.nestedjson.forEach(item => {
let checked = [];
item.checked.forEach((isCkecked, i) => {
if (isCkecked) {
checked.push(item.value[i]);
}
});
if (checked.length > 0) {
duplicatePushArray.push({
name: item.name,
value: checked
});
}
});
console.log("Final Array: ", duplicatePushArray);
/* output: [{"name":"parent1","value":["child11","child12"]},{"name":"parent2","value":["child2"]},{"name":"parent3","value":["child3"]}]*/
}
}
<input type="checkbox" [checked]="item.checked[i]" (change)="item.checked[i] = $event.target.checked">
or try template forms
<input type="checkbox" [name]="'checked_' + i" [(ngModel)]="item.checked[i]">
Make sure to import the forms module
Here is a reduce function that starts with an empty array, filters the values based on the checked array and the pushes a new object into the results if there are any checked ones with the checked results.
getit() {
var duplicatePushArray = this.nestedjson.reduce((results, item) => {
const checked = item.value.filter((value, index) => item.checked[index]);
if (checked.length) {
results.push({ name: item.name, value: checked });
}
return results;
}, []);
console.log("Final Array: ", duplicatePushArray);
}
Here is an update to your StackBlitz https://stackblitz.com/edit/angular-aasetj?file=src/app/app.component.ts

unable to set the radio button value selected on button click in list group using angular

Here the requirement is I am displaying the radio button as list group in each list we have 2 buttons with click functionality and whenever the user clicks on that button, it should automatically select the radio button too.
Now I am able to place button and radio button selected button I m not able to select the radio button when the respective sub button is clicked, below is code and Stackblitz
<div class="text-center mt-5">
<h4>Selected value is {{radioSel.name}}</h4>
<div>
<ul class="list-group">
<li class="list-group-item" *ngFor="let item of data">
<input type="radio" [(ngModel)]="radioSelected" name="list_name" value="{{item.value}}" (change)="onItemChange(item)" /> {{item.name}}
<span (click)="one(item.name,'A')">A</span>
<span (click)="two(item.name,'B')">B</span>
</li>
</ul>
</div>
<h5>{{radioSelectedString}}</h5>
</div>
TS code
radioSel:any;
radioSelected:string;
radioSelectedString:string;
public data = [
{
name:'Item 1',
value:'item_1'
},
{
name:'Item 2',
value:'item_2'
},
{
name:'Item 3',
value:'item_3'
},
{
name:'Item 4',
value:'item_4'
},
{
name:'Item 5',
value:'item_5'
}
];
constructor() {
this.radioSelected = "item_3";
this.getSelecteditem();
}
getSelecteditem(){
this.radioSel = this.data.find(Item => Item.value === this.radioSelected);
this.radioSelectedString = JSON.stringify(this.radioSel);
}
onItemChange(item){
this.getSelecteditem();
}
one(data,data1,data2){
console.log(data,data1,data2);
}
two(data,data1,data2){
console.log(data,data1,data2);
this.radioSelected = data;
this.data.find(item => item.value === this.radioSelected);
}
Stackblitz URL::--> https://stackblitz.com/edit/angular-e7utfs and if I click on the other external button I need to get the radio button values
I have tried to solve this with the following approach.
Assigned a dynamic id to each radio button id="{{ item.name }}".
A method onButtonClick(item, i) is fired on every button click. Inside the method, I am setting the input element checked flag to true
HTML
<div class="text-center mt-5">
<h4>Selected value is {{radioSel.name}}</h4>
<div>
<ul class="list-group">
<li class="list-group-item" *ngFor="let item of data; let i = index">
<input type="radio" id="{{ item.name }}" [(ngModel)]="radioSelected" name="list_name" value="{{item.value}}" (change)="onItemChange(item)" />
{{item.name}}
<button (click)="onButtonClick(item)">A</button>
<button (click)="two(item.name,'B')">B</button>
</li>
</ul>
</div>
<h5>{{radioSelectedString}}</h5>
</div>
TS Code
import { Component } from '#angular/core';
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: [ './app.component.css' ]
})
export class AppComponent {
radioSel:any;
radioSelected:string;
radioSelectedString:string;
public data = [
{
name:'Item 1',
value:'item_1'
},
{
name:'Item 2',
value:'item_2'
},
{
name:'Item 3',
value:'item_3'
},
{
name:'Item 4',
value:'item_4'
},
{
name:'Item 5',
value:'item_5'
}
];
constructor() {
this.radioSelected = "item_3";
this.getSelecteditem();
}
getSelecteditem(){
this.radioSel = this.data.find(Item => Item.value === this.radioSelected);
this.radioSelectedString = JSON.stringify(this.radioSel);
}
onItemChange(item){
this.getSelecteditem();
}
one(data,data1){
console.log(data,data1);
}
two(data,data1){
console.log(data,data1);
this.radioSelected = data;
}
onButtonClick(data) {
const el = document.getElementById(data.name) as HTMLInputElement;
el.checked = true;
}
}
In the given code, you are setting a value with item.name when button B is clicked.
It has to be item.value instead of item.name since input tag is bound with item.value.
HTML:
<span (click)="two(item.value,'B')">B</span>
TS:
two(data, data1) {
console.log(data, data1);
this.radioSelected = data;
}

Avoid two of the same componets alter each other Angular

I have a component for Tabs, it has its own variables and it works really good, but the thing is that if i place again other tab in the same page, when i change the value of the selected tab for one, it changes the other tab component also.
This is my tab component:
#Component({
selector: 'sys-tab',
styleUrls: ['./shared/sys.css'],
template: `
<div class="tabs">
<div *ngFor="let tab of tabs; let i = index" (click)="selectTab(tab)">
<input id="tab-{{i+1}}" type="radio" name="radio-set" class="tab-selector-{{i+1}}" [checked]="i===0"/>
<label for="tab-{{i+1}}" class="tab-label-{{i+1}}">{{tab.title}}</label>
</div>
<div class="content">
<ng-content></ng-content>
</div>
</div>
`,
})
export class TabView {
tabs: TabViewContent[] = [];
addTab(tab: TabViewContent) {
if (this.tabs.length === 0)
tab.active = true;
this.tabs.push(tab);
}
selectTab(tab) {
this.tabs.forEach((tab) => {
tab.active = false;
});
tab.active = true;
}
}
#Component({
selector: 'sys-tab-content',
styleUrls: ['./shared/sys.css'],
template: `
<div class="content-2" [hidden]="!active">
<ng-content></ng-content>
</div>
`
})
export class TabViewContent {
active: boolean;
#Input() title: string;
constructor(tabs: TabView) {
tabs.addTab(this);
}
}
It works really fine if i use it this way:
<sys-tab>
<sys-tab-content title="Principal">
Content 1
</sys-tab-content>
<sys-tab-content title="Complementar">
Content 2
</sys-tab-content>
</sys-tab>
but if i do something like this:
<sys-tab>
<sys-tab-content title="Principal">
Content 1
</sys-tab-content>
<sys-tab-content title="Complementar">
Content 2
</sys-tab-content>
</sys-tab>
<sys-tab>
<sys-tab-content title="Principal">
Content 3
</sys-tab-content>
<sys-tab-content title="Complementar">
Content 4
</sys-tab-content>
</sys-tab>
When i change the value of the first component, it also change the value of the second and viceversa.
You should specify different name for each of input[radio] group:
name="{{id}}-radio-set"
and unique id and for attribute for all controls.
So here is how it could be done:
let nextId = 0;
#Component({
selector: 'sys-tab',
template: `
...
<input id="{{id}}-tab-{{i+1}}" ... name="{{id}}-radio-set" .../>
<label for="{{id}}-tab-{{i+1}}" ...></label>
...
`,
})
export class TabView {
id = `tabview-${nextId++}`;
Plunker Example

Categories

Resources