An IO monad for Svelte. Fantasy Land compatible.
Disclaimer: This package, as written, has not been actually used in any practical way. It very well may not work the way you see it today. The documentation below also may very well not work as written. If this concept is interesting to you, but you find that the package is not working, please reach out and we'll get something sorted out!
Not currently published to NPM, so install from Github:
npm i -S github:foxfriends/svelte-io
For obvious reasons, svelte ^3.39
is also a (peer) dependency.
For a significant number of cases, usage of this package should come quite naturally. Such usage will be covered first.
<!-- Hello.svelte -->
<script>
export let name;
</script>
Hello {name}!
<!-- App.svelte -->
<script>
import { onMount } from 'svelte';
import Driver, { render } from 'svelte-io';
import Hello from './Hello.svelte';
let driver;
onMount(() => {
driver.run(render(Hello, { name: 'World' }));
});
</script>
<Driver bind:this={driver} />
This example shows the most fundamental parts of this package:
Driver
represents where in the Svelte component hierarchy each SvelteIO
will be run.SvelteIO
to driver.run()
, and it will be run by that driver.render(Component, props)
is a built-in SvelteIO
constructor that renders a component from the position of the Driver
.For those who want TypeScript types, and are finding they are missing from the package, here they are:
render(Component: SvelteComponent, props?: Record<string, any>): SvelteIO
driver.run(io: SvelteIO<T>) -> Promise<T>
Of course, the ability to render a single component is not all that exciting. What is exciting about
SvelteIO
is that these SvelteIO
values are composable.
<!-- App.svelte -->
<script>
import { onMount } from 'svelte';
import Driver, { doIO, render } from 'svelte-io';
import Hello from './Hello.svelte';
import GetName from './GetName.svelte';
let driver;
onMount(() => {
const main = doIO(async function* () {
const name = yield render(GetName);
yield render(Hello, { name });
})
driver.run(main);
});
</script>
<Driver bind:this={driver} />
In this example, doIO(generator: AsyncGenerator): SvelteIO<T>
is used to construct a more
complex SvelteIO
by transforming a generator that itself yields other SvelteIO
s.
Here, main
is a SvelteIO
that first renders the GetName
component then, using the
output value of GetName
, renders the Hello
as before.
Different than in regular Svelte, SvelteIO
components are generally expected to be displayed,
interacted with, and then be removed when its job is done, outputting some final value. Let's
take a look at how GetName
is implemented:
<!-- GetName.svelte -->
<script>
import { useSvelteIO } from 'svelte-io';
const { next } = useSvelteIO();
let name = '';
</script>
<input bind:value={name} />
<button on:click={() => next(name)}>
Submit
</button>
SvelteIO
components call useSvelteIO()
to get access to a function next(value: T)
.
This next(value)
function is the one that causes the generator from which the component
was yield
ed to continue, very much like you called the .next()
function on that
generator. Similarly, the value passed to this next(value)
is the one that gets returned
from the yield
statement!
There are a bunch more things you can do from SvelteIO: pretty much anything that you can do with
a regular Svelte component, including accessing context (getContext(key: any): SvelteIO<T>
),
accessing the props passed to the <Driver>
(getProp(prop: string): SvelteIO<T>
), and
dispatching events from the <Driver>
(dispatch(event: string, detail?: any): SvelteIO<void>
).
Check out all the details in the API reference.
As mentioned in the header, SvelteIO
is actually implemented as a monad (as defined
by Fantasy Land). For those versed in Haskell, doIO
is the closest thing we can get to
do
-notation for Javascript, providing the intuitive interface seen above.
Given this knowledge, it should become apparent that all the usual things you can do to monads
apply to SvelteIO
s. The example of main
from above could have equivalently been written
using monad operators as follows:
import { chain } from 'fantasy-land';
const main = render(GetName)[chain]((name) => render(Hello, { name }));
Or, since Ramda is also Fantasy Land compatible:
import { chain } from 'ramda';
const main = chain((name) => render(Hello, { name }), render(GetName));
Of course, this example does not show a great reason to use this syntax over that of doIO
,
but if you are using this library in situations advanced enough that you need this level of
power, you likely know what you need anyway.
Something I have not tried, but would like to believe works, is using this Monad as the base monad of other monad transforms (such as those from Akh) to further add to their flexibility.
Suggestions and improvements are welcome. Feel free to create issues, pull requests, or just message me somewhere to discuss anything.
For those of us still stuck working with React, a similar package Monadic React exists. Though it takes a different philosophy to the features it provides, it is built on the same concept.
Another (React world) package which can be used to achieve similar separation of concerns of data flow from component rendering is Redux-Saga.