I am learning the meteor-framework for it's ability to make realtime inputs and updates easily. Because it's new to me, I'm not quite sure if this is the most efficient way to do things.
I'm trying to create two forms to save some data in the MongoDB. I'm using SimpleSchema.
First there is a simple input field for adding a title to the DB.:
template
<template name="addForm">
<form>
<input type="text" name="text">
<input type="submit" value="add">
</form>
</template>
events.js
Template.addForm.events({
'submit form': function(event){
event.preventDefault();
var title = event.target.text.value;
MongoArticle.insert({
title: title,
slug: title.toLowerCase()
});
event.target.text.value = "";
}
});
collection.js
MongoArticle = new Mongo.Collection('articles');
var subset = new SimpleSchema({
age: {type: String, optional: true},
value: {type: String, optional: true},
info: {type: String, optional: true},
});
MongoArticle.attachSchema(new SimpleSchema({
title: {type: String},
slug: {type: String},
flag: {type: Boolean, optional: true},
elements: {type: [subset], optional: true}
}));
All titles saved in the DB will be displayed in a list:
<template name="list">
<ul>
{{#each art}}
<li>{{title}}</li>
{{/each}}
</ul>
</template>
If you click on the title you will get the detailed form. Here you can add and edit multiple elements, which are saved in a subdocument (elements -> subset).
<template name="Editor">
<table>
<tbody>
{{#each art.elements}}
<tr>
<td><input type="text" value="{{age}}"></td>
<td><input type="text" value="{{value}}"></td>
<td><input type="text" value="{{info}}"></td>
</tr>
{{/each}}
<tr>
<td><input type="text" value=""></td>
<td><input type="text" value=""></td>
<td><input type="text" value=""></td>
</tr>
</tbody>
</table>
</template>
So I'm trying to get all the elements of the selected article to be displayed in a row of input fields. That will allow them to be edited. I want to have realtime saving, so that (if possible) every change is saved without needing to click a button. Is that possible?
The last row should be used to input new data, which should then be added to the Editor.
I tried to do that with a update() on each row and an input() on the last row, but it didn't work for me, because I don't know how to save the row to the elements-field in the DB, which is a subdocument of the articles-collection.
So I read some websites and I found ´aldeed:autoform´ and ´aldeed:collection2´. Would this be helpful for what I am doing?
If you give your inputs names corresponding to your model fields then updating the existing elements boils down to the following snippet:
'change input[name]': function (e, t) {
var articleId = t.data.art._id; // or something similar
var updates = {};
var $target = $(e.target);
var index = $target.closest('tr').index();
var name = $target.attr('name');
updates['elements.' + index + '.' + name] = $target.val();
MongoArticle.update({ _id: articleId }, { $set: updates });
}
You may also consider using different events, e.g. keyup along with some _.debounce().
Inserting a new element is even simpler because you don't need the index. Also you will probably want to hook to different events (maybe button click?) and replace the $set modifier with $push, i.e.
MongoArticle.update({ _id: articleId }, { $push: {
elements: {
age: "",
value: "",
info: ""
}
}});
Related
Currently, I am using ng-model to insert data into my MongoDB. Is there a way to use ng-model to insert data into an array in MongoDB? answers is an array that should contain 4 strings that the user enters. I tried adding [0], [1], [2], [3] to quiz.quizData.answers but it did not enter the data correctly as an array. Instead it entered it like this:
"answers" : [
{
"0" : "My First Answer",
"1" : "My Second Answer"
}
],
instead of how it should be:
"answers" : [
"My First Answer",
"My Second Answer"
],
My HTML input form:
<input type="text" name="answers" ng-model="quiz.quizData.answers" placeholder="enter answers here" required>
<input type="text" name="answers2" ng-model="quiz.quizData.answers" placeholder="enter other answers here" required>
My End Point
// POST request for users to post a new quiz entry
apiRouter.route('/quiz')
.post(function(req, res) {
// Create quiz object and assign to 'quiz'
var quiz = new Quiz();
// Contains the quiz question
quiz.question = req.body.question;
// Contains an array with four quiz answers
quiz.answers = req.body.answers;
// Contains one string that matches the correct answer from the array above
quiz.correctAnswer = req.body.correctAnswer;
// Identifies user creating the quiz
quiz.postedBy = req.body.postedBy;
// Identifies the category of the quiz
quiz.category = req.body.category;
// then save new quiz to database
quiz.save(function(err) {
// If an error occurs, display error message in JSON format
if (err) {
return res.json({ success: false, message: 'something went way wrong....' + err });
} else {
// If no error occurs and it saves, display success
res.json({ message: 'Quiz Created!' });
}
});
});
My MongoDB Schema:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
// post schema
var QuizSchema = new Schema({
question: { type: String, lowercase: true, required: true },
answers: { type: Array, required: true },
correctAnswer: { type: String, required: true },
category: { type: String, lowercase: true, required: true },
postedBy: { type: String, required: true }
});
module.exports = mongoose.model('Quiz', QuizSchema);
You can just use ng-repeat iterating a range if you're starting with an empty array:
<input ng-repeat="i in [1,2,3,4]" type="text" name="answers{{i}}" ng-model="quiz.quizData.answers[i]" placeholder="enter answers here" required>
If your array already exists, and you want to account for variable array length you can use ng-repeat and $index. Make sure you add track by $index or you will encounter an issue with duplicate keys:
<input ng-repeat="answer in quiz.quizData.answers track by $index" type="text" name="answers{{$index}}" ng-model="quiz.quizData.answers[$index]" placeholder="enter answers here" required>
https://plnkr.co/edit/dwHIFTftgq5LFfTOpc9X?p=info
HTML
<input type="text" name="answer1" ng-model="answersArray[0]" placeholder="answer 1">
<input type="text" name="answer2" ng-model="answersArray[1]" placeholder="answer 2">
<input type="text" name="answer3" ng-model="answersArray[2]" placeholder="answer 3">
<input type="text" name="answer4" ng-model="answersArray[3]" placeholder="answer 4">
<input type="hidden" name="answers" ng-model="quiz.quizData.answers" placeholder="enter answers here" ng-init="quiz.quizData.answers = answersArray">
Controller
$scope.answersArray = [
answer1 = $scope.answer1,
answer2 = $scope.answer2,
answer3 = $scope.answer3,
answer4 = $scope.answer4
];
I have an array with many "contact" objects inside. Only one contact can be the primary contact (primary: true).
I also have a radio button to select which contact is the primary.
Q: How can I make one contact primary and deactivate all of the others (primary: false)? so only one object have the property (primary: true) and the rest false?
My example: http://plnkr.co/edit/Y3as4SXv2ZGQSF39W8O6?p=preview
.controller('ExampleController', ['$scope',
function($scope) {
$scope.addContList = [
{
email: "q#q.com",
jobTitle: "clerk",
name: "nico2",
phone: "1",
primary: true
},
{
email: "a#a.com",
jobTitle: "director",
name: "david",
phone: "1",
primary: false
}
];
$scope.$watch('addContList', function() {
console.log('changed', JSON.stringify($scope.addContList, null, 2))
}, true)
}
]);
Here is the view
<tr ng-repeat="contact in addContList">
<td>
<label class="radio-inline">
<input type="radio" value="" name="ui_cont" ng-model="contact.primary" ng-value="true">
</label>
</td>
<td>{{ contact.name }} value = {{contact.primary}} </td>
<td>Edit</td>
<td>Delete</td>
</tr>
You would want to add an ngChange event to your input and change all other inputs to false when one gets set to true. I have updated your Plnkr here: http://plnkr.co/edit/7gxI7if9nC7hAMQES1eu?p=preview
<input type="radio" value="" name="ui_cont" ng-change='changeOnPrimary(contact)' ng-model="contact.primary" ng-value="true">
Then in your controller:
$scope.changeOnPrimary = function(selectedContact) {
// iterate over your whole list
angular.forEach($scope.addContList, function(contact) {
// set primary to false for all contacts excepts selected
if (selectedContact.name !== contact.name) {
contact.primary = false;
}
});
}
Please note: the only reason I'm comparing the name field of the object is because there is no unique identifier to compare with. In real code, you would want to compare against an ID rather than a name field.
You can define a new scope property
$scope.primary = null
Then you can define a listener
$scope.$watch("primary", function(value) {
$scope.addContList.forEach(function(contact) {
contact.primary = angular.equals(value, contact);
})
})
and you can define a default value after defining the list
$scope.primary = $scope.addContList[0];
and in the html you change the input line in
<input type="radio" value="" name="ui_cont" ng-model="$parent.primary" ng-value="contact">
You need to use $parent.primary instead of primary, because ng-repeat defines a new child scope.
see http://plnkr.co/edit/5pvatBNwnrJhGzKhOIys?p=preview
With this template I'm showing all results, which are stored in the users-collection:
<template name="user">
{{#each users.roles}}
<input type="checkbox" data-role="{{this}}">{{this}}
{{/each}}
</template>
There are some roles defined like "admin", "editor", "user_manage". But this leads to some problems:
1) If one user has just the role "admin", there will only one checkbox displayed. But I need to display all possible roles. Only if the role is in the profile, the checkbox should be checked.
2) The displayed description of the role should be 'nicer'. I mean the result should be like:
<input type="checkbox" data-role="admin"> Administrator
<input type="checkbox" data-role="editor"> Editor
<input type="checkbox" data-role="user_manage"> Manage users
I guess I need a helper:
Template.user.helpers({
var roles = {admin: 'Administrator', editor: 'Editor', user_manage: 'Manage users'};
return roles;
});
Now I think of iterating over all elements in roles and checking if the role exists (=checked) or not (=unchecked). The values of the elements are for displaying the Label of the checkbox.
1) Would this be the correct way doing that?
2) How do I connect the helper var roles to the template for checking?
You're basically on the right track! Your helper needs to return an array of objects instead of an object. Then you need a helper which returns a boolean based on the whether that role is in the user's role array. For example:
js:
Template.user.helpers({
roles: function(){
return [
{ label: 'admin', name: 'Administrator'},
{ label: 'editor', name: 'Editor'},
{ label: 'user_manage', name: 'Manage users'}
];
},
hasRole: function(role){
return Meteor.user().roles.indexOf(role)>-1; //assuming user.roles is just an array
}
});
html:
<template name="user">
{{#each roles}}
<input type="checkbox" data-role="{{this.label}}"
{{#if hasRole this.label}}checked{{/if}}>{{this.name}}
{{/each}}
</template>
I have a data model persons which takes the following form:
personsInfo = {
name: Adam
dob: 31-FEB-1985
docs: [
{
docType: Drivers License,
number: 121212,
selected: false
id: 1
},
{
selected: true,
docType: None
},
{
docType: State ID,
number: 132345,
selected: false,
id: 2
}
]
}
In my markup I have defined the following to dynamically generate radio buttons.
<div ng-repeat="personDoc in personsInfo.docs">
<input type="radio" name="personDocs" ng-model="personDoc.selected" value=""/>
{{personDoc.docType}} <span ng-hide="personDoc.docType === 'None'">Number: {{personDoc.number}}</span>
</div>
I want to be able to check the documents which have selected as true on page load, and then depending on what the user selects save the selected flag in my personsInfo model.
My intent here is to send the personsInfo model back to another page.
If somebody could point me to a working fiddle it would be greatly appreciated!
Thanks!
You're almost there just missing the binding to show which document is selected. We'll add an object to the scope to represent the selected item, then bind the forms to that model.
JS
app.controller('...', function($scope) {
$scope.personInfo = { ... };
$scope.selectedDoc = {};
$scope.$watch('personInfo',function() {
$scope.selectedDoc = $scope.personInfo.docs[0];
});
});
HTML
<div ng-repeat='doc in personInfo.docs'>
<input type='radio' ng-model='selectedDoc' value='doc' /> {{doc.docType}}
</div>
<form>
<input type='text' ng-model='selectedDoc.number' />
...
</form>
In KO 3.0.0.beta (and I am almost sure it'd be the same in 2.3) I am trying to add new row to a dynamically created table:
HTML
<div id="root">
<table>
<tbody data-bind="foreach: events">
<tr>
<td>
<input type="text" data-type="name" data-bind="value: name, css: {hidden: !editing()}, hasFocus: true">
</td>
<td>
<input type="text" data-type="method" data-bind="value: age, css: {hidden: !editing()}">
</td>
</tr>
</tbody>
</table>
</div>
JavaScript
var $root = $('#root');
$root.on('blur', 'table input', function(event) {
var data = ko.dataFor(event.target),
context = ko.contextFor(event.target),
$input = $(event.target),
newData;
data.editing(false);
if($input.closest('td').is(':last-child') && $input.closest('tr').is(':last-child')) {
newData = {
name: '',
age: '',
editing: ko.observable(false)
};
context.$root.events.push(newData);
newData.editing(true);
}
});
var data = {
events: [
{name: 'aaa', age: '22', editing: false},
{name: 'bbb', age: '33', editing: false},
{name: 'ccc', age: '44', editing: false}
]
};
var mapping = {
events: {
key: function(data) {
return ko.unwrap(data.name);
}
}
};
var observable = ko.mapping.fromJS(data, mapping);
ko.applyBindings(observable, $root[0]);
JSFiddle
and it almost works.
I am successful with row creation - which was the siple part, but for the life of me I can't make the first input in the created raw to be focused.
Any ideas (and I went through a ton of suggestions, none of which worked in the above setting)?
I might be missing the point, but why not just set your hasFocus to editing() instead of true?
<div id="root">
<table>
<tbody data-bind="foreach: events">
<tr>
<td>
<input type="text" data-type="name" data-bind="value: name, css: {hidden: !editing()}, hasFocus: editing()">
</td>
<td>
<input type="text" data-type="method" data-bind="value: age, css: {hidden: !editing()}">
</td>
</tr>
</tbody>
</table>
</div>
Fiddle here
It often pays of to avoid mixing jQuery with Knockout like that. For most cases Knockout also has options. In your case, the ones to look at are the event binding and the hasFocus binding.
Looking at your code, I'd update the View to something like this:
<tr>
<td>
<input type="text" data-bind="value: name,
hasFocus: editingName">
</td>
<td>
<input type="text" data-bind="value: age,
hasFocus: editingAge,
event: { blur: $root.onBlurAgeEdit }">
</td>
</tr>
As an aside, if you're after "MS Word Style" creation of new rows when a user presses tab, you can also bind to the "keypress" event and check for the correct keycode (for tab).
Notice that I've split the editing observable in two observables: editingName and editingAge.
The function onBlurAgeEdit will fire whenever the age input blurs. It looks like this:
self.onBlurAgeEdit = function (item) {
// Unwrap the array
var myEvents = self.events();
// Check if this function was called for the *last* item in the array
if (myEvents[myEvents.length-1] === item) {
// Make sure the last item isn't in edit mode anymore
item.editingAge(false);
// Push a new item that has *name* in edit mode
self.events.push({name: ko.observable(''),
age: ko.observable(''),
editingName: ko.observable(true),
editingAge: ko.observable(false)});
}
};
Note that for Firefox you'll need to throttle the observables to make the hasFocus binding play nice.
See all this at work in this fiddle.