I have been working on an application using a comment function. That results in having to subscribe to both a collection which the comments are made on and the comments collection itself. Now it looks like this:
<template name="bookView">
{{> book}}
{{> comments}}
</template>
this.route('book', {
path: '/book/:_id',
template: 'bookView',
waitOn: function() { return Meteor.subscribe('book');},
action: function () {
if (this.ready()){
this.render();
}
else
this.render('loadingTemplate');
},
data: function() {return Books.findOne(this.params._id);}
});
But now I would like to load all comments belonging to that book also. Or should I handle the subscription of comments in Template.comments.rendered?
Yeah you have two ways:
Logic in Controller. You can subscribe with an array to multiple collections. This would be the way you go when you show all the comments instantly.
this.route('book', {
path: '/book/:_id',
template: 'bookView',
/* just subscribe to the book you really need, change your publications */
waitOn: function() {
return [Meteor.subscribe('book', this.params._id),
Meteor.subscribe('comments', this.params._id)];
},
data: function() {
return {
book : Books.findOne(this.params._id),
comments: Comments.find(this.params._id)}
}
});
If you dont want to show comments until they are requested by the user. You can follow another way:
You can set the bookId on buttonclick into a Session variable. Than you can define a Deps.autorun function which subscribes to the comments collection with the bookId provided in your Session variable. In your comments template you just have to do the normal collection request. If you need more hints about this way let me know.
Your waitOn function can wait for multiple subscriptions by returning an array of the subscription handles.
Related
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>
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
I want to implement a system that shows me the newest posts. For this I do not want to use the index action from the user as this is already taken for another post function but a "newest" action. It is showed on the index route with a {{ render "postNewest" }} call. I would prefer to load the data in the PostNewestController or PostNewestView instead of the route for abstraction reasons.
I tried two ideas to achieve this, but none worked so far:
create a custom adapter and add a findNewest() method: the findNewest() method is sadly not found when trying to call in the init method of the controller.
write the request directly into the init method and then update with store.loadMany(payload): data is successful request. However, I do not know how to access the data from the template and set the content of the controller.
Is there any way for this?
EDIT:
Here is the source code to better understand the problem:
PostModel.js
App.Post.reopenClass({
stream: function(items) {
var result = Ember.ArrayProxy.create({ content: [] });
var items = [];
$.getJSON("/api/v1/post/stream?auth_token=" + App.Auth.get("authToken"), function(payload) {
result.set('content', payload.posts);
});
return result;
}
});
PostStreamController.js
App.PostStreamController = Ember.ArrayController.extend({
init: function() {
this.set("content", App.Post.stream());
},
});
index.hbs
{{# if App.Auth.signedIn}}
{{ render "dashboard" }}
{{else}}
{{ render "GuestHeader" }}
{{/if}}
{{ render "postStream" }}
postStream.hbs
{{#each post in model}}
<li>{{#linkTo 'post.show' post data-toggle="tooltip"}}{{post.name}}{{/linkTo}}</li>
{{else}}
Nothing's there!
{{/each}}
PostShowRoute.js
App.PostShowRoute = Ember.Route.extend({
model: function(params) {
return App.Post.find(params.post_id);
},
setupController: function(controller, model) {
controller.set('content', model);
},
});
I Had this issue too. Just add init in your controller, and define the model you want to get there.
In your Controller
App.PostRecentController = Ember.ArrayController.extend({
init: function() {
return this.set('content', App.Post.find({
recent: true
}));
}
});
In your template
{{#each post in content}}
{{post.body}}
{{/each}}
I would recommend you check EMBER EXTENSION, it will give you a good idea of the naming, and see if everything is missing.
I figured out the problem. The Post model has a belongsTo relationship to another model. This relationship is not loaded by Ember so in the stream() method I have to load this manually. Then everything works as expected.
How do I pass data between two different routes and templates?
I have a javascript file on the front end (client folder) that simply calls Router.go() passing in the post ID as one of my parameters.
Below are the three main culprits (I believe). I've removed most of the code to make it easier to read. I can change to the PostDetail page with no problems. I can also retrieve the PostId on the PostDetail page from the Router. My problem is, the database entry (POLL) that is retrieved does not get rendered on the template. Hence {{Question}} is always blank even though the database entry is being returned.
Let me know if I should post more information.
FrontEnd.js
Template.PostTiles.events({
// When a choice is selected
'click .pin' : function(event, template) {
Router.go('Post', {_PostId: this.PostId});
}
});
post-detail.html
<template name="PostDetail">
<h3>{{Question}}</p>
</template>
Shared.js
Router.map( function() {
this.route('Home', {
path: '/',
template: 'PostTiles',
data: {
// Here we can return DB data instead of attaching
// a helper method to the Template object
QuestionsList: function() {
return POLL.find().fetch();
}
}
});
this.route('Post', {
template: 'PostDetail',
path: '/Post/:_PostId',
data: function() {
return POLL.findOne(this.params._PostId);
},
renderTemplates: {
'disqus': {to: 'comments'}
}
});
});
----- Update -----
I think I've narrowed down the issue to simply being able to render only one Database entry, instead of a list of them using the {{#each SomeList}} syntax.
Looks like you found the answer / resolved this, but just in case, I think it's in your findOne statement:
data: function() {
return POLL.findOne(this.params._PostId);
},
should read:
data: function() {
return POLL.findOne({_id:this.params._PostId});
},
(assuming that POLL has your posts listed by _id.
Hope that helps.
Could you pass the info in the Session? the docs for that are here http://docs.meteor.com/#session. That's what I'm planning on doing.
Hi I'm trying to wrap my head around backbone.js for some days now but since this is my first MVC Framework, it's pretty hard.
I can easily get my Collections to work, fetching data from the server etc, but it all depends on first "logging" in per API-Key. I just don't know how to model this with a good MVC approach. (btw: I can't use the Router/Controller because it's a Chrome Extension)
The Flow looks like this:
Start Extension
Is there an API-Key in localStorage?
No => Display a input field and a save button which saves the key to localStorage; Yes => proceed with the Application:
App......
The only way I could think of it is putting it all together in a big View... but I guess since I am fairly new to this there are surely some better approaches.
You can create a model that maintains the state of the user's login status and a view that renders a different template depending on that status. The view can show the "input field" template if the user is not logged in and a different template if the user is. I would keep all access to localStorage in the Model because the View should not be concerned with persistence. The view should probably not be concerned with the API Key as well, and that's why I have my view binding to the model's loggedIn change ('change:loggedIn') instead of apiKey change...although I am showing the API key in one of my templates for demonstration purpose only. Here's my very simplified sample. Note that I didn't bother with validating the API Key, but you'll probably want to:
Templates:
<script id="logged_in" type="text/html">
You're logged in. Your API key is <%= escape('apiKey') %>. Let's proceed with the application...
</script>
<script id="not_logged_in" type="text/html">
<form class="api_key">
API Key: <input name="api_key" type="text" value="" />
<button type="sumit">Submit</button>
</form>
</script>
Backbone Model and View:
var LoginStatus = Backbone.Model.extend({
defaults: {
loggedIn: false,
apiKey: null
},
initialize: function () {
this.bind('change:apiKey', this.onApiKeyChange, this);
this.set({'apiKey': localStorage.getItem('apiKey')});
},
onApiKeyChange: function (status, apiKey) {
this.set({'loggedIn': !!apiKey});
},
setApiKey: function(apiKey) {
localStorage.setItem('apiKey', apiKey)
this.set({'apiKey': apiKey});
}
});
var AppView = Backbone.View.extend({
_loggedInTemplate: _.template($('#logged_in').html()),
_notLoggedInTemplate: _.template($('#not_logged_in').html()),
initialize: function () {
this.model.bind('change:loggedIn', this.render, this);
},
events: {
'submit .api_key': 'onApiKeySubmit'
},
onApiKeySubmit: function(e){
e.preventDefault();
this.model.setApiKey(this.$('input[name=api_key]').val());
},
render: function () {
if (this.model.get('loggedIn')) {
$(this.el).empty().html(this._loggedInTemplate(this.model));
} else {
$(this.el).empty().html(this._notLoggedInTemplate(this.model));
}
return this;
}
});
var view = new AppView({model: new LoginStatus()});
$('body').append(view.render().el);