setting one line item 'active' at a time in vue - javascript

I have a small unordered list in a vue template:
<ul style="border-bottom:none !important; text-decoration:none">
<li class="commentToggle" v-bind:class="{active:commentActive}" v-on:click="setInputName('new')">New Comment</li>
<li class="commentToggle" v-bind:class="{active:commentActive}" v-on:click="setInputName('note')">Note</li>
</ul>
and I then have a data variable for commentActive and a function I call to set the input name:
data () {
return {
commentActive: false,
}
},
methods: {
setInputName(str) {
this.inputName = str;
this.commentActive = true;
},
}
And this is functional but it is obviously setting BOTH inputs to active when I click on one of them. How do I alter this to only set the clicked line item active?

You need to use a unique identifier to determine which comment is active. In the most rudimentary way using your current setup:
<li class="commentToggle"
v-bind:class="{active:commentActive === 'new'}"
v-on:click="setInputName('new')">
New Comment
</li>
<li class="commentToggle"
v-bind:class="{active:commentActive === 'note'}"
v-on:click="setInputName('note')">
Note
</li>
setInputName(str) {
this.inputName = str;
this.commentActive = str;
},
The adjustment that we made is to ensure that the commentActive matches the str that we've used. You can change that identifier to anything else, and pass additional arguments if you so choose.

Related

Vuejs html checkbox check state not updated correctly

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).

Including dynamically created sub-pages in JavaScript "isActive()"

I have a Bootstrap implementation where the navigation bar is an unordered list with each items set to highlight when active. The highlight itself is styled using CSS but the check for active status is made using AngularJS:
<ul class="nav navbar-nav navbar-right">
<li ng-class="{ active: isActive('/blog')}">
<a onClick="pagetop()" href="#blog">BLOG</a>
</li>
</ul>
The isActive() method is defined in an AngularJS Controller as:
function HeaderController($scope, $location){
$scope.isActive = function (viewLocation) {
return viewLocation === $location.path();
};
}
As you can see, AngularJS simply adds an .active class to the <li> item if the linked URL is active. This is the class that is styled using CSS. This works fine when the currently open page is http://localhost:8888/a-s/#/blog/ but not when it's http://localhost:8888/a-s/#/blog/sub-page/ or http://localhost:8888/a-s/#/blog/sub-page/sub-sub-page/. How can I modify the code to ensure all paths under /blog trigger the .active class logic? Is there any way one could use wild-cards in this syntax?
Now you checking whether path and the passed value are equal, instead you can check whether the passed value exists in the path
function HeaderController($scope, $location) {
$scope.isActive = function(viewLocation) {
return $location.path().indexOf(viewLocation) > -1;
};
}
This is not clean solution. Because it work on http://localhost:8888/a-s/#/blog-another/ b or http://localhost:8888/a-s/#/blog_edit/ b and so on. It is need to update like this:
function HeaderController($scope, $location) {
$scope.isActive = function(viewLocation) {
var path = $location.path();
var addViewLocation = viewLocation+"/";
var inPath = false;
if(path.indexOf(addViewLocation)==-1)
{
inPath = path.indexOf(viewLocation)+viewLocation.length==path.length;
}
else
inPath = true;
return inPath;
};
}
This is test locations:
isActive("www.location.ru/path","/path");
true
isActive("www.location.ru/path/tr","/path");
true
isActive("www.location.ru/path-r/tr","/path");
false
isActive("www.location.ru/path/","/path");
true
isActive("www.location.ru/path/we/ew","/path");
true
isActive("www.location.ru/path-another/we/ew","/path");
false

How to automatically react to DOM changes?

I have a list of items in the DOM:
<ul id="my_list">
<li class="relevant">item 1 <a onclick="remove(1)">Remove</a></li>
<li class="relevant">item 2 <a onclick="remove(2)">Remove</a></li>
...
</ul>
The user can remove items from the list by clicking the respective 'remove' links in the item.
I want to reactively monitor this list for changes, so that when there are no items left it triggers a function. For example:
var num_list_items = $("#my_list li.relevant").length;
if( num_list_items == 0 ){
do_something();
}
My app is being built in Meteor, so would ideally like to know if there is any native functionality that can do this. If not, any other suggestions are welcome.
You can use a MutationObserver(link)
You'll instantiate one with something to do everytime the observed node mutates, if the mutation reports the desired condition you'll do something about it.
var observed = document.getElementById('my_list');
var whatIsObserved = { childList : true };
var mo = new MutationObserver(function(mutations){
mutations.forEach(function(mutation){
if(mutation.type === 'childList'){
//DOM Tree mutated
if(mutated.target.querySelectorAll('li.relevant').length === 0){
//mutated target has no childs.
}
}
});
});
mo.observe(observed, whatIsObserved);
Alternatively, have the remove function trigger your condition.

In Vue.js, change value of specific attribute for all items in a data array

I'm trying to toggle an open class on a list of items in a v-repeat. I only want one list item (the one most recently clicked) to have the class open.
The data being output has a "class" attribute which is a blank string by default. I'm using this to set the class of the list items in the v-repeat like so:
<li v-repeat="dataSet"
v-on="click: toggleFunction(this)"
class="{{ class }}">
{{ itemContent }}
</li>
I'm using v-on="click: toggleFunction(this)" on each item, which lets me change the class for the specific item, but how do I change the class on all the other items?
My current on-click method:
toggleFunction: function(item) {
if (item.class == '') {
// code to remove the `open` class from all other items should go here.
item.class = 'open';
} else {
item.class = '';
}
}
I've tried using a regular jQuery function to strip the classes: that does remove the classes but it doesn't change the item.class attribute, so things get weird once an item gets clicked more than once...
I'm sure there must be a straightforward way to fix this that I'm not seeing, and having to set a class attribute in the data itself feels hacky anyway (but I'll settle for any fix that works).
I just ran into the same issue. I am still learning Vue, but I tackled it using the "v-class" directive. Using v-class, any time an active value is true for a record, it will automatically add the class "open" to the list element. Hope this JSFiddle helps.
<ul>
<li
v-repeat="people"
v-on="click: toggleActive(this)"
v-class="open: active">{{ name }}
</li>
</ul>
new Vue({
el: '#app',
data: {
people: [
{name: 'mike'},
{name: 'joe',active: true},
{name: 'tom'},
{name: 'mary'}
]
},
methods: {
toggleActive: function(person) {
// remove active from all people
this.people.forEach(function(person){
person.$set('active',false);
});
//set active to clicked person
person.$set('active',true);
}
}
});

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