The PHP code for a UI component renders a javascript initialization that looks like this
<script type="text/x-magento-init">
{
"*": {
"Magento_Ui/js/core/app":{
"types":{...},
"components":{...},
}
}
}
</script>
This bit of code in the page means Magento will invoke the Magento_Ui/js/core/app
RequireJS module to fetch a callback, and then call that that callback passing in the the {types:..., components:...}
JSON object as an argument (data
below)
#File: vendor/magento/module-ui/view/base/web/js/core/app.js
define([
'./renderer/types',
'./renderer/layout',
'Magento_Ui/js/lib/ko/initialize'
], function (types, layout) {
'use strict';
return function (data) {
types.set(data.types);
layout(data.components);
};
});
The data object contains all the data needed to render the UI component, as well as a configuration that links certain strings with certain Magento RequireJS modules. That mapping happens in the types
and layout
RequireJS modules. The application also loads the Magento_Ui/js/lib/ko/initialize
RequireJS library. The initialize
module kicks off Magento's KnockoutJS integration.
/**
* Copyright © 2016 Magento. All rights reserved.
* See COPYING.txt for license details.
*/
/** Loads all available knockout bindings, sets custom template engine, initializes knockout on page */
#File: vendor/magento/module-ui/view/base/web/js/lib/ko/initialize.js
define([
'ko',
'./template/engine',
'knockoutjs/knockout-repeat',
'knockoutjs/knockout-fast-foreach',
'knockoutjs/knockout-es5',
'./bind/scope',
'./bind/staticChecked',
'./bind/datepicker',
'./bind/outer_click',
'./bind/keyboard',
'./bind/optgroup',
'./bind/fadeVisible',
'./bind/mage-init',
'./bind/after-render',
'./bind/i18n',
'./bind/collapsible',
'./bind/autoselect',
'./extender/observable_array',
'./extender/bound-nodes'
], function (ko, templateEngine) {
'use strict';
ko.setTemplateEngine(templateEngine);
ko.applyBindings();
});
Each individual bind/...
RequireJS module sets up a single custom binding for Knockout.
The extender/...
RequireJS modules add some helper methods to native KnockoutJS objects.
Magento also extends the functionality of Knockout's javascript template engine in the ./template/engine
RequireJS module.
Finally, Magento calls applyBindings()
on the KnockoutJS object. This is normally where a Knockout program would bind a view model to the HTML page -- however, Magento calls applyBindings
without a view model. This means Knockout will start processing the page as a view, but with no data bound.
In a stock Knockout setup, this would be a little silly. However, because of the previously mentioned custom Knockout bindings, there's plenty of opportunities for Knockout to do things.
We're interested in the scope binding. You can see that in this HTML, also rendered by the PHP UI Component system.
<div class="admin__data-grid-outer-wrap" data-bind="scope: 'customer_listing.customer_listing'">
<div data-role="spinner" data-component="customer_listing.customer_listing.customer_columns" class="admin__data-grid-loading-mask">
<div class="spinner">
<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>
</div>
<!-- ko template: getTemplate() --><!-- /ko -->
<script type="text/x-magento-init">
</script>
</div>
Specifically, the data-bind="scope: 'customer_listing.customer_listing'">
attribute. When Magento kicks off applyBindings
, Knockout will see this custom scope
binding, and invoke the ./bind/scope
RequireJS module. The ability to apply a custom binding is pure KnockoutJS. The implementation of the scope binding is something Magento Inc. has done.
The implementation of the scope binding is at
#File: vendor/magento/module-ui/view/base/web/js/lib/ko/bind/scope.js
The important bit in this file is here
var component = valueAccessor(),
apply = applyComponents.bind(this, el, bindingContext);
if (typeof component === 'string') {
registry.get(component, apply);
} else if (typeof component === 'function') {
component(apply);
}
Without getting too into the details, the registry.get
method will pull out an already generated object using the string in the component
variable as an identifier, and pass it to the applyComponents
method as the third parameter. The string identifier is the value of scope:
(customer_listing.customer_listing
above)
In applyComponents
function applyComponents(el, bindingContext, component) {
component = bindingContext.createChildContext(component);
ko.utils.extend(component, {
$t: i18n
});
ko.utils.arrayForEach(el.childNodes, ko.cleanNode);
ko.applyBindingsToDescendants(component, el);
}
the call to createChildContext
will create what is, essentially, a new viewModel object based on the already instantiated component object, and then apply it to all the descendant element of the original div
that used data-bind=scope:
.
So, what is the already instantiated component object? Remember the call to layout
back in app.js
?
#File: vendor/magento/module-ui/view/base/web/js/core/app.js
layout(data.components);
The layout
function/module will descend into the passed in data.components
(again, this data comes from the object passed in via text/x-magento-init
). For each object it finds, it will look for a config
object, and in that config object it will look for a component
key. If it finds a component key, it will
Use RequireJS
to return a module instance -- as though the module were called in a requirejs
/define
dependency.
Call that module instance as a javascript constructor
Store the resulting object in the registry
object/module
So, that's a lot to take in. Here's a quick review, using
<div class="admin__data-grid-outer-wrap" data-bind="scope: 'customer_listing.customer_listing'">
<div data-role="spinner" data-component="customer_listing.customer_listing.customer_columns" class="admin__data-grid-loading-mask">
<div class="spinner">
<span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>
</div>
<!-- ko template: getTemplate() --><!-- /ko -->
<script type="text/x-magento-init">
</script>
</div>
as a starting point. The scope
value is customer_listing.customer_listing
.
If we look at the JSON object from the text/x-magento-init
initialization
{
"*": {
"Magento_Ui/js/core/app": {
/* snip */
"components": {
"customer_listing": {
"children": {
"customer_listing": {
"type": "customer_listing",
"name": "customer_listing",
"children": /* snip */
"config": {
"component": "uiComponent"
}
},
/* snip */
}
}
}
}
}
}
We see the components.customer_listing.customer_listing
object has a config
object, and that config object has a component
object that's set to uiComponent
. The uiComponent
string is a RequireJS module. In fact, its a RequireJS alias that corresponds to the Magento_Ui/js/lib/core/collection
module.
vendor/magento/module-ui/view/base/requirejs-config.js
14: uiComponent: 'Magento_Ui/js/lib/core/collection',
In layout.js
, Magento has run code that's equivalent to the following.
//The actual code is a bit more complicated because it
//involves jQuery's promises. This is already a complicated
//enough explanation without heading down that path
require(['Magento_Ui/js/lib/core/collection'], function (collection) {
object = new collection({/*data from x-magento-init*/})
}
For the really curious, if you take a look in the collection model and follow its execution path, you'll discover that collection
is a javascript object that's been enhanced both by the lib/core/element/element
module and the lib/core/class
module. Researching these customizations is beyond the scope of this answer.
Once instantiated, layout.js
stores this object
in the registry. This means when Knockout starts processing the bindings and encounters the custom scope
binding
<div class="admin__data-grid-outer-wrap" data-bind="scope: 'customer_listing.customer_listing'">
<!-- snip -->
<!-- ko template: getTemplate() --><!-- /ko -->
<!-- snip -->
</div>
Magento will fetch this object back out of the registry, and bind it as the view model for things inside the div
. In other words, the getTemplate
method that's called when Knockout invokes the tagless binding (<!-- ko template: getTemplate() --><!-- /ko -->
) is the getTemplate
method on the new collection
object.
I will try to answer your question(s).
No. This is not a correct way to add custom attributes to the shipping address form. You do not need to edit new-customer-address.js
. Indeed, this JS file lists all predefined address attributes and matches corresponding backend interface \Magento\Quote\Api\Data\AddressInterface
but Magento provides ability to pass any custom attributes to backend without modification of the backend/frontend components.
Mentioned JS component has customAttributes
property. Your custom attributes will be automatically handled if their $dataScopePrefix
is 'shippindAddress.custom_attributes
'.
If I understood your question correctly, you have data that is not a part of customer address but you need to send it to backend as well. The answer to this question is:
It depends. For instance, you can choose the following approach: add custom form to checkout page that includes all your extra fields (like comment, invoice request etc)
, add JS logic that will handle this form based on some events, and provide a custom service that will receive the data from frontend and store it somewhere for future use.
All official documentation related to checkout is located at http://devdocs.magento.com/guides/v2.1/howdoi/checkout/checkout_overview.html. The term static refers to the forms where all the fields are already known/predefined (for instance: form will always have 2 text fields with predefined labels) and cannot change based on some settings in the backend.
Such forms can be declared using layout XML configuration
. On the other hand, term dynamic refers to forms whose field set can change (for instance: checkout form can have more/less fields based on configuration settings).
In this case the only way to declare such form is to use LayoutProcessor
plugin.
:) Magento tries to cover as much possible use cases that can be significant to the merchants during Magento use/customization as possible. Sometimes this leads to situation when some simple use cases become more complex.
Hope, this helps.
=========================================================================
OK... Lets write some code ;)
- PHP code that inserts additional field in LayoutProcessor
========
/**
* @author aakimov
*/
$customAttributeCode = 'custom_field';
$customField = [
'component' => 'Magento_Ui/js/form/element/abstract',
'config' => [
// customScope is used to group elements within a single form (e.g. they can be validated separately)
'customScope' => 'shippingAddress.custom_attributes',
'customEntry' => null,
'template' => 'ui/form/field',
'elementTmpl' => 'ui/form/element/input',
'tooltip' => [
'description' => 'Yes, this works. I tested it. Sacrificed my lunch break but verified this approach.',
],
],
'dataScope' => 'shippingAddress.custom_attributes' . '.' . $customAttributeCode,
'label' => 'Custom Attribute',
'provider' => 'checkoutProvider',
'sortOrder' => 0,
'validation' => [
'required-entry' => true
],
'options' => [],
'filterBy' => null,
'customEntry' => null,
'visible' => true,
];
$jsLayout['components']['checkout']['children']['steps']['children']['shipping-step']['children']['shippingAddress']['children']['shipping-address-fieldset']['children'][$customAttributeCode] = $customField;
return $jsLayout;
As I already mentioned, this will add your field to customAttributes
property of the JS address object. This property was designed to contain custom EAV address attributes and is related to \Magento\Quote\Model\Quote\Address\CustomAttributeListInterface::getAttributes
method.
The code above will automatically handle local storage persistence on frontend. You can get your field value from local storage using checkoutData.getShippingAddressFromData()
(where checkoutData
is Magento_Checkout/js/checkout-data
).
- Add mixin to change the behavior of 'Magento_Checkout/js/action/set-shipping-information' (this component
is responsible for data submission between shipping and billing
checkout steps)
========
2.1. Create your_module_name/view/frontend/requirejs-config.js
/**
* @author aakimov
*/
var config = {
config: {
mixins: {
'Magento_Checkout/js/action/set-shipping-information': {
'<your_module_name>/js/action/set-shipping-information-mixin': true
}
}
}
};
2.2. Create your_module_name/view/frontend/web/js/action/set-shipping-information-mixin.js
/**
* @author aakimov
*/
/*jshint browser:true jquery:true*/
/*global alert*/
define([
'jquery',
'mage/utils/wrapper',
'Magento_Checkout/js/model/quote'
], function ($, wrapper, quote) {
'use strict';
return function (setShippingInformationAction) {
return wrapper.wrap(setShippingInformationAction, function (originalAction) {
var shippingAddress = quote.shippingAddress();
if (shippingAddress['extension_attributes'] === undefined) {
shippingAddress['extension_attributes'] = {};
}
// you can extract value of extension attribute from any place (in this example I use customAttributes approach)
shippingAddress['extension_attributes']['custom_field'] = shippingAddress.customAttributes['custom_field'];
// pass execution to original action ('Magento_Checkout/js/action/set-shipping-information')
return originalAction();
});
};
});
- Create your_module_name/etc/extension_attributes.xml
========
<?xml version="1.0"?>
<!--
/**
* @author aakimov
*/
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Api/etc/extension_attributes.xsd">
<extension_attributes for="Magento\Quote\Api\Data\AddressInterface">
<attribute code="custom_field" type="string" />
</extension_attributes>
</config>
This will add an extension attribute to address model on backend side. Extension attributes are one of the extension points that Magento provides.
To access your data on backend you can use:
// Magento will generate interface that includes your custom attribute
$value = $address->getExtensionAttributes()->getCustomField();
Hope, this helps and will be added to official documentation.
Best Answer
Hi Joel!
Regarding the following statement:
This is utilizing Knockout's template binding functionality to render the template specified by the JavaScript component's
getTemplate()
directive.The
getTemplate()
directive is defined in Magento'suiElement
JavaScript module (mapped with RequireJS toMagento_Ui/js/lib/core/element/element
).Other Resources