Comportement Ajax et ViewableData

Dans ce tutoriel, nous allons ajouter un comportement Ajax à notre site et couvrir un acteur clé du Framework SilverStripe appelé ViewableData.

Dépôt de code à télécharger

Ce que nous allons couvrir

  • Écrire le Javascript
  • Un aperçu de ViewableData
  • Rendre un gabarit partiel
  • Ajout de quelques améliorations UX

 

Écrire le Javascript

Dans le dernier didacticiel, nous avons ajouté la pagination à notre liste de résultats de recherche. Améliorons maintenant un peu l’expérience utilisateur en ajoutant Ajax aux liens de pagination.

Avant de faire quoi que ce soit, nous devrons ajouter du JavaScript qui ajoutera cette fonctionnalité. Nous ferons cela dans notre JavaScript fourre-tout fichier, scripts.js.

public / javascript / scripts.js

 

// Pagination
if ($('.pagination').length) {
  $('.main').on('click','.pagination a', function (e) {
        e.preventDefault();
        var url = $(this).attr('href');
        $.ajax(url)
            .done(function (response) {
                $('.main').html(response);
            })
            .fail (function (xhr) {
                alert('Error: ' + xhr.responseText);
            });
    });
}

 

C’est assez spécifique à ce cas d’utilisation. Plus loin dans la piste, nous pourrions constater que nous ajoutons de nombreux événements Ajax qui ressemblent beaucoup à cela. Nous voudrons peut-être le rendre plus réutilisable à un moment donné, mais pour le moment, faisons en sorte que cela fonctionne.

Essayons ceci. Cliquez sur un lien dans la pagination et voyez si cela fonctionne.

Cela fonctionne, non? Mais nous avons encore du chemin à faire. Le contrôleur renvoie la page entière – de <html>à </html>dans notre .maindiv. Pas bon, mais c’est le résultat attendu. L’URL Ajax est simplement l’ hrefattribut, donc tout ce qui est différent serait inhabituel.

Alors que faisons-nous? Changer l’URL dans notre Javascript pour utiliser autre chose que href? Nous pourrions utiliser une URL alternative dans quelque chose comme data-ajax-url. Ce n’est pas vraiment nécessaire. Nous visons toujours à garder les choses en ordre avec des points de terminaison uniques. Idéalement, le contrôleur en sait le moins possible sur l’interface utilisateur, et la configuration d’un point de terminaison distinct pour les demandes Ajax dans ce cas irait à l’encontre de cela. Nous conserverons le même point de terminaison et nous attribuerons simplement au contrôleur la possibilité de détecter les demandes Ajax.

Détecter Ajax dans un contrôleur

Mettons à jour PropertySearchPageController.phppour détecter Ajax.

app / src / PropertySearchPageController.php

 

public function index(HTTPRequest $request)
{

    //...

    if($request->isAjax()) {
        return "Ajax response!";
    }

    return [
        'Results' => $paginatedProperties
    ];
}

 

Essayez maintenant le lien et voyez ce que nous obtenons. Vous devriez voir votre réponse Ajax personnalisée. Nous devons maintenant renvoyer un contenu partiel. Avant de faire cela, parlons un peu d’un acteur clé de SilverStripe Framework appelé ViewableData.

 

Un aperçu de ViewableData

Pour établir une base pour la prochaine section de cette leçon, nous devons en savoir plus sur le fonctionnement des ViewableDataobjets. SilverStripe\View\ViewableDataest une classe primitive de SilverStripe qui permet essentiellement à ses propriétés publiques et à ses méthodes de rendre du contenu à un modèle. Les occurrences les plus courantes d’ SilverStripe\View\ViewableDataobjets se trouvent dans les SilverStripe\ORM\DataObjectinstances avec lesquelles nous avons travaillé exclusivement sur des modèles. Mais les modèles sont capables de rendre beaucoup plus que le contenu de la base de données. Vous devez juste aller plus haut dans la chaîne d’héritage, au-dessus DataObjectde ViewableData, ou dans une sous-classe de celle-ci.

Regardons un exemple simple de ViewableData.

 

use SilverStripe\View\ViewableData;

class Address extends ViewableData
{

    public $Street = '123 Main Street';

