I finally figured out a way to do this.
I will warn you, it's not pretty. If anyone has a better way of doing this, I'm all ears!
There has to be a better way of doing this, but this is what I was able to come up with after much trial and error... and it works in my scenario (Disclaimer: Your results may vary).
Basically, the idea here is to extend the modal with our own which has a few methods that take care of:
- "un-doing" all of the various knockout bindings etc
- changing the fields that need to be changed
- and then re-applying the bindings.
The updateElement method below is recursive and also takes care of all child elements.
It specifically targets certain properties that I need (like elem.imports.visible).
You may need others for your situation.
My hope at the start of this was to find one method I just needed to call which would basically re-build everything, but unfortunately I was never able to find one.
My custom modal component:
define([
'Magento_Ui/js/modal/modal-component',
'jquery',
'ko',
'underscore'
], function (Modal, $, ko, _) {
'use strict';
return Modal.extend({
/**
* Set the dataScope, and related properties / actions
*
* @param {String} dataScope
*/
setDataScope: function (dataScope) {
this.originalDataScope = this.dataScope;
this.newDataScope = dataScope;
// Unbind modal content children
var modalElement = $('.product_form_product_form_my_fields_modal');
var modalContent = modalElement.find('.modal-content');
modalContent.find('*').each(function () {
$(this).unbind();
});
// Clean nodes
ko.utils.arrayForEach(modalContent[0].childNodes, ko.cleanNode);
// Update Element and Child Elements
this.updateElement(this);
// Re-apply ko bindings
ko.applyBindings(this, modalContent[0]);
// Remove modal
delete this.modal;
// Detach modal DOM element
modalElement.detach();
// Re-init modal content
this.initializeContent();
},
/**
* Update element recursively
*
* @param {Object} elem
*/
updateElement: function (elem) {
// Update datascope
if (elem.dataScope) {
elem.dataScope = elem.dataScope.replace(this.originalDataScope, this.newDataScope);
}
// Update parent datascope
if (elem.parentScope) {
elem.parentScope = elem.parentScope.replace(this.originalDataScope, this.newDataScope);
}
// Container changes
if (elem.componentType === 'container') {
// Re-initialize links
if (elem.imports.visible) {
elem.imports.visible = elem.imports.visible.replace(this.originalDataScope, this.newDataScope);
elem.initLinks();
}
}
// Input field changes
if (elem.inputName) {
// Dispose of all existing subscriptions for input field value
while (elem.value._subscriptions.change[0]) {
elem.value._subscriptions.change[0].dispose();
}
// Remove imports & exports
elem.maps.imports = {};
elem.maps.exports = {};
// Change input name
elem.inputName = this.dataScopeToHtmlArray(elem.dataScope);
// Re-initialize links
if (elem.links.value) {
elem.links.value = elem.links.value.replace(this.originalDataScope, this.newDataScope);
elem.initLinks();
}
}
// Recursively update all child elements
if (_.isFunction(elem.elems)) {
_.each(elem.elems(), this.updateElement, this);
}
},
/**
* Get HTML array from data scope.
*
* @param {String} dataScopeString
* @returns {String}
*/
dataScopeToHtmlArray: function (dataScopeString) {
var dataScopeArray, dataScope, reduceFunction;
/**
* Add new level of nesting.
*
* @param {String} prev
* @param {String} curr
* @returns {String}
*/
reduceFunction = function (prev, curr) {
return prev + '[' + curr + ']';
};
dataScopeArray = dataScopeString.split('.');
dataScope = dataScopeArray.shift();
dataScope += dataScopeArray.reduce(reduceFunction, '');
return dataScope;
}
});
});
In the definition for my modal I changed the component
to be the one I created above.
Then, to call this component I then changed my PHP Modifier to call our method with the datascope for the current row:
'arguments' => [
'data' => [
'config' => [
'title' => __('My Custom Fields'),
'formElement' => Container::NAME,
'componentType' => Container::NAME,
'component' => 'Magento_Ui/js/form/components/button',
'actions' => [
[
'targetName' => 'ns=product_form, index=my_fields_modal',
'actionName' => 'setDataScope',
'params' => [
'${$.dataScope}'
]
],
[
'targetName' => 'ns=product_form, index=my_fields_modal',
'actionName' => 'openModal',
],
],
'displayAsLink' => true,
'sortOrder' => $sortOrder,
],
],
]
Note: A bug with Magento core code exists which will cause this to fail the second time the link is clicked.
A description of the issue and my work-around can be found here: https://github.com/magento/magento2/issues/7445
data.product.options.0.values.0
it shows/saves the value of the first option's first value.