v2.canGoForward is not a function - javascript

I am trying to move the code vm.canGoForward from my controller to a service to hide the implementation details.
BEFORE CODE CHANGE
This worked fine.
View:
<button ng-disabled="!vm.canGoForward()" class="btn btn-primary" name="next" type="button" ng-click="vm.gotoStep(vm.currentStep + 1)">
Controller:
var vm = this;
vm.currentStep = 1;
vm.steps = WizardService.getWizardSteps(vm.formData);
vm.canGoForward = function() {
var res = true,
i,
nextStateIndex = vm.currentStep + 1;
if (nextStateIndex > vm.steps.length) {
return false;
}
for (i = 1; res && i <= nextStateIndex; i++) {
res = (res && vm.steps[i-1].isReady());
}
return !!res;
};
Service
var wizardService = {
getWizardSteps: getWizardSteps
};
return wizardService;
function getWizardSteps(formData) {
var wizardSteps = [
{
step: 1,
name: 'Name',
template: 'views/wizard/step1.html',
isReady: function() { return true; }
},
{
step: 2,
name: 'Email',
template: 'views/wizard/step2.html',
isReady: function() { return formData.firstName && formData.lastName; }
},
{
step: 3,
name: 'Job Category',
template: 'views/wizard/step3.html',
isReady: function() { return formData.email; }
}
];
return wizardSteps;
}
AFTER CODE CHANGE
View
Remains the same
Controller
var vm = this;
vm.currentStep = 1;
vm.steps = WizardService.getWizardSteps(vm.formData);
vm.canGoForward = WizardService.canGoForward(vm.currentStep, vm.steps);
Service
var wizardService = {
getWizardSteps: getWizardSteps,
canGoForward: canGoForward
};
return wizardService;
function getWizardSteps(formData) {
var wizardSteps = [
{
step: 1,
name: 'Name',
template: 'views/wizard/step1.html',
isReady: function() { return true; }
},
{
step: 2,
name: 'Email',
template: 'views/wizard/step2.html',
isReady: function() { return formData.firstName && formData.lastName; }
},
{
step: 3,
name: 'Job Category',
template: 'views/wizard/step3.html',
isReady: function() { return formData.email; }
}
];
return wizardSteps;
}
function canGoForward(currentStep, steps) {
console.log(steps);
var res = true,
i,
nextStateIndex = currentStep + 1;
if (nextStateIndex > steps.length) {
return false;
}
for (i = 1; res && i <= nextStateIndex; i++) {
res = (res && steps[i-1].isReady());
}
return !!res;
}
I now get the following error: TypeError: v2.canGoForward is not a function. How can I resolve it?

In your second version, the following line will actually call WizardService.canGoForward on the spot, not assign it:
vm.canGoForward = WizardService.canGoForward(vm.currentStep, vm.steps);
What gets assigned is the return value of that call, which obviously is not a function, hence the error message when a call is attempted later.
If you want to assign the function, and ensure the arguments get passed when it is called later, then use bind:
vm.canGoForward = WizardService.canGoForward.bind(WizardService, vm.currentStep, vm.steps);

Related

Vue infinite UI update loop with function as param

