Search code examples
symfonyknockout.jstwigknockout-binding-handlers

Symfony Knockout: Custom Binding Handler cannot listen to the binding handler


On my symfony 3 project I made a form that I want to submit via ajax using knockout.js. But for some it makes the browser to redirect into the action instead of doing via ajax.

The form that I want to submit with ajax on a file named app/FosUserBundle/views/Registration/register.html.twig is:

{% extends "FOSUserBundle::layout.html.twig" %}

{% block fos_user_content %}

  {% form_theme form '::themes/register-form-theme.html.twig' %}
  {% trans_default_domain 'FOSUserBundle' %}

<div class="container" data-bind="with:registerVm">
  <div class="register-box">
    <div class="register-logo">
        <h1>PhotoShare!</h1>
    </div>
    <div class="register-box-body">
      <p class="login-box-msg">Register a new membership</p>
      {{ form_start(form, {'method': 'post', 'action': path('fos_user_registration_register'),'attr':{'data-bind':'formSubmitAjax:\{\'success\':registerComplete\}'}}) }}
        {{ form_widget(form) }}
        <div class="row">
          <div class="col-xs-4">
            <input type="submit" class="btn btn-primary btn-block btn-flat" value="{{ 'registration.submit'|trans }}" />
          </div>
        </div>
      {{ form_end(form) }}
  </div>
</div>

{% endblock fos_user_content %}

The form needs to use the following knockout.js custom binding handler:

define(['knockout','jquery'],function(ko,$)
{

  ko.bindingHandlers.formSubmitAjax={
    init: function(element, valueAccessor, allBindingsAccessor)
    {
      var callbacks=valueAccessor();

      var action=$(element).attr('action');
      var method=$(element).attr('method');
      var dataType=(callbacks && callbacks['type'])?callbacks['type']:'json';//By default use Json


      $(element).submit(function(e)
      {
        e.preventDefault();
        var form_data=   $(this).serialize();

        $.ajax({
          'method':method,
          'data':form_data,
          'url':action,
          'beforeSend':function()
          {
            if(typeof callbacks['beforeSend'] === 'function') callbacks['beforeSend']();
          },
          'success':function(data,textStatus,jqXHR)
          {
            if(typeof callbacks['success'] === 'function') callbacks['success'](data,textStatus,jqXHR);
          },
          'error':function(jqXHR,textStatus,errorThrown)
          {
            if(typeof callbacks['error'] === 'function') callbacks['error'](jqXHR,textStatus,errorThrown);
          },
          'complete':function(jqXHR,textStatus)
          {
            if(typeof callbacks['always'] === 'function') callbacks['always'](jqXHR,textStatus);
          }
        });
      });
    }
  };

})

This form gets rendered via pager.js on this twig template:

{% extends "base.html.twig" %}

{% set classes=''%}

{% block javascriptsHeader %}
    <script src="{{asset('assets/vendor/require.js')}}" data-main="{{path('main_javascript')}}" ></script>
{% endblock %}

{% block stylesheets %}
  {{ parent() }}
  <link rel="stylesheet" type="text.css" href="{{ asset('assets/vendor/xeditable/css/bootstrap-editable.css') }}" >
  <style>
    #message-area{
        z-index:9999;
    }
    .page{
        min-height:100%;
        min-width:100%;
    }
  </style>
{% endblock %}

{% block body %}
    <div id="message-area"></div>
    <div class="hold-transition register-page page" data-bind="page: {id:'register', title:'Register', role:'start', sourceOnShow: '{{ path('fos_user_registration_register') }}', withOnShow:function(a){registerVm.init();},with:registerVm}"></div>
{% endblock %}

That extends the following base template:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8" />
        <title>{% block title %}Welcome!{% endblock %}</title>
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        {% block stylesheets %}
          <link rel="stylesheet" type="text/css" href="{{asset('assets/vendor/bootstrap/css/bootstrap.css')}}" >
          <link rel="stylesheet" type="text/css" href="{{asset('assets/vendor/adminlte/adminlte.css')}}" >
          <link rel="stylesheet" type="text/css" href="{{asset('assets/vendor/adminlte/skin-blue.css')}}" >
        {% endblock %}
        <link rel="icon" type="image/x-icon" href="{{ asset('favicon.ico') }}" />

        {% block javascriptsHeader %}
        {% endblock %}

    </head>
    <body class="{{ classes }}">
      {% block body %}
      {% endblock body %}

      {% block javascriptsFooter %}

      {% endblock javascriptsFooter %}
   </body>
</html>

All the required javascript I render it via the following require.js template:

