Melody Documentation
  • Why Melody?
  • Quickstart
    • What is Melody?
    • Tutorial
    • Installation
  • Templates
    • Twig
    • Mounting Components
    • Keys and Loops
    • Attributes
    • CSS Classes
    • Inline Styles
    • Refs
    • CSS Modules
  • Melody-Streams
    • Basic concept
    • A Step-by-Step Walkthrough
  • MELODY-STREAMS API
    • createComponent
    • The component function
  • melody-hooks
    • Overview
    • Rules of Hooks
    • Event Handling
    • Separation of Concerns
  • melody-hooks API
    • useState
    • useAtom
    • useEffect
    • useRef
    • useCallback
    • useMemo
    • useReducer
    • usePrevious
  • melody-component
    • Overview
  • melody-hoc
    • Overview
Powered by GitBook
On this page
  • What does it look like?
  • Step by step

Was this helpful?

  1. Melody-Streams

A Step-by-Step Walkthrough

PreviousBasic conceptNextcreateComponent

Last updated 5 years ago

Was this helpful?

Since the traditional melody-component API can cause a lot of boilerplate code and requires the developer to become familiar with several technologies (Redux, higher-order components, possibly redux-observable), we were looking for an API that was simpler, allowed for less code when writing components, and was still as powerful.

The result is melody-streams.

What does it look like?

Let us look at a simple counter component with internal state (the current counter value), a "plus one" button, and a "minus one" button. The implementation with melody-streams might look like this:

index.twig
<div class="counter">
  <h3>Counter: {{ count }}</h3>
  <button type="button" ref="{{ incrementButtonRef }}"> + </button>
  <button type="button" ref="{{ decrementButtonRef }}"> - </button>
</div>
index.js
import { createComponent, attachEvent } from "melody-streams";
import template from "./index.twig";
import { merge } from "rxjs";
import { mapTo, scan, startWith } from "rxjs/operators";

const Counter = () => {
    const [incrementButtonRef, incrementClicks] = attachEvent("click");
    const [decrementButtonRef, decrementClicks] = attachEvent("click");

    const count = merge(
        incrementClicks.pipe(mapTo(1)),
        decrementClicks.pipe(mapTo(-1))
    ).pipe(
        scan((acc, curr) => acc + curr),
        startWith(0)
    );
    return {
        count,
        incrementButtonRef,
        decrementButtonRef
    };
};

export default createComponent(Counter, template);

Step by step

What is going on here? Let's examine it one step at a time.

First, we import the createComponent function from melody-streams. This function expects a component function and a template as arguments. We will call our component function Counter. By convention, component functions are capitalised. Moreover, we import a template file (index.twig).

index.js
import { createComponent } from "melody-streams";
import template from "./index.twig";

const Counter = () => {};

export default createComponent(Counter, template);

As you can see, the component function and the template are both passed to createComponent.

But what does this component function have to do?

First and foremost, it should return a stream of state objects, i.e., an Observable. Whenever a new (and different from the previous) state is delivered by this stream, Melody will re-draw the component. The contents of the state object will be available in the template.

Our template looks like this:

index.twig
<div class="counter">
  <h3>Counter: {{ count }}</h3>
  <button type="button" ref="{{ incrementButtonRef }}"> + </button>
  <button type="button" ref="{{ decrementButtonRef }}"> - </button>
</div>

Therefore, we need to provide state objects that contain the fields count, as well as incrementButtonRef and decrementButtonRef:

index.js
import { createComponent } from "melody-streams";
import template from "./index.twig";

const Counter = () => {
    return {
        count: ...,
        incrementButtonRef: ...,
        decrementButtonRef: ...
    };
};

export default createComponent(Counter, template);

Unfortunately, we do not have these values yet. Creating them is easy, however. Let's start with attachEvent, which gets us already half way there:

index.js
import { createComponent, attachEvent } from "melody-streams";
import template from "./index.twig";

const Counter = () => {
    const [ref1, clicks1] = attachEvent("click");
    const [ref2, clicks2] = attachEvent("click");
    
    return {
        count: ...,
        incrementButtonRef: ref1,
        decrementButtonRef: ref2
    };
};

export default createComponent(Counter, template);

As you can see, attachEvent takes only an event name as parameters (in fact, it can take multiple event names, but that does not matter here). Therefore, the result is not bound to any particular DOM element yet. More on that in a second.

attachEvent returns an array with two values:

  • A reference handler (which we name ref1)

  • An Observable of the requested events (in this case, click events)

By adding the reference handlers to the object returned from the component function (Counter), we make them available to the template. Here, we map ref1 to incrementButtonRef, and ref2 to decrementButtonRef. This is when the results of attachEvent become bound to actual DOM elements.

We still have to create the count value. This value will depend on how many times the "plus one" and the "minus one" buttons have been clicked. We get this information from the Observables clicks1 and clicks2, respectively. Moreover, we want to start with zero.

We assemble the functionality we need by using Rx.js operators and constructors.

index.js
import { merge } from "rxjs";
import { mapTo, scan, startWith } from "rxjs/operators";


const counts = merge(
    clicks1.pipe(mapTo(1)),
    clicks2.pipe(mapTo(-1))
).pipe(
    scan((acc, curr) => acc + curr, 0),
    startWith(0)
);

Putting it all together, with a little bit of variable renaming, the result looks like this:

index.js
import { createComponent, attachEvent } from "melody-streams";
import template from "./index.twig";
import { merge } from "rxjs";
import { mapTo, scan, startWith } from "rxjs/operators";

const Counter = () => {
    const [incrementButtonRef, incrementClicks] = attachEvent("click");
    const [decrementButtonRef, decrementClicks] = attachEvent("click");

    const count = merge(
        incrementClicks.pipe(mapTo(1)),
        decrementClicks.pipe(mapTo(-1))
    ).pipe(
        scan((acc, curr) => acc + curr),
        startWith(0)
    );
    
    return {
        count,
        incrementButtonRef,
        decrementButtonRef
    };
};

export default createComponent(Counter, template);

Here, we used the same variable names as are used in the template, so we can use the shortened object notation for the return value of Counter.

This simple component covers only some of what the melody-streams API can do. Of course, you want to receive props, you want to send events, and do other things. This is covered in the following sections.