[Widget] sfWidgetFormTimeAndClear

Après trois semaines de vacances au soleil et quelques semaines limitées sur le plan du dev, me voici de retour pour votre plus grand plaisir :o

Encore une fois, je vous propose un petit widget légèrement personnalisé : sfWidgetFormTimeAndClear.

On continue dans la lancée du sfWidgetFormDoctrineJQueryAutocompleterAndClear avec le même petit bouton permettant de vider le contenu des listes déroulantes du champ d’heure.

Il faut bien sûr intégrer jQuery pour que cela fonctionne mais vous l’aviez sans doute compris :)

Voici le code :

class sfWidgetFormTimeAndClear extends sfWidgetFormTime
{
  public function render($name, $value = null, $attributes = array(), $errors = array())
  {
    return parent::render($name, $value, $attributes, $errors).
           ' <img src="data:image/png;base64,'.$this->getImage().'" alt="" id="'.$this->getDeleteId($name).'" />'.
           sprintf(<<<EOF
<script type="text/javascript">
  $("#%s").bind("mouseover", function(e){
    $("#%s")[0].style.cursor = "pointer";
  });
  $("#%s").bind("click", function(e){
    $("#%s")[0].value = "";
    $("#%s")[0].value = "";
    %s
    return false;
  });

</script>
EOF
      ,
      $this->getDeleteId($name),
      $this->getDeleteId($name),
      $this->getDeleteId($name),
      $this->getHourId($name),
      $this->getMinuteId($name),
      $this->getSecondId($name)
    );
  }

  private function getHourId($value)
  {
    return $this->generateId($value."_hour");
  }

  private function getMinuteId($value)
  {
    return $this->generateId($value."_minute");
  }

  private function getSecondId($value)
  {
    if ($this->getOption('with_seconds'))
    {
      return '$("#'.$this->generateId($value."_second").'")[0].value = "";';
    }
    return '';
  }

  private function getDeleteId($value)
  {
    return $this->generateId("delete_hour_value_".$value);
  }

  private function getImage()
  {
    return "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJdSURBVDjLpZP7S1NhGMf9W7YfogSJboSEUVCY8zJ31trcps6zTI9bLGJpjp1hmkGNxVz4Q6ildtXKXzJNbJRaRmrXoeWx8tJOTWptnrNryre5YCYuI3rh+8vL+/m8PA/PkwIg5X+y5mJWrxfOUBXm91QZM6UluUmthntHqplxUml2lciF6wrmdHriI0Wx3xw2hAediLwZRWRkCPzdDswaSvGqkGCfq8VEUsEyPF1O8Qu3O7A09RbRvjuIttsRbT6HHzebsDjcB4/JgFFlNv9MnkmsEszodIIY7Oaut2OJcSF68Qx8dgv8tmqEL1gQaaARtp5A+N4NzB0lMXxon/uxbI8gIYjB9HytGYuusfiPIQcN71kjgnW6VeFOkgh3XcHLvAwMSDPohOADdYQJdF1FtLMZPmslvhZJk2ahkgRvq4HHUoWHRDqTEDDl2mDkfheiDgt8pw340/EocuClCuFvboQzb0cwIZgki4KhzlaE6w0InipbVzBfqoK/qRH94i0rgokSFeO11iBkp8EdV8cfJo0yD75aE2ZNRvSJ0lZKcBXLaUYmQrCzDT6tDN5SyRqYlWeDLZAg0H4JQ+Jt6M3atNLE10VSwQsN4Z6r0CBwqzXesHmV+BeoyAUri8EyMfi2FowXS5dhd7doo2DVII0V5BAjigP89GEVAtda8b2ehodU4rNaAW+dGfzlFkyo89GTlcrHYCLpKD+V7yeeHNzLjkp24Uu1Ed6G8/F8qjqGRzlbl2H2dzjpMg1KdwsHxOlmJ7GTeZC/nesXbeZ6c9OYnuxUc3fmBuFft/Ff8xMd0s65SXIb/gAAAABJRU5ErkJggg==";
  }
}

Et voici à quoi cela ressemble :

Je vais sans doute proposer également les widgets date et datetime sous ce format dans les jours à venir.

[Widget] sfWidgetFormDoctrineJQueryAutocompleterAndClear

