I'm having trouble figuring out how to render a parent component, display a list of contracts in a list on part of the page, and when a user clicks on one of them, display the details of that specific contract on the other part of the page.
Here is my slim file:
#contracts_area
.filter-section
ul
li.filter-item v-for="contract in contractsAry" :key="contract.id" #click="showContract(contract)"
| {{ contract.name }}
.display-section
component :is="currentView" transition="fade" transition-mode="out-in"
script type="text/x-template" id="manage-contracts-template"
div
h1 Blank when page is newly loaded for now
script type="text/x-template" id="view-contract-template"
div :apply_contract="showContract"
h1#display-item__name v-name="name"
javascript:
Vue.component('manage-template', {
template: '#manage-contracts-template'
});
Vue.component('view-contract', {
template: '#view-contract-template',
props: ['show_contract'],
data: function() {
return {
name: ''
}
},
methods: {
showContract: function(contract) {
return this.name = contract.name
}
}
});
Vue.http.headers.common['X-CSRF-Token'] = $('meta[name="csrf-token"]').attr('content');
var contractsResource = Vue.resource('/all_contracts{/id}.json');
var contracts = new Vue({
el: '#contracts_area',
data: {
currentView: 'manage-template',
contractsAry: [],
errors: {}
},
mounted: function() {
var that = this;
contractsResource.get().then(
function(res) {
that.contractsAry = res.data;
}
)
},
methods: {
showContract: function(contract) {
this.currentView = 'view-contract'
}
}
});
Basically I'd like it so that when a user clicks on any contract item in the .filter-section, it shows the data for that contract in the .display-section. How can I achieve this?
In short you can bind a value to a prop.
.display-section
component :is="currentView" :contract="currentContract"
view-contract
props: ['contract']
contracts-area
data: {
currentContract: null,
},
methods: {
showContract: function(contract) {
this.currentView = "view-contract";
this.currentContract = contract;
}
}
There are multiple ways to pass data in Vue.
Binding values to props.
Using ref to directly call a method from a child component.
Custom Events. Note that to pass events globally, you will need a global event bus.
A single central source of truth (i.e. vuex)
I have illustrated methods 1, 2, 3 in Codepen
Note that 2nd and 3rd methods will only work after your component has been rendered. In your case, since your components for currentView are dynamic and when user clicked, display-section component does not yet exists; it will not receive any events yet. So their content will be empty at first.
To workaround this you can directly access $parent in mounted() from child component, however this would create coupling between them. Another solution is creating the components but conditionally displaying them. And one another solution would be waiting until child component has been mounted and then emitting events.
If your needs are simple I suggest binding values to props (1), else you may consider using something like vuex.
Related
I have a Vue app which get an html from API that contains some code like <div class="play-video"> <input type="text" class="input-1"/></div>
Calling the API with axios via a promise, it is inserted into the dom something like:
<div v-if="content" v-html="content"></div>
How can I bind on change events to the children inputs with the .input-1 class?
You could query the container for those inputs, and add an event handler to each:
Apply a template ref (named container) on the container div:
<div ref="container">
Add a watcher on content that queries the container (via this.$refs.container.querySelectorAll()) for <input class="input-1">, and adds an event handler to each input. Note that the handler needs to wait until $nextTick(), after which the v-html directive would have had a chance to update.
export default {
watch: {
content: {
async handler(content) {
// wait til v-html directives takes effect
await this.$nextTick()
this.$refs.container
.querySelectorAll('.input-1')
.forEach((input) => input.addEventListener('change', this.onInputChange))
},
immediate: true,
},
},
methods: {
onInputChange(e) {
console.log('input change')
},
},
}
demo
There are two approaches that rise to the top for me:
From inside a vue app, use vanilla javascript to append those nodes to the DOM and then query for their inputs and add event handlers. Since you're inside the Vue app you can write the handlers to interact with Vue components. Or you could probably do something fancy with vuex getters returning those elements (if they're still attached to the document).
Use Vue.compile to create render functions from the html or add an async omponent. However, this will only work if you have a robust way to add #change="doSomething" to the template. For this reason, I'd probably should go with option 1) unless you control the source of the templates.
Whichever you do, keep malicious injections in mind.
const someHtml = '<div class="play-video"><input type="text" class="input-1"/></div>'
var app = new Vue({
el: '#app',
mounted() {
const dynamicContent = document.createElement('div');
dynamicContent.innerHTML = someHtml;
dynamicContent.querySelector('input').addEventListener('change',
e => this.inputValue = e.target.value);
this.$refs.container.appendChild(dynamicContent);
},
data() {
return {
name: 'Vue',
inputValue: ''
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<div>type something below then click this text</div>
<div ref="container"></div>
<div>The user entered: {{inputValue}}</div>
</div>
I'm trying to dynamically update a Vue JS prop after the view has been loaded and the custom has been initialised. I'm building a custom Vue plugin and am using props to pass options, one of which is a object which I need to dynamically update the value passed after the component has loaded, e.g:
<div id="app">
<script>
var seedData = {
percent: 50,
name: 'Smith'
}
setInterval(() => {
seedData = {
percent: Math.random(),
name: 'Smith'
}
}, 1000)
</script>
<offers :parent-data="seedData"></offers>
</div>
Vue.component('offers', {
template: '<h1>Parent Data: {{ parentData.percent }}</h1>',
props: {
parentData: {
default: () => ({
percent: 0,
name: 'John'
}),
type: Object
},
}
});
// create a new Vue instance and mount it to our div element above with the id of app
var vm = new Vue({
el: '#app'
});
This will load the initial name/values from offersData, however, the new values on the setInterval doesn't get passed through.
I've tried adding a watcher inside of my custom Vue plugin that gets loaded through <offers> but this doesn't seem to work either:
watch: {
parentData: function (newVal) {
this.parentData = newVal
}
}
UPDATE
The following is my implementation:
Code Pen -> https://codepen.io/sts-ryan-holton/pen/VwYNzdZ
There are multiple problems with your code
Everything inside <div id="app"> is treated by the Vue as a template and compiled - see the docs
If neither render function nor template option is present, the in-DOM HTML of the mounting DOM element will be extracted as the template. In this case, Runtime + Compiler build of Vue should be used.
Including <script> tag there is wrong. Just try to include vue.js (debug build) instead of vue.min.js (minified production build of Vue) and you will see bunch of errors (btw its always good idea to use debug build for development as it gives you lots of useful errors and warnings)
The fact that it "somehow works" in prod build (ie the initial values are shown on the page) doesn't mean it's supported...
So the inside of <div id="app"> is template for Vue. As I said before in the comments, all data referenced by template must be in the context of Vue instance. You cannot pass some global variable into props. So moving <script> outside of <div id="app"> won't help
[Vue warn]: Property or method "seedData" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initialising the property.
What you can do is to pass seedData object into root Vue instance like this:
var vm = new Vue({
el: '#app',
data: {
seedData: seedData
}
});
Now the errors are gone but data changes is not reflected still. Reason for that is not Vue specific. Its simple JavaScript. Object are passed by reference in JS. Look at this code:
var a = { name: 'John' }
var b = a
a = { name: 'Smith' }
// b is still pointing to "John" object
To workaround it, don't replace whole object. Just mutate it's properties (beware of Vue reactivity caveats)
setInterval(function() {
seedData.name = 'John';
seedData.percent = Math.random();
}, 1000)
Whole solution:
Vue.component('offers', {
template: '<h1>{{ parentData.name }}: {{ parentData.percent }}</h1>',
props: {
parentData: {
default: () => ({
percent: 0,
name: 'John'
}),
type: Object
},
}
});
// create a new Vue instance and mount it to our div element above with the id of app
var vm = new Vue({
el: '#app',
data: {
seedData: seedData
}
});
<script>
var seedData = {
percent: 60,
name: 'Smith'
}
setInterval(function() {
seedData.name = 'John';
seedData.percent = Math.random();
}, 1000)
</script>
<div id="app">
<offers :parent-data="seedData"></offers>
</div>
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
I'm programming a page that displays a list of meetings in a table. It's also possible to edit and delete meetings. Now I'd like to offer an alternative view using VCalendar.
Data is received from the server on page load and stored in a JS variable. Both the Vue instance containing the table and the VCalendar component share this data. If I edit a table cell, the changes are reflected in the component. But when I delete a date in the table view, it remains in the calendar.
This is the relevant HTML (edit: Added some attributes to the td):
<calendar-component></calendar-component>
<table id='meetings-table'>
<tr v-for='meeting in meetings' :key='date.id'>
<td contenteditable #blur='handleInput($event,meeting,"name")>
#{{ meeting.name }}
</td>
<td>
<input type='checkbox' v-model='selected'
:value='meeting.id'>
</td>
</tr>
</table>
<div>
<button v-if='selected.length' #click='deleteMeetings'>
Delete selected rows
</button>
</div>
My JS (edit: Added handleInput method):
let table = new Vue({
el:'#meetings-table',
data: {
selected: [],
meetings: window.meetings,
},
methods: {
/**
* Deletes selected meetings.
*/
deleteMeetings: function () {
let requests = [];
// Make a single request and store it
for (let id of this.selected) {
requests.push(axios.delete('/termine/' + id)
.then(response => {
// Remove meetings
this.meetings = this.meetings.filter(t => t.id != id);
// Remove id from list of selected meetings
this.selected = this.selected.filter(elem => elem != id);
}));
}
const axiosArray = axios.all(requests);
},
/**
* Handles edits in table cells.
*/
handleInput: function($event, meeting, field) {
const newValue = $event.target.textContent;
// Update value in $data
meeting[field] = newValue;
// AJAX request follows, but is not necessary for this example to work
}
}
});
The relevant parts of the component:
<template>
<v-calendar :attributes='attributes'>
<div
slot='meeting-row'
slot-scope='{ customData }'>
<!-- Popover content omitted -->
</div>
</v-calendar>
</template>
<script>
let meetings = window.meetings;
export default {
data() {
return {
incId: meetings.length,
editId: 0,
meetings,
};
},
computed: {
attributes() {
return [
// Today attribute
{
// ...
},
// Meeting attributes
...this.meetings.map(meeting => ({
key: meeting.id,
dates: new Date('2018,11,31'),// moment(meeting.slot.date, 'DD.MM.YY').format('YYYY, MM, DD'), //meeting.dates,
customData: meeting,
order: meeting.id,
dot: {
backgroundColor: '#ff8080',
},
popover: {
// Matches slot from above
slot: 'meeting-row',
}
}))
];
}
}
};
</script>
This is what happens:
I load the page containing only a single meeting. The meeting is
shown both in the table and the calendar component. Vue devtools show
it in both meetings arrays (in the component as well as in the other
Vue instance). Using the console, I can also see it in
window.meetings.
After clicking the delete button (triggering the deleteMeetings method in my JS), the meeting is gone from the table, but remains in
the calendar, in the component's meetings array and in
window.meetings.
What do I have to change to keep the meetings arrays in sync even when deleting a meeting in the table? Note that I haven't yet implemented any methods for the calendar component.
Calendar, and table components should share a single state: currently selected meetings. From what I understand, right now you have that state in 2 separate places: table Vue instance, and a calendar-component, which is a child of some other Vue instance.
It may look like you're sharing the state already (with window.meetings), but it's not the case: you only initialize the same set of meetings when the components are created. And then changes in one component are not reflected in another component.
What you can try to do is to have meetings stored in the 'main' Vue app on your page, pass them as props to table and calendar components, and then trigger events from table and calendar components, when meetings array is modified. You should also define the event hanlders in the 'main' Vue app, and listen on components. A rough sketch of the solution:
<div id="app">
<table-component
:meetings="meetings"
#meetingUpdated="handleMeetingUpdate"
#meetingDeleted="handleMeetingDeletion"
></table-component>
<calendar-component
:meetings="meetings"
#meetingUpdate="handleMeetingUpdate"
#meetingDeleted="handleMeetingDeletion"
></calendar-component>
</div>
let app = new Vue({
el:'#app',
data: {
meetings: []
},
methods: {
handleMeetingUpdate(event) {
//
},
handleMeetingDeletion(event) {
//
},
}
//
});
I hope the above is enough to point you in the right direction. If not, please let me know, and I'll do my best to help you with this further.
This seems like a fairly basic question but I can't seem to find a definitive (or even working) answer.
I have my root instance:
var vm = new Vue({
el: '#app',
// Data
data: {
events: {}
},
// Methods
methods: {
fetchEvents: function(){
this.$http.get('/api/events').success(function(theseEvents) {
this.$set('events', theseEvents);
}).error(function(error) {
});
}
},
ready: function(){
this.fetchEvents();
}
});
And I have a separate component in which I want to list out the events that are stored in the root instance. At the moment it looks like this:
var EventsList = Vue.extend({
template: '<ul><li v-for="event in events"><h2>{{ event.title }}</h2><p>{{ event.body }}</p></li></ul>',
data: function(){
return {
events: {}
}
},
methods: {
syncEvents: function(){
this.$set('events', this.$parent.events);
}
},
// When ready...
ready: function(){
this.syncEvents();
}
}
This doesn't seem to work. I have also tried this.$root.events to no avail. What is the correct way to go about this? Bear in mind I want to reference the data from the root, not create a copy with its own scope.
EDIT: trying to use props, here is the list component, which is also not working:
var EventsList = Vue.extend({
template: '<ul><li v-for="event in events"><h2>{{ event.title }}</h2><p>{{ event.body }}</p></li></ul>',
props: ['events']
}
Using props, you can easily pass the same data from parent to children. Since I don't know how you linked the root instance and the EventList together, I'm going to assume you registered it as a global component.
The documentation states:
Note that if the prop being passed down is an Object or an Array, it is passed by reference. Mutating the Object or Array itself inside the child will affect parent state, regardless of the binding type you are using.
So you will be using the same object throughout all your components when you pass it as a prop.
var vm = new Vue({
el: '#app',
// Data
data: {
events: {}
},
// Methods
methods: {
fetchEvents: function(){
this.$http.get('/api/events').success(function(theseEvents) {
this.$data.events = theseEvents; // You don't need to use $set here
}).error(function(error) {
});
}
},
ready: function(){
this.fetchEvents();
}
});
EventList:
var EventsList = Vue.extend({
template: '<ul><li v-for="event in events"><h2>{{ event.title }}</h2><p>{{ event.body }}</p></li></ul>',
data: function(){
return {
}
},
props: {
events: Object, // assuming that your prop is an object
},
}
// Register the vue component globally, if you want to:
Vue.component('eventlist', EventsList);
In the root vue instance template, you can pass the root vue instances events as a property called events in the child component:
<div class="app">
<!-- events is passed using the :events prop -->
<eventlist :events="events">
</eventlist>
</div>
That's what "Props" are for:
http://vuejs.org/guide/components.html#Props
If you pass an object/array as a prop (what your events data will surely be), it's two-way syncing automatically - change events in the child, they are changed in the parent.
If you pass simple values (strings, numbers - e.g. only event.name) via props, you have to explicitly use the .sync modifier: http://vuejs.org/guide/components.html#Prop_Binding_Types
this.$root.events
that will reference the root component, "that is the main.js or index.js or app.js" no matter the component you are in, using props requires that you pass it down each component supposing you want to use it in a third child component.
I'm struggling to understand how to pass data between components in vue.js. I have read through the docs several times and looked at many vue related questions and tutorials, but I'm still not getting it.
To wrap my head around this, I am hoping for help completing a pretty simple example
display a list of users in one component (done)
send the user data to a new component when a link is clicked (done) - see update at bottom.
edit user data and send it back to original component (haven't gotten this far)
Here is a fiddle, which fails on step two: https://jsfiddle.net/retrogradeMT/d1a8hps0/
I understand that I need to use props to pass data to the new component, but I'm not sure how to functionally do it. How do I bind the data to the new component?
HTML:
<div id="page-content">
<router-view></router-view>
</div>
<template id="userBlock" >
<ul>
<li v-for="user in users">{{user.name}} - <a v-link="{ path: '/new' }"> Show new component</a>
</li>
</ul>
</template>
<template id="newtemp" :name ="{{user.name}}">
<form>
<label>Name: </label><input v-model="name">
<input type="submit" value="Submit">
</form>
</template>
js for main component:
Vue.component('app-page', {
template: '#userBlock',
data: function() {
return{
users: []
}
},
ready: function () {
this.fetchUsers();
},
methods: {
fetchUsers: function(){
var users = [
{
id: 1,
name: 'tom'
},
{
id: 2,
name: 'brian'
},
{
id: 3,
name: 'sam'
},
];
this.$set('users', users);
}
}
})
JS for second component:
Vue.component('newtemp', {
template: '#newtemp',
props: 'name',
data: function() {
return {
name: name,
}
},
})
UPDATE
Ok, I've got the second step figured out. Here is a new fiddle showing the progress: https://jsfiddle.net/retrogradeMT/9pffnmjp/
Because I'm using Vue-router, I don't use props to send the data to a new component. Instead, I need set params on the v-link and then use a transition hook to accept it.
V-link changes see named routes in vue-router docs:
<a v-link="{ name: 'new', params: { name: user.name }}"> Show new component</a>
Then on the component, add data to the route options see transition hooks:
Vue.component('newtemp', {
template: '#newtemp',
route: {
data: function(transition) {
transition.next({
// saving the id which is passed in url
name: transition.to.params.name
});
}
},
data: function() {
return {
name:name,
}
},
})
-------------Following is applicable only to Vue 1 --------------
Passing data can be done in multiple ways. The method depends on the type of use.
If you want to pass data from your html while you add a new component. That is done using props.
<my-component prop-name="value"></my-component>
This prop value will be available to your component only if you add the prop name prop-name to your props attribute.
When data is passed from a component to another component because of some dynamic or static event. That is done by using event dispatchers and broadcasters. So for example if you have a component structure like this:
<my-parent>
<my-child-A></my-child-A>
<my-child-B></my-child-B>
</my-parent>
And you want to send data from <my-child-A> to <my-child-B> then in <my-child-A> you will have to dispatch an event:
this.$dispatch('event_name', data);
This event will travel all the way up the parent chain. And from whichever parent you have a branch toward <my-child-B> you broadcast the event along with the data. So in the parent:
events:{
'event_name' : function(data){
this.$broadcast('event_name', data);
},
Now this broadcast will travel down the child chain. And at whichever child you want to grab the event, in our case <my-child-B> we will add another event:
events: {
'event_name' : function(data){
// Your code.
},
},
The third way to pass data is through parameters in v-links. This method is used when components chains are completely destroyed or in cases when the URI changes. And i can see you already understand them.
Decide what type of data communication you want, and choose appropriately.
The best way to send data from a parent component to a child is using props.
Passing data from parent to child via props
Declare props (array or object) in the child
Pass it to the child via <child :name="variableOnParent">
See demo below:
Vue.component('child-comp', {
props: ['message'], // declare the props
template: '<p>At child-comp, using props in the template: {{ message }}</p>',
mounted: function () {
console.log('The props are also available in JS:', this.message);
}
})
new Vue({
el: '#app',
data: {
variableAtParent: 'DATA FROM PARENT!'
}
})
<script src="https://unpkg.com/vue#2.5.13/dist/vue.min.js"></script>
<div id="app">
<p>At Parent: {{ variableAtParent }}<br>And is reactive (edit it) <input v-model="variableAtParent"></p>
<child-comp :message="variableAtParent"></child-comp>
</div>
I think the issue is here:
<template id="newtemp" :name ="{{user.name}}">
When you prefix the prop with : you are indicating to Vue that it is a variable, not a string. So you don't need the {{}} around user.name. Try:
<template id="newtemp" :name ="user.name">
EDIT-----
The above is true, but the bigger issue here is that when you change the URL and go to a new route, the original component disappears. In order to have the second component edit the parent data, the second component would need to be a child component of the first one, or just a part of the same component.
The above-mentioned responses work well but if you want to pass data between 2 sibling components, then the event bus can also be used.
Check out this blog which would help you understand better.
supppose for 2 components : CompA & CompB having same parent and main.js for setting up main vue app. For passing data from CompA to CompB without involving parent component you can do the following.
in main.js file, declare a separate global Vue instance, that will be event bus.
export const bus = new Vue();
In CompA, where the event is generated : you have to emit the event to bus.
methods: {
somethingHappened (){
bus.$emit('changedSomething', 'new data');
}
}
Now the task is to listen the emitted event, so, in CompB, you can listen like.
created (){
bus.$on('changedSomething', (newData) => {
console.log(newData);
})
}
Advantages:
Less & Clean code.
Parent should not involve in passing down data from 1 child comp to another ( as the number of children grows, it will become hard to maintain )
Follows pub-sub approach.
I've found a way to pass parent data to component scope in Vue, i think it's a little a bit of a hack but maybe this will help you.
1) Reference data in Vue Instance as an external object (data : dataObj)
2) Then in the data return function in the child component just return parentScope = dataObj and voila. Now you cann do things like {{ parentScope.prop }} and will work like a charm.
Good Luck!
I access main properties using $root.
Vue.component("example", {
template: `<div>$root.message</div>`
});
...
<example></example>
A global JS variable (object) can be used to pass data between components. Example: Passing data from Ammlogin.vue to Options.vue. In Ammlogin.vue rspData is set to the response from the server. In Options.vue the response from the server is made available via rspData.
index.html:
<script>
var rspData; // global - transfer data between components
</script>
Ammlogin.vue:
....
export default {
data: function() {return vueData},
methods: {
login: function(event){
event.preventDefault(); // otherwise the page is submitted...
vueData.errortxt = "";
axios.post('http://vueamm...../actions.php', { action: this.$data.action, user: this.$data.user, password: this.$data.password})
.then(function (response) {
vueData.user = '';
vueData.password = '';
// activate v-link via JS click...
// JSON.parse is not needed because it is already an object
if (response.data.result === "ok") {
rspData = response.data; // set global rspData
document.getElementById("loginid").click();
} else {
vueData.errortxt = "Felaktig avändare eller lösenord!"
}
})
.catch(function (error) {
// Wu oh! Something went wrong
vueData.errortxt = error.message;
});
},
....
Options.vue:
<template>
<main-layout>
<p>Alternativ</p>
<p>Resultat: {{rspData.result}}</p>
<p>Meddelande: {{rspData.data}}</p>
<v-link href='/'>Logga ut</v-link>
</main-layout>
</template>
<script>
import MainLayout from '../layouts/Main.vue'
import VLink from '../components/VLink.vue'
var optData = { rspData: rspData}; // rspData is global
export default {
data: function() {return optData},
components: {
MainLayout,
VLink
}
}
</script>