Idyll Custom Components

Creating custom React components in Idyll

Fri Jul 17 2020

Introduction

Idyll is already rich with many built-it components. That said, the default components may not be enough to suit all of one’s needs. Many of the examples in the gallery page have components that are custom written, such as the components used in this Beat Basics article. Much of the creativity Idyll allows is through writing one’s own custom components.

This article serves as an introductory tutorial for writing your own custom components (in React) in Idyll, and is an extension to Idyll’s current docs with custom components.

Idyll components are React components under the hood, and it may be good to be familiar with the basics of React. Their docs are well documented, and serve as a great starting point for learning React.

Core custom component ideas

Very important to an Idyll document’s interactivity is its interaction with variables. This is important for writing custom components that have interactivity with users, as well as other components.

In Idyll, a variable can be bound to a component’s property (prop). If this variable is updated, all components bound to this variable also update with the new value.

In particular, for all custom components, Idyll injects an updateProps method into the props object of a custom component. We pass our variables into a custom component’s props, and the component can updateProps, which facilitates interaction between components--when a variable’s value gets updated, it will affect any other components bound to it as well!

This is very powerful.

Writing custom components

I demonstrate this by interacting with a small basic custom component, TwoColors. Try clicking on a colored section below, and notice how the color selected text changes.

Green
Orange

The color clicked is not yet chosen

To make this, I defined my own custom component, TwoColors, in a file called two-colors.js in my components/ folder, letting Idyll know about it. The contents start out with a basic React template:

import React from 'react';

class TwoColors extends React.PureComponent {
  constructor(props) {
    super(props);
  }

  render() {
    // ... rendered output here
  }
}

export default TwoColors;

Note: The export default TwoColors; statement allows Idyll to find the component TwoColors. See export docs for more info.

Now, let’s render two squares of 100x100 pixels. We will render them as divs, and wrap them inside a container div. So now, render() looks like:

render() {
  return (
    <div>
      <div
        className="square-green"
        style={{
          backgroundColor: 'green',
          width: '100px',
          height: '100px',
          textAlign: 'center',
          cursor: 'pointer',
        }}
      >
        Green 
      </div>
      <div
        className="square-orange"
        style={{
          backgroundColor: 'orange',
          width: '100px',
          height: '100px',
          textAlign: 'center',
          cursor: 'pointer',
        }}
      >
        Orange
      </div>
    </div>
  );
}

I injected CSS styling into the square div’s style property.

Now, inside our index.idyll file, we can create a TwoColors component via:

[TwoColors /]

and we get the output:

Green
Orange

Great! Now, let’s add some interactivity! Inside index.idyll, we define a variable that will track the last clicked color. Because we initially haven’t clicked on a color, we’ll initialize the variable’s value to null.

[var name:"lastClickedColor" value:null /]

Then, we’ll bind this variable to a property of TwoColors! Let’s call this property selectedColor, just to differentiate between our variable and the prop.

[var name:"lastClickedColor" value:null /]
[TwoColors selectedColor:lastClickedColor /]

Inside TwoColors, we’ll add a click event to each square, and upon clicking, the square will update its selectedColor prop, which will update our lastClickedColor variable. Let’s add onClick events to both squares. Our updated render() looks like:

render() {
  return (
    <div>
      <div
        className="square-green"
        style={{
          backgroundColor: 'green',
          width: '100px',
          height: '100px',
          textAlign: 'center',
          cursor: 'pointer',
        }}
        onClick={() => this.changePropsColor('green')} // Added this
      >
        Green 
      </div>
      <div
        className="square-orange"
        style={{
          backgroundColor: 'orange',
          width: '100px',
          height: '100px',
          textAlign: 'center',
          cursor: 'pointer',
        }}
        onClick={() => this.changePropsColor('orange')} // Added this
      >
        Orange
      </div>
    </div>
  );
}

Let’s also define the changePropsColor function! It will accept a color parameter, and then updateProps() to the clicked color! (Notice how we passed green and orange in the previous code block).

constructor(props) {
  super(props);
}

changePropsColor(color) {
  this.props.updateProps({ // This uses the `updateProps` method that Idyll injects into components
    currentColor: color
  });
}

Now, when we click on a square, the changePropsColor function is called, and the selectedColor prop updates to the color clicked, and then the lastClickedColor variable becomes updated to the color selected (“green” or “orange”).

Finally, let’s now display our lastClickedColor to see it update live! We’ll use Idyll’s default Display component to display the variable.

[var name:"lastClickedColor" value:null /]
[TwoColors selectedColor:lastClickedColor /]

The color clicked is [Display value:`lastClickedColor ? lastClickedColor : "not yet chosen"` /]

Which results in what we saw before:

Green
Orange

The color clicked is not yet chosen

A more complex version

Let’s work on a more interesting component. Below, we have two range sliders, and a balloon. The first slider controls the size of the balloon. The second slider controls the scale of the balloon’s deflation.

I’ve constructed this so that when we click on the balloon, it will deflate by the scale from the second slider.

