I'm creating a custom directive to make a form submit via ajax - however I can't seem to get validation errors to bind to the Vue instance.
I am adding my directive here:
<form action="{{ route('user.settings.update.security', [$user->uuid]) }}" method="POST"
enctype="multipart/form-data" v-ajax errors="formErrors.security" data="formData.security">
My directive looks like:
Vue.directive('ajax', {
twoWay: true,
params: ['errors', 'data'],
bind: function () {
this.el.addEventListener('submit', this.onSubmit.bind(this));
},
update: function (value) {
},
onSubmit: function (e) {
e.preventDefault();
this.vm
.$http[this.getRequestType()](this.el.action, vm[this.params.data])
.then(this.onComplete.bind(this))
.catch(this.onError.bind(this));
},
onComplete: function () {
swal({
title: 'Success!',
text: this.params.success,
type: 'success',
confirmButtonText: 'Back'
});
},
onError: function (response) {
swal({
title: 'Error!',
text: response.data.message,
type: 'error',
confirmButtonText: 'Back'
});
this.set(this.vm, this.params.errors, response.data);
},
getRequestType: function () {
var method = this.el.querySelector('input[name="_method"]');
return (method ? method.value : this.el.method).toLowerCase();
},
});
And my VUE instance looks like:
var vm = new Vue({
el: '#settings',
data: function () {
return {
active: 'profile',
updatedSettings: getSharedData('updated'),
formData: {
security: {
current_password: ''
}
},
formErrors: {
security: []
},
}
},
methods: {
setActive: function (name) {
this.active = name;
},
isActive: function (name) {
if (this.active == name) {
return true;
} else {
return false;
}
},
hasError: function (item, array, sub) {
if (this[array][sub][item]) {
return true;
} else {
return false;
}
},
isInArray: function (value, array) {
return array.indexOf(value) > -1;
},
showNotification: function () {
if (this.updatedSettings) {
$.iGrowl({
title: 'Updated',
message: 'Your settings have been updated successfully.',
type: 'success',
});
}
},
}
});
However, when I output the data, the value for formErrors.security is empty
Any idea why?
The issue is with the this.set(/* ... */) line. this.set doesn't work the same as Vue.set or vm.$set.
this.set attempts to set the path that you passed to the directive: v-my-directive="a.b.c". So running this.set('foo') will attempt to set $vm.a.b.c to 'foo'.
What you want to do is this:
(ES6)
this.params.errors.splice(0, this.params.errors.length, ...response.data)
(Vanilla JS)
this.params.errors.splice.apply(this.params.errors, [0,this.params.errors.length].concat(response.data))
That will update whatever Object is tied to the errors param on the DOM Node. Make sure that you do v-bind:errors="formErrors.security" or :errors="formErrors.security".
Related
I'd like to extend (not override) the existing UI5 event of MultiComboBox component.
I found that https://github.com/SAP/openui5/blob/master/src/sap.m/src/sap/m/MultiComboBox.js
has MultiComboBox.prototype._handleSelectionLiveChange, how do I actually extend it in my own custom control?
Here is what I've done:
sap.ui.define([
'sap/m/MultiComboBox',
'sap/m/HBox'
], function (MultiComboBox, HBox) {
return MultiComboBox.extend('TokenizedMultiComboBox', {
metadata: {
aggregations: {
_hbox: { type: 'sap.m.HBox', multiple: false }
}
},
init: function () {
MultiComboBox.prototype.init.apply(this, arguments);
this.setAggregation('_hbox', new HBox({
items: []
}));
},
onAfterRendering: function() {
const hbox = this.getAggregation('_hbox');
},
_handleSelectionLiveChange: function() {
// should my code go here or?
},
renderer: function (rm, oControl) {
sap.m.MultiComboBoxRenderer.render(rm, oControl);
rm.write('<div');
rm.writeControlData(oControl);
rm.write('>');
rm.write('<div>');
rm.renderControl(oControl.getAggregation('_hbox'));
rm.write('</div>');
rm.write('</div>');
},
})
})
I am using aldeed:autoform in order to render a form and run its result through a Meteor.method(). My form looks as follows:
SelectPlanTemplates = new SimpleSchema({
templates: {
type: [String],
autoform: {
options: function() {
return PlanTemplates.find().map(function(doc) {
return { label: doc.title, value: doc._id };
});
},
noselect: true
}
},
userId: {
type: String,
allowedValues: function() {
return Meteor.users.find().map(function(doc) {
return doc._id;
});
},
autoform: {
omit: true
}
}
});
On my template, I just do the following.
+ionContent
+quickForm(schema="SelectPlanTemplates" id="SelectPlanTemplatesForm" type="method" meteormethod="createPlanFromTemplates")
My url is constructed like /plan/from_templates/{:userId}. I tried creating a hook to add the user id before submitting it.
AutoForm.hooks({
SelectPlanTemplatesForm: {
before: {
method: function(doc) {
doc.userId = Router.current().params.userId;
return doc;
}
}
}
});
However, it never seems to get to this hook.
How would I take a route parameter and pass it with my form to a meteor method with auto form?
I think I figured out a little bit of a weird way of do it.
In the router:
this.route('selectPlans', {
waitOn: function() {
return Meteor.subscribe('plan_templates');
},
path: '/select/plan_templates/:_id',
template: 'selectTemplates',
data: function() {
return new selectPlanTemplates({ userId: this.params._id });
}
});
Then I added doc=this to my template
Which interface or component do you suggest to display the state of parallel async calls? (The language is not so important for me, just the pattern, I can rewrite the same class / interface in javascript...)
I load model data from REST service, and I want to display pending label before the real content, and error messages if something went wrong... I think this is a common problem, and there must be an already written component, or best practices, or a pattern for this. Do you know something like that?
Here is a spaghetti code - Backbone.syncParallel is not an existing function yet - which has 2 main states: updateForm, updated. Before every main state the page displays the "Please wait!" label, and by error the page displays an error message. I think this kind of code is highly reusable, so I think I can create a container which automatically displays the current state, but I cannot decide what kind of interface this component should have...
var content = new Backbone.View({
appendTo: "body"
});
content.render();
var role = new Role({id: id});
var userSet = new UserSet();
Backbone.syncParallel({
models: [role, userSet],
run: function (){
role.fetch();
userSet.fetch();
},
listeners: {
request: function (){
content.$el.html("Please wait!");
},
error: function (){
content.$el.html("Sorry, we could not reach the data on the server!");
},
sync: function (){
var form = new RoleUpdateForm({
model: role,
userSet: userSet
});
form.on("submit", function (){
content.$el.html("Please wait!");
role.save({
error: function (){
content.$el.html("Sorry, we could not save your modifications, please try again!");
content.$el.append(new Backbone.UI.Button({
content: "Back to the form.",
onClick: function (){
content.$el.html(form.$el);
}
}));
},
success: function (){
content.$el.html("You data is saved successfully! Please wait until we redirect you to the page of the saved role!");
setTimeout(function (){
controller.read(role.id);
}, 2000);
}
});
}, this);
form.render();
content.$el.html(form.$el);
}
}
});
I created a custom View to solve this problem. (It is in beta version now.)
Usage: (Form is a theoretical form generator)
var content = new SyncLabelDecorator({
appendTo: "body",
});
content.load(function (){
this.$el.append("normal html without asnyc calls");
});
var User = Backbone.Model.extend({
urlRoot: "/users"
});
var UserSet = Backbone.Collection.extend({
url: "/users",
model: User
});
var Role = Backbone.RelationalModel.extend({
relations: [{
type: Backbone.HasMany,
key: 'members',
relatedModel: User
}]
});
var administrator = new Role({id :1});
var users = new UserSet();
content.load({
fetch: [role, users],
sync: function (){
var form = new Form({
title: "Update role",
model: role,
fields: {
id: {
type: "HiddenInput"
},
name: {
type: "TextInput"
},
members: {
type: "TwoListSelection",
alternatives: users
}
},
submit: function (){
content.load({
tasks: {
save: role
},
sync: function (){
this.$el.html("Role is successfully saved.");
}
});
}
});
this.$el.append(form.render().$el);
}
});
Code:
var SyncLabelDecorator = Backbone.View.extend({
options: {
pendingMessage: "Sending request. Please wait ...",
errorMessage: "An unexpected error occured, we could not process your request!",
load: null
},
supported: ["fetch", "save", "destroy"],
render: function () {
if (this.options.load)
this.load();
},
load: function (load) {
if (load)
this.options.load = load;
this._reset();
if (_.isFunction(this.options.load)) {
this.$el.html("");
this.options.load.call(this);
return;
}
_(this.options.load.tasks).each(function (models, method) {
if (_.isArray(models))
_(models).each(function (model) {
this._addTask(model, method);
}, this);
else
this._addTask(models, method);
}, this);
this._onRun();
_(this.tasks).each(function (task) {
var model = task.model;
var method = task.method;
var options = {
beforeSend: function (xhr, options) {
this._onRequest(task, xhr);
}.bind(this),
error: function (xhr, statusText, error) {
this._onError(task, xhr);
}.bind(this),
success: function (data, statusText, xhr) {
this._onSync(task, xhr);
}.bind(this)
};
if (model instanceof Backbone.Model) {
if (method == "save")
model[method](null, options);
else
model[method](options);
}
else {
if (method in model)
model[method](options);
else
model.sync(method == "fetch" ? "read" : (method == "save" ? "update" : "delete"), model, options);
}
}, this);
},
_addTask: function (model, method) {
if (!_(this.supported).contains(method))
throw new Error("Method " + method + " is not supported!");
this.tasks.push({
method: method,
model: model
});
},
_onRun: function () {
this.$el.html(this.options.pendingMessage);
if (this.options.load.request)
this.options.load.request.call(this);
},
_onRequest: function (task, xhr) {
task.abort = function () {
xhr.abort();
};
},
_onError: function (task, xhr) {
this._abort();
this.$el.html(this.options.errorMessage);
if (this.options.load.error)
this.options.load.error.call(this);
},
_onSync: function (task, xhr) {
++this.complete;
if (this.complete == this.tasks.length)
this._onEnd();
},
_onEnd: function () {
this.$el.html("");
if (this.options.load.sync)
this.options.load.sync.call(this);
},
_reset: function () {
this._abort();
this.tasks = [];
this.complete = 0;
},
_abort: function () {
_(this.tasks).each(function (task) {
if (task.abort)
task.abort();
});
}
});
I have started learning the javascript module pattern and I have the following code:
// PersonalInformation.js
var PersonallInformation = (function () {
$.validator.addMethod("checkPhoneNumber", function (value, element) {
if (!value) return true;
return /^((\+7)|8)(700|701|702|705|707|712|713|717|718,721|725|726|727|777)[0-9]{7}$/.test(value);
}, "Wrong phone format");
function updateQTip() {
$('div.invalid_form').qtip({
content: function (api) {
var text = $(this).prev();
return "<div class='tip_cont'><span class='simple cost'><span class='corner'></span>" + $(text).attr('data-description') + "</span></div>";
},
position: {
target: 'mouse',
adjust: { x: 5, y: 17 }
},
style: {
tip: { corner: false }
}
});
}
function updateError() {
$('.invalid_form').closest('.wrap_input').addClass('error');
$('#reg_form_pay input').each(function(element) {
if ($(this).hasClass('invalid_form')) {
$(this).closest('.wrap_input').addClass('error');
} else {
$(this).closest('.wrap_input').removeClass('error');
}
});
updateQTip();
}
function validateForm() {
$("#reg_form_pay").validate({
rules: {
Email: { required: true, email: true },
PhoneNumber: { required: true, checkPhoneNumber: true },
FirstName: { required: true },
Surname: { required: true }
},
messages: {
Email: '',
PhoneNumber: '',
FirstName: '',
Surname: ''
},
errorClass: "invalid_form",
errorElement: "div",
errorPlacement: function (error, element) {
error.insertAfter(element);
},
onkeyup: false,
showErrors: function (errorMap, errorList) {
this.defaultShowErrors();
updateError();
}
});
}
function privateInit() {
validateForm();
console.log('init ok');
}
return {
init: privateInit,
};
}());
To make this code work I have to call the init method in the view as follows:
<script>
$(document).ready(function() {
PersonallInformation.init();
})
</script>
Is it possible to avoid having to call init in the view?
UPDATE:
I have rewritten it in the following way:
function library(module) {
$(function() {
if (module.init) {
module.init();
}
});
return module;
}
var PersonallInformation = library(function () {
...
The short answer is no
your validateForm method uses the DOM to do it's work. If you call the init method prior to document.ready the behavior is unspecified.
You will have to call some method in document.ready.
You are not really using the module partern since you are in effect just encapsulating function in another function so you would have the same kind of encapsulation if you moved the entire code between (function (){...}()) to document.ready in which case you could change the last part of the function to
validateForm();
console.log('init ok');
Ie inline the init function.
EDIT
A rewrite could be something like:
var setupValidate = (function () {
return function(options) {
var defaultOptions = {
qtipOptions : {
content: function (api) {
var text = $(this).prev();
return "<div class='tip_cont'><span class='simple cost'><span class='corner'></span>" + $(text).attr('data-description') + "</span></div>";
},
position: {
target: 'mouse',
adjust: { x: 5, y: 17 }
},
style: {
tip: { corner: false }
}
},
formSelector : "#reg_form_pay",
invalidFormSelector : 'div.invalid_form',
};
options = $.extend(defaultOptions,options);
$.validator.addMethod("checkPhoneNumber", function (value, element) {
if (!value) return true;
return /^((\+7)|8)(700|701|702|705|707|712|713|717|718,721|725|726|727|777)[0-9]{7}$/.test(value);
}, "Wrong phone format");
function updateQTip() {
$(options.invalidFormSelector).qtip();
}
function updateError() {
$(options.invalidFormSelector).closest('.wrap_input').addClass('error');
$(options.formSelector).find("input").each(function(element) {
if ($(this).hasClass('invalid_form')) {
$(this).closest('.wrap_input').addClass('error');
} else {
$(this).closest('.wrap_input').removeClass('error');
}
});
updateQTip();
}
function validateForm() {
$(formSelector).validate({...});
}
validateForm();
console.log('init ok');
}
}());
and you'd then call it like:
$(function(){
setupValidate (/*with options if you'd like to change the default*/);
});
connectLoadRenderStoreAndGetCheckBox: function () {
this.someStore = new Ext.data.Store({
proxy:
reader:
]),
sortInfo: { field: , direction: }
});
this.someStore.load(
{
params:
{
}
});
//I left out parameters; lets assume they are valid and work.
this.someStore.on("load", this._renderColumns, this);
return ([this._getCheckBox()]);
}
On 'load' I want to both execute the function this._renderColumns (which uses the data from the server) and also return a checkbox (which also uses data from the server).
What is a quick & easy way to only return the checkbox after the data is loaded?
_getCheckBox: function () {
if (UseDefault == "y") {
return new Ext.form.Checkbox(
{
fieldLabel:,
itemCls:,
checked: true,
labelStyle:
});
}
else {
return new Ext.form.Checkbox(
{
fieldLabel:,
itemCls:,
checked: false,
labelStyle:
});
}
},
_renderColumns: function () {
var record2 = this.something.something2(2);
UseDefault = record2.get("UseDefault");
}