    public $City = 'Compton';

    public $Zip = '90210';

    public $Country = 'US';

    public function Country()
    {
        return MyGeoLibrary::get_country_name($this->Country);
    }

    public function getFullAddress()
    {
        return sprintf(
            '%s<br>%s %s<br>%s'
            $this->Street,
            $this->City,
            $this->Zip,
            $this->Country() 
        );
    }
}

 

Créons maintenant un modèle pour rendre notre Addressobjet.

AddressTemplate.ss

 

<p>I live on $Street in $City.</p>
<p>My full address is $FullAddress.</p>

 

Comme vous pouvez le constater, nous rendons les données en utilisant une combinaison de méthodes et de propriétés. ViewableDataa une manière très spécifique de résoudre les variables de modèle sur l’objet:

  • Vérifier s’il existe une méthode publique sur l’objet appelée [NomVariable]
  • Sinon, vérifiez si une méthode appelée “get [NomVariable]” existe
  • Sinon, vérifiez s’il existe une propriété publique nommée [NomVariable]
  • Sinon, appelez “getField ([NomVariable])”

getField()est une méthode de secours. Pour la ViewableDataclasse de base , il retourne simplement $this->$VariableName. L’idée est que les sous-classes peuvent invoquer leurs propres gestionnaires pour cela. Par exemple, dans DataObjectgetField()regarde le $dbtableau.

Tous les ViewableDataobjets savent comment se rendre sur des modèles. Pour ce faire, il suffit d’appeler renderWith($templateName)sur l’objet, et les variables de modèle seront étendues à cet objet.

 

$myViewableData = Address::create();
echo $myViewableData->renderWith('AddressTemplate');

 

Une autre caractéristique vraiment utile ViewableDataest que l’objet lui-même peut être appelé sur un modèle et rendu lui-même. Si nous devions simplement appeler $MyAddressObjectun modèle, SilverStripe tenterait d’appeler une méthode appelée forTemplate()sur l’objet pour le restituer sous forme de chaîne. Dans notre exemple d’objet adresse, cela pourrait ressembler à ceci:

 

class Address extends ViewableData
{

    //...   

    public function forTemplate()
    {
        return $this->getFullAddress();
    }
}

La Imageclasse de SilverStripe en est un excellent exemple . Lorsque vous appelez $MyImageun modèle, il appelle sa forTemplate()méthode, qui renvoie une chaîne HTML représentant une <img />balise avec tous les attributs et toutes les valeurs appropriés.

Rendre un gabarit partiel

Alors maintenant que nous avons une bonne compréhension de ViewableData, jouons avec certaines de ses fonctionnalités. Pour le moment, nous renvoyons simplement une chaîne au modèle pour notre réponse Ajax. Retournons plutôt un modèle partiel.

L’utilisation de includes dans votre modèle de présentation est au centre des réponses Ajax. Prenons tout dans la .maindiv et exportons-le dans un include appelé PropertySearchResults.

app / templates / SilverStripe / Lessons / Includes / PropertySearchResults.ss

 

<!-- BEGIN MAIN CONTENT -->
<div class="main col-sm-8">
    <% include SilverStripe/Lessons/PropertySearchResults %>                
</div>  
<!-- END MAIN CONTENT -->

 

Notez que la Includes/partie du chemin est implicite lors de l’appel <% include %>.

Rechargez la page avec ?flushpour obtenir le nouveau modèle.

Maintenant, renvoyer une réponse Ajax est trivial. Simplement rendre l’inclusion.

 

//...
class PropertySearchPageController extends PageController
{

    public function index(HTTPRequest $request)
    {

        //...

        if($request->isAjax()) {
            return $this->renderWith('SilverStripe/Lessons/Includes/PropertySearchResults');
        }

        //..
    }
}

 

Cette fois, nous ne bénéficions pas du Includes/répertoire implicite . Contrairement à la syntaxe du modèle, nous devons le spécifier lorsque nous y faisons référence dans le code du contrôleur.

