项目作者: funkia

项目描述 :
Purely functional frontend framework for building web applications
高级语言: TypeScript
项目地址: git://github.com/funkia/turbine.git
创建时间: 2016-09-02T12:23:51Z
项目社区:https://github.com/funkia/turbine

开源协议:MIT License

下载


Turbine

A purely functional frontend framework based on functional reactive
programming. Experimental.

Gitter
Build Status
codecov

Table of contents

Why Turbine?

The JavaScript world is full of frameworks. So why another one?
Because we want something different. We want something that is
purely functional without compromises. Something that takes the best
lessons from existing JavaScript frameworks and couples them with the
powerful techniques found in functional languages like Haskell. We
want a framework that is highly expressive. Because when functional
programming is at its best it gives you more power, not less. Turbine
is supposed to be approachable for typical JavaScript developers while
still preserving the benefits that comes from embracing purely
functional programming.

We have done our best to realize our goal. But we are not done yet. We
hope you will find Turbine interesting, try it and maybe even help us
making it even better.

Examples

Email validator

See the example live and editable here.

  1. const isValidEmail = (s: string) => /.+@.+\..+/i.test(s);
  2. const app = component((on) => {
  3. const isValid = on.email.map(isValidEmail);
  4. return [
  5. span("Please enter an email address: "),
  6. input().use({ email: "value" }),
  7. div(["The address is ", isValid.map((b) => (b ? "valid" : "invalid"))])
  8. ];
  9. });
  10. // `runComponent` is the only impure function in application code
  11. runComponent("#mount", main);

Counter

See the example live and editable here.

  1. const counter = component((on, start) => {
  2. const count = start(accum((n, m) => n + m, 0, on.delta));
  3. return E.div([
  4. "Counter ",
  5. count,
  6. E.button({ class: "btn btn-default" }, " + ").use(o => ({
  7. delta: o.click.mapTo(1)
  8. })),
  9. E.button({ class: "btn btn-default" }, " - ").use(o => ({
  10. delta: o.click.mapTo(-1)
  11. }))
  12. ]);
  13. });
  14. // `runComponent` is the only impure function in application code
  15. runComponent("#mount", counter);

See more examples here.

High-level overview

Here our some of our key features.

  • Purely functional. A Turbine app is made up of only pure functions.
  • Leverage TypeScript and runtime checking to improve the developing
    experience.
  • Based on classic FRP. Behaviors represents values that change over
    time and streams provide reactivity. Turbine uses the FRP
    library Hareactive.
  • A component-based architecture. Components are immutable,
    encapsulated and composable. Components are monads and are typically
    used and composed with do-notation (we implement do-notation with
    generators).
  • Constructed DOM elements reacts directly to behaviors and streams.
    This avoids the overhead of using virtual DOM and should lead to
    great performance.
  • Side-effects are expressed with a declarative IO monad. This allows
    for easy testing of code with side-effects. Furthermore, the
    IO-monad is integrated with FRP.
  • The entire data flow through applications is explicit and easy to
    follow.
  • Our libraries are available both as CommonJS and ES2015 modules.
    This allows for tree-shaking.

Here are some of the features we want to implement and goals we’re
working towards.

  • Declarative and concise testing of time-dependent FRP code.
  • Performance. We think Turbine can be made very efficient. But we are
    not yet at a point where we focus on performance.
  • Support for server side rendering.
  • Browser devtools for easier development and debugging.
  • Hot-module replacement (if possible given our design).

Principles

This section describes some of the key principles and ideas underlying
the design of Turbine.

Purely functional

Turbine is purely functional. We mean that in the most strict sense of
the term. In a Turbine app, every single expression is pure. This
gives a huge benefit in how easy it is to understand and maintain a
Turbine app is.

One benefit of the complete purity is that every function in Turbine
supports what is called “referential transparency”. This means that an
expression can always be replaced with its value.

As a simple example, say you have the following code:

  1. const view = div([
  2. myComponent({ foo: "bar", something: 12 }),
  3. myComponent({ foo: "bar", something: 12 })
  4. ]);

One may notice that myComponent is called twice with the exact same
arguments. Since all functions in a Turbine app are pure myComponent
is no exception. Hence, we can make the following simple refactoring.

  1. const component = myComponent({foo: "bar", something: 12}),
  2. const view = div([
  3. component,
  4. component
  5. ]);

Such refactorings can always be safely done in Turbine.

Completely explicit data flow

One significant challenge when writing an interactive frontend
application is how to manage the data flow through an application.

