Form (parent) component:
export default {
template: `
<form name="form" class="c form" v-on:change="onChange">
<slot></slot>
</form>`,
methods: {
onChange:function(){
console.log("something changed");
}
}
};
and a c-tool-bar component (child) that is (if existing) inside the c-form's slot
export default {
template: `
<div class="c tool bar m006">
<div v-if="changed">
{{ changedHint }}
</div>
<!-- some other irrelevant stuff -->
</div>`,
props: {
changed: {
type: Boolean,
default: false,
},
changedHint: String
}
};
What I want to achieve is, that when onChange of c-form gets fired the function
checks if the child c-tool-bar is present && it has a changedHint text declared (from backend) it should change the c-tool-bar's prop "changed" to true and the c-bar-tool updates itself so the v-if="changed" actually displays the changedHint. I read the vue.js docs but I don't really know where to start and what the right approach would be.
You can use the two components like this:
<parent-component></parent-component>
You will be passing in the :changed prop a variable defined in the parent component in data(). Note the :, we are using dynamic props, so once the prop value is changed in the parent it will update the child as well.
To change the display of the hint, change the value of the variable being passed in as prop to the child component:
export default {
template: `
<form name="form" class="c form" v-on:change="onChange">
<child-component :changed="shouldDisplayHint"></child-component>
</form>`,
data() {
return {
shouldDisplayHint: false,
};
},
methods: {
onChange:function(){
this.shouldDisplayHint = true; // change the value accordingly
console.log("something changed");
}
}
};
One simple way is to use $refs.
When you will create your child component inside your parent component, you should have something like: <c-tool-bar ref="child"> and then you can use it in your parent component code:
methods: {
onChange:function(){
this.$refs.child.(do whatever you want)
}
}
Hope it helps but in case of any more questions: https://v2.vuejs.org/v2/guide/components.html#Child-Component-Refs
Related
I have a Vue compoment rendering inside index.vue
In index.vue we have some data :
data(){
return {
splashIsActive: true,
tutorialIsActive: false,
gameIsActive: false
}
}
and the component inside index.vue :
<SplashBox v-if="splashIsActive"/>
Then, if we go to splashBox there's a button.
<button #click="debug">....
Is there any way to create a method that after clicking the button sends, or changes splashIsActive to false, instead of true as it is stored in index.vue ?
You can achieve this by emitting an event from the button.
<button #click="$emit('updateSplashState', false)">...</button>
Then, You can capture this event in parent component.
<SplashBox v-if="splashIsActive" #updateSplashState="splashIsActive = $event"/>
Live Demo :
Vue.component('splashbox', {
template: `<button #click="$emit('update-splash-state', false)">Update</button>`
});
var app = new Vue({
el: '#app',
data: {
splashIsActive: true
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<SplashBox v-if="splashIsActive" #update-splash-state="splashIsActive = $event"></SplashBox>
</div>
You need to emit an event to the parent.
<button #click="debug">....
methods: {
debug() {
this.$emit('setSplashUnActive', false)
}
}
In the parent component.
<SplashBox v-if="splashIsActive" #setSplashUnActive="setSplashUnActive" />
methods: {
setSplashUnActive(value) {
this.splashIsActive = value
}
}
You can use a prop to pass the value from parent to child. Within the child component on pressing the key you can emit an event to update its value. To do this it is necessary to indicate the prop as sync:
<SplashBox: splashUnActive.sync="initValue"></Checkbox>
and then inside SplashBox on click of the button:
this.$emit('update:splashUnActive.', newValue)
Here's sample of my child component
HTML:
<div v-for="(input, index) in form.inputs" :key="index">
<div>
<input :name"input.name" :type="input.type" />
</div>
</div>
JavaScript (Vue):
<script>
export default {
name: "child",
props: ['parentForm'],
data() {
return {
form: {
inputs: [
{
name: 'name',
type: 'text'
],
[...]
}
}
}
And sample of root component
HTML:
<child :parentsForm="form"></child>
JavaScript (Vue):
<script>
import child from "./child";
export default {
name: "root",
components: { child },
data() {
return {
form: {
data: {
name: null,
email: null,
...
}
}
}
The question is, how do I achieve combining root + v-for?
Example I want to using child component this way
<input :name"input.name" :type="input.type" v-model="parentForm.data . input.name" />
Since parentForm.data will bind form:data:{ and this will be the variable get from input.name }
Output in v-model should be bind form.data.name or form.data.email on root component
Thank you
You can use it as per follow,
<input :name="input.name" :type="input.type" v-model="parentForm.data[input.name]" />
This will bind parentForm.data.name for input.name = 'name' to v-model.
If I understood you correctly, you want to update parent data from your child component. If yes then you have two options.
In you child component use $parent.form.data to bind.
Or you can pass it down as prop assign it to a data property in child. Bind this new data property in your child and emit it whenever any changes are made. Receive this emit in your parent and update the parent property respectively (Recommended)
I'm trying to figure out the Vue-way of referencing children from the parent handler.
Parent
<div>
<MyDropDown ref="dd0" #dd-global-click="ddClicked"></MyDropDown>
<MyDropDown ref="dd1" #dd-global-click="ddClicked"></MyDropDown>
<MyDropDown ref="dd2" #dd-global-click="ddClicked"></MyDropDown>
</div>
export default {
methods: {
ddClicked: function(id) {
console.log("I need to have MyDropDown id here")
}
}
}
Child
<template>
<h1>dropdown</h1>
<Button #click="bclick"></Button>
</template>
export default {
methods: {
bclick: function() {
this.$emit('dd-global-click')
}
}
}
In the parent component I need to see which dropdown was clicked.
What I've tried so far
I tried to set "ref" attribute in the parent. But I can't refer to this prop within the child component. Is there a way to do it? There is nothing like this.ref or this.$ref property.
I tried to use $event.targetElement in the parent, but it looks like I'm mixing Real DOM and Vue Components together. $event.targetElement is a DOM like . So in the parent I have to go over the tree until I find my dropdown. It is ugly I guess.
I set an additional :id property for the dropdown making it the copy of the 'ref' property. In the blick and I called this.$emit('dd-global-click', this.id). Later in the parent I check this.$refs[id]. I kind of works, but I'm not really content with it, because I have to mirror attributes.
Using the _uid property didn't work out either. On top of that, I think, that since it starts with an underscore it is not a recommended way to go.
It seems like a very basic task, so there must be a simplier way to achieve this.
If this custom dropdown element is the top level one (the root element) in the component, you could access the native DOM attributes (like id, class, etc) via this.$el, once it's mounted.
Vue.component('MyDropdown', {
template: '#my-dropdown',
props: {
items: Array
},
methods: {
changed() {
this.$emit('dd-global-click', this.$el.id);
}
}
})
new Vue({
el: '#app',
data: () => ({
items: [
{
id: 'dropdown-1',
options: ['abc', 'def', 'ghi']
},
{
id: 'dropdown-2',
options: ['jkl', 'lmn', 'opq']
},
{
id: 'dropdown-3',
options: ['rst', 'uvw', 'xyz']
}
]
}),
methods: {
ddClicked(id) {
console.log(`Clicked ID: ${id}`);
}
}
})
Vue.config.devtools = false;
Vue.config.productionTip = false;
<script src="https://cdn.jsdelivr.net/npm/vue#2.6.11"></script>
<div id="app">
<my-dropdown
v-for="item of items" :key="item.id"
:id="item.id"
:items="item.options"
#dd-global-click="ddClicked">
</my-dropdown>
</div>
<script id="my-dropdown" type="text/x-template">
<select #input="changed">
<option v-for="item of items" :key="item" :value="item">
{{item}}
</option>
</select>
</script>
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
Problem
Let's say I have a vue component called:
Note: All vue components has been simplified to explain what I'm trying to do.
reusable-comp.vue
<template>
<div class="input-group input-group-sm">
<input type="text" :value.number="setValue" class="form-control" #input="$emit('update:setValue', $event.target.value)">
<span>
<button #click="incrementCounter()" :disabled="disabled" type="button" class="btn btn-outline-bordercolor btn-number" data-type="plus">
<i class="fa fa-plus gray7"></i>
</button>
</span>
</div>
</template>
<script>
import 'font-awesome/css/font-awesome.css';
export default {
props: {
setValue: {
type: Number,
required: false,
default: 0
}
},
data() {
return {
}
},
methods: {
incrementCounter: function () {
this.setValue += 1;
}
}
}
</script>
Then in a parent component I do something like this:
subform.vue
<div class="row mb-1">
<div class="col-md-6">
Increment Value of Num A
</div>
<div class="col-md-6">
<reuseable-comp :setValue.sync="numA"></reuseable-comp>
</div>
</div>
<script>
import reusableComp from '../reusable-comp'
export default {
components: {
reusableComp
},
props: {
numA: {
type: Number,
required: false,
default: 0
}
},
data() {
return {
}
}
</script>
then lastly
page_layout.vue
<template>
<div>
<subform :numA.sync="data1" />
</div>
</template>
<script>
import subform from '../subform.vue'
export default {
components: {
subform
},
data() {
return {
data1: 0
}
}
}
</script>
Question
So, how do I sync a value between reusable-comp.vue, subform.vue, and page_layout.vue
I'm using reuseable-comp.vue is many different places. I'm using subform.vue only a couple times in page_layout.vue
And I'm trying to use this pattern several times. But I can't seem to get this to work. The above gives me an error:
Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "numA"
Okay I found a solution that worked.
In subform.vue, we change:
data() {
return {
numA_data : this.numA
}
}
So we now have reactive data to work with. Then in the template, we refer to that reactive data instead of the prop:
<reuseable-comp :setValue.sync="numA_data"></reuseable-comp>
Then finally we add a watcher to check if the reactive data gets changed, and then emit to the parent:
watch: {
numA_data: function(val) {
this.$emit('update:numA', this.numA_data);
}
}
Now all values from grandchildren to parent are synced.
Update (4/13/2018)
I made new changes to the reusable-comp.vue:
I replaced where it says 'setValue' to 'value'
I replaced where it says 'update:value' to 'input'
Everything else says the same.
Then in subform.vue:
I replaced ':setValue.sync' to 'v-model'
v-model is two way binding, so I made use of that where it needed to be. The sync between the parent-child (not child to grandchild), is still using sync modifier, only because the parent has many props to pass. I could modify this where I could group up the props as a single object, and just pass that.