Concaténation VS Template

La liste des contenus disponibles dans l'extension TODD est injectée dans une table dont l'ossature est présente dans todd_dashboard_contents.php :

<div class="table-responsive">
    <table class="table table-hover table-striped table-bordered w-100" id="todd_content_list">
        <thead>
            <tr class="bg-light">
                <th class="searchable">Titre</th>
                <th class="searchable">Module</th>
                <th class="searchable">Auteur</th>
                <th class="searchable">Date</th>
                <th class="searchable">Statut</th>
                <th class="text-center">Actions</th>
            </tr>
        </thead>
        <tbody>
            <!-- Loaded via JS -->
        </tbody>
    </table>
</div>

Dashboard : ancien tableau des contenus

Jusque là, nous avions une fonction qui construisait les lignes du tableau en faisant de la concaténation de chaînes de caractères.

function renderContentTable(items, canModerate, canEdit) {
    var html = '';
    if (items.length === 0) {
        html = '<tr><td colspan="6" class="text-center text-muted py-4">Aucun contenu trouvé</td></tr>';
    } else {
        $.each(items, function (i, item) {
            var badge = '';
            switch (item.status) {
                case 'draft': badge = '<span class="badge bg-secondary">Brouillon</span>'; break;
                case 'pending': badge = '<span class="badge bg-warning text-dark">En attente</span>'; break;
                case 'approved': badge = '<span class="badge bg-success">Validé</span>'; break;
                case 'rejected': badge = '<span class="badge bg-danger">Rejeté</span>'; break;
                case 'archived': badge = '<span class="badge bg-light text-muted border">Archivé</span>'; break;
            }

            var publicLink = BASE_URL + 'todd_view/' + item.id;

            html += '<tr>';
            html += '<td><div class="fw-semibold">' + item.title + '</div></td>';
            html += '<td><span class="text-muted">' + item.module_code + '</span></td>';
            html += '<td>' + (item.user_firstname || '') + ' ' + (item.user_lastname || '') + '</td>';
            html += '<td>' + moment(item.updated_dt).format('DD/MM/YYYY HH:mm') + '</td>';
            html += '<td>' + badge + '</td>';
            html += '<td class="text-end">';

            // Copy link button (only for approved)
            if (item.status == 'approved') {
                html += '<a href="#" class="copy-link btn btn-sm btn-outline-secondary me-1" data-link="' + publicLink + '" title="Copier le lien public">';
                html += '<svg class="icon" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-copy"/></svg>';
                html += '</a>';
            }

            // Action dropdown
            html += '<div class="btn-group">';
            html += '<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">';
            html += '<svg class="icon" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-dots-three-vertical"/></svg>';
            html += '</button>';
            html += '<div class="dropdown-menu dropdown-menu-end">';

            // Éditer
            html += '<a href="#" class="dropdown-item edit-content" data-id="' + item.id + '">';
            html += '<svg class="icon me-2" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-pencil-simple"/></svg> Éditer</a>';

            // Voir (Public) / Aperçu - only for approved, or draft if user has edit rights
            if (item.status == 'approved' || (item.status == 'draft' && canEdit)) {
                var viewLabel = (item.status == 'draft') ? 'Aperçu (Public)' : 'Voir (Public)';
                html += '<a href="' + publicLink + '" target="_blank" class="dropdown-item">';
                html += '<svg class="icon me-2" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-eye"/></svg> ' + viewLabel + '</a>';
                html += '<hr class="dropdown-divider">';
            }

            // Soumettre (draft or rejected)
            if (item.status == 'draft' || item.status == 'rejected') {
                html += '<a href="#" class="dropdown-item submit-content" data-id="' + item.id + '">';
                html += '<svg class="icon me-2" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-paper-plane-tilt"/></svg> Soumettre</a>';
                if (canModerate) {
                    html += '<a href="#" class="dropdown-item approve-content" data-id="' + item.id + '">';
                    html += '<svg class="icon me-2 text-success" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-check-circle"/></svg> Valider</a>';
                }
            }

            // Valider / Rejeter (pending)
            if (item.status == 'pending') {
                html += '<a href="#" class="dropdown-item approve-content" data-id="' + item.id + '">';
                html += '<svg class="icon me-2 text-success" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-check-circle"/></svg> Valider</a>';
                html += '<a href="#" class="dropdown-item reject-content" data-id="' + item.id + '">';
                html += '<svg class="icon me-2 text-danger" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-x-circle"/></svg> Rejeter</a>';
            }

            // Archiver (not archived)
            if (item.status != 'archived') {
                html += '<a href="#" class="dropdown-item archive-content" data-id="' + item.id + '">';
                html += '<svg class="icon me-2" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-archive"/></svg> Archiver</a>';
            }

            html += '<hr class="dropdown-divider">';
            html += '<a href="#" class="dropdown-item text-danger delete-content" data-id="' + item.id + '">';
            html += '<svg class="icon me-2" style="width:16px;height:16px;fill:currentColor;"><use href="#ph-trash"/></svg> Supprimer</a>';

            html += '</div></div>'; // dropdown-menu, btn-group
            html += '</td>';
            html += '</tr>';
        });
    }
    $('#tbl_todd_contents tbody').html(html);
}

C'est une mauvaise pratique. Le code est difficile à lire et à maintenir. Il vaut mieux utiliser un template dans lequel sont injectés dynamiquement les valeurs.