In Turbine we have strived to create an architecture where the data
flow is easy to follow and understand. For us, this means that when
looking at any piece of code it should be possible to see what other
parts of the application it affects and what other parts it is
affected by.

One manifestation of this principle is that in Turbine it is very
simple to see how the model affects the view and how the view affects
the model. The figure below illustrates this.

modelView figure

The arrows represent data flow between the model and the view. Note
how these “conceptual arrows” are clearly expressed in the code. For
instance, by looking at the buttons we can see exactly what output
they produce.

Declarative models

Imperative programming is about doing. Functional programming is
about being. This mean that ideally a functional program should be
about defining what things are. That property is what makes functional
programs declarative.

Below is a model from the counters example.
Notice how the model consists of nothing but a series of const
statements.

  1. function* counterModel({ incrementClick, decrementClick, deleteClick }) {
  2. const increment = incrementClick.mapTo(1);
  3. const decrement = decrementClick.mapTo(-1);
  4. const deleteS = deleteClick.mapTo(id);
  5. const count = yield accum(add, 0, combine(increment, decrement));
  6. return { count, deleteS };
  7. }

Each line is a declaration of a piece of the state. All models in
Turbine follows this pattern. This makes state in a Turbine app very
easy to understand. One can look at a single definition and be certain
that it tells everything there is to know about that specific piece of
state.

This is in sharp contrast to frameworks that mutate state or
frameworks where state is stepped forward by reducer functions. With
such approaches a single piece of state can potentially be affected
and changed in several places. That can make it hard to understand how
the state evolves. The benefits of having a definition as a
single source of truth is lost.

Installation

  1. npm install @funkia/turbine @funkia/hareactive

Hareactive is a peer
dependency. It is the FRP library that that Turbine is based upon.

Alternatively, for quickly trying out Turbine you may want to see our
Turbine starter kit.

More examples

Here is a series of examples that demonstrate how to use Turbine.
Approximately listed in order of increasing complexity.

  • Email validator — Very simple example of an email
    validator.
  • Fahrenheit celsius — A converter
    between fahrenheit and celsius.
  • Zip codes — A zip code validator. Shows one
    way of doing HTTP-requests with the IO-monad.
  • Continuous time — Shows how to utilize
    continuous time.
  • Counters — A list of counters. Demonstrates
    nested components, managing a list of components and how child
    components can communicate with parent components.
  • Todo — An implementation of the classic
    TodoMVC application.

Tutorial

In this tutorial, we will build a simple application with a list of
counters. The application will be simple but not completely trivial.
Along the way, most of the key concepts in Turbine will be explained.
We will see how to create HTML, how to create custom components, how a
component can be nested and how it can share state with its parent.

Please open an issue if you have questions regarding the tutorial or
ideas for improvements.

The final result and the intermediate states can be seen by cloning
this git repository, going into the directory with the counters
example and running webpack to serve the application.

  1. git clone https://github.com/funkia/turbine/
  2. cd turbine/examples/counters
  3. npm run start

FRP

Turbine builds on top of the FRP library Hareactive. The two key
concepts from FRP are behavior and stream. They are documented in
more detail in the Hareactive
readme
. But the most important
things to understand are behavior and stream.

  • Behavior represents values that change over time. For instance,
    the position of the mouse or the number of times a user has clicked
    a button.
  • Stream represents discrete events that happen over time. For
    instance click events.

What is Component

On top of the FRP primitives Turbine adds Component. Component is the
key concept in Turbine. Once you understand Component—and how to use
it—you understand Turbine. A Turbine app is just one big component.

Here is a high-level overview of what a component is.

  • Components can contain logic expressed through operations on
    behaviors and streams.
  • Components are encapsulated and have completely private state.
  • Components contain output through which they selectively decide
    what state they share with their parent.
  • Components write DOM elements as children to their parent. They
    can write zero, one or more DOM elements.
  • Components can declare side-effects expressed as IO-actions.
  • Components are composable—one component can be combined with
    another component and the result is a third component.

A Component in Turbine is pure and immutable. A Component can be
thought of as a huge description of all of the above mentioned things.
For instance, a Component contains a description about what its DOM
look like. That part is a bit like virtual DOM. But, on top op that
the description also explain how the DOM changes over time. The
description also tells what output the Component contains. More on
that later.

Creating HTML-elements

Turbine includes functions for creating components that represent
standard HTML-elements. When you create your own components they will
be made of these.

