phpmustachetemplating

Mustache.php : Idiomatic ways to template select dropdowns


So I'm having some problems wrapping my head around the best idiomatic way to deal with some complex html cases flexibly using Mustache.php

The first is a pre-selected select dropdown, e.g.

<select>
  <option value=''></option>
  <option value='bob'>Bob Williams</option>
  <option value='james' selected>James Smith</option>
</select>

I have a way that I deal with this, but my way seems really inflexible:

Is there an awesome approach for making preselected select dropdowns using partials or anonymous functions or methods or some other feature of mustache.php that I'm missing?

Edit: Pared down this question into separate parts to try to maximize clarity.


Solution

  • The idiomatic way to do this in Mustache would be to create a View (or ViewModel) rather than passing in a hash of data:

    <?php
    
    class Dropdown
    {
      public  $name;
      public  $value;
      private $options;
    
      public function __construct($name, array $options, $value)
      {
        $this->name    = $name;
        $this->options = $options;
        $this->value   = $value;
      }
    
      public function options()
      {
        $value = $this->value;
    
        return array_map(function($k, $v) use ($value) {
          return array(
            'value'    => $k,
            'display'  => $v,
            'selected' => ($value === $k),
          )
        }, array_keys($this->options), $this->options);
      }
    }
    

    Then you could combine this with a dropdown partial...

    <select name="{{ name }}">
      {{# options }}
        <option value="{{ value }}"{{# selected }} selected{{/ selected }}>
          {{ display }}
        </option>
      {{/ options }}
    </select>
    

    Which you can use in your template like this:

    {{# state }}
      <label for="{{ name }}">State</label>
      {{> dropdown }}
    {{/ state }}
    
    {{# country }}
      <label for="{{ name }}">Country</label>
      {{> dropdown }}
    {{/ country }}
    

    And render it:

    <?php
    
    $data = array(
      'state'   => new Dropdown('state',   $someListOfStates,    'CA'),
      'country' => new Dropdown('country', $someListOfCountries, 'USA'),
    );
    
    $template->render($data);
    

    ... But you can do even better than that :)

    With this:

    <?php
    
    class StateDropdown extends Dropdown
    {
      static $states = array(...);
    
      public function __construct($value, $name = 'state')
      {
        parent::__construct($name, self::$states, $value);
      }
    }
    

    And this:

    <?php
    
    class CountryDropdown extends Dropdown
    {
      static $countries = array(...);
    
      public function __construct($value, $name = 'country')
      {
        parent::__construct($name, self::$countries, $value);
      }
    }
    

    And one of these:

    <?php
    
    class Address
    {
      public $street;
      public $city;
      public $state;
      public $zip;
      public $country;
    
      public function __construct($street, $city, $state, $zip, $country, $name = 'address')
      {
        $this->street  = $street;
        $this->city    = $city;
        $this->state   = new StateDropdown($state, sprintf('%s[state]', $name));
        $this->zip     = $zip;
        $this->country = new CountryDropdown($country, sprintf('%s[country]', $name));
      }
    }
    

    Throw in a new address partial:

    <label for="{{ name }}[street]">Street</label>
    <input type="text" name="{{ name }}[street]" value="{{ street }}">
    
    <label for="{{ name }}[city]">City</label>
    <input type="text" name="{{ name }}[city]" value="{{ city }}">
    
    {{# state }}
      <label for="{{ name }}">State</label>
      {{> dropdown }}
    {{/ state }}
    
    <label for="{{ name }}[zip]">Postal code</label>
    <input type="text" name="{{ name }}[zip]" value="{{ zip }}">
    
    {{# country }}
      <label for="{{ name }}">Country</label>
      {{> dropdown }}
    {{/ country }}
    

    Update your main template:

    <h2>Shipping Address</h2>
    {{# shippingAddress }}
      {{> address }}
    {{/ shippingAddress }}
    
    <h2>Billing Address</h2>
    {{# billingAddress }}
      {{> address }}
    {{/ billingAddress }}
    

    And go!

    <?php
    
    $data = array(
      'shippingAddress' => new Address($shipStreet, $shipCity, $shipState, $shipZip, $shipCountry, 'shipping'),
      'billingAddress'  => new Address($billStreet, $billCity, $billState, $billZip, $billCountry, 'billing'),
    };
    
    $template->render($data);
    

    Now you have modular, reusable, easily testable, extensible bits of code and partials to go with 'em.

    Note that the classes we created are "Views" or "ViewModels". They're not your domain model objects... They don't care about persistence, or validation, all they care about is preparing values for your templates. If you're using Models as well, that makes it even easier, because things like our Address class can wrap your address model, and grab the values it needs directly off the model rather than requiring you to pass a bunch of things to the constructor.

    The Zen of Mustache

    If you take this approach to its logical conclusion, you end up with one top-level View or ViewModel class per action/template pair in your app — the View could internally delegate to sub-Views and partials, like we did with the Dropdowns from our Address View, but you'd have one first-class View or ViewModel responsible for rendering each action.

    Meaning (in an MVC/MVVM world), your Controller action would do whatever "action" was required of it, then create the View or ViewModel class responsible for populating your template, hand it a couple of domain Model objects, and call render on the template. The Controller wouldn't prepare any data, because that's the responsibility of the View layer. It would simply hand it a couple of model objects.

    Now all your logic for "rendering" is neatly encapsulated in the View layer, all your markup is neatly encapsulated in your template file, your Model is free from ugly formatting business, and your Controller is nice and light like it should be :)