Essayons ceci. Cela ne fonctionne pas tout à fait bien. Nous recevons un message “pas de résultat” lorsque nous paginons. En effet, la $Resultsvariable n’est pas exposée au modèle renderWith(). C’est juste une variable locale dans notre index()méthode. Nous avons deux choix ici:

  • Affecter $paginatedPropertiesà une propriété publique sur le contrôleur
  • Transmettez-le explicitement au modèle à l’aide de customise().

Parmi ces deux options, la dernière est beaucoup plus favorable. Il existe des cas où la première option a plus de sens, mais dans ce cas, le fait de passer explicitement la liste rend notre PropertySearchResultsmodèle plus réutilisable, et l’affectation d’une nouvelle propriété de membre polluerait inutilement notre contrôleur. Faisons cette mise à jour maintenant.

 

//...
class PropertySearchPageController extends PageController
{

    public function index(HTTPRequest $request)
    {

        //...

        if($request->isAjax()) {
            return $this->customise([
                'Results' => $paginatedResults
            ])->renderWith('SilverStripe/Lessons/Includes/PropertySearchResults');
        }

        return [
            'Results' => $paginatedProperties
        ];
    }
}

 

Nous avons maintenant répété notre tableau de données, nettoyons-le un peu.

 

//...
class PropertySearchPageController extends PageController
{

    public function index(HTTPRequest $request) {

        //...

        $data = [
            'Results' => $paginatedProperties
        ];

        if($request->isAjax()) {
            return $this->customise($data)
                         ->renderWith('SilverStripe/Lessons/Includes/PropertySearchResults');
        }

        return $data;
    }
}

 

Essayez-le maintenant. Ça a l’air beaucoup mieux!

 

Ajout de quelques améliorations UX

Cette expérience utilisateur présente deux inconvénients majeurs:

  • Le défilement reste fixé au bas des résultats, laissant à l’utilisateur peu d’indication que le contenu a été mis à jour
  • L’URL n’est pas mise à jour, donc l’actualisation de la page après la pagination ramène l’utilisateur à la première page.

Nettoyons ces deux choses maintenant, avec quelques mises à jour de notre code Javascript.

public / javascript / scripts.js

 

// Pagination
if ($('.pagination').length) {
    var paginate = function (url) {
        $.ajax(url)
            .done(function (response) {
                $('.main').html(response);
                $('html, body').animate({
                    scrollTop: $('.main').offset().top
                });
                window.history.pushState(
                    {url: url},
                    document.title,
                    url
                );    
            })
            .fail(function (xhr) {
                alert('Error: ' + xhr.responseText);
            });

    };
    $('.main').on('click','.pagination a', function (e) {
        e.preventDefault();
        var url = $(this).attr('href');
        paginate(url);
    });

    window.onpopstate = function(e) {
        if (e.state.url) {
            paginate(e.state.url);
        }
        else {
            e.preventDefault();
        }
    };        
}

 

 

Premièrement, nous allons ajouter une animate()méthode qui gérera le défilement automatique. Ensuite, nous allons pousser un état dans l’historique du navigateur en utilisant pushState.

Enfin, nous exportons l’ .ajax()appel vers une fonction afin que les liens de pagination et le bouton Précédent du navigateur puissent l’invoquer lorsque nous ajoutons un onpopstateévénement.

 

Réappliquer des plugins

Un grand nombre des plugins d’interface utilisateur que nous utilisons sont appliqués lors du chargement de documents, ce qui signifie que lorsqu’une partie du DOM est remplacée, ils ne sont pas appliqués. Notez, lors de la pagination des résultats, que la liste déroulante “Trier par” se dégrade en une entrée HTML standard. Faisons en sorte que la liste déroulante de fantaisie soit réappliquée.

Nous allons exporter le chosen()plug – in vers une fonction réutilisable et l’appeler si nécessaire.

public / javascript / scripts.js

 

