Meteor: Subscribed Colletion doesn't update automatically (Reactivity) - mongodb

I'm facing the problem that adding data to a subscribed Collection doesn't automatically refresh the shown elements of a collection. If I add a new element the element show's up for a second and then disappears! Refreshing the browser (F5) and the new element shows up.
I put the subscription into Meteor.autorun but things kept beeing the same.
lists.html (client):
<<template name="lists">
<div class="lists col-md-12" {{!style="border:1px red solid"}}>
<!-- Checklist Adder -->
<form id="list-add-form" class="form-inline" role="form" action="action">
<div class="col-md-6">
<input class="form-control" id="list-name" placeholder="Neue Liste" required="required"/>
</div>
<button type="submit" class="btn btn-primary" id="submit-add">
<span class="glyphicon glyphicon-plus-sign"></span>
Neue Liste
</button>
</form>
<!-- Checklist Ausgabe -->
<ul>
<br/>
{{#each lists}}
<li style="position: relative;" id="{{this._id}}" data-id="{{_id}}" class="clickOnList">
<!--<input type="button" class="deleteLists" id="{{this._id}}" value="-" style="z-index: 999;"/> -->
<span id="{{this._id}}" data-id="{{_id}}" style="padding-left: 10px; vertical-align:middle;">{{this.name}}</span>
<form id="changerForm_{{_id}}" class="changeList-name-form" data-id="{{_id}}" style="visibility: hidden; position: absolute; top:0;">
<input id="changerText_{{_id}}" type="text" class="list_name" data-id="{{_id}}" value="{{this.name}}" />
</form>
{{#if ownerOfList this._id}}
<a data-toggle="modal" class="userForListModal" id="{{this.name}}" data-id="{{this._id}}" data-target="#userForListModal">
<span class="glyphicon glyphicon-user" id="{{this.name}}" style="color:black;"data-id="{{this._id}}"></span><span style="color:black;" id="{{this.name}}" data-id="{{this._id}}" style="font-size: small; vertical-align: super">{{memberCount this._id}}</span></a>
<div class="deleteLists" id="dLBtn_{{_id}}" data-id="{{this._id}}" style="float: right; padding-right: 5px; padding-top: 1px; visibility: hidden;">
<span class="deleteLists glyphicon glyphicon-minus-sign" data-id="{{this._id}}"></span>
</div>
{{else}}
<a class="userForListModal">
<span class="glyphicon glyphicon-user" style="color:black;"></span><span style="color:black;" style="font-size: small; vertical-align: super">{{memberCount this._id}}</span></a>
{{/if}}
<!-- <button type="submit" class="deleteLists btn btn-default btn-xs" id="dLBtn_{{_id}}" data-id="{{this._id}}" style="float: right;" > -->
</button>
</li>
{{/each}}
</ul>
</div>
<div class="modal fade" id="userForListModal" >
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h4 class="modal-title" id="userForListModalLabel"></h4>
</div></template>
<div class="modal-body col-md-12">
<div id="userForListModalUsers">
</div>
<p>Neuen Benutzer zur Checkliste hinzufügen:</p>
<form id="list-addUser-form" class="form-inline" role="form" action="action">
<div class="col-md-12">
<div class="col-md-6">
{{inputAutocomplete settings id="name-list-addUser" class="input-xlarge" placeholder="Benutzer Name" required="required"}}
</div>
<div class="col-md-6">
<button type="submit" class="btn btn-secondary" id="submit-list-addUser">
<span class="glyphicon glyphicon-plus-sign"></span>
Benutzer hinzufügen
</button>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<div id="userForListModalerrorMessage" style="color:red; display: none; text-align:left"></div><button type="button" class="btn btn-default" data-dismiss="modal">Schließen</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
</template>
<template name="userPill">
<span class="label" style="color:black">{{username}}</span>
lists.js (client):
Template.lists.lists = function(){ return Lists.find(); }
Lists = new Meteor.Collection("lists");
Deps.autorun(function() {
Meteor.subscribe('lists');
})
lists.js
var activeListName = "";
var activeListID = "";
Template.lists.lists = function()
{
return Lists.find();
}
Template.lists.memberCount = function(id)
{
var count = "";
Meteor.call("listMemberCount", id, function(error,result)
{
if (error) {
console.log("List not initialized:" + error.reason);
}
else
{
Session.set("countMember_"+id,result);
}
});
return Session.get("countMember_"+id);
}
Template.lists.ownerOfList = function(id)
{
return ( Meteor.userId() == Lists.findOne({_id : id}).owner);
}
Template.lists.settings = function()
{
return {
position: "top",
limit: 5,
rules: [
{
token: '',
collection: Meteor.users,
field: "username",
template: Template.userPill
}]
}
}
Template.lists.events({
'submit #list-add-form' : function(e, t) {
/* Checklisten ausgeben */
e.preventDefault();
var name = t.find('#list-name').value;
var id = new Meteor.Collection.ObjectID().valueOf();
var id_block = new Meteor.Collection.ObjectID().valueOf();
Lists.insert({_id : id, name : name, owner : Meteor.userId()});
Meteor.users.update({_id : Meteor.userId()}, {$addToSet :{lists : id}});
Listitems.insert({_id : id_block, name : "", items: []});
Lists.update(id, {$addToSet : {items : id_block}});
},
'click .clickOnList' : function(e)
{
/* Eventhandler fuer klick auf Checkliste */
Session.set("activeListId", e.target.id);
$("#"+e.target.id).siblings('li').removeClass("active");
$("#"+e.target.id).addClass("active");
},
'mouseover .clickOnList' : function (e,t) {
$( ".deleteLists" ).each(function( index, item ) {
if ( item.getAttribute("data-id") == e.target.getAttribute("data-id")) {
item.style.visibility = 'visible';
} else {
item.style.visibility = 'hidden';
}
});
},
'mouseleave .clickOnList' : function (e,t) {
$( ".deleteLists" ).each(function( index, item ) {
item.style.visibility = 'hidden';
});
},
'click .deleteLists' : function(e, t)
{
/* Eventhandler zum loeschen einer Checkliste */
var id = e.target.getAttribute("data-id");
Meteor.call("removeList", id);
console.log("test");
},
'click .changeListnameButton' : function(e,t) {
var id = e.target.getAttribute("data-id");
document.getElementById("changerForm_" + id).style.visibility = 'visible';
document.getElementById(id).style.visibility = 'hidden';
document.getElementById("changerText_" + id).focus();
},
'dblclick .clickOnList' : function(e,t){
var id = e.target.getAttribute("data-id");
document.getElementById("changerForm_" + id).style.visibility = 'visible';
document.getElementById(id).style.visibility = 'hidden';
document.getElementById("changerText_" + id).focus();
},
'submit .changeList-name-form' : function(e,t) {
e.preventDefault();
var id = e.target.getAttribute("data-id");
var text = document.getElementById("changerText_" + id).value;
if(text != '') {
Meteor.call("changeListName", id, text);
}
if (Session.get("activeListId", e.target.id) == id ) {
Session.set("activeListName", text);
}
document.getElementById("changerForm_" + id).style.visibility = 'hidden';
document.getElementById(id).style.visibility = 'visible';
},
'blur .list_name' : function(e,t) {
e.preventDefault();
var id = e.target.getAttribute("data-id");
var text = document.getElementById("changerText_" + id).value;
if((text != '') && (document.getElementById(id).style.visibility == 'hidden')) {
Meteor.call("changeListName", id, text);
}
if (Session.get("activeListId", e.target.id) == id ) {
Session.set("activeListName", text);
}
document.getElementById("changerForm_" + id).style.visibility = 'hidden';
document.getElementById(id).style.visibility = 'visible';
},
'click .userForListModal' : function(e,t) {
e.preventDefault();
activeListName = e.target.id;
activeListID = e.target.getAttribute("data-id");
//console.log(activeListID + " " + activeListName);
//console.log("New user for Liste" + Lists.findOne({_id : activeListID}).name);
userForList(activeListID);
$("#userForListModalLabel").html("Benutzer der Liste '"+ activeListName+ "'");
},
'submit #list-addUser-form' : function(e,t) {
e.preventDefault();
var newUser = $('#name-list-addUser').val();
Meteor.call("addUserToList", newUser, activeListID, function(error,result)
{
if (error) {
console.log(error.reason);
}
else
{
if (result == 1) {
$('#userForListModalerrorMessage').fadeIn(1000, function() {$(this).delay(1000).fadeOut(1000);});
$('#userForListModalerrorMessage').html("<div class=\"alert alert-danger\">Benutzer wurde nicht gefunden...</div>");
}
else if (result == 2) {
$('#userForListModalerrorMessage').fadeIn(1000, function() {$(this).delay(1000).fadeOut(1000);});
$('#userForListModalerrorMessage').html("<div class=\"alert alert-warning\">Benutzer ist Besitzer der Liste...</div>");
}
}
});
}
});
function userForList(id)
{
try
{
var owner = Lists.findOne({_id : id}).owner;
var members = Lists.findOne({_id : id}).member;
}
catch(e){
}
output = "<ul>";
output += "<li> Besitzer der Liste: <ul><li>" + owner + "</li></ul></li>";
output += "<li>Mitarbeiter der Liste: <ul>"
if (members != undefined) {
for(i=0; i<members.length; i++)
{
output+= "<li>" + members[i] + "</li>";
}
}
output += "</ul></li></ul>";
$('#userForListModalUsers').html(output);
}
main.js (server):
Lists = new Meteor.Collection("lists");
Meteor.publish("lists", function(){
var ListsOfUser = Meteor.users.findOne({_id : this.userId}).lists;
return Lists.find({_id :{ $in : ListsOfUser}});
});
Lists.allow({
insert : function(userID, list)
{
return (userID && (list.owner === userID));
},
//todo
update : function(userID)
{
return true;
},
//todo
remove : function(userID)
{
return true;
}
});
Thanks in advance!

I believe this is happening because the ListsOfUser variable in your Meteor.publish "lists" function is not a reactive data source. ListsOfUser is an array drawn from your result set, not a reactive cursor. Therefore it is not being invalidated server side when a user adds a new list on the client. From the Meteor docs (note the last sentence especially):
If you call Meteor.subscribe within a reactive computation, for example using
Deps.autorun,the subscription will automatically be cancelled when the computation
is invalidated or stopped; it's not necessary to call stop on subscriptions made
from inside autorun. However, if the next iteration of your run function subscribes
to the same record set (same name and parameters), Meteor is smart enough to skip a
wasteful unsubscribe/resubscribe.
ListsOfUser is not changing when a user adds a new list, so you are not being unsubscribed and resubscribed to the lists publication. (Note also that Meteor.users.findOne() is also not a reactive data source - you might want to switch it to Meteor.users.find() depending on how you go about making ListsOfUser reactive).
There are a few ways you could go about making the user lists reactive.
First, you could publish both the user cursor and the lists cursor, either separately or as an array in the same publish function, place both subscriptions in your Deps.autorun, and then fish out the user lists client side in a helper.
Meteor.publish("userWithLists", function(){
return Meteor.users.find(
{_id: this.userId},
{fields: {'lists': 1}}
);
});
Second, you could publish the static array of user lists as its own Collection and then use cursor.observe or cursor.observeChanges to track when it changes. While my understanding is that this is closest to the "correct" or "Meteor" way of doing it, it is also apparently quite verbose and I have not tried it. This tutorial goes into great detail about how you would tackle something like this: https://www.eventedmind.com/feed/aGHZygsphtTWELpKZ
Third, you could simply stick the user lists into your Session object, which is already reactive, and then publish your Lists.find() based on the Session, i.e.:
Meteor.publish("lists", function(lists){/* find code goes here */});
and
Deps.autorun(function(){
Meteor.subscribe("lists", Session.get("listsOfUser"));
});
This last one is probably an overuse / abuse of the Session object, particularly if your listsOfUser grows large, but should work as a hack.

I know this question is old, but still someone might be seeking for the answer. And the answer is Meteor.publishComposite available with a publish-composite package - https://atmospherejs.com/reywood/publish-composite
And there's an example there
Meteor.publishComposite('topTenPosts', {
find: function() {
// Find top ten highest scoring posts
return Posts.find({}, { sort: { score: -1 }, limit: 10 });
},
children: [
{
find: function(post) {
// Find post author. Even though we only want to return
// one record here, we use "find" instead of "findOne"
// since this function should return a cursor.
return Meteor.users.find(
{ _id: post.authorId },
{ limit: 1, fields: { profile: 1 } });
}
},
{
find: function(post) {
// Find top two comments on post
return Comments.find(
{ postId: post._id },
{ sort: { score: -1 }, limit: 2 });
},
children: [
{
find: function(comment, post) {
// Find user that authored comment.
return Meteor.users.find(
{ _id: comment.authorId },
{ limit: 1, fields: { profile: 1 } });
}
}
]
}
]
});

I am rather new to meteor but, in your server code, should it not be:
var ListsOfUser = Meteor.users.findOne({_id : Meteor.userId}).lists;
rather than:
var ListsOfUser = Meteor.users.findOne({_id : Meteor.userId}).lists;

Related

VueJs submit multiple rows array in form

I have a form, which has a vue componenet that allows users to create additional input lines in the form.
I am having an issue getting all of these input lines to submit for the axios request, currently it only outputs the last added rows input.
Typically, in a normal PHP form, I would just make the field an array (name="MultiRow[]"), however I am lost at doing this in Vue and struggling to find any doc that covers this.
Here is the component in my form:
<div class="mt-5 md:mt-0 md:col-span-2">
<fieldset class="mt-6">
<div class="mt-6">
<response-set-input v-model="fields.response"></response-set-input>
</div>
</fieldset>
</div>
Here is my Vue File for form submission:
<script>
import Switch from '../../components/StatusToggle';
export default {
data() {
return {
fields: {},
errors: {},
statusToggle: false,
}
},
methods: {
toggled(toggleOn){
statusToggle: toggleOn
},
submit() {
this.errors = {};
axios.post('/submit', this.fields).then(response => {
alert('Message sent!');
}).catch(error => {
if (error.response.status === 422) {
this.errors = error.response.data.errors || {};
}
});
},
},
components: {
statusToggle: Switch
}
}
</script>
Here is my component code:
<template>
<div>
<div class="m-2" v-for="(row, index) in rows" :key="index">
<div class="col-span-12 sm:col-span-3 mb-1">
<label for="responseTitle" class="block text-sm font-medium leading-5 text-gray-700">Response</label>
<input
id="responseTitle"
class="mt-1 form-input block w-full py-2 px-3 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:shadow-outline-blue focus:border-blue-300 transition duration-150 ease-in-out sm:text-sm sm:leading-5"
type="text"
name="fields.response[]"
:value="responseInput"
#input="onInput($event.target.value)"/>
</div>
<div class="mt-2">
<button type="button" class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs leading-4 font-medium rounded text-gray-700 bg-green-100 hover:bg-indigo-50 focus:outline-none focus:border-indigo-300 focus:shadow-outline-indigo active:bg-indigo-200 transition ease-in-out duration-150" #click="addRow">
Add new Response
</button>
<button type="button" class="inline-flex items-center px-2.5 py-1.5 border border-transparent text-xs leading-4 font-medium rounded text-gray-700 bg-red-100 hover:bg-indigo-50 focus:outline-none focus:border-indigo-300 focus:shadow-outline-indigo active:bg-indigo-200 transition ease-in-out duration-150" #click="removeRow(index)">
Remove
</button>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['responseInput'],
data () {
return {
rows: [],
stopRemoval: true,
}
},
watch: {
rows () {
this.stopRemoval = this.rows.length <= 1
}
},
methods: {
onInput(responseInput){
this.$emit('input', responseInput),
console.log(responseInput)
},
addRow () {
let checkEmptyRows = this.rows.filter(row => row.number === null)
if (checkEmptyRows.length >= 1 && this.rows.length > 0) {
return
}
this.rows.push({
responseTitle: null,
})
},
removeRow (rowId) {
if (!this.stopRemoval) {
this.rows.splice(rowId, 1)
}
}
},
mounted () {
this.addRow()
}
}
</script>
How do I submit the multiple rows to the form submission with Vue?
There's a decent amount wrong with your code, I suggest that you read the documentation.
Just to name a few things:
You shouldn't update a prop in a component as it will get overridden when the parent updates, props: ['responseInput'], and :value="responseInput"
You're not passing any prop called responseInput, v-model passes a prop called value.
Vue is only reactive on properties that processed during instance initialisation and that means it doesn't know about response on fields: {},
You're using rows (which is good), but then you're only emitting the prop you passed in responseInput. I think :value="responseInput" is supposed to be :value="row"

How i can send data between controllers AngularJS

How i can send data between controllers AngularJS in my occasion?
I wanna send result of scanned qr code into things[] and,of course, show it.
I'm beginner in AngularJS and JavaScript and making this program just for me
App.js:
var MKscanner = angular.module('starter', ['ionic', 'ngCordova', 'ngStorage'])
MKscanner.controller('scanBarCtrl', function($scope, $cordovaBarcodeScanner) {
$scope.input ={
MyText : ''
};
$scope.scanBarcode = function() {
$cordovaBarcodeScanner.scan(
{
preferFrontCamera : false,
orientation : 'portrait'
}).then(function (result) {
alert(result.text);
},
function (error) {
alert('Scanning failed: ' + error);
});
};
});
MKscanner.factory ('StorageService', function ($localStorage) {
$localStorage = $localStorage.$default({
things: []
});
var _getAll = function () {
return $localStorage.things;
};
var _add = function (thing) {
$localStorage.things.push(thing);
}
var _remove = function (thing) {
$localStorage.things.splice($localStorage.things.indexOf(thing), 1);
}
return {
getAll: _getAll,
add: _add,
remove: _remove
};
});
MKscanner.controller( 'MainCtrl', function ($scope, StorageService) {
$scope.things = StorageService.getAll();
$scope.add = function (newThing) {
StorageService.add(newThing);
};
$scope.remove = function (thing) {
StorageService.remove(thing);
};
});
<div ng-controller="MainCtrl">
<div class="list">
<div class="item item-input-inset">
<label class="item-input-wrapper">
<input type="text" placeholder="Save your own text" ng-model="newThing">
</label>
<button class="button button-clear button-positive icon" ng-click="add(newThing)">
<i class="ion-ios-plus-outline"></i> Add
</button>
</div>
</div>
<ion-list show-delete="false" can-swipe="true">
<ion-item ng-repeat="thing in things">
{{thing}}
<ion-option-button class="button-assertive ion-trash-b" ng-click="remove(thing)"></ion-option-button>
</ion-item>
</ion-list>
</div>
You can share data between controllers using $emit and $broadcast :
These events can be used to send data as well, The classic example is given below. You can share data any controller in the app.
<div ng-app="myApp" ng-controller="myCtrl">
<button ng-click="sendData();"></button>
</div>
<script>
var app = angular.module('myApp', []);
app.controller('myCtrl', function($scope, $http) {
function sendData($scope) {
var arrayData = [1,2,3];
$scope.$emit('someEvent', arrayData);
}
});
app.controller('yourCtrl', function($scope, $http) {
$scope.$on('someEvent', function(event, data) {
console.log(data);
});
});
</script>

render data inserted to collection in react component

My entire application is built on different react classes and displayed like this:
MainLayout = React.createClass({
render() {
return (
<div id="body">
<Header />
<main className="container">{this.props.content}</main>
<Footer />
</div>
);
}
});
All my front-end is built in react classes like the one below:
InsertData = React.createClass({
insertToCollection(event) {
event.preventDefault();
console.log(this.state.message + " state med message");
var content = Posts.find().fetch();
Posts.insert({
Place: $("post1").val(),
Type: $("post2").val(),
dateAdded: new Date(),
});
},
handleChange(event) {
this.setState({
message: event.target.value
})
console.log(this.state + " mer state her");
function insert(event) {
event.preventDefault();
console.log("added stuff");
}
},
render() {
return (
<div>
<form onSubmit={this.insertToCollection}>
<input type='text' placeholder="Select a restaurant" className="input-field"
onChange={this.handleChange} id="post1"/>
<input type='text' placeholder="What type of food they have" className="input-field"
onChange={this.handleChange} id="post2"/>
<button className="waves-effect waves-light btn btn-block" onChange={this.insert}> Submit </button>
</form>
<DisplayData />
</div>
);
}
});
Insert data to my collection works fine. I would like to render the inserted data onto the page from the <DisplayData /> component:
DisplayData = React.createClass({
render(){
var posts = Posts.find().fetch();
var postList = posts.map(function(posts){
return posts;
})
return <p> Your collection </p>
}
});
I'm rather stuck here, and not really sure how to iterate through the collection and render it in a list-structure for example. Here is my collection so far:
Posts = new Mongo.Collection('posts');
Posts.allow({
insert: function(){
return true;
},
update : function(){
return true;
},
remove : function(){
return true;
}
});
Here is a demo of how you can approach this: http://codepen.io/PiotrBerebecki/pen/bwmAvJ
I'm not sure about the format of your posts collection, but assuming that it is just a regular array, for example var posts = ['One', 'Two'];, you can render the individual post as follows:
var DisplayData = React.createClass({
render(){
var posts = ['One', 'Two'];
var renderPosts = posts.map(function(post, index) {
return (
<li key={index}>{post}</li>
);
});
return (
<div>
<p> Your collection </p>
<ul>
{renderPosts}
</ul>
</div>
);
}
});
Here is the full code from my codepen.
var InsertData = React.createClass({
insertToCollection(event) {
event.preventDefault();
console.log(this.state.message + " state med message");
var content = Posts.find().fetch();
Posts.insert({
Place: $("post1").val(),
Type: $("post2").val(),
dateAdded: new Date(),
});
},
handleChange(event) {
this.setState({
message: event.target.value
})
console.log(this.state + " mer state her");
function insert(event) {
event.preventDefault();
console.log("added stuff");
}
},
render() {
return (
<div>
<form onSubmit={this.insertToCollection}>
<input type='text' placeholder="Select a restaurant" className="input-field"
onChange={this.handleChange} id="post1"/>
<input type='text' placeholder="What type of food they have" className="input-field"
onChange={this.handleChange} id="post2"/>
<button className="waves-effect waves-light btn btn-block" onChange={this.insert}> Submit </button>
</form>
<DisplayData />
</div>
);
}
});
var DisplayData = React.createClass({
render(){
var posts = ['One', 'Two'];
var renderPosts = posts.map(function(post, index) {
return (
<li key={index}>{post}</li>
);
});
return (
<div>
<p> Your collection </p>
<ul>
{renderPosts}
</ul>
</div>
);
}
});
ReactDOM.render(
<InsertData />,
document.getElementById('app')
);
You need to pass posts to your view component DisplayData as props, so in this case after you inserted a post, you should update your state in the InsertData component. Actually it would be better if you do the insertion login inside a service rather than the component itself, but for simplicity right now you can check the following code:
InsertData = React.createClass({
getInitialState: function() {
return {posts: []}; // initialize the state of your component
},
insertToCollection(event) {
event.preventDefault();
console.log(this.state.message + " state med message");
var content = Posts.find().fetch();
Posts.insert({
Place: $("post1").val(), // better to retrieve these values from state. You can use `handleChange` method to keep track of user inputs
Type: $("post2").val(),
dateAdded: new Date(),
}, function(err, data){
var posts = this.state.posts || [];
posts.push(data);
this.setState({posts: posts}); //after setting the state the render method will be called again, where the updated posts will be rendered properly
});
},
handleChange(event) {
this.setState({
message: event.target.value
})
console.log(this.state + " mer state her");
function insert(event) {
event.preventDefault();
console.log("added stuff");
}
},
render() {
return (
<div>
<form onSubmit={this.insertToCollection}>
<input type='text' placeholder="Select a restaurant" className="input-field"
onChange={this.handleChange} id="post1"/>
<input type='text' placeholder="What type of food they have" className="input-field"
onChange={this.handleChange} id="post2"/>
<button className="waves-effect waves-light btn btn-block" onChange={this.insert}> Submit </button>
</form>
<DisplayData posts={this.state.posts}/>
</div>
);
}
});
var DisplayData = React.createClass({
render(){
var posts = this.props.posts || [];
var renderPosts = posts.map(function(post, index) {
return (
<li key={index}>{post}</li>
);
});
return (
<div>
<p> Your collection </p>
<ul>
{renderPosts}
</ul>
</div>
);
}
});

How to get an image from gridFS in Meteorjs

I am using the gridFS package in MeteorJS.
What system does is upload a product with an image a title, a category and a price. This gets saved in the products collection.
With a template helper I can get a product from the db with it's title {{title}} and price {{price}}. But I'm stuck getting the image using the reference in de the product document.
Can anyone help me with that?
<template name="product">
<div class="small-6 medium-3 columns end">
<div class="product-container">
<div class="image-container">
{{#with image}}
<img src="{{this.url}}" class="product-image" alt="" />
{{/with}}
</div>
<div class="product-details">
<div class="product-title">
{{title}}
</div>
<div class="product-price">
{{price}}
</div>
<div class="product-buttons">
<span class="icon-heart"></span>
<span class="icon-repost"></span>
<span class="icon-plus"></span>
</div>
</div>
</div>
</div>
</template>
<template name="home">
{{#each products}}
{{> product}}
{{/each}}
</template>
Products = new Mongo.Collection("products");
var productImagesStore = new FS.Store.FileSystem('productImages', {
path: '~/uploads/full'
});
productImages = new FS.Collection('productImages', {
stores: [productImagesStore]
});
productImages.allow({
insert: function () {
return true;
},
update: function () {
return true;
},
remove: function () {
return true;
},
download: function () {
return true;
}
});
if (Meteor.isClient) {
// This code only runs on the client
Template.home.helpers({
products: function () {
return Products.find({});
},
image:function(){
return productImages.findOne({'metadata.productId':this._id})
}
});
Template.add.helpers({
categories: function(){
return categories.find({});
}
});
Template.add.events({
"submit form": function(event, template) {
event.preventDefault();
var file = $('#file').get(0).files[0],
fsFile = new FS.File(file),
productData = {
title:$('#title').val(),
price:$('#price').val(),
category:$('#category').val()
}
Products.insert(productData,function(err,result){
if(!err){
fsFile.metadata = {
productId:result, //we use metadata to refer the recent object id with the product images
}
productImages.insert(fsFile,function(err,result){
if(!err){
console.log("New images inserted")
}
});
}
});
}
});
}
You can store the reference on the productImages collection like this.
var file = $('#file').get(0).files[0],
fsFile = new FS.File(file),
productData = {
title:$('#title').val(),
price:$('#price').val(),
category:$('#category').val()
}
Products.insert(productData,function(err,result){
if(!err){
fsFile.metadata = {
productId:result, //we use metadata to refer the recent object id with the product images
}
productImages.insert(fsFile,function(err,result){
if(!err){
console.log("New images inserted")
}
});
}
});
This is the same insert but a little bit more cleaner
Now you can use the helper and the this context like this.
HTML
{{#each products}}
{{title}}
{{price}}
{{#with image}}
IMG URL:{{this.url}}
<img src="{{this.url}}" />
{{/with}}
{{/each}}
JS(Helpers)
Template.example.helpers({
products:function(){
return Products.find()
},
image:function(){
return productImages.findOne({'metadata.productId':this._id})
}
})

Handle radio button form in Marionette js

I'm trying to construct a view in my app that will pop up polling questions in a modal dialog region. Maybe something like this for example:
What is your favorite color?
>Red
>Blue
>Green
>Yellow
>Other
Submit Vote
I've read that Marionette js doesn't support forms out of the box and that you are advised to handle on your own.
That structure above, branch and leaves (question and list of options), suggests CompositeView to me. Is that correct?
How do I trigger a model.save() to record the selection? An html form wants an action. I'm unclear on how to connect the form action to model.save().
My rough draft ItemView and CompositeView code is below. Am I in the ballpark? How should it be adjusted?
var PollOptionItemView = Marionette.ItemView.extend({
template: Handlebars.compile(
'<input type="radio" name="group{{pollNum}}" value="{{option}}">{{option}}<br>'
)
});
var PollOptionsListView = Marionette.CompositeView.extend({
template: Handlebars.compile(
//The question part
'<div id="poll">' +
'<div>{{question}}</div>' +
'</div>' +
//The list of options part
'<form name="pollQuestion" action="? what goes here ?">' +
'<div id="poll-options">' +
'</div>' +
'<input type="submit" value="Submit your vote">' +
'</form>'
),
itemView: PollOptionItemView,
appendHtml: function (compositeView, itemView, index) {
var childrenContainer = $(compositeView.$("#poll-options") || compositeView.el);
var children = childrenContainer.children();
if (children.size() === index) {
childrenContainer.append(itemView.el);
} else {
childrenContainer.children().eq(index).before(itemView.el);
}
}
});
MORE DETAILS:
My goal really is to build poll questions dynamically, meaning the questions and options are not known at runtime but rather are queried from a SQL database thereafter. If you were looking at my app I'd launch a poll on your screen via SignalR. In essence I'm telling your browser "hey, go get the contents of poll question #1 from the database and display them". My thought was that CompositeViews are best suited for this because they are data driven. The questions and corresponding options could be stored models and collections the CompositeView template could render them dynamically on demand. I have most of this wired and it looks good. My only issue seems to be the notion of what kind of template to render. A form? Or should my template just plop some radio buttons on the screen with a submit button below it and I write some javascript to try to determine what selection the user made? I'd like not to use a form at all and just use the backbone framework to handle the submission. That seems clean to me but perhaps not possible or wise? Not sure yet.
I'd use the following approach:
Create a collection of your survey questions
Create special itemviews for each type of question
In your CompositeView, choose the model itemView based on its type
Use a simple validation to see if all questions have been answered
Output an array of all questions and their results.
For an example implementation, see this fiddle: http://jsfiddle.net/Cardiff/QRdhT/
Fullscreen: http://jsfiddle.net/Cardiff/QRdhT/embedded/result/
Note:
Try it without answering all questions to see the validation at work
Check your console on success to view the results
The code
// Define data
var surveyData = [{
id: 1,
type: 'multiplechoice',
question: 'What color do you like?',
options: ["Red", "Green", "Insanely blue", "Yellow?"],
result: null,
validationmsg: "Please choose a color."
}, {
id: 2,
type: 'openquestion',
question: 'What food do you like?',
options: null,
result: null,
validationmsg: "Please explain what food you like."
}, {
id: 3,
type: 'checkbox',
question: 'What movie genres do you prefer?',
options: ["Comedy", "Action", "Awesome", "Adventure", "1D"],
result: null,
validationmsg: "Please choose at least one movie genre."
}];
// Setup models
var questionModel = Backbone.Model.extend({
defaults: {
type: null,
question: "",
options: null,
result: null,
validationmsg: "Please fill in this question."
},
validate: function () {
// Check if a result has been set, if not, invalidate
if (!this.get('result')) {
return false;
}
return true;
}
});
// Setup collection
var surveyCollection = Backbone.Collection.extend({
model: questionModel
});
var surveyCollectionInstance = new surveyCollection(surveyData);
console.log(surveyCollectionInstance);
// Define the ItemViews
/// Base itemView
var baseSurveyItemView = Marionette.ItemView.extend({
ui: {
warningmsg: '.warningmsg',
panel: '.panel'
},
events: {
'change': 'storeResult'
},
modelEvents: {
'showInvalidMessage': 'showInvalidMessage',
'hideInvalidMessage': 'hideInvalidMessage'
},
showInvalidMessage: function() {
// Show message
this.ui.warningmsg.show();
// Add warning class
this.ui.panel.addClass('panel-warningborder');
},
hideInvalidMessage: function() {
// Hide message
this.ui.warningmsg.hide();
// Remove warning class
this.ui.panel.removeClass('panel-warningborder');
}
});
/// Specific views
var multipleChoiceItemView = baseSurveyItemView.extend({
template: "#view-multiplechoice",
storeResult: function() {
var value = this.$el.find("input[type='radio']:checked").val();
this.model.set('result', value);
}
});
var openQuestionItemView = baseSurveyItemView.extend({
template: "#view-openquestion",
storeResult: function() {
var value = this.$el.find("textarea").val();
this.model.set('result', value);
}
});
var checkBoxItemView = baseSurveyItemView.extend({
template: "#view-checkbox",
storeResult: function() {
var value = $("input[type='checkbox']:checked").map(function(){
return $(this).val();
}).get();
this.model.set('result', (_.isEmpty(value)) ? null : value);
}
});
// Define a CompositeView
var surveyCompositeView = Marionette.CompositeView.extend({
template: "#survey",
ui: {
submitbutton: '.btn-primary'
},
events: {
'click #ui.submitbutton': 'submitSurvey'
},
itemViewContainer: ".questions",
itemViews: {
multiplechoice: multipleChoiceItemView,
openquestion: openQuestionItemView,
checkbox: checkBoxItemView
},
getItemView: function (item) {
// Get the view key for this item
var viewId = item.get('type');
// Get all defined views for this CompositeView
var itemViewObject = Marionette.getOption(this, "itemViews");
// Get correct view using given key
var itemView = itemViewObject[viewId];
if (!itemView) {
throwError("An `itemView` must be specified", "NoItemViewError");
}
return itemView;
},
submitSurvey: function() {
// Check if there are errors
var hasErrors = false;
_.each(this.collection.models, function(m) {
// Validate model
var modelValid = m.validate();
// If it's invalid, trigger event on model
if (!modelValid) {
m.trigger('showInvalidMessage');
hasErrors = true;
}
else {
m.trigger('hideInvalidMessage');
}
});
// Check to see if it has errors, if so, raise message, otherwise output.
if (hasErrors) {
alert('You haven\'t answered all questions yet, please check.');
}
else {
// No errors, parse results and log to console
var surveyResult = _.map(this.collection.models, function(m) {
return {
id: m.get('id'),
result: m.get('result')
}
});
// Log to console
alert('Success! Check your console for the results');
console.log(surveyResult);
// Close the survey view
rm.get('container').close();
}
}
});
// Create a region
var rm = new Marionette.RegionManager();
rm.addRegion("container", "#container");
// Create instance of composite view
var movieCompViewInstance = new surveyCompositeView({
collection: surveyCollectionInstance
});
// Show the survey
rm.get('container').show(movieCompViewInstance);
Templates
<script type="text/html" id="survey">
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title" > A cool survey regarding your life </h3>
</div>
<div class="panel-body">
<div class="questions"></div>
<div class="submitbutton">
<button type="button" class="btn btn-primary">Submit survey!</button>
</div>
</div>
</div >
</script>
<script type="text/template" id="view-multiplechoice">
<div class="panel panel-success">
<div class="panel-heading">
<h4 class="panel-title" > <%= question %> </h4>
</div>
<div class="panel-body">
<div class="warningmsg"><%= validationmsg %></div>
<% _.each( options, function( option, index ){ %>
<div class="radio">
<label>
<input type="radio" name="optionsRadios" id="<%= index %>" value="<%= option %>"> <%= option %>
</label>
</div>
<% }); %>
</div>
</div>
</script>
<script type="text/template" id="view-openquestion">
<div class="panel panel-success">
<div class="panel-heading">
<h4 class="panel-title" > <%= question %> </h4>
</div>
<div class="panel-body">
<div class="warningmsg"><%= validationmsg %></div>
<textarea class="form-control" rows="3"></textarea>
</div>
</div >
</script>
<script type="text/template" id="view-checkbox">
<div class="panel panel-success">
<div class="panel-heading">
<h4 class="panel-title" > <%= question %> </h4>
</div>
<div class="panel-body">
<div class="warningmsg"><%= validationmsg %></div>
<% _.each( options, function( option, index ){ %>
<div class="checkbox">
<label>
<input type="checkbox" value="<%= option %>"> <%= option %>
</label>
</div>
<% }); %>
</div>
</div>
</script>
<div id="container"></div>
Update: Added handlebars example
Jsfiddle using handlebars: http://jsfiddle.net/Cardiff/YrEP8/