Vue-bootstrap nested checkboxes - vue.js

I want to have sidebar that would represent filter and in there I would have checkboxes that would represent what should be shown to user. I found this: https://bootstrap-vue.js.org/docs/components/form-checkbox/#indeterminate-tri-state-support
But this only goes one level deep and I would need to go two levels. An example of what I am trying to achieve:
Select All
-Breakfest
-Eggs
-Bacon
-Sandwich
-Lunch
-Salad
-Chicken
-Fish
-Dinner
-Pancakes
-Tacos
-Beef
Now I want something like example in documentation from link but that goes one level deeper.

It should be simple to adopt this to Bootstrap Vue since Bootstrap Vue only abstracts HTML. I just added the first filter, you should get the idea.
HTML
<script src="https://unpkg.com/vue"></script>
<div id="app">
<input type="checkbox" v-model="all">Select all<br>
<input type="checkbox" v-model="filters.breakfast.breakfast" class="indent">Breakfast<br>
<input type="checkbox" v-model="filters.breakfast.eggs" class="double-indent">Eggs<br>
<hr>
All: {{ all }}<br>
Breakfast: {{ filters.breakfast.breakfast }}<br>
Eggs: {{ filters.breakfast.eggs }}<br>
</div>
JavaScript
You may use a loop at 'filters.breakfast.breakfast' (). I'd use Lodash for this because the properties mustn't be known.
new Vue({
el: '#app',
data: () => ({
all: false,
filters: {
breakfast: {
breakfast: false,
eggs: false
}
}
}),
watch: {
all () {
this.filters.breakfast.breakfast = !this.filters.breakfast.breakfast
},
'filters.breakfast.breakfast' () {
this.filters.breakfast.eggs = !this.filters.breakfast.eggs
}
}
})
CSS
.indent {
margin-left: 1em;
}
.double-indent {
margin-left: 2em;
}
Preview: http://jsfiddle.net/xv3y04j2/

Basically, I created three batches of tri-state groupings (tri-state +3 choices each), and then created another tri-state checkbox at the top that changes it's state based on the number of choices selected in each of the three groupings.

Related

How to register an array of objects with v-model? Vue 2