The element functions accept two arguments, both of which are
optional. The first is an object describing various things like
attributes, classes, etc. The second argument is a child component.
For instance, to create a div with a span child we would write.

  1. const myDiv = div({ class: "foo" }, span("Some text"));

The element functions are overloaded. So instead of giving span a
component as child we can give it a string. The element functions also
accept an array of child elements like this.

  1. const myDiv = div({ class: "foo" }, [h1("A header"), p("Some text")]);

Using this we can build arbitrarily complex HTML. As an example we
will build a simple view for a counter in our counter-application.

  1. import { elements, runComponent } from "@funkia/turbine";
  2. const { br, div, button } = elements;
  3. // Counter
  4. const counterView = div(["Counter ", 1, " ", button("+"), " ", button("-")]);
  5. runComponent("body", counterView);

We define counterView as div-element with some text and two buttons
inside. Since div returns a component counterView is a component.
And a Turbine application is just a component so we have a complete
application. We run the application on the last line when we call
runComponent. It is an impure function that takes a selector, a
component and runs the component with the found element as parent. You
can view the entire code in version1.ts.

Dynamic HTML

The counterView above is completely static. The buttons do nothing
and we hard-coded the value 1 into the view. Our next task is to
make the program interactive.

Anywhere where we can give the element functions a constant value of a
certain type we can alternatively give them a behavior with a value of
that type. For instance, if we have a string-valued behavior we can
use it like this

  1. const mySpan = span(stringBehavior);

This will construct a component representing a span element with text
content that is kept up to date with the value of the behavior.

To make the count in our counter view dynamic we turn it into a
function that takes a behavior of a number and inserts it into the
view.

  1. const counterView = ({ count }: CounterViewInput) =>
  2. div(["Counter ", count, " ", button("+"), " ", button("-")]);

Because it will be easier going forward counterView takes an object
with a count property.

Output from components

The above covers the input to the counter view. We now need to get
output from it.

Remember that we mentioned how a Turbine component is a description
about what the component will behave and look like. Part of that
description also explains what output will come from the component.

To get a feel for what “output” means it may be helpful to mention a
few examples.

  • A button outputs, among other things, a stream of click events. So
    part of its output is a stream of the type Stream<ClickEvent>>.
  • An input box’s output includes a behavior of the text inside the
    input. The type would be Behavior<string>.
  • A checkbox might output a behavior representing whether it is
    checked or not. It would have type Behavior<boolean>.

One way of looking at the output is that it is the information we
would like to get from the view.

In practice a component will almost always output more than a single
stream or behavior. By convention the output is therefore almost alway
an object.

Components are represented by a generic type Component<O, A>. The
A represents the available output of the component and the O
represents the selected out of the component. The difference
between selected and available output is highlighted in the example
below.

Constructing an input element looks like this

  1. const usernameInput = input({ placeholder: "Username" });

The type of the component constructed above is as follows ( the ...
refer to the fact that we have omitted a lot of the output to keep
things simple).

  1. Component<{}, { value: Behavior<string>, click: Stream<ClickEvent>, ... }>

Among its available output an input element produces a string valued
behavior named value that contains the current content of the
input element.

Like this input component a newly constructed component always have
{} as its selected output. This means that initially no output is
selected. We can move output from the available output into the
selected output by using the output method on components.

  1. const usernameInput = input({
  2. attrs: { placeholder: "Username" }
  3. }).output({ username: "value" });

Here usernameInput has the type

  1. Component<{ username: Behavior<string> }, ...>

In the above code the invocation to output means: from the object of
available output take the value property and add it to the object of
selected output with the property name username.

The difference between available output and selected output matters
when components are combined. In most cases, when components are
composed or combined all their available output is discarded and only
the selected output becomes part of the combined component.

For instance, in the code below the div is given two children.

  1. div([
  2. button("Click me").output({ firstButtonClick: "click" }),
  3. button("Don't click me")
  4. ]);

The div element composes the two buttons. When doing so all output
from the buttons except for the click stream from the first button
is discarded.

Using the output method is a bit like adding event handlers in other
UI frameworks. There are many events that one can add handlers to but
on any given element only a few events are actually of interest and
for these one will add event handlers. Similarly, in Turbine
components have a lot of available output but only the piece of it
that gets selected will be output in the end.

Back to the counters app. We want our counter view to produce two
streams as output. One stream should be from whenever the first button
is clicked and the other stream should contain clicks from the second
button. That is, the view’s output should have the type

  1. {
  2. incrementClick: Stream<ClickEvent>,
  3. decrementClick: Stream<ClickEvent>
  4. }

