Search code examples
phpjqueryajaxopencartuserfrosting

Error redirects for OpenCart with UserFrosting


So I have been working on integrating OpenCart in to UserFrosting (http://www.userfrosting.com/). Basically I am making all the admin dashboard pages load in the UserFrosting dashboard by using .load to inject the pages into a div on my own html pages. I am almost finished but have one last piece to get working that isn't remaining within the UF dashboard.

I will use the Catalog / Categories page as an example to show what I am doing:

Here is my html file for categories, it's called cata-categories.html

<!DOCTYPE html>
        <html lang="en">
        {% include 'components/head.html' %}
        <script src="https://code.jquery.com/jquery-1.10.2.js"></script>
        <body>
            <div id="wrapper">
                {% include 'components/nav-account.html' %}
                <div id="page-wrapper">
                    {% include 'components/alerts.html' %}

                    <ol id="categories"></ol>

                <script>
                    $( "#categories" ).load( "../solutions/admin/index.php?route=catalog/category" );
                </script>



                    <br>
                    {% include 'components/footer.html' %}    
                </div>
            </div></div>
        </body>
        </html>

This is accessed via the sidebar of UserFrosting with the following code in sidebar.html:

{% if checkAccess('uri_manage_groups') %}
                <li>
                    <a href="{{site.uri.public}}/cata-categories/"><i class="fa fa-tags fa-fw"></i>Categories</a>
                </li>
                {% endif %}

The page is rendered via the following code in public/index.php for UserFrosting:

 $app->get('/cata-categories/?', function () use ($app) {   
           $app->render('cata-categories.html', [
               'page' => [
                   'author' =>         $app->site->author,
                   'title' =>          "Catagories",
                   'description' =>    "Catalogue Catagories.",
                   'alerts' =>         $app->alerts->getAndClearMessages()
               ]
           ]);  
        });

To keep all links clicked on the OpenCart pages I put the following script on the bottom of each main .tpl (so for example category_list.tpl):

<script>
$("#categories").on("click", "a", function (e) {
    $("#categories").load($(this).attr("href"));
    e.preventDefault();
});
</script>

Then to keep things on the same page when submitting forms I changed the redirects in the controllers (eg. category.php) to be the following:

$this->response->redirect('../../portal/cata-categories');

Everything works great, now the only issue is errors in the form submission or filtering results. For example if someone tries to add a new category but doesn't fill in any information and clicks save, it redirects to /admin/index.php?route=catalog/category/add but not within the UserFrosting dashboard.

I want it to remain within my dashboard, and need a nudge in the right direction on what to modify within the controller to get it to do this, I'm assuming some sort of AJAX to force it not to reload the page and just show the errors. Same thing with filter buttons, it obviously needs to add filters to the URL and therefore doesn't load within the dashboard - need a way to filter without refreshing.

Any help from people with experience modifying OpenCart would be greatly appreciated and if you need any more info on what I am doing feel free to ask.


Solution

  • OpenCart's routes are not built for AJAX requests

    The way that UserFrosting and OpenCart handle POST requests seem to be fundamentally at odds. If you look at the code for the catalog/category/add route (or at least, that's what I think it is, but the code doesn't seem to have any in-code documentation so it's hard to be sure), you'll see this:

    public function add() {
        $this->language->load('catalog/category');
    
        $this->document->setTitle($this->language->get('heading_title'));
    
        $this->load->model('catalog/category');
    
        if (($this->request->server['REQUEST_METHOD'] == 'POST') && $this->validateForm()) {
            $this->model_catalog_category->addCategory($this->request->post);
    
            $this->session->data['success'] = $this->language->get('text_success');
    
            $url = '';
    
            if (isset($this->request->get['sort'])) {
                $url .= '&sort=' . $this->request->get['sort'];
            }
    
            if (isset($this->request->get['order'])) {
                $url .= '&order=' . $this->request->get['order'];
            }
    
            if (isset($this->request->get['page'])) {
                $url .= '&page=' . $this->request->get['page'];
            }
    
            $this->response->redirect($this->url->link('catalog/category', 'token=' . $this->session->data['token'] . $url, 'SSL'));
        }
    
        $this->getForm();
    }
    

    So what happens here? It would seem that when you POST to catalog/category/add, it starts to generate a document (I'm guessing that is what the first three lines are doing), and then it processes your POST request (I believe $this->model_catalog_category->addCategory($this->request->post); actually adds the category).

    Then, it rebuilds the URL for listing all of the categories (I assume), and actually redirects the response to that page (still in the server-side code that processes the POST request, mind you). This is all fine, and is actually how most old-school (pre-AJAX) applications worked, but UF does things a little differently.

    UserFrosting's routes are (mostly) RESTful

    In UF, a POST request is RESTful, which means that the only task of a POST request is to modify a resource. So when you click "submit" in a UserFrosting form, the POST request is dispatched behind the scenes, by AJAX, where it is processed (either successfully updating the resource and returning an HTTP 200 success code, or failing for some reason and returning an error code). If the page needs to be refreshed or redirected after this request, it is the responsibility of the client-side (Javascript) code to do this.

    Recommendation

    My advice would be to discard OpenCart's controller entirely, and re-implement it in a RESTful pattern. So instead of having OpenCart's add() method, you should have methods like:

    1. formCategoryAdd: generates the form to add a category. This might be as simple as a wrapper around OpenCart's getForm method. This is basically the same type of thing that UserFrosting's formUserCreate does.
    2. addCategory: this actually processes the POST request to add a category. Again, you might need to do nothing more here than call OpenCart's addCategory and validateForm methods. The only difference is that you won't redirect the request when it's finished. Instead, you'll add a success message to the message stream (if it succeeds in adding the category), or you'll add an error message and then return an appropriate error code (if it fails for some reason). See UserFrosting's createUser method for an example of how this could work.

    Then, back on the client side, you'll need some Javascript to process the form submission. You can basically steal it from any of the UF pages. Assuming your form is named add-category:

    <script>
        $(document).ready(function() {            
          // Process form 
          $("form[name='add-category']").formValidation({
            framework: 'bootstrap',
            // Feedback icons
            icon: {
                valid: 'fa fa-check',
                invalid: 'fa fa-times',
                validating: 'fa fa-refresh'
            },
            fields: { "" : ""}
          }).on('success.form.fv', function(e) {
            // Prevent double form submission
            e.preventDefault();
            // Get the form instance
            var form = $(e.target);
            // Serialize and post to the backend script in ajax mode
            var serializedData = form.serialize();            
            var url = form.attr('action');
            $.ajax({  
              type: "POST",  
              url: url,  
              data: serializedData       
            }).done(function(data, statusText, jqXHR) {
                // Forward to account home page on success
                window.location.replace(site.uri.public);
            }).fail(function(jqXHR) {
                if ((typeof site !== "undefined") && site['debug'] == true) {
                    document.body.innerHTML = jqXHR.responseText;
                } else {
                    console.log("Error (" + jqXHR.status + "): " + jqXHR.responseText );
                    // Display errors on failure
                    $('#userfrosting-alerts').flashAlerts().done(function() {
                        // Re-enable submit button
                        form.data('formValidation').disableSubmitButtons(false);
                    });
                }
            });
          });
        });        
    </script>
    

    If you want to reduce client-side code repetition, I've put this Javascript into a separate function in the dev branch: https://github.com/alexweissman/UserFrosting/blob/dev/public/js/userfrosting.js#L58-L113