requirejs.config({
  baseUrl:'{{asset('assets')}}',
  paths:{
    'text':'{{ asset('assets/vendor/text') }}',

    'knockout':'{{ asset('assets/vendor/knockout') }}',
    'pager':"{{asset('assets/vendor/pager')}}",
    'bootstrap':"{{asset('assets/vendor/bootstrap/js/bootstrap')}}",

    'jquery':"{{asset('assets/vendor/jquery')}}",
    'jquery_ui':"{{ asset('assets/vendor/jquery-ui') }}",

    'xeditable_bootstrap':"{{ asset('assets/vendor/xeditable/xeditable') }}",
    'ko_xeditable':"{{ asset('assets/vendor/knockout/knockout.x-editable') }}",

    'jquery-fileupload':"{{ asset('assets/vendor/jquery_fileupload/jquery.fileupload') }}",
    'jquery-iframe':"{{ asset('assets/vendor/jquery_fileupload/jquery.iframe-transport') }}",
    'jquery-ui-widget':"{{ asset('assets/vendor/jquery_fileupload/jquery.ui.widget') }}",

    'masterViewModel':"{{ asset('assets/js/viewModels/masterViewModel') }}",
    'registerViewModel':"{{ asset('assets/js/viewModels/registerPageViewModel') }}",
    {% block Viewmodels %}
    {% endblock %}

    'formPost':"{{ asset('assets/js/bindingHandlers/formPost') }}",

    'compMessage':"{{ asset('assets/js/components/message/message') }}",
    'extBooleanToggle':'assets/js/extenders/booleanToggle',
  },
  shim:{
    'pager': ['jquery', 'knockout'],
    'jquery_ui':['jquery'],
    'bootstrap':['jquery'],
    'xeditable_bootstrap':['jquery-ui','bootstrap'],
    'ko_xeditable':['xeditable_bootstrap'],
    'jquery-fileupload':['jquery-iframe','jquery-ui-widget'],
    'jquery-ui-widget':['jquery_ui'],//Jquery_ui already load jquery
    'jquery-iframe':['jquery']
    {% block CustomShim %}
    {% endblock %}
    },
  waitSeconds: 200,
});

define(['jquery','knockout','pager','masterViewModel','bootstrap'],function($,ko,pager,masterViewModel)
{
    $(document).ready(function(){
        pager.extendWithPage(masterViewModel);
        ko.applyBindings(masterViewModel);
        pager.start();
    });
});

And the master view model that knockout and pager.js initialize with is has the following:

define(['knockout','jquery','registerViewModel'],function(ko,$,registervm){

    function MasterViewModel()
    {
        var self=this;      
        self.registerVm=new registervm(self);
    }

    return new MasterViewModel();
})

Do you fellows have any Idea why the formSubmitAjax does not gets called?


Solution

  • I put the following on require.js shim section and worked like a charm:

     'masterViewModel':['registerViewModel'],
     'registerViewModel':['formPost']
    

    Resulting to the followimg main.js.twig

    requirejs.config({
      baseUrl:'{{asset('assets')}}',
      paths:{
        'text':'{{ asset('assets/vendor/text') }}',
    
        'knockout':'{{ asset('assets/vendor/knockout') }}',
        'pager':"{{asset('assets/vendor/pager')}}",
        'bootstrap':"{{asset('assets/vendor/bootstrap/js/bootstrap')}}",
    
        'jquery':"{{asset('assets/vendor/jquery')}}",
        'jquery_ui':"{{ asset('assets/vendor/jquery-ui') }}",
    
        'xeditable_bootstrap':"{{ asset('assets/vendor/xeditable/xeditable') }}",
        'ko_xeditable':"{{ asset('assets/vendor/knockout/knockout.x-editable') }}",
    
        'jquery-fileupload':"{{ asset('assets/vendor/jquery_fileupload/jquery.fileupload') }}",
        'jquery-iframe':"{{ asset('assets/vendor/jquery_fileupload/jquery.iframe-transport') }}",
        'jquery-ui-widget':"{{ asset('assets/vendor/jquery_fileupload/jquery.ui.widget') }}",
    
        'masterViewModel':"{{ asset('assets/js/viewModels/masterViewModel') }}",
        'registerViewModel':"{{ asset('assets/js/viewModels/registerPageViewModel') }}",
        {% block Viewmodels %}
        {% endblock %}
    
        'formPost':"{{ asset('assets/js/bindingHandlers/formPost') }}",
        //'debug':"{{ asset('assets/js/bindingHandlers/debug') }}",
    
        'compMessage':"{{ asset('assets/js/components/message/message') }}",
        'extBooleanToggle':'assets/js/extenders/booleanToggle',
      },
      shim:{
        'pager': ['jquery', 'knockout'],
        'jquery_ui':['jquery'],
        'bootstrap':['jquery'],
        'xeditable_bootstrap':['jquery-ui','bootstrap'],
        'ko_xeditable':['xeditable_bootstrap'],
        'jquery-fileupload':['jquery-iframe','jquery-ui-widget'],
        'jquery-ui-widget':['jquery_ui'],//Jquery_ui already load jquery
        'jquery-iframe':['jquery'],
        'masterViewModel':['registerViewModel'],
        'registerViewModel':['formPost']
        {% block CustomShim %}
        {% endblock %}
        },
      waitSeconds: 200,
    });
    
    define(['jquery','knockout','pager','masterViewModel','bootstrap'],function($,ko,pager,masterViewModel)
    {
        $(document).ready(function(){
                pager.extendWithPage(masterViewModel);
                ko.applyBindings(masterViewModel);
                pager.start();
        });
    });
    

    And worked like a charm!!!