Originally this article was to be about how to implement currency formatting using the template language Mustache, but I see it also covers a bit on how to synchronize template variables between the server-side and client-side. Hopefully you’ll get something out of this 🙂
Here’s your situation:
– You’re using Mustache and Mustache is a logic-less templating language
– You want to support different currency formats ie. “$500”, “£500”, “500 kr” (notice how the currency symbol can be in different locations)
– You’re using Mustache across several languages (in my case PHP and JavaScript)
What you need is:
– A lambda function that formats the currency
– A database to store the currencies
What we want is to be able to extend our existing templates with simple lambda functions so we don’t need to rewrite backend and hopefully not frontend code as well. We don’t want to introduce new logic or change our variables in our templates, that’s why this solution is great – it works out-of-the-box.
Let’s say you have this template:
Milk costs ${{ price }}
With our implementation we can change it to:
Milk costs {{# formatCurrency }}{{ price }}{{/ formatCurrency }}
With the hash:
{ price: 500 }
And the currency data (we come back to this shortly):
{ symbol: "$", format: "@symbol@value" }
We get the output:
Milk costs $500
With different currency data, for example:
{ symbol: "kr", format: "@value @symbol" // notice how the format has changed }
We get:
Milk costs 500 kr
How sick is that?! Let’s get down to business though and see how it works.
Basically we need to inject a lambda function into the template engine in order for this to work.
Here’s a simple implementation that works with the data used in the previous examples:
<?php $vars = array( 'price' => 500, 'formatCurrency' => function( $number ) { /** * Pulls data from database, you know the drill :) * * I give a take on how to support multiple different currencies near the end of the article */ $currency = getCurrency(); $format = $currency['format']; $symbol = $currency['symbol']; $search = array( '@value', '@symbol' ); $replace = array( $number, $symbol ); return str_replace( $search, $replace, $format ); } ); $m = new Mustache_Engine; echo $m->render( 'Milk costs {{# formatCurrency }}{{ price }}{{/ formatCurrency }}', $vars ); ?>
As you can see our anonymous lambda function does the “hard” (search and replace) work for us.
Let’s improve our code and wrap this into a function so it will get included in all the templates:
<?php function renderMustache( $template, array $vars = array() ) { $vars['formatCurrency'] = function( $number ) { $currency = getCurrency(); $format = $currency['format']; $symbol = $currency['symbol']; $search = array( '@value', '@symbol' ); $replace = array( $number, $symbol ); return str_replace( $search, $replace, $format ); }; /** * Note that you'd probably want to instantiate the engine with helpers instead of doing it this way, * but I won't cover that in this article for the sake of simplicity * * Read more about Mustache for PHP here: https://github.com/bobthecow/mustache.php/wiki */ $m = new Mustache_Engine; return $m->render( $template, $vars ); } ?>
So far so good, but we want to support currency formatting on the client-side as well.
Here’s the equivalent JavaScript code of the first PHP example:
var vars = { price: 500, formatCurrency: function() { //!! Notice how we must return a function and manually render the given variable return function( variable, render ) { var currency = getCurrency(), format = currency.format, symbol = currency.symbol var search = [ '@value', '@symbol' ], replace = [ render( variable ), symbol ] return str_replace( search, replace, format ) } } } var rendered = Mustache.render( 'Milk costs {{# formatCurrency }}{{ price }}{{/ formatCurrency }}', vars )
Before we move on: notice how the render function is used as an argument in the returned function, despite it not being used in the official Mustache manual. This is an error and I’ve notified Chris, Mustache’s creator, of the issue. It has not yet been fixed.
Notice how we must duplicate the logic on the client-side because you can’t seemlessly convert the PHP code into JS (it’s actually a fairly interesting subject come to think of it, might follow up on that one – eg. question on stackoverflow). Anyway hopefully this implementation will be more painless than refactoring backend code or even changing template language.
Let’s continue. Let’s also wrap our JavaScript code in a function:
function renderMustache( template, vars ) { vars.formatCurrency = function() { return function( variable, render ) { var currency = getCurrency(), format = currency.format, symbol = currency.symbol var search = [ '@value', '@symbol' ], replace = [ render( variable ), symbol ] return str_replace( search, replace, format ) } } return Mustache.render( template, vars ) }
We still have several duplication problems; let’s first deal with the formatCurrency variable name.
What we could do is to use a (kind of) event-driven solution ala. using WordPress filters. I’m going to use filters in this example as it should give working code and is pretty straightforward. add_filter() is used to register a callback that can be used to modify a value and apply_filters() simply triggers all of the added filter callbacks.
Without further ado, let’s create our filter for our Mustache variables:
<?php add_filter( 'mustacheVars', function( $vars ) { $formatCurrencyVarName = 'formatCurrency'; // notice how we define our formatCurrency variable name in only one place $vars[$formatCurrencyVarName] = function( $number ) { $currency = getCurrency(); $format = $currency['format']; $symbol = $currency['symbol']; $search = array( '@value', '@symbol' ); $replace = array( $number, $symbol ); return str_replace( $search, $replace, $format ); }; $vars['formatCurrencyVarName'] = $formatCurrencyVarName; return $vars; }); ?>
Our original renderMustache() function will now look like this:
<?php function renderMustache( $template, array $vars = array() ) { $vars = apply_filters( 'mustacheVars', $vars ); // we trigger the mustacheVars "event" and the callback return values are applied $m = new Mustache_Engine; return $m->render( $template, $vars ); } ?>
So far so good, now let’s synchronize with the client-side:
<?php $vars = array(); $vars = apply_filters( 'mustacheVars', $vars ); // We get the same variables as used when rendering Mustache templates // Inject the variables as JavaScript $mustacheVars = sprintf( '<script type="text/javascript">var mustacheVars = %s;</script>', json_encode( $vars ) ); ?> <!DOCTYPE html> <html> <head> <title>Very simple example</title> <?php echo $mustacheVars ?> </head> <body> <!-- some content here --> </body> </html>
Then we must also update our JavaScript function:
function renderMustache( template, vars ) { // Notice how we're using the global variable set earlier vars[mustacheVars.formatCurrencyVarName] = function() { return function( variable, render ) { var currency = getCurrency(), format = currency.format, symbol = currency.symbol var search = [ '@value', '@symbol' ], replace = [ render( variable ), symbol ] return str_replace( search, replace, format ) } } return Mustache.render( template, vars ) }
Let’s wrap this last version all in one for a full overview:
<?php // Define Mustache rendering function function renderMustache( $template, array $vars = array() ) { $vars = apply_filters( 'mustacheVars', $vars ); $m = new Mustache_Engine; return $m->render( $template, $vars ); } // For the sake of completelessness let's define a getCurrency() method function getCurrency() { $currency = array(); // Because I'm too lazy to wire up a database and hardcoding is ugly! $currency = apply_filters( 'getCurrency', $currency ); return $currency; } // Add filter for our default Mustache variables add_filter( 'mustacheVars', function( $vars ) { $formatCurrencyVarName = 'formatCurrency'; $vars[$formatCurrencyVarName] = function( $number ) { $currency = getCurrency(); $format = $currency['format']; $symbol = $currency['symbol']; $search = array( '@value', '@symbol' ); $replace = array( $number, $symbol ); return str_replace( $search, $replace, $format ); }; $vars['formatCurrencyVarName'] = $formatCurrencyVarName; return $vars; }); // Just hardcoding a currency add_filter( 'getCurrency', function($currency) { return array( 'symbol' => 'kr', 'format' => '@value @symbol' ); } ); // Render a template $template = renderMustache( 'Milk costs {{# formatCurrency }}{{ price }}{{/ formatCurrency }}', array( 'price' => 500 ) ); // Get and inject the default template variables into JavaScript $vars = array(); $vars = apply_filter( 'mustacheVars', $vars ); $mustacheVars = sprintf( '<script type="text/javascript">var mustacheVars = %s;</script>', json_encode( $vars ) ); ?> <!DOCTYPE html> <html> <head> <title>Product page</title> <?php echo $mustacheVars ?> <script type="text/javascript"> function renderMustache( template, vars ) { vars[mustacheVars.formatCurrencyVarName] = function( number ) { var currency = getCurrency(), format = currency.format, symbol = currency.symbol var search = [ '@value', '@symbol' ], replace = [ number, symbol ] return str_replace( search, replace, format ) } return Mustache.render( template, vars ) } </script> </head> <body> <p> <?php echo $template ?> </p> <p> <script type="text/javascript"> document.write( renderMustache( 'Milk costs {{# formatCurrency }}{{ price }}{{/ formatCurrency }}', { price: 500 } ) ) </script> </p> </body> </html>
And that should output something like this:
Milk costs 500 kr Milk costs 500 kr
Phew.
Let’s wrap this up with how I suggest you could implement multiple currencies.
What you’d want is something like this in the templates:
{{! This is a price formatted by the current currency.. choose whatever "non-normal" property name you'd like: }} {{# formatCurrency._ }}{{ price }}{{/ formatCurrency }} {{! This is a price formatted by a specific currency (here: US Dollars): }} {{# formatCurrency.USD }}{{ price }}{{/ formatCurrency }}
Here’s one way to do it where we use the create_function() function in PHP:
<?php // We separate the currency formatting functionality in its own function function formatCurrency( $format, $symbol, $number ) { $search = array( '@value', '@symbol' ); $replace = array( $number, $symbol ); return str_replace( $search, $replace, $format ); } // We rewrite our mustacheVars filter callback add_filter( 'mustacheVars', function( $vars ) { // just hardcoding some currencies $currencies = array( 'USD' => array( 'symbol' => '$', 'format' => '@symbol@value' ), 'NOK' => array( 'symbol' => 'kr', 'format' => '@value @symbol' ), ); $currencies['_'] = $currencies['NOK']; // set NOK as default currency $currencyCallbacks = array(); foreach ( $currencies as $currency => $data ) { // We use create_function() to dynamically create callbacks.. $currencyCallbacks[$currency] = create_function( '$number', "return formatCurrency('{$data['format']}', '{$data['symbol']}', \$number );" ); } $formatCurrencyVarName = 'formatCurrency'; $vars[$formatCurrencyVarName] = $currencyCallbacks; $vars['formatCurrencyVarName'] = $formatCurrencyVarName; $vars['currencies'] = $currencies; // notice how we set the currencies in the variablese array return $vars; }); ?>
Well as you can see we just made a new dependency on the formatCurrency function, and we also have to mirror this logic in the JavaScript code:
function renderMustache( template, vars ) { var currencies = mustacheVars.currencies, callbacks = {} for ( var currency in currencies ) { var data = currencies.currency // Luckily JavaScript has closures so we don't need a create_function() equivalent callbacks.currency = function() { return function( variable, render ) { var currency = getCurrency(), format = currency.format, symbol = currency.symbol var search = [ '@value', '@symbol' ], replace = [ render( variable ), symbol ] return str_replace( search, replace, format ) } } } vars[mustacheVars.formatCurrencyVarName] = callbacks return Mustache.render( template, vars ) }
Again despite the new logic which seems pretty messy on the server-side, there are actually not that many dependencies on the client-side – most information is given directly from the server.
With that: I hope you enjoyed this article and leave a comment below if you want 🙂