Skip to content

Reusable UI Dust modules

jimmyhchan edited this page Jul 5, 2012 · 5 revisions

Goal

Most often we have markup that needs to be re-used across different pages and use cases. Dust already provides constructs to write partials and inherit and extend templates. But writing reusable, local or Dust defined default scoped partials/components, with support for branching logic, default/ fallback values, inline documentation for the expected params is not always easy. We have been exploring a few options on how to use one ore more of these inbuilt constructs effectively to support the common use cases that are shared across different pages/apps at LinkedIn.

How to build reusable partials/templates?

Some solutions we came up with,

  • Partials with {>partial/} syntax for shared markup and default/fallback values in the markup, for param declaration

  • Inline params for aliasing values and creating default values

  • Blocks and Inline partials with {+/} and {</} syntax for placeholder values

  • Dynamic partials for selecting one of the many flavors of reusable partial at run-time

  • Combination of > partial and @partial, @param , Inline partials/blocks (+ and <) for placeholders, defaults

  • Using the default Dust override context {>partial:context/} syntax with a partial for accepting any json object

  • Extending the > partial syntax to take params as part of the {>partial a=b/} and @param helpers for defining defaults/fallback values

Comparing Helpers (with @ syntax) and Partials with ( with > syntax )

Helpers with @ syntax

  1. client rendered, inline to the template, can be rendered on server with JS engine. Since we expect to server-side render the same template, we strictly enforce no DOM manipulation in the helpers
  2. anything possible in Javascript can be done in a helper tag, a helper tag itself can include partials in the body
  3. they are not limited to being lambda's in JSON, hence cacheable on CDN as any other Javascript
  4. context modification on the fly to append more info to the given JSON, or if required create a new local context using dust.makebase

Partials with {>partial/} syntax

  1. best bet for shared/ common snippets of markup, but writing re-usable partials is not easy since the including template needs to adhere to the exact JSON contract in the partial, a clean way to pass params, default values from including template, or defining params and fallback values in partials needs some thinking.
  2. the markup is inline and thus supports readability, rather than Javascript code generating markup in a @helper
    • all the benefits of helper, since every partial is pre-compiled to Javascript and should be on par with a helper in term of performance
  3. partials can themselves use @ helpers inline!! For performance reasons, we also strictly enforce no inline script blocks in the partial.

Simple use case for a reusable partial

A simple use case is displaying a degree icon and a placeholder text on the LinkedIn profile based on the connection strength (a.k.a distance, that can have 0,1,2,100 values based on a 1st, 2nd and 3rd degree or not be connected at all.

Solution 1 : Create a partial named "degree_icon" and then invoke this partial from the parent template within a block, say a list of people

 {#people distance=distance name="Jeff Willis"}
  {>"shared/degree_icon"/}
 {/people}
  • default Dust constructs are used, but not generic, it is not always possible to have a block like #people. What if I need to render the degree icon by itself? How do I pass the params to the partial?

Solution 2 : Use-case specific helpers, for instance, @degree_icon for our example

  dust.helpers.degree_icon = function(chunk, context, bodies, params){
    var distance = params.distance || 0:
    switch( parseInt(distance) ){
      // do something 
      default: 
      }
   }
  • Works, it can define the defaults, do some branching logic if required, create the markup on the fly ( kind of ugly ). What if we need an @i18n helper to resolve strings while displaying the degree icon text? One way is to wrap one helper within the other. Another way is to inject i18n strings into the context helper before rendering the template. It is still not elegant.

See below ...

    {#people}
     {@i18n key="label" text="Degree"}
      {@i18n key="distanceStr" text="you have {num} connections with {name}"}
           {@degree_icon distance=profile.distance}
             // access to keys label and distanceStr
           {/degree_icon}
       {/i18n}
      {/i18n}
    {/people}

Solution 3: Generic @partial/@invoke a partial helper that can render any partial such as the degree_icon partial and take expected and default values, similar to Solution 1, but does not rely on a exisitng # in the JSON.

  {#people}
    {@partial distance=people.distance name="Jim Willis"}
     {>"shared/degree_icon"/}
    {/partial}
  {/people}
  • One more aspect we have not touched upon, do we want a local scope in the @partial block or allow the degree_icon to access the parent scope. With @partial it is easy to use a scope="local" param and then use that info in the @partial helper to either use the existing context while rendering the body chunk or create a new context using dust.makebase. Default values can be defined in the partial, but to be generic enough we need some convention. One way is for every param we need to pass in a default value, like the below

      {#people}
       {@partial distance=people.distance def_distance="2" name="Don Draper" def_name="None"}
         {>"shared/degree_icon"/}
       {/partial}
      {/people}
    

Solution 4 : Use the @partial generic helper, but the onus of defining the default params and expected params is on the partial itself and not on the including template. @param helper is used inside the partials, so that the invoking template is no longer concerned about the expected params and its default values

     // degree_icon partial can define one or more of these for all the expected params
     // It can create fallback/default values, serves as a documentation as well

      {@param name="distance" default="1"/} // inserts into the current context
      {@param name="distanceStr"}
        {@i18n key="label" text="Degree"/} // use the i18n output as a value for the param distanceStr
      {/param}

Solution 5 : Extend the Dust grammar to support params in the default partial invocation syntax instead of using the @partial helper to pass args/params. This is probably the most clean one. Still use the @param helper to define the defaults and expected values inside the partial template. Since every partial invoked with the > uses the default Dust scoping rules to navigate the json context, if at all we need only the local context, use >> (Proposed enhancement) to distinguish between local scoped partial vs how the default Dust partial works.

     // uses default Dust scoping rules
     {>"shared/degree_icon" distance=people.distance name="Don Draper"/}  
     // create a local context with the inline params
     {>>"shared/degree_icon" distance=people.distance name="Don Draper"/} 

What we chose in the end?

Solution 4 : a custom helper, @partial that can pass arguments/params to the partial been invoked. The partial itself will define the default and document the expected params using the @param helper. The @partial will always create a new context which the body content will use.

Sample helper code...

 "partial": function( chunk, context, bodies, params ){
   var partial_context = {};
   /* optional : context fo server processed partial related data*/
   var p_context = context.get("partial");
    if(p_context || params) {
      // add to the partial context
     }
    return bodies.block( chunk, dust.makeBase(partial_context));
 }

Discussion on above

rragan: Defaulting should belong to the partial/reuseable component -- not to the including template. We have tried a simple helper that works more or less adequately for modest-sized partials

{@setDefault showHeader="true" showSecondaryNav="false" showFooter="true"} body of partial code {/setDefault}

By pushing the extra params on the context stack they provide values for missing params and leave supplied values alone. This is all I want from a default mechanism. I'd rather avoid the wrapping required vs. just setting a value that is transparently set and used. Can you actually merge values into the current context? If you can, I'm worried about side effects this might cause as the partial changes things that others may also be consuming.

I favor extending the compiler syntax to allow params. This would make partials consistent with other parts of the language.