How Does the Knockout-JS Part Work in Magento 2

checkoutknockoutjsmagento-2.1magento2magento2.1.5

I still haven't figured how Magento 2 structures the knockout js part.

For example how does the process work (step by step) of fetching the cart summary number.
You can find this in app/code/Magento/Checkout/view/frontend/templates/cart/minicart.phtml:

<span class="counter qty empty"
          data-bind="css: { empty: !!getCartParam('summary_count') == false }, blockLoader: isLoading">
        <span class="counter-number"><!-- ko text: getCartParam('summary_count') --><!-- /ko --></span>
        <span class="counter-label">
        <!-- ko if: getCartParam('summary_count') -->
            <!-- ko text: getCartParam('summary_count') --><!-- /ko -->
            <!-- ko i18n: 'items' --><!-- /ko -->
        <!-- /ko -->
        </span>
    </span>

The question is how I would find all the code js, php (,html) that is in use to get the summary_count number.

I found a minicart.js but summary_count is nowhere to be seen there. I suspect this is some kind of identifier that is given to a function getCartParam. But I can not find getCartParam too.

Another example would be the onepage checkout for example:
I can find all the templates for the appropriate steps like shipping.html or payment.html. But how do they come together?
Where are they loaded and what defines ie. the order?

I not only seek to find the exact information on these two cases but be generally able to find this information myself. Thus I think to understand how the process works generally would be helpful.

Some enlightenment would be great here!

Best Answer

Unfortunately I can't paste the content (it's too extensive anyway) so I can just give a link.

I found Alan Storm has written a series where he explains the Javascript concepts of Magento 2. If you read this answer you will hopefully still find it here:

http://alanstorm.com/category/magento-2/#magento2-advanced-javascript

With the information from above I try to analyze what happens (feel free to add any information to clarify things or correct me):

Actually the code in the question is not enough to analyze it. Let's look at the whole code.

<?php
/**
 * Copyright © 2013-2017 Magento, Inc. All rights reserved.
 * See COPYING.txt for license details.
 */

// @codingStandardsIgnoreFile

/** @var $block \Magento\Checkout\Block\Cart\Sidebar */
?>

<div data-block="minicart" class="minicart-wrapper">
    <a class="action showcart" href="<?php /* @escapeNotVerified */ echo $block->getShoppingCartUrl(); ?>"
       data-bind="scope: 'minicart_content'">
        <span class="text"><?php /* @escapeNotVerified */ echo __('My Cart'); ?></span>
        <span class="counter qty empty"
              data-bind="css: { empty: !!getCartParam('summary_count') == false }, blockLoader: isLoading">
            <span class="counter-number"><!-- ko text: getCartParam('summary_count') --><!-- /ko --></span>
            <span class="counter-label">
            <!-- ko if: getCartParam('summary_count') -->
                <!-- ko text: getCartParam('summary_count') --><!-- /ko -->
                <!-- ko i18n: 'items' --><!-- /ko -->
            <!-- /ko -->
            </span>
        </span>
    </a>
    <?php if ($block->getIsNeedToDisplaySideBar()): ?>
        <div class="block block-minicart empty"
             data-role="dropdownDialog"
             data-mage-init='{"dropdownDialog":{
                "appendTo":"[data-block=minicart]",
                "triggerTarget":".showcart",
                "timeout": "2000",
                "closeOnMouseLeave": false,
                "closeOnEscape": true,
                "triggerClass":"active",
                "parentClass":"active",
                "buttons":[]}}'>
            <div id="minicart-content-wrapper" data-bind="scope: 'minicart_content'">
                <!-- ko template: getTemplate() --><!-- /ko -->
            </div>
            <?php echo $block->getChildHtml('minicart.addons'); ?>
        </div>
    <?php endif ?>
    <script>
        window.checkout = <?php /* @escapeNotVerified */ echo \Zend_Json::encode($block->getConfig()); ?>;
    </script>
    <script type="text/x-magento-init">
    {
        "[data-block='minicart']": {
            "Magento_Ui/js/core/app": <?php /* @escapeNotVerified */ echo $block->getJsLayout();?>
        },
        "*": {
            "Magento_Ui/js/block-loader": "<?php /* @escapeNotVerified */ echo $block->getViewFileUrl('images/loader-1.gif'); ?>"
        }
    }
    </script>
</div>

You need to go further up the code sample than what I initially posted. The first thing to find is: <a class="action showcart" ... data-bind="scope: 'minicart_content'". What Magento does here is (I'll quote Alan's series):

Magento will apply the [minicart_content] view model to this tag and its descendants.

But where does the minicart_content view model come in place? You have to look a bit further down in the code at the x-magento-init script tag:

