{"id":35553954,"url":"https://github.com/artofcodelabs/simplicit","last_synced_at":"2026-01-13T22:03:38.579Z","repository":{"id":42840997,"uuid":"263588343","full_name":"artofcodelabs/simplicit","owner":"artofcodelabs","description":"Loco-JS-Core provides a logical structure for JavaScript code","archived":false,"fork":false,"pushed_at":"2026-01-07T04:52:28.000Z","size":796,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-01-07T23:47:38.639Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/artofcodelabs.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"MIT-LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2020-05-13T09:44:46.000Z","updated_at":"2026-01-04T06:33:47.000Z","dependencies_parsed_at":"2023-02-05T11:16:36.340Z","dependency_job_id":"530a570e-b234-4a8c-aaaa-b76d6f6f2c33","html_url":"https://github.com/artofcodelabs/simplicit","commit_stats":null,"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/artofcodelabs/simplicit","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/artofcodelabs%2Fsimplicit","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/artofcodelabs%2Fsimplicit/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/artofcodelabs%2Fsimplicit/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/artofcodelabs%2Fsimplicit/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/artofcodelabs","download_url":"https://codeload.github.com/artofcodelabs/simplicit/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/artofcodelabs%2Fsimplicit/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28399514,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-13T14:36:09.778Z","status":"ssl_error","status_checked_at":"2026-01-13T14:35:19.697Z","response_time":56,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-01-04T08:14:29.116Z","updated_at":"2026-01-13T22:03:38.574Z","avatar_url":"https://github.com/artofcodelabs.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🧐 What is Simplicit?\n\nSimplicit is a small library for structuring front-end JavaScript around **controllers** and **components**.\n\nOn the MVC side, it mirrors the “controller/action” convention you may know from frameworks like [Ruby on Rails](https://rubyonrails.org): based on `\u003cbody\u003e` attributes, it finds the corresponding controller and calls its lifecycle hooks and action method.\n\nOn 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.\n\n# 🤝 Dependencies\n\nSimplicit relies only on `dompurify` for sanitizing HTML.\n\n# 📲 Installation\n\n```bash\n$ npm install --save simplicit\n```\n\n# 🎮 Usage\n\n## 🖲️ Components\n\nSimplicit ships with a small component runtime built around DOM attributes.\n\n### ✅ Quick start\n\n```javascript\nimport { start, Component } from \"simplicit\";\n\nclass Hello extends Component {\n  static name = \"hello\";\n\n  connect() {\n    const { input, button, output } = this.refs();\n    this.on(button, \"click\", () =\u003e {\n      output.textContent = `Hello ${input.value}!`;\n    });\n  }\n}\n\ndocument.addEventListener(\"DOMContentLoaded\", () =\u003e {\n  start({ root: document, components: [Hello] });\n});\n```\n\n```html\n\u003cdiv data-component=\"hello\"\u003e\n  \u003cinput data-ref=\"input\" type=\"text\" /\u003e\n  \u003cbutton data-ref=\"button\"\u003eGreet\u003c/button\u003e\n  \u003cspan data-ref=\"output\"\u003e\u003c/span\u003e\n\u003c/div\u003e\n```\n\n### DOM conventions\n\n* **`data-component=\"\u003cname\u003e\"`**: marks an element as a component root.\n  * `\u003cname\u003e` must match the component class’ **`static name`**.\n  * `\u003cscript\u003e` tags are never treated as components, even if they have `data-component`.\n* **`data-component-id=\"\u003cid\u003e\"`**: set automatically on every element with `data-component` (each component instance).\n  * Also available as `instance.componentId`.\n* **`data-ref=\"\u003ckey\u003e\"`**: marks ref elements inside a component (see `ref()` / `refs()`).\n\n### `start({ root, components })`\n\n`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).\n\n* **Validation**\n  * Throws if there are **no** `data-component` elements within `root`.\n  * Throws if the DOM contains `data-component=\"X\"` but you didn’t pass a matching class in `components`.\n  * Throws if any provided component class does not define a writable `static name`.\n* **Lifecycle**\n  * 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`).\n  * When a component element is removed from the DOM, its `instance.disconnect()` is called automatically.\n\n#### Return value\n\n`start()` returns an object:\n\n* **`roots`**: array of root component instances (components whose parent is `null`) discovered at startup.\n* **`addComponents(newComponents)`**: registers additional component classes later.\n  * Validates the DOM again.\n  * Scans the existing DOM for elements with `data-component` matching the newly added classes and initializes those that weren’t initialized yet.\n  * Returns the newly created instances (or `null` if nothing was added).\n\n### Base class: `Component`\n\nSimplicit exports a `Component` base class you can extend.\n\n#### Core properties\n\n* **`element`**: the root DOM element of the component (`data-component=\"...\"`).\n* **`node`**: internal node graph `{ name, element, parent, children, siblings }`.\n* **`componentId`**: string id mirrored to `data-component-id`.\n* **`parent`**: parent component instance (or `null` for root components).\n\n#### Relationships\n\nAll relationship helpers filter by component name(s):\n\n* **`children(nameOrNames)`**: direct children component instances (DOM order).\n* **`siblings(nameOrNames)`**: sibling component instances.\n* **`ancestor(name)`**: nearest matching ancestor component instance (or `null`).\n* **`descendants(name)`**: all matching descendants (flat array).\n\n#### Refs\n\nRefs are scoped to the component’s root element.\n\n* **`ref(name)`**: returns `null`, a single element, or an array of elements (when multiple match).\n* **`refs()`**: returns an object mapping each `data-ref` key to `Element | Element[]`. Only elements inside the component that have `data-ref` are included.\n\n#### Cleanup \u0026 lifecycle utilities\n\n`disconnect()` runs cleanup callbacks once and detaches the instance from its parent/child links.\n\nYou can register cleanup manually or use helpers that auto-register cleanup:\n\n* **`registerCleanup(fn)`**\n* **`on(target, type, listener, options)`** (auto-removes the listener on disconnect)\n* **`timeout(fn, delay)`** (auto-clears on disconnect)\n* **`interval(fn, delay)`** (auto-clears on disconnect)\n\n### Server-driven templates via `\u003cscript type=\"application/json\"\u003e`\n\nIf a component class defines `static template(data)`, Simplicit can render HTML from JSON embedded in the page.\n\n```javascript\nimport { start, Component } from \"simplicit\";\n\nclass Slide extends Component {\n  static name = \"slide\";\n  static template = ({ text }) =\u003e `\u003cdiv data-component=\"slide\"\u003e${text}\u003c/div\u003e`;\n}\n\nstart({ root: document, components: [Slide] });\n```\n\n```html\n\u003cdiv id=\"slideshow\"\u003e\u003c/div\u003e\n\n\u003cscript\n  type=\"application/json\"\n  data-component=\"slide\"\n  data-target=\"slideshow\"\n  data-position=\"beforeend\"\n\u003e\n  [{\"text\":\"A\"},{\"text\":\"B\"}]\n\u003c/script\u003e\n```\n\nNotes:\n\n* The JSON payload must be an **array**; each item is passed to `ComponentClass.template(item)`.\n* The rendered HTML is sanitized with `dompurify` before being inserted.\n* `data-target` must match an existing element id, otherwise an error is thrown.\n* Insertion uses `targetEl.insertAdjacentHTML(position, html)` where `position` comes from `data-position` (default: `beforeend`). Valid values: `beforebegin`, `afterbegin`, `beforeend`, `afterend`.\n* Inserted component elements are then auto-initialized like any other DOM addition.\n\n## 🕹️ Controllers\n\nSimplicit must have access to all controllers you want to run. In practice, you build a `Controllers` object and pass it to `init()`.\n\n_Example:_\n\n```javascript\n// js/index.js (entry point)\n\nimport { init } from 'simplicit';\n\nimport Admin from \"./controllers/Admin.js\"; // namespace controller\nimport User from \"./controllers/User.js\";   // namespace controller\n\nimport Articles from \"./controllers/admin/Articles.js\";\nimport Comments from \"./controllers/admin/Comments.js\";\n\nObject.assign(Admin, {\n  Articles,\n  Comments\n});\n\nconst Controllers = {\n  Admin,\n  User\n};\n\ndocument.addEventListener(\"DOMContentLoaded\", function() {\n  init(Controllers);\n});\n```\n\n### 💀 Anatomy of the controller\n\nExample controller:\n\n```javascript\n// js/controllers/admin/Articles.js\n\nimport { helpers } from \"simplicit\";\n\nimport Index from \"views/admin/articles/Index.js\";\nimport Show from \"views/admin/articles/Show.js\";\n\nclass Articles {\n  // Simplicit supports both static and instance actions\n  static index() {\n    Index.render();\n  }\n\n  show() {\n    Show.render({ id: helpers.params.id });\n  }\n}\n\nexport default Articles;\n```\n\nMinimal view example (one possible approach):\n\n```javascript\n// views/admin/articles/Show.js\n\nexport default {\n  render: ({ id }) =\u003e {\n    const el = document.getElementById(\"app\");\n    el.textContent = `Article ${id}`;\n    // If you need data loading, you can fetch here and update the DOM after.\n  },\n};\n```\n\n### 👷🏻‍♂️ How does it work?\n\nOn `DOMContentLoaded`, Simplicit reads these `\u003cbody\u003e` attributes:\n\n* `data-namespace` (optional): a namespace path like `Main` or `Main/Panel`\n* `data-controller`: controller name (e.g. `Pages`)\n* `data-action`: action name (e.g. `index`)\n\n```html\n\u003cbody data-namespace=\"Main/Panel\" data-controller=\"Pages\" data-action=\"index\"\u003e\n\u003c/body\u003e\n```\n\nThen it resolves the matching controller(s), runs lifecycle hooks, and calls the action.\n\nResolution rules (simplified):\n\n* 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`).\n* Otherwise it skips the namespace controller and falls back to `Controllers.Pages`.\n\nCall order (per controller):\n\n* If a method exists as **static** or **instance**, Simplicit will call it.\n* On navigation/re-init, previously active controllers receive `deinitialize()` (if present).\n\n```javascript\nnamespaceController = new Controllers.Main.Panel;\nControllers.Main.Panel.initialize();               // if exists\nnamespaceController.initialize();                  // if exists\n\ncontroller = new Controllers.Main.Panel.Pages;\nControllers.Main.Panel.Pages.initialize();         // if exists\ncontroller.initialize();                           // if exists\nControllers.Main.Panel.Pages.index();              // if exists\ncontroller.index();                                // if exists\n```\n\nYou don’t need controllers for every page; if a controller/method is missing, Simplicit skips it.\n\nThe `init` function returns `{ namespaceController, controller, action }`.\n\n### Ruby on Rails: generating `\u003cbody\u003e` data attributes\n\nIf you want Rails to generate the controller metadata for Simplicit automatically, you can derive it from `controller_path`, `controller_name`, and `action_name`.\n\nThis version supports nested namespaces like `Main/Panel` (any depth):\n\n```ruby\n# app/helpers/application_helper.rb\n\nmodule ApplicationHelper\n  def simplicit_body_attrs(default_namespace: nil)\n    namespace = controller_path\n      .split(\"/\")\n      .then { |parts| parts[0...-1] } # everything except the controller name\n      .map(\u0026:camelize)\n      .join(\"/\")\n\n    # If you want a default namespace (e.g. \"Main\") for non-namespaced controllers:\n    namespace = default_namespace if namespace.blank? \u0026\u0026 default_namespace\n\n    {\n      data: {\n        namespace: namespace.presence,           # -\u003e data-namespace=\"Main/Panel\"\n        controller: controller_name.camelize,    # -\u003e data-controller=\"Articles\"\n        action: action_name,                     # -\u003e data-action=\"index\"\n      }.compact,\n    }\n  end\nend\n```\n\n```erb\n\u003c%= content_tag :body, simplicit_body_attrs(default_namespace: \"Main\") do %\u003e\n  \u003c%= yield %\u003e\n\u003c% end %\u003e\n```\n\n## 🛠 Helpers\n\nSimplicit exports `helpers` object that has the following properties:\n\n* **params** (getter) - facilitates fetching params from the URL\n\n# 👩🏽‍🔬 Tests\n\n```bash\nnpx playwright install\n\nnpm run test\n\nnpx playwright test --headed e2e/slideshow.spec.js\n```\n\n# 📜 License\n\nSimplicit is released under the [MIT License](https://opensource.org/licenses/MIT).\n\n# 👨‍🏭 Author\n\nZbigniew Humeniuk from [Art of Code](https://artofcode.co)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fartofcodelabs%2Fsimplicit","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fartofcodelabs%2Fsimplicit","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fartofcodelabs%2Fsimplicit/lists"}