I'm looking at this:
https://jsfiddle.net/chrisvfritz/pnqzspoe/
<script type="text/x-template" id="item-template">
<li>
<div
:class="{bold: isFolder}"
#click="toggle"
#dblclick="changeType">
{{ model.name }}
<span v-if="isFolder">[{{ open ? '-' : '+' }}]</span>
</div>
<ul v-show="open" v-if="isFolder">
<item
class="item"
v-for="(model, index) in model.children"
:key="index"
:model="model">
</item>
<li class="add" #click="addChild">+</li>
</ul>
</li>
</script>
Vue.component('item', {
template: '#item-template',
props: {
model: Object
},
data: function () {
return {
open: false
}
},
computed: {
isFolder: function () {
return this.model.children &&
this.model.children.length
}
},
methods: {
toggle: function () {
if (this.isFolder) {
this.open = !this.open
this.$emit("expand") // doesn't work properly
}
},
changeType: function () {
if (!this.isFolder) {
Vue.set(this.model, 'children', [])
this.addChild()
this.open = true
}
},
addChild: function () {
this.model.children.push({
name: 'new stuff'
})
}
}
})
And I want to implement some features here:
1. Node selecting functionality. So each node will have a 'selected' property whether it is selected or not.
2. An 'expand' event which will fire when a node is expanded (to load nodes via ajax).
The problem:
The 'expand' event is going only one level up. So it will not be work properly. I've figured two solutions: creating an event bus and passing it to each instance. Or creating a wrapper component and pass it to all children and use it as an event bus.
Also I would like to manage all the selected nodes in one place, so I'll have to duplicate this data on both the nodes and in another element.
Because of this I am confusing whether to nest components like that, or maybe just create a single component, and pass it all the data. The overhead here is that I will have to iterate (recursion?) all the nodes in order to add additional attributes like 'selected', and 'open'. (and vue already iterate recursively in order to make properties reactive)
So what is the better option - nested components, or one component?
I don't think you need nested component, it will make the logic complex.
Below is my solution for lazy load one node (expand):
add one prop = actions, which allow parent component/view register its event, then bind to the click event.
parent component/view defines its lazy load function, then bind <item :actions="{your lazy load function}">
You can do the similar thing for the selected.
The codes will be like below:
var demoData = {
name: 'My Tree',
children: [
{ name: 'hello' },
{ name: 'wat' },
{
name: 'child folder',
children: [
{
name: 'child folder',
children: [
{ name: 'hello' },
{ name: 'wat' }
]
},
{ name: 'hello' },
{ name: 'wat' },
{
name: 'child folder',
children: [
{ name: 'hello' },
{ name: 'wat' }
]
}
]
}
]
}
Vue.component('item', {
template: '#item-template',
props: {
model: Object,
'actions': {
type: Object,
default: ()=>{ return {
'loadNode': function(clickedObj){console.log('default',clickedObj)},
'selectedChange': function(node){console.log('default',node)}
}
}
}
},
data: function () {
return {
open: false
}
},
computed: {
isFolder: function () {
return this.model.children &&
this.model.children.length
}
},
methods: {
toggle: function () {
if (this.isFolder) {
this.open = !this.open
this.$emit("expand") // doesn't work properly
}
},
changeType: function () {
if (!this.isFolder) {
Vue.set(this.model, 'children', [])
this.addChild()
this.open = true
}
},
addChild: function () {
this.model.children.push({
name: 'new stuff'
})
}
}
})
app = new Vue({ //not vue, it is Vue
el: "#app",
data: {
treeData: demoData,
selected: {}
},
methods: {
myLoadNode: function (parent) {
setTimeout(()=>{
if(!parent.children) {
this.$set(parent, 'children', [])
}
parent.children.push(
{
name: 'ajax folder',
children: [
{ name: 'ajax a1' },
{ name: 'ajax a2' }
]
}
)
}, 500)
},
mySelectedChange: function (node) {
// if each node has unique id, use
// this.$set(this.selected, node.id, true) will be better
this.$set(this.selected, JSON.stringify(node), true)
}
}
})
body {
font-family: Menlo, Consolas, monospace;
color: #444;
}
.item {
cursor: pointer;
}
.bold {
font-weight: bold;
}
ul {
padding-left: 1em;
line-height: 1.5em;
list-style-type: dot;
}
.load-button1{
background-color:red
}
.load-button2{
background-color:yellow
}
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<script type="text/x-template" id="item-template">
<li>
<div
:class="{bold: isFolder}"
#click="toggle"
#dblclick="changeType"
>
<a class="load-button1" #click="actions.loadNode(model)">Load</a>
<a class="load-button2" #click="actions.selectedChange(model)">Select</a>
{{ model.name }}
<span v-if="isFolder">[{{ open ? '-' : '+' }}]</span>
</div>
<ul v-show="open" v-if="isFolder">
<item
class="item"
v-for="(model, index) in model.children"
:key="index"
:model="model"
:actions="{'loadNode':actions.loadNode, 'selectedChange':actions.selectedChange}"
>
</item>
<li class="add" #click="addChild">+</li>
</ul>
</li>
</script>
<div id="app">
<item
class="item"
:model="treeData"
:actions="{'loadNode':myLoadNode, 'selectedChange': mySelectedChange}">
</item>
<p>{{selected}}
</div>
Related
I have created dynamic buttons in vuejs where each button represents a different answer to a question.
My goal is: when I get the answer wrong, the correct option is highlighted in green until the next question is shown.
Is it also possible to change other settings of these "BaseButtons" with CSS? How can I do this?
<template>
<div class="container-botoes">
<BaseButton class="optionsButtons"
v-for="options in optionsAnswers"
:key="options.id" #click="handleAnswer(options)">
{{options.ans}}
</BaseButton>
</div>
</template>
methods:{
handleAnswer(options){
if (options.id === this.correctAnswer){
this.playerHit = true;
}
else {
this.opponentHit = true;
}
this.nextquestion();
},
One option is to create css classes with styles you need and append them to BaseButton component depending on your conditions
Have a look at this one:
HTML block:
<template>
<div class="container-botoes">
<BaseButton
v-for="(options, index) in optionsAnswers"
:key="options.id"
class="optionsButtons"
:class="correctAnsIndex === index ? 'green-button' : 'red-button'"
#click="handleAnswer(options, index)"
>
{{ options.ans }}
</BaseButton>
</div>
</template>
JavaScript block:
<script>
export default {
data() {
return {
correctAnsIndex: null,
}
},
methods: {
handleAnswer(options, index) {
if (options.id === this.correctAnswer) {
this.playerHit = true
this.correctAnsIndex = index
} else {
this.opponentHit = true
this.correctAnsIndex = null
}
this.nextquestion()
},
},
}
</script>
CSS block:
<style>
.red-button {
background: red;
color: white;
font-weight: 700;
}
.green-button {
background: green;
color: white;
font-weight: 700;
}
</style>
Code explanation:
We have passed the index of the loop in the handleAnswer method, where the value of the index will be assigned to the correctAnsIndex variable if options.id === this.correctAnswer and in the else part we will assign null value to the correctAnsIndex variable.
Now, we have applied conditional classes in HTML block, where if the index and correctAnsIndex matches then it would apply green-button class or else it will apple red-button class.
Eventually getting your expected result.
Try this :
Vue.component('basebutton', {
data() {
return {
isCorrect: false
}
},
props: ['answerobj'],
template: `<button :class="{ 'green': isCorrect, 'white': !isCorrect}" #click="handleAnswer(answerobj)">{{ answerobj.answer }}</button>`,
methods: {
handleAnswer(answerobj) {
if (answerobj.correct) {
this.isCorrect = true
} else {
this.isCorrect = false
}
}
}
});
var app = new Vue({
el: '#app',
data: {
list: [{
question: 'Who is the tallest animal ?',
optionsAnswers: [{
answer: 'Elephant',
correct: false
}, {
answer: 'Jirafe',
correct: true
}, {
answer: 'Lion',
correct: false
}, {
answer: 'Zebra',
correct: false
}]
}]
}
});
.green {
background-color: green;
}
.white {
background-color: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div v-for="(item, index) in list" :key="index">
<p><strong>Question : </strong>{{ item.question }}</p>
<p><strong>Answers :</strong></p>
<BaseButton v-for="(options, i) in item.optionsAnswers" :key="i" :answerobj="options">
</BaseButton>
</div>
</div>
I am stuck at this very point: exporting filter function and using it in vuex store. No problem'till here. Now am trying to put #click event on divs. And when I click, for example. Audi the filter needs to show just "audi" And if I click "audi" again then it needs remove it from the filter.
Here is the sandbox: https://codesandbox.io/s/filtering-bzphi
filter.js
export const carFilter = car => allcars => {
if (car.length > 0) {
if (allcars.name.includes(car)) {
return true;
} else {
return false;
}
} else {
return true;
}
};
Store
export const store = new Vuex.Store({
state: {
cars: [
{ name: "AUDI" },
{ name: "BMW" },
{ name: "MERCEDES" },
{ name: "HONDA" },
{ name: "TOYOTA" }
],
carBrand: []
},
mutations: {
updateCarsFilter(state, carBrand) {
state.carBrand = carBrand;
}
},
getters: {
filteredCars: state => {
return state.cars.filter(carFilter(state.carBrand));
}
}
});
and App.js
<template>
<div id="app">
<div class="boxes" :key="index" v-for="(item, index) in cars">{{item.name}}</div>
<List/>
</div>
</template>
<script>
import List from "./List.vue";
export default {
name: "App",
components: {
List
},
computed: {
selectBrand: {
set(val) {
this.$store.commit("updateCarsFilter", val);
},
get() {
return this.$store.state.carBrand;
}
},
cars() {
return this.$store.getters.filteredCars;
}
}
};
</script>
I also created a sandbox for this. You can check it for better understanding. https://codesandbox.io/s/filtering-bzphi
In the store.js
changed the carBrand default to ''
added Mutation clearFilter
added Getter isActiveFilter
update
remove carBrand from state
replaced by selectedCars that is an array
removed mutation about carBrand
added mutation addCarSelection removeCarSelection
filteredCars return selectedCars array if contains cars, otherwise cars state
added isSelectedCar to check if a car is in the selection
carFilter function from filter.js is no longer needed.
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export const store = new Vuex.Store({
state: {
cars: [
{ name: "AUDI" },
{ name: "BMW" },
{ name: "MERCEDES" },
{ name: "HONDA" },
{ name: "TOYOTA" }
],
selectedCars: []
},
mutations: {
addCarSelection(state, car) {
state.selectedCars.push(car);
},
removeCarSelection(state, car) {
state.selectedCars = state.selectedCars.filter(r => r.name !== car.name);
}
},
getters: {
filteredCars: state => {
if (state.selectedCars.length !== 0) {
// There's selected cars, return filtered
return state.selectedCars;
} else {
return state.cars;
}
},
isSelectedCar: state => car => {
return state.selectedCars.some(r => r.name === car.name);
}
}
});
In the App.vue
added method filterCars (moved from computed property searchText)
added method clearFilter
update
removed filterCars and 'clearFilter' method and mapped new mutation and getters from store
methods: {
addCarSelection(car) {
this.$store.commit("addCarSelection", car);
},
removeCarSelection(car) {
this.$store.commit("removeCarSelection", car);
},
isSelectedCar(car) {
return this.$store.getters.isSelectedCar(car)
},
}
added isFilterActive() computed property
update
removed isFilterActive() and searchText from computed property
computed: {
cars() {
return this.$store.getters.filteredCars;
},
},
update
Changed the Template code to manage #click event to add car or remove car from selection
boxes always show cars available, if isSelectedCar toggle between add or remove function.
List show selected cars if presents otherwise the full car catalog.
<template>
<div id="app">
<div class="boxes" :key="index" v-for="(item, index) in cars">
<div
v-if="!isSelectedCar(item)"
style="cursor:pointer"
#click="addCarSelection(item)"
>{{item.name}}</div>
<div v-else style="cursor:pointer;" #click="removeCarSelection(item)">
{{item.name}}
<small>[x]</small>
</div>
</div>
<List/>
</div>
</template>
Updated version is available in this sandbox
https://codesandbox.io/s/filtering-3ej7d
I want my sidebar dropdown to be available only one at a time, so when I click on another dropdown, the previous dropdown will be hidden again.
Below is the example of my current dropdown, which where you can open multiple dropdowns at a time. https://coreui.io/vue/demo/#/dashboard
<template>
<router-link tag="li" class="nav-item nav-dropdown" :to="url" disabled>
<div class="nav-link nav-dropdown-toggle" #click="handleClick"><i :class="icon"></i> {{name}}</div>
<ul class="nav-dropdown-items">
<slot></slot>
</ul>
</router-link>
</template>
<script>
export default {
props: {
name: {
type: String,
default: ''
},
url: {
type: String,
default: ''
},
icon: {
type: String,
default: ''
}
},
methods: {
handleClick(e) {
e.preventDefault();
e.target.parentElement.classList.toggle('open');
}
}
};
</script>
Please help.
The usual way to make a radio-group type controller (where only one item can be selected at once) is to have a variable that indicates which one is selected. Then each element compares itself to the selected one to determine whether it should be in the open state.
Since you have multiple router-links that don't know about each other, the parent object is going to have to own the which-one-is-selected indicator variable. The handleClick of your router-link should $emit an event that the parent will handle by changing the indicator variable. And the router-link should receive the indicator variable as a prop and use it in a computed to set the open class as appropriate.
Your code might look like this:
<template>
<router-link tag="li" class="nav-item nav-dropdown" :class="openClass" :to="url" disabled>
<div class="nav-link nav-dropdown-toggle" #click="handleClick"><i :class="icon"></i> {{name}}</div>
<ul class="nav-dropdown-items">
<slot></slot>
</ul>
</router-link>
</template>
<script>
export default {
props: {
name: {
type: String,
default: '',
selectedItem: Object
},
url: {
type: String,
default: ''
},
icon: {
type: String,
default: ''
}
},
computed: {
openClass() {
return this.selectedItem === this ? 'open' : '';
}
}
methods: {
handleClick(e) {
e.preventDefault();
this.$emit('setSelected', this);
}
}
};
</script>
You can add "itemAttr" property in _nav.js like:
items: [
{
name: 'Dropdown',
url: '/dropdown',
icon: 'icon-grid',
itemAttr: { id: 'drop-1' },
children: [{
name: 'Sub-Item 1',
url: '/dropdown/subitem1'
}, {
name: 'Sub-Item 2',
url: '/dropdown/subitem2'
}, {
name: 'Sub-Item 3',
url: '/dashboard/subitem3'
}]
},
{
name: 'Base',
url: '/base',
icon: 'icon-base',
itemAttr: { id: 'item-1' }
}
]
and in DefaultLayout.js, add event-listeners for click on these two id's, like:
var e1 = document.getElementById("drop-1")
e1.addEventListener("click", function () {
e1.classList.className += " open";
});
var ev1 = document.getElementById("item-1")
ev1.addEventListener("click", function () {
e1.className = "nav-item nav-dropdown"
});
Similarly, you can add more dropdowns and give them id's "drop-2" and "drop-3". OnClick, if you want to open that dropdown list use:
e<i>.classList.className += " open";
and for all the remaining dropdowns that you want to close use:
e<j>.className = "nav-item nav-dropdown";
When clicking on an item you want to close all dropdowns, so use:
e<i>.className = "nav-item nav-dropdown"; //for all the dropdown items.
I'm implementing Vue paper dashboard sidebar. So I have something like this:
Into Index I have
<template>
<div>
AdminIndex
<side-bar>
</side-bar>
</div>
</template>
<script>
import { faBox, faImages } from '#fortawesome/fontawesome-free-solid';
import Sidebar from '#/components/sidebar/SideBar';
export default {
name: 'admin-index-view',
components: {
SideBar,
},
data() {
return {
showSidebar: false,
sidebarLinks: [
{
name: 'admin.menu.products',
icon: faBoxes,
route: { name: 'adminProducts' },
},
{
name: 'admin.menu.sliders',
icon: faImages,
route: { name: '/admin/stats' },
},
],
};
},
methods: {
displaySidebar(value) {
this.showSidebar = value;
},
},
};
</script>
SideBar component:
<template>
<div :class="sidebarClasses"
:data-background-color="backgroundColor"
:data-active-color="activeColor">
<!--
Tip 1: you can change the color of the sidebar's background using: data-background-color="white | black | darkblue"
Tip 2: you can change the color of the active button using the data-active-color="primary | info | success | warning | danger"
-->
<!-- -->
<div class="sidebar-wrapper"
id="style-3">
<div class="logo">
<a href="#"
class="simple-text">
<div class="logo-img">
<img src="static/img/vue-logo.png"
alt="">
</div>
Paper Dashboard
</a>
</div>
<slot>
</slot>
<ul :class="navClasses">
<!--By default vue-router adds an active class to each route link. This way the links are colored when clicked-->
<router-link v-for="(link,index) in sidebarLinks"
:key="index"
:to="link.route"
tag="li"
:ref="link.name">
<a>
<font-awesome-icon :icon="link.icon" />
<p v-t="link.name" />
</a>
</router-link>
</ul>
<moving-arrow :move-y="arrowMovePx">
</moving-arrow>
</div>
</div>
</template>
<script>
import FontAwesomeIcon from '#fortawesome/vue-fontawesome';
import MovingArrow from './MovingArrow';
export default {
name: 'side-bar',
components: {
MovingArrow,
FontAwesomeIcon,
},
props: {
type: {
type: String,
default: 'sidebar',
validator: value => {
const acceptedValues = ['sidebar', 'navbar'];
return acceptedValues.indexOf(value) !== -1;
},
},
backgroundColor: {
type: String,
default: 'black',
validator: value => {
const acceptedValues = ['white', 'black', 'darkblue'];
return acceptedValues.indexOf(value) !== -1;
},
},
activeColor: {
type: String,
default: 'success',
validator: value => {
const acceptedValues = [
'primary',
'info',
'success',
'warning',
'danger',
];
return acceptedValues.indexOf(value) !== -1;
},
},
sidebarLinks: {
type: Array,
default: () => [],
},
},
data() {
return {
linkHeight: 60,
activeLinkIndex: 0,
windowWidth: 0,
isWindows: false,
hasAutoHeight: false,
};
},
computed: {
sidebarClasses() {
if (this.type === 'sidebar') {
return 'sidebar';
}
return 'collapse navbar-collapse off-canvas-sidebar';
},
navClasses() {
if (this.type === 'sidebar') {
return 'nav';
}
return 'nav navbar-nav';
},
/**
* Styles to animate the arrow near the current active sidebar link
* #returns {{transform: string}}
*/
arrowMovePx() {
return this.linkHeight * this.activeLinkIndex;
},
},
watch: {
$route() {
this.findActiveLink();
},
},
methods: {
findActiveLink() {
this.sidebarLinks.find((element, index) => {
const found = element.path === this.$route.path;
if (found) {
this.activeLinkIndex = index;
}
return found;
});
},
},
mounted() {
this.findActiveLink();
},
};
</script>
I dont receive any issues or vue errors, sidebar just don't display. In Chrome console just return empty: <side-bar data-v-66018f3c=""></side-bar> Someone knows why sidebar is not binded? What I need to do to get correctly implementation of it? Regards
Chrome console error:
[Vue warn]: Unknown custom element: - did you register the
component correctly? For recursive components, make sure to provide
the "name" option.
I am writing a react app to print a tree-view from an array of nested objects. Each node should be collapsible (open by default). A node may have any number of child nodes. Here is a sample of what I am trying to achieve:
I am collecting all node components inside an array and then accessing that collection in render method. Right now all node components are printed at same heirarchy as they are not nested in each other. In the code below, how can I add <PrintNode treeData={obj} as a child of <PrintNode treeData={obj} hasChildNodes={true} /> in the treeToRender array?
App.js:
import React from 'react';
import PrintNode from './components/PrintNode';
export default class App extends React.Component {
data = [ // this data will come from another file in real app
{
text: 'Parent 1',
nodes: [
{
text: 'Child 1',
nodes: [
{
text: 'Grandchild 1'
},
{
text: 'Grandchild 2'
}
]
},
{
text: 'Child 2'
}
]
},
{
text: 'Parent 2'
},
{
text: 'Parent 3'
},
{
text: 'Parent 4'
},
{
text: 'Parent 5'
}
];
treeToRender = [];
getNextNode(nextData) {
let key = 0;
for (let i = 0; i < nextData.length; i++) {
let obj = nextData[i];
if (!obj.nodes) {
this.treeToRender.push(<PrintNode treeData={obj} />)
key++;
}
else {
this.treeToRender.push(<PrintNode treeData={obj} hasChildNodes={true} />)
key++;
this.getNextNode(obj.nodes);
}
}
return this.treeToRender;
}
render() {
return (
<div className="app-container">
{this.getNextNode(this.data)}
</div>
);
}
}
PrintNode.js
import React from 'react';
export default class PrintNode extends React.Component {
render() {
let nodeToRender = '';
if (this.props.hasChildNodes) {
nodeToRender = <div className="has-child">{this.props.treeData.text}</div>
}
else {
nodeToRender = <div>{this.props.treeData.text}</div>
}
return (
<div>
<div>{this.props.treeData.text}</div>
</div>
);
}
}
Please note that each JSX element is an object, not a string which can be easily manipulated.
Also I am getting error "Each child in an array or iterator should have a unique "key" prop. Check the render method of App." even though I have added key to PrintNode. Why is this happening?
Here is the fiddle.
Here is an updated solution for. I hope this helps you understand how to recursively build JSX objects.
I've provided a Codepen as well for you to look at so that you can see the working code.
function createTree(data) {
return (
<ul>
{
data.map((node) => {
return createNode(node);
})
}
</ul>
);
}
function createNode(data) {
if (data.nodes) {
return (
<li>
<PrintNode text={ data.text }>{createTree(data.nodes)}</PrintNode>
</li>
);
} else {
return <PrintNode text={ data.text } />
}
}
const PrintNode = (props) => {
return <li>{ props.text }{ props.children }</li>
};
const App = (props) => {
return createTree(props.data);
}
ReactDOM.render(<App data={ data } />, document.getElementById("app"));
const data = [
{
text: 'Parent 1',
nodes: [
{
text: 'Child 1',
nodes: [
{
text: 'Grandchild 1'
},
{
text: 'Grandchild 2'
}
]
},
{
text: 'Child 2'
}
]
},
{
text: 'Parent 2'
},
{
text: 'Parent 3'
},
{
text: 'Parent 4'
},
{
text: 'Parent 5'
}
];