Search code examples
angularjsajaxwordpressgravity-forms-plugin

Using gravity forms with an asynchronous site


I have a wordpress site using angular to handle ajaxed page loads. When loading a page directly, the gravity forms load as expected. If you load from a non grav forms page then navigate to one with a form, you get nothing.

This may have something to do with the scripts grav forms embeds below the forms. So I am wondering if there is a way to init a form after being loaded in via ajax.

I can use some html, jquery magic to fetch the form via ajax after page load using this in functions.php:

gravity_form_enqueue_scripts( $form_id, true );
echo gravity_form($form_id, false, false, false, null, true, 12, true);

When I inject the results into the DOM I just get this console error:

gformInitSpinner is not defined

and even if I target the new html and show the form, which comes through as html, I get a massively malformed form that doesn't work. Clearly because the scripts don't get evaluated and that js error blocks it anyhow.

So does anyone know if there is a way to make gravity forms work with an asynchronous site? Or if you can load a functional form using ajax?

I am not able to fill the enqueue system with gravity_form_enqueue_scripts since there could be any number of forms made at any time. That's not a practical solution.


Solution

  • Well, no thanks to the gravity forms development team and the entire internet, we have a solution for all of us tasked with what seems like an obvious use of a very commonly used plugin...

    First off, set up your functions.php file so it can handle this:

    // filter the Gravity Forms button type
    add_filter( 'gform_submit_button', 'form_submit_button', 10, 2 );
    function form_submit_button( $button, $form ) {
        return "<button class='btn btn-green' type='submit' id='gform_submit_button_{$form['id']}'>Submit</button>";
    }
    
    // Hook up the AJAX actions
    add_action( 'wp_ajax_nopriv_gf_button_get_form', 'gform_ajax_get_form' );
    add_action( 'wp_ajax_gf_button_get_form', 'gform_ajax_get_form' );
    
    // Add the "button" action to the gravityforms shortcode
    // e.g. [gravityforms action="button" id=1 text="button text"]
    add_filter( 'gform_shortcode_form', 'gform_shortcode', 10, 3 );
    
    function gform_shortcode( $shortcode_string, $attributes, $content )
    {
        $a = shortcode_atts( array(
            'id' => 0,
            'text' => 'Show me the form!',
        ), $attributes );
    
        $form_id = absint( $a['id'] );
    
        if ( $form_id < 1 ) {
            return 'Missing the ID attribute.';
        }
    
        $ajax_url = admin_url( 'admin-ajax.php' );
    
        $html = sprintf('<div ng-controller="GavityFormController as garvityFormCtrl" ng-init="init(\'%s\', \'%s\')"><div class="gform_container" ng-include="form"></div></div>', $form_id, $ajax_url);
    
        return $html;
    }
    
    function gform_ajax_get_form(){
        $form_id = isset( $_GET['form_id'] ) ? absint( $_GET['form_id'] ) : 0;
        $form = gravity_form($form_id, false, false, false, false, true, 0, false);
    
        $form = str_replace('text/javascript', 'text/ng-javascript', $form);
        $form = str_replace('<script ', '<script lazy-js ', $form);
    
        echo $form;
    
        die();
    }
    
    add_filter("wp_footer", "init_gf_scripts");
    function init_gf_scripts() {
        global $wpdb;
    
        //obviously change this if your prefix is different
        $sql = "SELECT `id` FROM `wp_rg_form` WHERE `is_active` = 1 AND `is_trash` = 0";
        $res = $wpdb->get_results($sql);
        foreach($res AS $r)
        {
            gravity_form_enqueue_scripts( $r->id, true );
        }
    }
    

    Next step can vary depending on how you set up your main js file. The necessary points being below. There's usually quite a bit more but hopefully you get the idea.

    (function(){
        require('./directives/forms/form');
    
        var util = require('./util');
    
        var app = angular.module('yourapp', [
            'yourapp-form',
        ])...
    

    util.js ... probably a common file. I'm not sure if it really has anything to do with the actual problem but it is required by the form components so here you go:

    module.exports = (function() {
        var api = {
            isMobileOrSmaller: function () {
                return $(window).width() < 681
            },
            findByProp: function (data, value, prop) {
                prop = prop || 'id';
                return $.grep(data, function(e){ return e[prop] == value; });
            },
            getIndexOf: function (collection, value, prop) {
                prop = prop || 'id';
                return collection.map(function (e) { return e[prop] }).indexOf(value);
            },
            roundTo: function (value, to) {
                to = to || 10;
                return Math.floor(value/to) * to;
            },
            debounce: function(func, wait, immediate) {
                var timeout;
                return function() {
                    var context = this, args = arguments;
                    var later = function() {
                        timeout = null;
                        if (!immediate) func.apply(context, args);
                    };
                    var callNow = immediate && !timeout;
                    clearTimeout(timeout);
                    timeout = setTimeout(later, wait);
                    if (callNow) func.apply(context, args);
                };
            },
            getScrollbarWidth: function () {
                return $(window).width();// - $('#siteBody > div').width();
            },
            getRandomInt: function (min, max) {
                return Math.floor(Math.random() * (max - min + 1)) + min;
            }
        };
    
        return api;
    })();
    

    And the form directives...

    /directives/forms/form.js

    module.exports = (function() {
        var api = {};
    
        var form = angular.module('yourapp-form', [
        ]).directive('lazyJs', [function () {
            return {
                restricted: 'A',
                link: function (scope, element) {
                    var code = element.text();
                    var tmpFunc = new Function(code);
    
                    scope.$on('GavityFormController::includeContentLoaded', function () {
                        tmpFunc();
    
                        if(window['gformInitDatepicker']) {
                            gformInitDatepicker();
                        }
    
                        if (window['gformInitPriceFields']) {
                            gformInitPriceFields();
                        }
                    })
                }
            }
        }]).controller('GavityFormController', ['$scope', '$rootScope', '$element', '$http', function ($scope, $rootScope, $element, $http) {
            var panel = this,
                formId = 0; 
    
            $scope.form = '';
            $scope.init = function (id, url) {
                formId = id;
                $scope.form = url + '?action=gf_button_get_form&form_id=' + id
            }
    
            $scope.$on('$includeContentLoaded', function(e, src){
                if ($('#gform_wrapper_' + formId).length > 0) {
                    $scope.$broadcast('GavityFormController::includeContentLoaded');
                }
            });
        }]);
    
        //registerring componenets
        var radio = require ('./radio');
        radio.register(form);
    
        var select = require ('./select');
        select.register(form);
    
        var text = require ('./text');
        text.register(form);
    
        var check = require ('./check');
        check.register(form);
    
        var textarea = require('./textarea');
        textarea.register(form);
    
        api.form = form;
    
    
        return api;
    })();
    

    and these are all the required form component files:

    radio.js:

    module.exports = (function() {
        var api = {};
    
        api.register = function (form) {
            form.directive('myappRadio', function(){
                return {
                    restricted: 'E',
                    transclude: true,
                    replace: true,
                    scope: {
                        model: '=model'
                    },
                    template: function ($element, $attrs) {
                        return '<div class="ipt ipt-radio"> \
                                    <div  ng-transclude></div> \
                                    <label for="' + $attrs.for + '">' + $attrs.label + '</label> \
                                </div>';
    
                    }
                }
            });
        }
    
    
        return api;
    })();
    

    select.js:

    var util = require('./../../util');
    
    module.exports = (function() {
        var api = {};
    
        api.register = function (form) {
            form.directive('select', ['$timeout', function($timeout){
                return {
                    restricted: 'E',
                    link: function($scope, $element, $attrs) {
                        $timeout(function () {
                            Selectize.define('input_modify', function(options) {
                                var self = this;
    
                                this.setup = (function() {
                                    var original = self.setup;
                                    return function() {
                                        original.apply(this, arguments);
    
                                        this.$control.find('input').attr('id', '');
                                    };
                                })();
                            });
    
                            $($element).selectize({
                                plugins: ['remove_button', 'input_modify']
                            });
                        }, 10);
                    }
                }
            }])
        }
    
    
        return api;
    })();
    

    text.js

    module.exports = (function() {
        var api = {};
    
        api.register = function (form) {
    
            form.directive('myapptext', function(){
                return {
                    restricted: 'E',
                    scope: {
                        model: '=model',
                        ngrequired: '='
                    },
                    link: function(scope, element, attrs, controllers) {
                        var panel = this;
    
                        scope.isActive = scope.model !== '';
                        scope.required = false;
    
                        if (attrs.required === "")
                            scope.required = true;
    
                        if (typeof scope.ngrequired !== "undefined")
                            scope.required = scope.ngrequired;
    
                        scope.onEnter = function () {
                            scope.isActive = true;
                        }
    
                        scope.onLeave = function () {
                            if (scope.model == '' || typeof scope.model == 'undefined'){
                                scope.isActive = false;
                            }
                        }
    
                        scope.$watch('ngrequired', function(value){
                            scope.required = scope.ngrequired;
                        });
                    },
                    template: function (element, attr) {
                        var id = attr.id || 'frm-' + attr.name,
                            type = attr.type || 'text';
                            value = attr.value || '';
                        return '<div class="ipt-text-wrappper" ng-class="{\'m-active\': isActive}"> \
                                    <input ng-required="required" ng-focus="onEnter()" ng-blur="onLeave()" ng-model="model" id="' + id + '" type="' + type + '" name="' + attr.name + '" value="' + value + '" class="ipt ipt-text"> \
                                    <label ng-click="onEnter()" for="' + id + '">' + attr.label + '</label> \
                                </div>';
                    }
                }
            });
        }
    
    
        return api;
    })();
    

    check.js:

    module.exports = (function() {
        var api = {};
    
        api.register = function (form) {
            form.directive('myappCheck', function(){
                return {
                    restricted: 'E',
                    transclude: true,
                    replace: true,
                    scope: {
                        model: '=model'
                    },
                    template: function ($element, $attrs) {
                        return '<div class="ipt ipt-radio"> \
                                    <div  ng-transclude></div> \
                                    <label for="' + $attrs.for + '">' + $attrs.label + '</label> \
                                </div>';
    
                    }
                }
            });
        }
    
    
        return api;
    })();
    

    textarea.js:

    module.exports = (function() {
        var api = {};
    
        api.register = function (form) {
            form.directive('myapptextarea', function(){
                return {
                    restricted: 'E',
                    scope: {
                        model: '=model',
                        ngrequired: '='
                    },
                    link: function(scope, element, attrs, controllers) {
                        var panel = this;
    
                        scope.isActive = scope.model !== '';
                        scope.required = false;
    
                        if (attrs.required === "")
                            scope.required = true;
    
                        if (typeof scope.ngrequired !== "undefined")
                            scope.required = scope.ngrequired;
    
                        scope.onEnter = function () {
                            scope.isActive = true;
                        }
    
                        scope.onLeave = function () {
                            if (scope.model == '' || typeof scope.model == 'undefined'){
                                scope.isActive = false;
                            }
                        }
    
                        scope.$watch('ngrequired', function(value){
                            scope.required = scope.ngrequired;
                        });
                    },
                    template: function (element, attr) {
                        var id = attr.id || 'frm-' + attr.name,
                            value = attr.value || '';
                        return '<div class="ipt-text-wrappper" ng-class="{\'m-active\': isActive}"> \
                                    <textarea ng-required="required" ng-focus="onEnter()" ng-blur="onLeave()" ng-model="model" id="' + id + '" name="' + attr.name + '" class="ipt ipt-textarea">' + value + '</textarea> \
                                    <label ng-click="onEnter()" for="' + id + '">' + attr.label + '</label> \
                                </div>';
                    }
                }
            });
        }
    
    
        return api;
    })();
    

    And that's it. Personally I invoke this in php by calling gravity_form function:

    <?php echo gravity_form(1, false, false, false, '', true, 12); ?>
    

    But I assume that a basic embedded form in the WYSIWYG would work also.

    I didn't write this myself so I am not sure if I could answer questions, but if you have them feel free to ask