Vuejs html checkbox check state not updated correctly - javascript

Having issues getting some checkboxes to work properly. So in my component I have an array of objects set in a state variable tokenPermissions that look like this
tokenPermissions: [
{
groupName: "App",
allSelected: false,
someSelected: false,
summary: "Full access to all project operations",
permissions: [
{
name: "can_create_app",
summary: "Create new projects",
selected: false,
},
{
name: "can_delete_app",
summary: "Delete existing projects",
selected: false,
},
{
name: "can_edit_app",
summary: "Edit an existing project",
selected: false,
},
],
}
],
The goal is to loop through this array and have a parent and children checkboxes like so tokenPermissions[i].allSelected bound to the parent checkbox and for each object in tokenPermissions[i].permissions a corresponding checkbox bound to the selected property like so tokenPermissions[i].permissions[j].selected.
Desired behaviour when the parent checkbox is selected,
If all child checkboxes checked, uncheck all, including the parent
If child checkboxes are unchecked check all including the parent
if only some of the child checkboxes are selected, the parent would show the indeterminate - icon or sign and on click, uncheck all child checkboxes including the parent.
The issue is point 3. The issue is sometimes the parent checkbox is not correctly checked based on the state of the attribute bounded to. For example allSelected can be false but the parent checkbox is checked.
I put a complete working example on github here https://github.com/oaks-view/vuejs-checkbox-issue.
The bit of code with the binding is as follows
<ul
class="list-group"
v-for="(permissionGroup, permissionGroupIndex) in tokenPermissions"
:key="`${permissionGroup.groupName}_${permissionGroupIndex}`"
>
<li class="list-group-item">
<div class="permission-container">
<div>
<input
type="checkbox"
:indeterminate.prop="
!permissionGroup.allSelected && permissionGroup.someSelected
"
v-model="permissionGroup.allSelected"
:id="permissionGroup.groupName"
v-on:change="
permissionGroupCheckboxChanged($event, permissionGroupIndex)
"
/>
<label :for="permissionGroup.groupName" class="cursor-pointer"
>{{ permissionGroup.groupName }} -
<span style="color: red; margin-left: 14px; padding-right: 3px">{{
permissionGroup.allSelected
}}</span></label
>
</div>
<div class="permission-summary">
{{ permissionGroup.summary }}
</div>
</div>
<ul class="list-group">
<li
class="list-group-item list-group-item-no-margin"
v-for="(permission, permissionIndex) in permissionGroup.permissions"
:key="`${permissionGroup.groupName}_${permission.name}_${permissionIndex}`"
>
<div class="permission-container">
<div>
<input
type="checkbox"
:id="permission.name"
v-bind:checked="permission.selected"
v-on:change="
permissionGroupCheckboxChanged(
$event,
permissionGroupIndex,
permissionIndex
)
"
/>
<label :for="permission.name" class="cursor-pointer"
>{{ permission.name
}}<span
style="color: red; margin-left: 3px; padding-right: 3px"
> {{ permission.selected }}</span
></label
>
</div>
<div class="permission-summary">
{{ permission.summary }}
</div>
</div>
</li>
</ul>
</li>
</ul>
And for updating the checkbox
getPermissionGroupSelectionStatus: function (permissionGroup) {
let allSelected = true;
let someSelected = false;
permissionGroup.permissions.forEach((permission) => {
if (permission.selected === false) {
allSelected = false;
}
if (permission.selected === true) {
someSelected = true;
}
});
return { allSelected, someSelected };
},
permissionGroupCheckboxChanged: function (
$event,
permissionGroupIndex,
permissionIndex
) {
const { checked } = $event.target;
// its single permission selected
if (permissionIndex !== undefined) {
this.tokenPermissions[permissionGroupIndex].permissions[
permissionIndex
].selected = checked;
const { allSelected, someSelected } =
this.getPermissionGroupSelectionStatus(
this.tokenPermissions[permissionGroupIndex]
);
this.tokenPermissions[permissionGroupIndex].allSelected = allSelected;
this.tokenPermissions[permissionGroupIndex].someSelected = someSelected;
} else {
// its selectAll check box
const { allSelected, someSelected } =
this.getPermissionGroupSelectionStatus(
this.tokenPermissions[permissionGroupIndex]
);
let checkAll;
// no checkbox / permission is selected then set all
if (!someSelected && !allSelected) {
checkAll = true;
} else {
checkAll = false;
}
this.tokenPermissions[permissionGroupIndex].allSelected = checkAll;
this.tokenPermissions[permissionGroupIndex].someSelected = checkAll;
for (
let i = 0;
i < this.tokenPermissions[permissionGroupIndex].permissions.length;
i++
) {
this.tokenPermissions[permissionGroupIndex].permissions[i].selected =
checkAll;
}
}
},

