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::Value models.
  • 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.
  • LinkTo creates the correct href for @route="index", and supports a new @model param.
  • All HTMLbars grammar rules now have human-readable parse error messages.

What's Next?

Take a peek at the 0.2.4 Roadmap.


©2026 Megalithic LLC | Website | Guide | GitLab | Contact