3 releases
0.1.6 | Feb 20, 2023 |
---|---|
0.1.5 | Feb 14, 2023 |
0.1.4 | Feb 3, 2023 |
#3 in #comet
11KB
328 lines
Comet
Reactive isomorphic rust web framework.
Index
Introduction
Work in progress, this is still an early naive prototype. Don't expect anything to work properly, expect things to break often.
Comet is a framework for the web build with Rust + Wasm <3. It takes its inspiration from MeteorJS, Seed-rs, Yew and others.
This crate aims to be an all-in-one all-inclusive battery-included isomorphic reactive framework.
- You keep saying 'Isomorphic', but why ?
In this context, Isomorphic means that you only write one program for both client and server.
One crate. One. For both. Yes.
This means that we rely a lot on macros and code generation, with all the good and the bad this could bring,
but it allows for a great deal of features, close to no boilerplate, and a little quality of life improvement on different aspects.
- Ok, and how is it reactive then ?
It is reactive in many sense, first by its component
system, that encapsulate little bits of logic into an HTML templating system,
and which can bind your struct's methods directly to JS events, triggering a render of only the components that changed.
There is also a reactive layer on top of a PostgreSQL
database, that permits to watch for some queries to change over time and
to send push notifications over websocket to every client watching for thoses change, triggering a render when needed.
Visit the examples folder.
Features
- Isomorphic client/server
- Reactive view
- Virtual dom
- Client cache
- Reactive database with PostgreSQL
- Auto database generation every time your structs change (Alpha)
- Websocket
- Auto procol generation
- Remote procedure calls
- (Almost) Zero boilerplate
Getting started
Install Comet Binary and dependencies
$> cargo install comet-cli
You will need to install and run an instance of PostgreSQL.
If not found on your system, Comet will install these following crates using cargo install
on the first run:
wasm-pack
diesel-cli
Create a simple incrementing counter
$> comet new my_counter && cd my_counter
There is already the dependency setup in the Cargo.toml:
comet-web = "0.1.6"
This newly generated project contains all you need to get started. Your journey starts with src/main.rs
.
Conveniently, this generated file is already the simpliest incrementing counter you can think of:
use comet::prelude::*;
pub struct Counter {
pub value: i32,
}
component! {
Counter {
button click: self.value += 1 {
self.value
}
}
}
comet::run!(Counter { value: 0 });
Run it
Setup your database address as an env variable
/!\ Warning: This database will be COMPLETELY WIPED at startup and everytime your models change
This is not ideal but, hey ! This is still a work in progress :p
$> export DATABASE_URL="postgres://your_user:your_password@localhost/your_db"
Actually run your project
$> comet run
This will download and install the tools it needs to build and run your crate.
[✓] Installing wasm-pack
[✓] Installing diesel-cli
[✓] Diesel setup
[✓] Migrating database
[✓] Patching schema
[✓] Building client
[✓] Building server
[✓] Running
-> Listening on 0.0.0.0:8080
Then go to http://localhost:8080
Quick tour
- Easy definition of the dom
- Use conditional rendering and loops
- Bind your variables to
input
fields that react to events - Embed your components between them
- Database persistence for free
- Remote procedure calls
- Database queries
- HTML view
- Full chat example
Easy definition of the dom
use comet::prelude::*;
struct MyStruct {
my_value: String,
my_height: u32,
}
component! {
MyStruct {
// Here #my_id defined the id,
// and the dot .class1 and .class2 add some classes to the element
// The #id must always preceed the classes, if any
div #my_id.class1.class2 {
span {
// You can access your context anywhere
self.my_value.as_str()
}
// Define style properties
div style: { height: self.my_height } {
"Another child"
}
}
}
};
Use conditional rendering and loops
use comet::prelude::*;
struct MyComponent {
show: bool,
value: HashMap<String, i32>,
}
component! {
MyComponent {
div {
div {
// Conditional rendering with if
if self.show {
"Visible !"
}
button click: self.show = !self.show {
"Toggle"
}
}
div {
// Use a for-like loop.
for (key, value) in self.value {
div {
key.as_str()
value
}
}
button click: self.value.push(42) {
"Add a number"
}
}
}
}
}
Bind your variables to input
fields that react to events
This is exclusive to input
and select
fields for now
Each binding should be unique, as in a different variable for each one, or you will experience conflicts
use comet::prelude::*;
struct MyStruct {
value: String,
current_id: i32,
}
component! {
MyStruct {
div {
input bind: self.value {}
select bind: self.current_id {
option value: 0 {
"-- Choose a value --"
}
for id in 1..9 {
option value: (id) {
id
}
}
}
self.value.as_str()
self.current_id
}
}
}
Embed your components between them
use comet::prelude::*;
struct Child {
value: String,
}
component! {
Child {
div {
self.value
}
}
}
struct Parent {
// You need to wrap your components with a Shared<T> that is basically an Arc<RwLock<T>>
// This is necessary for your states to persist and be available between each render
child: Shared<Child>,
}
component! {
Parent {
div {
// To include a component, just include it like any other variable
self.child.clone()
}
}
}
Database persistence for free
All the previous examples until now were client-side only. Its time to introduce some persistance.
Deriving with the #[model]
macro gives you access to many default DB methods implemented for your types:
- async Self::fetch(i32) -> Result<T, String>;
- async Self::list() -> Result<Vec<T>, String>;
- async self.save() -> Result<(), String>;
- async Self::delete(i32) -> Result<(), String>;
The String
error type is meant to change into a real error type soon.
You have a way to add your own database query methods, please read Database queries below.
use comet::prelude::*;
// You just have to add this little attribute to your type et voila !
// It will add a field `id: i32` to the struct, for database storing purpose
// Also, when adding/changing a field to this struct, the db will
// automatically update its schema and generate new diesel bindings
#[model]
struct Todo {
title: String,
completed: bool,
}
impl Todo {
pub async fn toggle(&mut self) {
self.completed = !self.completed;
// This will save the model in the db
self.save().await;
}
}
component! {
Todo {
div {
self.id
self.title.as_str()
self.completed
button click: self.toggle().await {
"Toggle"
}
}
}
}
// This will create a new Todo in db every time this program runs
comet::run!(Todo::default().create().await.unwrap());
Remote procedure calls
Note: The structs involved in the #[rpc]
macro MUST be accessible from the root module (i.e. src/main.rs
)
use comet::prelude::*;
// If you have other mods that use `#[rpc]`, you have to import them explicitly
// in the root (assuming this file is the root). This is a limitation that will not last, hopefully
mod other_mod;
use other_mod::OtherComponent;
#[model]
#[derive(Default)]
pub struct Counter {
pub count: i32,
}
// This attribute indicates that all the following methods are to be treated as RPC
// These special methods are only executed server side
// The only difference with the similar method above is that the `self.count +=1` is done server side,
// and the `self` sent back to the client
#[rpc]
impl Counter {
// The RPC methods MUST be async (at least for now)
pub async fn remote_increment(&mut self) {
self.count += 1;
self.save().await;
}
}
component! {
Counter {
button click: self.remote_increment().await {
self.count
}
}
}
comet::run!(Counter::default().create().await.unwrap());
Database queries
The most simple way to define a new database query is with the macro #[sql]
, that uses #[rpc]
underneath.
All your models have been augmented with auto-generated diesel bindings, so you can use a familiar syntax. There will be a way to give raw SQL in the near future.
use comet::prelude::*;
#[model]
#[derive(Default, Debug)]
pub struct Todo {
pub title: String,
pub completed: bool,
}
#[sql]
impl Todo {
// Use the watch macro to get back your data whenever the result set change in DB
// Only valid for select statement for now
#[watch]
pub async fn db_get_all(limit: u16) -> Vec<Todo> {
// The diesel schema has been generated for you
use crate::schema::todos;
// You don't have to actually execute the query, all the machinery
// of creating a db connection and feeding it everywhere have been
// abstracted away so you can concentrate on what matters
todos::table.select(todos::all_columns).limit(limit as i64)
}
}
HTML view
Until now, we always used components to manage our views and logic.
Whenever you define a component using the component!
macro, you define bits of HTML directly inside the macro.
Under the hood, we call the html!
macro that is a lot simpler in term of features.
// You can define basic function that return an HTML
pub async fn my_view(my_arg: MyType) -> Html {
html! {
div {
my_arg.my_property
}
}
}
// Then you can call it from a component, or another existing html view.
component! {
SomeComponent {
div {
my_view(self.some_value).await
}
}
}
Please note that the html!
macro does not support input bindings (bind
) or event bindings (click
, change
),
at least for now.
Full chat example
This is a client/server fully reactive chat room
There is a more elaborate multi-channel chat in the examples folder
use comet::prelude::*;
#[model]
pub struct Message {
pub sender: String,
pub content: String,
}
#[sql]
impl Message {
#[watch]
pub async fn list_watch() -> Vec<Message> {
use crate::schema::messages;
messages::table.select(messages::all_columns)
}
}
component! {
Message {
div {
self.sender.to_owned() + ": " + &self.content
}
}
}
#[derive(Default)]
pub struct App {
pub sender: String,
pub content: String,
}
impl App {
async fn send_message(&mut self) {
let mut message = Message {
id: -1,
sender: self.sender.clone(),
content: self.content.clone(),
};
self.content = "".into();
message.save().await.unwrap();
}
}
component! {
App {
div {
Message::list_watch().await
input bind: self.sender {}
input bind: self.content {}
button click: self.send_message().await {
"Send"
}
}
}
}
comet::run!(App::default());
Todo List
-
Function Component
-
Allow for iterators inside html
-
Have a ComponentId that allows to fetch the corresponding root dom element
-
Find a way for global inter-component message passing
-
Use the cache for non-watched rpc queries (because this cause a lot of traffic on each redraw)
-
Find a way to have a global state
-
Postgres pool and reusable connections
-
Implement ToVirtualNode for Result<T, Error>
-
Add an extensible error system
-
Separate all the reusable features in different crates:
- Comet crate
- The view system
- The html macro
- The component macro
- The isomorphic db model through websocket
- The #[model] proc macro that generates basic model queries
- An abstract ws server/client
- The auto-proto macro
- The reactive/listening part of the db reactive-postgres-rs
- The view system
- Comet crate
Dependencies
~0–9MB
~86K SLoC