-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.idyll
367 lines (284 loc) · 13 KB
/
index.idyll
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
[meta title:"Idyll Custom Components" description:"Short description of your project" /]
[Header
fullWidth:true
title:"Idyll Custom Components"
subtitle:"Creating custom React components in Idyll"
author:"Alan Tan"
authorLink:"https://github.com/tanalan"
date:`(new Date('July 17, 2020')).toDateString()`
background:"#222222"
color:"#ffffff"
/]
## Introduction
Idyll is already rich with many [built-it](https://idyll-lang.org/docs/components) components. That said, the default components may not be enough to suit all of one's needs.
Many of the examples in the [gallery](https://idyll-lang.org/gallery) page have components that are custom written, such as the components used in this [Beat Basics](https://megan-vo.github.io/basic-beats/) 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](https://idyll-lang.org/docs/components/custom) 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](https://reactjs.org/docs/getting-started.html) 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](https://idyll-lang.org/docs/syntax#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!
[br/]
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.
[var name:"theCurrentColor" value:null /]
[TwoColors selectedColor:theCurrentColor /]
The color clicked is [Display value:`theCurrentColor ? theCurrentColor : "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:
```js
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](https://developer.mozilla.org/en-US/docs/web/javascript/reference/statements/export) docs for more info.
Now, let's render two squares of 100x100 pixels. We will render them as `div`s, and wrap them inside a container `div`.
So now, `render()` looks like:
```js
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:
[TwoColors /]
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:
```js
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).
```js
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"` /]
// ```
```
[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:
[var name:"lastClickedColor" value:null /]
[TwoColors selectedColor:lastClickedColor /]
The color clicked is [Display value:`lastClickedColor ? lastClickedColor : "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).
[var name:"balloonSize" value:0.5 /]
[var name:"deflationSize" value:0.1 /]
[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]
[Balloon size:balloonSize deflationSize:deflationSize /]
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:
```js
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).
```js
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](https://mathisonian.com/), 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.
```js
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:
```js
<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.
```js
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](https://github.com/tanalan/idyll-react-components-tutorial/blob/master/components/balloon.js).
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!
[br /]
Feel free to check out the source code for this tutorial [here](https://github.com/tanalan/idyll-react-components-tutorial/blob/master/index.idyll).
And lastly, for any questions, feedback, and/or comments regarding Idyll, do drop by its [Gitter](https://gitter.im/idyll-lang/Lobby) page!