Release 0.2.3 - 2026-10-17
This is the biggest Auric release yet. It introduces controllers with tracked properties and event handlers,
conditional content in templates, dynamic class attributes, and the built-in array and concat helpers.
On the data side, it adds a JSON:API adapter, Store::save(), and dirty tracking for models.
Read the full change log here.
Controllers
A controller holds the business logic for a route, such as event handlers and tracked values. Generate one with the CLI:
$ auric generate controller customers/new
created src/controllers/customers/new_controller.rs
updated src/controllers/customers/mod.rs
updated src/controllers/mod.rs
Add a tracked value by decorating a field with #[tracked], and add an event handler to the #[controller]
impl block:
#[derive(AuricController, Default)]
pub struct CustomersNewController {
#[tracked]
counter: u16,
}
#[controller]
impl CustomersNewController {
pub fn increment(&mut self) {
self.counter += 1;
}
}
Then reference both from the route's template:
{{!-- src/templates/customers/new.hbs --}}
<h2>Counter Example</h2>
Counter: {{this.counter}}
<button class="btn btn-default" {{on "click" this.increment}}>
Increment
</button>
When you build, the tracked field becomes a Leptos signal, and the {{on "click" ...}} modifier becomes an
on:click handler that moves the value into the controller, calls your function, and moves the result back
out through generated accessor functions:
let (counter, set_counter) = signal::<u16>(Default::default()); // tracked from controller
view! {
<h2>"Counter Example"</h2>
"Counter: "
{move || counter.get()}
<button
class="btn btn-default"
on:click=move |_| {
controller.set_counter(&counter);
controller.increment();
set_counter.set(*controller.get_counter());
}
>
"Increment"
</button>
}
Because {{this.counter}} and this.increment are bound to real Rust fields and functions, a reference to
a field or function that doesn't exist is a compile error. The Rust compiler checks your templates.
The Auric Guide has a new Routing/Controllers page with more detail.
Conditional content
Templates now support if, both inline and as a block.
The inline form takes a condition, a value if true, and an optional value if false:
The car is {{if this.model.isVeryNice 'very' 'not very'}} nice.
let signal0 = move || model.get().map(|model| model.get("isVeryNice")).unwrap_or_default();
let signal1 = move || {if (signal0()).as_bool().unwrap_or_default() {"very"} else {"not very"}};
view! {
"The car is " {move || signal1()} "."
}
The block form handles nested content, and is generated as a Leptos <Show> component:
The car is
{{#if this.model.isVeryNice}}
<strong>very nice</strong>
{{else}}
nice
{{/if}}.
view! {
"The car is "
<Show when=move || { (&signal0()).as_bool().unwrap_or_default() } fallback=|| view! { "nice" }>
<strong>"very nice"</strong>
</Show>
"."
}
See Templates/Conditional Content in the guide.
Dynamic class attributes
The 0.2.2 release noted that mustaches inside a class attribute were not supported yet. They are now.
A class attribute that mixes static text and mustaches is compiled into a derived signal:
<button class="btn {{if this.model.isVeryNice 'color-highlight' 'color-normal'}}">Click</button>
array and concat helpers
The built-in array and concat helpers join the string, math, and truth helpers that shipped in 0.2.2:
<p>{{sum (array 3 5 7)}}</p>
<p>{{concat this.model.firstName " " this.model.lastName}}</p>
concat joins its parameters into a single string, and array builds a JSON array from its parameters, which
you can pass to other helpers.
Data framework
JSON:API adapter
A new JsonApiAdapter lets your application talk to a JSON:API server. Target it when you generate your application adapter:
$ auric generate adapter application --target json-api
Serialization logic now sits behind a new Serializer trait, and the REST-specific logic has moved into a
RestSerializer.
Saving records and dirty tracking
Store::save() creates or updates a record, depending on whether it was loaded from an adapter:
let mut customer: Customer = store.find_record(Bucket::Customers, "123", None).await?;
customer.name = "New Name".to_string();
let customer = store.save(&customer).await?;
Generated models now keep a snapshot of the resource they were loaded from, which makes Ember's isNew and
hasDirtyAttributes available:
let mut customer: Customer = store.find_record(Bucket::Customers, "123", None).await?;
assert!(!customer.is_new());
assert!(!customer.has_dirty_attributes());
customer.credit_limit = 1000;
assert!(customer.has_dirty_attributes());
Fixes
Some notable fixes in this release:
- A failing route
model()fn no longer panics and crashes the app. - Templates can now bind to fields of data framework models, not only to
serde_json::Valuemodels. - Content after
{{outlet}}in the application template now renders below the outlet instead of above it. - The REST adapter now passes query params to the server, and keeps numeric ids numeric.
LinkTocreates the correct href for@route="index", and supports a new@modelparam.- All HTMLbars grammar rules now have human-readable parse error messages.
What's Next?
Take a peek at the 0.2.4 Roadmap.