<script type="text/x-magento-init">
    {
        "[data-block='minicart']": {
            "Magento_Ui/js/core/app": <?php /* @escapeNotVerified */ echo $block->getJsLayout();?>
        },
        "*": {
            "Magento_Ui/js/block-loader": "<?php /* @escapeNotVerified */ echo $block->getViewFileUrl('images/loader-1.gif'); ?>"
        }
    }
</script>

Here Magento sets, via knockoutJs, the loaderAnimation.gif. And before the minicart is somehow initialized to the data-block named minicart by [data-block='minicart'] which suspiciously looks like a CSS style selector. For me what is returned by $block->getJsLayout() is the following:

{
   "components":{
      "minicart_content":{
         "children":{
            "subtotal.container":{
               "children":{
                  "subtotal":{
                     "children":{
                        "subtotal.totals":{
                           "config":{
                              "display_cart_subtotal_incl_tax":0,
                              "display_cart_subtotal_excl_tax":1,
                              "template":"Magento_Tax\/checkout\/minicart\/subtotal\/totals"
                           },
                           "children":{
                              "subtotal.totals.msrp":{
                                 "component":"Magento_Msrp\/js\/view\/checkout\/minicart\/subtotal\/totals",
                                 "config":{
                                    "displayArea":"minicart-subtotal-hidden",
                                    "template":"Magento_Msrp\/checkout\/minicart\/subtotal\/totals"
                                 }
                              }
                           },
                           "component":"Magento_Tax\/js\/view\/checkout\/minicart\/subtotal\/totals"
                        }
                     },
                     "component":"uiComponent",
                     "config":{
                        "template":"Magento_Checkout\/minicart\/subtotal"
                     }
                  }
               },
               "component":"uiComponent",
               "config":{
                  "displayArea":"subtotalContainer"
               }
            },
            "item.renderer":{
               "component":"uiComponent",
               "config":{
                  "displayArea":"defaultRenderer",
                  "template":"Magento_Checkout\/minicart\/item\/default"
               },
               "children":{
                  "item.image":{
                     "component":"Magento_Catalog\/js\/view\/image",
                     "config":{
                        "template":"Magento_Catalog\/product\/image",
                        "displayArea":"itemImage"
                     }
                  },
                  "checkout.cart.item.price.sidebar":{
                     "component":"uiComponent",
                     "config":{
                        "template":"Magento_Checkout\/minicart\/item\/price",
                        "displayArea":"priceSidebar"
                     }
                  }
               }
            },
            "extra_info":{
               "component":"uiComponent",
               "config":{
                  "displayArea":"extraInfo"
               }
            },
            "promotion":{
               "component":"uiComponent",
               "config":{
                  "displayArea":"promotion"
               }
            }
         },
         "config":{
            "itemRenderer":{
               "default":"defaultRenderer",
               "simple":"defaultRenderer",
               "virtual":"defaultRenderer"
            },
            "template":"Magento_Checkout\/minicart\/content"
         },
         "component":"Magento_Checkout\/js\/view\/minicart"
      }
   },
   "types":[

   ]
}

Now that's a whole lot of hoopla! For our purpose here we will find where the minicart_content view model comes from. I'll quote Alan again:

As we know [...] when Magento encounters a text/x-magento-init script tag with an * attribute, it will

  1. Initialize the specified RequireJS module (Magento_Ui/js/core/app)
  2. Call the function returned by that module, passing in the data object

The Magento_Ui/js/core/app RequireJS module is a module that instantiates KnockoutJS view models to use with the scope attribute. Its full implementation is beyond the, um, “scope” of this article, but at a high level Magento will instantiate a new javascript object for each individual RequireJS module configured as a component, and that new object becomes the view model.

Now we can see that a whole lot of components are initialized here like for the product images in the cart. But also this one: "component":"Magento_Checkout\/js\/view\/minicart". This translates to the file module-checkout/view/frontend/web/js/view/minicart.js that @Mitch7663 pointed at in his answer.

Here you find the function getCartParam that's referenced in the template file:

<span class="counter-number"><!-- ko text: getCartParam('summary_count') --><!-- /ko --></span>

With the use of knockoutJs magic this places a text node with the return of getCartParam called with the identifier summary_count. Which itself is the key of a field in this.cart.

This is as far as I got now. I'm not yet sure where the data from this.cart gets into all of this. Also I skipped all the hazzle with observables and what not that you can read about in Alans series...no actually you should read it!

Also it seems like Magento breaks its own standards here as I see stuff selected with jQuery in that file when actually that should come from outside of it.

Anyway I'll update the answer if I get further.

Related Topic