The first time that I dive into making this type of form with Vue, the issue is that I can't think of how to save the data inside the foreach that I generate with axios.
Where I would like to save the ID and the option selected with the input select as an object in order to make faster the match in the backend logic.
<template>
<div class="row" v-else>
<div class=" col-sm-6 col-md-6 col-lg-6" v-for="(project, index) in projects" :key="index">
<fieldset class="border p-2 col-11">
<legend class="w-auto col-12">Proyecto: {{project.name}}</legend>
<b-form-group
id="user_id"
label="Reemplazante"
>
<b-form-select
v-model="formProject[index].us"
:options="project.users"
value-field="replacement_user_id"
text-field="replacement_user_name"
#change="addReemplacemet($event,project.id)"
>
<template v-slot:first>
<b-form-select-option value="All">Seleccione</b-form-select-option>
</template>
</b-form-select>
<input type="hidden" name="project" v-model="formProject[index].proj">
</b-form-group>
</fieldset>
</div>
</div>
</template>
<script>
import skeleton from './skeleton/ProjectUserSkeleton.vue'
export default {
name: 'ProjectsUser',
components: { skeleton },
props:{
user: { type: String },
},
data() {
return {
user_id: null,
showProject: false,
projects: [],
loadingProjects: true,
formProject: [
{
us: 'All',
proj: null
}
]
}
},
watch: {
user: function() {
this.viewProjects(this.user)
}
},
methods: {
async getProjects(salesman){
this.loadingProjects = true
await axios.get(route('users.getProjects'),{
params: {
filter_user: salesman
}
})
.then((res)=>{
this.projects = res.data.data
setTimeout(() => {
this.loadingProjects = false
}, 800);
})
},
This is the form:
This is the message error:
At first glance, this looks like an issue with data. Your error suggests that you're trying to read an undefined variable.
It's often undesirable to use an index from iterating one array on another. In your Vue code, the index is projects, but you use it to access the variable formProject. This is usually undesirable because you can no longer expect that index to reference a defined variable (unless you're referencing the array you are currently iterating).
The easiest solution, for now, is to make sure the arrays are of the same length. Then utilizing v-if or other methods to not render the snippet if the variable is not defined.
A more complicated but better solution is restructuring your data such that formProject exists in projects

Vue - Select all checkbox

I'm relatively new to Vue so bare with me on this.
I have a v-for which produces a list of checkboxes, these all individually function correctly if I were to click on them separately, what I am after, like the title says is a select all checkbox to sit ontop which selects all in the list, but I'm running into a few issues which I don't understand, see relevant code below:
new Vue({
el: "#lister-app",
mixins: [pagination, baseDownloadParent],
components: { selectField, articleItem, documentItem },
data: {
facets: {},
isCheckAll: false,
},
computed: {
getFacets() {
return this.facets;
},
getFacetsLength() {
return this.facets.length;
},
},
methods: {
toggleSelect(item, facet) {
if (!this.params[facet.name]) Vue.set(this.params, facet.name, []);
const existElem = this.params[facet.name].findIndex((el) => {
return el === item.identifier;
});
if (existElem !== -1) this.params[facet.name].splice(existElem, 1);
else this.params[facet.name].push(item.identifier);
},
checkAll(){
console.log('FunctionWhichChecksAll');
},
},
});
<label class="option-checkbox" for="Select all">
<input id="Select all" class="option-checkbox__input" type="checkbox" #click='checkAll()' v-model='isCheckAll'>
<span class="option-checkbox__text">Select all</span>
<span class="option-checkbox__icon"></span>
</label>
<option-field inline-template v-for="(item, i) in facet.items" :for="item.name" :key="i + item.name">
<label class="option-checkbox">
<input :id="item.name" class="option-checkbox__input" type="checkbox" v-model="checked" #change="toggleSelect(item, facet)">
<span class="option-checkbox__text">{{item.name}} </span>
<span class="option-checkbox__icon"></span>
</label>
</option-field>
What I am picturing is as piece of script which is inside the checkAll() function?
Any help would be appreciated, thank you in advance.
Here is a working demo, based on your code (simplified):
https://stackblitz.com/edit/vue-prghrq?file=src%2FApp.vue
Checkboxes in Vue can sometimes be a mess, when you combine v-model with #click handlers and especially if you have a check-all field. That's why I usually don't use v-models for it. Using only the :checked value as a one-way binding makes it easier to read and to maintain. With the click handler you can then update the state of each entry.

How to search within nested objects

I have done my research trying to figure out how to achieve what I am describing below, however I had no luck.
In my Algolia index, some records have nested objects.
For example, title and subtitle attributes are of the following format:
title:
{
"en": "English title",
"gr": "Greek title"
}
I would like to execute queries only for a specific subset (in our example "en" or "gr") of these attributes, withoute "exposing" any facet in the UI — language selection would ideally be done “automatically” based on a variable (lang) passed to the Vue component with props. I am using Laravel Scout package with default Vue implementation, as described in documentation here.
My InstantSearch implementation is pretty simple, I am not defining anything specific regarding queries and searchable attributes, I am currently using all the default functionality of Algolia.
<template>
<ais-instant-search
:search-client="searchClient"
index-name="posts_index"
>
<div class="search-box">
<ais-search-box placeholder="Search posts..."></ais-search-box>
</div>
<ais-hits>
<template
slot="item"
slot-scope="{ item }"
>
<div class="list-image">
<img :src="'/images/' + item.image" />
</div>
<div class="list-text">
<h2">
{{ item.title }}
</h2>
<h3>
{{ item.subtitle }}
</h3>
</div>
</template>
</ais-hits>
</ais-instant-search>
</template>
<script>
import algoliasearch from 'algoliasearch/lite';
export default {
data() {
return {
searchClient: algoliasearch(
process.env.ALGOLIA_APP_ID,
process.env.ALGOLIA_SEARCH
),
route: route,
};
},
props: ['lang'],
computed: {
computedItem() {
// computed_item = this.item;
}
}
};
</script>
I would like to somehow pass an option to query “title.en” and “subtitle.en” when variable lang is set to “en”. All this, without the user having to select “title.en” or “subtitle.en” in the UI.
Update
Maybe computed properties is the path to go, however I cannot find how to reference search results/hits attributes (eg item.title) within computed property. It is the code I have commented out.
I think, you can use computed property. Just transform current item according to the current language variable.
new Vue({
template: "<div>{{ computedItem.title }}</div>",
data: {
langFromCookie: "en",
item: {
title: {
en: "Hello",
ru: "Привет"
}
}
},
computed: {
computedItem() {
const item = JSON.parse(JSON.stringify(this.item));
for (value in item) {
if (typeof item[value] === "object" && Object.keys(item[value]).includes(this.langFromCookie))
item[value] = item[value][this.langFromCookie];
}
return item;
}
}
}).$mount("#app")
<div id="app"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
If lang variable is available via props, you can check that inside list-text class and return {{title.en}} or {{title.gr}} accordingly by passing a dynamic lang value title[lang] like below
...
<div class="list-text">
<h2>
{{ item.title[lang] }}
</h2>
<h3>
{{ item.subtitle[lang] }}
</h3>
</div>
If you want to make a request according to lang prop when component mounts ,then you can make a request inside mounted() method then query like below
mounted() {
axios.get(`/getSomethingWithLang/:${this.item.title[this.lang]}`)
...
}

Vuejs multiple active buttons

I'm trying to create a list where every list item contains a button and i want a user to be able to click multiple button. I'm generating my list like so:
<ul>
<li v-for="item in items" :key="item.id">
<button type="button">{{item.title}}</button>
</li>
</ul>
but the problem with my code is whenever a user click a button, it turns the rest of the buttons to "unclicked". been trying to play with the focus and active stats but even with just css i cant get to enable multiple select .
i did manage to change the look of the current selected button:
button:focus {
outline: none;
background-color: #6acddf;
color: #fff;
}
any idea how can i allow multiple buttons to be clicked?
to make things a bit clearer, i am going to create an AJAX call later and pass the item.id of each item where it's button is clicked
I would much rather avoid changing the data structure if possible
Well you have to store somewhere that you clicked on the clicked item.
If you can't edit the items array then you can always create a new one, like isClicked where you store those values.
new Vue({
el: '#app',
data: {
items: [{
id: 1,
title: 'foo'
},
{
id: 2,
title: 'bar'
},
{
id: 3,
title: 'baz'
}
],
isClicked: []
},
beforeMount() {
// set all values to false
this.items.forEach((item, index) => this.$set(this.isClicked, index, false))
},
methods: {
clicked(index) {
// toggle the active class
this.$set(this.isClicked, index, !this.isClicked[index])
}
}
})
.active {
background: red
}
<script src="https://unpkg.com/vue"></script>
<div id="app">
<div v-for="(item, index) in items" :key="index">
<button #click="clicked(index)" :class="{'active': isClicked[index]}">{{item.title}}</button>
</div>
</div>
Or you can use vuex for storing those values.
However you can just use Vues event to manipulate the classList property, like:
new Vue({
el: '#app',
data: {
items: [1, 2, 3, 4, 5, 6, 7]
}
})
.active {
color: red
}
<script src="https://unpkg.com/vue"></script>
<div id="app">
<button v-for="i in items" #click="e => e.target.classList.toggle('active')">{{ i }}</button>
</div>
But it doesn't feel right, IMHO.
Also you can use cookies or localStorage to store those states. So it's really up to you.
Use id attribute for list items to make it unique.
<ul>
<li v-for="item in items" :key="item.id" :id="item.id">
<button type="button" #click="doThis">{{item.title}}</button>
</li>
</ul>
new Vue({
el: '#app',
data: {},
methods: {
doThis() {
// Use this to access current object
}
}
});

Array of inputs, with last field always blank for new add

JSBin and Stackoverflow snippet are below.
I am trying to have a list of input components. If all input components are filled with a value (not blank), then there should be a "new blank field" visible at the end for the user to type into. When he types into it, it should make this field apart of the list above it, maintaining focus in it.
However the problem I'm having is, focus maintains in the new field, and never moves into the array. Here is my code:
JSBIN and stackoverflow snippet - https://jsbin.com/cudabicese/1/edit?html,js,output
const app = new Vue({
el: '#app',
data: {
inputs: [
{ id:'foo', value:'foo' },
{ id:'bar', value:'bar' }
]
},
methods: {
addRow(e) {
this.inputs.push({
id: Date.now(),
value: e.target.value
})
},
deleteRow(index) {
this.inputs.splice(index, 1)
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<div id="app">
<ul>
<li v-for="(input, index) of inputs">
<input type="text" v-model="input.value">
</li>
<li v-if="inputs.filter(input => !!input.value).length">
<input type="text" #input="addRow">
</li>
</ul>
</div>
I'd recommend you put the input for the list within a computed function vs directly using the data. The examples at https://v2.vuejs.org/v2/examples/ are a good place to start.