City and street validation at checkout - javascript

I am a newcomer in Magento2 and face with a next task. I have to do custom validation for city and street inputs at Checkout page for both shipping and payment steps.
There are 2 issues I am misunderstanding totally.
As I could investigate templates for city and street are inserted by Knockout. To get cities and streets list I have to insert php method in script tag. This php method provides to me URL for next Ajax request. Because of general Knockout template with '.html' type I can't insert php code there.
So how can I call my js file from Knockout html template?
City and street inputs must offer coincidences for first entered letters (as a result of Ajax request) in their lists below. How this lists can be realized?
I have read Magento devdocs and a lot of communities but couldn't find intelligible explanation. Sorry for possible repeat.
app/design/frontend/Vendor/Theme/Magento_Checkout/web/template/shipping-address/form.html (inputs are inserted inside id="shipping-new-address-form")
<div class="amtheme-shipping-wrap">
<form class="form form-shipping-address amtheme-form-address"
id="co-shipping-form"
data-bind="attr: {'data-hasrequired': $t('* Required Fields')}">
<div class="step-title" data-bind="text: setAddressTitle" data-role="title"></div>
<!-- ko foreach: getRegion('before-fields') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!--/ko-->
<div id="shipping-new-address-form" class="fieldset address">
<!-- ko foreach: getRegion('additional-fieldsets') -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!--/ko-->
<!-- ko if: (isCustomerLoggedIn) -->
<div class="field choice" data-bind="visible: !isFormInline">
<input type="checkbox" class="checkbox" id="shipping-save-in-address-book" data-bind="checked: saveInAddressBook" />
<label class="label" for="shipping-save-in-address-book">
<span data-bind="i18n: 'Save in address book'"></span>
</label>
</div>
<!-- /ko -->
<div class="amtheme-address-toolbar" data-bind="visible: !isFormInline && !isFormPopUpVisible()">
<button type="button"
class="action action-cancel"
click="hideNewAddress"
text="$t('Cancel')"
data-bind="attr: {title: $t('Cancel')}">
</button>
<button type="button"
class="action action-save"
click="saveNewAddress"
text="$t('Ship here')"
data-bind="attr: {title: $t('Ship here')}">
</button>
</div>
</div>
</form>
</div>
I was about to write inside form.html something like this:
<script>
require([
'jquery',
'Magento_Theme/js/govaddress-validation'
], function($) {
$(function () {
$('input[name="city"]').keyup(function () {
console.log('keyup event worked');
govAddressValidation.getCityList('<?php echo $this->getUrl("opgovaddress"); ?>');
});
})
})
</script>
My JS file is not matter as it is unreachable for now
app/design/frontend/Vendor/Theme/Magento_Theme/web/js/govaddress-validation.js
define([
'jquery'
], function ($) {
'use strict';
return {
url: '',
getCityList: function (url) {
var inputValue = $('input[name="city"]').val();
this.inputValue = inputValue;
this.url = url;
this.ajaxCall();
console.log('getCityList');
},
...
}
})
app/design/frontend/Vendor/Theme/Magento_Theme/requirejs-config.js
var config = {
map: {
'*': {
backTop: 'Magento_Theme/js/components/back-to-top',
amMenu: 'Magento_Theme/js/components/am-menu',
amQty: 'Magento_Theme/js/components/am-qty',
amSelect: 'Magento_Theme/js/components/am-select',
amFileUpload: 'Magento_Theme/js/components/am-file-upload',
amStickyHeader: 'Magento_Theme/js/components/am-sticky-header',
govAddressValidation: 'Magento_Theme/js/govaddress-validation'
}
},
config: {
mixins: {
'mage/validation': {
'Magento_Theme/js/lib/mage/validation-mixin': false
},
'mage/menu': {
'Magento_Theme/js/lib/mage/menu-mixin': true
}
}
}
};

