An open API service indexing awesome lists of open source software.

https://github.com/artofcodelabs/simplicit

Loco-JS-Core provides a logical structure for JavaScript code
https://github.com/artofcodelabs/simplicit

Last synced: 5 months ago
JSON representation

Loco-JS-Core provides a logical structure for JavaScript code

Awesome Lists containing this project

README

          

# 🧐 What is Simplicit?

Simplicit is a small library for structuring front-end JavaScript around **controllers** and **components**.

On the MVC side, it mirrors the “controller/action” convention you may know from frameworks like [Ruby on Rails](https://rubyonrails.org): based on `` attributes, it finds the corresponding controller and calls its lifecycle hooks and action method.

On the component side, it provides a lightweight runtime (`start()` + `Component`) that instantiates and binds components from `data-component`, builds parent/child relationships, and automatically tears them down when elements are removed from the DOM.

# 🤝 Dependencies

Simplicit relies only on `dompurify` for sanitizing HTML.

# 📲 Installation

```bash
$ npm install --save simplicit
```

# 🎮 Usage

## 🖲️ Components

Simplicit ships with a small component runtime built around DOM attributes.

### ✅ Quick start

```javascript
import { start, Component } from "simplicit";

class Hello extends Component {
static name = "hello";

connect() {
const { input, button, output } = this.refs();
this.on(button, "click", () => {
output.textContent = `Hello ${input.value}!`;
});
}
}

document.addEventListener("DOMContentLoaded", () => {
start({ root: document, components: [Hello] });
});
```

```html



Greet


```

### DOM conventions

* **`data-component=""`**: marks an element as a component root.
* `` must match the component class’ **`static name`**.
* `` tags are never treated as components, even if they have `data-component`.
* **`data-component-id="<id>"`**: set automatically on every element with `data-component` (each component instance).
* Also available as `instance.componentId`.
* **`data-ref="<key>"`**: marks ref elements inside a component (see `ref()` / `refs()`).

### `start({ root, components })`

`start()` scans `root` (defaults to `document.body`) for elements with `data-component`, creates and binds component instances for them, and keeps them in sync with DOM changes (new elements get initialized, removed ones get disconnected).

* **Validation**
* Throws if there are **no** `data-component` elements within `root`.
* Throws if the DOM contains `data-component="X"` but you didn’t pass a matching class in `components`.
* Throws if any provided component class does not define a writable `static name`.
* **Lifecycle**
* When an instance is created, if it has `connect()`, it is called after the instance is bound to its root DOM element (available as `this.element`).
* When a component element is removed from the DOM, its `instance.disconnect()` is called automatically.

#### Return value

`start()` returns an object:

* **`roots`**: array of root component instances (components whose parent is `null`) discovered at startup.
* **`addComponents(newComponents)`**: registers additional component classes later.
* Validates the DOM again.
* Scans the existing DOM for elements with `data-component` matching the newly added classes and initializes those that weren’t initialized yet.
* Returns the newly created instances (or `null` if nothing was added).

### Base class: `Component`

Simplicit exports a `Component` base class you can extend.

#### Core properties

* **`element`**: the root DOM element of the component (`data-component="..."`).
* **`node`**: internal node graph `{ name, element, parent, children, siblings }`.
* **`componentId`**: string id mirrored to `data-component-id`.
* **`parent`**: parent component instance (or `null` for root components).

#### Relationships

All relationship helpers filter by component name(s):

* **`children(nameOrNames)`**: direct children component instances (DOM order).
* **`siblings(nameOrNames)`**: sibling component instances.
* **`ancestor(name)`**: nearest matching ancestor component instance (or `null`).
* **`descendants(name)`**: all matching descendants (flat array).

#### Refs

Refs are scoped to the component’s root element.

* **`ref(name)`**: returns `null`, a single element, or an array of elements (when multiple match).
* **`refs()`**: returns an object mapping each `data-ref` key to `Element | Element[]`. Only elements inside the component that have `data-ref` are included.

#### Cleanup & lifecycle utilities

`disconnect()` runs cleanup callbacks once and detaches the instance from its parent/child links.

You can register cleanup manually or use helpers that auto-register cleanup:

* **`registerCleanup(fn)`**
* **`on(target, type, listener, options)`** (auto-removes the listener on disconnect)
* **`timeout(fn, delay)`** (auto-clears on disconnect)
* **`interval(fn, delay)`** (auto-clears on disconnect)

### Server-driven templates via `<script type="application/json">`

If a component class defines `static template(data)`, Simplicit can render HTML from JSON embedded in the page.

```javascript
import { start, Component } from "simplicit";

class Slide extends Component {
static name = "slide";
static template = ({ text }) => `<div data-component="slide">${text}</div>`;
}

start({ root: document, components: [Slide] });
```

```html
<div id="slideshow"></div>

<script
type="application/json"
data-component="slide"
data-target="slideshow"
data-position="beforeend"
>
[{"text":"A"},{"text":"B"}]

```

Notes:

* The JSON payload must be an **array**; each item is passed to `ComponentClass.template(item)`.
* The rendered HTML is sanitized with `dompurify` before being inserted.
* `data-target` must match an existing element id, otherwise an error is thrown.
* Insertion uses `targetEl.insertAdjacentHTML(position, html)` where `position` comes from `data-position` (default: `beforeend`). Valid values: `beforebegin`, `afterbegin`, `beforeend`, `afterend`.
* Inserted component elements are then auto-initialized like any other DOM addition.

## 🕹️ Controllers

Simplicit must have access to all controllers you want to run. In practice, you build a `Controllers` object and pass it to `init()`.

_Example:_

```javascript
// js/index.js (entry point)

import { init } from 'simplicit';

import Admin from "./controllers/Admin.js"; // namespace controller
import User from "./controllers/User.js"; // namespace controller

import Articles from "./controllers/admin/Articles.js";
import Comments from "./controllers/admin/Comments.js";

Object.assign(Admin, {
Articles,
Comments
});

const Controllers = {
Admin,
User
};

document.addEventListener("DOMContentLoaded", function() {
init(Controllers);
});
```

### 💀 Anatomy of the controller

Example controller:

```javascript
// js/controllers/admin/Articles.js

import { helpers } from "simplicit";

import Index from "views/admin/articles/Index.js";
import Show from "views/admin/articles/Show.js";

class Articles {
// Simplicit supports both static and instance actions
static index() {
Index.render();
}

show() {
Show.render({ id: helpers.params.id });
}
}

export default Articles;
```

Minimal view example (one possible approach):

```javascript
// views/admin/articles/Show.js

export default {
render: ({ id }) => {
const el = document.getElementById("app");
el.textContent = `Article ${id}`;
// If you need data loading, you can fetch here and update the DOM after.
},
};
```

### 👷🏻‍♂️ How does it work?

On `DOMContentLoaded`, Simplicit reads these `` attributes:

* `data-namespace` (optional): a namespace path like `Main` or `Main/Panel`
* `data-controller`: controller name (e.g. `Pages`)
* `data-action`: action name (e.g. `index`)

```html

```

Then it resolves the matching controller(s), runs lifecycle hooks, and calls the action.

Resolution rules (simplified):

* If `data-namespace` resolves (e.g. `Main/Panel` → `Controllers.Main.Panel`), Simplicit initializes the namespace controller and resolves the page controller under it (e.g. `Controllers.Main.Panel.Pages`).
* Otherwise it skips the namespace controller and falls back to `Controllers.Pages`.

Call order (per controller):

* If a method exists as **static** or **instance**, Simplicit will call it.
* On navigation/re-init, previously active controllers receive `deinitialize()` (if present).

```javascript
namespaceController = new Controllers.Main.Panel;
Controllers.Main.Panel.initialize(); // if exists
namespaceController.initialize(); // if exists

controller = new Controllers.Main.Panel.Pages;
Controllers.Main.Panel.Pages.initialize(); // if exists
controller.initialize(); // if exists
Controllers.Main.Panel.Pages.index(); // if exists
controller.index(); // if exists
```

You don’t need controllers for every page; if a controller/method is missing, Simplicit skips it.

The `init` function returns `{ namespaceController, controller, action }`.

### Ruby on Rails: generating `` data attributes

If you want Rails to generate the controller metadata for Simplicit automatically, you can derive it from `controller_path`, `controller_name`, and `action_name`.

This version supports nested namespaces like `Main/Panel` (any depth):

```ruby
# app/helpers/application_helper.rb

module ApplicationHelper
def simplicit_body_attrs(default_namespace: nil)
namespace = controller_path
.split("/")
.then { |parts| parts[0...-1] } # everything except the controller name
.map(&:camelize)
.join("/")

# If you want a default namespace (e.g. "Main") for non-namespaced controllers:
namespace = default_namespace if namespace.blank? && default_namespace

{
data: {
namespace: namespace.presence, # -> data-namespace="Main/Panel"
controller: controller_name.camelize, # -> data-controller="Articles"
action: action_name, # -> data-action="index"
}.compact,
}
end
end
```

```erb
<%= content_tag :body, simplicit_body_attrs(default_namespace: "Main") do %>
<%= yield %>
<% end %>
```

## 🛠 Helpers

Simplicit exports `helpers` object that has the following properties:

* **params** (getter) - facilitates fetching params from the URL

# 👩🏽‍🔬 Tests

```bash
npx playwright install

npm run test

npx playwright test --headed e2e/slideshow.spec.js
```

# 📜 License

Simplicit is released under the [MIT License](https://opensource.org/licenses/MIT).

# 👨‍🏭 Author

Zbigniew Humeniuk from [Art of Code](https://artofcode.co)