Creating Custom Tabs

Get Started

Creating Custom Tabs

Instructor content tabs use a Template Method pattern introduced in version 7.5.0. The LDDashboardTab_Base abstract class defines the rendering algorithm; subclasses implement the content-specific steps.


LDDashboardTab_Base

Located at includes/tabs/class-ld-dashboard-tab-base.php.

All custom tabs must extend this class. The render() method is declared final — you cannot override it. Instead, implement the abstract methods that render() calls internally.

Required Properties

protected $post_type;     // WordPress post type for this tab (e.g., 'sfwd-lessons')
protected $tab_name;      // Tab identifier string

Abstract Methods (required)

MethodReturnDescription
get_id(): stringstringUnique tab ID (e.g., gradebook).
getlearndashlabel_key(): stringstringLearnDash custom label key (e.g., lesson, quiz). Used by LearnDashCustomLabel::get_label().
getaccessibleids( array $params ): arrayarrayReturn an array of post IDs the current user can see. Return empty array for admins (no restriction).
getfilterargs( array $args, array $params ): arrayarrayModify the WP_Query args array with tab-specific filters (status, course, etc.).
renderitem( WPPost $post, array $params ): stringstringRender a single list item’s HTML. Must return a string.

render() — Template Method

The final public function render( array $params ): array method executes the tab rendering algorithm:

render( $params )
  ├── get_accessible_ids( $params )   → restrict posts to user's scope
  ├── build_query_args( $params, $ids )  → WP_Query base args
  ├── get_filter_args( $args, $params )  → tab-specific filters
  ├── apply_filters('ld_dashboard_{id}_content_args', $args)  → developer hook
  ├── new WP_Query( $args )           → execute query
  ├── render_items( $query, $params ) → calls render_item() per post
  ├── get_stats( $ids )               → count totals by status
  └── build_response( $content, $query, $page, $stats )
      → apply_filters('ld_dashboard_{id}_content', $content)
      → apply_filters('ld_dashboard_{id}_content_data', $response)

The response array contains:

array(
    'content'     => string,  // Rendered HTML
    'next'        => bool,    // Has next page
    'prev'        => bool,    // Has previous page
    'first'       => bool,    // Is not first page
    'last'        => bool,    // Is not last page
    'maxpages'    => int,
    'currentpage' => int,
    'totalitems'  => int,
    'stats'       => array(   // Counts by post status
        'total'     => int,
        'published' => int,
        'draft'     => int,
        'pending'   => int,
    ),
)

Access Control Helpers

The base class provides access control methods you can call inside render_item():

// Get full access status for a post
$access = $this->get_access_status( $post );
// Returns:
// array(
//     'can_edit'   => bool,
//     'can_delete' => bool,
//     'badge'      => 'your-content' | 'shared' | 'view-only',
//     'badge_text' => string,
// )

// Individual checks
$can_edit   = $this->user_can_edit( $post );
$can_delete = $this->user_can_delete( $post );

Badge values:

  • your-content (green) — User is the post author.
  • shared (blue) — User can edit via course co-instructor relationship.
  • view-only (gray) — User can see but not edit.

Edit permission logic:

  1. Administrator → always true.
  2. Group leader with admin capability → always true.
  3. Post author → always true.
  4. Co-instructor on the associated course → true.

Built-in Helper Methods

MethodDescription
buildactionlinks( $post, $nonce, $access )Returns HTML for view/edit/delete action links.
getelementoredit_link( $post )Returns an Elementor edit link if the post type supports it.
renderemptystate( $search_term )Returns empty-state HTML with a “Clear Filters” button.
getstatuslabel( $status )Returns a translated status label for publish, draft, pending.
getassociatedcourseid( $postid )Retrieves the course ID associated with a post via meta or learndashgetcourse_id().
getnonceaction()Returns {id}-nonce as the nonce action.
geteditaction()Returns edit-{id}.
getdeleteaction()Returns delete-{id}.
gettabparam()Returns my-{id}s.
getpostparam()Returns ld-{id}.

LDDashboardTab_Registry

Located at includes/tabs/class-ld-dashboard-tab-registry.php.

The singleton registry manages tab instances and dispatches AJAX requests.

Registering Custom Tabs

// Via filter (preferred — runs before any tab is instantiated)
add_filter( 'ld_dashboard_registered_tabs', function( $tabs ) {
    $tabs['gradebook'] = 'My_Gradebook_Tab';
    return $tabs;
} );

// Via registry method (must run before AJAX dispatch)
add_action( 'init', function() {
    LD_Dashboard_Tab_Registry::instance()->register( 'gradebook', 'My_Gradebook_Tab' );
} );

AJAX Dispatch

The registry handles the lddashboardgetinstructortabcontent and lddashboardtabcontent_filter AJAX actions. Your tab is automatically included in AJAX dispatch when registered.

// The registry dispatches to your tab via:
$response = LD_Dashboard_Tab_Registry::instance()->render( 'gradebook', $params );

AJAX params passed to render():

