Skip to content

Decorators/hooks #469

@Rich-Harris

Description

@Rich-Harris

A feature request that's come up once or twice — the ability to decorate elements with additional functionality, such as lazy loading images. Since 'using decorators' has come to mean 'smothering your code in @ symbols' it's probably no longer a great name. @TehShrike suggested 'hooks' which is pretty good, but this is open to bikeshedding.

Essentially, the idea is that you add a directive to an element and define a hook that gets invoked at runtime:

<div class='tall-element'>...</div>
<img hook:lazyload data-src='giant-photo.jpg'/>

<script>
  import 'intersection-observer'; // polyfill

  // this code is probably wrong, never used IntersectionObserver
  // but you get the gist
  var observer = new IntersectionObserver( entries => {
    entries.forEach( entry => {
      if ( entry.intersectionRatio ) {
        const img = entry.target;
        observer.unobserve( img );
        img.src = img.getAttribute( 'data-src' );
        img.onload = () => img.style.opacity = 1;
      }
    });
  });

  export default {
    hooks: {
      lazyload ( img ) {
        img.style.opacity = 0;
        img.style.transition = 'opacity 0.5s';
        observer.observe( img );

        return {
          destroy () {
            observer.unobserve( img );
          };
        };
      }
    }
  };
</script>

Or, passing data in:

<div hook:tooltip='hello!'>hover on me</div>

<script>
  import Tooltip from 'tooltip';

  export default {
    hooks: {
      tooltip ( node, data ) {
        function handleMouseover () {
          const tooltip = new Tooltip( node, data );

          function handleMouseout () {
            tooltip.remove();
            node.removeEventListener( 'mouseout', handleMouseout );
          }

          node.addEventListener( 'mouseout', handleMouseout );
        }

        node.addEventListener( 'mouseover', handleMouseover );

        return {
          destroy () {
            node.removeEventListener( 'mouseover', handleMouseover );
          };
        };
      }
    }
  };
</script>

Finally, updating data:

<span hook:sparkline='lastTenValues'></span>

<script>
  import Sparkline from 'render-sparkline';

  export default {
    hooks: {
      sparkline ( node, data ) {
        const sparkline = new Sparkline( node, data );

        return {
          update ( data ) {
            sparkline.update( data );
          },
          destroy () {
            sparkline.remove();
          };
        };
      }
    }
  };
</script>

Arguably the last one should be a component instead, but you get the idea.

One design challenge, which the last two examples gloss over: it doesn't feel like it makes sense to force the use of {{delimiters}} here (i.e. hook:sparkline='{{lastTenValues}}'), particularly because you might want to pass in an object and then you have a situation {{{ like: this }}} which just looks awful. But if we stipulate that in the context of this directive, the quotes are the delimiters, then if you want to use text then the second example doesn't really work — it would have to be this instead:

<div hook:tooltip='"hello!"'>hover on me</div>

Not sure if there's a clean way to resolve that.

I don't plan to work on this immediately, I think there are more pressing issues. Just wanted to have a place to discuss this.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions