Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation website #83

Merged
merged 11 commits into from
Apr 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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