Introduction

Knockout JS is a Javascript library using the MVVM (Model-View-ViewModel) pattern to bind data with DOM elements. It is widely used in Magento 2, where implementations often include a ViewModel in javascript and an HTML template bound to the ViewModel.

In this article, we will go through the steps to create a module called Magenest_Feedback, used to collect feedback. We will use KnockoutJS in Magento 2 to:

- Form submit feedback

- Get feedback data to the frontend page

Initialize module

Let's skim through the usual steps.

First, we need to create the registration.php and etc/module.xml files:

Registration.php:

<?php
\Magento\Framework\Component\ComponentRegistrar::register(
        \Magento\Framework\Component\ComponentRegistrar::MODULE,
        'Magenest_Feedback',
        __DIR__
    );

Module.xml:

<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="Magenest_Feedback"/>
</config>

Next up, we need to create a table to store the feedback, we will use a new way to create tables instead of writing regular scripts. Create the file etc/db_schema.xml

Db_schema.xml:

<?xml version="1.0"?>
<schema xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:Setup/Declaration/Schema/etc/schema.xsd">
   <table name="magenest_feedback" resource="default" engine="innodb" comment="Magenest Feedback">
       <column xsi:type="int" name="entity_id" padding="10" unsigned="true" nullable="false" identity="true" comment="ID"/>
       <column xsi:type="int" name="product_id" nullable="false" comment="Product Id"/>
       <column xsi:type="int" name="order_id" comment="Order Id"/>
       <column xsi:type="int" name="customer_id" comment="Customer Id"/>
       <column xsi:type="varchar" name="message" length="255"  comment="Message"/>
       <column xsi:type="boolean" name="status" default="0"  comment="Status"/>
       <constraint xsi:type="primary" referenceId="PRIMARY">
           <column name="entity_id"/>
       </constraint>
       <index referenceId="magenest_feedback_entity_id" indexType="btree">
           <column name="entity_id"/>
       </index>
   </table>
</schema>

Create routes, controller, and layout:

Create etc\frontend\routes.xml:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
   <router id="standard">
       <route id="feedback" frontName="feedback">
           <module name="Magenest_Feedback"/>
       </route>
   </router>
</config>

Create controller Controller\Customer\Index.php:

<?php
/**
* Copyright © 2021 Magenest. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magenest\Feedback\Controller\Customer;


use Magento\Framework\App\Action\Action;
use Magento\Framework\App\Action\Context;
use Magento\Framework\App\ResponseInterface;
use Magento\Framework\View\Result\PageFactory;


class Index extends Action
{
   protected $_pageFactory;


   public function __construct(
	Context $context, 
	PageFactory $pageFactory
   ){
       parent::__construct($context);
       $this->_pageFactory = $pageFactory;
   }


   public function execute()
   {
       return $this->_pageFactory->create();
   }
}

With these files, you can access  http://baseURL/feedback/customer/index with default Magento layout and blank content area.

Create the layout for the custom page in view\frontend\layout\feedback_customer_index.xml:

<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
   <body>
       <referenceContainer name="content">
           <block class="Magenest\Feedback\Block\Customer\Feedback" name="customer_feedback" template="Magenest_Feedback::customer_feedback.phtml"/>
       </referenceContainer>
   </body>
</page>

Create block, template

Create the file customer_feedback.phtml. Here we bind a scope called ‘feedback-form’ and declare a component. 

View\frontend\templates\customer_feedback.phtml :

<?php
/**
* @var \Magenest\Feedback\Block\Customer\Feedback $block ;
*/
?>
<div id="feedback-form-data" data-bind="scope: 'feedback-data'">
   <!-- ko template: getTemplate() --> <!-- /ko -->
</div>


<script type="text/x-magento-init">
   {
       "*": {
           "Magento_Ui/js/core/app": {
               "components": {
                   "feedback-data": {
                       "component": "Magenest_Feedback/js/view/customer-feedback",
                       "feedbackData": <?php /* @escapeNotVerified */ echo $block->feedbackLastData(); ?>
                   }
               }
           }
       }
   }

</script>

KnockoutJS will process the component and display the template of the component in feedback-form-data via <!-- ko template: getTemplate() --> <!-- /ko -->  tag.

Create block Block\Customer\Feedback.php, we get feedback last item from the collection and return the result to the component:

Block\Customer\Feedback.php:

<?php
/**
* Copyright © 2021 Magenest. All rights reserved.
* See COPYING.txt for license details.
*/
namespace Magenest\Feedback\Block\Customer;

use Magento\Framework\View\Element\Template;
use Magenest\Feedback\Model\ResourceModel\Feedback\CollectionFactory;
use Magento\Framework\Json\Helper\Data;

class Feedback extends Template
{
   protected $feedbackCollectionFactory;
   protected $jsonHelper;