Firstly, Magento's validation for checkout_index_index.xml is not working properly so, you have to insert the rules while you replace the street fields.
You can do this by creating a plugin and inserting the fields in Layout AfterProcessor.
namespace/module/etc/frontend/di.xml
...
<type name="Magento\Checkout\Block\Checkout\LayoutProcessor">
<plugin name="rewrite-street" type="Namespace\Module\Model\Checkout\LayoutProcessorPlugin" sortOrder="10"/>
</type>
...
And the LayoutProcessorPlugin should look like this: (Please see in my attachment)
https://pastebin.com/AE6AiLxk
You can insert in the validation array any validation rule you want.

So what you do is open your checkout and look for this field you want to validate. I will choose your first field, city. Opening dev tools gives me this:
<input class="input-text" type="text" data-bind=" ..." name="city" ...>
So we could do something with the name since we don't have a really useful id. I see in your question that you already have this part!
Since you say you want both shipping and payment step, I will go for the last. So to do this we need a custom validator on the payment step. I assume you already have a custom extension, something like Vendor_MyCheckoutValidator. First we will create a copy of checkout_index_index.xml (from Magento Checkout module) in Vendor/MyCheckoutValidator/view/frontend/layout and we will leave out all the stuff we don't need. Simply copy the steps down to payment:
<body>
<referenceBlock name="checkout.root">
<arguments>
[... all the way down to payment ...]
<item name="payment" xsi:type="array">
<item name="children" xsi:type="array">
<item name="additional-payment-validators" xsi:type="array">
<item name="children" xsi:type="array">
<item name="my-city-validator" xsi:type="array">
<item name="component" xsi:type="string">Vendor_MyCheckoutValidator/js/view/finalcheck-validation</item>
</item>
[... and all the way back down again ...]
So there you have it, I named it my-city-validator and it points to Vendor_MyCheckoutValidator/js/view/finalcheck-validation.
Let's create ths finalcheck-validation.js in your module under Vendor/MyCheckoutValidator/view/frontend/web/js/view:
define(
[
'uiComponent',
'Magento_Checkout/js/model/payment/additional-validators',
'Vendor_MyCheckoutValidator/js/model/final-address-check-validator'
],
function (Component, additionalValidators, finalAddressCheckValidator) {
'use strict';
additionalValidators.registerValidator(finalAddressCheckValidator);
return Component.extend({});
}
);
It looks more complicated then it is but simply said our define says it is a payment additinal validator, it can be found in js/model/final-address-check-validator and these are then used in the function. So we now we need our final bit, the final-address-check-validator.js that should be in Vendor/MyCheckoutValidator/view/frontend/web/js/model :
Note: the path mentioned in the checkout_index_index.xml is Vendor_MyCheckoutValidator/js/view/finalcheck-validation and in the above js it is Vendor_MyCheckoutValidator/js/model/final-address-check-validator but in reality you need the "view/frontend/web" bit as well (Magento knows where to find it but this can be confusing for new developers) so our path is Vendor/MyCheckoutValidator/view/frontend/web/js/view and Vendor/MyCheckoutValidator/view/frontend/web/js/model
Back to our last bit, the final-address-check-validator.js. Here we have this problem of not knowing the id but we do have the name of the input as seen at the beginning of my post, the name is "city". So let's check if it starts with a Z because we don't want to send things to cities that start with an Z.
We need some standard stuff such as mage/translate and the (error) messageList and of course jquery:
define(
['jquery', 'mage/translate', 'Magento_Ui/js/model/messageList'],
function ($, $t, messageList) {
'use strict';
return {
validate: function () {
var cityIsFine = true,
city = $('input[name="city"]').val(); // getting our city input by name!
if(city.match("^Z") {
messageList.addErrorMessage({ message: $t('We are sorry, your city starts with a Z.') });
cityIsFine = false;
}
// you could do some checks here and maybe a console.log
console.log(city);
if(!cityIsFine) {
return false;
}
return true;
}
}
}
);
Install your module, make sure to clear all cache! Especially frontend cache! Deploy your frontend, clear browser cache thoroughly and try with a city named Zen. You should be able to fill in everything fine and then you push the payment button and you get an error!
As you can see I did not make use of any templates or mixins or other things (I could but it simply isn't needed here). You only have to declare your custom validator on the payment step, add it to the additional validators as shown and do some jquery magic in it.
I concentrated on city but you could do the same for street. Have a look on your frontend what its name is. Say it has name="street[0]" then your jQuery value would look like street0 = $('input[name="street[0]"]').val();
You also could start with throwing an error right away and get that working before you add regex and other rules. Then you know you have this working :-)
define(
['jquery', 'mage/translate', 'Magento_Ui/js/model/messageList'],
function ($, $t, messageList) {
'use strict';
return {
validate: function () {
console.log('Sorry!');
messageList.addErrorMessage({ message: $t('We are sorry you will never get past the payment button') });
return false;
}
}
}
);
And this part I haven't tried but I think you can do an ajax call in the above validator. I'm not sure if you want a kind of auto-complete from your question so I concentrated on the validation of city in the above answer.

Related

How to use injected constants in html file in AngularJS

I have a constant file which looks like this
demo.constant.js
// Add detail constans here
(function () {
angular.module('myApp').constant('TYPE', {
DYNAMIC: 'dynamic',
STATIC: 'static'
});
}());
Now I have a controller file that looks similar to this.
demo.controller.js
(function() {
var DemoController = function(DEP1,DEP2, .... , TYPE)
console.log(TYPE.DYNAMIC); // works perfectly
var self = this;
self.type = '';
...
...
angular.module('myApp.controllers').controller('DemoController', DemoController);
})();
I am trying to access these constants in the HTML file like this:-
<div ng-controller="DemoController as self">
<span ng-if="(self.type === 'dynamic')"> <!--instead of 'dynamic' I want to use TYPE.DYNAMIC-->
...
...
...
</span>
</div>
Note:- {{self.type}} works but {{TYPE.DYNAMIC}} doesn't.
Another problem is that I want to use this constant as the value of radio buttons.
somewhat like this:-
<input type="radio" name="type" ng-model="self.inputType" value="dynamic"> <!-- Here I want to use TYPE.DYNAMIC -->
<input type="radio" name="type" ng-model="self.inputType" value="static"> <!-- Same as above -->
I have searched everywhere but nothing seems to work. Please Help!!
One approach is to assign the constant to a controller property:
function DemoController(DEP1,DEP2, /*.... ,*/ TYPE) {
console.log(TYPE.DYNAMIC); // works perfectly
this.TYPE = TYPE;
}
angular.module('myApp.controllers').controller('DemoController', DemoController)
Then use it in the template:
<div ng-controller="DemoController as $ctrl">
<span ng-if="$ctrl.type === $ctrl.TYPE.DYNAMIC">
...
</span>
</div>
Note: The ng-if directive uses creates a child scope. Consider instead using the ng-show directive which uses CSS and less resources.
You can use $rootScope and initilize it in run phase:
angular.module('app')
.run(function ($rootScope, TYPE) {
$rootScope.TYPE = TYPE
});
then you can use it directly in your HTML

Knockout.js: combining "visible" and multiple view models

I have spent some time trying to unscramble this issue. I have a page that I would like to be used by a beauty salon owner to manage two things:
Appointments
Days with special schedule
To select a 'view' I am using a toggle that is based on Knockout's visible binding:
So when a radio button is clicked a div is being rendered and another hidden:
<div id="selectingScopes" class="col-xs-6 col-md-4">
<fieldset>
<legend>View:</legend>
<div id="radioControls" class="switch-toggle well">
<input jsf:id="appointmentsRadio" name="appointmentsRadio" type="radio"
data-bind="checked: selectedScope" value="#{bigcopy.appointmentsLabel}"/>
<label for="appointmentsRadio" onclick="">#{bigcopy.appointmentsLabel}</label>
<input jsf:id="specialdaysRadio" name="specialdaysRadio" type="radio"
data-bind="checked: selectedScope" value="#{bigcopy.specialDaysLabel}"/>
<label for="specialdaysRadio" onclick="">#{bigcopy.specialDaysLabel}</label>
<a class="btn btn-primary"></a>
</div>
</fieldset>
</div>
<div id="appointmentsModel" class="row" data-bind="fadeVisible: selectedScope()==='#{bigcopy.appointmentsLabel}'">
<ui:include src="includes/appointmentsManagement.xhtml"/>
</div>
<div class="row" data-bind="fadeVisible: selectedScope()==='#{bigcopy.specialDaysLabel}'">
<ui:include src="includes/specdaysManagement.html"/>
</div>
In the same time I want to bind the "appointmentsModel" div above to a different view model that will be responsible only for managing the table of appointments that have to be uploaded from the server. This is the js file sifted of irrelevant clutter:
var restServiceRoot = "localhost:8080/RimmaNew/rest";
var scopeSelector = {
selectedScope: ko.observable()
};
ko.applyBindings(scopeSelector);
//appointments part
//Initial load
function appointmentsModel() {
var self = this;
self.serviceURL = restServiceRoot + "/appointments";
self.Appointments = ko.observableArray([]);
$.ajax({
url: self.serviceURL,
type: 'get',
data: null,
dataType: 'json',
success: function (appointments) {
var parsed = JSON.parse(appointments);
var appointmentsArray = parsed.current;
var mappedAppointments = $.map(appointmentsArray, function(item){
return new Appointment(item);
});
self.Appointments(mappedAppointments);
},
error: function (xhr, ajaxOptions, thrownError) {
var err = xhr.responseText;
alert(err);
}
});
}
var appointmentsModelImpl = new appointmentsModel();
ko.applyBindings(appointmentsModelImpl, document.getElementById('appointmentsModel'));
My code is inspired of two books: Knockout.js (O'Reilly - Jamie Munro) and Java EE and HTML 5 Enterprise Application Development (Oracle Press) that barely touch upon the topic of having cross-bindings in KO. I equally didn't find official KO documentation to be helpful in this endeavor.
There is a stackoverflow ticket that dealt with a similar issue, but in one it was the author who answered himself - his explanation made perfect sense to himself, but the mentions of 'stopBinding' and 'controlsDescendantBindings' seemed taken out of context to the two bindings that he uses below...I can't see how to apply this to my problem...
So you would make me a huuge favor if you could either
direct me to a resource where I could educate myself on how to use cross-bindings in KO
Or guide me in how can I mend my code to be able to control the visibility of these two divs and apply a viewmodel to each of them
Doing both would be fantastic.
1 The full code for the original page (xhtml) is here
2The js file
3The js 'classes' file
4The enclosed page with the 'appointments' table
Your best bet will probably be to think of your models a little bit differently. Instead of doing a separate model binding, it may make more sense to do a default appointmentsModel, then update the observables inside that.
What I would do is have a single parent binding to the body, or a containing div in the body, then submodels for the various children.
Sample JS:
var MyParentModel = function(){
var self = this;
self.selectingScopes = {
scope: ko.observable("");
};
self.appointmentModel = {
param1: ko.observable(),
param2: ko.observable()
}
};
ko.applyBindings(new MyParentModel());
Sample HTML:
<div id="appointmentsModel" data-bind="with: appointmentModel"></div>

How to add multiple items to a list

I'm building an app where users can add items to a list and I decided, for the sake of learning, to use Angular (which I'm very new to). So far, I've been able to successfully add a single item to that list without any issues. Unfortunately, whenever I try to add more than one without a page refresh, I get an error - specifically a "Undefined is not a function."
I've spent more time than I care to think about trying to resolve this issue and I'm hoping an expert out there can give me a hand. Here's what I have so far:
Controllers:
angular.module('streakApp')
.controller('StreakController', function($scope) {
// Removed REST code since it isn't relevant
$scope.streaks = Streak.query();
// Get user info and use it for making new streaks
var userInfo = User.query(function() {
var user = userInfo[0];
var userName = user.username;
$scope.newStreak = new Streak({
'user': userName
});
});
})
.controller('FormController', function($scope) {
// Works for single items, not for multiple items
$scope.addStreak = function(activity) {
$scope.streaks.push(activity);
$scope.newStreak = {};
};
});
View:
<div class="streaks" ng-controller="FormController as formCtrl">
<form name="streakForm" novalidate >
<fieldset>
<legend>Add an activity</legend>
<input ng-model="newStreak.activity" placeholder="Activity" required />
<input ng-model="newStreak.start" placeholder="Start" type="date" required />
<input ng-model="newStreak.current_streak" placeholder="Current streak" type="number" min="0" required />
<input ng-model="newStreak.notes" placeholder="Notes" />
<button type="submit" ng-click="addStreak(newStreak)">Add</button>
</fieldset>
</form>
<h4>Current streaks: {{ streaks.length }}</h4>
<div ng-show="newStreak.activity">
<hr>
<h3>{{ newStreak.activity }}</h3>
<h4>Current streak: {{ newStreak.current_streak }}</h4>
<p>Start: {{ newStreak.start | date }}</p>
<p>Notes: {{ newStreak.notes }}</p>
<hr>
</div>
<div ng-repeat="user_streak in streaks">
<!-- Removed most of this for simplicity -->
<h3>{{ user_streak.fields }}</h3>
</div>
</div>
Could you post the html of StreakController too? Your solution works fine in this fiddle:
http://jsfiddle.net/zf9y0yyg/1/
.controller('FormController', function($scope) {
$scope.streaks = [];
// Works for single items, not for multiple items
$scope.addStreak = function(activity) {
$scope.streaks.push(activity);
$scope.newStreak = {};
};
});
The $scope inject in each controller is different, so you have to define the "streaks" in FormController.
Your problems comes from :
.controller('FormController', function($scope) {
// Works for single items, not for multiple items
$scope.addStreak = function(activity) {
$scope.streaks.push(activity);
^^^^^^
// Streaks is initialized in another controller (StreakController)
// therefore, depending of when is instantiated StreakController,
// you can have an error or not
$scope.newStreak = {};
};
});
A better design would be to implement a StreakService, and to inject that service in the controller you need it. Of course, initializing $scope.streaks in FormController will make your code work, but that's not the responsibility of FormController to initialize this data.
I assume FormController is a nested controller of StreakController, so they share the same scope.
if that works for single object, it should work for mulitiple objects, the problems is you can't just use push to push an array of object to the streaks, you can for loop the array and add them individually or use push.apply trick. I thought the reason of Undefined is not a function. is because the Stack.query() return an element instead of an array of elements so, the method push doesn't exists on the $scope.streaks.
http://jsbin.com/jezomutizo/2/edit

Accessing objects within viewmodel Knockout

I'm using knockout for a single page app that does some basic calculations based on several inputs to then populate the value of some html . In an attempt to keep my html concise I've used an array of objects in my viewModel to store my form. I achieved the basic functionality of the page however I wish to add a 'display' value to show on html that has formatted decimal points and perhaps a converted value in the future.
I'm not sure of a 'best practices' way of accessing the other values of the object that I'm currently 'in'. For example: If I want my display field to be a computed value that consists of the value field rounded to two decimal places.
display: ko.computed(function()
{
return Math.round(100 * myObj.value())/100;
}
I've been reading through the documentation for knockout and it would appear that managing this is a common problem with those new to the library. I believe I could make it work by adding the computed function outside of the viewmodel prototype and access the object by
viewModel.input[1].value()
However I would imagine there is a cleaner way to achieve this.
I've included a small snippet of the viewModel for reference. In total the input array contains 15 elements. The HTML is included below that.
var ViewModel = function()
{
var self = this;
this.unitType = ko.observable('imperial');
this.input =
[
{
name: "Test Stand Configuration",
isInput: false
},
{
name: "Charge Pump Displacement",
disabled: false,
isInput: true,
unitImperial: "cubic inches/rev",
unitMetric: "cm^3/rev",
convert: function(incomingSystem)
{
var newValue = this.value();
if(incomingSystem == 'metric')
{
//switch to metric
newValue = convert.cubicinchesToCubiccentimeters(newValue);
}
else
{
//switch to imperial
newValue = convert.cubiccentimetersToCubicinches(newValue);
}
this.value(newValue);
},
value: ko.observable(1.4),
display: ko.computed(function()
{
console.log(self);
}, self)
}
]
};
__
<!-- ko foreach: input -->
<!-- ko if: $data.isInput == true -->
<div class="form-group">
<div class="col-sm-6">
<label class="control-label" data-bind="text: $data.name"></label>
</div>
<div class="col-sm-6">
<div class="input-group">
<!-- ko if: $data.disabled == true -->
<input data-bind="value: $data.value" type="text" class="form-control" disabled>
<!-- /ko -->
<!-- ko if: $data.disabled == false -->
<input data-bind="value: $data.value" type="text" class="form-control">
<!-- /ko -->
<!-- ko if: viewModel.unitType() == 'imperial'-->
<span data-bind="text: $data.unitImperial" class="input-group-addon"></span>
<!-- /ko -->
<!-- ko if: viewModel.unitType() == 'metric' -->
<span data-bind="text: $data.unitMetric" class="input-group-addon"></span>
<!-- /ko -->
</div>
</div>
</div>
<!-- /ko -->
<!-- ko if: $data.isInput == false -->
<div class="form-group">
<div class="col-sm-6">
<h3 data-bind="text: $data.name"></h3>
</div>
</div>
<!-- /ko -->
If you want to read/ write to & from the same output, #Aaron Siciliano's answer is the way to go. Else, ...
I'm not sure of a 'best practices' way of accessing the other values of the object that > I'm currently 'in'. For example: If I want my display field to be a computed value that consists of the value field rounded to two decimal places.
I think there's a misconception here about what KnockoutJS is. KnockoutJS allows you to handle all your logic in Javascript. Accessing the values of the object you are in is simple thanks to Knockout's context variables: $data (the current context, and the same as JS's this), $parent (the parent context), $root(the root viewmodel context) and more at Binding Context. You can use this variables both in your templates and in your Javascript. Btw, $index returns the observable index of an array item (which means it changes automatically when you do someth. wth it). In your example it'd be as simple as:
<span data-bind="$data.display"></span>
Or suppose you want to get an observable w/e from your root, or even parent. (Scenario: A cost indicator that increases for every item purchased, which are stored separately in an array).
<span data-bind="$root.totalValue"></span>
Correct me if I'm wrong, but given that you have defined self only in your viewmodel, the display function should output the whole root viewmodel to the console. If you redefine a self variable inside your object in the array, self will output that object in the array. That depends on the scope of your variable. You can't use object literals for that, you need a constructor function (like the one for your view model). So you'd get:
function viewModel() {
var self = this;
self.inputs = ko.observableArray([
// this builds a new instance of the 'input' prototype
new Input({initial: 0, name: 'someinput', display: someFunction});
])
}
// a constructor for your 15 inputs, which takes an object as parameter
function Input(obj) {
var self = this; // now self refers to a single instance of the 'input' prototype
self.initial = ko.observable(obj.initial); //blank
self.name = obj.name;
self.display = ko.computed(obj.fn, this); // your function
}
As you mentioned, you can also handle events afterwards, see: unobtrusive event handling. Add your event listeners by using the ko.dataFor & ko.contextFor methods.
It appears as though KnockoutJS has an example set up on its website for this exact scenario.
http://knockoutjs.com/documentation/extenders.html
From reading that page it looks as though you can create an extender to intercept an observable before it updates and apply a function to it (to format it for currency or round or perform whatever changes need to be made to it before it updates the ui).
This would probably be the closest thing to what you are looking for. However to be completely honest with you i like your simple approach to the problem.

AngularJS using UI-JQ with Two-Way Data Binding

Good Evening,
I have a small issue when I am trying to create a wizard form using jQuery Steps inside AngularJS. I can pass data into the plugin, but I cannot get the plugin to send data back to my controller. Here is how I have it setup:
jqConfig
myApp.value('uiJqConfig', {
steps: {}
};
Conroller
myApp.controller('MainCtrl', function ($scope) {
$scope.user = {
name = "",
address = ""
};
$scope.wizardOptions = {
onStepChanging: function (event, currentIndex, newIndex) {
console.log($scope.user);
}
};
HTML Template
<form ui-jq="steps" ui-options="wizardOptions">
<h1>Step 1</h1>
<div>
<input ng-model="user.name" />
</div>
<h1>Step 2</h1>
<div>
<input ng-model="user.address" />
</div>
</form>
I get the wizard to load up properly and everything. Now the issue is when I type into the inputs, it does not update the user object inside the controller.
Is there a way that I can accomplish this at all? Or should I look for a different solution?
Thanks,
Joshua

Categories

Resources