# LearnDash Course List Widget

> LearnDash-integrated course catalogue widget. Lists courses from the LearnDash `sfwd-courses` post type with 4 layout templates and filtering by category / status / enrolled.

**Class file:** `includes/Elements/LD_Course_List.php` (3,465 lines)
**Slug:** `learn-dash-course-list` (widget id `eael-learn-dash-course-list`)
**Public docs:** <https://essential-addons.com/elementor/docs/learndash-course-list/>
**Pro-shared:** Pro-only. **External dependency: LearnDash plugin** (constant `LEARNDASH_VERSION`).

## Overview

Renders a list of LearnDash courses with one of four templates (default, layout__1, layout__2, layout__3). Pulls course data from LearnDash's `sfwd-courses` custom post type and integrates with LearnDash's enrollment / progress / pricing APIs. The widget hard-gates on `LEARNDASH_VERSION` being defined — if LearnDash is not installed, the editor shows a warning section instead of normal controls.

## Pro vs Lite

Pro-only. Lite has no LMS integration.

## File Map

| File | Role |
| --- | --- |
| `includes/Elements/LD_Course_List.php` | Widget class — controls + render (3,465 lines) |
| `includes/templates/ld-courses/default.php` | Default layout template |
| `includes/templates/ld-courses/layout__1.php` | Alternative layout 1 |
| `includes/templates/ld-courses/layout__2.php` | Alternative layout 2 |
| `includes/templates/ld-courses/layout__3.php` | Alternative layout 3 |
| `src/css/view/learn-dash-course-list.scss` → `assets/front-end/css/view/learn-dash-course-list.min.css` | Card styling |
| `src/js/view/learn-dash-course-list.js` → `assets/front-end/js/view/learn-dash-course-list.min.js` | Filter / load-more interactions |
| `config.php` entry `'learn-dash-course-list'` | Self CSS + self JS |

## Architecture

- **LearnDash hard gate** — `register_controls()` (line 92) checks `defined('LEARNDASH_VERSION')`. If false, only renders a "Warning!" section that says LearnDash is required. The widget loads but offers no functionality. This is the canonical "Pro widget that depends on a third-party plugin" pattern.
- **Template-based rendering** — instead of all 4 layouts being inline in `render()`, Pro `require`s the matching template file from `includes/templates/ld-courses/` (line 3309). Layout selection is a `switch` on `eael_learndash_layout`.
- **Optimistic edit-mode reload** — `is_reload_preview_required()` returns true (line 85). Forces Elementor to reload the preview iframe on every save — needed because course data depends on the LMS state that the live preview can't easily mock.
- **3,465 lines is the largest Pro widget** — most lines are control definitions (ribbon styles, 4 layout-specific style sections, filter UI, etc.). The actual render dispatcher is small.
- **Direct LearnDash API integration** — uses LearnDash functions (`learndash_*`) for course meta, enrollment status, pricing. Tied to LearnDash's API surface; LearnDash version bumps may break this widget.
- **No external HTTP** — all data is from local DB via LearnDash's WP_Query helpers.

## Render Output (default template — high level)

```html
<div class="eael-learndash-wrapper">
  <div class="eael-learn-dash-course"
       data-course-id="{ID}">
    <div class="eael-learn-dash-course-inner">
      [?] <div class="eael-learn-dash-course-ribbon">Featured</div>
      <div class="eael-learn-dash-course-header">
        <img src="{thumbnail}" />
        <h3>{course_title}</h3>
      </div>
      <div class="eael-learn-deash-course-content-card">
        <p>{description}</p>
        <div class="meta">
          [?] <span>Price: {price}</span>
          [?] <span>Lessons: {lesson_count}</span>
          [?] <span>Students: {enrolled_count}</span>
        </div>
        <div class="layout-button-wrap">
          [?] <a href="{course_link}">Enroll Now</a>
        </div>
      </div>
    </div>
  </div>
</div>
```

Note: template files contain typos that persist as published HTML (`eael-learn-deash-course-content-card` — "deash" not "dash"). User content depends on this; cannot rename without breaking saved pages and CSS.

## Controls Reference