We can achieve that by using the output method in each button.

  1. const counterView = ({ count }) =>
  2. div([
  3. "Counter ",
  4. count,
  5. " ",
  6. button("+").output({ incrementClick: "click" }),
  7. " ",
  8. button("-").output({ decrementClick: "click" })
  9. ]);

The call to output on each button tells them what output we are
interested in. The first buttons selected output is then object with a
stream named incrementClick and the later and object with one named
decrementClick.

The div function then combines the selected output from the
components in the array passed to it and output that as its own
selected output. The result is that counterView returns a component
that produces two streams as its output.

An analogy with promises

As mentioned above using the output method is a bit like adding
event listeners in other frameworks. However, there are fundamental
differences between the two things. If you are familiar with how
asynchronous functions that takes callbacks differ from asynchronous
function that returns promises then the following analogy may help
understand this difference.

An asynchronous function for reading a file may look like this

  1. readFileCallback("foo.txt", (file) => ...)

A similar function based on promises looks like
this.

  1. readFilePromise("foo.txt").then((file) => ...)

Notice that the readFileCallback function does not return the file
that it reads. The file is instead passed to a callback that it gets
as an argument. The readFilePromise function on the other hand
returns the file wrapped in a promise of the type Promise<File>.

Most UI frameworks are similar to the readFileCallback function. In
order to know when a button is pressed you do something like this.

  1. <button onClick={(clickEvent) => ...}>Click me</button>

The click events on the button are not returned from the button
function. Instead they are passed to a callback (or event handler)
that the button function gets as an argumen.

The same thing in Turbine looks like this.

  1. button("Click me").output({ click: "click" });

This is similar to the readFilePromise function. The button
function does not take any callbacks but returns a stream of clicks
wrapped in a component of the type Component<{ click: Stream<ClickEvent> }, ...>.

This example should give some intuition about how Turbine differs from
most other frameworks. Other frameworks handle events similar to doing
asynchronous computations with callbacks but Turbine handle events
similarly to doing asynchronous computations with promises. In
particular when creating components the output is returned as part
of the component.

Adding a model

We now need to add a model with some logic to our counter view. The
model needs to handle the increment and decrement stream and turn them
into a behavior that represents the current count.

Turbine offers the function modelView for creating components with
logic. modelView takes two arguments. The first describes the logic
and the second the view. This keeps the logic neatly separated from
the view.

The second argument to modelView, the view, is a function that
returns a component. We already have such a function: counterView.

The first argument is a function that returns a Now-computation. You
don’t have to fully understand Now. One of the things it does is to
make it possible to create stateful behaviors. The model function will
as input receive the output from the component that the view function
returns. The result of the Now-computation will be passed on to the
view function and will be the output of the component that modelView
returns. Here is how we use to create our counter component.

  1. function* counterModel({ incrementClick, decrementClick }: CounterModelInput) {
  2. const increment = incrementClick.mapTo(1);
  3. const decrement = decrementClick.mapTo(-1);
  4. const changes = combine(increment, decrement);
  5. const count = yield accum((n, m) => n + m, 0, changes);
  6. return { count };
  7. }
  8. const counter = modelView(counterModel, counterView)();

Note that there is a cyclic dependency between the model and the view.
The figure below illustrates this.

modelView figure

We now have a fully functional counter. You have now seen how to
create a simple component with encapsulated state and logic. The
current code can be seen in version2.ts.

Creating a list of counters

Our next step is to create a list of counters. To do that we will
create a new component called counterList. The component will
contain a list of counter components as well as a button for adding
counters to the list.

Let’s begin by defining a view function that creates a header and a
button.

  1. function* counterListView() {
  2. yield h1("Counters");
  3. const { click: addCounter } = yield button(
  4. { class: "btn btn-primary" },
  5. "Add counter"
  6. );
  7. return { addCounter };
  8. }

We hook the view up to a model using modelView. Again, the model
function receives the return value from the view function.

  1. const counterList = modelView(counterListModel, counterListView);
  2. const counterListModel = fgo(function*({ addCounter, listOut }) {
  3. const nextId = yield scan(add, 2, addCounter.mapTo(1));
  4. const appendCounterFn = map(
  5. (id) => (ids: number[]) => ids.concat([id]),
  6. nextId
  7. );
  8. const counterIds = yield accum(apply, [0], appendCounterFn);
  9. return { counterIds };
  10. });
  11. const counterListView = ({ sum, counterIds }) => [
  12. h1("Counters"),
  13. button({ class: "btn btn-primary" }, "Add counter").output({
  14. addCounter: "click"
  15. }),
  16. ul(list(counter, counterIds).output((o) => ({ listOut: o })))
  17. ];
  18. const counterList = modelView(counterListModel, counterListView);

