dustjs icon indicating copy to clipboard operation
dustjs copied to clipboard

Would it be possible to pass a sub-tree as parameter to a partial?

Open eduardoboucas opened this issue 8 years ago • 9 comments

Apologies if this has been previously discussed or is simply unpractical to implement, but I wish there as a way to pass a sub-tree of DOM elements to a partial as a parameter.

Example:

<!-- Calling the partial -->
{>"components/my-component1" parameter1="foobar"}
    <div class="component__inner">
        <p>Hello world</p>
    </div>
{<}
<!-- Inside the partial -->
<div class="component component--{parameter1}">
    {_content}
</div>

Result:

<div class="component component--foobar">
    <div class="component__inner">
        <p>Hello world</p>
    </div>
</div>

This would allow developers to create self-contained and reusable components as Dust.js partials. You can argue that this is technically possible now, but one would have to pass any markup required by the component as a normal parameter, which can get messy very easily:

{>"components/my-component1" parameter1="foobar" content="<div class='component__inner'><p>Hello world</p></div>"}

A similar approached is used by Sass in their mixins, as developers can define normal parameters as well a special code block defined as @content.

@mixin component1($arg1, $arg) {
    .component1 {
        width: $arg1;
        height: $arg2;
        @content;
    }
}

React components also make this possible through this.props.children.

I'd love to hear your thoughts about this. Thanks in advance!

eduardoboucas avatar Feb 15 '16 22:02 eduardoboucas

I think this is possible now using string interpolation in partial names:

Content partial (named "innerComponent"):

<div class="component__inner">
  <p>Hello world</p>
</div>

Main partial (named "component"):

<div class="component component--{parameter1}">
  {>"{contentPartial}"/}
</div>

Usage:

{>component parameter1="foobar" contentPartial="innerComponent"/}

smfoote avatar Feb 25 '16 16:02 smfoote

That's true, but it requires the "inner component" to be a partial and therefore not arbitrary dynamic content, which almost defeats the purpose. Being able to pass any block of markup to a partial would allow me to do something like this:

Component 1: form.dust

Creates a <form> element wrapped in a container, applies the correct classes based on type, includes various meta information in the form of hidden elements and adds a submit button.

Accepts

  • action
  • method — defaulting to POST
  • type — let's imagine we created 2 variations of a form, normal and collapsed
  • content — the content of the form

Component 2: checkbox.dust

Creates an accessible checkbox input field with customised styling.

Accepts

  • checked — defaulting to false
  • label

Usage

Creating a form with 3 elements: two regular text inputs and an instance of checkbox.

{> "form.dust" method="GET" action="myuri.com" type="collapsed" }
    <input type="text" placeholder="Name">
    <input type="email" placeholder="Email">
    {> "checkbox.dust" label="Subscribe to newsletter" }
{<}

Forcing the elements passed to form.dust to be in a partial would mean creating a file for every possible combination of elements, which is unpractical when the elements need to be generated dynamically (e.g. reading from a JSON schema).

Unless I'm missing something here :sweat_smile:

eduardoboucas avatar Feb 25 '16 16:02 eduardoboucas

Alright, what you want is a custom Dust helper (see http://www.dustjs.com/guides/dust-helpers/ and http://www.dustjs.com/docs/helper-api/ for more info on writing custom helpers).

For the form.dust example above, here's more or less what it would look like:

Helper

dust.helpers['form.dust'] = function(chunk, context, bodies, params) {
  var action = params.action;
  var method = params.method || 'POST';
  var type = params.type;

  // Write the container
  chunk.write('<div class="dust-form-container">')
       .write(`<form method="${method}" action="${action}" class="${type}">`)

       // Render the body, using the current context
       .render(bodies.block, context)

       // Close the container
       .write('</form>')
       .write('</div>');
};

Usage

{@form.dust method="GET" action="myuri.com" type="collapsed"}
  <input type="text" placeholder="Name">
  <input type="email" placeholder="Email">
  {@checkbox.dust label="Subscribe to newsletter"/}
{/form.dust}

Output

<div class="dust-form-container">
  <form method="GET" action="myuri.com" class="collapsed">
    <input type="text" placeholder="Name">
    <input type="email" placeholder="Email">
    <!-- Whatever you defined in your @checkbox.dust helper -->
  </form>
</div>

smfoote avatar Feb 25 '16 17:02 smfoote

Hmm, that's kind of icky to have to put HTML into your helper though. I wonder if we could use chunk.partial in a creative way to do this.

The reason we haven't supported this is mostly around the magic {content} variable or similar that you would need. There's another issue on it I'll dig up.

sethkinast avatar Feb 25 '16 18:02 sethkinast

Alright, what you want is a custom Dust helper (...)

I don't think helpers are the answer. With that pattern, I would end up no partials at all and all my components would be JavaScript files with HTML inside.

The reason we haven't supported this is mostly around the magic {content} variable or similar that you would need.

I understand the reluctancy of introducing a magic variable, but in my opinion the benefits would surpass any disadvantages. Couldn't it be something like $content to be inline with things like $idx?

eduardoboucas avatar Feb 25 '16 20:02 eduardoboucas

Any updates on this? Thanks!

eduardoboucas avatar Mar 29 '16 11:03 eduardoboucas

For anyone interested, this is possible by creating a very simple helper used to call a partial and inject its content block as a parameter:

(function (dust) {
  /**
   * @partial helper
   * Calls a partial and injects the body block as a `$content` parameter
   *
   * Example:
   *    {@partial $name="partials/form"}
   *      <input name="foo" type="text">
   *    {/partial}
   *
   * By @eduardoboucas
   * https://eduardoboucas.com/blog/2016/04/15/creating-modular-ui-components-with-dustjs.html
   */  
  dust.helpers.partial = function (chunk, context, bodies, params) {
    var newContext = {
      $content: bodies.block
    };

    return chunk.partial(params.$name, context.push(newContext), params);
  };
})(typeof exports !== 'undefined' ? module.exports = require('dustjs-linkedin') : dust);

A more detailed explanation can be found here.

eduardoboucas avatar Apr 15 '16 10:04 eduardoboucas

Nice write-up. We would want to do this as a grammar extension instead of in the runtime, so we'll see where that goes.

On Fri, Apr 15, 2016, 3:30 AM Eduardo Bouças [email protected] wrote:

For anyone interested, this is possible by creating a very simple helper used to call a partial and inject its content block as a parameter:

(function (dust) { /** * @partial helper * Calls a partial and injects the body block as a $content parameter * * Example: * {@partial $name="partials/form"} * * {/partial} * * By @eduardoboucas * https://eduardoboucas.com/blog/2016/04/15/creating-modular-ui-components-with-dustjs.html */ dust.helpers.partial = function (chunk, context, bodies, params) { var newContext = { $content: bodies.block };

return chunk.partial(params.$name, context.push(newContext), params);

}; })(typeof exports !== 'undefined' ? module.exports = require('dustjs-linkedin') : dust);

A more detailed explanation can be found here https://eduardoboucas.com/blog/2016/04/15/creating-modular-ui-components-with-dustjs.html .

— You are receiving this because you commented. Reply to this email directly or view it on GitHub https://github.com/linkedin/dustjs/issues/715#issuecomment-210407610

sethkinast avatar Apr 15 '16 10:04 sethkinast

That'd be great. Thank you!

eduardoboucas avatar Apr 15 '16 10:04 eduardoboucas