I'm new in Vue.js and tried to convert some legacy code for pagination. I've created a pager component which accepts a function as one of its params. But it's causing an infinite UI render loop.
Could you help me to resolve or suggest some solution for such problem?
Here is my pager component js:
const PagerComponent = {
name: "pagerComponent",
template: "#pagerComponent",
props: {
pageSize: Number,
pageIndex: Number,
totalPages: Number,
totalRecords: Number,
pageSlide: Number,
hasNextPage: Boolean,
hasPrevPage: Boolean,
pages: Array,
loadFunc: Function
},
data() {
return {
pager: {
pageSize: 0,
pageIndex: 0,
totalPages: 0,
totalCount: 0,
pageSlide: 1,
hasNextPage: false,
hasPrevPage: false,
pages: [],
loadFunc: function () { }
}
}
},
methods: {
load(index) {
this.pager.pageIndex = index;
if (this.pager.loadFunc != null) {
this.pager.loadFunc();
}
},
isActivePage(page) {
return this.pager.pageIndex + 1 == page;
},
update(newPager) {
this.pager.pageSize = newPager.pageSize;
this.pager.pageIndex = newPager.pageIndex;
this.pager.totalPages = newPager.totalPages;
this.pager.totalCount = newPager.totalCount;
this.pager.hasNextPage = newPager.hasNextPage;
this.pager.hasPrevPage = newPager.hasPrevPage;
this.generatePages();
},
generatePages() {
this.pager.pages = [];
var pageNum = this.pager.pageIndex + 1;
var pageFrom = Math.max(1, pageNum - this.pager.pageSlide);
var pageTo = Math.min(this.pager.totalPages, pageNum + this.pager.pageSlide);
pageFrom = Math.max(1, Math.min(pageTo - this.pager.pageSlide, pageFrom));
pageTo = Math.min(this.pager.totalPages, Math.max(pageFrom + this.pager.pageSlide, pageNum == 1 ? pageTo + this.pager.pageSlide : pageTo));
for (var i = pageFrom; i <= pageTo; i++) {
this.pager.pages.push(i);
}
}
},
computed: {
hasPages() {
if (this.pager.pages == null)
return false;
return this.pager.pages.length > 0;
},
doNotHavePrevPage() {
return !this.pager.hasPrevPage;
},
doNotHaveNextPage() {
return !this.pager.hasNextPage;
}
},
beforeMount() {
this.pager.pageSize = this.pageSize;
this.pager.pageIndex = this.pageIndex;
this.pager.totalPages = this.totalPages;
this.pager.totalCount = this.totalRecords;
this.pager.hasNextPage = this.hasNextPage;
this.pager.hasPrevPage = this.hasPrevPage;
this.pager.loadFunc = this.loadFunc;
this.pager.pages = this.pages || [];
this.generatePages();
},
mounted() {
}
}
Here is how it's used in html:
<pager-Component v-bind="Pager" v-bind:load-Func="GetItems" ref="pager"></pager-Component>
And GetItems funciton:
function () {
var self = this;
const data = {
Pager: self.Pager,
Filter: []
};
$.ajax({
url: self.GetItemsUrl,
type: "POST",
dataType: "json",
busy: self.Loading,
data: data
}).done(function (result) {
if (result.isSuccess) {
self.$refs.pager.update(result.data.pager);
self.Items.splice(0);
result.data.items.map(function (value, key) {
self.Items.push(value);
});
}
else {
alert(result.data.errors[0]);
}
});
}
Finally after tones of tests, the solution was found and it's pretty easy.
I just needed to use v-on:click instead of :click. I just don't know why lot of tutorials suggest to use :click if it doesn't work
So for example use
<div v-on:click="load(pageIndex)">My button</div>
instead of
<div :click="load(pageIndex)">My button</div>

Customizing easyAutoComplete

var options = {
url: function (phrase) {
return "location/autoComplete?key=" + phrase;
},
getValue: "cityName",
template: {
type: "custom",
method: function (value, item) {
return value + ", " + item.stateName;
}
},
list: {
onClickEvent: function () {
var selectedLongitude = $("#autoCompleteSearch").getSelectedItemData().longitude;
var selectedLatitude = $("#autoCompleteSearch").getSelectedItemData().latitude;
userPos = {
lat: Number(selectedLatitude),
lng: Number(selectedLongitude)
};
map.setCenter(userPos);
map.setZoom(17)
},
showAnimation: {
type: "fade", //normal|slide|fade
time: 400,
callback: function () {
}
},
hideAnimation: {
type: "slide", //normal|slide|fade
time: 400,
callback: function () {
}
}
}
};
This is the code I am using right now to fetch a remote webservice data. Now I need to get the data and remove the duplicate "city names". Also I need to show "Data not available" or some custom method when there is no data from the webservice. I am kind of stuck. Would be great if someone could point me on how to achieve this.
You can get rid of duplicates using a helper function:
function removeDuplicates(input) {
if (!input) { //Falsy input
return input;
}
var isArray = Array.isArray(input) ? [] : {};
var items = [];
var output = {};
for (var key in input) {
if (items.indexOf(input[key]) < 0) {
items.push(input[key]);
if (!isArray) {
output[key] = input[key];
}
}
}
return isArray ? items : output;
}
You can show your custom text if Array.isArray(input) ? (input.length === 0) : (Object.keys(input).length === 0 && input.constructor === Object).