To create a dynamic list of counters we have to use the list function.

Documentation

Understanding generator functions

Turbine’s use of generator functions may seem a bit puzzling at first.
For instance, it may seem like generator functions serve two different
purposes. One when they’re used in the model and another when they’re
used in the view

But, what they do under the hood is exactly the same in both cases.
The key to understand is that generator functions is just sugar for
calling chain several times in succession.

When we use chain on components we can combine elements and pipe
output from one component into the next. The code below combines two
input elements with a span element that shows the concatenation of
the text in the two input fields.

  1. input({ attrs: { placeholder: "foo" } }).chain(({ value: aValue }) =>
  2. input().chain(({ value: bValue }) => {
  3. const concatenated = lift((a, b) => a + b, aValue, bValue);
  4. return span(["Concatenated text: ", concatenated]).mapTo({ concatenated });
  5. })
  6. );

However, the above code is very awkward as each invocation of chain
adds an extra layer of nesting. To solve this problem we use
generators.

  1. go(function*() {
  2. const { value: aValue } = yield input();
  3. const { value: bValue } = yield input();
  4. const concatenated = lift((a, b) => a + b, aValue, bValue);
  5. yield span(["Concatenated text: ", concatenated]);
  6. return { concatenated };
  7. });

The above code does exactly the same as the previous example. But it
is a lot easier to read!

The go function works like this. We yield a value with a chain
method. go then calls chain on the yielded value. go calls
chain with a function that continues the generator function with the
value that chain passes it. The end result is a value of the same
type that we yield inside the generator function. When we yield a
Component<A> we will get an A back inside the generator function.

Finally we return a value and that value will be the output of the
component that go returns.

Here is another example. The following code uses chain explicitly.

  1. const view = button("Accept").chain(({ click: acceptClick }) =>
  2. button("Reject").map(({ click: rejectClick }) => ({
  3. acceptClick,
  4. rejectClick
  5. }))
  6. );

The above code is equivalent to the following.

  1. const view = go(function*() {
  2. const { click: acceptClick } = yield button("Accept");
  3. const { click: rejectClick } = yield button("Reject");
  4. return { acceptClick, rejectClick };
  5. });

Again, the code that uses generator functions is a lot easier to read.
This is why they’re useful in Turbine.

Component is not the only type in Turbine that has a chain method.
Now and Behavior does as well. And since go is only sugar for
calling chain it works with these types as well.

API

The API documentation is incomplete. See also the
examples, the tutorial, the Hareactive
documentation
and this tutorial
about IO.

Component

Component#map

Mapping over a component is a way of applying a function to the output
of a component. If a component has output of type A then we can map
a function from A to B over the component and get a new component
whose output is of type B.

In the example below input creates a component with an object as
output. The object contains a behavior named value. The
function given to map receives the output from the component.

We then call map on the behavior value and take the length of
the string. The result is that usernameInput has the type
Component<Behavior<number>> because it’s mapped output is a
number-valued behavior whose value is the current length of the text
in the input element.

  1. const usernameInput = input({ class: "form-control" }).map((output) =>
  2. output.value.map((s) => s.length)
  3. );

Component#chain

map makes it possible to transform and change the output from a
component. However, it does not make it possible to take output from
one component and pipe it into another component. That is where
chain enters the picture. The type of the chain method is as
follows.

  1. chain((output: Output) => Component<NewOutput>): Component<NewOutput>;

The chain method on a components with output Output takes a
function that takes Output as argument and returns a new component.
Here is an example. An invocation component.chain(fn) returns a new
component that works like this:

  • The output from component is passed to fn.
  • fn returns a new component, let’s call it component2
  • The DOM-elements from component and component2 are both added to
    the parent.
  • The output is the output from component2.

Here is an example.

  1. input().chain((inputOutput) => span(inputOutput.value));

The above example boils down to this:

  1. Create input component Create span component with text content
  2. input().chain((inputOutput) => span(inputOutput.value));
  3. Output from input-element Behavior of text in input-element

The result is an input element followed by a span element. When
something is written in the input the text in the span element is
updated accordingly.

loop

Sometimes situations arise where there is a cyclic dependency between
two components.

For instance, you may have a function that creates a component that
shows the value of an input string-value behavior and outputs a
string-valued behavior.

  1. const myComponent = (b: Behavior<string>) => span(b).chain((_) => input());

