Simi
A framework for building front-end web app in Rust. Simi
is inspired by Yew
A simple app
Everything from simi-project/simi/examples/a-counter/lib.rs
:
extern crate simi;
extern crate wasm_bindgen;
use *;
use *; // Because simi use wasm-bindgen internally
// This attribute (use `wasm-bindgen` internally) will create
// some boilerplate code to define and export functionality
// to run the app on the JS side.
The simi app
is actually a library
Simi
does not have its own event loop. It is just a library. It must be executed from JS
side, simi build
(a simi-cli
command) will create that portion of code for you.
#[simi_app]
Add #[simi_app]
on the struct or enum that you implement SimiApp
. It will generate glue code (in Rust, using wasm-bindgen
) to allow your app
to be executed from JS. The glue code on the JS side to execute the app is generated by simi-cli
.
By default, #[simi_app]
start your app inside document.body
. If you provide your own index.html
and you want #[simi_app]
to be rendered inside your pre-defined element, give it an element id: #[simi_app(element-id-to-start-the-app-in)]
Virtual DOM, application!
and component!
Similar to other frameworks, Simi
core thing is a virtual DOM. Everything that may change in the future will be tracked in the virtual DOM. On every update, they are check against the current value in the virtual DOM, if it has been changed, both virtual and real DOM will be updated. Everything else that will not be changed, Simi
will not track them, instead, Simi
render them directly to real DOM when the app started. application!
and component!
are responsible for building and updating DOMs. Internally, these two macros have the same implementation, working in two different mode (application mode or component mode). They generated code for building/update DOMs from a simi-style HTML template. Here, we have a closer look at these two macros.
Syntax
application!
Important:
simi
only works with a set of supported html tags. If simi
found out that
an identifier is not an html tag, it will automatically treat it as an expression.
If you believe there are valid html tags that must be supported by simi
please
open an issue (or better: open a pull/merge request).
Text and child content
application!
Attributes
Attributes must be provided in the form of name=value
pair.
Rules for name
:
- An event name:
onclick
,oninput
... - An attribute name:
type
,checked
,id
,class
... - A hyphen-separated-series of identifiers such as
data-my-custom-attribute
. - A special simi-attribute: a
str
literal like"class1"
, this is only use as a class name, see more below - in the Conditional classes section.
Rules for value
:
- A literal:
"class-name"
,"tag-id"
... - A bool literal:
true
,false
- A Rust expression:
self.get_name()
- A
Message
variant:Msg::SendMessage
,Msg::SetSomething(some_value)
,Msg::ButtonClick(?)
(for events only)
application!
Conditional classes
A str
literal that is used in place of attribute name
will be treated as a class
. Its value will be the condition to add/remove itself to/from the real element.
let some_name = "my-class";
application!
Events
Currently, Simi
only support 3 types of element events:
application!
if ... else ...
and match
application!
and component!
support if else
and match
:
application!
When rendering a new if else
/match
arm, all nodes (both virtual and real DOM) of the previous rendered-arm will be removed from their DOM, then the new arm will be created. So, you should minimize things in if else
/match
construct.
A special case is if
or if else if
... (without the final else
). Simi will create an empty <span class="simi-empty-span-element-use-in-place-of-omitted-else">
as the content of the omitted else
. If you want everything in your control, you must create the phantom-element yourself.
for
loop
application!
and component!
supports for
loop. Unlike if else
or match
, for
only supports a single item at the root of its body:
application!
Currently, for
loop is implemented with the most naive algorithm: If you have N items, you remove the first item, then Simi
will update the first N-1 current items, then remove the last item. Simi
is not (yet) smart enough to remove just the first item in the real DOM to leave other items as is.
Do not update things with #
application!
and component!
try to avoid building a new virtual DOM on update. It will try to update current virtual DOM, then - if it is required, updates the real DOM.
When updating, Simi
always ignore literal
s. Simi
can also ignore updating an attribute, a content or a whole sub DOM if you want.
application!
Not allow complex Rust code in macros
application!/component!
do not allow a complex handler. The following piece of code will NOT work:
application!
It is better to move it out of the macros. In the case of onclick
above, you should only have a Msg::Clicked(?)
. And do the check for SendMessage
in the update
method.
For other attributes:
// Complex code here, outside of `application!` or ``component!`
let result = match something ;
application!
Component
Component
is a way to reuse code. I personaly do not like the way Component
works right now. But it is my best effort. I don't know how to make it better!
Here is a component extract from simi-project/simi/examples/b-counter-component/src/lib.rs
As shown, a component has two source of data: one from Self::Properties
, the other from the component struct's fields. The Self::Properties
will always be passed down from its parent, but the struct's field is optional. If you decide that the field will never need update, you can prefix it with #
to passed the value of None
down to the component.
And how to use a component (in a simi app, also extracted from examples/b-counter-component/src/lib.rs
):
As you see, the properties of the component is passed down to it by using ()
: Counter(self.value1)
, this will be pass down as a reference
(&
). The component struct's fields are passed down by placing it inside a {}
along with its field's name: field_name: value
. Literals, such as title: "First value: "
will only passed down (as Some(value)
) on the creation of the component. When the component is updating, it receives None
from the parent. It is because literals never change, hence we never need to update it.
Child Component
You can use a component directly in another component, just like using it in application!
in previous section. But simi also support a more flexible use case. A component can have a child-placeholder
, then when using the component, you can pass in any component to be rendered at the preserved-place. See in the full example here.
You define a component that has two child-components:
Then, in the application:
application!
UbuComponent (Update-by-user Component)
A Simi app change its state in SimiApp
trait's update
method. If update
return true
, then the framework execute render
(it's is all so a SimiApp
method). render
will check the whole DOM on every run. In some cases, your app have a region that update frequently, for example, a clock must update every second, but the rest of your app only update on user's interaction. It's very nice if you can control your clock seperately. This is where to use UbuComponent
. See the full example code here
Sub App
A similar use case as UbuComponent
, but it has its own data state and messages to process? You may want organize your app as a simi-multi-app.
Sub App: If your app is big? And the app state can be split into smaller and disjoint group? You may want to implement your app as a multi-app. div() [sub_app:clock]