Try changing the size of the balloon with the first slider, and also try deflating it by clicking it. (Note: values of zero are not allowed by choice).

0.50 Controls the size of the balloon

0.10 Controls the deflation scale

Let’s go over how we can do this.

We introduce two variables binded to each range slider:

[var name:"balloonSize" value:0.5 /]
[var name:"deflationSize" value:0.1 /]

// The div and styling are added so that we can still see these over the balloon
[div style:`{position:'relative', zIndex:100}`]
  [Range min:0.1 max:1 value:balloonSize step:0.1 /]
  [Display value:balloonSize /]
  Controls the size of the balloon

  [Range min:0.1 max:balloonSize value:deflationSize step:0.1 /]
  [Display value:deflationSize /]
  Controls the deflation scale
[/div]

Now, we will render a balloon component that will take our two variables as props, so that its size can be updated based off them. As such, this balloon will take two props, size and deflationSize.

[Balloon size:balloonSize deflationSize:deflationSize /]

We want to be able to click on the balloon, and have its size changed (deflated). So, here’s what our implementation will look like:

import React from 'react';

// props: size and deflationSize
class Balloon extends React.PureComponent {
  constructor(props) {
    super(props);
  }

  deflate() {
    ... TODO
  }

  render() {
    ... TODO
  }
}

export default Balloon;

The deflate method will be called when we click on the balloon. To implement the deflate method, we want to subtract the deflation size from the balloon size, and use the injected updateProps method to update the balloon size. (I added a few details to handle floating point subtraction and also disallowing our size to get to 0).

deflate() {
  const updatedSize = (this.props.size - this.props.deflationSize).toFixed(2); // handling floating point subtraction
  if (updatedSize > 0) {
    this.props.updateProps({
      size: updatedSize,
    });
  }
}

Now, let’s put the balloon in our render method! This balloon is actually a custom SVG.

Matthew Conlen, Idyll’s creator, helped make it for me.

There are lots of details about the SVG, but we will focus on the part that changes its size based off our balloonSize, which is in the <g> tag.

render() {
  return (
    <div onClick={() => this.deflate}>
      <svg width="100%" height="auto" viewBox="0 0 400 500" style={{overflow: 'visible'}} >
        <path d="M101.124805,255 C100.146564,233.732769 103.780451,291.312889 116.550101,326.561293 C129.319752,361.809698 147.686331,348.668032 144.832964,375.648791 C141.979596,402.62955 110.037653,406.908957 106.501018,421.583026 C102.964383,436.257095 123.051823,445.402516 130.601671,491.110588" id="Path-3" stroke="#000000" strokeWidth="4" fill="none"></path>
        <g id="Group" transform={`translate(${194/2 * (1-Math.sqrt(this.props.size))},${255* (1-Math.sqrt(this.props.size))}) scale(${Math.sqrt(this.props.size)})`}>
          <path d="M101.124805,255 C155.402086,255 210.611891,140.073889 188.585412,77.1065481 C166.558932,14.1392068 127.46589,5.68434189e-14 101.124805,5.68434189e-14 C74.7066494,5.68434189e-14 15.5948675,12.267268 2.1949056,77.1065481 C-11.2050563,141.945828 46.8475228,255 101.124805,255 Z" id="Path" fill="#D0021B"></path>
          <path d="M28.9961639,135 L60,36 C43.3256612,48.9474492 32.9910492,61.8445106 28.9961639,74.6911843 C25.0012787,87.537858 25.0012787,107.640797 28.9961639,135 Z" id="Path-2" fill="#FE8B99"></path>
        </g>
      </svg>
    </div>
  );
}

In the <g> tag:

<g id="Group" transform={`translate(${194/2 * (1-Math.sqrt(this.props.size))},${255* (1-Math.sqrt(this.props.size))}) scale(${Math.sqrt(this.props.size)})`}>

Essentially, to transform the SVG, we translate and scale it based off this.props.size. Because this.props.size is binded to our balloonSize, changing the first slider will change the size of the balloon.

The container <div onClick={this.deflate}> over the SVG allows us to click on the balloon, and have the balloon be deflated.

As a final note, we also want to limit the deflationSize to be no greater than the balloonSize, as otherwise we would get a negative balloonSize if we kept deflating. So, as a last addition, we will check that when the deflationSize is bigger than the balloonSize, we update the deflationSize to the balloonSize.

This is added to our render method, before the return.

render() {
  if (this.props.deflationSize > this.props.size) {
    this.props.updateProps({
      deflationSize: this.props.size,
    });
  }

  return ...
}

And that’s it! Check out the full balloon component source code here.

To summarize, we introduced variables that are bounded to the props of components. The components will updateProps to update the variables, and all components bounded to the variables will also receive their updated values. Idyll grants a lot of flexibility and interaction between custom components through its use of bounded variables!

Feel free to check out the source code for this tutorial here. And lastly, for any questions, feedback, and/or comments regarding Idyll, do drop by its Gitter page!