I'm trying to build what I'd assumed would be a relatively straightforward implementation of Ant-D's Tree component, and I'm running into issues.
Specifically, I'm unclear how to replicate the "half-checked" vs. "full-checked" behavior of their example. I'd like for the child nodes, when all fully checked, to also check their parent. Likewise when the parents are de-selected, I'd like all of the children to be deselected as well. Finally, when only some of a child nodes are checked/unchecked, the parent should go into a "half-checked" state.
The API seems to allow for this, and indeed they have an example here that purports to show off this functionality.
<template>
<a-tree
v-model:selectedKeys="selectedKeys"
v-model:checkedKeys="checkedKeys"
default-expand-all
checkable
:height="233"
:tree-data="treeData"
>
<template #title="{ title, key }">
<span v-if="key === '0-0-1-0'" style="color: #1890ff">{{ title }}</span>
<template v-else>{{ title }}</template>
</template>
</a-tree>
</template>
<script lang="ts">
import type { TreeProps } from 'ant-design-vue';
import { defineComponent, ref, watch } from 'vue';
function dig(path = '0', level = 3) {
const list: TreeProps['treeData'] = [];
for (let i = 0; i < 10; i += 1) {
const key = `${path}-${i}`;
const treeNode: TreeProps['treeData'][number] = {
title: key,
key,
};
if (level > 0) {
treeNode.children = dig(key, level - 1);
}
list.push(treeNode);
}
return list;
}
export default defineComponent({
setup() {
const selectedKeys = ref<string[]>(['0-0-0', '0-0-1']);
const checkedKeys = ref<string[]>(['0-0-0', '0-0-1']);
watch(selectedKeys, () => {
console.log('selectedKeys', selectedKeys);
});
watch(checkedKeys, () => {
console.log('checkedKeys', checkedKeys);
});
return {
treeData: dig(),
selectedKeys,
checkedKeys,
};
},
});
</script>
It's not clear to me how this works. Nowhere are they setting the checkedKeys data. Is this handled internally by the tree? I've tried copying this example locally and it's not even working.
The documentation further states about the checkedKeys prop:
"When this specifies the key of a treeNode which is also a parent treeNode, all the children treeNodes of will be checked; and vice versa, when it specifies the key of a treeNode which is a child treeNode, its parent treeNode will also be checked. When checkable and checkStrictly is true, its object has checked and halfChecked property."
If this example does not have checkStrictly set to true, then how are only some of the nodes supposed to become "half checked"?
Related
Vue.js 3, Composition API
I have data coming from a legacy server (can't change it!) which looks like this:
{
"propA": [{"valueA": "x", "valueB": "y"}, {"valueA": "o", "valueB": "p"}],
"propB": [{"valueL": "a", "valueM": "b"}]
}
Each value of each property has an optional field state (e.g. {"valueA": "x", "valueB": "y", "state": "updated"}), which needs to be set accordingly, depending if the property was updated or deleted.
Each property has an editor for each of its values (e.g. text input field) which is wrapped in another (reusable) component handling state changes:
it can reset the value to its initial state
it can set the values state to deleted
it needs to set its state to updated whenever any property of a value (except state!) changes
Each value is passed as v-model to the wrapper component.
The wrapper component doesn't know which fields exist, except for state (doesn't need to, fields vary a lot), it has buttons to reset the state of the object to its initial value and to set state to deleted. It should watch for changes to initial data as well and set updated or delete state accordingly.
On form submit the application sends the data to the server along with the state properties.
What would the best way be to solve this with Vue.js 3?
I tried watching each value ({"valueA": "x", "valueB": "y"}) with a watcher function, but then it gets into endless loop on updateValue / deleteValue in watch:
const initialState = lodash.cloneDeep(props.modelValue)
watch(props.modelValue, (new, old) => {
if (hasInitialStateChanged(new)) {
updateValue("state", "updated");
} else {
deleteValue("state");
}
});
function updateValue(key, value) {
emit("update:modelValue", { ...props.modelValue, [key]: value });
}
function deleteValue(key) {
const o = { ...props.modelValue };
delete o[key];
emit("update:modelValue", o);
}
function hasInitialStateChanged(newState) {
const item = lodash.cloneDeep(newState);
const originalItem = lodash.cloneDeep(initialItem);
delete item["state"];
delete originalItem["state"];
return !isEqual(item, originalItem);
}
Any ideas? For Angular folks: I used ngDoChange and mutated the state property accordingly.
After some discussion on Vue forums I found quite an elegant solution, which works fine with the data in its current form.
Here is a working solution for anyone interested: Vue SFC Playground
//App.vue
<script setup>
import { ref } from 'vue'
import StatefulEditor from './StatefulEditor.vue'
const o = ref({
"propA": [{"valueA": "x", "valueB": "y"}, {"valueA": "o", "valueB": "p"}],
"propB": [{"valueL": "a", "valueM": "b"}]
})
</script>
<template>
<h1>
HELLO
</h1>
<StatefulEditor v-model="o.propA[index]"
v-for="(prop, index) in o.propA"
:key="index"
>
<input type="text" v-model="prop.valueA" />
</StatefulEditor>
</template>
//StatefulEditor.vue
<template>
<div>
<div>
<slot></slot>
</div>
<div>
<button
v-if="modelValue.state === 'updated'"
#click="resetItem()"
>
RESET
</button>
<button
v-if="modelValue.state === 'deleted'"
#click="undeleteItem()"
>
UNDO
</button>
<button
v-if="(modelValue.state === undefined || modelValue.state === 'created')"
#click="deleteItem()"
>
DELETE
</button>
<span>{{modelValue.state}}</span>
</div>
</div>
</template>
<script setup>
import { watch, computed } from "vue";
import { cloneDeep, isEqual } from "lodash";
const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
const initialItem = cloneDeep(props.modelValue);
const initialStateChanged = computed(() => hasInitialStateChanged(props.modelValue))
watch(initialStateChanged, (n) => {updateValue("state", n ? "updated" : undefined)})
function resetItem() {
emit("update:modelValue", { ...initialItem });
}
function deleteItem() {
updateValue("state", "deleted");
}
function undeleteItem() {
updateValue("state", undefined);
}
function hasInitialStateChanged(newState) {
const item = cloneDeep(newState);
const originalItem = cloneDeep(initialItem);
delete item["state"];
delete originalItem["state"];
return !isEqual(item, originalItem);
}
function updateValue(key, value) {
emit("update:modelValue", { ...props.modelValue, [key]: value });
}
function deleteValue(key) {
const o = { ...props.modelValue };
delete o[key];
emit("update:modelValue", o);
}
</script>
I'm working currently with BootstrapVue.
I have a b-dropdown in my parent.vue where I can select a object of a JSON-File and convert it into an array because I need the length of this json object. This works fine!!
My problem is that I need to check in my parent.vue if something was selected - so if this.arrayLength is higher than 0 (until this point it works all well!). If this is true, it should use and show addElementsNotClickable() in my child.vue where no elements can be added (count of the inputs are equal to length of array) - otherwise it should use and show my button addElement() where multiple elements can be added manually.
But I'm not able to check in my child.vue if arrayLenght > 0... AND i don't know what to use on second button e.g #change(??) How can I solve that?
Many thanks! I've tried to be as detailed as I can!
Additional Info: I get no error codes!!
my parent.vue:
methods: {
inputedValue(input, index) {
var array = [];
const item= this.json.find((i) => i.Number === input);
for (let key in item.ID) {
array.push(item.ID[key]);
}
if(array.length > 0) {
this.getIndex = index;
this.getDataArray = array;
this.getLengthArray = array.length;
}
}
}
my child.vue (template)
<div class="mt-4 mb-5 ml-3 mr-3">
<b-button v-if="!hide" #click="addElement" variant="block">Add Element</b-button>
<b-button v-if="hide" #???="addElementNotClickable" variant="block">Not clickable ! </b-button>
</div>
my child.vue (script)
methods: {
addElementsNotClickable() {
for(let i = 1; i < this.arrayLength; i++) {
this.inputs.push({})
}
},
addElement() {
this.inputs.push({})
},
}
data() {
return {
inputs: [{}]
arrayLength: this.getLengthArray,
arrayIndex: this.getIndex,
hide: false,
}
props: [
"getLengthArray",
"getIndex"
],
You are misunderstanding how components should work in Vue. In short you can understand them by:
parent send props down and child send events up
What you are looking for is that whenever your arrayLength updates, you send an event to the parent. Then, it is the responsibility of the parent to handle that event. In this case, the parent would receive the event and store the length of the array.
Parent.vue
<template>
<div>
<child #arrayLenght:update="onArrayLenghtUpdate"/>
</div>
</template>
<script>
export default {
data: () => {
arrayLength: 0,
},
methods: {
onArrayLenghtUpdate(length) {
this.arrayLength = length;
}
}
}
</script>
Child.vue
<template> ... </template>
<script>
export default {
data: () => ({
arrayLength: 0,
}),
watch: {
arrayLenghth: function (newLenght) {
this.$emit('arrayLenght:update', newLenght);
}
}
}
</script>
This is the standard way and extremely useful if your Parent and Child aren't highly coupled together. If they are dependent on each other (you won't use Child.vue anywhere else in the app. Just as direct child of Parent.vue) then you can use the inject/provide approach. This approach is beyond the scope of this answer, feel free to read an article on it
I'm a beginner trying to get my app to pass props that set CSS styles down a chain to child components. I have a listener that checks for view port size, and as the window gets resized, it checks past a certain point and then swaps the css class and passes it down the chain..
I think I may be doing something incorrectly because my child components don't seem to be receiving the new styles and aren't updating in the DOM as I drag the window.
Here is my code.. I removed irrelevant code to make it easier to read:
Page_Listings.vue
<template>
<main>
<section>
<ListingRack
:prp_classes="rackClass"
/>
</section>
</main>
</template>
<script>
import ListingRack from './Listing__Rack.vue';
export default {
name: 'Front_Page__Panel',
data() {
return {
viewportWidth: window.innerWidth
}
},
methods: {},
mounted() { window.onresize = () => this.viewportWidth = window.innerWidth },
components: {ListingRack},
},
computed: {
rackClass: function(){
let theValue;
console.log('>> viewport width is now: ',this.viewportWidth)
if(this.viewportWidth > 1200) {
theValue = "grid_view";
console.log('>> grid view')
}
else {
theValue = 'card_view';
console.log('>> card view')
}
return theValue
}
}
}
</script>
Listing__Rack.vue
<template>
<div class="listing_rack" :class="classes">
<ul>
<li v-for="item in listings" :key="item.postId">
// I removed irrelevant code for hte sake of simplicity in this example.
// listings is a GraphQL returned array of data that generates a list of "listings".
<Listing
:prp_classes=classes
/>
</li>
</ul>
</div>
</template>
<script>
import Listing from './Listing.vue'
export default {
name: 'listing__rack',
data() {
return {
posts: [], // what we get from the database.
listings: [], // what we copy from the database.
classes: this.prp_classes
}
},
props: {
prp_classes: String
},
components: {
Listing
},
watch: {
classes: function(){
//just to check if we're receiving anything...
console.log(">> [Listing_Rack](watch)(classes) there was a change to classes!");
}
}
}
</script>
Listing.vue
<template>
<div :id=id
:class=classes
class="listing"
:style="backgroundStyle"
>
</div>
</template>
<script>
export default {
name: 'Listing',
data() {
return {
classes: this.prp_classes,
backgroundStyle: String
}
},
props: {
prp_classes: String
},
methods: {
checkClasses: function(){
if(this.classes === 'grid_view') this.backgroundStyle = 'background: center / cover no-repeat url(background.jpg);';
}
},
mounted: function() {
this.checkClasses();
},
watch: {
classes: function(){
this.checkClasses();
}
}
}
</script>
My console.logs on rackClass so I know the class swapping part is working, but all my subsequent child components don't seem to be updating accordingly..
Can someone tell me what I'm doing wrong? Is there a better way to do this? How come my props aren't being passed when I drag the window, and how can I dynamically set styles in the DOM?
Your code does not work because of the one big mistake (don't worry, many people do it)
You are passing your classes using props to child components. But instead of using this prop (prp_classes) directly in the child's template, you create an absolutely unnecessary classes property in the data()
Problem with that is that data() is executed only once when the component is created. If the value of the prp_classes prop changes later, classes property from the data() just holds the old value.
To fix this, remove unnecessary classes from the data and use the prop directly in the template...
...bit more explanation by example what is going on:
let prp_classes = 'card_view'
let classes = prp_classes
prp_classes = 'grid_view'
// prp_classes === 'grid_view', classes === 'card_view', prp_classes !== classes
// strings/numbers/Date ...all work the same
let o1 = { a: 1 }
let o2 = o1
o1.a = 2
// o1.a === 2, o2.a === 2, o1 === o2
More to study
I have made a global component that will render the content we want.
This component is very simple
<template>
<section
id="help"
class="collapse"
>
<div class="container-fluid">
<slot />
</div>
</section>
</template>
<script>
export default {
name: 'VHelp',
};
</script>
I use it inside my base template with
<v-help />
I'm trying to add content to this component slot from another single file component using.
<v-help>
<p>esgssthsrthsrt</p>
</v-help>
But this logically create another instance of my comp, with the p tag inside. Not the correct thing I want to do.
So I tried with virtual DOM and rendering function, replacing slot by <v-elements-generator :elements="$store.state.help.helpElements" /> inside my VHelp comp.
The store helpElements is a simple array with objects inside.
{
type: 'a',
config: {
class: 'btn btn-default',
},
nestedElements: [
{
type: 'span',
value: 'example',
},
{
type: 'i',
},
],
},
Then inside my VElementsGenerator comp I have a render function that with render element inside virtual DOM from an object like
<script>
import {
cloneDeep,
isEmpty,
} from 'lodash';
export default {
name: 'VElementsGenerator',
props: {
elements: {
type: Array,
required: true,
},
},
methods: {
iterateThroughObject(object, createElement, isNestedElement = false) {
const generatedElement = [];
for (const entry of object) {
const nestedElements = [];
let elementConfig = {};
if (typeof entry.config !== 'undefined') {
elementConfig = cloneDeep(entry.config);
}
if (entry.nestedElements) {
nestedElements.push(this.iterateThroughObject(entry.nestedElements, createElement, true));
}
generatedElement.push(createElement(
entry.type,
isEmpty(elementConfig) ? entry.value : elementConfig,
nestedElements
));
if (typeof entry.parentValue !== 'undefined') {
generatedElement.push(entry.parentValue);
}
}
if (isNestedElement) {
return generatedElement.length === 1 ? generatedElement[0] : generatedElement;
}
return createElement('div', generatedElement);
},
},
render(createElement) {
if (this.elements) {
return this.iterateThroughObject(this.elements, createElement);
}
return false;
},
};
</script>
This second method is working well but if I want to render complex data, the object used inside the rendering function is very very long and complex to read.
So I'm trying to find another way to add content to a global component used inside a base layout only when I want it on a child component.
I can't use this VHelp component directly inside children comps because the HTML page architecture will be totally wrong.
I'm wondering if this is possible to add content (preferably HTML) to a component slot from a single file comp without re-creating a new instance of the component?
Furthermore I think this is very ugly to save HTML as string inside a Vuex store. So I don't even know if this is possible and if I need to completely change the way I'm trying to do this.
Any ideas ?
In the store, you should only store data and not an HTML structure. The way to go with this problem would be to store the current state of the content of the v-help component in the store. Then, you would have a single v-help component with a slot (like you already proposed). You should pass different contents according to the state in the store. Here is an abstract example:
<v-help>
<content-one v-if="$store.state.content === 'CONTENT_ONE' />
<content-two v-else-if="$store.state.content === 'CONTENT_TWO' />
<content-fallback v-else />
</v-help>
Child element somewhere else:
<div>
<button #click="$store.commit('setContentToOne')">Content 1</button>
</div>
Vuex Store:
state: {
content: null
},
mutations: {
setContentToOne(state) {
state.content = 'CONTENT_ONE';
}
}
Of course it depends on your requirements and especially on how many different scenarios are used if this is the best way to achieve this. If I understood you correctly, you are saving help elements to the store. You could also save an array of currently selected help elements in there and just display them directly in the v-help component.
EDIT:
Of course you can also just save the static component (or its name) in the store. Then, you could dynamically decide in the child components, which content is shown in v-help. Here is an example:
<v-help>
<component :is="$store.state.helpComponent" v-if="$store.state.helpComponent !== null" />
</v-help>
Test Component:
<template>
test component
</template>
<script>
export default {
name: 'test-component'
};
</script>
Child element somewhere else (variant 1, storing the name in Vuex):
<div>
<button #click="$store.commit('setHelpComponent', 'test-component')">Set v-help component to 'test-component'</button>
</div>
Child element somewhere else (variant 2, storing the whole component in Vuex):
<template>
<button #click="$store.commit('setHelpComponent', testComponent)">Set v-help component to testComponent (imported)</button>
</template>
<script>
import TestComponent from '#/components/TestComponent';
export default {
name: 'some-child-component',
computed: {
testComponent() {
return TestComponent;
}
}
};
</script>
Child element somewhere else (variant 3, storing the name, derived from the imported component, in Vuex; I would go with this variant):
<template>
<button #click="$store.commit('setHelpComponent', testComponentName)">Set v-help component to 'test-component'</button>
</template>
<script>
import TestComponent from '#/components/TestComponent';
export default {
name: 'some-child-component',
computed: {
testComponentName() {
return TestComponent.name;
}
}
};
</script>
Vuex Store:
state: {
helpComponent: null
},
mutations: {
setHelpComponent(state, value) {
state.helpComponent = value;
}
}
See also the documentation for dynamic components (<component :is=""> syntax): https://v2.vuejs.org/v2/guide/components.html#Dynamic-Components
In Vue.js i have a component (Answer Component) like this:
<template>
<a class="quiz-input-choice" :class="{'quiz-input-choice--selected': answer.selected}"
#click="toggleSelect()" :selected="answer.selected">
<img :src="answer.image_path"/>
<p class="quiz-input-choice__description">{{answer.title}}</p>
</a>
</template>
<script>
export default {
props: ['answer'],
methods: {
toggleSelect() {
this.$parent.$emit('answer-selected', this.answer.id);
}
}
}
</script>
If in the parent (Question Component) I update the "selected" attribute of the element, this component will not be rerendered.
export default {
props: ['question'],
components: {QuizAnswer},
created: function () {
let _self = this;
this.$on('answer-selected', id => {
let i = _self.question.answers.map(item => item.id).indexOf(id);
let answer = _self.question.answers[i];
answer.selected = !answer.selected;
});
}
}
In Vue Developer Console, i checked that Answer component data are updated, so the answer is marked as selected. Anyway, is not rendered with the "quiz-input-choice--selected" class.
If, strangely, I update from the parent other attribute of the prop (for example (answer.title), then the child component is rendered correctly with also the class "quiz-input-choice--selected".
So i guess it's a problem of detecting changes from the child.
Thank you everybody for the answers.
I discovered the problem. The "selected" attribute of the answer was not present in the initial object, so Vue cannot make reactive that attribute.
https://v2.vuejs.org/v2/guide/reactivity.html
I solved making reactive that property in the parent component.
created() {
let self = this;
this.question.answers.forEach(function (answer) {
self.$set(answer, 'selected', false);
});
},
I think you have a structural issue here. You shouldn't submit an event to your parent, since the component is supposed to be self-contained.
What you can do however is emitting an event in the child component (Answer) that will be catch in the parent (Question).
Answer.vue
<template>
<a class="quiz-input-choice" :class="{'quiz-input-choice--selected': answer.selected}"
#click="toggleSelect()" :selected="answer.selected">
<img :src="answer.image_path"/>
<p class="quiz-input-choice__description">{{answer.title}}</p>
</a>
</template>
<script>
export default {
props: ['answer'],
methods: {
toggleSelect() {
this.$emit('answer-selected');
}
}
}
</script>
Question.vue
Your template will have be catching the event like this (I don't know where your answers are so I assume that you have a answers array) :
<answer
v-for="(answer, index) in answers"
:answer="answer"
#answer-selected="answerSelected(index)"
></answer>
And your script will look like this :
export default {
props: ['question'],
components: {QuizAnswer},
data() {
return {
answers: [],
selectedAnswer: -1,
};
},
watch: {
selectedAnswer(newIndex, oldIndex) {
if (oldIndex > -1 && this.answers.length > oldIndex) {
// Reset old value
this.answers[oldIndex].selected = false;
}
if (newIndex > -1 && this.answers.length > newIndex) {
// Set new value
this.answers[newIndex].selected = true;
}
},
},
methods: {
answerSelected(index) {
this.selectedAnswer = index;
},
},
};