namespace WPForms\Frontend;
* Form front-end rendering.
private const FIELD_FORMAT = 'wpforms-%d-field_%s';
* Render engine setting value.
protected $render_engine;
* Render engine class instance.
* CSS vars class instance.
* Store form data to be referenced later.
* Store information for multipage forms.
* False for forms that do not contain pages, otherwise an array that contains the number of total pages
* and page counter used when displaying pagebreak fields.
* If the active form, confirmation should auto-scroll.
public $confirmation_message_scroll = false;
* Whether ChoiceJS library has already been enqueued on the front end.
* This lib is used in different fields that can enqueue it separately,
* and we use this property to avoid config duplication.
public $is_choicesjs_enqueued = false;
* Rendered field IDs array.
private $rendered_fields;
public function init(): void {
$this->amp_obj = wpforms()->obj( 'amp' );
$this->css_vars_obj = wpforms()->obj( 'css_vars' );
$this->init_render_engine( wpforms_get_render_engine() );
add_shortcode( 'wpforms', [ $this, 'shortcode' ] );
private function hooks(): void {
add_action( 'init', [ $this, 'init_style_settings' ] );
add_action( 'wpforms_frontend_output_success', [ $this, 'confirmation' ], 10, 3 );
add_action( 'wpforms_frontend_output', [ $this, 'head' ], 5, 5 );
add_action( 'wpforms_frontend_output', [ $this, 'fields' ], 10, 5 );
add_action( 'wpforms_display_field_before', [ $this, 'field_container_open' ], 5, 2 );
add_action( 'wpforms_display_field_before', [ $this, 'field_fieldset_open' ], 10, 2 );
add_action( 'wpforms_display_field_before', [ $this, 'field_label' ], 15, 2 );
add_action( 'wpforms_display_field_before', [ $this, 'field_description' ], 20, 2 );
add_action( 'wpforms_display_field_after', [ $this, 'field_error' ], 3, 2 );
add_action( 'wpforms_display_field_after', [ $this, 'field_description' ], 5, 2 );
add_action( 'wpforms_display_field_after', [ $this, 'field_fieldset_close' ], 10, 2 );
add_action( 'wpforms_display_field_after', [ $this, 'field_container_close' ], 15, 2 );
add_action( 'wpforms_frontend_output', [ $this, 'foot' ], 25, 5 );
add_action( 'wp_enqueue_scripts', [ $this, 'assets_header' ] );
add_action( 'wp_footer', [ $this, 'assets_footer' ], 15 );
add_action( 'wp_footer', [ $this, 'missing_assets_error_js' ], 20 );
add_action( 'wp_footer', [ $this, 'footer_end' ], 99 );
* Initialize render engine.
* @param string $engine Render engine slug, `classic` or `modern`.
public function init_render_engine( string $engine ): void {
$this->render_engine = $engine;
$this->render_obj = wpforms()->obj( "frontend_{$this->render_engine}" );
$this->render_obj->hooks();
* Initialize form styling settings.
public function init_style_settings(): void {
// Skip if modern markup settings are already set.
$modern_markup_is_set = wpforms_setting( 'modern-markup-is-set' );
if ( $modern_markup_is_set ) {
$settings = (array) get_option( 'wpforms_settings', [] );
$count_posts = wp_count_posts( 'wpforms' );
// Set the Modern markup checkbox to the checked state for all new users.
$settings['modern-markup'] = ( $count_posts->publish + $count_posts->trash ) === 0 ? '1' : '0';
$settings['modern-markup-is-set'] = true;
// Hide the Modern markup checkbox for all new users.
if ( $settings['modern-markup'] ) {
$settings['modern-markup-hide-setting'] = true;
update_option( 'wpforms_settings', $settings );
* Primary function to render a form on the frontend.
* @param int $id Form ID.
* @param bool $title Whether to display form title.
* @param bool $description Whether to display form description.
public function output( $id, $title = false, $description = false ): void {
// Grab the form data, if not found, then we bail.
$form = $this->get_form( $id );
if ( $form === null || empty( $form->post_content ) ) {
// We should display only the published form.
if ( ! empty( $form->post_status ) && $form->post_status !== 'publish' ) {
$form_data = wpforms_decode( $form->post_content );
// Skip if the form data is empty.
if ( empty( $form_data ) ) {
* Filter frontend form data.
* @param array $form_data Form data.
$form_data = (array) apply_filters( 'wpforms_frontend_form_data', $form_data );
$form_id = absint( $form->ID );
$this->action = esc_url_raw( remove_query_arg( 'wpforms' ) );
$errors = empty( wpforms()->obj( 'process' )->errors[ $form_id ] ) ? [] : wpforms()->obj( 'process' )->errors[ $form_id ];
$title = filter_var( $title, FILTER_VALIDATE_BOOLEAN );
$description = filter_var( $description, FILTER_VALIDATE_BOOLEAN );
// Pass the current form data to the render object.
$this->render_obj->form_data = $form_data;
if ( $this->stop_output( $form, $form_data ) ) {
// All checks have passed, so calculate multipage details for the form.
$this->pages = $this->get_pages( $form_data );
* Allow modifying a form action attribute.
* @param string $action Action attribute.
* @param array $form_data Form data and settings.
* @param null $deprecated A deprecated argument.
$this->action = apply_filters( 'wpforms_frontend_form_action', $this->action, $form_data, null );
$form_classes = [ 'wpforms-validate', 'wpforms-form' ];
if ( ! empty( $form_data['settings']['ajax_submit'] ) && ! $this->amp_obj->is_amp() ) {
$form_classes[] = 'wpforms-ajax-form';
'id' => sprintf( 'wpforms-form-%d', absint( $form_id ) ),
'class' => $form_classes,
'formid' => absint( $form_id ),
'enctype' => 'multipart/form-data',
'action' => esc_url( $this->action ),
* Allow modifying form attributes.
* @param array $form_atts Form attributes.
* @param array $form_data Form data and settings.
$form_atts = apply_filters( 'wpforms_frontend_form_atts', $form_atts, $form_data );
$this->form_container_open( $form_data, $form );
// Reset rendered fields array.
$this->rendered_fields = [];
* Fires before form output.
* @param array $form_data Form data.
* @param WP_Post $form Form.
do_action( 'wpforms_frontend_output_form_before', $form_data, $form );
echo '<form ' . wpforms_html_attributes( $form_atts['id'], $form_atts['class'], $form_atts['data'], $form_atts['atts'] ) . '>';
* Fires before closing the form.
* @param array $form_data Form data.
* @param null $deprecated Null.
* @param bool $title Whether to display form title.
* @param bool $description Whether to display form description.
* @param array $errors Form processing errors.
do_action( 'wpforms_frontend_output', $form_data, null, $title, $description, $errors );
* Allow adding content after a form.
* @param array $form_data Form data and settings.
* @param WP_Post $form Form post type.
do_action( 'wpforms_frontend_output_form_after', $form_data, $form );
$this->form_container_close( $form_data, $form );
// Add a form to class property that tracks all forms in a page.
$this->forms[ $form_id ] = $form_data;
// Optional debug information if WPFORMS_DEBUG is defined.
wpforms_debug_data( $_POST ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
* Fires after frontend output.
* @param array $form_data Form data and settings.
* @param WP_Post $form Form post type.
do_action( 'wpforms_frontend_output_after', $form_data, $form );
* @param int|string|false $id Form id.
* @return array|WP_Post|null
* @noinspection NullPointerExceptionInspection
private function get_form( $id ) {
// Grab the form data, if not found, then we bail.
$form = wpforms()->obj( 'form' )->get( (int) $id );
// We should display only the published form.
if ( ! empty( $form->post_status ) && $form->post_status !== 'publish' ) {
* Check whether we should stop the output.
* @param WP_Post $form Form.
* @param array $form_data Form data.
private function stop_output( WP_Post $form, array $form_data ): bool {
$form_id = absint( $form->ID );
* Check before output the form on the frontend.
* @param bool $form_is_empty Is the form empty?
* @param array $form_data Form data.
$form_is_empty = apply_filters( 'wpforms_frontend_output_form_is_empty', empty( $form_data['fields'] ), $form_data );
// If the form does not contain any fields - do not proceed.
$this->render_obj->form_is_empty();
// We need to stop output processing in case we are on the AMP page.
if ( $this->amp_obj->stop_output( $form_data ) ) {
// Add url query var wpforms_form_id to track post_max_size overflows.
if ( in_array( 'file-upload', wp_list_pluck( $form_data['fields'], 'type' ), true ) ) {
$this->action = add_query_arg( 'wpforms_form_id', $form_id, $this->action );
* Fires before form data output.
* @param array $form_data Form data.
* @param WP_Post $form Form.
do_action( 'wpforms_frontend_output_before', $form_data, $form );
if ( $this->output_success( $form ) ) {
* Allow filter to return early if some condition is not met.
* @param bool $load Load frontend flag.
* @param array $form_data Form data.
* @param null $deprecated Deprecated.
if ( ! apply_filters( 'wpforms_frontend_load', true, $form_data, null ) ) {
$this->form_container_open( $form_data, $form );
* Fires when the frontend is not loaded.
* @param array $form_data Form data.
* @param WP_Post $form Form.
do_action( 'wpforms_frontend_not_loaded', $form_data, $form );
$this->form_container_close( $form_data, $form );
* @param array $form_data Form data.
* @noinspection PhpTernaryExpressionCanBeReducedToShortVersionInspection
* @noinspection ElvisOperatorCanBeUsedInspection
private function get_pages( array $form_data ) {
$pages = wpforms_get_pagebreak_details( $form_data );
return $pages ? $pages : false;