Vue.js - access data from root instance within component - javascript

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.

Related

How to pass data into Vue instance so that subsequent updates are reflected?

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>

rendering vue.js components and passing in data

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.

Passing data to components in vue.js

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>

Why is component being disposed in params change?

According to ko's component documentation on a component lifecycle:
If the component binding’s name value changes observably, or if an
enclosing control-flow binding causes the container element to be
removed, then any dispose function on the viewmodel is called just
before the container element is removed from the DOM
I am not sure why my component is being disposed on this fiddle.
<div data-bind='component: { name: "some-component", params: foo }'>
<p data-bind="text: name"></p>
</div>
function ComponentViewModel(params) {
}
ComponentViewModel.prototype.dispose = function() {
console.log('disposing...');
};
ko.components.register('some-component', {
viewModel: ComponentViewModel,
template : '<div></div>'
});
var rootvm = {
foo : ko.observable('1')
};
ko.applyBindings(rootvm);
setTimeout(function() {
rootvm.foo('2'); // this is disposing ComponentViewModel, why ??
}, 3000);
I can't see any of above's points in the documentation occurring on my fiddle. I certainly don't expect a component to be disposed and re-instantiated if the params injected change.
Any ideas why this is happening?
You are passing component parameters in the wrong way: KnockoutJs requires an object with keys and values, you are passing in an observable. I didn't dig into the details of why that ends up triggering disposal, but if you pass an object as it expects, the dispose function is not invoked anymore.
<div data-bind='component: { name: "some-component", params: {foo: foo} }'>
<p data-bind="text: name"></p>
</div>

How to fetch data from collections when the route/template consist on more than collection?

Example:
routes.js:
this.route("chapterPage", {
path: "/books/:bookId/chapters/:_id",
data: function() {
var chapter = Chapters.findOne(this.params._id);
var book = Books.findOne(this.params.bookId);
var chapters = Chapters.find({
bookId: this.params.bookId
}, {
sort: {
position: 1
}
});
return {
chapter: chapter,
book: book,
chapters: chapters
};
}
});
As you can see this template/route has two collections Book and Chapter. Previously, I used to call the collections individually like this:
chapter_form.js:
Template.chapterForm.events({
"input #input-content": function() {
var currentChapter = Session.get("currentChapter");
Chapters.update(currentChapter, {
$set: {
content: $("#input-content").html();
}
});
}
});
But now in my new route/template I can't do that since it isn't based on any collection:
chapter_page.js:
Template.chapterPage.events({
"input #input-content": function() {
console.log(chapter._id); // this returns is not defined
console.log(this._id); // this one too
}
});
How to get around this?
EDIT:
I also tried calling the chapter_form.html template:
<template name="chapterPage">
{{> chapterForm}}
</template>
But it doesn't display and shows stuff like: Cannot read property 'content' of undefined so it isn't recognizing the template.
There are two problems in your code.
First in the data function of the chapterPage route, you do not return the object containing your data.
// no return here in your question, need to do :
return {
chapter: chapter,
book: book,
chapters: chapters
};
Then in your event handler, you can access the data context using this, so the correct syntax to access the chapter or book id is this.chapter._id or this.book._id.
EDIT :
Inside templates route helpers and event handlers, this refers to the current data context assigned to the template.
There are several ways to assign a data context to a template.
You can use attribute="value" syntax along with template inclusion syntax.
{{> myTemplate param1="value1" param2="value2"}}
Template.myTemplate.helpers({
paramsJoined:function(){
return [this.param1,this.param2].join(",");
}
});
You may also use a helper value coming from the parent template data context :
<template name="parent">
{{> myTemplate someHelper}}
</template>
Template.parent.helpers({
someHelper:function(){
return {
param1:"value1",
param2:"value2"
};
}
});
If you don't specify a data context when using the template inclusion syntax, it is assumed to be inherited from the parent data context.
You can also use {{UI.dynamic}} (http://docs.meteor.com/#ui_dynamic) to specify a dynamic template name along with a dynamic data context.
{{> UI.dynamic template=Router.template data=Router.data}}
This is this kind of approach that iron:router is using to set dynamically the route data context of the route template (implementation is slightly more complex though).
Meteor provides utilities to access current data contexts as well as parent data contexts, which can be useful :
http://docs.meteor.com/#template_currentdata
http://docs.meteor.com/#template_parentdata

Categories

Resources