Stimulus Discourse

Adding Models to the architecture

Hi all — I’m absolutely loving Stimulus and it’s matching up well with how my brain works and my past Rails experience. My question is this: how have you approached (if at all) the idea of adding client-side models to your frontend architecture and how that could work with your Stimulus controllers?

For example, I have a controller that handles two-way UI communication for a “post” on a timeline. When someone clicks a button on a post to, say, save that for reading later, I set it up so my Stimulus controller instantiates and calls a model object, and the model object is responsible for sending the relevant data up to the server. I like the feel of keeping data transformation and persistance in a separate layer than the Stimulus controller — that way I have true MVC on the frontend.

Right now, my model is just a POJO (loving those ES6 classes), and I’m not doing anything fancy like autoloading models. So that’s fine, but I guess I’m wishing there was a bit more framework magic under the hood so each model isn’t just a individualized snowflake.

Any thoughts? Other approaches?

Jared,

(disclosure: I’m bad at Javascripting; don’t use this code as a model.)

  • To keep a JS controller simple, I extracted logic in a POJO (ProductCatalogue) that is imported.
  • The POJO was initialized with DB data stored temporarily in the DOM

The ruby controller stores the products(records) details in an instance variable

  def show
     @products_details_json = Product.all.details.to_json
  end

View renderer stores the info as json in a data attribute

    ..
    data:    { 
        controller: 'foobar',
        'foobar-product-details' => @products_details_json&.html_safe,
    } ...

The Stimulus controller grabs the info from the data attribute and buils a ProductCatalogue object

import {ProductCatalogue} from "./product_catalogue";

export default class extends Controller {
    connect() {
        this.catalogue = new ProductCatalogue(this.data.get("product-details"))
    }

export class ProductCatalogue {
    //data_as_json example:
    //  {"product_53":{"cost":2.19,"pail_size":"25.0","available_quantity":null},
    //   "product_63":{"cost":3.12,"pail_size":"20.0","available_quantity":null},
    //   ... }
    constructor(data_as_json) {
        this.data = JSON.parse(data_as_json);
    }

    static empty_details = {"cost": null, "available_quantity": null}

    find_product_details(product_id, color_id) {
        const product_key = `product_${product_id}`,
            color_key   = `color_${color_id}`;

        const product_data = this.data[product_key]
        if (product_data === undefined) {
            return ProductCatalogue.empty_details
        }

        const specific_data = product_data[color_key];
        return specific_data  || product_data;
    }
}
4 Likes

Nice! I like several aspects of this pattern, particularly the way to load the Javascript data into the model.

How about using a state object like this?

import { Controller } from "stimulus"

export default class extends Controller {
  initialize() {
    this.state = {};
  }

  connect() {
    this.setState(Object.assign({}, this.state, JSON.parse(this.data.get("post")));
  }

  setState(newState) {
    this.state=  newState;
  }

 changePostTitle(e) {
   this.setState(Object.assign({}, this.state, {title: e.target.value});
 }
}

In your view something like this:

<form action="whatever" data-controller="post">
<input type="text" name="post[title]" data-action="change->post#changePostTitle" />
</div>