De plus, nous utilisons la librairie JavaScript Datatable bien connue pour construire dynamiquement des tables : nous y gagnons plain de fonctionnalités : tri sur les colonnes, pagination, recherche dans les lignes, etc.

if ($('#todd_content_list').length) {
    // récupération de la table dans la page par son ID
    dtToddContents = $('#todd_content_list').DataTable({
        autoWidth: false,
        pageLength: 10,
        language: typeof globaltranslation !== 'undefined' ? globaltranslation.datatables : {},
        dom: '<"datatable-scroll"t><"datatable-footer"ip>',
        stateSave: false,
        order: [[3, "desc"]], // tri par Date d2croissant
        columnDefs: [
            { targets: 'searchable', searchable: true, orderable: true },
            { targets: '_all', searchable: false, orderable: false }
        ],
        ajax: {
            // récupération des données dans la base via le gestionaire de srvices
            url: (typeof SITE_URL !== 'undefined' ? SITE_URL : '') + 'plugins/todd/scripts/todd.ws.php',
            type: 'POST',
            data: function (d) {
                d.action = 'getContents';
            },
            dataSrc: function (res) {
                if (res.status) {
                    window.toddCanModerate = res.can_moderate;
                    window.toddCanEdit = res.can_edit;
                    return res.data;
                }
                return [];
            }
        },
        // déclaration du contenu des colonnes
        columns: [
            {
                data: "title",
                render: function (data, type, row) {
                    return '<div class="fw-semibold">' + data + '</div>';
                }
            },
            {
                data: "module_code",
                render: function (data, type, row) {
                    var label = window.toddModulesMap && window.toddModulesMap[data] ? window.toddModulesMap[data] : data;
                    return '<span class="text-muted">' + label + '</span>';
                }
            },
            {
                data: null,
                render: function (data, type, row) {
                    var author = (row.user_firstname || '') + ' ' + (row.user_lastname || '');
                    return author.trim();
                }
            },
            {
                data: "updated_dt",
                render: function (data, type, row) {
                    if (type === 'sort' || type === 'type') {
                        return data; // Date brutes pour l'ordre
                    }
                    var formatted = typeof moment !== 'undefined' ? moment(data).format('DD/MM/YYYY HH:mm') : data;
                    return '<small class="text-muted">' + formatted + '</small>';
                }
            },
            {
                data: "status",
                render: function (data, type, row) {
                    var badge = '';
                    switch (data) {
                        case 'draft': badge = '<span class="badge bg-secondary">Brouillon</span>'; break;
                        case 'pending': badge = '<span class="badge bg-warning text-dark">En attente</span>'; break;
                        case 'approved': badge = '<span class="badge bg-success">Validé</span>'; break;
                        case 'rejected': badge = '<span class="badge bg-danger">Rejeté</span>'; break;
                        case 'archived': badge = '<span class="badge bg-light text-muted border">Archivé</span>'; break;
                        default: badge = '<span class="badge bg-light text-muted border">' + data + '</span>'; break;
                    }
                    // For sort, return the raw value
                    if (type === 'sort') return data;
                    return badge;
                }
            },
            {
                data: null,
                render: function (data, type, row) {
                    if (type !== 'display') return '';
                    var canEdit = window.toddCanEdit;
                    var canModerate = window.toddCanModerate;
                    var baseUrl = typeof SITE_URL !== 'undefined' ? SITE_URL : (typeof BASE_URL !== 'undefined' ? BASE_URL : '/');
                    var publicLink = baseUrl + 'todd_view/' + row.id;

                    // construction du menu déroulant pour les actions sur le contenu
                    var $tpl = $($('#tpl_todd_content_actions').html());

                    $tpl.find('.edit-content, .submit-content, .approve-content, .reject-content, .archive-content, .delete-content').attr('data-id', row.id);
                    $tpl.find('.btn-copy-public-link').attr('data-link', publicLink);
                    $tpl.find('.btn-view-public').attr('href', publicLink);

                    // Copy link button
                    if (row.status !== 'approved') {
                        $tpl.find('.btn-copy-public-link').remove();
                    }

                    // View Public
                    if (row.status == 'approved' || (row.status == 'draft' && canEdit)) {
                        if (row.status == 'draft') $tpl.find('.view-label').text('Aperçu (Public)');
                    } else {
                        $tpl.find('.btn-view-public, .divider-view').remove();
                    }

                    // Submit / Approve (from Draft/Rejected)
                    if (row.status == 'draft' || row.status == 'rejected') {
                        $tpl.find('.reject-content').remove();
                        if (!canModerate) {
                            $tpl.find('.approve-content').remove();
                        }
                    }
                    // Pending
                    else if (row.status == 'pending') {
                        $tpl.find('.submit-content').remove();
                    }
                    // Others
                    else {
                        $tpl.find('.submit-content, .approve-content, .reject-content').remove();
                    }

                    // Archive
                    if (row.status == 'archived') {
                        $tpl.find('.archive-content').remove();
                    }

                    return $('<div>').append($tpl).html();
                }
            }
        ]
    });
}

Nous avons à présent un tableau bien plus fonctionnel avec un code très robuste. Noius en avons profité pour remplacé l'ID du module par son label et simplifier le design des boutons d'action.

Dashboard : noveau tableau des contenus