5

I try to add UI Component to product item on product list due to difficult FE login for each item. But I was wondering when conflicts between components appeared. All components for each item has a unique name.

This is an example how I try to implement it:

Added div with scope to list.phtml and added script for each item:

<div class="test-component" data-bind="scope: 'test_<?= $_product->getId() ?>'">
    <!-- ko template: getTemplate() --><!-- /ko -->
</div>

<script type="text/x-magento-init">
    {
        "*": {
            "Magento_Ui/js/core/app": {
                "components": {
                    "test_<?= $_product->getId() ?>": {
                        "component": "Magento_Catalog/js/test-component",
                        "template" : "Magento_Catalog/test-component",
                        "config": {
                        }
                    }
                }
            }
        }
    }
</script>

Then I added JS Component test-component.js:

define([
    'jquery',
    'uiComponent',
    'ko'
], function ($, Component, ko) {
    return Component.extend({
        testObservable: ko.observable(1),

        initialize: function() {
            this._super();
        }
    });
});

Finally added HTML template test-component.html

<div>
    <input type="text"
           data-bind='value: testObservable'
           class="input-text qty" min="1"/>
</div>

As a result, fields display for each product and observable works but when I change it for one item it changes for all items:

enter image description here

The real example is much more complex but this is an example of a problem. Maybe UI components don't a good solution? As a solution, I can call some JS method on changing and set it to some observable object with the productId key but it looks weird when observable value shares between components.

Thanks.

2
  • Where are you initialising your script in relation to the rest of the template? It looks like it needs to invoked on each individual product (within the foreach)
    – Ben Crook
    Commented Feb 27, 2020 at 12:02
  • @BenCrook Yeah, I did initialization in foreach loop Commented Feb 28, 2020 at 10:07

1 Answer 1

7
+50

I dont know what exactly goes wrong with that implementation of view models, but seems like creating observable properties this way (e.g. testObservable: ko.observable(1)) make that property shared through all same models.

I tested some other solution with same logic and it works in a different way:

component

define([
    'jquery',
    'uiComponent',
    'ko'
], function ($, Component, ko) {
    return Component.extend({
        // testObservable: ko.observable(1),

        initialize: function() {
            this._super();
        },

        initObservable: function () {
            this._super();
            this.observe('testObservable');

            this.initSubscriptions();

            return this;
        },

        initSubscriptions: function () {
            var self = this;

            this.testObservable.subscribe(function (value) {
                console.log(self);
                console.log(value);
            } );
        }
    });
});

template:

<div>
    <input type="text"
           data-bind='value: testObservable'
           class="input-text qty" min="1"/>
</div>

phtml:

<div class="test-component" data-bind="scope: 'test<?= $_product->getId() ?>'">
    <!-- ko template: getTemplate() --><!-- /ko -->
</div>

<script type="text/x-magento-init">
{
    "*": {
        "Magento_Ui/js/core/app": {
            "components": {
                "test<?= $_product->getId() ?>": {
                    "component": "Magento_Catalog/js/test-component",
                    "template" : "Magento_Catalog/test-component",
                    "config": {
                    }
                }
            }
        }
    }
}
</script>

Result:

result example

Update:

As Magento use a underscore.js extend method to copy objects, all properties in the destination object will be copied "by refernce" and all your values will be transfered to the parent object, which is the same for all your instances created using factory. This way you has many objects with same parent object and one shared property testObservable. Here is description of the extend method of underscore.js lib from the docs:

extend_.extend(destination, *sources)
Shallowly copy all of the properties in the source objects over to the destination object, and return the destination object. Any nested objects or arrays will be copied by reference, not duplicated. It's in-order, so the last source will override properties of the same name in previous arguments.

_.extend({name: 'moe'}, {age: 50});
=> {name: 'moe', age: 50}

Note:

In JavaScript, functions are first-class objects, because they can have properties and methods just like any other object. What distinguishes them from other objects is that functions can be called. In brief, they are Function objects.

Source: MDN web docs

Here is object view from browsers console. As you can see there is no own property testObservable in the main object:

object tree from browsers console

Resume: Magento use the underscore.js which extend an objects with properties as a link because function in JS is an object. :) But buil-in components has a workaround for that case: just use regular initObservable method to declare object-own observable properties in any way.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.