Question

I am starting to use Ractive and I would like to understand better how to make reusable components. It is quite easy to write a generic widget that can be used in different situations. My problem is that often the data in my applications does not come in the shape that the widget expects and I am not sure how to extend the widget to make it more application specific.

To make a concrete example, say I am writing a Ractive component to design a line chart. I would do something along the lines of

var Chart = Ractive.extend({
  template: chart,
  data: {
    points_to_path: function(points) {
      // whatever
    }
  }
});

with a template like

<svg>
  {{# points_to_path(points) }}
    <path d="{{ path }}" />
  {{/ end of path }}
</svg>

This component could be used like

var chart = new Chart({
  data: {
    points: [[1, 2], [3, 4], [5, 6]]
  }
});

Now assume I actually need to use such a component. Probably my data does not come in the shape of an array of two-components arrays, like points above. Maybe I have something more like

[
  {
    date: 'aug 2011',
    value: 12.51
  },
  {
    date: 'sep 2011',
    value: 12.38
  },
  ...
]

So, what I would like to do is to make a more application specific Chart that accepts data in the above form. I do not see an easy way to do this with Chart.extend, because the reference to the points variable is hardcoded in chart.html.

In a framework like Backbone, views are nested, so I could just wrap the inner widget into a bigger view and in the external view define the logic for the data transformation. I am not quite sure how to go on from here in Ractive. Maybe I should have designed the widget in a different way from the start.

More generally, my problem is that I can write small components in Ractive, but I am not quite sure how to compose them into something bigger.

What is the idiomatic way in Ractive to specialize or extend components? Or combine many components into a bigger application?

Was it helpful?

Solution

There's a couple of possible solutions. I'm not sure what would qualify as idiomatic - generally, Ractive's philosophy is 'if it works, it's the right solution!' - I'm the author, but I'll leave it to the community to decide over time what is idiomatic.

1. Passing in functions at initialisation

If you were using d3 to create a scatterplot, typically you would do something like this:

d3.select( 'circle' ).data( points )
     .enter().append( 'circle' )
       .attr( 'cx', function ( d ) { return d.x; })
       .attr( 'cy', function ( d ) { return d.y; })
       .attr( 'r', 5 );

In other words, for each datum item in the array, we've supplied a function that extracts the value we need to plot its x and y position.

The Ractive equivalent would be to specify a function when initialising the component:

<!-- the template -->
<svg>
  <path d='{{ points_to_path(points) }}'/>
</svg>
// let's create a chart component that assumes we get an array
// of points with an `x` and a `y` property
var Chart = Ractive.extend({
  template: chart,
  data: {
    points_to_path: function(points) {
      // is there a 'normalise' function? If so, normalise
      // the data we've been given
      if ( var normalise = this.get( 'normalise' ) ) {
        points = points.map( normalise );
      }

      // turn a normalised `{x: x, y: y}` point into 'x,y'
      var pointToString = function ( point ) {
        return point.x + ',' + point.y;
      };

      // create the SVG path
      return 'M' + points.map( pointToString ).join( ' ' );
    }
  }
});

// now, we need to create a function that tells the Chart
// component how to deal with the data we're about to give it
var chart = new Chart({
  data: {
    points: [[1, 2], [3, 4], [5, 6]],
    normalise: function ( point ) {
      return { x: point[0], y: point[1] };
    }
  }
});

So if you had an array of points with date and value properties, you'd just pass a normalise function (or normalize if you prefer American spelling...) that returned {x: point.date, y: point.value} or whatever.

(Of course, you could overwrite the whole points_to_path function instead if that was easier.)

2. Using partials

An alternative would be to use different partials in different situations:

<!-- widget template -->
<h2>This is a widget</h2>
<div class='contains-dynamic-contents'>
  {{>inner}}
</div>
var widget = new Widget({
  el: 'widgetContainer',
  partials: {
    inner: '<p>This paragraph will appear inside the div</p>'
  }
});

This gives you a lot of freedom, but the line chart example is probably more cumbersome to adapt.

Inline components

The first method would also work with inline components - this example assumes you're using the development (0.3.8-pre) version, which will be released soon:

Ractive.components.linechart = Chart;
<div class='application'>
  <h1>This is a chart</h1>
  <linechart points='{{someData}}' normalise='{{someNormaliseFunction}}'/>
</div>

(Protip: you could specify a default normalise function with Chart.data.normalise = someFunction.)

The second method is a little trickier - you would have to dynamically switch out the partial on initialisation:

Chart = Ractive.extend({
  beforeInit: function () {
    this.partials.inner = ( someCondition ? thisPartial : thatPartial );
  },
  // ...
});

Sorry for the epic length of this response, hope the answer you wanted was in here somewhere!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top