Pour faire suite à mon billet sur le widget sfWidgetFormDoctrineJQueryAutocompleter, je vous propose mon petit widget sfWidgetFormDoctrineJQueryAutocompleterAndClear.

Il propose un bouton permettant de vider le contenu du champ texte mais également du champ caché d’autocompletion.

Il fonctionne exactement de la même manière que le widget original, il ne faut rien de plus et rien de moins (l’icône est intégrée en base64).

Voici le code :

class sfWidgetFormDoctrineJQueryAutocompleterAndClear extends sfWidgetFormDoctrineJQueryAutocompleter
{
  public function render($name, $value = null, $attributes = array(), $errors = array())
  {
    return parent::render($name, $value, $attributes, $errors).
           ' <img src="data:image/png;base64,'.$this->getImage().'" alt="" id="'.$this->getDeleteId($name).'" />'.
           sprintf(<<<EOF
<script type="text/javascript">
  $("#%s").bind("mouseover", function(e){
    $("#%s")[0].style.cursor = "pointer";
  });
  $("#%s").bind("click", function(e){
    $("#%s")[0].value = "";
    $("#%s")[0].value = "";
    return false;
  });

</script>
EOF
      ,
      $this->getDeleteId($name),
      $this->getDeleteId($name),
      $this->getDeleteId($name),
      $this->generateId('autocomplete_'.$name),
      $this->generateId($name)
    );
  }

  private function getDeleteId($value)
  {
    return $this->generateId("delete_autocomplete_value_".$value);
  }

  private function getImage()
  {
    return "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAJdSURBVDjLpZP7S1NhGMf9W7YfogSJboSEUVCY8zJ31trcps6zTI9bLGJpjp1hmkGNxVz4Q6ildtXKXzJNbJRaRmrXoeWx8tJOTWptnrNryre5YCYuI3rh+8vL+/m8PA/PkwIg5X+y5mJWrxfOUBXm91QZM6UluUmthntHqplxUml2lciF6wrmdHriI0Wx3xw2hAediLwZRWRkCPzdDswaSvGqkGCfq8VEUsEyPF1O8Qu3O7A09RbRvjuIttsRbT6HHzebsDjcB4/JgFFlNv9MnkmsEszodIIY7Oaut2OJcSF68Qx8dgv8tmqEL1gQaaARtp5A+N4NzB0lMXxon/uxbI8gIYjB9HytGYuusfiPIQcN71kjgnW6VeFOkgh3XcHLvAwMSDPohOADdYQJdF1FtLMZPmslvhZJk2ahkgRvq4HHUoWHRDqTEDDl2mDkfheiDgt8pw340/EocuClCuFvboQzb0cwIZgki4KhzlaE6w0InipbVzBfqoK/qRH94i0rgokSFeO11iBkp8EdV8cfJo0yD75aE2ZNRvSJ0lZKcBXLaUYmQrCzDT6tDN5SyRqYlWeDLZAg0H4JQ+Jt6M3atNLE10VSwQsN4Z6r0CBwqzXesHmV+BeoyAUri8EyMfi2FowXS5dhd7doo2DVII0V5BAjigP89GEVAtda8b2ehodU4rNaAW+dGfzlFkyo89GTlcrHYCLpKD+V7yeeHNzLjkp24Uu1Ed6G8/F8qjqGRzlbl2H2dzjpMg1KdwsHxOlmJ7GTeZC/nesXbeZ6c9OYnuxUc3fmBuFft/Ff8xMd0s65SXIb/gAAAABJRU5ErkJggg==";
  }
}

Et à quoi cela ressemble au final :

sfWidgetFormTextareaTinyMCEWithLang

Je souhaitais vous faire partager mon petit widget TinyMCE basé sur celui intégré au plugin sfFormExtraPlugin.

Pourquoi réécrire un widget existant ? Parce que celui-ci n’acceptait pas le paramètre de langue !

Quelque part, il s’agit plus d’un correctif du widget mais bon, il s’avère que j’en ai souvent besoin donc je le partage ici :

class sfWidgetFormTextareaTinyMCEWithLang extends sfWidgetFormTextarea
{
  protected function configure($options = array(), $attributes = array())
  {
    $this->addOption('theme', 'advanced');
    $this->addOption('language', 'en');
    $this->addOption('width');
    $this->addOption('height');
    $this->addOption('config', '');
  }

  public function render($name, $value = null, $attributes = array(), $errors = array())
  {
    $textarea = parent::render($name, $value, $attributes, $errors);

    $js = sprintf(<<<EOF
<script type="text/javascript">
  tinyMCE.init({
    mode:                              "exact",
    elements:                          "%s",
    language:                          "%s",
    theme:                             "%s",
    %s
    %s
    theme_advanced_toolbar_location:   "top",
    theme_advanced_toolbar_align:      "left",
    theme_advanced_statusbar_location: "bottom",
    theme_advanced_resizing:           true
    %s
  });
</script>
EOF
    ,
      $this->generateId($name),
      $this->getOption('language'),
      $this->getOption('theme'),
      $this->getOption('width')  ? sprintf('width:                             "%spx",', $this->getOption('width')) : '',
      $this->getOption('height') ? sprintf('height:                            "%spx",', $this->getOption('height')) : '',
      $this->getOption('config') ? ",\n".$this->getOption('config') : ''
    );

    return $textarea.$js;
  }
}

Et pour l’utiliser :

  • dans votre classe de formulaire
$this->widgetSchema['widget'] = new sfWidgetFormTextareaTinyMCEWithLang(sfConfig::get('app_tinymce_config'));
  • dans le fichier app.yml
all:
  tinymce:
    width:  550
    height: 125
    language: fr
    config: |
      plugins: "paste",
      paste_auto_cleanup_on_paste : true,
      theme_advanced_buttons1: "bullist,pasteword,bold,italic,underline,fontselect,fontsizeselect",
      theme_advanced_buttons2: "",
      theme_advanced_buttons3: "",
      theme_advanced_buttons4: ""
    theme: <?php echo sfConfig::get('app_tinymce_theme','advanced') ?>

Edit : un billet sur le même sujet existe en anglais (merci à Tomasz Ducin)

[Tuto] Bien utiliser le widget sfWidgetFormDoctrineJQueryAutocomplete

Enfin un tutoriel ! Cela faisait bien longtemps que je n’en avais pas publié (par faute de temps …) !

Sujet du jour : sfWidgetFormDoctrineJQueryAutocomplete

Ce widget propose un champ input de type text qui, lorsque vous tapez des données, effectue une recherche en ajax dans la base de données (ici, nous travaillons avec Doctrine) et dans le cas de résultats, qui vous les retranscrit dans une liste placée sous le champ input.

L’idée étant de rechercher rapidement et simplement des données dans une liste plus ou moins longue. Qui plus est, cela permet de charger plus rapidement la page et par la même occasion, d’éviter de créer des listes très longues.

Voici une représentation du widget en fonction :

Alors, pour le mettre en œuvre, il faut au préalable installer le plugin sfFormExtraPlugin qui l’intègre (l’installation est standard).

Pour bien comprendre la technique, je vais vous décrire chaque étape importante et vous fournir la structuration des données.

  • le schéma de données (schema.yml)
options:
  collate: utf8_general_ci
  charset: utf8
Maitre:
  connection: doctrine
  columns:
    titre:
      type: varchar(255)
      notnull: true
Eleve:
  connection: doctrine
  columns:
    titre:
      type: varchar(255)
      notnull: true
    id_maitre:
      type: integer
      notnull: true
  relations:
    Maitre: { onDelete: cascade, onUpdate: cascade, local: id_maitre, foreign: id }

J’ai ensuite généré ma base de données et deux modules d’admin generator avec Doctrine.
Je me retrouve donc avec un module de gestion des Maîtres et un autre des Elèves. Du fait du schéma, lorsqu’on rajoute un élève, on doit choisir son maître parmi la liste déroulante de maîtres. Nous placerons donc ici notre autocomplete.

Avant de l’intégrer, voici comment se présente le formulaire d’ajout d’élève :

La première chose qu’on travaille généralement dans ce cas est l’affichage d’un texte à la place des identifiants :

  • lib/model/doctrine/Maitre.class.php
class Maitre extends BaseMaitre
{
  public function __toString()
  {
    return $this->titre;
  }
}

Ce qui nous donne :

