Skip to content

Commit

Permalink
Documentation website (#83)
Browse files Browse the repository at this point in the history
* Add getting started page

* Add installation guide

* Hello, World!

* Force install wasm-bindgen-cli

* Add template! and reactivity docs

* Add Netlify.toml

* Fix config file name

* Use rewrite instead of redirect

* Add highlight.js

* Add some styling

* Add more docs
  • Loading branch information
lukechu10 authored Apr 4, 2021
1 parent 274e0ae commit a28ee7b
Show file tree
Hide file tree
Showing 20 changed files with 580 additions and 255 deletions.
222 changes: 6 additions & 216 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,227 +1,17 @@
# maple
# Maple

[![Crates.io](https://img.shields.io/crates/v/maple-core)](https://crates.io/crates/maple-core)
[![docs.rs](https://img.shields.io/docsrs/maple-core?color=blue&label=docs.rs)](https://docs.rs/maple-core)
[![GitHub contributors](https://img.shields.io/github/contributors/lukechu10/maple)](https://github.com/lukechu10/maple/graphs/contributors)
[![Discord](https://img.shields.io/discord/820400041332179004?label=discord)](https://discord.gg/vDwFUmm6mU)

A VDOM-less web library with fine-grained reactivity.
## What is Maple

## Getting started
Maple is a modern VDOM-less web library with fine-grained reactivity with [Rust](https://www.rust-lang.org/) and [WebAssembly](https://webassembly.org/).

The recommended build tool is [Trunk](https://trunkrs.dev/).
Start by adding `maple-core` to your `Cargo.toml`:

```toml
maple-core = "0.4.3"
```

Add the following to your `src/main.rs` file:

```rust
use maple_core::prelude::*;

fn main() {
let root = template! {
p {
"Hello World!"
}
};

render(|| root);
}
```

That's it! There's your hello world program using `maple`. To run the app, simply run `trunk serve --open` and see the result in your web browser.

## The `template!` macro

`maple` uses the `template!` macro as an ergonomic way to create complex user interfaces.

```rust
// You can create nested elements.
template! {
div {
p {
span { "Hello " }
strong { "World!" }
}
}
};

// Attributes (including classes and ids) can also be specified.
template! {
p(class="my-class", id="my-paragraph", aria-label="My paragraph")
};

template! {
button(disabled="true") {
"My button"
}
}

// Events are attached using the `on:*` directive.
template! {
button(on:click=|_| { /* do something */ }) {
"Click me"
}
}
```

## Reactivity

Instead of relying on a Virtual DOM (VDOM), `maple` uses fine-grained reactivity to keep the DOM and state in sync.
In fact, the reactivity part of `maple` can be used on its own without the DOM rendering part.

Reactivity is based on reactive primitives. Here is an example:

```rust
use maple_core::prelude::*;
let state = Signal::new(0); // create an atom with an initial value of 0
```

If you are familiar with [React](https://reactjs.org/) hooks, this will immediately seem familiar to you.

Now, to access the state, we call the `.get()` method on `state` like this:

```rust
println!("The state is: {}", state.get()); // prints "The state is: 0"
```

To update the state, we call the `.set(...)` method on `state`:

```rust
state.set(1);
println!("The state is: {}", state.get()); // should now print "The state is: 1"
```

Why would this be useful? It's useful because it provides a way to easily be notified of any state changes.
For example, say we wanted to print out every state change. This can easily be accomplished like so:

```rust
let state = Signal::new(0);

create_effect(cloned!((state) => move || {
println!("The state changed. New value: {}", state.get());
})); // prints "The state changed. New value: 0" (note that the effect is always executed at least 1 regardless of state changes)

state.set(1); // prints "The state changed. New value: 1"
state.set(2); // prints "The state changed. New value: 2"
state.set(3); // prints "The state changed. New value: 3"
```

How does the `create_effect(...)` function know to execute the closure every time the state changes?
Calling `create_effect` creates a new _"reactivity scope"_ and calling `state.get()` inside this scope adds itself as a _dependency_.
Now, when `state.set(...)` is called, it automatically calls all its _dependents_, in this case, `state` as it was called inside the closure.

> #### What's that `cloned!` macro doing?
>
> The `cloned!` macro is an utility macro for cloning the variables into the following expression. The previous `create_effect` function call could very well have been written as:
>
> ```rust
> create_effect({
> let state = state.clone();
> move || {
> println!("The state changed. New value: {}", state.get());
> }
> }));
> ```
>
> This is ultimately just a workaround until something happens in [Rust RFC #2407](https://github.com/rust-lang/rfcs/issues/2407).
We can also easily create a derived state using `create_memo(...)` which is really just an ergonomic wrapper around `create_effect`:
```rust
let state = Signal::new(0);
let double = create_memo(cloned!((state) => move || *state.get() * 2));
assert_eq!(*double.get(), 0);
state.set(1);
assert_eq!(*double.get(), 2);
```
`create_memo(...)` automatically recomputes the derived value when any of its dependencies change.

Now that you understand `maple`'s reactivity system, we can look at how to use this to update the DOM.

### Using reactivity with DOM updates

Reactivity is automatically built-in into the `template!` macro. Say we have the following code:

```rust
use maple_core::prelude::*;

let state = Signal::new(0);

let root = template! {
p {
(state.get())
}
}
```

This will expand to something approximately like:

```rust
use maple_core::prelude::*;
use maple_core::internal;

let state = Signal::new(0);

let root = {
let element = internal::element(p);
let text = internal::text(String::new() /* placeholder */);
create_effect(move || {
// update text when state changes
text.set_text_content(Some(&state.get()));
});

internal::append(&element, &text);

element
}
```

If we call `state.set(...)` somewhere else in our code, the text content will automatically be updated!

## Components

Components in `maple` are simply functions that return `HtmlElement`.
They receive their props through function arguments.

For components to automatically react to prop changes, they should accept a prop with type `StateHandle<T>` and call the function in the `template!` to subscribe to the state.

Getting a `StateHandle<T>` for a `Signal<T>` is easy. Just call the `.handle()` method.

Here is an example of a simple component:

```rust
// This is temporary and will later be removed.
// Currently, the template! macro assumes that all components start with an uppercase character.
#![allow(non_snake_case)]

use maple_core::prelude::*;

fn Component(value: StateHandle<i32>) -> TemplateResult {
template! {
div(class="my-component") {
"Value: " (value.get())
}
}
}

// ...
let state = Signal::new(0);

template! {
Component(state.handle())
}

state.set(1); // automatically updates value in Component
```
Learn more at the [Maple website](https://maple-rs.netlify.app), or stop by the [Discord server](https://discord.gg/vDwFUmm6mU).

## Contributing

Issue reports and PRs are welcome!
Get familiar with the project structure with [ARCHITECTURE.md](https://github.com/lukechu10/maple/blob/master/ARCHITECTURE.md).
Issue reports and Pull Requests are always welcome!
Check out the [section on contributing](https://maple-rs.netlify.app/contributing/architecture) in the docs.
5 changes: 4 additions & 1 deletion docs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ console_error_panic_hook = "0.1.6"
console_log = "0.2.0"
log = "0.4.14"
maple-core = {path = "../maple-core"}
pulldown-cmark = "0.8"
reqwest = "0.11"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"

[dependencies.web-sys]
features = ["HtmlInputElement", "InputEvent"]
features = ["HtmlInputElement", "InputEvent", "Location"]
version = "0.3"
41 changes: 41 additions & 0 deletions docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,53 @@
<title>Maple</title>

<link data-trunk rel="scss" href="index.scss" />
<link data-trunk rel="copy-dir" href="markdown" />

<link
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl"
crossorigin="anonymous"
/>
<script
src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
integrity="sha384-JEW9xMcG8R+pH31jmWH6WWP0WintQrMb4s7ZOdauHnUtxwoG2vI5DkLtS3qm9Ekf"
crossorigin="anonymous"
></script>
<script
defer
src="https://use.fontawesome.com/releases/v5.0.13/js/solid.js"
integrity="sha384-tzzSw1/Vo+0N5UhStP3bvwWPq+uvzCMfrN1fEFe+xBmv1C/AtVX5K0uZtmcHitFZ"
crossorigin="anonymous"
></script>
<script
defer
src="https://use.fontawesome.com/releases/v5.0.13/js/fontawesome.js"
integrity="sha384-6OIrr52G08NpOFSZdxxz1xdNSndlD4vdcf/q2myIUVO0VsqaGHJsB0RaBE01VTOY"
crossorigin="anonymous"
></script>

<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.2/styles/tomorrow.min.css"
integrity="sha512-r3qr7vMdHYmWUzqC7l4H/W03ikAyiFIQld+B71yAmHhu7wcsnvm/adhikM+FzDxxK9ewznhCVnAm+yYZpPBnSg=="
crossorigin="anonymous"
/>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.2/highlight.min.js"
integrity="sha512-s+tOYYcC3Jybgr9mVsdAxsRYlGNq4mlAurOrfNuGMQ/SCofNPu92tjE7YRZCsdEtWL1yGkqk15fU/ark206YTg=="
crossorigin="anonymous"
></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.2/languages/rust.min.js"
integrity="sha512-IAokowDa3HRCVDgsia9en7N6lnBhMVlqx3stsJjKD5tY9fGh0RS/1WU4ftXYIB05WMA/GWcoX/nxER+P5JJFZw=="
crossorigin="anonymous"
></script>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.7.2/languages/xml.min.js"
integrity="sha512-0CjSoD/wLTBYgboRLU6i71o0LK21KZpkyu8bKCVUoP/2OFhYnPxYWqcATFpDtjikyuoxIhCYFgNJeh8w7rggTg=="
crossorigin="anonymous"
></script>
</head>
<body></body>
</html>
13 changes: 13 additions & 0 deletions docs/index.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
blockquote {
background-color: #e9e9e9;
padding: 7px;
border-radius: 4px;
}

pre {
border-radius: 2px;

& code {
background-color: #f9f9f9 !important;
}
}
37 changes: 37 additions & 0 deletions docs/markdown/concepts/components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Components

Components in `maple` are simply functions that return `TemplateResult<G>`.
They receive their props through function arguments.

For components to automatically react to prop changes, they should accept a prop with type `StateHandle<T>` and call the function in the `template!` to subscribe to the state.
A `StateHandle<T>` is just a readonly `Signal<T>`.

Getting a `StateHandle<T>` from a `Signal<T>` is easy. Just call the `.handle()` method.

Here is an example of a simple component that displays the value of its prop:

```rust
// This is temporary and will later be removed.
// Currently, the template! macro assumes that all
// components start with an uppercase character.
#![allow(non_snake_case)]

use maple_core::prelude::*;

fn MyComponent<G: GenericNode>(value: StateHandle<i32>) -> TemplateResult<G> {
template! {
div(class="my-component") {
"Value: " (value.get())
}
}
}

// ...
let state = Signal::new(0);

template! {
MyComponent(state.handle())
}

state.set(1); // automatically updates value in Component
```
5 changes: 5 additions & 0 deletions docs/markdown/concepts/control_flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Control Flow

TODO

Help us out by writing the docs and sending us a PR!
5 changes: 5 additions & 0 deletions docs/markdown/concepts/iteration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Iteration

TODO

Help us out by writing the docs and sending us a PR!
Loading

0 comments on commit a28ee7b

Please sign in to comment.