I would like to create a form, using the autoform package for Meteor, for my CAS_Entry collection. The code can be seen below. I also added the defined hooks, of which unfortunately only beginSubmit and before are executed and no entry is added to the collection. Using Meteor shell, the insert works like a charm.
I am grateful for any hint.
addCasEntry.html, Template for displaying the form:
{{#autoForm collection="CAS_Entry" type="insert" id="addCasEntryForm"}}
{{> afQuickField name="type" options="allowed"}}
{{> afQuickField name="description" rows="6" type="textarea"}}
{{> afQuickField name="file" type="cfs-file" collection="Images"}}
{{> afQuickField name="date" }}
<button type="submit" class="btn btn-primary">Add</button>
{{/autoForm}}
addCasEntry.js, adding debugging hooks:
AutoForm.hooks({
addCasEntryForm: {
before: {
insert: function(doc) {
console.log(doc);
}
},
after: {
insert: function(error, result) {
console.log('Occured error: ' + error);
}
},
beginSubmit: function() {
console.log('begin submit');
},
onSuccess: function(formType, result) {
console.log("Insert succeeded");
console.log('Result ' + result);
},
onError: function(formType, error) {
console.log('Error!!!');
console.log(error);
}
}
});
SimpleSchema.debug = true;
/lib/collection/cas_entry.js:
CAS_Entry = new Mongo.Collection("cas_entries");
CAS_Entry.attachSchema(new SimpleSchema({
type: {
type: String,
allowedValues: ['reflection', 'evidence']
},
description: {
type: String,
optional: true
},
file: {
type: String,
optional: true,
},
timeUploaded: {
type: Date,
optional: true,
autoValue: function() {
return new Date();
}
},
date: {
type: Date,
}
}));
CAS_Entry.allow({
'insert': function() {
return true;
},
'update': function() {
return true;
}
});
And here is the console output:
Your form won't be submitted because you are not returning or passing the document to this.result(); inside your before hook.
AutoForm.hooks({
addCasEntryForm: {
// ...
before: {
insert: function(doc) {
console.log(doc);
return doc;
}
}
// ...
}
});
According to the documentation, you should use one of the following statements depending on your defined preconditions:
Synchronous, submit: return doc;.
Synchronous, cancel: return false;.
Asynchronous, submit: this.result(doc);.
Asynchronous, cancel: this.result(false);.
Related
I'm trying to avoid letting users submit stripe form when inputs are empty, I`m using stripe.js elements integration to render my form and handle form submition inside my vue component.
this.cardNumberElement.on('change', this.enableForm);
this.cardExpiryElement.on('change', this.enableForm);
this.cardCvcElement.on('change', this.enableForm);
After checking the docs I tried to use the change event on inputs but this is not working sice the user can just not type anything and click submit button.
This is my component:
mounted()
{
console.log(this.$options.name + ' component succesfully mounted');
this.stripe = Stripe(this.stripePK);
this.elements = this.stripe.elements();
this.cardNumberElement = this.elements.create('cardNumber', {style: this.stripeStyles});
this.cardNumberElement.mount('#card-number-element');
this.cardExpiryElement = this.elements.create('cardExpiry', {style: this.stripeStyles});
this.cardExpiryElement.mount('#card-expiry-element');
this.cardCvcElement = this.elements.create('cardCvc', {style: this.stripeStyles});
this.cardCvcElement.mount('#card-cvc-element');
let stripeElements = document.querySelectorAll("#card-number-element, #card-expiry-element, #card-cvc-element");
stripeElements.forEach(el => el.addEventListener('change', this.printStripeFormErrors));
this.cardNumberElement.on('change', this.enableForm);
this.cardExpiryElement.on('change', this.enableForm);
this.cardCvcElement.on('change', this.enableForm);
},
methods:
{
...mapActions('Stripe', ['addSource', 'createSourceAndCustomer']),
...mapMutations('Stripe', ['TOGGLE_PAYMENT_FORM']),
...mapMutations('Loader', ['SET_LOADER', 'SET_LOADER_ID']),
enableForm:function(event){
if(event.complete){
this.disabled = false;
}
else if(event.empty){
this.disabled = true;
}
},
submitStripeForm: function()
{
this.SET_LOADER({ status:1, message: 'Procesando...' });
var self = this;
this.stripe.createSource(this.cardNumberElement).then(function(result) {
if (result.error) {
self.cardErrors = result.error.message;
}
else {
self.stripeSourceHandler(result.source.id);
}
});
},
stripeSourceHandler: function(sourceId)
{
console.log('stripeSourceHandler');
this.cardNumberElement.clear();
this.cardExpiryElement.clear();
this.cardCvcElement.clear();
if(this.customerSources.length == 0)
{
console.log('createSourceAndCustomer');
this.createSourceAndCustomer({ id: sourceId });
}
else
{
console.log('addSource');
this.addSource({ id: sourceId });
}
},
printStripeFormErrors: function(event)
{
if(event.error)
{
self.cardErrors = event.error.message
}
else
{
self.cardErrors = '';
}
}
}
Given the stripe docs, the use of the event seems correct (though it can be improved a bit with using this.disabled = !event.complete to cover error case and not only empty case).
You may try to console.log in the event callback enableForm to check if event is well fired.
Anyway, it's more likely coming from the disabling logic of the submit button and it misses in your post. I've created below a fake secure-component that triggers a change event when value change.
The interesting part in on the container component :
Submit is disabled by default through data disabled,
Submit is enabled if event received has a property complete set to true. If false, it is disabled.
Hope it will help you to focus your trouble.
/**
Mock component to emulate stripes card element behavior with change event
*/
const SecureInput = {
template: '<input type="text" v-model="cardnumber"/>',
data: () => ({
cardnumber: null
}),
watch: {
cardnumber: function(val) {
if(!val) {
this.$emit('change', {empty: true, error: false, complete: false});
return;
}
if(val.length < 5) {
this.$emit('change', {empty: false, error: true, complete: false});
return;
}
this.$emit('change', {empty: false, error: false, complete: true});
}
}
}
/* Logic is here */
const app = new Vue({
el: '#app',
components: {
SecureInput
},
data: {
disabled: true
},
methods: {
updateDisable: function(event) {
this.disabled = !event.complete;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<form #submit.prevent="$emit('submitted')">
<p><secure-input #change="updateDisable"/></p>
<p><input type="submit" :disabled="disabled"/></p>
</form>
</div>
My component looks like this:
<template>
<div>
<div v-if="!loaded">
<p><i class="fas fa-spinner fa-spin"></i> Loading feed</p>
</div>
<div v-else>
<div data-slider ref="feedSlider" v-if="length > 0">
<div class="swiper-wrapper">
<div class="slide" v-for="record in records" :key="record.id">
<slot :record="record"></slot>
</div>
</div>
</div>
<div v-else>
<p>There are no records available.</p>
</div>
</div>
</div>
</template>
<script>
import Swiper from 'swiper';
import AjaxCaller from '../../mixins/AjaxCaller';
export default {
mixins: [AjaxCaller],
data() {
return {
loaded: false,
records: [],
length: 0,
}
},
mounted() {
this.makeCall(this.success, this.failure);
},
methods: {
success(response) {
this.loaded = true;
if (!response.data.records) {
return;
}
this.records = response.data.records;
this.length = this.records.length;
if (this.length < 2) {
return;
}
setTimeout(() => {
this.initiateSlider();
}, 1000);
},
initiateSlider() {
(new Swiper(this.$refs.feedSlider, {
effect: 'slide',
slideClass: 'slide',
slideActiveClass: 'slide-active',
slideVisibleClass: 'slide-visible',
slideDuplicateClass: 'slide-duplicate',
slidesPerView: 1,
spaceBetween: 0,
loop: true,
speed: 2000,
autoplay: {
delay: 5000,
},
autoplayDisableOnInteraction: false,
}));
},
failure(error) {
this.stopProcessing();
console.log(error);
}
}
}
</script>
The imported mixin AjaxCaller, which works fine with any other component:
<script>
export default {
props: {
url: {
type: String,
required: true
},
method: {
type: String,
default: 'post'
}
},
data() {
return {
processing: false
}
},
computed: {
getMethodParams() {
if (this.method === 'post') {
return {};
}
return this.requestData();
},
postMethodData() {
if (this.method === 'get') {
return {};
}
return this.requestData();
}
},
methods: {
requestData() {
return {};
},
startProcessing() {
this.processing = true;
this.startProcessingEvent();
},
stopProcessing() {
this.processing = false;
this.stopProcessingEvent();
},
startProcessingEvent() {},
stopProcessingEvent() {},
makeCall(success, failure) {
this.startProcessing();
window.axios.request({
url: this.url,
method: this.method,
params: this.getMethodParams,
data: this.postMethodData
})
.then(success)
.catch(failure);
}
}
}
</script>
And here's how I call it from within the view:
<feed-wrapper url="{{ route('front.news.feed') }}">
<div slot-scope="{ record }">
<p>
<a :href="record.uri" v-text="record.name"></a><br />
<span v-text="record.excerpt"></span>
</p>
</div>
</feed-wrapper>
Everything works fine in any browser other than IE 11 (and lower).
It even works in Edge - no issues what so ever.
In IE I get
[Vue warn]: Failed to generate render function:
Syntax Error: Expected identifier in ...
It doesn't even get to execute method call from within the mounted segment.
I use laravel-mix with Laravel so everything is compiled using webpack with babel so it's not ES6 related issue.
I've already spent whole night trying to un-puzzle this so any help would be much appreciated.
I know you've already said that you don't believe it's an ES6 issue but the evidence suggests it is.
IE11 doesn't support destructuring. If you type something like var {record} = {} into your IE11 console you'll see this same error message, 'Expected identifier'.
Try doing a search through the compiled code in your original error message and look for the word record. I suspect you'll find something like this:
fn:function({ record })
If you see that it means that the destructuring has made it to the browser without being compiled through Babel.
Exactly why this is happening depends on where you're using that scoped slot template. If you're using it inside a single-file component it should be going through Babel but if you aren't then it may be making it to the browser without transpiling. You said that you're calling it 'from within the view' but that doesn't clarify exactly how you're using it. There's a note about this in the docs, for what it's worth:
https://v2.vuejs.org/v2/guide/components-slots.html#Destructuring-slot-scope
Assuming you aren't able to fix the transpiling problem directly (e.g. by moving the template to somewhere it'll go through Babel) you can just remove the ES6 destructuring. So something like:
<div slot-scope="slotProps">
and then using slotProps.record instead of record in the code that follows.
I have a knockout viewModel and am wiring up jQuery Validation for it. One of the values, code, I want a remote check to ensure it's not already in use. The problem is that in my method for the remote validation, the self.code() call is returning the old value instead of the new one.
My Validate code (note I also tried a "more direct" method of getting the value, to no avail - same result):
form.validate({
rules: {
'plandetail-code': {
required: true,
remote: {
url: '/Plans/ValidatePlanCode',
type: 'POST',
data: {
id: self.id(),
code: self.code() //form.find('[name="plandetail-code"]').val()
}
}
},
'plandetail-name': "required"
}
});
Relevant Html:
<div class="form-group">
<label for="plandetail-code">Code</label>
<input type="text" name="plandetail-code" data-bind="textInput: code" class="form-control" />
</div>
My controller action is simple, but note that code always comes through as the original value:
[HttpPost]
public string ValidatePlanCode(int? id, string code) {
return _service.ValidatePlanCode(id, code) ? "true" : "false";
}
And here's my viewmodel: I run the form.Validate({}) before applying bindings (tried putting that after as well), and in the saveChanges method I check form.valid():
function PlanDetailVM(model) {
var self = this;
self.originalModel = model;
self.form = $('#pgPlan-plan-detail-form');
self.id = ko.observable(model.ID);
self.active = ko.observable(model.Active);
self.code = ko.observable(model.Code);
self.name = ko.observable(model.Name);
self.notes = ko.observable(model.notes);
self.dirty = ko.computed(function () { return isDirty(); });
self.save = function () { saveChanges(); }
self.cancel = function () { cancelChanges(); }
ko.applyBindings(self, document.getElementById('pgPlan-detail-container'));
initValidation(self.form);
return self;
function initValidation(form) {
form.validate({
rules: {
'plandetail-code': {
required: true,
remote: {
url: '/Plans/ValidatePlanCode',
type: 'POST',
data: {
id: self.id(),
code: self.code() //form.find('[name="plandetail-code"]').text()
}
}
},
'plandetail-name': "required"
}
});
}
function isDirty() { ... }
function saveChanges() {
if (!self.form.valid()) {
return;
}
// ajax snipped
}
function cancelChanges() { ... }
}
Repro:
Load initial view, Code has value AAAA
Change Code to BBBB
Observe controller action called
Controller action code param = AAAA
I'm unsure why I can't get the latest value from the text input. Am I missing something? Thanks
rules is an object which is evaluated immediately, so the data object will get created with default values if you use self.id() (since it returns value not function)
so you need to use it as functions
form.validate({
rules: {
'plandetail-code': {
required: true,
remote: {
url: '/Plans/ValidatePlanCode',
type: 'POST',
data: {
id: self.id, // function evaluated at runtime
code: self.code
}
}
},
'plandetail-name': "required"
}
});
I'm stuck in a reply function to intern messages: the email reply-sending function works fine (if I choose manually in the code the to field), but I'm failing, when I choose the message to reply, to select automatically the email in the contact-messages collection (field email) with my Meteor.methods.
In few words :
var to = "bob#bob.com" => ok
var to = this.email => no value catched
Here below my event on the reply form submit and the method
Event (can't catch var to = this.email)
Template.ContactReplyModal.events({
'click .send-message':function(e) {
e.preventDefault();
Meteor.call('replyMessage', this._id, function(error) {
if(error) {
Bert.alert({
title: 'Error',
message: error.reason,
type: 'danger'
});
} else {
var to = this.email;
var from = "my#mail.com";
var subject = $('#reply-subject').val();
var message = $('#reply-message').val();
if(message != '' && subject != '') {
Meteor.call('sendEmailContact', to, from, subject, message, function (error) {
if(error) {
Bert.alert({
title: 'Error',
message: error.reason,
type: 'danger'
});
} else {
$('#reply-message').val('');
$('#reply-subject').val('');
Bert.alert({
title: 'Success',
message: 'Message sended.',
type: 'success'
});
}
});
} else {
Bert.alert({
title: 'Error',
message: 'Message error.',
type: 'danger'
});
}
}
});
},
//Close events for ContactReplyModal
'click .close-login': ()=> {
Session.set('nav-toggle-contactreply', '');
},
'click .modal-overlay-contactreply': ()=> {
Session.set('nav-toggle-contactreply', '');
}
});
Method (using here the replyMessage function)
//Contact Method
Meteor.methods({
insertMessage: function(message) {
ContactMessages.insert(message);
},
openMessage: function(messageId) {
ContactMessages.update({_id: messageId}, {$set: {new: false}});
},
replyMessage: function(messageId) {
ContactMessages.findOne({_id: messageId});
},
deleteMessage: function(messageId) {
ContactMessages.remove({_id: messageId});
}
});
EDIT
The bind of the variable email with an arrow function doesn't work.
So maybe it is an issue of capturing the variable?
I cant' read console.log (this); and console.log (this.email); says undefined.
Here below is my message collection.
"_id": "6c3478WugEajr6zaw",
"name": "bob",
"email": "bob#bob.com",
"message": "This is a try.",
"submitted": "2017-01-05T15:19:04.642Z",
"new": true
I really don't understand, cause this below event works on the openMessage method (so the right message is correctly identified from the others)
//CLIENTSIDE
'click .open-message':function() {
Meteor.call('openMessage', this._id, function(error) {
if(error) {
Bert.alert({
title: 'Error',
message: error.reason,
type: 'danger'
});
}
});
}
//SERVERSIDE
Meteor.methods({
insertMessage: function(message) {
ContactMessages.insert(message);
},
openMessage: function(messageId) {
ContactMessages.update({_id: messageId}, {$set: {new: false}});
},
replyMessage: function(message) {
ContactMessages.findOne({_id: message});
},
deleteMessage: function(messageId) {
ContactMessages.remove({_id: messageId});
}
});
EDIT 2
As asked, below the template & the js linked to. The method is already showed and an example of the data in collection too.
template (contact-reply.html)
<template name="ContactReply">
<h3>Reply</h3>
<h3>To: {{email}}</h3>
<input class="form-control" type="text" name="reply-subject" id="reply-subject" placeholder="Subject"/>
<br>
<textarea class="form-control" name="reply-message" id="reply-message" rows="6"></textarea>
<br>
<button class="btn btn-success send-message">Send</button>
</template>
<template name="ContactReplyModal">
<div class="contactreply-modal {{$.Session.get 'nav-toggle-contactreply'}}">
<i class="fa fa-close close-login"></i>
<h3>Send a reply</h3>
{{> ContactReply}}
</div>
<div class="modal-overlay-contactreply"></div>
</template>
js of the template (contact-reply.js)
import './contact-reply.html';
Template.ContactReplyModal.events({
'click .send-message':function(e) {
e.preventDefault();
console.log(this);
Meteor.call('replyMessage', this._id, (error) => {
if(error) {
Bert.alert({
title: 'Error',
message: error.reason,
type: 'danger'
});
} else {
console.log (this.email);
const to = this.email;
var from = "my#mail.com";
var subject = $('#reply-subject').val();
var message = $('#reply-message').val();
if(message != '' && subject != '') {
Meteor.call('sendEmailContact', to, from, subject, message, (error) => {
if(error) {
Bert.alert({
title: 'Error',
message: error.reason,
type: 'danger'
});
} else {
$('#reply-message').val('');
$('#reply-subject').val('');
Bert.alert({
title: 'Success',
message: 'Message sended.',
type: 'success'
});
}
});
} else {
Bert.alert({
title: 'Error',
message: 'Message error.',
type: 'danger'
});
}
}
});
},
//Close events for ContactReplyModal
'click .close-login': ()=> {
Session.set('nav-toggle-contactreply', '');
},
'click .modal-overlay-contactreply': ()=> {
Session.set('nav-toggle-contactreply', '');
}
});
First, it is important to be sure that the data context is correct.
Each element within the template is rendered with a certain data context. If you target them in a template event handler, the data context will be available to the handler via this.
If you target an element that is not rendered by the current template (e.g, rendered by a third-party library or belongs to a sub-template), it will not have a data contest, which is what causes your data context to be undefined).
Having that fixed, assuming the data context (the external function's this) is indeed what you expect in the event handler (i.e, has an email field), you need to make it available to the callback. You can capture it in a local variable and make it available in a closure or lexically bind it with an arrow function:
Template.ContactReplyModal.events({
'click .send-message':function(e) {
e.preventDefault();
console.log(this); // to make sure that it is what you are expecting.
Meteor.call('replyMessage', this._id, (e) => { // note the arrow function
if(e) {
// ...
} else {
const to = this.email;
// ...
if(message != '' && subject != '') {
Meteor.call('sendEmailContact', to, from, subject, message, (e) => {
if(e) {
// ...
} else {
// ...
}
});
} else {
// ...
}
}
});
},
// ...
});
However, it does not seem like a good idea to use multiple nested method calls. It would probably be better to do it all in a single method call.
You can not access the template variable in template events using this, you can access them by the 2nd parameter in your events, here is your code, hope it will work
Template.ContactReplyModal.events({
'click .send-message'(e, instance) {
e.preventDefault();
Meteor.call('replyMessage', this._id, function(error) {
if(error) {
Bert.alert({
title: 'Error',
message: error.reason,
type: 'danger'
});
} else {
var to = instance.email;
var from = "my#mail.com";
var subject = $('#reply-subject').val();
var message = $('#reply-message').val();
if(message != '' && subject != '') {
Meteor.call('sendEmailContact', to, from, subject, message, function (error) {
if(error) {
Bert.alert({
title: 'Error',
message: error.reason,
type: 'danger'
});
} else {
$('#reply-message').val('');
$('#reply-subject').val('');
Bert.alert({
title: 'Success',
message: 'Message sended.',
type: 'success'
});
}
});
} else {
Bert.alert({
title: 'Error',
message: 'Message error.',
type: 'danger'
});
}
}
});
},
});
I'm trying to figure how to take an Image (a file using CollectionFS) and insert the Image's Id into my Items imageId field:
lib/collections/items.js
Items = new Mongo.Collection("items");
Items.attachSchema(new SimpleSchema({
name: {
type: String,
label: "Name",
},
userId: {
type: String,
regEx: SimpleSchema.RegEx.Id,
autoform: {
type: "hidden",
label: false
},
autoValue: function () { return Meteor.userId() },
},
image: {
type: String,
optional: true,
autoform: {
label: false,
afFieldInput: {
type: "fileUpload",
collection: "Images",
label: 'Select Photo',
}
}
},
imageId: {
type: String
}
}));
lib/collections/images.js
if (Meteor.isServer) {
var imageStore = new FS.Store.S3("images", {
accessKeyId: Meteor.settings.AWSAccessKeyId,
secretAccessKey: Meteor.settings.AWSSecretAccessKey,
bucket: Meteor.settings.AWSBucket,
});
Images = new FS.Collection("Images", {
stores: [imageStore],
filter: {
allow: {
contentTypes: ['image/*']
}
}
});
}
// On the client just create a generic FS Store as don't have
// access (or want access) to S3 settings on client
if (Meteor.isClient) {
var imageStore = new FS.Store.S3("images");
Images = new FS.Collection("Images", {
stores: [imageStore],
filter: {
allow: {
contentTypes: ['image/*']
},
}
});
}
Right now my allow rules are:
server/allows.js
Items.allow({
insert: function(userId, doc){return doc && doc.userId === userId;},
update: function(userId, doc){ return doc && doc.userId === userId;},
remove: function(userId, doc) { return doc && doc.userId === userId;},
})
Images.allow({
insert: function(userId, doc) { return true; },
update: function(userId,doc) { return true; },
remove: function(userId,doc) { return true; },
download: function(userId, doc) {return true;},
});
I'm using Autoform so my form looks like this:
client/item_form.html
<template name="insertItemForm">
{{#autoForm collection="Items" id="insertItemForm" type="insert"}}
{{> afQuickField name="name" autocomplete="off"}}
{{> afQuickField name="image" id="imageFile"}}
<button type="submit">Continue</button>
{{/autoForm}}
</template>
Right now when I select browse and select an image it will be in the database and I want to take that _id it has and place it in the Item that is created afterwards, but how do I fetch that particular image? I figured this is a good way to reference an image.
UPDATE 1
Find out the ID is actually located hidden after a file is selected:
<input type="hidden" class="js-value" data-schema-key="image" value="ma633fFpKHYewCRm8">
So I'm trying to get ma633fFpKHYewCRm8 to be placed as a String in the ImageId.
UPDATE 2
Maybe one way is to use FS.File Reference?
I have solved the same problem rather simpler, after file is inserted, I just call a method that does the related collection update:
client.html
<template name="hello">
<p>upload file for first texture: <input id="myFileInput1" type="file"> </p>
</template>
lib.js
var textureStore = new FS.Store.GridFS("textures");
TextureFiles = new FS.Collection("textures", {
stores: [textureStore]
});
Textures = new Mongo.Collection("textures");
client.js
Template.hello.events({
'change #myFileInput1': function(event, template) {
uploadTextureToDb('first',event);
}
});
function uploadTextureToDb(name, event) {
FS.Utility.eachFile(event, function(file) {
TextureFiles.insert(file, function (err, fileObj) {
// Inserted new doc with ID fileObj._id, and kicked off the data upload using HTTP
console.log('inserted');
console.log(fileObj);
//after file itself is inserted, we also update Texture object with reference to this file
Meteor.call('updateTexture',name,fileObj._id);
});
});
}
server.js
Meteor.methods({
updateTexture: function(textureName, fileId) {
Textures.upsert(
{
name:textureName
},
{
$set: {
file: fileId,
updatedAt: Date.now()
}
});
}
});
as you are using autoForm and simpleSchema, it might not be so easy, but I suggest to you to forget about autoForm and simpleSchema at first and try to make it work with simple html and default collections.
After everything works, you can go back to setting up those, but beware that there might be more issues when it comes to CollectionFS, especially when it comes to styling generated by autoForm.