Jusque là, rien de particulier mais la liste paraît assez longue tout de même (899 entrées !) et en environnement dev, cache vidé, cette simple page met plus de 0,8s à apparaître. C’est trop.

Remplaçons cette liste déroulante par un champ autocomplete !

  • lib/form/doctrine/EleveForm.class.php
  public function configure()
  {
    $this->widgetSchema['id_maitre']->setOption('renderer_class', 'sfWidgetFormDoctrineJQueryAutocompleter');
    $this->widgetSchema['id_maitre'] = new sfWidgetFormDoctrineJQueryAutocompleter(
      array(
        'model' => "Maitre",
        'url'   => url_for("@ajax_maitre"),
        'config' => '{ max: 50}'
      )
    );
  }

On remarque qu’on remplace bien le widget d’origine (sfWidgetFormDoctrineChoice) par l’autocomplete.

On remarque également qu’on lui passe en option le modèle avec lequel travaillé et une route symfony mais qu’on peut également lui passer le nombre de lignes à afficher (ici 50).

Utilisant le helper Url, il faut penser à le charger ici :

  public function configure()
  {
    sfProjectConfiguration::getActive()->loadHelpers('Url');
    // ....
  }

Il faut donc maintenant créer cette route

  • apps/your_app/config/routing.yml
ajax_maitre:
  url: /ajax-maitre
  param: { module: maitre, action: ajaxMaitre }

Cette route est simple à comprendre : on charge la méthode executeAjaxMaitre du fichiers actions.class.php du module maitre.

Créons donc cette méthode alors !

  • apps/your_app/modules/maitre/actions/actions.class.php
  public function executeAjaxMaitre(sfWebRequest $request)
  {
    // si ce n'est pas une requête ajax, on renvoit vers du 404
    $this->forward404unless($request->isXmlHttpRequest());
    // c'est de l'ajax, on retourne donc du json
    $this->getResponse()->setContentType('application/json');
    $choices = array();
    // on récupère les données de la base de données
    $maitres = Doctrine::getTable('Maitre')->getMaitreAutocompletion($request->getParameter('q'), $request->getParameter('limit'));
    // on boucle sur les données et on charge les données dans un nouveau tableau, celui qu'on va retourner
    foreach($maitres->getData() as $m)
    {
      $choices[$m->id] = $m->titre;
    }
    // s'il y a des données, on retourne le tableau encodé en json
    if($choices != array())
    {
      return $this->renderText(json_encode($choices));
    }
  }

Il nous reste à créer la méthode getMaitreAutocompletion du modèle Maitre

  • lib/model/doctrine/MaitreTable.class.php
  public function getMaitreAutocompletion($q, $limit){
    $dq = Doctrine_Query::create()
            ->select("m.id, m.titre")
            ->from("Maitre m")
            ->where("m.titre LIKE ?","%".$q."%")
            ->limit($limit)
            ->orderBy('m.titre ASC');
    return $dq->execute();
  }

Dernière chose importante avant de tester, il faudrait peut-être intégrer jQuery non ? Commençons par télécharger la dernière version minifiée sur le site officiel, copions là dans web/js/jquery.min.js puis

  • apps/your_app/config/view.yml
  javascripts:    [jquery.min.js]

That’s all folks !

Regardons alors le résultat :

Quoi de plus simple, je vous le demande !

Ainsi s’achève ce petit tutoriel qui, je l’espère, vous sera bien utile !

A bientôt pour de nouvelles aventures sur symfony !

EDIT : Merci à Jonathan T pour l’astuce concernant le nombre de lignes affichées lors de l’autocompletion !

Conférence symfony : « The State of Symfony 2″

Petit billet pour vous informer de la tenue prochaine d’une nouvelle conférence Symfony. Petite nouveauté car celle-ci se tiendra online.

Il y aura 2 sessions :

  • le 22 juin à 10h00
  • le 23 juin à 17h00

Il faut compter environ 3h de conférence (30 minutes par intervention).

Au programme :

  • Tests fonctionnels et unitaires
  • Propel 1.5 pour Symfony 2.0
  • Le nouveau framework de formulaire
  • Améliorations diverses
  • Intégration de Doctrine 2 à Symfony 2
  • Nouveauté « The Symfony 2 killer feature »

Que du bon en fait !

