Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamic number of Contact Form 7 recipients

I am working a on a form with, at it's core, two fieldset: main and "other recipient"; at the end of of the "other recipient" fieldset, I have a "add another recipient" link.

Here's who needs what:

Main recipient: everything

Other recipient: the "other recipient" fieldset;

Sub-sequent recipients: Respective fieldsets

So far, I've been looking at the Documentation but not much luck there, not that I expected any, either.


Edit

I think this is unclear, so I will be a little more explicit as to what is the context. My form is a registration where we can sign up multiple people; one of the fields is labeled "Your email". Since we can register more than one person at once, I need to duplicate the fieldset containing "Your email".


Edit 2

To help clarify, imagine that we are signing up kids for a summer camp. The first fieldset is general, say the parent's billing information, and the second fieldset is the child's information. The parent needs to be able to fill out a single form and dynamically add as many children as the parent desires.

In each of the children's fieldset, their email is required and they receive the information relevant to this child, where the email would be similar to:

Hello {children's name},

You've been registered to StackOverflow Summer Camp. Here's the information you need to know:

[...]

Thanks for being a good sport!

Hope this helps.

like image 304
davewoodhall Avatar asked Sep 28 '16 17:09

davewoodhall


1 Answers

When you've got a specific use case like this, shoehorning the functionality into peripherally related plugins often results in frustration. That being said - there are times where you're married to a specific plugin or approach, and just have to build on top of it.

With that caveat out of the way, I think you should approach this from the angle of creating a new fieldtype for Contact Form 7. This way you have control over rendering the field's HTML, the data validation, among other things. It might also provide an launch point for DB storage and sending reminders, later, as you've mentioned in a comment on another answer.

Here's this approach in action:

Testing CF7 Duplication

The new fieldtype is called emailplus, and you include it into a form like this:

<div class="cf7-duplicable-recipients">
    <label>Main Recipient (required)</label>
    [emailplus emails]
    [submit "Sign Up"]
</div>

Additionally, I've set the recipient under the "mail" panel in the form's settings to [emails].

If an emailsplus field is set as the recipient, the class overrides the default wpcf7 behaviour and sends mail to each value in the email array, substituting any [emails] placeholders in the body of the message on a per-email basis.

The emailplus fieldtype is encapsulated here in a class, and commented liberally:

<?php

class WPCF7_Duplicable_Email
{
    /**
     * The emails this form's field is addressed to
     *
     * @var array
     */
    public $emails = array();

    /**
     * The name of the tag bearing the emailplus type
     *
     * @var string
     */
    public $tag_name;

    /**
     * Instantiate the class & glom onto the wpcf7 hooks
     *
     * @return void
     */
    public function __construct()
    {
        add_action('wpcf7_init', array($this, 'add_emailplus_tag'));
        add_action('wpcf7_form_tag', array($this, 'assign_values_to_field'));
        add_filter('wpcf7_validate_emailplus', array($this, 'validate'), 2);
        add_action('wpcf7_before_send_mail', array($this, 'send_email'));
        add_action('wp_enqueue_scripts', array($this, 'emailplus_scripts'));
    }

    /**
     * Add our the [emailplus __YOUR_FIELD_NAME__] shortcode for use in wpcf7 forms.
     *
     * @uses wpcf7_add_shortcode
     *
     * @return void
     */
    public function add_emailplus_tag()
    {
        wpcf7_add_shortcode(
            array('emailplus'),
            array($this, 'shortcode_handler'),
            true
        );
    }

    /**
     * Renders the HTML for the [emailplus] shortcode
     *
     * Referenced from wpcf7_text_shortcode_handler in wp-content/plugins/contact-form-7/modules/text.php
     *
     * @param array $tag The data relating to the emailplus form field.
     *
     * @uses wpcf7_get_validation_error
     * @uses wpcf7_form_controls_class
     *
     * @return string
     */
    public function shortcode_handler(array $tag) {
        $tag = new WPCF7_Shortcode($tag);

        if (true === empty($tag->name)) {
            return '';
        }

        $validation_error = wpcf7_get_validation_error($tag->name);
        $class            = wpcf7_form_controls_class($tag->type);

        if ($validation_error) {
            $class .= ' wpcf7-not-valid';
        }

        $atts = array(
            'class'        => $tag->get_class_option($class),
            'id'           => $tag->get_id_option(),
            'tabindex'     => $tag->get_option('tabindex', 'int', true),
            'aria-invalid' => $validation_error ? 'true' : 'false',
            'type'         => 'email',
            'name'         => $tag->name.'[]', // <-- Important! the trailing [] Tells PHP this is an array of values
            'value'        => $tag->get_default_option()
        );

        $atts = wpcf7_format_atts($atts);

        $html = sprintf(
            '<div class="emailplus-wrapper %1$s"><input %2$s />%3$s</div>',
            sanitize_html_class($tag->name),
            $atts,
            $validation_error
        );

        // We identify the container that will hold cloned fields with the [data-wpcf7-duplicable-email] attr
        return '<div class="wpcf7-form-control-wrap %1$s" data-wpcf7-duplicable-email>'.$html.'</div>';
    }

    /**
     * Validates the value of the emailplus tag.
     *
     * Must be handled separately from other text-based form inputs,
     * since the submitted POST value is an array.
     *
     * We can safely assume emailplus while creating the WPCF7_Shortcode,
     * because we know we hooked this function onto 'wpcf7_validate_emailplus'
     *
     * @uses wpcf7_is_email
     * @uses WPCF7_Validation::invalidate
     *
     * @param WPCF7_Validation $result The validation helper class from wpcf7.
     * @param array            $tag    The array of values making up our emailplus tag
     *
     */
    public function validate(WPCF7_Validation $result, $tag)
    {
        $tag = new WPCF7_Shortcode(
            array(
                'basename' => 'emailplus',
                'name' => $this->tag_name,
                'raw_values' => $this->emails
            )
        );

        // Check each value of the form field.
        // Emails must be validated individually.
        foreach($tag->raw_values as $email) {
            if (false === wpcf7_is_email($email)) {
                $result->invalidate($tag, wpcf7_get_message('invalid_email'));
            }
        }

        return $result;
    }

    /**
     * For completeness' sake, manually assign the value to the emailplus fieldtype.
     *
     * Wpcf7 doesn't know how to treat our fieldtype's value by default.
     *
     * As a side effect, this sets the email addresses that are up for delivery.
     *
     * @param array $scanned_tag The tag that wpcf7 is scanning through, and processing.
     *
     * @return $array;
     */
    public function assign_values_to_field($scanned_tag)
    {
        if ($scanned_tag['type'] !== 'emailplus') {
            return $scanned_tag;
        }

        $this->tag_name = $scanned_tag['name'];

        if (key_exists($scanned_tag['name'], $_POST)) {
            // Stores the emails on a class property for use later.
            $this->emails = $_POST[$scanned_tag['name']];
            $scanned_tag['raw_values'] = $this->emails;
            $scanned_tag['values']     = $this->emails;
        }

        return $scanned_tag;
    }

    /**
     * Performs a substitution on the emailplus field's fieldname, on a per-value basis.
     *
     * Ex. in two parts
     *  1 - The shortcode [emailsplus emails] renders into <input type="email" name="emails[]" value="" >
     *      Which the user clones and submits, processing into something like
     *      ['[email protected]', '[email protected]'].
     *  2 - The user has set [emails] as the recipient in the "mail" panel for the form.
     *
     * Because wpcf7 isn't aware of how to process a field w/ multiple values when emailing,
     * we loop over the values of [emails], replace the tag, and invoke WPCF7_Mail::send()
     * for each value.
     *
     * @param WPCF7_ContactForm $form The contact form object.
     *
     * @uses WPCF7_Mail::send
     *
     * @return void
     */
    public function send_email(WPCF7_ContactForm $form)
    {
        $placeholder = '['.$this->tag_name.']';

        if (false === strpos($form->prop('mail'), $placeholder)) {
            return;
        }

        foreach ($this->emails as $email) {
            $template = $form->prop('mail');
            $template['recipient'] = str_replace($placeholder, $email, $template['recipient']);
            $template['body']      = str_replace($placeholder, $email, $template['body']);
            WPCF7_Mail::send($template);
        }

        // Tell cf7 to skip over the default sending behaviour in WPCF7_Submission->mail()
        $form->skip_mail = true;
    }

    /**
     * Adds our js that will clone the emailplus field.
     *
     * Could be optimized with a conditional that checks if there is a form with the [emailplus]
     * fieldtype somewhere on the page
     *
     * @return void
     */
    public function emailplus_scripts()
    {
        wp_enqueue_script(
            'cf7-duplication',
            get_template_directory_uri() . '/js/cf7-duplication.js',
            array('jquery'),
            '20161006',
            true
        );
    }
}

$wpcf7DuplicableEmail = new WPCF7_Duplicable_Email();

And, the .js file that handles the cloning. It should live in /path/to/your/theme/js/cf7-duplication.js'.

(function($) {
  $(document).ready(function() {
    var addEmailField = function(inputRow, container) {
      inputRow.find('input[type=email]').val('');

      var removeButton = $('<a href="#">&times;</a>')
        .click(function(e) {
          e.preventDefault();
          inputRow.remove();
        });

      inputRow.append(removeButton);
      inputRow.insertAfter(container.find('.emailplus-wrapper').last());
    }

    $.each($('[data-wpcf7-duplicable-email]'), function(i, el) {
      var container = $(el);
      var inputRow  = container.find('.emailplus-wrapper');
      var addButton = $('<a href="#">Add another email</a>');

      addButton.click(function(e) {
        e.preventDefault();
        var newEmailField = inputRow.clone();
        addEmailField(newEmailField, container);
      });

      container.append(addButton);
    });
  });
})(jQuery);

Last, but not least, if you'd like the form to fadeout when it's valid and the emails have gone out, add this to the "additional settings" panel.

on_sent_ok: "jQuery('.cf7-duplicable-recipients').fadeOut();"

This approach is best for CF7's AJAX submissions. If you want to extend it to handle vanilla POST requests, you could update the shortcode handler to render multiple <input> fields where you need to preserve the value attr on invalid submissions.

like image 83
Cameron Hurd Avatar answered Oct 28 '22 17:10

Cameron Hurd