ParamTypeDescription
course_idintFilter by course
lesson_idintFilter by lesson
topic_idintFilter by topic
quiz_idintFilter by quiz
pageintPagination page
searchstringSearch term
grade_typestringGrade type filter

Complete Example: Gradebook Tab

post_type = 'sfwd-quiz';
        $this->tab_name  = 'gradebook';
    }

    /**
     * Get the unique tab ID.
     *
     * @since 1.0.0
     * @return string
     */
    public function get_id(): string {
        return 'gradebook';
    }

    /**
     * Get the LearnDash custom label key.
     *
     * @since 1.0.0
     * @return string
     */
    protected function get_learndash_label_key(): string {
        return 'quiz';
    }

    /**
     * Get post IDs accessible to the current user.
     *
     * @since 1.0.0
     * @param array $params Request parameters.
     * @return array Post IDs the user can access.
     */
    protected function get_accessible_ids( array $params ): array {
        $user_id = $this->current_user_id;

        // Admins see all quizzes — return empty to skip post__in.
        if ( learndash_is_admin_user( $user_id ) || LD_Dashboard_Helper::group_leader_has_admin_cap() ) {
            return array();
        }

        // Instructors see quizzes attached to their courses.
        $course_ids = LD_Dashboard_Course_Helper::get_instructor_courses( $user_id );

        if ( empty( $course_ids ) ) {
            return array( 0 ); // Force empty result.
        }

        $quiz_ids = array();
        foreach ( $course_ids as $course_id ) {
            $quizzes = learndash_get_course_quiz_list( $course_id );
            foreach ( $quizzes as $quiz ) {
                $quiz_ids[] = $quiz['post']->ID;
            }
        }

        return array_unique( $quiz_ids ) ?: array( 0 );
    }

    /**
     * Apply tab-specific filter args.
     *
     * @since 1.0.0
     * @param array $args   Base WP_Query args.
     * @param array $params Request parameters.
     * @return array Modified args.
     */
    protected function get_filter_args( array $args, array $params ): array {
        // Filter by course.
        if ( ! empty( $params['course_id'] ) ) {
            $course_quizzes = learndash_get_course_quiz_list( absint( $params['course_id'] ) );
            $course_quiz_ids = wp_list_pluck(
                array_column( $course_quizzes, 'post' ),
                'ID'
            );

            // Intersect with already-restricted IDs.
            if ( ! empty( $args['post__in'] ) && ! empty( $course_quiz_ids ) ) {
                $args['post__in'] = array_intersect( $args['post__in'], $course_quiz_ids );
            } elseif ( ! empty( $course_quiz_ids ) ) {
                $args['post__in'] = $course_quiz_ids;
            }
        }

        // Filter by status.
        if ( ! empty( $params['grade_type'] ) && 'all' !== $params['grade_type'] ) {
            $args['post_status'] = sanitize_key( $params['grade_type'] );
        }

        return $args;
    }

    /**
     * Render a single quiz item.
     *
     * @since 1.0.0
     * @param WP_Post $post   Quiz post object.
     * @param array   $params Request parameters.
     * @return string HTML content.
     */
    protected function render_item( WP_Post $post, array $params ): string {
        $access   = $this->get_access_status( $post );
        $nonce    = wp_create_nonce( $this->get_nonce_action() );
        $attempts = learndash_get_user_quiz_attempts_by_quiz( $this->current_user_id, $post->ID );

        ob_start();
        ?>
        
ID ); ?>">
">

post_title ); ?>

post_status ); ?>"> get_status_label( $post->post_status ) ); ?>
build_action_links( $post, $nonce, $access ) ); ?>

Using the Tab via Registry

// Render programmatically.
$response = LD_Dashboard_Tab_Registry::render_tab( 'gradebook', array(
    'course_id' => 123,
    'page'      => 1,
    'search'    => '',
) );

// $response['content'] contains the HTML.
// $response['totalitems'] contains the total count.

Extending Built-in Tabs

You can replace a built-in tab by re-registering it with your own class:

add_filter( 'ld_dashboard_registered_tabs', function( $tabs ) {
    // Replace the quiz tab with a custom implementation.
    $tabs['quiz'] = 'My_Enhanced_Quiz_Tab';
    return $tabs;
} );

MyEnhancedQuizTab can extend the original LDDashboardTabQuiz to inherit its logic:

class My_Enhanced_Quiz_Tab extends LD_Dashboard_Tab_Quiz {

    protected function render_item( WP_Post $post, array $params ): string {
        // Add custom fields before the default render.
        $extra_html = '
...
'; return $extra_html . parent::render_item( $post, $params ); } }

Built-in Tab IDs

IDClassPost Type
lessonLDDashboardTab_Lessonsfwd-lessons
topicLDDashboardTab_Topicsfwd-topic
quizLDDashboardTab_Quizsfwd-quiz
questionLDDashboardTab_Questionsfwd-question
assignmentLDDashboardTab_Assignmentsfwd-assignment
announcementsLDDashboardTab_Announcementsfwd-announcements
certificateLDDashboardTab_Certificatesfwd-certificates
Last updated: March 4, 2026