| Control id | Tab → Section | Type | Purpose |
| --- | --- | --- | --- |
| `eael_learndash_layout` | Content → Layout | SELECT | `default` / `layout__1` / `layout__2` / `layout__3` |
| `eael_learndash_ribbon_show` | Content → Ribbon | SWITCHER | Show "featured" / "new" ribbon |
| Course filter — category / status / enrolled | Content → Query | SELECT / SELECT2 | LearnDash query args |
| Items per page | Content → Query | NUMBER | Course count |
| Pagination type | Content → Query | SELECT | None / numbered / load-more |
| Ribbon style controls | Style → Ribbon | various | Color / position / shape |
| Per-layout style sections (4 separate) | Style → Layout X | various | Each layout has its own typography / color / spacing |

## Conditional Dependencies

```text
eael_learndash_ribbon_show = 'true'  (note: string 'true', not boolean)
  └── shows ribbon style controls

eael_learndash_layout = 'layout__1'
  └── shows layout__1-specific style section
  └── hides default / layout__2 / layout__3 style sections

LEARNDASH_VERSION not defined
  └── only the warning section is registered
  └── all other controls are absent
```

**`'true'` (string) is the convention** for `eael_learndash_ribbon_show`, not the EA-standard `'yes'`. Predates the SWITCHER convention; preserved for user content.

## JavaScript Lifecycle

`src/js/view/learn-dash-course-list.js`:

```js
var LDCourseListHandler = function( $scope, $ ) {
    var $wrap = $scope.find( '.eael-learndash-wrapper' );
    if ( ! $wrap.length ) return;

    // Filter handlers
    $wrap.on( 'click', '.eael-ld-filter', function() { /* AJAX filter */ } );

    // Load-more handler
    $wrap.on( 'click', '.eael-ld-load-more', function() { /* AJAX next page */ } );
};
```

Standard `eael.elementStatusCheck` guard.

## Hooks & Filters

### Elementor hooks consumed

Standard widget render.

### Pro / EA hooks emitted

None visible in the widget.

### LearnDash dependencies

- Reads `LEARNDASH_VERSION` constant
- Uses LearnDash functions (`learndash_get_courses_count`, `learndash_get_user_course_status`, course-meta accessors) — verify exact calls when LearnDash refactors

## Common Issues

| Symptom | Likely cause | Diagnose | Fix |
| --- | --- | --- | --- |
| Warning section instead of controls | LearnDash not installed / activated | Check `LEARNDASH_VERSION` constant exists in PHP | Install + activate LearnDash |
| Courses don't appear | LearnDash post type `sfwd-courses` not present | Check post type query | Verify LearnDash version matches Pro's expectations |
| Pricing wrong | LearnDash pricing meta key renamed | Inspect course post meta | Check LearnDash changelog; update accessor |
| Ribbon CSS not applying | Typo class `eael-learn-deash-course-content-card` (template) vs CSS selector | Inspect HTML vs CSS | Cannot rename the typo (user content). Update CSS to match the typo. |
| Filter doesn't update | AJAX handler missing / nonce mismatch | Network: check filter AJAX | Verify Lite's AJAX handler is hit; nonce is `essential-addons-elementor` |

## Known Limitations

- **LearnDash version compatibility** — Pro depends on specific LearnDash API surface. Major LearnDash refactors break Pro
- **`eael-learn-deash-course-content-card` typo** — frozen in user content; cannot rename
- **Template files are PHP, not data** — adding a 5th layout requires a new PHP file + a control option. No JSON-driven template system
- **3,465 lines is large** — refactor target: extract per-layout control registration into separate methods
- **`'true'` string vs `'yes'` SWITCHER inconsistency** — predates current convention; preserved
- **No fallback for missing LearnDash** beyond the warning section — widget loads but is non-functional
- **Editor reload required on every save** (`is_reload_preview_required = true`) — sacrifices editor speed for live-data accuracy

## Cross-References

- Architecture: [`docs/architecture/pro-lite-bridge.md`](../architecture/pro-lite-bridge.md) — Pro widget depending on third-party plugin
- Shared patterns: [`_patterns.md`](_patterns.md)
- Rule: [`.claude/rules/widget-development.md`](../../.claude/rules/widget-development.md)
