{"id":27261536,"url":"https://github.com/oscarpalmer/magnus","last_synced_at":"2026-02-24T09:33:17.406Z","repository":{"id":143888532,"uuid":"353971595","full_name":"oscarpalmer/magnus","owner":"oscarpalmer","description":"A JavaScript framework for developers who like HTML.","archived":false,"fork":false,"pushed_at":"2026-01-03T15:18:38.000Z","size":605,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-01-08T16:49:12.580Z","etag":null,"topics":["framework","frontend","javascript","stimulusjs"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","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/oscarpalmer.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"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":"2021-04-02T09:30:56.000Z","updated_at":"2026-01-03T15:18:42.000Z","dependencies_parsed_at":null,"dependency_job_id":"1b777107-c197-46d6-8eb5-041688cda144","html_url":"https://github.com/oscarpalmer/magnus","commit_stats":{"total_commits":61,"total_committers":1,"mean_commits":61.0,"dds":0.0,"last_synced_commit":"63737b8a239fc892cd7afe1af0a655de95fc3459"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/oscarpalmer/magnus","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oscarpalmer%2Fmagnus","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oscarpalmer%2Fmagnus/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oscarpalmer%2Fmagnus/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oscarpalmer%2Fmagnus/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/oscarpalmer","download_url":"https://codeload.github.com/oscarpalmer/magnus/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/oscarpalmer%2Fmagnus/sbom","scorecard":{"id":713268,"data":{"date":"2025-08-11","repo":{"name":"github.com/oscarpalmer/magnus","commit":"fe733e2d34f341d62e2121f25b75def590a0b668"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":2.9,"checks":[{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Code-Review","score":0,"reason":"Found 0/7 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"SAST","score":0,"reason":"SAST tool is not run on all commits -- score normalized to 0","details":["Warn: 0 commits out of 24 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Vulnerabilities","score":9,"reason":"1 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GHSA-v6h2-p8h4-qcjw"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-22T08:45:00.173Z","repository_id":143888532,"created_at":"2025-08-22T08:45:00.173Z","updated_at":"2025-08-22T08:45:00.173Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29777892,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-24T04:54:30.205Z","status":"ssl_error","status_checked_at":"2026-02-24T04:53:58.628Z","response_time":75,"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":["framework","frontend","javascript","stimulusjs"],"created_at":"2025-04-11T05:34:34.773Z","updated_at":"2026-02-24T09:33:17.339Z","avatar_url":"https://github.com/oscarpalmer.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Magnus\n\n[![npm](https://badge.fury.io/js/@oscarpalmer%2Fmagnus.svg)](https://www.npmjs.com/package/@oscarpalmer/magnus)\n\nA JavaScript framework for developers who like HTML.\n\nIt's basically a lightweight version of [Stimulus](https://github.com/hotwired/stimulus), so you may want to use that lovely framework instead.\n\n## Quick start\n\nTo quickly get started, Magnus needs some nice HTML:\n\n```html\n\u003cdiv data-controller=\"talin\"\u003e\n  \u003cinput data-target=\"talin.input\" type=\"text\" /\u003e\n\n  \u003cbutton data-action=\"click-\u003etalin@greet\"\u003eGreet\u003c/button\u003e\n\n  \u003cpre data-target=\"talin.output\"\u003e\u003c/pre\u003e\n\u003c/div\u003e\n```\n\n… and some JavaScript:\n\n```typescript\nimport {Controller} from 'magnus';\n\nexport class Talin extends Controller {\n  greet(): void {\n    this.targets.get('output').textContent = this.targets.get('input').value;\n  }\n}\n```\n\n… and that's it. Entering any value in the input field and then clicking the button will display the same value below the button. Awesome!\n\n_(Thanks to [Stimulus](https://github.com/hotwired/stimulus) for the example, which looks mostly the same for Magnus!)_\n\n## Installation\n\nMagnus is available on NPM as `@oscarpalmer/magnus` and works well with JavaScript-bundlers, or you may use it completely standalone using the file in the `dist`-folder.\n\n## How Magnus works\n\nMagnus isn't very opinionated, but does have some ideas on how HTML and JavaScript should be written.\n\n### Lifecycles\n\nMagnus is built on top of mutation observers, which are objects that watch for changes in the DOM, allowing Magnus to connect specific element and attribute changes to event handlers within Magnus.\n\nThese event handlers are then able to:\n\n- create _(and remove)_ controller instances that can react to interactivity in the DOM\n- create _(and remove)_ targets _(element groups)_ from controllers\n- create _(and remove)_ actions _(controller-specific event handlers)_ that handle interactivity for the controller\n- create _(and remove)_ input- and output-targets, which are built-in targets _(with actions)_ for handling reactivity for the controller\n\n#### Controller lifecycle events\n\nAs controllers can be created and destroyed, they may also need to react to such events. When a controller is created, it will call the `connect`-method; when it's destroyed, it will call the `disconnect`-method.\n\n### Startup\n\nTo get things going with Magnus, all we need to do is import the application, like below:\n\n```typescript\nimport {magnus} from '@oscarpalmer/magnus'; // And it will automatically start and observe the document\n\n// Stops observing the document element\napplication.stop();\n\n// Begins to observe the document element again\napplication.start();\n```\n\n### Controllers\n\nMagnus controllers should be regular JavaScript classes, while also extending the base class available in Magnus.\n\n```typescript\nimport {Controller} from '@oscarpalmer/magnus';\n\nexport class Talin extends Controller {}\n```\n\nTo allow Magnus to create instances of a controller, it must also be added to Magnus with a name, which is used when evaluating the `data-controller`-attribute for elements.\n\n```typescript\nimport {magnus} from '@oscarpalmer/magnus';\nimport {Talin} from 'talin';\n\n// Add controller to Magnus with a name to watch for in observer\nmagnus.add('talin', Talin);\n```\n\nWhen a controller has been instantiated, it will call its `connect`-method.\n\nWhen a controller is removed, either by having its element removed from the DOM or by modifying the element's `data-controller`-attribute, it will stop observing the element, as well as remove all actions and targets from itself, and finally call the `disconnect`-method.\n\nThe same goes for other attributed elements, as well: e.g., modifying an element's `data-action`-attribute _(or removing the element completely)_ will then remove the element and its relevant information from the controllers it has been connected to.\n\n### Targets\n\nTargets are elements in your controller that are useful to have quick and easy access to. Targets are defined by the attribute `data-target`.\n\nThe value for the attribute should be a string of space-separated names, allowing for an element to be part of multiple target groups _(and multiple controllers!)_\n\nTo map the target to your controller, you may use `talin.output`, where `talin` is the name of your controller and `output` is the name of the target group, and Magnus will attempt to find controller closest to your element.\n\nIf you wish to map the target to a specific controller, you may use `talin#id.output`, where `talin` and `output` remain the same as above, but `id` points to an element with the same ID.\n\n#### Target example\n\nDefine your targets in HTML:\n\n```html\n\u003cspan data-target=\"talin.output\"\u003e\u003c/span\u003e\n```\n\nAnd access them in JavaScript:\n\n```typescript\nimport {Controller} from 'magnus';\n\nexport class Talin extends Controller {\n  // Custom method for showcasing built-in target-methods\n  getTargets(): void {\n    // Returns the first target (or undefined, if none exists)\n    const targets = this.targets.get('output')\n\n    // Returns an array of targets\n    const targets = this.targets.getAll('output')\n\n    // Returns true if at least one target exists\n    const has = this.targets.has('output')\n\n    // Finds and returns an array of elements within the controller (not just targets!),\n    // and accepts whatever 'querySelectorAll' will take\n    const found = this.targets.find('pre');\n  }\n}\n```\n\nThe methods above _(except `has`)_ also accepts an optional type for easier management of whatever is found and returned, e.g., `this.targets.find\u003cHTMLButtonElement\u003e('button')` for retrieving a list of properly typed button-elements.\n\n### Actions\n\nActions are events for elements within a controller and are defined by the attribute `data-action`.\n\nThe value for the attribute should be a space-separated string of actions, where each action should match any of the following:\n\n- `controller@method`\n- `controller@method:options`\n- `controller#id@method`\n- `controller#id@method:options`\n- `event-\u003econtroller@method`\n- `event-\u003econtroller@method:options`\n- `event-\u003econtroller#id@method`\n- `event-\u003econtroller#id@method:options`\n- `external@event-\u003econtroller@method`\n- `external@event-\u003econtroller@method:options`\n- `external@event-\u003econtroller#id@method`\n- `external@event-\u003econtroller#id@method:options`\n- `external#identifier@event-\u003econtroller@method`\n- `external#identifier@event-\u003econtroller@method:options`\n- `external#identifier@event-\u003econtroller#id@method`\n- `external#identifier@event-\u003econtroller#id@method:options`\n\nPhew, that's a lot, but it helps Magnus do a lot of cool stuff automagically with your events.\n\n#### Action parameters\n\n|         Part | Required | Description                                                                                                                                                                                                                      |\n|-------------:|:--------:|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|\n| `controller` | \u0026check;  | Name of controller                                                                                                                                                                                                               |\n|     `method` | \u0026check;  | Name of controller method                                                                                                                                                                                                        |\n|    `options` | \u0026ndash;  | Event options; a colon-separated string that may contain:\u003cbr\u003e\u0026bull;\u0026nbsp;`a` or `active` for allowing `preventDefault`\u003cbr\u003e\u0026bull;\u0026nbsp;`c` or `capture` for capturing events\u003cbr\u003e\u0026bull;\u0026nbsp;`o` or `once` for handling event once |\n|         `id` | \u0026ndash;  | ID for element that has the controller                                                                                                                                                                                           |\n|      `event` | \u0026ndash;  | Event name\u003cbr\u003e_(Whenever event is omitted, Magnus will try to interpret a default event type based on the element)_                                                                                                              |\n|   `external` | \u0026ndash;  | External target, either `window`, `document`, a controller, or an element ID                                                                                                                                                     |\n| `identifier` | \u0026ndash;  | When included, `identifier` implies that `external` should be a controller and is used to identify a unique element using the controller                                                                                         |\n\n#### Action example\n\nDefine your actions in HTML:\n\n```html\n\u003cbutton data-action=\"click@greet:once\"\u003eGreet\u003c/button\u003e\n```\n\nAnd their methods in JavaScript:\n\n```typescript\nimport {Controller} from 'magnus';\n\nexport class Talin extends Controller {\n  greet(): void {\n    // Called on a click event once\n  }\n}\n```\n\n#### Dispatching actions\n\n##### Dispatching actions example\n\n```typescript\nimport {Controller} from 'magnus';\n\nexport class Talin extends Controller {\n  trigger(): void {\n    this.actions.dispatch('event'); // Dispatches an event on the controller's element\n\n    this.actions.dispatch('event', target);\n    // Dispatches an event for the `target`:\n    // - a string, to find the first existing target in the controller\n    // - an `EventTarget`, i.e., the document, window, or an element\n\n    this.actions.dispatch('event', options, target?)\n    // Dispatches an event on the controller's element (or target)\n    // `options` allow for bubbling, cancellation, composition,\n    // and may hold details to pass along with the event\n  }\n}\n```\n\n### Input \u0026 output\n\nInputs and outputs are built-in targets that allow for reactivity in a controller, by listening to change events on input-targets - using the attribute `data-input` - and outputting values into output-targets, using the attribute `data-output`.\n\nTo map such a target to your controller, you may use `talin.message`, where `talin` is the name of your controller and `message` is the key in the controller's data, and Magnus will attempt to find controller closest to your element.\n\nIf you wish to map the target to a specific controller, you may use `talin#id.message`, where `talin` and `message` remain the same as above, but `id` points to an element with the same ID.\n\nWhenever the value for an input-target changes, the new value will be stored in the controller's data, update the contents of output-targets _(using the same key)_, as well as update the values of input-targets _(using the same key)_.\n\n\u003e [!IMPORTANT]\n\u003e _Unlike regular targets, these special ones will only try to map the first proper attribute value, i.e., `talin.first talin.second` will only match `talin.first` to avoid unwanted effects in controller data stores._\n\n#### Inputting \u0026 outputting JSON\n\nIf you have any kind of object you wish to edit or display, either the complete data for a controller or a key-based value in the controller's data, you may do so with the `:json`-suffix in the attribute value, e.g., `talin.object:json`, and Magnus will do its best to handle whatever JSON-y data you're working with.\n\n#### Input \u0026 output example\n\n```html\n\u003cdiv\u003e\n  \u003clabel for=\"message\"\u003eMessage\u003c/label\u003e\n  \u003ctextarea id=\"message\" data-input=\"talin.message\"\u003e\u003c/textarea\u003e\n  \u003c!-- The textarea now automatically responds to input events... --\u003e\n\u003c/div\u003e\n\u003cpre data-output=\"talin.message\"\u003e\u003c/pre\u003e\n\u003c!-- ... and updates the formatted block! --\u003e\n```\n\n### Data\n\nMagnus is also able to handle simple, mostly-flat data structures, as well as respond to changes when needed.\n\nData can be initialized for a controller using attributes on your controller element, e.g. `data-talin-name`, where `name` is the key for the value to store, and its value is the actual data value _(of any type!)_. If the name contains dashes or underscores, it will be converted to its camel-cased variant in the controller, i.e., `data-talin-my-property` \u0026rarr; `myProperty`.\n\nTo access the data structure for retrieving and storing information, the controller has the property `data` which returns [a Proxy-object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy), so be mindful of how you retrieve and store nested objects.\n\nWhen storing values, Magnus will first: update the attribute as set in the HTML; and second: update input- and output-targets and set their contents and values respectively.\n\n#### Data example\n\n```html\n\u003cdiv data-controller=\"talin\" data-talin-my-cool-property=\"and a value\"\u003e\u003c/div\u003e\n```\n\n```typescript\nimport {Controller} from 'magnus';\n\nexport class Talin extends Controller {\n  // Custom method accessing your custom data property\n  onAlert(): void {\n    alert(this.data.myCoolProperty);\n  }\n}\n```\n\n#### Custom data models\n\nMagnus also lets you set a custom type for your data model to allow for nicer management of your controller's data. This can be done by extending the base controller with your type, e.g. `class Talin extends Controller\u003cMyCustomDataModel\u003e` where `MyCustomDataModel` is your nicely structured interface.\n\n## The name\n\n\u003e … it was Magnus who created the schematics and diagrams needed to construct the mortal plane.\n\n— Brother Mikhael Karkuxor, [Varieties of Faith in Tamriel](https://en.uesp.net/wiki/Lore:Varieties_of_Faith_in_Tamriel) :books:\n\n## License\n\n[MIT licensed](LICENSE), natch :wink:\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foscarpalmer%2Fmagnus","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Foscarpalmer%2Fmagnus","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Foscarpalmer%2Fmagnus/lists"}