Now we’d have a cyclic dependency if we wanted to construct two of
these views so that the first showed the output from the second and
the second showed output from the first. With loop we can do it like
this:

  1. loop(({ output1, output2 }) =>
  2. go(function*() {
  3. const output1_ = yield myComponent(output2);
  4. const output2_ = yield myComponent(output1);
  5. return { output1: output1_, output2: output2_ };
  6. })
  7. );

The loop functional seems pretty magical. It has the following
signature (slightly simplified):

  1. loop<A extends ReactiveObject>(f: (a: A) => Component<A>): Component<A>

I.e. loop takes a function that returns a component whose output has
the same type as the argument to the function. loop then passes the
output in as argument to the function. That is, f will as argument
receive the output from the component it returns. The only restriction
is that the output from the component must be an object with streams
and/or behaviors as values.

Visually it looks like this.

loop figure

modelView

The modelView functions makes it possible to create components where
the view is decoupled from the model and its logic.

modelView takes two arguments:

  • The model which is a function that returns a Now computation. The
    Now computation is run when the component is being created.
  • The view which is a function that returns a Component.

modelView establishes a circular dependency between the model and
the view. The model returns a Now computation and the result of this
computation is passed into the view function. The view function then
returns a component. The output of the component is passed to the
model function.

Visually the circular dependency looks like this.

modelView figure

modelView returns a function that returns a component. The
arguments given to this function will be passed along to both the
model and the view functions. This makes it easy to create components
that take input.

  1. const myComponent = modelView(
  2. (outputFromView, arg1, arg2) => ...,
  3. (outputFromModel, arg1, arg2) => ...
  4. );
  5. myComponent("foo", "bar");

list

The list function is used to create dynamic lists in the UI.

Note: If you are familiar with frameworks like Angular or Vue then you can
think of list as being similar to ngRepeat in Angular 1, ngFor in
Angular 2, and v-for in Vue.

The list function has the following type.

  1. function list<A, O>(
  2. componentCreator: (a: A) => Component<O, any>,
  3. listB: Behavior<A[]>,
  4. getKey: (a: A, index: number) => number | string = id
  5. ): Component<{}, Behavior<O[]>>;

The first parameter, componentCreator, is a function that takes a value of
type A and returns a component. This function will be invoked to create the
elements of the dynamic list. The second argument, listB, is a behavior of an
array where the elements in the array are of some type A.

The list function will return a component that at any given point is time is
equivalent to applying componentCreator to the current array in listB and
then showing the resulting components one after another.

Whenever listB changes the component returned by list will react to those
changes and keep the displayed list up-to-date. To do this, the last argument,
the getKey function, is used to figure out how elements are moved, removed, or
added. Therefore getKey should return a value that is unique for each element.

The following example illustrates the above. Let us say we have a list of users
where each user is an object with an id and a username:

  1. type User = {
  2. id: number;
  3. username: string;
  4. };

The current list of users is represented by a behavior users: Behavior<User[]>. We want to display the users in a list with their username
being editable. This can be achieved with the list function.

  1. list((user) => input({ value: user.username }), users, (user) => user.id);

If the users behavior starts out with the value

  1. [{ username: "foo", id: 1 }, { username: "bar", id: 2 }];

Then the component created by calling list will produce HTML like this

  1. <input value="foo" /> <input value="bar" />

Now, if the value of users changes into

  1. [
  2. { username: "baz", id: 3}
  3. { username: "bar", id: 2 }
  4. { username: "foo", id: 1 },
  5. ]

Then list will reorder the two existing input elements and insert a new
input element in the beginning. Thanks to the getKey function list can
efficiently do this by applying getKey to the old and the current value of the
list and figure out how the elements have moved around.

SVG

You can use embed SVG in Turbine in much the same way you’d embed it in HTML:

  1. svg({ height: "100", width: "100" }, [
  2. circle({
  3. cx: "50",
  4. cy: "50",
  5. r: "40",
  6. fill: "red"
  7. }),
  8. svgText({ x: 100, y: 30 }, "Hello SVG!")
  9. ]);

The only element with a different name is svgText because text in Turbine is an HTML Text Node.

Contributing

Turbine is developed by Funkia. We write functional libraries. You can
be a part of it too. Share your feedback and ideas. We also love PRs.

Run tests once with the below command. It will additionally generate
an HTML coverage report in ./coverage.

  1. npm test

Continuously run the tests with

  1. npm run test-watch