(function($) {
  var applyChosen = function (selector) {
    if ($(selector).length) {
      $(selector).chosen({
        allow_single_deselect: true,
        disable_search_threshold: 12
      });
    }
  };
  $(function () {

    applyChosen('select');

    //...

 

 

Maintenant, sur la réponse ajax réussie, nous allons la réappliquer.

public / javascript / scripts.js

 

  $.ajax(ajaxUrl)
    .done(function (response) {
      $('.main').html(response);
      applyChosen('.main select');

Cache de cache

Il reste une dernière idiosyncrasie à régler avant de pouvoir appeler cela fini. Essayons simplement de paginer quelques fois et de cliquer sur un lien non-Ajax qui nous mènera à une autre page. Maintenant, cliquez sur le bouton retour. Beurk! Nous ne récupérons que le contenu de la demande Ajax. Vous ne pourrez peut-être pas répliquer cela dans tous les navigateurs. Google Chrome semble cependant reproduire le bogue de manière fiable. Pourquoi cela se produit-il donc?

La réponse courte est que les bons navigateurs tels que Google Chrome sont vraiment très intelligents. C’est ce qui les rend si vite. Dans ce cas, c’est peut-être un peu trop intelligent, mais au final, nous avons commis une grave erreur.

Plus tôt dans le didacticiel, nous avons parlé d’un point de terminaison commun pour les requêtes HTTP standard et les XHR. S’il est vrai que les deux types de demandes doivent être acheminés via la même action de contrôleur, l’idée qu’ils doivent partager exactement la même URL est erronée. L’un des piliers de la mise en cache HTTP, et du protocole HTTP en général, est que les demandes doivent être déterministes. Pour toute URL donnée, nous devrions nous attendre à la même réponse. Lorsque les URL renvoient diverses réponses basées sur un état externe arbitraire, tel que l’état de session ou, dans notre cas, le type de demande, elles ne sont plus déterministes et de mauvaises choses peuvent se produire, car le navigateur a mis cette adresse en cache, en pensant qu’il l’a déjà vu. la réponse qu’il génère. C’est excellent pour les performances, mais pas pour le type de fonctionnalité clé en main que nous essayons d’implémenter.

Nous devons mettre à jour notre code Javascript afin que la requête Ajax ait une URL légèrement différente de celle stockée dans l’historique. Ajoutons simplement un ajax=1paramètre de requête simple à l’URL.

 

 

    // Pagination
    if ($('.pagination').length) {
        var paginate = function (url) {
            var param = '&ajax=1',
                ajaxUrl = (url.indexOf(param) === -1) ? 
                           url + '&ajax=1' : 
                           url,
                cleanUrl = url.replace(new RegExp(param+'$'),'');

            $.ajax(ajaxUrl)
                .done(function (response) {
                    $('.main').html(response);
                    applyChosen('.main select');
                    $('html, body').animate({
                        scrollTop: $('.main').offset().top
                    });
                    window.history.pushState(
                        {url: cleanUrl},
                        document.title,
                        cleanUrl
                    );
                })
                .fail (function (xhr) {
                    alert('Error: ' + xhr.responseText);
                });
        };

 

Passons à travers cela.

  • Tout d’abord, nous générons la ajaxUrlvariable, quelle que soit l’URL de la fonction donnée, plus un ajax=1paramètre. Notez que nous devons faire attention à ne pas ajouter le ajaxparamètre plusieurs fois. Nous devons le faire car les liens de pagination conservent tous les GETparamètres de la requête Ajax, ils contiendront donc tous des chaînes de requête similaires à ?start=10&ajax=1.
  • Ensuite, nous générons la cleanURLvariable, qui est l’URL avec le ajax=1supprime. Encore une fois, les liens de pagination ont tous ajax=1une valeur, donc cette désinfection est importante.
  • Nous mettons ensuite à jour la requête Ajax pour aller à la ajaxUrlplace de l’URL donnée.
  • Enfin, nous stockons le cleanUrldans l’historique du navigateur. Ainsi, lorsque vous appuyez sur le bouton Précédent, le navigateur sait que la demande Ajax et la demande standard sont différentes.

Afin de tester cela, il est impératif que vous effaciez le cache de votre navigateur. Tout ce bogue tourne autour de la mise en cache désirée, de sorte que vous ne verrez aucun résultat avant de le faire. Si vous utilisez Google Chrome, vous pouvez essayer le mode Incognito pour cela.

Maintenant, le bouton de retour renvoie le résultat attendu et les choses se présentent et se sentent beaucoup mieux.

TOUT VOIR Ajouter une remarque
VOUS
Ajoutez votre commentaire
 
© Academy EdTech. 2019 All rights reserved.
X