It's a rendering problem.
Vue set the allSelected checkbox as checked, then in the same cycle updates it to false; you can read about Vue life cycle here: https://it.vuejs.org/v2/guide/instance.html
A pretty brutal (but simple) way to resolve it (which I don't recommend, but it's useful to understand what's happening) is to delay the update.
Wrap the last part of the method permissionGroupCheckboxChanged with a this.$nextTick:
this.$nextTick(() => {
this.tokenPermissions[permissionGroupIndex].allSelected = checkAll;
this.tokenPermissions[permissionGroupIndex].someSelected = checkAll;
for (
let i = 0;
i < this.tokenPermissions[permissionGroupIndex].permissions.length;
i++
) {
this.tokenPermissions[permissionGroupIndex].permissions[i].selected =
checkAll;
}
})
This way when you change the values, the engine reacts accordingly.
Still I don't recommend it (I think nextTick is useful to understand the Vue life cycle, but I would recommend against using it whenever is possible).
A less brutal (and simpler) way is to set the allSelected to null instead of false when checkAll is not true permissionGroupCheckboxChanged:
// this
this.tokenPermissions[permissionGroupIndex].allSelected = checkAll ? checkAll : null;
// instead of this
this.tokenPermissions[permissionGroupIndex].allSelected = checkAll;
this way the prop wins against the model (as the model value becomes null).
But the even better option (imho) would be to use a component of its own inside the v-for loop and have allSelected and someSelected as computed properties instead of values bound to real variables.
Usually you should not store ui status as data when it can be inferred from real data (I may be wrong, as I don't know your application, but in your case I suspect you are interested in the single checkboxes' values, while allSelected/someSelected are merely used for ui).

Related

How to run a function to set height on all HTML tags of a particular type?

I have some conditional renders of textarea elements in many places on a form that shows/hides these elements depending on what the user is doing. For example:
<li v-if="Form.Type === 1">
<textarea v-model="Form.Title" ref="RefTitle"></textarea>
</li>
There could be any number of textarea elements like above. What I need to do is resize these elements at certain points in the lifecycle (e.g. onMounted, onUpdated).
The function that gets triggered to do this is:
setup() {
...
const RefTitle = ref(); // This is the ref element in the template
function autosizeTextarea() {
RefTitle.value.style.height = "35px"; // Default if value is empty
RefTitle.value.style.height = `${RefTitle.value.scrollHeight}px`;
}
...
}
In the code above I am specifically targeting a known textarea by its ref value of RefTitle. I could test for its existence using an if(RefTitle.value) statement.
But how could I get all the textarea elements that may be rendered and then run autosizeTextarea on all of them?
I can get all the textarea elements like such:
setup() {
...
function autosizeTextarea() {
const AllTextareas = document.getElementsByTagName('TEXTAREA');
for (let i=0; i < AllTextareas.length; i++) {
// How can I set the style.height = `${RefTitle.value.scrollHeight}px`;
// in here for each element?
}
}
...
}
But how can style.height be set on all of them?
You could create your own custom component representing a textarea with the functionality in the component itself, so you don't have to get all textareas which are dynamically created.
It could look something like this:
<template>
<textarea :value="modelValue" #input="$emit('update:modelValue', $event.target.value)" ref="textarea" :style="styleObject"></textarea>
</template>
<script>
export default {
emits: {
'update:modelValue': null,
},
props: {
modelValue: {
type: String,
},
// Prop for dynamic styling
defaultHeight: {
type: Number,
required: false,
default: 35,
validator(val) {
// Custom Validator to ensure that there are no crazy values
return val > 10 && val < 100;
}
},
computed: {
styleObject() {
return {
height: this.$refs['textarea'].value.scrollHeight ? `${this.$refs['textarea'].value.scrollHeight}px` : `${this.defaultHeight}px`,
}
},
</script>
That way you can even use v-model on it.
<li v-if="Form.Type === 1">
<custom-textarea v-model="Form.Title" :defaultHeight="45"></textarea>
</li>
The Template I provided is just to show you how a custom component could look like. You might have to fit it into your logic depending on when you actually want things to change/trigger.
I have managed to do it like this:
const AllTextareas = ref(document.getElementsByTagName("TEXTAREA")); //returns an object not an array
for (const [key, value] of Object.entries(AllTextareas.value)) {
AllTextareas.value[key].style.height = AllTextareas.value[key].scrollHeight ? `${AllTextareas.value[key].scrollHeight}px` : "35px";
}

Work out whether custom radio component is checked or not

I have a custom radio component in React, when I check and uncheck the values it adds items to an object and should have true or false based on whether they are checked.
At the moment it adds the true value correctly with the name of the radio but I can't seem to find out how to work to make the option false if another option is chosen.
I am currently using
constructor() {
super();
this.state = {
time_frame: {},
}
this.handleRadioChange = this.handleRadioChange.bind(this);
}
handleRadioChange(event) {
let name = event.target.name
let timeFrameCopy = this.state.time_frame;
console.log(event.target)
timeFrameCopy[event.target.value] = true
this.setState({[name]: timeFrameCopy,}, this.checkState)
return
}
}
checkState(event) {
console.log(this.state)
}
My radio component is
const Radio = (props) => {
return (
<Col>
<div>
<input id={props.value} type="radio" name={props.name} value={props.value} className="visually-hidden" onChange={props.handleChange}/>
<label htmlFor={props.value} className="switch-label checkbox-label text-center">{props.label}</label>
</div>
</Col>
)
}
export default Radio
If I check one radio button and then the other my state still has the data:
time_frame: {single: true, recurring: true}
Even though I would expect one of them to be false
If I understand correctly, you're trying to store in the state an object called time_frame, which is going to contain one pair of property-value per radio input, where the name of each of them would be the property name and the checked status the value. If that's the case, I see a logic problem. since you're hard-coding true (for what I understand from your code) always instead of looking for the value stored and toggling/flipping it.
handleRadioChange() function should be something like:
handleRadioChange(event) {
let name = event.target.name;
this.setState((currentState)=>{
let timeFrameCopy = currentState.time_frame;
timeFrameCopy[name] = event.target.checked;
return { "time_frame": timeFrameCopy };
});
}

document.getElementById retuning is not updating dynamically in vue.js returning null every time

js
I have a method to enable and disable print option which has element id printReport_AId, so the existence of element is dynamic based on selection.
I have a code to get
document.getElementById('printReport_AId')
this returning null everytime , i guess we need something like windows onload or interval method , not sure how to implement in vue.js
I have attached the code below
<template>
<div id = "intro" style = "text-align:center;">
<div class="printIconSection" v-for="report in reports" :key="report.property" >
<div class="Icon" id="printReport_AId" v-if="isOverview">
<font-awesome-icon :icon="report.icon" #click="printWindow()"/>
</div>
</div>
<div class="logo"v-bind:class="{ 'non-print-css' : noprint }">
</div>
</div>
</template>
<script type = "text/javascript">
var vue_det = new Vue({
el: '#intro',
data: {
timestamp: ''
},
created() {
},
methods: {
printWindow() { },
mounted() {
// window.addEventListener('print', this.noprint);
},
computed(){
noprint() {
const printicon = document.getElementById('printReport_AId');
if (printicon != 'null') {
return true;
}
return false;
},
},
}
}
});
</script>
<style>
#media print {
.non-print-css {
display: none;
}
}
</style>
i just tried window.addEventListener that didnt worked for computed .
I need to get the element id dynamically.
whenever i enable the print element , the element id should not be null. similarly,
whenever i dont enable the print element , the element id should be null
From your code, it seems that you're inserting the ID in each iteration of the v-for loop. This means that the ID will not be unique in the event that there is more than one entry in your reports array. Based on your code, this is what I understand that you want to achieve:
each report will contain an isOverview property. It is probably a boolean, so it has a value of true or false
when isOverview is true, the print icon will be shown
when isOverview is false, the print icon will not be shown
based on whether the print icon(s) are shown, you want to toggle the .logo class
This comes the tricky part, as you have multiple isOverview to evaluate. Do you want to toggle the .logo element when:
all reports has isOverview property set to true, or
one or more reports has isOverview property set to true?
If you want to show the .logo element when all reports has isOverview set to true, then you can do this:
computed() {
noprint() {
return this.reports.every(report => report.isOverview);
}
}
Otherwise, if you only want one or more report to have isOverview as true, you can do this instead:
computed() {
noprint() {
return this.reports.some(report => report.isOverview);
}
}
computed would not watch for document methods, because it is not wrapped with vue's setters and getters.
What you can do is mark your div with ref and then access it via this.$refs
<div class="printIconSection" v-for="(report, index) in reports" :key="report.property" >
<div class="Icon" :ref="'printReport_AId' + index" v-if="isOverview">
<font-awesome-icon :icon="report.icon" #click="printWindow()"/>
</div>
</div>
methods(){
noprint(index) {
const printicon = this.$refs['printReport_AId' + index];
if (printicon != null) {
return true;
}
return false;
},
}
When you need to achieve noprint you just call noprint(index)
For each element of array noprint would be different

vuejs Computed data detecting if model has been updated

I'm trying to detect if a Model Data has not been changed within a Vue Computed data.
I have two sets of Variables that need to be checked,
before Computed:filteredItems should return a new list or current list.
Below are two data i'm checking
text ( the text input )
selectedInput ( currently selected item )
Current Behavior:
I've changed, selectedInput to null, this updates Computed:filteredList to be triggered. which is expected.
The first Condition is to make sure that this update returns current list if text === selectedInput.text, work as expected
However, I need a second condition to detect if text has not been changed.
<input v-model="text" />
<ul>
<li v-for="item in filteredItems" #click="text=item.text"></li>
</ul>
{
data():{
text: 1,
items: [],
tempList: [],
selectedItem: {text: 1}
},
computed: {
filteredItems(){
// when selectedItem.text === current text input, do not run
if (this.selectedItem.text === text) return this.tempList;
// how do i detect if selectedItem.text has not been changed
if (this.selectedItem.text.hasNotChange??) return this.tempList;
}
}
}
Data Flow: 1update the text > 2filter list > 3click on listItem, update (1) text
[input(text): update on type ] >
[li(filteredItem): filter list on type by value (text) and (selectedInput.text) ] >
[li(item)#click: update (1), and also another value(selectedInput.text) input(text) to equal (item.text) ]
This cycle works until I have action somewhere else that updates selectedInput.text
is there something i can do with a setter/getter for the Text model.
Create a variable, changed. Watch selectedItem.text, and set changed to true. In a watcher on text, set changed to false.
I got this to work using a temp variable
data(){
return: {
text: "",
temp: {
text
}
}
}
computed(){
filteredList(){
var temporaryList,originalList,filteredList
if ((this.text === $store.state.selectedText )||
(this.text === this.temp.text ) ) {
return temporaryList || originalList
}
// update
this.temp.text = this.text
return filteredList
}
}
thought it would be a bad practice to update variables within a Computed method.

How to disable all parent and childs if one parent is selected?

Here in first condition i was able to disbale all parents except current parent that is selected.
checkbox.js
if (geoLocation.id === 5657){
var getParent = geoLocation.parent();
$.each(geoLocation.parent(),function(index,location) {
if (location.id !== geoLocation.id) {
var disableItemId = 'disabled' + location.id;
// Get **strong text**the model
var model = $parse(disableItemId);
// Assigns a value to it
model.assign($scope, true);
}
}
);
At this point i am trying to disbale all the child for the parents that are disabled in above condition. How to achieve that task with below code any help will be appreciated.
So far tried code...
$.each(geoLocation.parent().items,function(index,location) {
if(location.id !== geoLocation.id){
var disableItemId = 'disabled' + location.children.data;
// Get the model
var model = $parse(disableItemId);
// Assigns a value to it
model.assign($scope, true);
}
});
console.log(getParent);
}
If you plan to use angular, better express all of this in a model structure, and use ng-click and ng-disabled to achieve what you need.
Template:
<ul>
<li ng-repeat="item in checkboxes">
<input type="checkbox" ng-disabled="item.disabled" ng-click="disableOthers(item)"> {{item.name}}
</li>
</ul>
Controller:
$scope.disableOthers = function (item) {
// iterate over parents and childs and mark disabled to true
};

Categories