JavaScript Class: Dynamic Default Value

Trying to set creditCardExpMonth to the current month in the below Magento 2 JavaScript class (cc-form.js). The option month values are 1-12. When I manually add a month value like 3 creditCardExpMonth: 3, to the defaults:{ }, it works as expected. I just can't seem to figure out how to set it to the current month dynamically. I'm open to any solution that allows for the value to be overridden by the user's selection but I'd prefer it be inside this class or on the html page and not a JQuery update after the page loads.
I created a getCurrentMonth() function in this class but couldn't figure out how to access it correctly to set creditCardExpMonth to a default value.
define(
[
'underscore',
'Mageplaza_Osc/js/view/payment/default',
'Magento_Payment/js/model/credit-card-validation/credit-card-data',
'Magento_Payment/js/model/credit-card-validation/credit-card-number-validator',
'mage/translate'
],
function (_, Component, creditCardData, cardNumberValidator, $t) {
return Component.extend({
defaults: {
creditCardType: '',
creditCardExpYear: '',
creditCardExpMonth: '',
creditCardNumber: '',
creditCardSsStartMonth: '',
creditCardSsStartYear: '',
creditCardVerificationNumber: '',
selectedCardType: null
},
initObservable: function () {
this._super()
.observe([
'creditCardType',
'creditCardExpYear',
'creditCardExpMonth',
'creditCardNumber',
'creditCardVerificationNumber',
'creditCardSsStartMonth',
'creditCardSsStartYear',
'selectedCardType'
]);
return this;
},
initialize: function() {
var self = this;
this._super();
//Set credit card number to credit card data object
this.creditCardNumber.subscribe(function(value) {
var result;
self.selectedCardType(null);
if (value == '' || value == null) {
return false;
}
result = cardNumberValidator(value);
if (!result.isPotentiallyValid && !result.isValid) {
return false;
}
if (result.card !== null) {
self.selectedCardType(result.card.type);
creditCardData.creditCard = result.card;
}
if (result.isValid) {
creditCardData.creditCardNumber = value;
self.creditCardType(result.card.type);
}
});
//Set expiration year to credit card data object
this.creditCardExpYear.subscribe(function(value) {
creditCardData.expirationYear = value;
});
//Set expiration month to credit card data object
this.creditCardExpMonth.subscribe(function(value) {
creditCardData.expirationYear = value;
});
//Set cvv code to credit card data object
this.creditCardVerificationNumber.subscribe(function(value) {
creditCardData.cvvCode = value;
});
},
getCode: function() {
return 'cc';
},
getData: function() {
return {
'method': this.item.method,
'additional_data': {
'cc_cid': this.creditCardVerificationNumber(),
'cc_ss_start_month': this.creditCardSsStartMonth(),
'cc_ss_start_year': this.creditCardSsStartYear(),
'cc_type': this.creditCardType(),
'cc_exp_year': this.creditCardExpYear(),
'cc_exp_month': this.creditCardExpMonth(),
'cc_number': this.creditCardNumber()
}
};
},
getCcAvailableTypes: function() {
return window.checkoutConfig.payment.ccform.availableTypes[this.getCode()];
},
getIcons: function (type) {
return window.checkoutConfig.payment.ccform.icons.hasOwnProperty(type)
? window.checkoutConfig.payment.ccform.icons[type]
: false
},
getCcMonths: function() {
return window.checkoutConfig.payment.ccform.months[this.getCode()];
},
getCcYears: function() {
return window.checkoutConfig.payment.ccform.years[this.getCode()];
},
hasVerification: function() {
return window.checkoutConfig.payment.ccform.hasVerification[this.getCode()];
},
hasSsCardType: function() {
return window.checkoutConfig.payment.ccform.hasSsCardType[this.getCode()];
},
getCvvImageUrl: function() {
return window.checkoutConfig.payment.ccform.cvvImageUrl[this.getCode()];
},
getCvvImageHtml: function() {
return '<img src="' + this.getCvvImageUrl()
+ '" alt="' + $t('Card Verification Number Visual Reference')
+ '" title="' + $t('Card Verification Number Visual Reference')
+ '" />';
},
getSsStartYears: function() {
return window.checkoutConfig.payment.ccform.ssStartYears[this.getCode()];
},
getCcAvailableTypesValues: function() {
return _.map(this.getCcAvailableTypes(), function(value, key) {
return {
'value': key,
'type': value
}
});
},
getCcMonthsValues: function() {
return _.map(this.getCcMonths(), function(value, key) {
return {
'value': key,
'month': value.substring(0,2)
}
});
},
getCcYearsValues: function() {
return _.map(this.getCcYears(), function(value, key) {
return {
'value': key,
'year': value
}
});
},
getCurrentMonth: function() {
var d = new Date();
var n = d.getMonth() + 1;
return n;
},
getCurrentYear: function() {
var d = new Date();
var n = d.getYear();
return n;
},
getSsStartYearsValues: function() {
return _.map(this.getSsStartYears(), function(value, key) {
return {
'value': key,
'year': value
}
});
},
isShowLegend: function() {
return false;
},
getCcTypeTitleByCode: function(code) {
var title = '';
_.each(this.getCcAvailableTypesValues(), function (value) {
if (value['value'] == code) {
title = value['type'];
}
});
return title;
},
formatDisplayCcNumber: function(number) {
return 'xxxx-' + number.substr(-4);
},
getInfo: function() {
return [
{'name': 'Credit Card Type', value: this.getCcTypeTitleByCode(this.creditCardType())},
{'name': 'Credit Card Number', value: this.formatDisplayCcNumber(this.creditCardNumber())}
];
}
});
});
Here is the knockout HTML with select data-bind just in case it's needed (taken from Magento payment cc-form.html):
<select name="payment[cc_exp_month]"
class="select select-month"
data-bind="attr: {id: getCode() + '_expiration', 'data-container': getCode() + '-cc-month', 'data-validate': JSON.stringify({required:true, 'validate-cc-exp':'#' + getCode() + '_expiration_yr'})},
enable: isActive($parents),
options: getCcMonthsValues(),
optionsValue: 'value',
optionsText: 'month',
optionsCaption: $t('Month'),
value: creditCardExpMonth">
</select>
If this script is run every time the page loads, you could do something like this:
defaults: {
creditCardType: '',
creditCardExpYear: '',
creditCardExpMonth: (function(){
return ((new Date()).getMonth()+1);
})(),
creditCardNumber: '',
creditCardSsStartMonth: '',
creditCardSsStartYear: '',
creditCardVerificationNumber: '',
selectedCardType: null
}
or if you want something cleaner, you can refactor the code into a function that is defined prior to this object creation:
function (_, Component, creditCardData, cardNumberValidator, $t) {
function getCurrentMonth() {
return ((new Date()).getMonth()+1);
}
return Component.extend({
defaults: {
creditCardType: '',
creditCardExpYear: '',
creditCardExpMonth: getCurrentMonth(),
creditCardNumber: '',
creditCardSsStartMonth: '',
creditCardSsStartYear: '',
creditCardVerificationNumber: '',
selectedCardType: null
},

Angularjs Directives Scope Issue

I am having trouble with a directive. My model updates with data that is being loaded by a http call. Without the call the service the directive works correctly.
I have a directive called validationmessagefor that listens to when a field has been marked to have an error. When it has an error it displays the error message.
The plunker attached Plnkr
How do I get the directive to respond or load correctly against the model when using services to load data?
angular.module('dataApp', ['servicesModule', 'directivesModule'])
.controller('dataCtrl', ['$scope', 'ProcessService', 'ValidationRuleFactory', 'Validator',
function($scope, ValidationRuleFactory, Validator, ProcessService) {
$scope.viewModel = {};
var FormFields = {};
// we would get this from the meta api
ProcessService.getProcessMetaData().then(function(data) {
FormFields = {
Name: "Course",
Fields: [{
type: "text",
Name: "name",
label: "Name",
data: "",
required: true,
ruleSet: "personFirstNameRules"
}, {
type: "text",
Name: "description",
label: "Description",
data: "",
required: true,
ruleSet: "personEmailRules"
}]
};
$scope.viewModel = FormFields;
ProcessService.getProcessRuleData().then(function(data) {
var genericErrorMessages = {
required: 'Required',
minlength: 'value length must be at least %s characters',
maxlength: 'value length must be less than %s characters'
};
var rules = new ValidationRuleFactory(genericErrorMessages);
$scope.viewModel.validationRules = {
personFirstNameRules: [rules.isRequired(), rules.minLength(3)],
personEmailRules: [rules.isRequired(), rules.minLength(3), rules.maxLength(7)]
};
$scope.viewModel.validator = new Validator($scope.viewModel.validationRules);
});
});
var getRuleSetValuesMap = function() {
return {
personFirstNameRules: $scope.viewModel.Fields[0].data,
personEmailRules: $scope.viewModel.Fields[1].data
};
};
$scope.save = function() {
$scope.viewModel.validator.validateAllRules(getRuleSetValuesMap());
if ($scope.viewModel.validator.hasErrors()) {
$scope.viewModel.validator.triggerValidationChanged();
return;
} else {
alert('person saved in!');
}
};
}
]);
The directive to display the custom error message
(function(angular, $) {
angular.module('directivesModule')
.directive('validationMessageFor', [function() {
return {
restrict: 'A',
scope: {eID: '#val'},
link: function(scope, element, attributes) {
//var errorElementId = attributes.validationMessageFor;
attributes.$observe('validationMessageFor', function(value) {
errorElementId = value;
if (!errorElementId) {
return;
}
var areCustomErrorsWatched = false;
var watchRuleChange = function(validationInfo, rule) {
scope.$watch(function() {
return validationInfo.validator.ruleSetHasErrors(validationInfo.ruleSetName, rule.errorCode);
}, showErrorInfoIfNeeded);
};
var watchCustomErrors = function(validationInfo) {
if (!areCustomErrorsWatched && validationInfo && validationInfo.validator) {
areCustomErrorsWatched = true;
var validator = validationInfo.validator;
var rules = validator.validationRules[validationInfo.ruleSetName];
for (var i = 0; i < rules.length; i++) {
watchRuleChange(validationInfo, rules[i]);
}
}
};
// get element for which we are showing error information by id
var errorElement = $("#" + errorElementId);
var errorElementController = angular.element(errorElement).controller('ngModel');
var validatorsController = angular.element(errorElement).controller('validator');
var getValidationInfo = function() {
return validatorsController && validatorsController.validationInfoIsDefined() ? validatorsController.validationInfo : null;
};
var validationChanged = false;
var subscribeToValidationChanged = function() {
if (validatorsController.validationInfoIsDefined()) {
validatorsController.validationInfo.validator.watchValidationChanged(function() {
validationChanged = true;
showErrorInfoIfNeeded();
});
// setup a watch on rule errors if it's not already set
watchCustomErrors(validatorsController.validationInfo);
}
};
var getErrorMessage = function(value) {
var validationInfo = getValidationInfo();
if (!validationInfo) {
return '';
}
var errorMessage = "";
var errors = validationInfo.validator.errors[validationInfo.ruleSetName];
var rules = validationInfo.validator.validationRules[validationInfo.ruleSetName];
for (var errorCode in errors) {
if (errors[errorCode]) {
var errorCodeRule = _.findWhere(rules, {errorCode: errorCode});
if (errorCodeRule) {
errorMessage += errorCodeRule.validate(value).errorMessage;
break;
}
}
}
return errorMessage;
};
var showErrorInfoIfNeeded = function() {
var validationInfo = getValidationInfo();
if (!validationInfo) {
return;
}
var needsAttention = validatorsController.ruleSetHasErrors() && (errorElementController && errorElementController.$dirty || validationChanged);
if (needsAttention) {
// compose and show error message
var errorMessage = getErrorMessage(element.val());
// set and show error message
element.text(errorMessage);
element.show();
} else {
element.hide();
}
};
subscribeToValidationChanged();
if (errorElementController)
{
scope.$watch(function() {
return errorElementController.$dirty;
}, showErrorInfoIfNeeded);
}
scope.$watch(function() {
return validatorsController.validationInfoIsDefined();
}, subscribeToValidationChanged());
});
}
};
}]);
})(angular, $);

How to use jQuery $.extend(obj1, obj2)

I'm trying to create a button class that extends an AbstractComponent class using $.extend() but the functions in AbstractComponent aren't available when I'm constructing the button.
The specific error I'm receiving is:
Uncaught TypeError: Object [object Object] has no method 'setOptions'
var Button = {};
var abstract = new AbstractComponent;
$.extend(Button,abstract);
//debugger;
//this.setOptions is available here
Button = function(options) {
'use strict';
var defaultOptions = {
templateName: '#button-tmpl',
title: "Label goes here",
type: "primary",
size: "medium",
disabled: null,
autosave: null,
href: null,
onclick: null
};
//debugger
//this.setOptions is not available here
this.setOptions(options, defaultOptions);
this.checkRequiredKeys('title');
return this;
};
Button.prototype.updateOptions = function() {
var options = this.options;
if (options.href === null) {
options.href = 'javascript:;';
}
if (options.disabled === null) {
options.disabled = 'disabled';
}
if (options.autosave === true) {
options.autosave = 'ping-autosave';
}
};
AbstractComponent.js
var AbstractComponent = function() {
console.log('this will be the constructor for elements extending this class');
};
AbstractComponent.prototype.show = function() {
this.render();
};
AbstractComponent.prototype.close = function() {
// stop listeners and remove this component
this.stopListening();
this.remove();
};
AbstractComponent.prototype.getTemplateName = function() {
return this.options.templateName;
};
AbstractComponent.prototype.checkRequiredKeys = function() {
var errors = new Array();
if (typeof this.getTemplateName() === "undefined") {
errors.push('templateName');
}
for (var i = 0; i < arguments.length; i++) {
if (!this.options.hasOwnProperty(arguments[i])) {
errors.push(arguments[i]);
}
}
if (errors.length > 0) {
throw new Exception("Required property(s) not found:" + errors.join(', ') + " in " + this.toString());
}
};
AbstractComponent.prototype.getElement = function() {
'use strict';
if(!this.options.updated) {
this.updateOptions();
}
return new AbstractView(this.options).render().$el;
};
AbstractComponent.prototype.updateOptions = function() {
this.options.updated = true;
return true;
};
AbstractComponent.prototype.getHtml = function() {
return this.getElement().html();
};
AbstractComponent.prototype.setOptions = function(options, defaultOptions) {
this.options = _.defaults(options, defaultOptions);
};
AbstractComponent.prototype.toString = function() {
return "Component" + this.getTemplateName() + "[id=" + this.options.id + "]";
};
jQuery extend is for moving properties from one (or more) object(s) to another object.
$.extend({}, {
foo: 10,
bar: 20
});
You should use prototypal inheritance isntead
function Button(options) {
'use strict';
var defaultOptions = {
templateName: '#button-tmpl',
title: "Label goes here",
type: "primary",
size: "medium",
disabled: null,
autosave: null,
href: null,
onclick: null
};
//debugger
//this.setOptions is not available here
this.setOptions(options, defaultOptions);
this.checkRequiredKeys('title');
return this;
};
Button.prototype = new AbstractComponent;

Categories

Resources