I'm building a Vue component that consists of an unspecified number of child components. Only one child component is visible at all times, and the user can only switch between child components when the one currently visible has emitted an is-valid event.
I want to keep this decoupled, such that children do not know about their parent and only communicate by emitting events. This also means that the children do not know their position within the parent component.
So, the parent component somehow has to keep track of which child the event came from. If the event came from the right child (the one currently visible) then some buttons are activated that allows the user to go to the next or previous child.
Here's my code so far:
HTML
<div id="app">
<template id="m-child">
<div>
<button v-on:click="setstate(true)">Valid</button>
<button v-on:click="setstate(false)">Invalid</button>
</div>
</template>
<template id="m-parent">
<div>
<m-child v-on:newstate="newchildstate"></m-child>
<m-child v-on:newstate="newchildstate"></m-child>
<m-child v-on:newstate="newchildstate"></m-child>
</div>
</template>
<m-parent></m-parent>
</div>
JS
Vue.component('m-child', {
template: '#m-child',
data: function() {
return {};
},
methods: {
setstate: function (valid) {
this.$emit('newstate', valid);
}
}
});
Vue.component('m-parent', {
template: '#m-parent',
methods: {
newchildstate: function (valid) {
console.log('valid:' + valid + ', but where from?');
}
}
});
new Vue({
el: '#app'
});
Of course I could hardcode an index on the child event binding:
<m-child v-on:newstate="newchildstate(0, $event)"></m-child>
<m-child v-on:newstate="newchildstate(1, $event)"></m-child>
<m-child v-on:newstate="newchildstate(2, $event)"></m-child>
But that would make the whole setup a lot less modular, I just want to be able to plug in a number of children in the DOM and make it work right away.
I've looked at the API for Vue events and there doesn't seem to be a way to get the source from the event object.
This depends on what you want to receive back, my personal preference is to pass in a prop to set a unique id and pass it back in the $emit:
<m-child v-on:newstate="newchildstate" :id="1"></m-child>
<m-child v-on:newstate="newchildstate" :id="2"></m-child>
<m-child v-on:newstate="newchildstate" :id="3"></m-child>
Then in your child component you can emit an object with the state and id the id:
Child:
this.$emit('newstate', {id: this.id, state: valid});
Parent:
newchildstate: function (valid) {
console.log('valid:' + valid.state + ', from' + valid.id);
}
I realise that this doesn't look hugely different from your hard coded example, but at some point your parent is going to want to deal with the event, so you could set up an array in data with the initial states and then use a v-for:
data: {
children: [true, false, false] // setup states
}
You would then do:
<div v-for="(state, index) in states">
<m-child v-on:newstate="newchildstate" :id="index"></m-child>
</div>
And in your view model:
methods: {
newchildstate: function(valid) {
this.$set(this.states, valid.id, valid.state);
}
}
Here's a JSFiddle that initiates the array dynamically via a prop and sets up the child components: https://jsfiddle.net/2y9727e2/
Related
I have a form fragment wrapped in a component that is hidden by v-if. When the user clicks a button, it toggles the boolean, revealing the hidden component, and when that happens I'd like to transfer focus to the first form input on the fragment.
I've tried using aria-live to no avail. I suspect the nature of the SPA interferes with the registration of those live regions (meaning my guess is that they must be registered when the page is rendered, as they don't seem responsive when injected into the DOM). I did not however, chase the answer down a rabbit hole so that is speculative. So then I added a class to the target input and tried to use HTMLElement.focus()
document.querySelector('.focus')[0].focus();
This also did not work. Does anyone know of a reason why I cannot seem to focus on an element that was recently injected into the DOM and is visible on the page at the time?
I think what's needed is for your form component to have something defined for when it's mounted:
Vue.config.productionTip = false;
const template = `<div>
<div>
<inner v-if="showInner" />
<button #click="toggle">Toggle inner component</button>
</div>
</div>`
const inner = {
name: 'inner',
template: '<form><input ref="textInput" type="text"/></form>',
mounted() {
this.$refs.textInput.focus()
}
};
new Vue({
template,
data: function() {
return {
showInner: false
};
},
methods: {
toggle() {
this.showInner = !this.showInner;
}
},
components: {
inner
}
}).$mount("#app");
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app"></div>
Is there any way to get computed data from child component to parent component? Because I'm sending the data from parent to child first and then I want to use the comuted property (data) in the parent component. I want to do that because I want to reuse that important component (child) in other components too.
I have a search input field for filtering my items, and when i wrote something down i wanna get back that list from the child component.
Parent component
<input class="form-control form-control-search m-input" autocomplete="off" type="text" v-on:input='testFunc()' v-model="search" placeholder="Search...">
<paginate-links v-if="items.length > 0" :models="items">
<div class="m-list-timeline__item no-timeline" v-for="item in filterItems" v-bind:key="item.id">
{{ item.title }}
</div>
</paginate-links>
Child component
props: ['item']
computed: {
filterItems () {
return filter // here goes my code
}
}
So can i get the filterItems in the parent component?
There are a couple ways you can send data back to the parent, but probably the easiest way I'd say is using an emit within the computed.
in child:
computed:{
myVal() {
let temp = this.num + 1;
this.$emit('onNumChange', temp);
return temp;
}
}
in parent template:
<my-child-component #onNumChange="changeHandler"/>
in parent script
methods: {
changeHandler(value) {
console.log('Value changed to: ', value);
}
}
You could do a similar thing with watch, or pass a function from parent as a prop that notifies the parent, but my recommended way would be to use vuex
https://vuex.vuejs.org/en/
I have following kind of code:
<div>
<compA />
<compB />
</div>
How do I make sure that first compA is rendered only after it compB is rendered.
Why I want is I have some dependency on few elements of compA, and style of compB depends on presence of those elements.
Why in details:
I have some complex UI design, where one box will become fixed when you scroll. SO It will not go above the screen when you scroll, it will be fixed once you start scrolling and it start touching the header. So I am using jquery-visible to find if a div with a particular id is visible on the screen, if it is not visible, I change the style and make that box fixed. Following code should give the idea what I am doing:
methods: {
onScroll () {
if ($('#divId').visible(false, false, 'vertical')) { // This is div from the compA, so I want to make sure it is rendered first and it is visible
this.isFixed = false
} else {
this.isFixed = true
}
}
},
mounted () {
window.addEventListener('scroll', this.onScroll() }
},
destroyed () {
window.removeEventListener('scroll', this.onScroll)
}
I dont want to make those in same component as one reason is it dont make sense as the nature of these components, and other I use compA at many places, while compB is specific to only one page. Also layout of these does not allow me to make compB child of compA as suggested in comments.
Any suggestions are welcome.
An option with events:
<!-- Parent -->
<div>
<comp-a #rendered="rendered = true"></comp-a>
<component :is="compB"></component>
</div>
<script>
// import ...
export default {
components: { CompA, CompB },
watch: {
rendered: function (val) {
if (val) this.compB = 'comp-b';
}
},
data() {
return {
rendered: false,
compB: null
}
}
}
</script>
<!-- Component B -->
<script>
export default {
mounted() {
this.$emit('rendered');
}
}
</script>
After going through the edit I realised that the dependency is not data driven but event driven (onscroll). I have tried something and looks like it works (the setTimeout in the code is for demonstration).
My implementation is slightly different from that of Jonatas.
<div id="app">
RenderSwitch: {{ renderSwitch }} // for demonstration
<template v-if='renderSwitch'>
<comp-a></comp-a>
</template>
<comp-b #rendered='renderSwitchSet'></comp-b>
</div>
When the component-B is rendered it emits an event, which just sets a data property in the parent of both component-A and component-B.
The surrounding <template> tags are there to reduce additional markup for a v-if.
The moment renderSwitch is set to true. component-a gets created.
I'm trying to select elements (divs with a specific class) within one instance of a React component when a click event is registered from within that same component. I don't want divs with that class to be selected from other instances of this component.
My issue is that, at any given time, there could be any number of instances of this component. Do I have to create a unique ID for each component when it's mounted, or is there some way React lets me reference to only elements found within a specific instance of a component? Can it be done using a callback function within a React class somehow?
For Example:
var MyClass = React.createClass({
makeDivsBigger: function() {
...? // How to only select the divs within the current instance?
},
render: function() {
<div className="container">
<div className="dark"></div>
<div className="light"></div>
<div className="bright"></div>
<button onClick={this.makeDivsBigger}></button>
</div>
}
});
There's a few ways to go about this, but one thing you rarely want to do is query the DOM to find elements to work on. Those elements are already inside your React universe so you don't need to find them!
A better approach would be to have the parent component control the size of the elements using internal state.
Here's one example implementation:
var MyClass = React.createClass({
getInitialState: function() {
return {
size: ''
};
},
makeDivsBigger: function() {
this.setState({
size: 'bigger'
})
},
render: function() {
<div className="container">
<div className={"dark " + this.state.size}></div>
<div className="light"></div>
<div className="bright"></div>
<button onClick={this.makeDivsBigger}></button>
</div>
}
});
// somewhere in your CSS
.bigger {
height: 500px;
}
Is there any way of detecting when the content of a component changes?
For example:
HTML:
<component>
<div>{{name}}</div>
</component>
JS:
component = Ractive.extend({
on...: function () {
// invoked when the inner HTML changes
}
});
I use {{yield}} so the content is rendered in the context of the parent.
For now I'll have to pass name to the component just for the purpose of observing changes (even though I don't need the value in the context of the component). (Or I'll add a function that I can call).
<component changes="{{name}}">
<div>{{name}}</div>
</component>
Any ideas?
It's possible, but a bit hacky.
http://plnkr.co/edit/YoTZpyTTyCyXijEteGkg?p=preview
<comp>
{{name}} hi {{thing}}
</comp>
comp = Ractive.extend({
template: '<div>{{yield}}</div>',
oncomplete: function() {
var self = this;
self.partials.content.forEach(function(partial) {
if (partial.r) {
self.parent.observe(partial.r, function(newValue) {
console.log(partial.r + ' changed to ', newValue)
}, {init: false});
}
});
}
})
Yield/Content are really just special partials, so this will loop through the items in that partial and set up an observer for each keypath.
This demo only works with simple expressions like {{foo}}. If you have more complicated things inside the partial you'll have to inspect the rendered partial to figure out what keypath you want to observe the parent on.