L’inscription, c’est par ici et le site ici.

Internationaliser le filtre ‘is_empty’ de l’admin generator

Quel développeur symfony ne s’est pas posé cette question : comment internationaliser le filtre ‘is_empty’ de l’admin generator ?

Vous trouverez plusieurs solutions sur le net mais il y en a une que j’apprécie fortement et je vais vous en faire profiter :
- ajoutez la fonction privée __($string) dans le fichier /path_du_projet/lib/filter/doctrine/BaseFormFilterDoctrine.class.php :

protected  function __($string) {
  sfProjectConfiguration::getActive()->loadHelpers(array('I18N'));
  return __($string, array(), 'sf_admin');
}

- dans chaque fichier des filtres de votre modèle (/path_du_projet/lib/filter/doctrine/ModelFormFilter.class.php) :

$this->widgetSchema['champ']->setOption('empty_label', $this->__('is empty'));

- ajoutez dans le fichier /path_du_projet/apps/votre_app/i18n/sf_admin.fr.xml (si vous n’utilisez pas ce fichier, n’hésitez pas à le récupérer dans le répertoire /path_de_symfony/plugins/sfDoctrinePlugin/i18n/sf_admin.fr.xml) :

<trans-unit>
 <source>is empty</source>
 <target>est vide</target>
</trans-unit>

Cette méthode fonctionne pour toutes les traductions nécessaires et peut servir un peu partout.

Retour & nouveau plugin : vjThemePlugin !

Bonjour ! Bonjour !
Me revoilà ! Après plusieurs semaines d’absence de mon blog pour quelques vacances et parce que c’est bientôt la rentrée, nous voilà reparti pour de nouvelles aventures :p

Pour commencer, rien de bien compliquer histoire de se remettre en jambe : un petit plugin de theme d’admin generator vjThemePlugin !

Je préfère préciser tout de suite, ce thème n’est pas une révolution, loin de là. Il se base sur le thème d’admin generator de Doctrine en lui ajoutant quelques petites choses :

  • pagination au dessus de la liste (ainsi qu’en dessous)
  • nom des colonnes (+ tri) en dessous de la liste (ainsi qu’au dessus)
  • les filtres ne sont plus affichés en colonnes sur le côté mais sont placés en ligne au dessus de la liste mais sont cachés en standard (un lien permet de les afficher puis de les recacher)

Pour préciser un peu les choses, voici trois screenshots :

  • liste avec pagination + colonnes + filtres cachés

  • liste avec pagination + colonnes + filtres affichés

  • liste de nombreux filtres

Ce petit plugin intègre de base les fichiers javascript jQuery, les deux icônes + et -, le thème complet de l’admin generator.

Une fois installé, il suffit de générer un module d’admin generator ou de modifier le thème d’un module existant.

Voili voilou ! Je vous laisse le tester directement pour vous faire une idée :)

Page du plugin sur le site officiel symfony
Guide d’installation et d’utilisation
Repository SVN : http://svn.symfony-project.com/plugins/vjThemePlugin/

Pour toutes questions, n’hésitez pas !

A bientôt pour de nouvelles aventures :)

Que peut-on mettre de spécial dans le fichier generator.yml ?

Allez, en route pour la première astuce !

Outre les paramètres standards sur lesquels je ne m’étendrai pas (Documentation officielle), je me suis rendu compte que certains paramètres supplémentaires n’étaient pas très explicites (surtout depuis les modifications de l’admin generator entre la 1.0 et la 1.1/1.2).  Je vais donc vous retranscrire ce que j’ai pu trouver lors de mes recherches :


- params est ignoré (paramètre utilisé en version 1.0)


- correspond au nom du champ qui sera affiché entre les balises label du formulaire

numero_dossier:  {label: Numéro }

- permet d’afficher une popup de confirmation

confirm: Etes-vous sûr de vouloir supprimer cette fiche ?

- récupère la valeur d’un champ dans le fichier app.yml

type:
  label: <?php echo sfConfig::get('app_label_type') ?>

- format de date

date_format: dd/MM/yyyy

- permet d’afficher un texte sous le champ de formulaire

help: "1 à 9 (voir plan)"

- attributs HTML qui seront appliqués au champ correspondant

attributes: { readonly: true, class: ma_classe }