   public function __construct(
       Template\Context $context,
       CollectionFactory $feedbackCollectionFactory,
       Data $jsonHelper,
       array $data = []
   ){
       $this->feedbackCollectionFactory = $feedbackCollectionFactory;
       $this->jsonHelper = $jsonHelper;
       parent::__construct($context, $data);
   }

   public function feedbackLastData(){
       //we get the last feedback
       $feedbackData = $this->feedbackCollectionFactory->create()
			 ->getLastItem()->getData();
       return $this->jsonHelper->serialize($feedbackData);
   }
}

Create component and template:

Create component to process data view\frontend\web\js\customer_feedback.js:

define([
   'jquery',
   'ko',
   'uiComponent',
   'mage/url',
   'Magento_Ui/js/modal/alert',
], function($, ko, Component, url, alert) {
   return Component.extend({
       defaults: {
           template: 'Magenest_Feedback/customer-feedback',
           data : ko.observableArray([])
       },

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

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

       sendFeedback: function ($param) {
           var data = {
               'order_id': $('#order_id').val(),
               'product_id': $('#product_id').val(),
               'message': $('#message').val(),
           };
           $.ajax({
               url: url.build('feedback/customer/save'),
               data: data,
               type: 'get',
               dataType: 'json',
               context: this,
               success: function (response) {
                   if (response.status === true){
                       this.data(response.newData);
                       alert({
                           content: $.mage.__('Thanks for Submitting.')
                       });
                   }
               },
           });
       }
   });
});

Some commonly used functions in components:

  • Defaults: used to declare local variables for components and bind to the template, is declared as key: value.
  • Initialize function:  
    • Initialize the specified RequireJS module (Magento_Ui/js/core/app).
    • Used to handle logic during component initialization, Here we add the feedback data so that we can bind data to the template.
  • The initObservable method is called in the uiElement's initialize method Magento's custom javascript object system.
  • Send Feedback function: Called when submitting the form feedback in the customer-feedback.html file below and call ajax to save feedback information, if successful, will set new data

The Magento_Ui/js/core/app RequireJS module is a module that instantiates KnockoutJS view models to use with the scope attribute.

Create template component to render the content of the feedback data and form submit feedback view\frontend\web\template\customer-feedback.html:

<form class="form customer-feedback">
   <div class="field">
       <label class="label">
           <span><!-- ko i18n: 'Product Id'--><!-- /ko --></span>
       </label>
       <div class="control">
           <input id="product_id" name="product_id" type="number" value="" class="input-text">
       </div>
   </div>
   <div class="field">
       <label class="label">
           <span><!-- ko i18n: 'Order Id'--><!-- /ko --></span>
       </label>
       <div class="control">
           <input id="order_id" name="order_id" type="number" value="" class="input-text">
       </div>
   </div>
   <div class="field">
       <label class="label">
           <span><!-- ko i18n: 'Message'--><!-- /ko --></span>
       </label>
       <div class="control">
           <textarea id="message" name="message"></textarea>
       </div>
   </div>
   <br/>
   <div class="actions-toolbar">
       <div class="primary">
           <button type="submit" class="action primary" data-bind="click : sendFeedback">
               <span>Submit</span>
           </button>
       </div>
   </div>
</fieldset>
</form>
<table>
   <thead>
   <tr>
       <th><span data-bind="i18n: 'Id'"></span></th>
       <th><span data-bind="i18n: 'Product Id'"></span></th>
       <th><span data-bind="i18n: 'Order Id'"></span></th>
       <th><span data-bind="i18n: 'Message'"></span></th>
       <th><span data-bind="i18n: 'Status'"></span></th>
   </tr>
   </thead>
   <tbody data-bind="foreach: data">
       <tr>
           <td data-bind="text: entity_id"></td>
           <td data-bind="text: product_id"></td>
           <td data-bind="text: order_id"></td>
           <td data-bind="text: message"></td>
           <td data-bind="text: status"></td>
       </tr>
   </tbody>
</table>

File Structure

The file structure should be looking similar to our screencap below:

Show in frontend

As soon as I submit feedback, the new information is updated immediately

Debug

When using KnockoutJS for writing components in a website, sometimes we will get stuck when it doesn’t work as we expected. And in that case, we need to use the debugger to debug this issue, with the KnockoutJS, we introduced the Knockout Context Debugger tool. With this tool, we can debug our knockout code with ease. Please see some guides below:

We can see the necessary information in the Knockout Context tab, such as a component file, data …

You can find the file customer-feedback.js in the source and each time the page loads, it is initialized and reset the bind data to with the template.

When inputting information and submitting feedback forms successfully:

We will get the data returned from Ajax and then set up new data to update the information.

Conclusion

Thank you for reading the article. I hope that this tutorial is helpful to you.

You can get the completed module here.

If any problem occurs, please don't hesitate to contact us via support@magenest.com.