Showing preview only (417K chars total). Download the full file or copy to clipboard to get everything.
Repository: hotwired/stimulus
Branch: main
Commit: 422eb81fa649
Files: 163
Total size: 379.6 KB
Directory structure:
gitextract_o806b5yi/
├── .eslintignore
├── .eslintrc
├── .github/
│ ├── scripts/
│ │ └── publish-dev-build
│ └── workflows/
│ ├── build.yml
│ ├── dev-builds.yml
│ ├── lint.yml
│ └── test.yml
├── .gitignore
├── .node-version
├── .npmignore
├── .prettierignore
├── .prettierrc.json
├── CHANGELOG.md
├── CODE_OF_CONDUCT.md
├── LICENSE.md
├── README.md
├── SECURITY.md
├── docs/
│ ├── handbook/
│ │ ├── 00_the_origin_of_stimulus.md
│ │ ├── 01_introduction.md
│ │ ├── 02_hello_stimulus.md
│ │ ├── 03_building_something_real.md
│ │ ├── 04_designing_for_resilience.md
│ │ ├── 05_managing_state.md
│ │ ├── 06_working_with_external_resources.md
│ │ └── 07_installing_stimulus.md
│ └── reference/
│ ├── actions.md
│ ├── controllers.md
│ ├── css_classes.md
│ ├── lifecycle_callbacks.md
│ ├── outlets.md
│ ├── targets.md
│ ├── using_typescript.md
│ └── values.md
├── examples/
│ ├── .babelrc
│ ├── controllers/
│ │ ├── clipboard_controller.js
│ │ ├── content_loader_controller.js
│ │ ├── hello_controller.js
│ │ ├── slideshow_controller.js
│ │ └── tabs_controller.js
│ ├── index.js
│ ├── package.json
│ ├── public/
│ │ ├── examples.css
│ │ └── main.css
│ ├── server.js
│ ├── views/
│ │ ├── clipboard.ejs
│ │ ├── content-loader.ejs
│ │ ├── hello.ejs
│ │ ├── layout/
│ │ │ ├── head.ejs
│ │ │ └── tail.ejs
│ │ ├── slideshow.ejs
│ │ └── tabs.ejs
│ └── webpack.config.js
├── karma.conf.cjs
├── package.json
├── packages/
│ └── stimulus/
│ ├── .gitignore
│ ├── .npmignore
│ ├── index.d.ts
│ ├── index.js
│ ├── package.json
│ ├── rollup.config.js
│ ├── webpack-helpers.d.ts
│ └── webpack-helpers.js
├── rollup.config.js
├── src/
│ ├── core/
│ │ ├── action.ts
│ │ ├── action_descriptor.ts
│ │ ├── action_event.ts
│ │ ├── application.ts
│ │ ├── binding.ts
│ │ ├── binding_observer.ts
│ │ ├── blessing.ts
│ │ ├── class_map.ts
│ │ ├── class_properties.ts
│ │ ├── constructor.ts
│ │ ├── context.ts
│ │ ├── controller.ts
│ │ ├── data_map.ts
│ │ ├── definition.ts
│ │ ├── dispatcher.ts
│ │ ├── error_handler.ts
│ │ ├── event_listener.ts
│ │ ├── guide.ts
│ │ ├── index.ts
│ │ ├── inheritable_statics.ts
│ │ ├── logger.ts
│ │ ├── module.ts
│ │ ├── outlet_observer.ts
│ │ ├── outlet_properties.ts
│ │ ├── outlet_set.ts
│ │ ├── router.ts
│ │ ├── schema.ts
│ │ ├── scope.ts
│ │ ├── scope_observer.ts
│ │ ├── selectors.ts
│ │ ├── string_helpers.ts
│ │ ├── target_observer.ts
│ │ ├── target_properties.ts
│ │ ├── target_set.ts
│ │ ├── utils.ts
│ │ ├── value_observer.ts
│ │ └── value_properties.ts
│ ├── index.d.ts
│ ├── index.js
│ ├── index.ts
│ ├── multimap/
│ │ ├── index.ts
│ │ ├── indexed_multimap.ts
│ │ ├── multimap.ts
│ │ └── set_operations.ts
│ ├── mutation-observers/
│ │ ├── attribute_observer.ts
│ │ ├── element_observer.ts
│ │ ├── index.ts
│ │ ├── selector_observer.ts
│ │ ├── string_map_observer.ts
│ │ ├── token_list_observer.ts
│ │ └── value_list_observer.ts
│ └── tests/
│ ├── cases/
│ │ ├── application_test_case.ts
│ │ ├── controller_test_case.ts
│ │ ├── dom_test_case.ts
│ │ ├── index.ts
│ │ ├── log_controller_test_case.ts
│ │ ├── observer_test_case.ts
│ │ └── test_case.ts
│ ├── controllers/
│ │ ├── class_controller.ts
│ │ ├── default_value_controller.ts
│ │ ├── log_controller.ts
│ │ ├── outlet_controller.ts
│ │ ├── target_controller.ts
│ │ └── value_controller.ts
│ ├── fixtures/
│ │ └── application_start/
│ │ ├── helpers.ts
│ │ ├── index.html
│ │ └── index.ts
│ ├── index.ts
│ └── modules/
│ ├── core/
│ │ ├── action_click_filter_tests.ts
│ │ ├── action_keyboard_filter_tests.ts
│ │ ├── action_ordering_tests.ts
│ │ ├── action_params_case_insensitive_tests.ts
│ │ ├── action_params_tests.ts
│ │ ├── action_tests.ts
│ │ ├── action_timing_tests.ts
│ │ ├── application_start_tests.ts
│ │ ├── application_tests.ts
│ │ ├── class_tests.ts
│ │ ├── data_tests.ts
│ │ ├── default_value_tests.ts
│ │ ├── error_handler_tests.ts
│ │ ├── es6_tests.ts
│ │ ├── event_options_tests.ts
│ │ ├── extending_application_tests.ts
│ │ ├── legacy_target_tests.ts
│ │ ├── lifecycle_tests.ts
│ │ ├── loading_tests.ts
│ │ ├── memory_tests.ts
│ │ ├── outlet_order_tests.ts
│ │ ├── outlet_tests.ts
│ │ ├── string_helpers_tests.ts
│ │ ├── target_tests.ts
│ │ ├── value_properties_tests.ts
│ │ └── value_tests.ts
│ └── mutation-observers/
│ ├── attribute_observer_tests.ts
│ ├── selector_observer_tests.ts
│ ├── token_list_observer_tests.ts
│ └── value_list_observer_tests.ts
├── tsconfig.json
└── tsconfig.test.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
dist/
node_modules/
================================================
FILE: .eslintrc
================================================
{
"root": true,
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint",
"prettier"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"prefer-rest-params": "off",
"prettier/prettier": ["error"],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
"@typescript-eslint/ban-types": ["error", {
"types": {
"Function": false,
"Object": false,
"{}": false
}
}]
}
}
================================================
FILE: .github/scripts/publish-dev-build
================================================
#!/usr/bin/env bash
set -eux
DEV_BUILD_REPO_NAME="hotwired/dev-builds"
DEV_BUILD_ORIGIN_URL="https://${1}@github.com/${DEV_BUILD_REPO_NAME}.git"
BUILD_PATH="$HOME/publish-dev-build"
mkdir "$BUILD_PATH"
cd "$GITHUB_WORKSPACE"
package_name="$(jq -r .name package.json)"
package_files=( dist package.json )
tag="${package_name}/${GITHUB_SHA:0:7}"
name="$(git log -n 1 --format=format:%cn)"
email="$(git log -n 1 --format=format:%ce)"
subject="$(git log -n 1 --format=format:%s)"
date="$(git log -n 1 --format=format:%ai)"
url="https://github.com/${GITHUB_REPOSITORY}/tree/${GITHUB_SHA}"
message="$tag $subject"$'\n\n'"$url"
cp -R "${package_files[@]}" "$BUILD_PATH"
cd "$BUILD_PATH"
git init .
git remote add origin "$DEV_BUILD_ORIGIN_URL"
git symbolic-ref HEAD refs/heads/publish-dev-build
git add "${package_files[@]}"
GIT_AUTHOR_DATE="$date" GIT_COMMITTER_DATE="$date" \
GIT_AUTHOR_NAME="$name" GIT_COMMITTER_NAME="$name" \
GIT_AUTHOR_EMAIL="$email" GIT_COMMITTER_EMAIL="$email" \
git commit -m "$message"
git tag "$tag"
[ "$GITHUB_REF" != "refs/heads/main" ] || git tag -f "${package_name}/latest"
git push -f --tags
echo done
================================================
FILE: .github/workflows/build.yml
================================================
name: Build
on: [push, pull_request]
jobs:
build:
name: Build
runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 19, 20, 21]
steps:
- uses: actions/checkout@v4
- name: Setup Node v${{ matrix.node }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'yarn'
- name: Install Dependencies
run: yarn install --frozen-lockfile
- name: Build
run: yarn build
- name: Test Build
run: yarn build:test
================================================
FILE: .github/workflows/dev-builds.yml
================================================
name: dev-builds
on:
workflow_dispatch:
push:
branches:
- main
- 'builds/**'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'yarn'
- run: yarn install
- run: yarn build
- name: Publish dev build
run: .github/scripts/publish-dev-build '${{ secrets.DEV_BUILD_GITHUB_TOKEN }}'
================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint
on: [push, pull_request]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 21
cache: 'yarn'
- name: Install Dependencies
run: yarn install --frozen-lockfile
- name: Lint
run: yarn lint
================================================
FILE: .github/workflows/test.yml
================================================
name: Test
on: [push, pull_request]
jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: browser-actions/setup-chrome@v1
- uses: browser-actions/setup-firefox@v1
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'yarn'
- name: Install Dependencies
run: yarn install --frozen-lockfile
- name: Test
run: yarn test
================================================
FILE: .gitignore
================================================
coverage/
dist/
node_modules/
docs/api/
*.log
*.tsbuildinfo
================================================
FILE: .node-version
================================================
20.11.0
================================================
FILE: .npmignore
================================================
src/tests/
dist/tests/
tsconfig*
*.log
================================================
FILE: .prettierignore
================================================
dist/
node_modules/
================================================
FILE: .prettierrc.json
================================================
{
"singleQuote": false,
"printWidth": 120,
"semi": false
}
================================================
FILE: CHANGELOG.md
================================================
# Changelog
Please see [our GitHub "Releases" page](https://github.com/hotwired/stimulus/releases).
================================================
FILE: CODE_OF_CONDUCT.md
================================================
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of experience,
education, socio-economic status, nationality, personal appearance, race,
religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at <opensource@basecamp.com>. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
================================================
FILE: LICENSE.md
================================================
# MIT License
Copyright © 2021 Basecamp, LLC.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
The Software is provided "as is," without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software.
================================================
FILE: README.md
================================================
# <img src="assets/logo.svg?sanitize=true" width="24" height="24" alt="Stimulus"> Stimulus
### A modest JavaScript framework for the HTML you already have
Stimulus is a JavaScript framework with modest ambitions. It doesn't seek to take over your entire front-end—in fact, it's not concerned with rendering HTML at all. Instead, it's designed to augment your HTML with just enough behavior to make it shine. Stimulus pairs beautifully with [Turbo](https://turbo.hotwired.dev) to provide a complete solution for fast, compelling applications with a minimal amount of effort.
How does it work? Sprinkle your HTML with controller, target, and action attributes:
```html
<div data-controller="hello">
<input data-hello-target="name" type="text">
<button data-action="click->hello#greet">Greet</button>
<span data-hello-target="output"></span>
</div>
```
Then write a compatible controller. Stimulus brings it to life automatically:
```js
// hello_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "name", "output" ]
greet() {
this.outputTarget.textContent =
`Hello, ${this.nameTarget.value}!`
}
}
```
Stimulus continuously watches the page, kicking in as soon as attributes appear or disappear. It works with any update to the DOM, regardless of whether it comes from a full page load, a [Turbo](https://turbo.hotwired.dev) page change, or an Ajax request. Stimulus manages the whole lifecycle.
You can write your first controller in five minutes by following along in the [Stimulus Handbook](https://stimulus.hotwired.dev/handbook/introduction).
You can read more about why we created this new framework in [The Origin of Stimulus](https://stimulus.hotwired.dev/handbook/origin).
## Installing Stimulus
You can use Stimulus with any asset packaging systems. And if you prefer no build step at all, just drop a `<script>` tag on the page and get right down to business.
See the [Installation Guide](https://stimulus.hotwired.dev/handbook/installing) for detailed instructions.
## Getting Help
Looking for the docs? Once you've read through the Handbook, consult the [Stimulus Reference](https://stimulus.hotwired.dev/reference/controllers) for API details.
Have a question about Stimulus? Connect with other Stimulus developers on the [Hotwire Discourse](https://discuss.hotwired.dev/) community forum.
## Contributing Back
Find a bug? Head over to our [issue tracker](https://github.com/hotwired/stimulus/issues) and we'll do our best to help. We love pull requests, too!
We expect all Stimulus contributors to abide by the terms of our [Code of Conduct](CODE_OF_CONDUCT.md).
### Development
- Fork the project locally
- `yarn install`
- `yarn start` - to run the local dev server with examples
- `yarn test` - to run the unit tests
- `yarn lint` - to run the linter with ESLint
- `yarn format` - to format changes with Prettier
## Acknowledgments
Stimulus is [MIT-licensed](LICENSE.md) open-source software from [Basecamp](https://basecamp.com/), the creators of [Ruby on Rails](https://rubyonrails.org).
---
© 2024 Basecamp, LLC.
================================================
FILE: SECURITY.md
================================================
# Security Considerations
### Q: Can I be confident that if my cross-site scripting countermeasures fail, there is no way for an attacker to run arbitrary JavaScript using Stimulus?
A: While there is no way for an attacker to run arbitrary JavaScript using Stimulus, if an attacker can insert or modify DOM elements on your page, they can use the `data-action` attribute to [define an action](docs/reference/actions.md) that invokes an arbitrary method on one of your controllers in response to an event.
### Q: If an attacker manages to make changes to my application's DOM, how can I ensure that what they can do is limited if I use Stimulus?
A: Ensure that none of the methods on any of your controllers perform sensitive operations without appropriate safeguards.
Refer to the MDN [Content Security Policy documentation](https://content-security-policy.com) for a general overview of cross-site scripting attacks and how to defend against them.
### Q: Will Stimulus only instantiate and invoke methods on classes marked as controllers?
A: Yes. All controller classes must be registered with corresponding identifiers, either implicitly by way of an autoloader like used in [stimulus-rails](https://github.com/hotwired/stimulus-rails) with import maps or explicitly through a call to `Application#register()`.
### Q: Does Stimulus use `eval()`?
A: No. There is no use of `eval()` in Stimulus. The action system _does_ use dynamic dispatch to invoke controller methods, which corresponds to a runtime property lookup on the controller instance. See the implementation of [`Binding#method`](src/core/binding.ts) for details.
================================================
FILE: docs/handbook/00_the_origin_of_stimulus.md
================================================
---
permalink: /handbook/origin.html
nav_prefix: Preface
order: 00
---
# The Origin of Stimulus
We write a lot of JavaScript at [Basecamp](https://basecamp.com), but we don’t use it to create “JavaScript applications” in the contemporary sense. All our applications have server-side rendered HTML at their core, then add sprinkles of JavaScript to make them sparkle.
This is the way of the [majestic monolith](https://signalvnoise.com/svn3/the-majestic-monolith/). Basecamp runs across half a dozen platforms, including native mobile apps, with a single set of controllers, views, and models created using Ruby on Rails. Having a single, shared interface that can be updated in a single place is key to being able to perform with a small team, despite the many platforms.
It allows us to party with productivity like days of yore. A throwback to when a single programmer could make rapacious progress without getting stuck in layers of indirection or distributed systems. A time before everyone thought the holy grail was to confine their server-side application to producing JSON for a JavaScript-based client application.
That’s not to say that there isn’t value in such an approach for some people, some of the time. Just that as a general approach to many applications, and certainly the likes of Basecamp, it’s a regression in overall simplicity and productivity.
And it’s also not to say that the proliferation of single-page JavaScript applications hasn’t brought real benefits. Chief amongst which has been faster, more fluid interfaces set free from the full-page refresh.
We wanted Basecamp to feel like that too. As though we had followed the herd and rewritten everything with client-side rendering or gone full-native on mobile.
This desire led us to a two-punch solution: [Turbo](https://turbo.hotwired.dev) and Stimulus.
### Turbo up high, Stimulus down low
Before I get to Stimulus, our new modest JavaScript framework, allow me to recap the proposition of Turbo.
Turbo descends from an approach called [pjax](https://github.com/defunkt/jquery-pjax), developed at GitHub. The basic concept remains the same. The reason full-page refreshes often feel slow is not so much because the browser has to process a bunch of HTML sent from a server. Browsers are really good and really fast at that. And in most cases, the fact that an HTML payload tends to be larger than a JSON payload doesn’t matter either (especially with gzipping). No, the reason is that CSS and JavaScript has to be reinitialized and reapplied to the page again. Regardless of whether the files themselves are cached. This can be pretty slow if you have a fair amount of CSS and JavaScript.
To get around this reinitialization, Turbo maintains a persistent process, just like single-page applications do. But largely an invisible one. It intercepts links and loads new pages via Ajax. The server still returns fully-formed HTML documents.
This strategy alone can make most actions in most applications feel really fast (if they’re able to return server responses in 100-200ms, which is eminently possible with caching). For Basecamp, it sped up the page-to-page transition by ~3x. It gives the application that feel of responsiveness and fluidity that was a massive part of the appeal for single-page applications.
But Turbo alone is only half the story. The coarsely grained one. Below the grade of a full page change lies all the fine-grained fidelity within a single page. The behavior that shows and hides elements, copies content to a clipboard, adds a new todo to a list, and all the other interactions we associate with a modern web application.
Prior to Stimulus, Basecamp used a smattering of different styles and patterns to apply these sprinkles. Some code was just a pinch of jQuery, some code was a similarly sized pinch of vanilla JavaScript, and some again was larger object-oriented subsystems. They all usually worked off explicit event handling hanging off a `data-behavior` attribute.
While it was easy to add new code like this, it wasn’t a comprehensive solution, and we had too many in-house styles and patterns coexisting. That made it hard to reuse code, and it made it hard for new developers to learn a consistent approach.
### The three core concepts in Stimulus
Stimulus rolls up the best of those patterns into a modest, small framework revolving around just three main concepts: Controllers, actions, and targets.
It’s designed to read as a progressive enhancement when you look at the HTML it’s addressing. Such that you can look at a single template and know which behavior is acting upon it. Here’s an example:
```html
<div data-controller="clipboard">
PIN: <input data-clipboard-target="source" type="text" value="1234" readonly>
<button data-action="clipboard#copy">Copy to Clipboard</button>
</div>
```
You can read that and have a pretty good idea of what’s going on. Even without knowing anything about Stimulus or looking at the controller code itself. It’s almost like pseudocode. That’s very different from reading a slice of HTML that has an external JavaScript file apply event handlers to it. It also maintains the separation of concerns that has been lost in many contemporary JavaScript frameworks.
As you can see, Stimulus doesn’t bother itself with creating the HTML. Rather, it attaches itself to an existing HTML document. The HTML is, in the majority of cases, rendered on the server either on the page load (first hit or via Turbo) or via an Ajax request that changes the DOM.
Stimulus is concerned with manipulating this existing HTML document. Sometimes that means adding a CSS class that hides an element or animates it or highlights it. Sometimes it means rearranging elements in groupings. Sometimes it means manipulating the content of an element, like when we transform UTC times that can be cached into local times that can be displayed.
There are cases where you’d want Stimulus to create new DOM elements, and you’re definitely free to do that. We might even add some sugar to make it easier in the future. But it’s the minority use case. The focus is on manipulating, not creating elements.
### How Stimulus differs from mainstream JavaScript frameworks
This makes Stimulus very different from the majority of contemporary JavaScript frameworks. Almost all are focused on turning JSON into DOM elements via a template language of some sort. Many use these frameworks to birth an empty page, which is then filled exclusively with elements created through this JSON-to-template rendering.
Stimulus also differs on the question of state. Most frameworks have ways of maintaining state within JavaScript objects, and then render HTML based on that state. Stimulus is the exact opposite. State is stored in the HTML, so that controllers can be discarded between page changes, but still reinitialize as they were when the cached HTML appears again.
It really is a remarkably different paradigm. One that I’m sure many veteran JavaScript developers who’ve been used to work with contemporary frameworks will scoff at. And hey, scoff away. If you’re happy with the complexity and effort it takes to maintain an application within the maelstrom of, say, React + Redux, then Turbo + Stimulus will not appeal to you.
If, on the other hand, you have nagging sense that what you’re working on does not warrant the intense complexity and application separation such contemporary techniques imply, then you’re likely to find refuge in our approach.
### Stimulus and related ideas were extracted from the wild
At Basecamp we’ve used this architecture across several different versions of Basecamp and other applications for years. GitHub has used a similar approach to great effect. This is not only a valid alternative to the mainstream understanding of what a “modern” web application looks like, it’s an incredibly compelling one.
In fact, it feels like the same kind of secret sauce we had at Basecamp when we developed [Ruby on Rails](https://rubyonrails.org/). The sense that contemporary mainstream approaches are needlessly convoluted, and that we can do more, faster, with far less.
Furthermore, you don’t even have to choose. Stimulus and Turbo work great in conjunction with other, heavier approaches. If 80% of your application does not warrant the big rig, consider using our two-pack punch for that. Then roll out the heavy machinery for the part of your application that can really benefit from it.
At Basecamp, we have and do use several heavier-duty approaches when the occasion calls for it. Our calendars tend to use client-side rendering. Our text editor is [Trix](https://trix-editor.org/), a fully formed text processor that wouldn’t make sense as a set of Stimulus controllers.
This set of alternative frameworks is about avoiding the heavy lifting as much as possible. To stay within the request-response paradigm for all the many, many interactions that work well with that simple model. Then reaching for the expensive tooling when there’s a call for peak fidelity.
Above all, it’s a toolkit for small teams who want to compete on fidelity and reach with much larger teams using more laborious, mainstream approaches.
Give it a go.
---
David Heinemeier Hansson
================================================
FILE: docs/handbook/01_introduction.md
================================================
---
permalink: /handbook/introduction.html
order: 01
---
# Introduction
## About Stimulus
Stimulus is a JavaScript framework with modest ambitions. Unlike other front-end frameworks, Stimulus is designed to enhance _static_ or _server-rendered_ HTML—the "HTML you already have"—by connecting JavaScript objects to elements on the page using simple annotations.
These JavaScript objects are called _controllers_, and Stimulus continuously monitors the page waiting for HTML `data-controller` attributes to appear. For each attribute, Stimulus looks at the attribute's value to find a corresponding controller class, creates a new instance of that class, and connects it to the element.
You can think of it this way: just like the `class` attribute is a bridge connecting HTML to CSS, Stimulus's `data-controller` attribute is a bridge connecting HTML to JavaScript.
Aside from controllers, the three other major Stimulus concepts are:
* _actions_, which connect controller methods to DOM events using `data-action` attributes
* _targets_, which locate elements of significance within a controller
* _values_, which read, write, and observe data attributes on the controller's element
Stimulus's use of data attributes helps separate content from behavior in the same way CSS separates content from presentation. Further, Stimulus's conventions naturally encourage you to group related code by name.
In turn, Stimulus helps you build small, reusable controllers, giving you just enough structure to keep your code from devolving into "JavaScript soup."
## About This Book
This handbook will guide you through Stimulus's core concepts by demonstrating how to write several fully functional controllers. Each chapter builds on the one before it; from start to finish, you'll learn how to:
* print a greeting addressed to the name in a text field
* copy text from a text field to the system clipboard when a button is clicked
* navigate through a slide show with multiple slides
* fetch HTML from the server into an element on the page automatically
* set up Stimulus in your own application
Once you've completed the exercises here, you may find the [reference documentation](../reference/controllers) helpful for understanding technical details about the Stimulus API.
Let's get started!
================================================
FILE: docs/handbook/02_hello_stimulus.md
================================================
---
permalink: /handbook/hello-stimulus.html
order: 02
---
# Hello, Stimulus
The best way to learn how Stimulus works is to build a simple controller. This chapter will show you how.
## Prerequisites
To follow along, you'll need a running copy of the [`stimulus-starter`](https://github.com/hotwired/stimulus-starter) project, which is a preconfigured blank slate for exploring Stimulus.
We recommend [remixing `stimulus-starter` on Glitch](https://glitch.com/edit/#!/import/git?url=https://github.com/hotwired/stimulus-starter.git) so you can work entirely in your browser without installing anything:
[](https://glitch.com/edit/#!/import/git?url=https://github.com/hotwired/stimulus-starter.git)
Or, if you'd prefer to work from the comfort of your own text editor, you'll need to clone and set up `stimulus-starter`:
```
$ git clone https://github.com/hotwired/stimulus-starter.git
$ cd stimulus-starter
$ yarn install
$ yarn start
```
Then visit http://localhost:9000/ in your browser.
(Note that the `stimulus-starter` project uses the [Yarn package manager](https://yarnpkg.com/) for dependency management, so make sure you have that installed first.)
## It All Starts With HTML
Let's begin with a simple exercise using a text field and a button. When you click the button, we'll display the value of the text field in the console.
Every Stimulus project starts with HTML. Open `public/index.html` and add the following markup just after the opening `<body>` tag:
```html
<div>
<input type="text">
<button>Greet</button>
</div>
```
Reload the page in your browser and you should see the text field and button.
## Controllers Bring HTML to Life
At its core, Stimulus's purpose is to automatically connect DOM elements to JavaScript objects. Those objects are called _controllers_.
Let's create our first controller by extending the framework's built-in `Controller` class. Create a new file named `hello_controller.js` in the `src/controllers/` folder. Then place the following code inside:
```js
// src/controllers/hello_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
}
```
## Identifiers Link Controllers With the DOM
Next, we need to tell Stimulus how this controller should be connected to our HTML. We do this by placing an _identifier_ in the `data-controller` attribute on our `<div>`:
```html
<div data-controller="hello">
<input type="text">
<button>Greet</button>
</div>
```
Identifiers serve as the link between elements and controllers. In this case, the identifier `hello` tells Stimulus to create an instance of the controller class in `hello_controller.js`. You can learn more about how automatic controller loading works in the [Installation Guide](/handbook/installing).
## Is This Thing On?
Reload the page in your browser and you'll see that nothing has changed. How do we know whether our controller is working or not?
One way is to put a log statement in the `connect()` method, which Stimulus calls each time a controller is connected to the document.
Implement the `connect()` method in `hello_controller.js` as follows:
```js
// src/controllers/hello_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
console.log("Hello, Stimulus!", this.element)
}
}
```
Reload the page again and open the developer console. You should see `Hello, Stimulus!` followed by a representation of our `<div>`.
## Actions Respond to DOM Events
Now let's see how to change the code so our log message appears when we click the "Greet" button instead.
Start by renaming `connect()` to `greet()`:
```js
// src/controllers/hello_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
greet() {
console.log("Hello, Stimulus!", this.element)
}
}
```
We want to call the `greet()` method when the button's `click` event is triggered. In Stimulus, controller methods which handle events are called _action methods_.
To connect our action method to the button's `click` event, open `public/index.html` and add a `data-action` attribute to the button:
```html
<div data-controller="hello">
<input type="text">
<button data-action="click->hello#greet">Greet</button>
</div>
```
> ### Action Descriptors Explained
>
> The `data-action` value `click->hello#greet` is called an _action descriptor_. This particular descriptor says:
> * `click` is the event name
> * `hello` is the controller identifier
> * `greet` is the name of the method to invoke
Load the page in your browser and open the developer console. You should see the log message appear when you click the "Greet" button.
## Targets Map Important Elements To Controller Properties
We'll finish the exercise by changing our action to say hello to whatever name we've typed in the text field.
In order to do that, first we need a reference to the input element inside our controller. Then we can read the `value` property to get its contents.
Stimulus lets us mark important child elements as _targets_ so we can easily reference them in the controller through corresponding properties. Open `public/index.html` and add a `data-hello-target` attribute to the input element:
```html
<div data-controller="hello">
<input data-hello-target="name" type="text">
<button data-action="click->hello#greet">Greet</button>
</div>
```
Next, we'll create a property for the target by adding `name` to our controller's list of target definitions. Stimulus will automatically create a `this.nameTarget` property which returns the first matching target element. We can use this property to read the element's `value` and build our greeting string.
Let's try it out. Open `hello_controller.js` and update it like so:
```js
// src/controllers/hello_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "name" ]
greet() {
const element = this.nameTarget
const name = element.value
console.log(`Hello, ${name}!`)
}
}
```
Then reload the page in your browser and open the developer console. Enter your name in the input field and click the "Greet" button. Hello, world!
## Controllers Simplify Refactoring
We've seen that Stimulus controllers are instances of JavaScript classes whose methods can act as event handlers.
That means we have an arsenal of standard refactoring techniques at our disposal. For example, we can clean up our `greet()` method by extracting a `name` getter:
```js
// src/controllers/hello_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "name" ]
greet() {
console.log(`Hello, ${this.name}!`)
}
get name() {
return this.nameTarget.value
}
}
```
## Wrap-Up and Next Steps
Congratulations—you've just written your first Stimulus controller!
We've covered the framework's most important concepts: controllers, actions, and targets. In the next chapter, we'll see how to put those together to build a real-life controller taken right from Basecamp.
================================================
FILE: docs/handbook/03_building_something_real.md
================================================
---
permalink: /handbook/building-something-real.html
order: 03
---
# Building Something Real
We've implemented our first controller and learned how Stimulus connects HTML to JavaScript. Now let's take a look at something we can use in a real application by recreating a controller from Basecamp.
## Wrapping the DOM Clipboard API
Scattered throughout Basecamp's UI are buttons like these:
<img src="../../assets/bc3-clipboard-ui.png" width="1023" height="317" class="docs__screenshot" alt="Screenshot showing a text field with an email address inside and a ”Copy to clipboard“ button to the right">
When you click one of these buttons, Basecamp copies a bit of text, such as a URL or an email address, to your clipboard.
The web platform has [an API for accessing the system clipboard](https://www.w3.org/TR/clipboard-apis/), but there's no HTML element that does what we need. To implement a "Copy to clipboard" button, we must use JavaScript.
## Implementing a Copy Button
Let's say we have an app which allows us to grant someone else access by generating a PIN for them. It would be convenient if we could display that generated PIN alongside a button to copy it to the clipboard for easy sharing.
Open `public/index.html` and replace the contents of `<body>` with a rough sketch of the button:
```html
<div>
PIN: <input type="text" value="1234" readonly>
<button>Copy to Clipboard</button>
</div>
```
## Setting Up the Controller
Next, create `src/controllers/clipboard_controller.js` and add an empty method `copy()`:
```js
// src/controllers/clipboard_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
copy() {
}
}
```
Then add `data-controller="clipboard"` to the outer `<div>`. Any time this attribute appears on an element, Stimulus will connect an instance of our controller:
```html
<div data-controller="clipboard">
```
## Defining the Target
We'll need a reference to the text field so we can select its contents before invoking the clipboard API. Add `data-clipboard-target="source"` to the text field:
```html
PIN: <input data-clipboard-target="source" type="text" value="1234" readonly>
```
Now add a target definition to the controller so we can access the text field element as `this.sourceTarget`:
```js
export default class extends Controller {
static targets = [ "source" ]
// ...
}
```
> ### What's With That `static targets` Line?
>
> When Stimulus loads your controller class, it looks for target name strings in a static array called `targets`. For each target name in the array, Stimulus adds three new properties to your controller. Here, our `"source"` target name becomes the following properties:
>
> * `this.sourceTarget` evaluates to the first `source` target in your controller's scope. If there is no `source` target, accessing the property throws an error.
> * `this.sourceTargets` evaluates to an array of all `source` targets in the controller's scope.
> * `this.hasSourceTarget` evaluates to `true` if there is a `source` target or `false` if not.
>
> You can read more about targets in the [reference documentation](/reference/targets).
## Connecting the Action
Now we're ready to hook up the Copy button.
We want a click on the button to invoke the `copy()` method in our controller, so we'll add `data-action="clipboard#copy"`:
```html
<button data-action="clipboard#copy">Copy to Clipboard</button>
```
> ### Common Events Have a Shorthand Action Notation
>
> You might have noticed we've omitted `click->` from the action descriptor. That's because Stimulus defines `click` as the default event for actions on `<button>` elements.
>
> Certain other elements have default events, too. Here's the full list:
>
> | Element | Default Event |
> | ----------------- | ------------- |
> | a | click |
> | button | click |
> | details | toggle |
> | form | submit |
> | input | input |
> | input type=submit | click |
> | select | change |
> | textarea | input |
Finally, in our `copy()` method, we can select the input field's contents and call the clipboard API:
```js
copy() {
navigator.clipboard.writeText(this.sourceTarget.value)
}
```
Load the page in your browser and click the Copy button. Then switch back to your text editor and paste. You should see the PIN `1234`.
## Stimulus Controllers are Reusable
So far we've seen what happens when there's one instance of a controller on the page at a time.
It's not unusual to have multiple instances of a controller on the page simultaneously. For example, we might want to display a list of PINs, each with its own Copy button.
Our controller is reusable: any time we want to provide a way to copy a bit of text to the clipboard, all we need is markup on the page with the right annotations.
Let's go ahead and add another PIN to the page. Copy and paste the `<div>` so there are two identical PIN fields, then change the `value` attribute of the second:
```html
<div data-controller="clipboard">
PIN: <input data-clipboard-target="source" type="text" value="3737" readonly>
<button data-action="clipboard#copy">Copy to Clipboard</button>
</div>
```
Reload the page and confirm that both buttons work.
## Actions and Targets Can Go on Any Kind of Element
Now let's add one more PIN field. This time we'll use a Copy _link_ instead of a button:
```html
<div data-controller="clipboard">
PIN: <input data-clipboard-target="source" type="text" value="3737" readonly>
<a href="#" data-action="clipboard#copy">Copy to Clipboard</a>
</div>
```
Stimulus lets us use any kind of element we want as long as it has an appropriate `data-action` attribute, and is a child of the controller component.
Note that in this case, clicking the link will also cause the browser to follow the link's `href`. We can cancel this default behavior by calling `event.preventDefault()` in the action:
```js
copy(event) {
event.preventDefault()
navigator.clipboard.writeText(this.sourceTarget.value)
}
```
Similarly, our `source` target need not be an `<input type="text">`. The controller only expects it to have a `value` property and a `copy()` method. That means we can use a `<textarea>` instead:
```html
PIN: <textarea data-clipboard-target="source" readonly>3737</textarea>
```
## Wrap-Up and Next Steps
In this chapter we looked at a real-life example of wrapping a browser API in a Stimulus controller. We saw how multiple instances of the controller can appear on the page at once, and we explored how actions and targets keep your HTML and JavaScript loosely coupled.
Now let's see how small changes to the controller's design can lead us to a more robust implementation.
================================================
FILE: docs/handbook/04_designing_for_resilience.md
================================================
---
permalink: /handbook/designing-for-resilience.html
order: 04
---
# Designing For Resilience
Although the clipboard API is [well-supported in current browsers](https://caniuse.com/#feat=clipboard), we might still expect to have a small number of people with older browsers using our application.
We should also expect people to have problems accessing our application from time to time. For example, intermittent network connectivity or CDN availability could prevent some or all of our JavaScript from loading.
It's tempting to write off support for older browsers as not worth the effort, or to dismiss network issues as temporary glitches that resolve themselves after a refresh. But often it's trivially easy to build features in a way that's gracefully resilient to these types of problems.
This resilient approach, commonly known as _progressive enhancement_, is the practice of delivering web interfaces such that the basic functionality is implemented in HTML and CSS, and tiered upgrades to that base experience are layered on top with CSS and JavaScript, progressively, when their underlying technologies are supported by the browser.
## Progressively Enhancing the PIN Field
Let's look at how we can progressively enhance our PIN field so that the Copy button is invisible unless it's supported by the browser. That way we can avoid showing someone a button that doesn't work.
We'll start by hiding the Copy button in CSS. Then we'll _feature-test_ support for the Clipboard API in our Stimulus controller. If the API is supported, we'll add a class name to the controller element to reveal the button.
We start off by adding `data-clipboard-supported-class="clipboard--supported"` to the `div` element that has the `data-controller` attribute:
```html
<div data-controller="clipboard" data-clipboard-supported-class="clipboard--supported">
```
Then add `class="clipboard-button"` to the button element:
```html
<button data-action="clipboard#copy" class="clipboard-button">Copy to Clipboard</button>
```
Then add the following styles to `public/main.css`:
```css
.clipboard-button {
display: none;
}
.clipboard--supported .clipboard-button {
display: initial;
}
```
First we'll add the `data-clipboard-supported-class` attribute inside the controller as a static class:
```js
static classes = [ "supported" ]
```
This will let us control the specific CSS class in the HTML, so our controller becomes even more easily adaptable to different CSS approaches. The specific class added like this can be accessed via `this.supportedClass`.
Now add a `connect()` method to the controller which will test to see if the clipboard API is supported and add a class name to the controller's element:
```js
connect() {
if ("clipboard" in navigator) {
this.element.classList.add(this.supportedClass);
}
}
```
You can place this method anywhere in the controller's class body.
If you wish, disable JavaScript in your browser, reload the page, and notice the Copy button is no longer visible.
We have progressively enhanced the PIN field: its Copy button's baseline state is hidden, becoming visible only when our JavaScript detects support for the clipboard API.
## Wrap-Up and Next Steps
In this chapter we gently modified our clipboard controller to be resilient against older browsers and degraded network conditions.
Next, we'll learn about how Stimulus controllers manage state.
================================================
FILE: docs/handbook/05_managing_state.md
================================================
---
permalink: /handbook/managing-state.html
order: 05
---
# Managing State
Most contemporary frameworks encourage you to keep state in JavaScript at all times. They treat the DOM as a write-only rendering target, reconciled by client-side templates consuming JSON from the server.
Stimulus takes a different approach. A Stimulus application's state lives as attributes in the DOM; controllers themselves are largely stateless. This approach makes it possible to work with HTML from anywhere—the initial document, an Ajax request, a Turbo visit, or even another JavaScript library—and have associated controllers spring to life automatically without any explicit initialization step.
## Building a Slideshow
In the last chapter, we learned how a Stimulus controller can maintain simple state in the document by adding a class name to an element. But what do we do when we need to store a value, not just a simple flag?
We'll investigate this question by building a slideshow controller which keeps its currently selected slide index in an attribute.
As usual, we'll begin with HTML:
```html
<div data-controller="slideshow">
<button data-action="slideshow#previous"> ← </button>
<button data-action="slideshow#next"> → </button>
<div data-slideshow-target="slide">🐵</div>
<div data-slideshow-target="slide">🙈</div>
<div data-slideshow-target="slide">🙉</div>
<div data-slideshow-target="slide">🙊</div>
</div>
```
Each `slide` target represents a single slide in the slideshow. Our controller will be responsible for making sure only one slide is visible at a time.
Let's draft our controller. Create a new file, `src/controllers/slideshow_controller.js`, as follows:
```js
// src/controllers/slideshow_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "slide" ]
initialize() {
this.index = 0
this.showCurrentSlide()
}
next() {
this.index++
this.showCurrentSlide()
}
previous() {
this.index--
this.showCurrentSlide()
}
showCurrentSlide() {
this.slideTargets.forEach((element, index) => {
element.hidden = index !== this.index
})
}
}
```
Our controller defines a method, `showCurrentSlide()`, which loops over each slide target, toggling the [`hidden` attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/hidden) if its index matches.
We initialize the controller by showing the first slide, and the `next()` and `previous()` action methods advance and rewind the current slide.
> ### Lifecycle Callbacks Explained
>
> What does the `initialize()` method do? How is it different from the `connect()` method we've used before?
>
> These are Stimulus _lifecycle callback_ methods, and they're useful for setting up or tearing down associated state when your controller enters or leaves the document.
>
> Method | Invoked by Stimulus…
> ------------ | --------------------
> initialize() | Once, when the controller is first instantiated
> connect() | Anytime the controller is connected to the DOM
> disconnect() | Anytime the controller is disconnected from the DOM
Reload the page and confirm that the Next button advances to the next slide.
## Reading Initial State from the DOM
Notice how our controller tracks its state—the currently selected slide—in the `this.index` property.
Now say we'd like to start one of our slideshows with the second slide visible instead of the first. How can we encode the start index in our markup?
One way might be to load the initial index with an HTML `data` attribute. For example, we could add a `data-index` attribute to the controller's element:
```html
<div data-controller="slideshow" data-index="1">
```
Then, in our `initialize()` method, we could read that attribute, convert it to an integer, and pass it to `showCurrentSlide()`:
```js
initialize() {
this.index = Number(this.element.dataset.index)
this.showCurrentSlide()
}
```
This might get the job done, but it's clunky, requires us to make a decision about what to name the attribute, and doesn't help us if we want to access the index again or increment it and persist the result in the DOM.
### Using Values
Stimulus controllers support typed value properties which automatically map to data attributes. When we add a value definition to the top of our controller class:
```js
static values = { index: Number }
```
Stimulus will create a `this.indexValue` controller property associated with a `data-slideshow-index-value` attribute, and handle the numeric conversion for us.
Let's see that in action. Add the associated data attribute to our HTML:
```html
<div data-controller="slideshow" data-slideshow-index-value="1">
```
Then add a `static values` definition to the controller and change the `initialize()` method to log `this.indexValue`:
```js
export default class extends Controller {
static values = { index: Number }
initialize() {
console.log(this.indexValue)
console.log(typeof this.indexValue)
}
// …
}
```
Reload the page and verify that the console shows `1` and `Number`.
> ### What's with that `static values` line?
>
> Similar to targets, you define values in a Stimulus controller by describing them in a static object property called `values`. In this case, we've defined a single numeric value called `index`. You can read more about value definitions in the [reference documentation](/reference/values).
Now let's update `initialize()` and the other methods in the controller to use `this.indexValue` instead of `this.index`. Here's how the controller should look when we're done:
```js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "slide" ]
static values = { index: Number }
initialize() {
this.showCurrentSlide()
}
next() {
this.indexValue++
this.showCurrentSlide()
}
previous() {
this.indexValue--
this.showCurrentSlide()
}
showCurrentSlide() {
this.slideTargets.forEach((element, index) => {
element.hidden = index !== this.indexValue
})
}
}
```
Reload the page and use the web inspector to confirm the controller element's `data-slideshow-index-value` attribute changes as you move from one slide to the next.
### Change Callbacks
Our revised controller improves on the original version, but the repeated calls to `this.showCurrentSlide()` stand out. We have to manually update the state of the document when the controller initializes and after every place where we update `this.indexValue`.
We can define a Stimulus value change callback to clean up the repetition and specify how the controller should respond whenever the index value changes.
First, remove the `initialize()` method and define a new method, `indexValueChanged()`. Then remove the calls to `this.showCurrentSlide()` from `next()` and `previous()`:
```js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "slide" ]
static values = { index: Number }
next() {
this.indexValue++
}
previous() {
this.indexValue--
}
indexValueChanged() {
this.showCurrentSlide()
}
showCurrentSlide() {
this.slideTargets.forEach((element, index) => {
element.hidden = index !== this.indexValue
})
}
}
```
Reload the page and confirm the slideshow behavior is the same.
Stimulus calls the `indexValueChanged()` method at initialization and in response to any change to the `data-slideshow-index-value` attribute. You can even fiddle with the attribute in the web inspector and the controller will change slides in response. Go ahead—try it out!
### Setting Defaults
It's also possible to set a default values as part of the static definition. This is done like so:
```js
static values = { index: { type: Number, default: 2 } }
```
That would start the index at 2, if no `data-slideshow-index-value` attribute was defined on the controller element. If you had other values, you can mix and match what needs a default and what doesn't:
```js
static values = { index: Number, effect: { type: String, default: "kenburns" } }
```
## Wrap-Up and Next Steps
In this chapter we've seen how to use the values to load and persist the current index of a slideshow controller.
From a usability perspective, our controller is incomplete. The Previous button appears to do nothing when you are looking at the first slide. Internally, `indexValue` decrements from `0` to `-1`. Could we make the value wrap around to the _last_ slide index instead? (There's a similar problem with the Next button.)
Next we'll look at how to keep track of external resources, such as timers and HTTP requests, in Stimulus controllers.
================================================
FILE: docs/handbook/06_working_with_external_resources.md
================================================
---
permalink: /handbook/working-with-external-resources.html
order: 06
---
# Working With External Resources
In the last chapter we learned how to load and persist a controller's internal state using values.
Sometimes our controllers need to track the state of external resources, where by _external_ we mean anything that isn't in the DOM or a part of Stimulus. For example, we may need to issue an HTTP request and respond as the request's state changes. Or we may want to start a timer and then stop it when the controller is no longer connected. In this chapter we'll see how to do both of those things.
## Asynchronously Loading HTML
Let's learn how to populate parts of a page asynchronously by loading and inserting remote fragments of HTML. We use this technique in Basecamp to keep our initial page loads fast, and to keep our views free of user-specific content so they can be cached more effectively.
We'll build a general-purpose content loader controller which populates its element with HTML fetched from the server. Then we'll use it to load a list of unread messages like you'd see in an email inbox.
Begin by sketching the inbox in `public/index.html`:
```html
<div data-controller="content-loader"
data-content-loader-url-value="/messages.html"></div>
```
Then create a new `public/messages.html` file with some HTML for our message list:
```html
<ol>
<li>New Message: Stimulus Launch Party</li>
<li>Overdue: Finish Stimulus 1.0</li>
</ol>
```
(In a real application you'd generate this HTML dynamically on the server, but for demonstration purposes we'll just use a static file.)
Now we can implement our controller:
```js
// src/controllers/content_loader_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { url: String }
connect() {
this.load()
}
load() {
fetch(this.urlValue)
.then(response => response.text())
.then(html => this.element.innerHTML = html)
}
}
```
When the controller connects, we kick off a [Fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) request to the URL specified in the element's `data-content-loader-url-value` attribute. Then we load the returned HTML by assigning it to our element's `innerHTML` property.
Open the network tab in your browser's developer console and reload the page. You'll see a request representing the initial page load, followed by our controller's subsequent request to `messages.html`.
## Refreshing Automatically With a Timer
Let's improve our controller by changing it to periodically refresh the inbox so it's always up-to-date.
We'll use the `data-content-loader-refresh-interval-value` attribute to specify how often the controller should reload its contents, in milliseconds:
```html
<div data-controller="content-loader"
data-content-loader-url-value="/messages.html"
data-content-loader-refresh-interval-value="5000"></div>
```
Now we can update the controller to check for the interval and, if present, start a refresh timer.
Add a `static values` definition to the controller, and define a new method `startRefreshing()`:
```js
export default class extends Controller {
static values = { url: String, refreshInterval: Number }
startRefreshing() {
setInterval(() => {
this.load()
}, this.refreshIntervalValue)
}
// …
}
```
Then update the `connect()` method to call `startRefreshing()` if an interval value is present:
```js
connect() {
this.load()
if (this.hasRefreshIntervalValue) {
this.startRefreshing()
}
}
```
Reload the page and observe a new request once every five seconds in the developer console. Then make a change to `public/messages.html` and wait for it to appear in the inbox.
## Releasing Tracked Resources
We start our timer when the controller connects, but we never stop it. That means if our controller's element were to disappear, the controller would continue to issue HTTP requests in the background.
We can fix this issue by modifying our `startRefreshing()` method to keep a reference to the timer:
```js
startRefreshing() {
this.refreshTimer = setInterval(() => {
this.load()
}, this.refreshIntervalValue)
}
```
Then we can add a corresponding `stopRefreshing()` method below to cancel the timer:
```js
stopRefreshing() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer)
}
}
```
Finally, to instruct Stimulus to cancel the timer when the controller disconnects, we'll add a `disconnect()` method:
```js
disconnect() {
this.stopRefreshing()
}
```
Now we can be sure a content loader controller will only issue requests when it's connected to the DOM.
Let's take a look at our final controller class:
```js
// src/controllers/content_loader_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = { url: String, refreshInterval: Number }
connect() {
this.load()
if (this.hasRefreshIntervalValue) {
this.startRefreshing()
}
}
disconnect() {
this.stopRefreshing()
}
load() {
fetch(this.urlValue)
.then(response => response.text())
.then(html => this.element.innerHTML = html)
}
startRefreshing() {
this.refreshTimer = setInterval(() => {
this.load()
}, this.refreshIntervalValue)
}
stopRefreshing() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer)
}
}
}
```
## Using action parameters
If we wanted to make the loader work with multiple different sources, we could do it using action parameters. Take this HTML:
```html
<div data-controller="content-loader">
<a href="#" data-content-loader-url-param="/messages.html" data-action="content-loader#load">Messages</a>
<a href="#" data-content-loader-url-param="/comments.html" data-action="content-loader#load">Comments</a>
</div>
```
Then we can use those parameters through the `load` action:
```js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
load({ params }) {
fetch(params.url)
.then(response => response.text())
.then(html => this.element.innerHTML = html)
}
}
```
We could even destruct the params to just get the URL parameter:
```js
load({ params: { url } }) {
fetch(url)
.then(response => response.text())
.then(html => this.element.innerHTML = html)
}
```
## Wrap-Up and Next Steps
In this chapter we've seen how to acquire and release external resources using Stimulus lifecycle callbacks.
Next we'll see how to install and configure Stimulus in your own application.
================================================
FILE: docs/handbook/07_installing_stimulus.md
================================================
---
permalink: /handbook/installing.html
order: 07
---
# Installing Stimulus in Your Application
To install Stimulus in your application, add the [`@hotwired/stimulus` npm package](https://www.npmjs.com/package/@hotwired/stimulus) to your JavaScript bundle. Or, import [`stimulus.js`](https://unpkg.com/@hotwired/stimulus/dist/stimulus.js) in a `<script type="module">` tag.
## Using Stimulus for Rails
If you're using [Stimulus for Rails](https://github.com/hotwired/stimulus-rails/) together with an [import map](https://github.com/rails/importmap-rails), the integration will automatically load all controller files from `app/javascript/controllers`.
### Controller Filenames Map to Identifiers
Name your controller files `[identifier]_controller.js`, where `identifier` corresponds to each controller's `data-controller` identifier in your HTML.
Stimulus for Rails conventionally separates multiple words in filenames using underscores. Each underscore in a controller's filename translates to a dash in its identifier.
You may also namespace your controllers using subfolders. Each forward slash in a namespaced controller file's path becomes two dashes in its identifier.
If you prefer, you may use dashes instead of underscores anywhere in a controller's filename. Stimulus treats them identically.
If your controller file is named… | its identifier will be…
--------------------------------- | -----------------------
clipboard_controller.js | clipboard
date_picker_controller.js | date-picker
users/list_item_controller.js | users\-\-list-item
local-time-controller.js | local-time
## Using Webpack Helpers
If you're using Webpack as your JavaScript bundler, you can use the [@hotwired/stimulus-webpack-helpers](https://www.npmjs.com/package/@hotwired/stimulus-webpack-helpers) package to get the same form of autoloading behavior as Stimulus for Rails. First add the package, then use it like this:
```js
import { Application } from "@hotwired/stimulus"
import { definitionsFromContext } from "@hotwired/stimulus-webpack-helpers"
window.Stimulus = Application.start()
const context = require.context("./controllers", true, /\.js$/)
Stimulus.load(definitionsFromContext(context))
```
## Using Other Build Systems
Stimulus works with other build systems too, but without support for controller autoloading. Instead, you must explicitly load and register controller files with your application instance:
```js
// src/application.js
import { Application } from "@hotwired/stimulus"
import HelloController from "./controllers/hello_controller"
import ClipboardController from "./controllers/clipboard_controller"
window.Stimulus = Application.start()
Stimulus.register("hello", HelloController)
Stimulus.register("clipboard", ClipboardController)
```
If you're using stimulus-rails with a builder like esbuild, you can use the `stimulus:manifest:update` Rake task and `./bin/rails generate stimulus [controller]` generator to keep a controller index file located at `app/javascript/controllers/index.js` automatically updated.
## Using Without a Build System
If you prefer not to use a build system, you can load Stimulus in a `<script type="module">` tag:
```html
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<script type="module">
import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"
window.Stimulus = Application.start()
Stimulus.register("hello", class extends Controller {
static targets = [ "name" ]
connect() {
}
})
</script>
</head>
<body>
<div data-controller="hello">
<input data-hello-target="name" type="text">
…
</div>
</body>
</html>
```
## Overriding Attribute Defaults
In case Stimulus `data-*` attributes conflict with another library in your project, they can be overridden when creating the Stimulus `Application`.
- `data-controller`
- `data-action`
- `data-target`
These core Stimulus attributes can be overridden (see: [schema.ts](https://github.com/hotwired/stimulus/blob/main/src/core/schema.ts)):
```js
// src/application.js
import { Application, defaultSchema } from "@hotwired/stimulus"
const customSchema = {
...defaultSchema,
actionAttribute: 'data-stimulus-action'
}
window.Stimulus = Application.start(document.documentElement, customSchema);
```
## Error handling
All calls from Stimulus to your application's code are wrapped in a `try ... catch` block.
If your code throws an error, it will be caught by Stimulus and logged to the browser console, including extra detail such as the controller name and event or lifecycle function being called. If you use an error tracking system that defines `window.onerror`, Stimulus will also pass the error on to it.
You can override how Stimulus handles errors by defining `Application#handleError`:
```js
// src/application.js
import { Application } from "@hotwired/stimulus"
window.Stimulus = Application.start()
Stimulus.handleError = (error, message, detail) => {
console.warn(message, detail)
ErrorTrackingSystem.captureException(error)
}
```
## Debugging
If you've assigned your Stimulus application to `window.Stimulus`, you can turn on [debugging mode](https://github.com/hotwired/stimulus/pull/354) from the console with `Stimulus.debug = true`. You can also set this flag when you're configuring your application instance in the source code.
## Browser Support
Stimulus supports all evergreen, self-updating desktop and mobile browsers out of the box. Stimulus 3+ does not support Internet Explorer 11 (but you can use Stimulus 2 with the @stimulus/polyfills for that).
================================================
FILE: docs/reference/actions.md
================================================
---
permalink: /reference/actions.html
order: 02
---
# Actions
_Actions_ are how you handle DOM events in your controllers.
<meta data-controller="callout" data-callout-text-value="click->gallery#next">
```html
<div data-controller="gallery">
<button data-action="click->gallery#next">…</button>
</div>
```
<meta data-controller="callout" data-callout-text-value="next">
```js
// controllers/gallery_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
next(event) {
// …
}
}
```
An action is a connection between:
* a controller method
* the controller's element
* a DOM event listener
## Descriptors
The `data-action` value `click->gallery#next` is called an _action descriptor_. In this descriptor:
* `click` is the name of the DOM event to listen for
* `gallery` is the controller identifier
* `next` is the name of the method to invoke
### Event Shorthand
Stimulus lets you shorten the action descriptors for some common element/event pairs, such as the button/click pair above, by omitting the event name:
<meta data-controller="callout" data-callout-text-value="gallery#next">
```html
<button data-action="gallery#next">…</button>
```
The full set of these shorthand pairs is as follows:
Element | Default Event
----------------- | -------------
a | click
button | click
details | toggle
form | submit
input | input
input type=submit | click
select | change
textarea | input
## KeyboardEvent Filter
There may be cases where [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent) Actions should only call the Controller method when certain keystrokes are used.
You can install an event listener that responds only to the `Escape` key by adding `.esc` to the event name of the action descriptor, as in the following example.
```html
<div data-controller="modal"
data-action="keydown.esc->modal#close" tabindex="0">
</div>
```
This will only work if the event being fired is a keyboard event.
The correspondence between these filter and keys is shown below.
Filter | Key Name
-------- | --------
enter | Enter
tab | Tab
esc | Escape
space | " "
up | ArrowUp
down | ArrowDown
left | ArrowLeft
right | ArrowRight
home | Home
end | End
page_up | PageUp
page_down | PageDown
[a-z] | [a-z]
[0-9] | [0-9]
If you need to support other keys, you can customize the modifiers using a custom schema.
```javascript
import { Application, defaultSchema } from "@hotwired/stimulus"
const customSchema = {
...defaultSchema,
keyMappings: { ...defaultSchema.keyMappings, at: "@" },
}
const app = Application.start(document.documentElement, customSchema)
```
If you want to subscribe to a compound filter using a modifier key, you can write it like `ctrl+a`.
```html
<div data-action="keydown.ctrl+a->listbox#selectAll" role="option" tabindex="0">...</div>
```
The list of supported modifier keys is shown below.
| Modifier | Notes |
| -------- | ------------------ |
| `alt` | `option` on MacOS |
| `ctrl` | |
| `meta` | Command key on MacOS |
| `shift` | |
### Global Events
Sometimes a controller needs to listen for events dispatched on the global `window` or `document` objects.
You can append `@window` or `@document` to the event name (along with any filter modifier) in an action descriptor to install the event listener on `window` or `document`, respectively, as in the following example:
<meta data-controller="callout" data-callout-text-value="resize@window">
```html
<div data-controller="gallery"
data-action="resize@window->gallery#layout">
</div>
```
### Options
You can append one or more _action options_ to an action descriptor if you need to specify [DOM event listener options](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Parameters).
<meta data-controller="callout" data-callout-text-value=":!passive">
<meta data-controller="callout" data-callout-text-value=":capture">
```html
<div data-controller="gallery"
data-action="scroll->gallery#layout:!passive">
<img data-action="click->gallery#open:capture">
```
Stimulus supports the following action options:
Action option | DOM event listener option
------------- | -------------------------
`:capture` | `{ capture: true }`
`:once` | `{ once: true }`
`:passive` | `{ passive: true }`
`:!passive` | `{ passive: false }`
On top of that, Stimulus also supports the following action options which are not natively supported by the DOM event listener options:
Custom action option | Description
-------------------- | -----------
`:stop` | calls `.stopPropagation()` on the event before invoking the method
`:prevent` | calls `.preventDefault()` on the event before invoking the method
`:self` | only invokes the method if the event was fired by the element itself
You can register your own action options with the `Application.registerActionOption` method.
For example, consider that a `<details>` element will dispatch a [toggle][]
event whenever it's toggled. A custom `:open` action option would help
to route events whenever the element is toggled _open_:
```javascript
import { Application } from "@hotwired/stimulus"
const application = Application.start()
application.registerActionOption("open", ({ event }) => {
if (event.type == "toggle") {
return event.target.open == true
} else {
return true
}
})
```
Similarly, a custom `:!open` action option could route events whenever the
element is toggled _closed_. Declaring the action descriptor option with a `!`
prefix will yield a `value` argument set to `false` in the callback:
```javascript
import { Application } from "@hotwired/stimulus"
const application = Application.start()
application.registerActionOption("open", ({ event, value }) => {
if (event.type == "toggle") {
return event.target.open == value
} else {
return true
}
})
```
In order to prevent the event from being routed to the controller action, the
`registerActionOption` callback function must return `false`. Otherwise, to
route the event to the controller action, return `true`.
The callback accepts a single object argument with the following keys:
| Name | Description |
| ---------- | ----------------------------------------------------------------------------------------------------- |
| name | String: The option's name (`"open"` in the example above) |
| value | Boolean: The value of the option (`:open` would yield `true`, `:!open` would yield `false`) |
| event | [Event][]: The event instance, including with the `params` action parameters on the submitter element |
| element | [Element]: The element where the action descriptor is declared |
| controller | The `Controller` instance which would receive the method call |
[toggle]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLDetailsElement/toggle_event
[Event]: https://developer.mozilla.org/en-US/docs/web/api/event
[Element]: https://developer.mozilla.org/en-US/docs/Web/API/element
## Event Objects
An _action method_ is the method in a controller which serves as an action's event listener.
The first argument to an action method is the DOM _event object_. You may want access to the event for a number of reasons, including:
* to read the key code from a keyboard event
* to read the coordinates of a mouse event
* to read data from an input event
* to read params from the action submitter element
* to prevent the browser's default behavior for an event
* to find out which element dispatched an event before it bubbled up to this action
The following basic properties are common to all events:
Event Property | Value
------------------- | -----
event.type | The name of the event (e.g. `"click"`)
event.target | The target that dispatched the event (i.e. the innermost element that was clicked)
event.currentTarget | The target on which the event listener is installed (either the element with the `data-action` attribute, or `document` or `window`)
event.params | The action params passed by the action submitter element
<br>The following event methods give you more control over how events are handled:
Event Method | Result
----------------------- | ------
event.preventDefault() | Cancels the event's default behavior (e.g. following a link or submitting a form)
event.stopPropagation() | Stops the event before it bubbles up to other listeners on parent elements
## Multiple Actions
The `data-action` attribute's value is a space-separated list of action descriptors.
It's common for any given element to have many actions. For example, the following input element calls a `field` controller's `highlight()` method when it gains focus, and a `search` controller's `update()` method every time the element's value changes:
<meta data-controller="callout" data-callout-text-value="focus->field#highlight">
<meta data-controller="callout" data-callout-text-value="input->search#update">
```html
<input type="text" data-action="focus->field#highlight input->search#update">
```
When an element has more than one action for the same event, Stimulus invokes the actions from left to right in the order that their descriptors appear.
The action chain can be stopped at any point by calling `Event#stopImmediatePropagation()` within an action. Any additional actions to the right will be ignored:
```javascript
highlight(event) {
event.stopImmediatePropagation()
// ...
}
```
## Naming Conventions
Always use camelCase to specify action names, since they map directly to methods on your controller.
Avoid action names that simply repeat the event's name, such as `click`, `onClick`, or `handleClick`:
<meta data-controller="callout" data-callout-text-value="#click" data-callout-type-value="avoid">
```html
<button data-action="click->profile#click">Don't</button>
```
Instead, name your action methods based on what will happen when they're called:
<meta data-controller="callout" data-callout-text-value="#showDialog" data-callout-type-value="prefer">
```html
<button data-action="click->profile#showDialog">Do</button>
```
This will help you reason about the behavior of a block of HTML without having to look at the controller source.
## Action Parameters
Actions can have parameters that are be passed from the submitter element. They follow the format of `data-[identifier]-[param-name]-param`. Parameters must be specified on the same element as the action they intend to be passed to is declared.
All parameters are automatically typecast to either a `Number`, `String`, `Object`, or `Boolean`, inferred by their value:
Data attribute | Param | Type
----------------------------------------------- | -------------------- | --------
`data-item-id-param="12345"` | `12345` | Number
`data-item-url-param="/votes"` | `"/votes"` | String
`data-item-payload-param='{"value":"1234567"}'` | `{ value: 1234567 }` | Object
`data-item-active-param="true"` | `true` | Boolean
<br>Consider this setup:
```html
<div data-controller="item spinner">
<button data-action="item#upvote spinner#start"
data-item-id-param="12345"
data-item-url-param="/votes"
data-item-payload-param='{"value":"1234567"}'
data-item-active-param="true">…</button>
</div>
```
It will call both `ItemController#upvote` and `SpinnerController#start`, but only the former will have any parameters passed to it:
```js
// ItemController
upvote(event) {
// { id: 12345, url: "/votes", active: true, payload: { value: 1234567 } }
console.log(event.params)
}
// SpinnerController
start(event) {
// {}
console.log(event.params)
}
```
If we don't need anything else from the event, we can destruct the params:
```js
upvote({ params }) {
// { id: 12345, url: "/votes", active: true, payload: { value: 1234567 } }
console.log(params)
}
```
Or destruct only the params we need, in case multiple actions on the same controller share the same submitter element:
```js
upvote({ params: { id, url } }) {
console.log(id) // 12345
console.log(url) // "/votes"
}
```
================================================
FILE: docs/reference/controllers.md
================================================
---
permalink: /reference/controllers.html
order: 00
---
# Controllers
A _controller_ is the basic organizational unit of a Stimulus application.
```js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
// …
}
```
Controllers are instances of JavaScript classes that you define in your application. Each controller class inherits from the `Controller` base class exported by the `@hotwired/stimulus` module.
## Properties
Every controller belongs to a Stimulus `Application` instance and is associated with an HTML element. Within a controller class, you can access the controller's:
* application, via the `this.application` property
* HTML element, via the `this.element` property
* identifier, via the `this.identifier` property
## Modules
Define your controller classes in JavaScript modules, one per file. Export each controller class as the module's default object, as in the example above.
Place these modules in the `controllers/` directory. Name the files `[identifier]_controller.js`, where `[identifier]` corresponds to each controller's identifier.
## Identifiers
An _identifier_ is the name you use to reference a controller class in HTML.
When you add a `data-controller` attribute to an element, Stimulus reads the identifier from the attribute's value and creates a new instance of the corresponding controller class.
For example, this element has a controller which is an instance of the class defined in `controllers/reference_controller.js`:
<meta data-controller="callout" data-callout-text-value="reference">
```html
<div data-controller="reference"></div>
```
The following is an example of how Stimulus will generate identifiers for controllers in its require context:
If your controller file is named… | its identifier will be…
--------------------------------- | -----------------------
clipboard_controller.js | clipboard
date_picker_controller.js | date-picker
users/list_item_controller.js | users\-\-list-item
local-time-controller.js | local-time
## Scopes
When Stimulus connects a controller to an element, that element and all of its children make up the controller's _scope_.
For example, the `<div>` and `<h1>` below are part of the controller's scope, but the surrounding `<main>` element is not.
```html
<main>
<div data-controller="reference">
<h1>Reference</h1>
</div>
</main>
```
## Nested Scopes
When nested, each controller is only aware of its own scope excluding the scope of any controllers nested within.
For example, the `#parent` controller below is only aware of the `item` targets directly within its scope, but not any targets of the `#child` controller.
```html
<ul id="parent" data-controller="list">
<li data-list-target="item">One</li>
<li data-list-target="item">Two</li>
<li>
<ul id="child" data-controller="list">
<li data-list-target="item">I am</li>
<li data-list-target="item">a nested list</li>
</ul>
</li>
</ul>
```
## Multiple Controllers
The `data-controller` attribute's value is a space-separated list of identifiers:
<meta data-controller="callout" data-callout-text-value="clipboard">
<meta data-controller="callout" data-callout-text-value="list-item">
```html
<div data-controller="clipboard list-item"></div>
```
It's common for any given element on the page to have many controllers. In the example above, the `<div>` has two connected controllers, `clipboard` and `list-item`.
Similarly, it's common for multiple elements on the page to reference the same controller class:
<meta data-controller="callout" data-callout-text-value="list-item">
```html
<ul>
<li data-controller="list-item">One</li>
<li data-controller="list-item">Two</li>
<li data-controller="list-item">Three</li>
</ul>
```
Here, each `<li>` has its own instance of the `list-item` controller.
## Naming Conventions
Always use camelCase for method and property names in a controller class.
When an identifier is composed of more than one word, write the words in kebab-case (i.e., by using dashes: `date-picker`, `list-item`).
In filenames, separate multiple words using either underscores or dashes (snake_case or kebab-case: `controllers/date_picker_controller.js`, `controllers/list-item-controller.js`).
## Registration
If you use Stimulus for Rails with an import map or Webpack together with the `@hotwired/stimulus-webpack-helpers` package, your application will automatically load and register controller classes following the conventions above.
If not, your application must manually load and register each controller class.
### Registering Controllers Manually
To manually register a controller class with an identifier, first import the class, then call the `Application#register` method on your application object:
```js
import ReferenceController from "./controllers/reference_controller"
application.register("reference", ReferenceController)
```
You can also register a controller class inline instead of importing it from a module:
```js
import { Controller } from "@hotwired/stimulus"
application.register("reference", class extends Controller {
// …
})
```
### Preventing Registration Based On Environmental Factors
If you only want a controller registered and loaded if certain environmental factors are met – such a given user agent – you can overwrite the static `shouldLoad` method:
```js
class UnloadableController extends ApplicationController {
static get shouldLoad() {
return false
}
}
// This controller will not be loaded
application.register("unloadable", UnloadableController)
```
### Trigger Behaviour When A Controller Is Registered
If you want to trigger some behaviour once a controller has been registered you can add a static `afterLoad` method:
```js
class SpinnerButton extends Controller {
static legacySelector = ".legacy-spinner-button"
static afterLoad(identifier, application) {
// use the application instance to read the configured 'data-controller' attribute
const { controllerAttribute } = application.schema
// update any legacy buttons with the controller's registered identifier
const updateLegacySpinners = () => {
document.querySelector(this.legacySelector).forEach((element) => {
element.setAttribute(controllerAttribute, identifier)
})
}
// called as soon as registered so DOM may not have loaded yet
if (document.readyState == "loading") {
document.addEventListener("DOMContentLoaded", updateLegacySpinners)
} else {
updateLegacySpinners()
}
}
}
// This controller will update any legacy spinner buttons to use the controller
application.register("spinner-button", SpinnerButton)
```
The `afterLoad` method will get called as soon as the controller has been registered, even if no controlled elements exist in the DOM. The function will be called bound to the original controller constructor along with two arguments; the `identifier` that was used when registering the controller and the Stimulus application instance.
## Cross-Controller Coordination With Events
If you need controllers to communicate with each other, you should use events. The `Controller` class has a convenience method called `dispatch` that makes this easier. It takes an `eventName` as the first argument, which is then automatically prefixed with the name of the controller separated by a colon. The payload is held in `detail`. It works like this:
```js
class ClipboardController extends Controller {
static targets = [ "source" ]
copy() {
this.dispatch("copy", { detail: { content: this.sourceTarget.value } })
navigator.clipboard.writeText(this.sourceTarget.value)
}
}
```
And this event can then be routed to an action on another controller:
```html
<div data-controller="clipboard effects" data-action="clipboard:copy->effects#flash">
PIN: <input data-clipboard-target="source" type="text" value="1234" readonly>
<button data-action="clipboard#copy">Copy to Clipboard</button>
</div>
```
So when the `Clipboard#copy` action is invoked, the `Effects#flash` action will be too:
```js
class EffectsController extends Controller {
flash({ detail: { content } }) {
console.log(content) // 1234
}
}
```
If the two controllers don't belong to the same HTML element, the `data-action` attribute
needs to be added to the *receiving* controller's element. And if the receiving controller's
element is not a parent (or same) of the emitting controller's element, you need to add
`@window` to the event:
```html
<div data-action="clipboard:copy@window->effects#flash">
```
`dispatch` accepts additional options as the second parameter as follows:
option | default | notes
-------------|--------------------|----------------------------------------------------------------------------------------------
`detail` | `{}` empty object | See [CustomEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail)
`target` | `this.element` | See [Event.target](https://developer.mozilla.org/en-US/docs/Web/API/Event/target)
`prefix` | `this.identifier` | If the prefix is falsey (e.g. `null` or `false`), only the `eventName` will be used. If you provide a string value the `eventName` will be prepended with the provided string and a colon.
`bubbles` | `true` | See [Event.bubbles](https://developer.mozilla.org/en-US/docs/Web/API/Event/bubbles)
`cancelable` | `true` | See [Event.cancelable](https://developer.mozilla.org/en-US/docs/Web/API/Event/cancelable)
`dispatch` will return the generated [`CustomEvent`](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent), you can use this to provide a way for the event to be cancelled by any other listeners as follows:
```js
class ClipboardController extends Controller {
static targets = [ "source" ]
copy() {
const event = this.dispatch("copy", { cancelable: true })
if (event.defaultPrevented) return
navigator.clipboard.writeText(this.sourceTarget.value)
}
}
```
```js
class EffectsController extends Controller {
flash(event) {
// this will prevent the default behaviour as determined by the dispatched event
event.preventDefault()
}
}
```
## Directly Invoking Other Controllers
If for some reason it is not possible to use events to communicate between controllers, you can reach a controller instance via the `getControllerForElementAndIdentifier` method from the application. This should only be used if you have a unique problem that cannot be solved through the more general way of using events, but if you must, this is how:
```js
class MyController extends Controller {
static targets = [ "other" ]
copy() {
const otherController = this.application.getControllerForElementAndIdentifier(this.otherTarget, 'other')
otherController.otherMethod()
}
}
```
================================================
FILE: docs/reference/css_classes.md
================================================
---
permalink: /reference/css-classes.html
order: 06
---
# CSS Classes
In HTML, a _CSS class_ defines a set of styles which can be applied to elements using the `class` attribute.
CSS classes are a convenient tool for changing styles and playing animations programmatically. For example, a Stimulus controller might add a "loading" class to an element when it is performing an operation in the background, and then style that class in CSS to display a progress indicator:
```html
<form data-controller="search" class="search--busy">
```
```css
.search--busy {
background-image: url(throbber.svg) no-repeat;
}
```
As an alternative to hard-coding classes with JavaScript strings, Stimulus lets you refer to CSS classes by _logical name_ using a combination of data attributes and controller properties.
## Definitions
Define CSS classes by logical name in your controller using the `static classes` array:
<meta data-controller="callout" data-callout-text-value="static classes = [ "loading" ]">
```js
// controllers/search_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static classes = [ "loading" ]
// …
}
```
## Attributes
The logical names defined in the controller's `static classes` array map to _CSS class attributes_ on the controller's element.
<meta data-controller="callout" data-callout-text-value="data-search-loading-class="search--busy"">
```html
<form data-controller="search"
data-search-loading-class="search--busy">
<input data-action="search#loadResults">
</form>
```
Construct a CSS class attribute by joining together the controller identifier and logical name in the format `data-[identifier]-[logical-name]-class`. The attribute's value can be a single CSS class name or a list of multiple class names.
**Note:** CSS class attributes must be specified on the same element as the `data-controller` attribute.
If you want to specify multiple CSS classes for a logical name, separate the classes with spaces:
<meta data-controller="callout" data-callout-text-value="data-search-loading-class="bg-gray-500 animate-spinner cursor-busy"">
```html
<form data-controller="search"
data-search-loading-class="bg-gray-500 animate-spinner cursor-busy">
<input data-action="search#loadResults">
</form>
```
## Properties
For each logical name defined in the `static classes` array, Stimulus adds the following _CSS class properties_ to your controller:
Kind | Name | Value
----------- | ---------------------------- | -----
Singular | `this.[logicalName]Class` | The value of the CSS class attribute corresponding to `logicalName`
Plural | `this.[logicalName]Classes` | An array of all classes in the corresponding CSS class attribute, split by spaces
Existential | `this.has[LogicalName]Class` | A boolean indicating whether or not the CSS class attribute is present
<br>Use these properties to apply CSS classes to elements with the `add()` and `remove()` methods of the [DOM `classList` API](https://developer.mozilla.org/en-US/docs/Web/API/Element/classList).
For example, to display a loading indicator on the `search` controller's element before fetching results, you might implement the `loadResults` action like so:
<meta data-controller="callout" data-callout-text-value="this.loadingClass">
```js
export default class extends Controller {
static classes = [ "loading" ]
loadResults() {
this.element.classList.add(this.loadingClass)
fetch(/* … */)
}
}
```
If a CSS class attribute contains a list of class names, its singular CSS class property returns the first class in the list.
Use the plural CSS class property to access all class names as an array. Combine this with [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) to apply multiple classes at once:
<meta data-controller="callout" data-callout-text-value="...this.loadingClasses">
```js
export default class extends Controller {
static classes = [ "loading" ]
loadResults() {
this.element.classList.add(...this.loadingClasses)
fetch(/* … */)
}
}
```
**Note:** Stimulus will throw an error if you attempt to access a CSS class property when a matching CSS class attribute is not present.
## Naming Conventions
Use camelCase to specify logical names in CSS class definitions. Logical names map to camelCase CSS class properties:
<meta data-controller="callout" data-callout-text-value="noResultsClass">
<meta data-controller="callout" data-callout-text-value="noResults">
```js
export default class extends Controller {
static classes = [ "loading", "noResults" ]
loadResults() {
// …
if (results.length == 0) {
this.element.classList.add(this.noResultsClass)
}
}
}
```
In HTML, write CSS class attributes in kebab-case:
<meta data-controller="callout" data-callout-text-value="no-results">
```html
<form data-controller="search"
data-search-loading-class="search--busy"
data-search-no-results-class="search--empty">
```
When constructing CSS class attributes, follow the conventions for identifiers as described in [Controllers: Naming Conventions](controllers#naming-conventions).
================================================
FILE: docs/reference/lifecycle_callbacks.md
================================================
---
permalink: /reference/lifecycle-callbacks.html
order: 01
---
# Lifecycle Callbacks
Special methods called _lifecycle callbacks_ allow you to respond whenever a controller or certain targets connects to and disconnects from the document.
<meta data-controller="callout" data-callout-text-value="connect()">
```js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
connect() {
// …
}
}
```
## Methods
You may define any of the following methods in your controller:
Method | Invoked by Stimulus…
------------ | --------------------
initialize() | Once, when the controller is first instantiated
[name]TargetConnected(target: Element) | Anytime a target is connected to the DOM
connect() | Anytime the controller is connected to the DOM
[name]TargetDisconnected(target: Element) | Anytime a target is disconnected from the DOM
disconnect() | Anytime the controller is disconnected from the DOM
## Connection
A controller is _connected_ to the document when both of the following conditions are true:
* its element is present in the document (i.e., a descendant of `document.documentElement`, the `<html>` element)
* its identifier is present in the element's `data-controller` attribute
When a controller becomes connected, Stimulus calls its `connect()` method.
### Targets
A target is _connected_ to the document when both of the following conditions are true:
* its element is present in the document as a descendant of its corresponding controller's element
* its identifier is present in the element's `data-{identifier}-target` attribute
When a target becomes connected, Stimulus calls its controller's `[name]TargetConnected()` method, passing the target element as a parameter. The `[name]TargetConnected()` lifecycle callbacks will fire *before* the controller's `connect()` callback.
## Disconnection
A connected controller will later become _disconnected_ when either of the preceding conditions becomes false, such as under any of the following scenarios:
* the element is explicitly removed from the document with `Node#removeChild()` or `ChildNode#remove()`
* one of the element's parent elements is removed from the document
* one of the element's parent elements has its contents replaced by `Element#innerHTML=`
* the element's `data-controller` attribute is removed or modified
* the document installs a new `<body>` element, such as during a Turbo page change
When a controller becomes disconnected, Stimulus calls its `disconnect()` method.
### Targets
A connected target will later become _disconnected_ when either of the preceding conditions becomes false, such as under any of the following scenarios:
* the element is explicitly removed from the document with `Node#removeChild()` or `ChildNode#remove()`
* one of the element's parent elements is removed from the document
* one of the element's parent elements has its contents replaced by `Element#innerHTML=`
* the element's `data-{identifier}-target` attribute is removed or modified
* the document installs a new `<body>` element, such as during a Turbo page change
When a target becomes disconnected, Stimulus calls its controller's `[name]TargetDisconnected()` method, passing the target element as a parameter. The `[name]TargetDisconnected()` lifecycle callbacks will fire *after* the controller's `disconnect()` callback.
## Reconnection
A disconnected controller may become connected again at a later time.
When this happens, such as after removing the controller's element from the document and then re-attaching it, Stimulus will reuse the element's previous controller instance, calling its `connect()` method multiple times.
Similarly, a disconnected target may be connected again at a later time. Stimulus will invoke its controller's `[name]TargetConnected()` method multiple times.
## Order and Timing
Stimulus watches the page for changes asynchronously using the [DOM `MutationObserver` API](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver).
This means that Stimulus calls your controller's lifecycle methods asynchronously after changes are made to the document, in the next [microtask](https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/) following each change.
Lifecycle methods still run in the order they occur, so two calls to a controller's `connect()` method will always be separated by one call to `disconnect()`. Similarly, two calls to a controller's `[name]TargetConnected()` for a given target will always be separated by one call to `[name]TargetDisconnected()` for that same target.
================================================
FILE: docs/reference/outlets.md
================================================
---
permalink: /reference/outlets.html
order: 04
---
# Outlets
_Outlets_ let you reference Stimulus _controller instances_ and their _controller element_ from within another Stimulus Controller by using CSS selectors.
The use of Outlets helps with cross-controller communication and coordination as an alternative to dispatching custom events on controller elements.
They are conceptually similar to [Stimulus Targets](https://stimulus.hotwired.dev/reference/targets) but with the difference that they reference a Stimulus controller instance plus its associated controller element.
<meta data-controller="callout" data-callout-text-value='data-chat-user-status-outlet=".online-user"'>
<meta data-controller="callout" data-callout-text-value='class="online-user"'>
```html
<div>
<div class="online-user" data-controller="user-status">...</div>
<div class="online-user" data-controller="user-status">...</div>
...
</div>
...
<div data-controller="chat" data-chat-user-status-outlet=".online-user">
...
</div>
```
While a **target** is a specifically marked element **within the scope** of its own controller element, an **outlet** can be located **anywhere on the page** and doesn't necessarily have to be within the controller scope.
## Attributes and Names
The `data-chat-user-status-outlet` attribute is called an _outlet attribute_, and its value is a [CSS selector](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors) which you can use to refer to other controller elements which should be available as outlets on the _host controller_. The outlet identifier in the host controller **must be the same** as the target controller's identifier. If not, it will throw an error message that outlet does not exist.
```html
data-[identifier]-[outlet]-outlet="[selector]"
```
<meta data-controller="callout" data-callout-text-value='data-chat-user-status-outlet=".online-user"'>
```html
<div data-controller="chat" data-chat-user-status-outlet=".online-user"></div>
```
## Definitions
Define controller identifiers in your controller class using the `static outlets` array. This array declares which other controller identifiers can be used as outlets on this controller:
<meta data-controller="callout" data-callout-text-value='static outlets'>
<meta data-controller="callout" data-callout-text-value='"user-status"'>
<meta data-controller="callout" data-callout-text-value='userStatus'>
```js
// chat_controller.js
export default class extends Controller {
static outlets = [ "user-status" ]
connect () {
this.userStatusOutlets.forEach(status => ...)
}
}
```
## Properties
For each outlet defined in the `static outlets` array, Stimulus adds five properties to your controller, where `[name]` corresponds to the outlet's controller identifier:
| Kind | Property name | Return Type | Effect
| ---- | ------------- | ----------- | -----------
| Existential | `has[Name]Outlet` | `Boolean` | Tests for presence of a `[name]` outlet
| Singular | `[name]Outlet` | `Controller` | Returns the `Controller` instance of the first `[name]` outlet or throws an exception if none is present
| Plural | `[name]Outlets` | `Array<Controller>` | Returns the `Controller` instances of all `[name]` outlets
| Singular | `[name]OutletElement` | `Element` | Returns the Controller `Element` of the first `[name]` outlet or throws an exception if none is present
| Plural | `[name]OutletElements` | `Array<Element>` | Returns the Controller `Element`'s of all `[name]` outlets
**Note:** For nested Stimulus controller properties, make sure to omit namespace delimiters in order to correctly access the referenced outlet:
```js
// chat_controller.js
export default class extends Controller {
static outlets = [ "admin--user-status" ]
selectAll(event) {
// returns undefined
this.admin__UserStatusOutlets
// returns controller reference
this.adminUserStatusOutlets
}
}
```
## Accessing Controllers and Elements
Since you get back a `Controller` instance from the `[name]Outlet` and `[name]Outlets` properties you are also able to access the Values, Classes, Targets and all of the other properties and functions that controller instance defines:
```js
this.userStatusOutlet.idValue
this.userStatusOutlet.imageTarget
this.userStatusOutlet.activeClasses
```
You are also able to invoke any function the outlet controller may define:
```js
// user_status_controller.js
export default class extends Controller {
markAsSelected(event) {
// ...
}
}
// chat_controller.js
export default class extends Controller {
static outlets = [ "user-status" ]
selectAll(event) {
this.userStatusOutlets.forEach(status => status.markAsSelected(event))
}
}
```
Similarly with the Outlet Element, it allows you to call any function or property on [`Element`](https://developer.mozilla.org/en-US/docs/Web/API/Element):
```js
this.userStatusOutletElement.dataset.value
this.userStatusOutletElement.getAttribute("id")
this.userStatusOutletElements.map(status => status.hasAttribute("selected"))
```
## Outlet Callbacks
Outlet callbacks are specially named functions called by Stimulus to let you respond to whenever an outlet is added or removed from the page.
To observe outlet changes, define a function named `[name]OutletConnected()` or `[name]OutletDisconnected()`.
```js
// chat_controller.js
export default class extends Controller {
static outlets = [ "user-status" ]
userStatusOutletConnected(outlet, element) {
// ...
}
userStatusOutletDisconnected(outlet, element) {
// ...
}
}
```
### Outlets are Assumed to be Present
When you access an Outlet property in a Controller, you assert that at least one corresponding Outlet is present. If the declaration is missing and no matching outlet is found Stimulus will throw an exception:
```html
Missing outlet element "user-status" for "chat" controller
```
### Optional outlets
If an Outlet is optional or you want to assert that at least Outlet is present, you must first check the presence of the Outlet using the existential property:
```js
if (this.hasUserStatusOutlet) {
this.userStatusOutlet.safelyCallSomethingOnTheOutlet()
}
```
### Referencing Non-Controller Elements
Stimulus will throw an exception if you try to declare an element as an outlet which doesn't have a corresponding `data-controller` and identifier on it:
<meta data-controller="callout" data-callout-text-value='data-chat-user-status-outlet="#user-column"'>
<meta data-controller="callout" data-callout-text-value='id="user-column"'>
```html
<div data-controller="chat" data-chat-user-status-outlet="#user-column"></div>
<div id="user-column"></div>
```
Would result in:
```html
Missing "data-controller=user-status" attribute on outlet element for
"chat" controller`
```
================================================
FILE: docs/reference/targets.md
================================================
---
permalink: /reference/targets.html
order: 03
---
# Targets
_Targets_ let you reference important elements by name.
<meta data-controller="callout" data-callout-text-value="search.query">
<meta data-controller="callout" data-callout-text-value="search.errorMessage">
<meta data-controller="callout" data-callout-text-value="search.results">
```html
<div data-controller="search">
<input type="text" data-search-target="query">
<div data-search-target="errorMessage"></div>
<div data-search-target="results"></div>
</div>
```
## Attributes and Names
The `data-search-target` attribute is called a _target attribute_, and its value is a space-separated list of _target names_ which you can use to refer to the element in the `search` controller.
<meta data-controller="callout" data-callout-text-value="search">
<meta data-controller="callout" data-callout-text-value="results">
```html
<div data-controller="search">
<div data-search-target="results"></div>
</div>
```
## Definitions
Define target names in your controller class using the `static targets` array:
```js
// controllers/search_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "query", "errorMessage", "results" ]
// …
}
```
## Properties
For each target name defined in the `static targets` array, Stimulus adds the following properties to your controller, where `[name]` corresponds to the target's name:
Kind | Name | Value
----------- | ---------------------- | -----
Singular | `this.[name]Target` | The first matching target in scope
Plural | `this.[name]Targets` | An array of all matching targets in scope
Existential | `this.has[Name]Target` | A boolean indicating whether there is a matching target in scope
<br>**Note:** Accessing the singular target property will throw an error when there is no matching element.
## Shared Targets
Elements can have more than one target attribute, and it's common for targets to be shared by multiple controllers.
<meta data-controller="callout" data-callout-text-value="data-search-target="projects"">
<meta data-controller="callout" data-callout-text-value="data-search-target="messages"">
<meta data-controller="callout" data-callout-text-value="data-checkbox-target="input"">
```html
<form data-controller="search checkbox">
<input type="checkbox" data-search-target="projects" data-checkbox-target="input">
<input type="checkbox" data-search-target="messages" data-checkbox-target="input">
…
</form>
```
In the example above, the checkboxes are accessible inside the `search` controller as `this.projectsTarget` and `this.messagesTarget`, respectively.
Inside the `checkbox` controller, `this.inputTargets` returns an array with both checkboxes.
## Optional Targets
If your controller needs to work with a target which may or may not be present, condition your code based on the value of the existential target property:
```js
if (this.hasResultsTarget) {
this.resultsTarget.innerHTML = "…"
}
```
## Connected and Disconnected Callbacks
Target _element callbacks_ let you respond whenever a target element is added or
removed within the controller's element.
Define a method `[name]TargetConnected` or `[name]TargetDisconnected` in the controller, where `[name]` is the name of the target you want to observe for additions or removals. The method receives the element as the first argument.
Stimulus invokes each element callback any time its target elements are added or removed. When the controller is connected or disconnected from the document, these callbacks are invoked *before* `connect()` and *after* `disconnect()` lifecycle hooks.
```js
export default class extends Controller {
static targets = [ "item" ]
itemTargetConnected(element) {
this.sortElements(this.itemTargets)
}
itemTargetDisconnected(element) {
this.sortElements(this.itemTargets)
}
// Private
sortElements(itemTargets) { /* ... */ }
}
```
**Note** During the execution of `[name]TargetConnected` and
`[name]TargetDisconnected` callbacks, the `MutationObserver` instances behind
the scenes are paused. This means that if a callback add or removes a target
with a matching name, the corresponding callback _will not_ be invoked again.
## Naming Conventions
Always use camelCase to specify target names, since they map directly to properties on your controller:
```html
<span data-search-target="camelCase"></span>
<span data-search-target="do-not-do-this"></span>
```
```js
export default class extends Controller {
static targets = [ "camelCase" ]
}
```
================================================
FILE: docs/reference/using_typescript.md
================================================
---
permalink: /reference/using-typescript.html
order: 07
---
# Using Typescript
Stimulus itself is written in [TypeScript](https://www.typescriptlang.org/) and provides types directly over its package.
The following documentation shows how to define types for Stimulus properties.
## Define Controller Element Type
By default, the `element` of the controller is of type `Element`. You can override the type of the controller element by specifiying it as a [Generic Type](https://www.typescriptlang.org/docs/handbook/2/generics.html). For example, if the element type is expected to be a `HTMLFormElement`:
<meta data-controller="callout" data-callout-text-value="Controller<HTMLFormElement>">
```ts
import { Controller } from "@hotwired/stimulus"
export default class MyController extends Controller<HTMLFormElement> {
submit() {
new FormData(this.element)
}
}
```
## Define Value Properties
You can define the properties of configured values using the TypeScript `declare` keyword. You just need to define the properties if you are making use of them within the controller.
```ts
import { Controller } from "@hotwired/stimulus"
export default class MyController extends Controller {
static values = {
code: String
}
declare codeValue: string
declare readonly hasCodeValue: boolean
}
```
> The `declare` keyword avoids overriding the existing Stimulus property, and just defines the type for TypeScript.
## Define Target Properties
You can define the properties of configured targets using the TypeScript `declare` keyword. You just need to define the properties if you are making use of them within the controller.
The return types of the `[name]Target` and `[name]Targets` properties can be any inheriting from the `Element` type. Choose the best type which fits your needs. Pick either `Element` or `HTMLElement` if you want to define it as a generic HTML element.
```ts
import { Controller } from "@hotwired/stimulus"
export default class MyController extends Controller {
static targets = [ "input" ]
declare readonly hasInputTarget: boolean
declare readonly inputTarget: HTMLInputElement
declare readonly inputTargets: HTMLInputElement[]
}
```
> The `declare` keyword avoids overriding the existing Stimulus property, and just defines the type for TypeScript.
## Custom properties and methods
Other custom properties can be defined the TypeScript way on the controller class:
<meta data-controller="callout" data-callout-text-value="container: HTMLElement">
```ts
import { Controller } from "@hotwired/stimulus"
export default class MyController extends Controller {
container: HTMLElement
}
```
Read more in the [TypeScript Documentation](https://www.typescriptlang.org/docs/handbook/intro.html).
================================================
FILE: docs/reference/values.md
================================================
---
permalink: /reference/values.html
order: 05
---
# Values
You can read and write [HTML data attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/data-*) on controller elements as typed _values_ using special controller properties.
<meta data-controller="callout" data-callout-text-value="data-loader-url-value="/messages"">
```html
<div data-controller="loader" data-loader-url-value="/messages">
</div>
```
As per the given HTML snippet, remember to place the data attributes for values on the same element as the `data-controller` attribute.
<meta data-controller="callout" data-callout-text-value="static values = { url: String }">
<meta data-controller="callout" data-callout-text-value="this.urlValue">
```js
// controllers/loader_controller.js
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static values = {
url: String
}
connect() {
fetch(this.urlValue).then(/* … */)
}
}
```
## Definitions
Define values in a controller using the `static values` object. Put each value's _name_ on the left and its _type_ on the right.
```js
export default class extends Controller {
static values = {
url: String,
interval: Number,
params: Object
}
// …
}
```
## Types
A value's type is one of `Array`, `Boolean`, `Number`, `Object`, or `String`. The type determines how the value is transcoded between JavaScript and HTML.
| Type | Encoded as… | Decoded as… |
| ------- | ------------------------ | --------------------------------------- |
| Array | `JSON.stringify(array)` | `JSON.parse(value)` |
| Boolean | `boolean.toString()` | `!(value == "0" \|\| value == "false")` |
| Number | `number.toString()` | `Number(value.replace(/_/g, ""))` |
| Object | `JSON.stringify(object)` | `JSON.parse(value)` |
| String | Itself | Itself |
## Properties and Attributes
Stimulus automatically generates getter, setter, and existential properties for each value defined in a controller. These properties are linked to data attributes on the controller's element:
Kind | Property name | Effect
---- | ------------- | ------
Getter | `this.[name]Value` | Reads `data-[identifier]-[name]-value`
Setter | `this.[name]Value=` | Writes `data-[identifier]-[name]-value`
Existential | `this.has[Name]Value` | Tests for `data-[identifier]-[name]-value`
### Getters
The getter for a value decodes the associated data attribute into an instance of the value's type.
If the data attribute is missing from the controller's element, the getter returns a _default value_, depending on the value's type:
Type | Default value
---- | -------------
Array | `[]`
Boolean | `false`
Number | `0`
Object | `{}`
String | `""`
### Setters
The setter for a value sets the associated data attribute on the controller's element.
To remove the data attribute from the controller's element, assign `undefined` to the value.
### Existential Properties
The existential property for a value evaluates to `true` when the associated data attribute is present on the controller's element and `false` when it is absent.
## Change Callbacks
Value _change callbacks_ let you respond whenever a value's data attribute changes.
Define a method `[name]ValueChanged` in the controller, where `[name]` is the name of the value you want to observe for changes. The method receives its decoded value as the first argument and the decoded previous value as the second argument.
Stimulus invokes each change callback after the controller is initialized and again any time its associated data attribute changes. This includes changes as a result of assignment to the value's setter.
```js
export default class extends Controller {
static values = { url: String }
urlValueChanged() {
fetch(this.urlValue).then(/* … */)
}
}
```
### Previous Values
You can access the previous value of a `[name]ValueChanged` callback by defining the callback method with two arguments in your controller.
```js
export default class extends Controller {
static values = { url: String }
urlValueChanged(value, previousValue) {
/* … */
}
}
```
The two arguments can be named as you like. You could also use `urlValueChanged(current, old)`.
## Default Values
Values that have not been specified on the controller element can be set by defaults specified in the controller definition:
```js
export default class extends Controller {
static values = {
url: { type: String, default: '/bill' },
interval: { type: Number, default: 5 },
clicked: Boolean
}
}
```
When a default is used, the expanded form of `{ type, default }` is used. This form can be mixed with the regular form that does not use a default.
## Naming Conventions
Write value names as camelCase in JavaScript and kebab-case in HTML. For example, a value named `contentType` in the `loader` controller will have the associated data attribute `data-loader-content-type-value`.
================================================
FILE: examples/.babelrc
================================================
{
"presets": [
["@babel/preset-env"]
],
"plugins": [
["@babel/plugin-proposal-class-properties"],
["@babel/plugin-transform-runtime"]
]
}
================================================
FILE: examples/controllers/clipboard_controller.js
================================================
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "source" ]
static classes = [ "supported" ]
initialize() {
if (document.queryCommandSupported("copy")) {
this.element.classList.add(this.supportedClass)
}
}
copy() {
navigator.clipboard.writeText(this.sourceTarget.value)
}
}
================================================
FILE: examples/controllers/content_loader_controller.js
================================================
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["item"]
static values = { url: String, refreshInterval: Number }
connect() {
this.load()
if (this.hasRefreshIntervalValue) {
this.startRefreshing()
}
}
itemTargetConnected(target) {
console.log("itemTargetConnected:", target)
}
itemTargetDisconnected(target) {
console.log("itemTargetDisconnected:", target)
}
disconnect() {
this.stopRefreshing()
}
load() {
fetch(this.urlValue)
.then(response => response.text())
.then(html => {
this.element.innerHTML = html
})
}
startRefreshing() {
this.refreshTimer = setInterval(() => {
this.load()
}, this.refreshIntervalValue)
}
stopRefreshing() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer)
}
}
}
================================================
FILE: examples/controllers/hello_controller.js
================================================
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["name"]
greet() {
alert(`Hello, ${this.name}!`)
}
get name() {
return this.nameTarget.value
}
}
================================================
FILE: examples/controllers/slideshow_controller.js
================================================
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "slide" ]
static classes = [ "currentSlide" ]
static values = { index: Number }
next() {
if (this.indexValue < this.lastIndex) {
this.indexValue++
}
}
previous() {
if (this.indexValue > 0) {
this.indexValue--
}
}
indexValueChanged() {
this.render()
}
render() {
this.slideTargets.forEach((element, index) => {
element.classList.toggle(this.currentSlideClass, index == this.indexValue)
})
}
get lastIndex() {
return this.slideTargets.length - 1
}
}
================================================
FILE: examples/controllers/tabs_controller.js
================================================
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = [ "tab", "tabpanel" ]
static classes = [ "current" ]
static values = { index: { default: 0, type: Number } }
next() {
if (this.indexValue < this.lastIndex) {
this.indexValue++
return
}
this.indexValue = 0
}
previous() {
if (this.indexValue > 0) {
this.indexValue--
return
}
this.indexValue = this.lastIndex
}
open(evt) {
this.indexValue = this.tabTargets.indexOf(evt.currentTarget)
}
get lastIndex() {
return this.tabTargets.length - 1
}
indexValueChanged(current, old) {
let panels = this.tabpanelTargets
let tabs = this.tabTargets
if (old != null) {
panels[old].classList.remove(...this.currentClasses)
tabs[old].tabIndex = -1
}
panels[current].classList.add(...this.currentClasses)
tabs[current].tabIndex = 0
tabs[current].focus()
}
}
================================================
FILE: examples/index.js
================================================
import { Application } from "@hotwired/stimulus"
import "@hotwired/turbo"
const application = Application.start()
import ClipboardController from "./controllers/clipboard_controller"
application.register("clipboard", ClipboardController)
import ContentLoaderController from "./controllers/content_loader_controller"
application.register("content-loader", ContentLoaderController)
import HelloController from "./controllers/hello_controller"
application.register("hello", HelloController)
import SlideshowController from "./controllers/slideshow_controller"
application.register("slideshow", SlideshowController)
import TabsController from "./controllers/tabs_controller"
application.register("tabs", TabsController)
================================================
FILE: examples/package.json
================================================
{
"name": "@hotwired/stimulus-examples",
"version": "3.1.0",
"private": true,
"dependencies": {
"@babel/core": "^7.5.5",
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-transform-runtime": "^7.14.5",
"@babel/preset-env": "^7.5.5",
"@hotwired/turbo": "^7.2.4",
"babel-loader": "^8.0.6",
"ejs": "^3.1.10",
"express": "^4.20.0",
"webpack": "^4.39.1",
"webpack-dev-middleware": "^5.3.4"
},
"scripts": {
"start": "node server.js"
}
}
================================================
FILE: examples/public/examples.css
================================================
body {
background: rgb(251, 247, 240);
}
main {
display: flex;
flex-flow: row;
align-items: flex-start;
justify-content: flex-start;
}
.logo {
width: 6ex;
height: 6ex;
margin: 0 0 1ex;
}
.sidebar {
width: 10em;
margin-top: 3ex;
margin-left: 2em;
}
.nav {
white-space: nowrap;
}
.nav li {
list-style: none;
}
.nav a {
display: block;
position: relative;
text-decoration: none;
}
.nav a.active {
font-weight: bold;
color: #000;
}
.nav a.active::before {
content: "►";
display: inline-block;
position: absolute;
left: -1em;
font-size: 1em;
}
.container {
flex-grow: 0;
background-color: #fff;
border: 0.25ex solid #333;
border-radius: 1ex;
box-shadow: 1ex 1ex 0 0 rgba(0, 0, 0, 0.25);
margin: 7ex 3em 0 0;
padding: 2.5ex 2em;
display: flex;
flex-direction: column;
}
.clipboard-button {
display: none;
}
.clipboard--supported .clipboard-button {
display: initial;
}
.clipboard-source {
background-color: transparent;
border: none;
box-shadow: none;
color: inherit;
font-size: inherit;
}
.clipboard-paste {
min-width: 20em;
min-height: 8ex;
}
.slide {
display: none;
font-size: 6rem;
}
.slide--current {
display: block;
}
.container--content-loader {
min-width: 16em;
}
.tabpanel {
border: 1px solid #dedede;
display: none;
margin-top: .4rem;
padding: 0.8rem;
font-size: 6rem;
}
.tabpanel--current {
display: block;
}
================================================
FILE: examples/public/main.css
================================================
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
*:not(:active) {
color: #333;
}
a:active {
color: #333;
}
body {
background: #fff;
font-family: -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif;
font-size: 16px;
padding: 2ex 0;
line-height: 1.5;
}
p {
margin-bottom: 2ex;
}
textarea {
resize: none;
}
button,
input,
textarea {
background: #fff;
border: 0.25ex solid #333;
border-radius: 0.5em;
margin: 0.5ex 0.5em;
padding: 0.5ex 0.5em;
font-family: inherit;
font-size: 16px;
box-shadow: 0 0 0 0.5ex rgba(0, 0, 0, 0.25);
vertical-align: middle;
}
button {
background: #ccc;
border-left-color: #fff;
border-top-color: #fff;
}
button:active {
border-left-color: #333;
border-top-color: #333;
border-bottom-color: #ccc;
border-right-color: #ccc;
color: #333;
padding: calc(0.5ex + 1px) calc(0.5em - 1px) calc(0.5ex - 1px) calc(0.5em + 1px);
margin: calc(0.5ex - 1px) calc(0.5em + 1px) calc(0.5ex + 1px) calc(0.5em - 1px);
transform: translate(1px, 1px);
}
button:active,
button:focus {
outline: none;
}
button:focus-visible,
input:focus-visible,
textarea:focus-visible {
outline: none;
box-shadow: 0 0 0 0.5ex rgba(0, 51, 255, 0.5);
}
input,
textarea {
border-bottom-color: #ccc;
border-right-color: #ccc;
}
================================================
FILE: examples/server.js
================================================
const fs = require("fs")
const path = require("path")
const express = require("express")
const webpack = require("webpack")
const webpackMiddleware = require("webpack-dev-middleware")
const webpackConfig = require("./webpack.config")
const app = express()
const port = 9000
const publicPath = path.join(__dirname, "public")
const viewPath = path.join(__dirname, "views")
const viewEngine = "ejs"
app.set("views", viewPath)
app.set("view engine", viewEngine)
app.use(express.static(publicPath))
app.use(webpackMiddleware(webpack(webpackConfig)))
const pages = [
{ path: "/hello", title: "Hello" },
{ path: "/clipboard", title: "Clipboard" },
{ path: "/slideshow", title: "Slideshow" },
{ path: "/content-loader", title: "Content Loader" },
{ path: "/tabs", title: "Tabs" },
]
app.get("/", (req, res) => {
res.redirect(pages[0].path)
})
app.get("/uptime", (req, res, next) => {
res.send(`<span data-content-loader-target="item">${process.uptime().toString()}</span>`)
})
app.get("/:page", (req, res, next) => {
const currentPage = pages.find(page => page.path == req.path)
res.render(req.params.page, { pages, currentPage })
})
app.listen(port, () => {
console.log(`Listening on http://localhost:${port}/`)
})
================================================
FILE: examples/views/clipboard.ejs
================================================
<%- include("layout/head") %>
<p data-controller="clipboard" data-clipboard-supported-class="clipboard--supported">
<input data-clipboard-target="source" class="clipboard-source" type="text" value="https://stimulus.hotwired.dev" readonly>
<button data-action="clipboard#copy" class="clipboard-button">Copy</button>
</p>
<p data-controller="clipboard" data-clipboard-supported-class="clipboard--supported">
<input data-clipboard-target="source" class="clipboard-source" type="text" value="https://basecamp.com" readonly>
<button data-action="clipboard#copy" class="clipboard-button">Copy</button>
</p>
<br>
<p>
<textarea class="clipboard-paste" placeholder="Paste here…"></textarea>
</p>
<%- include("layout/tail") %>
================================================
FILE: examples/views/content-loader.ejs
================================================
<%- include("layout/head") %>
<p data-controller="content-loader"
data-content-loader-url-value="/uptime"></p>
<p data-controller="content-loader"
data-content-loader-url-value="/uptime"
data-content-loader-refresh-interval-value="3500"></p>
<p data-controller="content-loader"
data-content-loader-url-value="/uptime"
data-content-loader-refresh-interval-value="1000"></p>
<%- include("layout/tail") %>
================================================
FILE: examples/views/hello.ejs
================================================
<%- include("layout/head") %>
<form method="dialog" data-controller="hello">
<p>
<input data-hello-target="name" type="text" placeholder="Type your name…" autofocus>
<button data-action="click->hello#greet">Greet</button>
</p>
</form>
<%- include("layout/tail") %>
================================================
FILE: examples/views/layout/head.ejs
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Stimulus: <%= currentPage.title %> Example</title>
<link rel="stylesheet" href="/main.css">
<link rel="stylesheet" href="/examples.css">
<script src="/main.js" defer></script>
</head>
<body>
<main>
<div class="sidebar">
<img id="logo" src="/logo.svg" class="logo" alt="Stimulus logo" data-turbolinks-permanent>
<ul class="nav">
<% pages.forEach(page => { %>
<li>
<a href="<%= page.path %>" class="<%= page.path == currentPage.path ? " active " : " " %>">
<%= page.title %>
</a>
</li>
<% }) %>
</ul>
</div>
<div class="container container--<%= currentPage.path.slice(1) %>">
================================================
FILE: examples/views/layout/tail.ejs
================================================
</div>
</main>
</body>
</html>
================================================
FILE: examples/views/slideshow.ejs
================================================
<%- include("layout/head") %>
<div data-controller="slideshow" data-slideshow-current-slide-class="slide--current">
<button data-action="slideshow#previous">←</button>
<button data-action="slideshow#next">→</button>
<div data-slideshow-target="slide" class="slide">🐵</div>
<div data-slideshow-target="slide" class="slide">🙈</div>
<div data-slideshow-target="slide" class="slide">🙉</div>
<div data-slideshow-target="slide" class="slide">🙊</div>
</div>
<%- include("layout/tail") %>
================================================
FILE: examples/views/tabs.ejs
================================================
<%- include("layout/head") %>
<div data-controller="tabs" data-tabs-current-class="tabpanel--current" aria-label="example">
<p>This tabbed interface is operated by focusing on a button and pressing the left and right keys.</p>
<div role="tablist">
<button
id="tab1"
role="tab"
tabindex="0"
data-action="keydown.left->tabs#previous keydown.right->tabs#next click->tabs#open"
data-tabs-target="tab"
aria-controls="panel1"
>tab1</button>
<button
id="tab2"
role="tab"
tabindex="0"
data-action="keydown.left->tabs#previous keydown.right->tabs#next click->tabs#open"
data-tabs-target="tab"
aria-controls="panel2"
>tab2</button>
</div>
<div
id="panel1"
role="tabpanel"
tabindex="0"
data-tabs-target="tabpanel"
class="tabpanel tabpanel--current"
aria-labelledby="tab1"
>🐵</div>
<div
id="panel2"
role="tabpanel"
tabindex="0"
data-tabs-target="tabpanel"
class="tabpanel"
aria-labelledby="tab2"
>🙈</div>
</div>
<%- include("layout/tail") %>
================================================
FILE: examples/webpack.config.js
================================================
const path = require("path")
module.exports = {
entry: {
main: "./index.js"
},
output: {
filename: "[name].js"
},
mode: "development",
devtool: "inline-source-map",
module: {
rules: [
{
test: /\.ts$/,
use: [
{ loader: "ts-loader" }
]
},
{
test: /\.js$/,
exclude: [
/node_modules/
],
use: [
{ loader: "babel-loader" }
]
}
]
},
resolve: {
extensions: [".ts", ".js"],
modules: ["src", "node_modules"],
alias: {
"@hotwired/stimulus": path.resolve(__dirname, "../dist/stimulus.js"),
}
}
}
================================================
FILE: karma.conf.cjs
================================================
const config = {
basePath: ".",
browsers: ["ChromeHeadless", "FirefoxHeadless"],
frameworks: ["qunit"],
reporters: ["progress"],
singleRun: true,
autoWatch: false,
files: [
"dist/tests/index.js",
{ pattern: "src/tests/fixtures/**/*", watched: true, served: true, included: false },
{ pattern: "dist/tests/fixtures/**/*", watched: true, served: true, included: false },
],
preprocessors: {
"dist/tests/**/*.js": ["webpack"],
},
webpack: {
mode: "development",
resolve: {
extensions: [".js"],
},
},
client: {
clearContext: false,
qunit: {
showUI: true,
},
},
hostname: "0.0.0.0",
captureTimeout: 180000,
browserDisconnectTimeout: 180000,
browserDisconnectTolerance: 3,
browserNoActivityTimeout: 300000,
}
module.exports = function (karmaConfig) {
karmaConfig.set(config)
}
================================================
FILE: package.json
================================================
{
"name": "@hotwired/stimulus",
"version": "3.2.2",
"license": "MIT",
"description": "A modest JavaScript framework for the HTML you already have.",
"author": "Basecamp, LLC",
"contributors": [
"David Heinemeier Hansson <david@basecamp.com>",
"Javan Makhmali <javan@javan.us>",
"Sam Stephenson <sstephenson@gmail.com>"
],
"homepage": "https://stimulus.hotwired.dev",
"repository": {
"type": "git",
"url": "git+https://github.com/hotwired/stimulus.git"
},
"bugs": {
"url": "https://github.com/hotwired/stimulus/issues"
},
"publishConfig": {
"access": "public"
},
"module": "dist/stimulus.js",
"main": "dist/stimulus.umd.js",
"types": "dist/types/index.d.ts",
"files": [
"dist/stimulus.js",
"dist/stimulus.umd.js",
"dist/types/**/*"
],
"scripts": {
"clean": "rm -fr dist",
"types": "tsc --noEmit false --declaration true --emitDeclarationOnly true --outDir dist/types",
"prebuild": "yarn build:test",
"build": "yarn types && rollup -c",
"build:test": "tsc -b tsconfig.test.json",
"watch": "rollup -wc",
"prerelease": "yarn clean && yarn build && yarn build:test && git --no-pager diff && echo && npm pack --dry-run",
"release": "npm publish",
"start": "concurrently \"npm:watch\" \"npm:start:examples\"",
"start:examples": "cd examples && yarn install && node server.js",
"test": "yarn build:test && karma start karma.conf.cjs",
"test:watch": "yarn test --auto-watch --no-single-run",
"lint": "eslint . --ext .ts",
"format": "yarn lint --fix"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^16.0.1",
"@rollup/plugin-typescript": "^11.1.1",
"@types/qunit": "^2.19.10",
"@types/webpack-env": "^1.14.0",
"@typescript-eslint/eslint-plugin": "^5.59.11",
"@typescript-eslint/parser": "^5.59.11",
"concurrently": "^9.1.2",
"eslint": "^8.43.0",
"eslint-config-prettier": "^8.8.0",
"eslint-plugin-prettier": "^4.2.1",
"karma": "^6.4.4",
"karma-chrome-launcher": "^3.2.0",
"karma-firefox-launcher": "^2.1.3",
"karma-qunit": "^4.2.1",
"karma-webpack": "^4.0.2",
"prettier": "^2.8.8",
"qunit": "^2.20.0",
"rollup": "^2.53",
"rollup-plugin-terser": "^7.0.2",
"ts-loader": "^9.4.3",
"tslib": "^2.5.3",
"typescript": "^5.1.3",
"webpack": "^4.47.0"
},
"resolutions": {
"webdriverio": "^7.19.5"
}
}
================================================
FILE: packages/stimulus/.gitignore
================================================
README.md
================================================
FILE: packages/stimulus/.npmignore
================================================
rollup.config.js
*.log
================================================
FILE: packages/stimulus/index.d.ts
================================================
export * from "@hotwired/stimulus"
export as namespace Stimulus
================================================
FILE: packages/stimulus/index.js
================================================
export * from "@hotwired/stimulus"
================================================
FILE: packages/stimulus/package.json
================================================
{
"name": "stimulus",
"version": "3.2.2",
"description": "Stimulus JavaScript framework",
"homepage": "https://stimulus.hotwired.dev",
"repository": {
"type": "git",
"url": "git+https://github.com/hotwired/stimulus.git"
},
"author": "Basecamp, LLC",
"contributors": [
"David Heinemeier Hansson <david@basecamp.com>",
"Javan Makhmali <javan@javan.us>",
"Sam Stephenson <sstephenson@gmail.com>"
],
"module": "./dist/stimulus.js",
"main": "./dist/stimulus.umd.js",
"types": "./index.d.ts",
"exports": {
".": {
"main": "./dist/stimulus.umd.js",
"browser": "./dist/stimulus.js",
"import": "./dist/stimulus.js",
"module": "./dist/stimulus.js",
"umd": "./dist/stimulus.umd.js",
"types": "./index.d.ts"
},
"./webpack-helpers": {
"main": "./dist/webpack-helpers.umd.js",
"browser": "./dist/webpack-helpers.js",
"import": "./dist/webpack-helpers.js",
"module": "./dist/webpack-helpers.js",
"umd": "./dist/webpack-helpers.umd.js",
"types": "./webpack-helpers.d.ts"
}
},
"files": [
"index.d.ts",
"dist/stimulus.js",
"dist/stimulus.umd.js",
"webpack-helpers.js",
"webpack-helpers.d.ts",
"dist/webpack-helpers.js",
"dist/webpack-helpers.umd.js",
"README.md"
],
"license": "MIT",
"dependencies": {
"@hotwired/stimulus": "^3.2.2",
"@hotwired/stimulus-webpack-helpers": "^1.0.0"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-typescript": "^8.2.1",
"rollup": "^2.53"
},
"scripts": {
"clean": "rm -rf dist",
"build": "rollup --config rollup.config.js",
"prerelease": "yarn build && git --no-pager diff && echo && npm pack --dry-run",
"release": "npm publish"
},
"publishConfig": {
"access": "public"
}
}
================================================
FILE: packages/stimulus/rollup.config.js
================================================
import resolve from "@rollup/plugin-node-resolve"
export default [
{
input: "index.js",
output: [
{
name: "Stimulus",
file: "dist/stimulus.umd.js",
format: "umd"
},
{
file: "dist/stimulus.js",
format: "es"
},
],
context: "window",
plugins: [
resolve()
]
},
{
input: "webpack-helpers.js",
output: [
{
name: "StimulusWebpackHelpers",
file: "dist/webpack-helpers.umd.js",
format: "umd"
},
{
file: "dist/webpack-helpers.js",
format: "es"
},
],
context: "window",
plugins: [
resolve()
]
}
]
================================================
FILE: packages/stimulus/webpack-helpers.d.ts
================================================
export * from "@hotwired/stimulus-webpack-helpers"
export as namespace StimulusWebpackHelpers
================================================
FILE: packages/stimulus/webpack-helpers.js
================================================
export * from "@hotwired/stimulus-webpack-helpers"
================================================
FILE: rollup.config.js
================================================
import resolve from "@rollup/plugin-node-resolve"
import typescript from "@rollup/plugin-typescript"
import { terser } from "rollup-plugin-terser"
import { version } from "./package.json"
const year = new Date().getFullYear()
const banner = `/*\nStimulus ${version}\nCopyright © ${year} Basecamp, LLC\n */`
export default [
{
input: "src/index.js",
output: [
{
name: "Stimulus",
file: "dist/stimulus.umd.js",
format: "umd",
banner
},
{
file: "dist/stimulus.js",
format: "es",
banner
},
],
context: "window",
plugins: [
resolve(),
typescript()
]
},
{
input: "src/index.js",
output: {
file: "dist/stimulus.min.js",
format: "es",
banner,
sourcemap: true
},
context: "window",
plugins: [
resolve(),
typescript(),
terser({
mangle: true,
compress: true
})
]
}
]
================================================
FILE: src/core/action.ts
================================================
import { ActionDescriptor, parseActionDescriptorString, stringifyEventTarget } from "./action_descriptor"
import { Token } from "../mutation-observers"
import { Schema } from "./schema"
import { camelize } from "./string_helpers"
import { hasProperty } from "./utils"
const allModifiers = ["meta", "ctrl", "alt", "shift"]
export class Action {
readonly element: Element
readonly index: number
readonly eventTarget: EventTarget
readonly eventName: string
readonly eventOptions: AddEventListenerOptions
readonly identifier: string
readonly methodName: string
readonly keyFilter: string
readonly schema: Schema
static forToken(token: Token, schema: Schema) {
return new this(token.element, token.index, parseActionDescriptorString(token.content), schema)
}
constructor(element: Element, index: number, descriptor: Partial<ActionDescriptor>, schema: Schema) {
this.element = element
this.index = index
this.eventTarget = descriptor.eventTarget || element
this.eventName = descriptor.eventName || getDefaultEventNameForElement(element) || error("missing event name")
this.eventOptions = descriptor.eventOptions || {}
this.identifier = descriptor.identifier || error("missing identifier")
this.methodName = descriptor.methodName || error("missing method name")
this.keyFilter = descriptor.keyFilter || ""
this.schema = schema
}
toString() {
const eventFilter = this.keyFilter ? `.${this.keyFilter}` : ""
const eventTarget = this.eventTargetName ? `@${this.eventTargetName}` : ""
return `${this.eventName}${eventFilter}${eventTarget}->${this.identifier}#${this.methodName}`
}
shouldIgnoreKeyboardEvent(event: KeyboardEvent): boolean {
if (!this.keyFilter) {
return false
}
const filters = this.keyFilter.split("+")
if (this.keyFilterDissatisfied(event, filters)) {
return true
}
const standardFilter = filters.filter((key) => !allModifiers.includes(key))[0]
if (!standardFilter) {
// missing non modifier key
return false
}
if (!hasProperty(this.keyMappings, standardFilter)) {
error(`contains unknown key filter: ${this.keyFilter}`)
}
return this.keyMappings[standardFilter].toLowerCase() !== event.key.toLowerCase()
}
shouldIgnoreMouseEvent(event: MouseEvent): boolean {
if (!this.keyFilter) {
return false
}
const filters = [this.keyFilter]
if (this.keyFilterDissatisfied(event, filters)) {
return true
}
return false
}
get params() {
const params: { [key: string]: any } = {}
const pattern = new RegExp(`^data-${this.identifier}-(.+)-param$`, "i")
for (const { name, value } of Array.from(this.element.attributes)) {
const match = name.match(pattern)
const key = match && match[1]
if (key) {
params[camelize(key)] = typecast(value)
}
}
return params
}
private get eventTargetName() {
return stringifyEventTarget(this.eventTarget)
}
private get keyMappings() {
return this.schema.keyMappings
}
private keyFilterDissatisfied(event: KeyboardEvent | MouseEvent, filters: Array<string>): boolean {
const [meta, ctrl, alt, shift] = allModifiers.map((modifier) => filters.includes(modifier))
return event.metaKey !== meta || event.ctrlKey !== ctrl || event.altKey !== alt || event.shiftKey !== shift
}
}
const defaultEventNames: { [tagName: string]: (element: Element) => string } = {
a: () => "click",
button: () => "click",
form: () => "submit",
details: () => "toggle",
input: (e) => (e.getAttribute("type") == "submit" ? "click" : "input"),
select: () => "change",
textarea: () => "input",
}
export function getDefaultEventNameForElement(element: Element): string | undefined {
const tagName = element.tagName.toLowerCase()
if (tagName in defaultEventNames) {
return defaultEventNames[tagName](element)
}
}
function error(message: string): never {
throw new Error(message)
}
function typecast(value: any): any {
try {
return JSON.parse(value)
} catch (o_O) {
return value
}
}
================================================
FILE: src/core/action_descriptor.ts
================================================
import type { Controller } from "./controller"
export type ActionDescriptorFilters = Record<string, ActionDescriptorFilter>
export type ActionDescriptorFilter = (options: ActionDescriptorFilterOptions) => boolean
type ActionDescriptorFilterOptions = {
name: string
value: boolean
event: Event
element: Element
controller: Controller<Element>
}
export const defaultActionDescriptorFilters: ActionDescriptorFilters = {
stop({ event, value }) {
if (value) event.stopPropagation()
return true
},
prevent({ event, value }) {
if (value) event.preventDefault()
return true
},
self({ event, value, element }) {
if (value) {
return element === event.target
} else {
return true
}
},
}
export interface ActionDescriptor {
eventTarget: EventTarget
eventOptions: AddEventListenerOptions
eventName: string
identifier: string
methodName: string
keyFilter: string
}
// capture nos.: 1 1 2 2 3 3 4 4 5 5 6 6 7 7
const descriptorPattern = /^(?:(?:([^.]+?)\+)?(.+?)(?:\.(.+?))?(?:@(window|document))?->)?(.+?)(?:#([^:]+?))(?::(.+))?$/
export function parseActionDescriptorString(descriptorString: string): Partial<ActionDescriptor> {
const source = descriptorString.trim()
const matches = source.match(descriptorPattern) || []
let eventName = matches[2]
let keyFilter = matches[3]
if (keyFilter && !["keydown", "keyup", "keypress"].includes(eventName)) {
eventName += `.${keyFilter}`
keyFilter = ""
}
return {
eventTarget: parseEventTarget(matches[4]),
eventName,
eventOptions: matches[7] ? parseEventOptions(matches[7]) : {},
identifier: matches[5],
methodName: matches[6],
keyFilter: matches[1] || keyFilter,
}
}
function parseEventTarget(eventTargetName: string): EventTarget | undefined {
if (eventTargetName == "window") {
return window
} else if (eventTargetName == "document") {
return document
}
}
function parseEventOptions(eventOptions: string): AddEventListenerOptions {
return eventOptions
.split(":")
.reduce((options, token) => Object.assign(options, { [token.replace(/^!/, "")]: !/^!/.test(token) }), {})
}
export function stringifyEventTarget(eventTarget: EventTarget) {
if (eventTarget == window) {
return "window"
} else if (eventTarget == document) {
return "document"
}
}
================================================
FILE: src/core/action_event.ts
================================================
export interface ActionEvent extends Event {
params: { [key: string]: any }
}
================================================
FILE: src/core/application.ts
================================================
import { Controller, ControllerConstructor } from "./controller"
import { Definition } from "./definition"
import { Dispatcher } from "./dispatcher"
import { ErrorHandler } from "./error_handler"
import { Logger } from "./logger"
import { Router } from "./router"
import { Schema, defaultSchema } from "./schema"
import { ActionDescriptorFilter, ActionDescriptorFilters, defaultActionDescriptorFilters } from "./action_descriptor"
export class Application implements ErrorHandler {
readonly element: Element
readonly schema: Schema
readonly dispatcher: Dispatcher
readonly router: Router
readonly actionDescriptorFilters: ActionDescriptorFilters
logger: Logger = console
debug = false
static start(element?: Element, schema?: Schema): Application {
const application = new this(element, schema)
application.start()
return application
}
constructor(element: Element = document.documentElement, schema: Schema = defaultSchema) {
this.element = element
this.schema = schema
this.dispatcher = new Dispatcher(this)
this.router = new Router(this)
this.actionDescriptorFilters = { ...defaultActionDescriptorFilters }
}
async start() {
await domReady()
this.logDebugActivity("application", "starting")
this.dispatcher.start()
this.router.start()
this.logDebugActivity("application", "start")
}
stop() {
this.logDebugActivity("application", "stopping")
this.dispatcher.stop()
this.router.stop()
this.logDebugActivity("application", "stop")
}
register(identifier: string, controllerConstructor: ControllerConstructor) {
this.load({ identifier, controllerConstructor })
}
registerActionOption(name: string, filter: ActionDescriptorFilter) {
this.actionDescriptorFilters[name] = filter
}
load(...definitions: Definition[]): void
load(definitions: Definition[]): void
load(head: Definition | Definition[], ...rest: Definition[]) {
const definitions = Array.isArray(head) ? head : [head, ...rest]
definitions.forEach((definition) => {
if ((definition.controllerConstructor as any).shouldLoad) {
this.router.loadDefinition(definition)
}
})
}
unload(...identifiers: string[]): void
unload(identifiers: string[]): void
unload(head: string | string[], ...rest: string[]) {
const identifiers = Array.isArray(head) ? head : [head, ...rest]
identifiers.forEach((identifier) => this.router.unloadIdentifier(identifier))
}
// Controllers
get controllers(): Controller[] {
return this.router.contexts.map((context) => context.controller)
}
getControllerForElementAndIdentifier(element: Element, identifier: string): Controller | null {
const context = this.router.getContextForElementAndIdentifier(element, identifier)
return context ? context.controller : null
}
// Error handling
handleError(error: Error, message: string, detail: object) {
this.logger.error(`%s\n\n%o\n\n%o`, message, error, detail)
window.onerror?.(message, "", 0, 0, error)
}
// Debug logging
logDebugActivity = (identifier: string, functionName: string, detail: object = {}): void => {
if (this.debug) {
this.logFormattedMessage(identifier, functionName, detail)
}
}
private logFormattedMessage(identifier: string, functionName: string, detail: object = {}) {
detail = Object.assign({ application: this }, detail)
this.logger.groupCollapsed(`${identifier} #${functionName}`)
this.logger.log("details:", { ...detail })
this.logger.groupEnd()
}
}
function domReady() {
return new Promise<void>((resolve) => {
if (document.readyState == "loading") {
document.addEventListener("DOMContentLoaded", () => resolve())
} else {
resolve()
}
})
}
================================================
FILE: src/core/binding.ts
================================================
import { Action } from "./action"
import { ActionEvent } from "./action_event"
import { Context } from "./context"
import { Controller } from "./controller"
import { Scope } from "./scope"
export class Binding {
readonly context: Context
readonly action: Action
constructor(context: Context, action: Action) {
this.context = context
this.action = action
}
get index(): number {
return this.action.index
}
get eventTarget(): EventTarget {
return this.action.eventTarget
}
get eventOptions(): AddEventListenerOptions {
return this.action.eventOptions
}
get identifier(): string {
return this.context.identifier
}
handleEvent(event: Event) {
const actionEvent = this.prepareActionEvent(event)
if (this.willBeInvokedByEvent(event) && this.applyEventModifiers(actionEvent)) {
this.invokeWithEvent(actionEvent)
}
}
get eventName(): string {
return this.action.eventName
}
get method(): Function {
const method = (this.controller as any)[this.methodName]
if (typeof method == "function") {
return method
}
throw new Error(`Action "${this.action}" references undefined method "${this.methodName}"`)
}
private applyEventModifiers(event: Event): boolean {
const { element } = this.action
const { actionDescriptorFilters } = this.context.application
const { controller } = this.context
let passes = true
for (const [name, value] of Object.entries(this.eventOptions)) {
if (name in actionDescriptorFilters) {
const filter = actionDescriptorFilters[name]
passes = passes && filter({ name, value, event, element, controller })
} else {
continue
}
}
return passes
}
private prepareActionEvent(event: Event): ActionEvent {
return Object.assign(event, { params: this.action.params })
}
private invokeWithEvent(event: ActionEvent) {
const { target, currentTarget } = event
try {
this.method.call(this.controller, event)
this.context.logDebugActivity(this.methodName, { event, target, currentTarget, action: this.methodName })
} catch (error: any) {
const { identifier, controller, element, index } = this
const detail = { identifier, controller, element, index, event }
this.context.handleError(error, `invoking action "${this.action}"`, detail)
}
}
private willBeInvokedByEvent(event: Event): boolean {
const eventTarget = event.target
if (event instanceof KeyboardEvent && this.action.shouldIgnoreKeyboardEvent(event)) {
return false
}
if (event instanceof MouseEvent && this.action.shouldIgnoreMouseEvent(event)) {
return false
}
if (this.element === eventTarget) {
return true
} else if (eventTarget instanceof Element && this.element.contains(eventTarget)) {
return this.scope.containsElement(eventTarget)
} else {
return this.scope.containsElement(this.action.element)
}
}
private get controller(): Controller {
return this.context.controller
}
private get methodName(): string {
return this.action.methodName
}
private get element(): Element {
return this.scope.element
}
private get scope(): Scope {
return this.context.scope
}
}
================================================
FILE: src/core/binding_observer.ts
================================================
import { Action } from "./action"
import { Binding } from "./binding"
import { Context } from "./context"
import { ErrorHandler } from "./error_handler"
import { Schema } from "./schema"
import { Token, ValueListObserver, ValueListObserverDelegate } from "../mutation-observers"
export interface BindingObserverDelegate extends ErrorHandler {
bindingConnected(binding: Binding): void
bindingDisconnected(binding: Binding, clearEventListeners?: boolean): void
}
export class BindingObserver implements ValueListObserverDelegate<Action> {
readonly context: Context
private delegate: BindingObserverDelegate
private valueListObserver?: ValueListObserver<Action>
private bindingsByAction: Map<Action, Binding>
constructor(context: Context, delegate: BindingObserverDelegate) {
this.context = context
this.delegate = delegate
this.bindingsByAction = new Map()
}
start() {
if (!this.valueListObserver) {
this.valueListObserver = new ValueListObserver(this.element, this.actionAttribute, this)
this.valueListObserver.start()
}
}
stop() {
if (this.valueListObserver) {
this.valueListObserver.stop()
delete this.valueListObserver
this.disconnectAllActions()
}
}
get element() {
return this.context.element
}
get identifier() {
return this.context.identifier
}
get actionAttribute() {
return this.schema.actionAttribute
}
get schema(): Schema {
return this.context.schema
}
get bindings(): Binding[] {
return Array.from(this.bindingsByAction.values())
}
private connectAction(action: Action) {
const binding = new Binding(this.context, action)
this.bindingsByAction.set(action, binding)
this.delegate.bindingConnected(binding)
}
private disconnectAction(action: Action) {
const binding = this.bindingsByAction.get(action)
if (binding) {
this.bindingsByAction.delete(action)
this.delegate.bindingDisconnected(binding)
}
}
private disconnectAllActions() {
this.bindings.forEach((binding) => this.delegate.bindingDisconnected(binding, true))
this.bindingsByAction.clear()
}
// Value observer delegate
parseValueForToken(token: Token): Action | undefined {
const action = Action.forToken(token, this.schema)
if (action.identifier == this.identifier) {
return action
}
}
elementMatchedValue(element: Element, action: Action) {
this.connectAction(action)
}
elementUnmatchedValue(element: Element, action: Action) {
this.disconnectAction(action)
}
}
================================================
FILE: src/core/blessing.ts
================================================
import { Constructor } from "./constructor"
import { readInheritableStaticArrayValues } from "./inheritable_statics"
export type Blessing<T> = (constructor: Constructor<T>) => PropertyDescriptorMap
export interface Blessable<T> extends Constructor<T> {
readonly blessings?: Blessing<T>[]
}
export function bless<T>(constructor: Blessable<T>): Constructor<T> {
return shadow(constructor, getBlessedProperties(constructor))
}
function shadow<T>(constructor: Constructor<T>, properties: PropertyDescriptorMap) {
const shadowConstructor = extend(constructor)
const shadowProperties = getShadowProperties(constructor.prototype, properties)
Object.defineProperties(shadowConstructor.prototype, shadowProperties)
return shadowConstructor
}
function getBlessedProperties<T>(constructor: Constructor<T>) {
const blessings = readInheritableStaticArrayValues(constructor, "blessings") as Blessing<T>[]
return blessings.reduce((blessedProperties, blessing) => {
const properties = blessing(constructor)
for (const key in properties) {
const descriptor = blessedProperties[key] || ({} as PropertyDescriptor)
blessedProperties[key] = Object.assign(descriptor, properties[key])
}
return blessedProperties
}, {} as PropertyDescriptorMap)
}
function getShadowProperties(prototype: any, properties: PropertyDescriptorMap) {
return getOwnKeys(properties).reduce((shadowProperties, key) => {
const descriptor = getShadowedDescriptor(prototype, properties, key)
if (descriptor) {
Object.assign(shadowProperties, { [key]: descriptor })
}
return shadowProperties
}, {} as PropertyDescriptorMap)
}
function getShadowedDescriptor(prototype: any, properties: PropertyDescriptorMap, key: string | symbol) {
const shadowingDescriptor = Object.getOwnPropertyDescriptor(prototype, key)
const shadowedByValue = shadowingDescriptor && "value" in shadowingDescriptor
if (!shadowedByValue) {
const descriptor = Object.getOwnPropertyDescriptor(properties, key)!.value
if (shadowingDescriptor) {
descriptor.get = shadowingDescriptor.get || descriptor.get
descriptor.set = shadowingDescriptor.set || descriptor.set
}
return descriptor
}
}
const getOwnKeys = (() => {
if (typeof Object.getOwnPropertySymbols == "function") {
return (object: any) => [...Object.getOwnPropertyNames(object), ...Object.getOwnPropertySymbols(object)]
} else {
return Object.getOwnPropertyNames
}
})()
const extend = (() => {
function extendWithReflect<T extends Constructor<any>>(constructor: T): T {
function extended() {
return Reflect.construct(constructor, arguments, new.target)
}
extended.prototype = Object.create(constructor.prototype, {
constructor: { value: extended },
})
Reflect.setPrototypeOf(extended, constructor)
return extended as any
}
function testReflectExtension() {
const a = function (this: any) {
this.a.call(this)
} as any
const b = extendWithReflect(a)
b.prototype.a = function () {}
return new b()
}
try {
testReflectExtension()
return extendWithReflect
} catch (error: any) {
return <T extends Constructor<any>>(constructor: T) => class extended extends constructor {}
}
})()
================================================
FILE: src/core/class_map.ts
================================================
import { Scope } from "./scope"
import { tokenize } from "./string_helpers"
export class ClassMap {
readonly scope: Scope
constructor(scope: Scope) {
this.scope = scope
}
has(name: string) {
return this.data.has(this.getDataKey(name))
}
get(name: string): string | undefined {
return this.getAll(name)[0]
}
getAll(name: string) {
const tokenString = this.data.get(this.getDataKey(name)) || ""
return tokenize(tokenString)
}
getAttributeName(name: string) {
return this.data.getAttributeNameForKey(this.getDataKey(name))
}
getDataKey(name: string) {
return `${name}-class`
}
get data() {
return this.scope.data
}
}
================================================
FILE: src/core/class_properties.ts
================================================
import { Constructor } from "./constructor"
import { Controller } from "./controller"
import { readInheritableStaticArrayValues } from "./inheritable_statics"
import { capitalize } from "./string_helpers"
export function ClassPropertiesBlessing<T>(constructor: Constructor<T>) {
const classes = readInheritableStaticArrayValues(constructor, "classes")
return classes.reduce((properties, classDefinition) => {
return Object.assign(properties, propertiesForClassDefinition(classDefinition))
}, {} as PropertyDescriptorMap)
}
function propertiesForClassDefinition(key: string) {
return {
[`${key}Class`]: {
get(this: Controller) {
const { classes } = this
if (classes.has(key)) {
return classes.get(key)
} else {
const attribute = classes.getAttributeName(key)
throw new Error(`Missing attribute "${attribute}"`)
}
},
},
[`${key}Classes`]: {
get(this: Controller) {
return this.classes.getAll(key)
},
},
[`has${capitalize(key)}Class`]: {
get(this: Controller) {
return this.classes.has(key)
},
},
}
}
================================================
FILE: src/core/constructor.ts
================================================
export type Constructor<T> = new (...args: any[]) => T
================================================
FILE: src/core/context.ts
================================================
import { Application } from "./application"
import { BindingObserver } from "./binding_observer"
import { Controller } from "./controller"
import { Dispatcher } from "./dispatcher"
import { ErrorHandler } from "./error_handler"
import { Module } from "./module"
import { Schema } from "./schema"
import { Scope } from "./scope"
import { ValueObserver } from "./value_observer"
import { TargetObserver, TargetObserverDelegate } from "./target_observer"
import { OutletObserver, OutletObserverDelegate } from "./outlet_observer"
import { namespaceCamelize } from "./string_helpers"
export class Context implements ErrorHandler, TargetObserverDelegate, OutletObserverDelegate {
readonly module: Module
readonly scope: Scope
readonly controller: Controller
private bindingObserver: BindingObserver
private valueObserver: ValueObserver
private targetObserver: TargetObserver
private outletObserver: OutletObserver
constructor(module: Module, scope: Scope) {
this.module = module
this.scope = scope
this.controller = new module.controllerConstructor(this)
this.bindingObserver = new BindingObserver(this, this.dispatcher)
this.valueObserver = new ValueObserver(this, this.controller)
this.targetObserver = new TargetObserver(this, this)
this.outletObserver = new OutletObserver(this, this)
try {
this.controller.initialize()
this.logDebugActivity("initialize")
} catch (error: any) {
this.handleError(error, "initializing controller")
}
}
connect() {
this.bindingObserver.start()
this.valueObserver.start()
this.targetObserver.start()
this.outletObserver.start()
try {
this.controller.connect()
this.logDebugActivity("connect")
} catch (error: any) {
this.handleError(error, "connecting controller")
}
}
refresh() {
this.outletObserver.refresh()
}
disconnect() {
try {
this.controller.disconnect()
this.logDebugActivity("disconnect")
} catch (error: any) {
this.handleError(error, "disconnecting controller")
}
this.outletObserver.stop()
this.targetObserver.stop()
this.valueObserver.stop()
this.bindingObserver.stop()
}
get application(): Application {
return this.module.application
}
get identifier(): string {
return this.module.identifier
}
get schema(): Schema {
return this.application.schema
}
get dispatcher(): Dispatcher {
return this.application.dispatcher
}
get element(): Element {
return this.scope.element
}
get parentElement(): Element | null {
return this.element.parentElement
}
// Error handling
handleError(error: Error, message: string, detail: object = {}) {
const { identifier, controller, element } = this
detail = Object.assign({ identifier, controller, element }, detail)
this.application.handleError(error, `Error ${message}`, detail)
}
// Debug logging
logDebugActivity = (functionName: string, detail: object = {}): void => {
const { identifier, controller, element } = this
detail = Object.assign({ identifier, controller, element }, detail)
this.application.logDebugActivity(this.identifier, functionName, detail)
}
// Target observer delegate
targetConnected(element: Element, name: string) {
this.invokeControllerMethod(`${name}TargetConnected`, element)
}
targetDisconnected(element: Element, name: string) {
this.invokeControllerMethod(`${name}TargetDisconnected`, element)
}
// Outlet observer delegate
outletConnected(outlet: Controller, element: Element, name: string) {
this.invokeControllerMethod(`${namespaceCamelize(name)}OutletConnected`, outlet, element)
}
outletDisconnected(outlet: Controller, element: Element, name: string) {
this.invokeControllerMethod(`${namespaceCamelize(name)}OutletDisconnected`, outlet, element)
}
// Private
invokeControllerMethod(methodName: string, ...args: any[]) {
const controller: any = this.controller
if (typeof controller[methodName] == "function") {
controller[methodName](...args)
}
}
}
================================================
FILE: src/core/controller.ts
================================================
import { Application } from "./application"
import { ClassPropertiesBlessing } from "./class_properties"
import { Constructor } from "./constructor"
import { Context } from "./context"
import { OutletPropertiesBlessing } from "./outlet_properties"
import { TargetPropertiesBlessing } from "./target_properties"
import { ValuePropertiesBlessing, ValueDefinitionMap } from "./value_properties"
export type ControllerConstructor = Constructor<Controller>
type DispatchOptions = Partial<{
target: Element | Window | Document
detail: Object
prefix: string
bubbles: boolean
cancelable: boolean
}>
export class Controller<ElementType extends Element = Element> {
static blessings = [
ClassPropertiesBlessing,
TargetPropertiesBlessing,
ValuePropertiesBlessing,
OutletPropertiesBlessing,
]
static targets: string[] = []
static outlets: string[] = []
static values: ValueDefinitionMap = {}
static get shouldLoad() {
return true
}
static afterLoad(_identifier: string, _application: Application) {
return
}
readonly context: Context
constructor(context: Context) {
this.context = context
}
get application() {
return this.context.application
}
get scope() {
return this.context.scope
}
get element() {
return this.scope.element as ElementType
}
get identifier() {
return this.scope.identifier
}
get targets() {
return this.scope.targets
}
get outlets() {
return this.scope.outlets
}
get classes() {
return this.scope.classes
}
get data() {
return this.scope.data
}
initialize() {
// Override in your subclass to set up initial controller state
}
connect() {
// Override in your subclass to respond when the controller is connected to the DOM
}
disconnect() {
// Override in your subclass to respond when the controller is disconnected from the DOM
}
dispatch(
eventName: string,
{
target = this.element,
detail = {},
prefix = this.identifier,
bubbles = true,
cancelable = true,
}: DispatchOptions = {}
) {
const type = prefix ? `${prefix}:${eventName}` : eventName
const event = new CustomEvent(type, { detail, bubbles, cancelable })
target.dispatchEvent(event)
return event
}
}
================================================
FILE: src/core/data_map.ts
================================================
import { Scope } from "./scope"
import { dasherize } from "./string_helpers"
export class DataMap {
readonly scope: Scope
constructor(scope: Scope) {
this.scope = scope
}
get element(): Element {
return this.scope.element
}
get identifier(): string {
return this.scope.identifier
}
get(key: string): string | null {
const name = this.getAttributeNameForKey(key)
return this.element.getAttribute(name)
}
set(key: string, value: string): string | null {
const name = this.getAttributeNameForKey(key)
this.element.setAttribute(name, value)
return this.get(key)
}
has(key: string): boolean {
const name = this.getAttributeNameForKey(key)
return this.element.hasAttribute(name)
}
delete(key: string): boolean {
if (this.has(key)) {
const name = this.getAttributeNameForKey(key)
this.element.removeAttribute(name)
return true
} else {
return false
}
}
getAttributeNameForKey(key: string): string {
return `data-${this.identifier}-${dasherize(key)}`
}
}
================================================
FILE: src/core/definition.ts
================================================
import { bless } from "./blessing"
import { ControllerConstructor } from "./controller"
export interface Definition {
identifier: string
controllerConstructor: ControllerConstructor
}
export function blessDefinition(definition: Definition): Definition {
return {
identifier: definition.identifier,
controllerConstructor: bless(definition.controllerConstructor),
}
}
================================================
FILE: src/core/dispatcher.ts
================================================
import { Application } from "./application"
import { Binding } from "./binding"
import { BindingObserverDelegate } from "./binding_observer"
import { EventListener } from "./event_listener"
export class Dispatcher implements BindingObserverDelegate {
readonly application: Application
private eventListenerMaps: Map<EventTarget, Map<string, EventListener>>
private started: boolean
constructor(application: Application) {
this.application = application
this.eventListenerMaps = new Map()
this.started = false
}
start() {
if (!this.started) {
this.started = true
this.eventListeners.forEach((eventListener) => eventListener.connect())
}
}
stop() {
if (this.started) {
this.started = false
this.eventListeners.forEach((eventListener) => eventListener.disconnect())
}
}
get eventListeners(): EventListener[] {
return Array.from(this.eventListenerMaps.values()).reduce(
(listeners, map) => listeners.concat(Array.from(map.values())),
[] as EventListener[]
)
}
// Binding observer delegate
bindingConnected(binding: Binding) {
this.fetchEventListenerForBinding(binding).bindingConnected(binding)
}
bindingDisconnected(binding: Binding, clearEventListeners = false) {
this.fetchEventListenerForBinding(binding).bindingDisconnected(binding)
if (clearEventListeners) this.clearEventListenersForBinding(binding)
}
// Error handling
handleError(error: Error, message: string, detail: object = {}) {
this.application.handleError(error, `Error ${message}`, detail)
}
private clearEventListenersForBinding(binding: Binding) {
const eventListener = this.fetchEventListenerForBinding(binding)
if (!eventListener.hasBindings()) {
eventListener.disconnect()
this.removeMappedEventListenerFor(binding)
}
}
private removeMappedEventListenerFor(binding: Binding) {
const { eventTarget, eventName, eventOptions } = binding
const eventListenerMap = this.fetchEventListenerMapForEventTarget(eventTarget)
const cacheKey = this.cacheKey(eventName, eventOptions)
eventListenerMap.delete(cacheKey)
if (eventListenerMap.size == 0) this.eventListenerMaps.delete(eventTarget)
}
private fetchEventListenerForBinding(binding: Binding): EventListener {
const { eventTarget, eventName, eventOptions } = binding
return this.fetchEventListener(eventTarget, eventName, eventOptions)
}
private fetchEventListener(
eventTarget: EventTarget,
eventName: string,
eventOptions: AddEventListenerOptions
): EventListener {
const eventListenerMap = this.fetchEventListenerMapForEventTarget(eventTarget)
const cacheKey = this.cacheKey(eventName, eventOptions)
let eventListener = eventListenerMap.get(cacheKey)
if (!eventListener) {
eventListener = this.createEventListener(eventTarget, eventName, eventOptions)
eventListenerMap.set(cacheKey, eventListener)
}
return eventListener
}
private createEventListener(
eventTarget: EventTarget,
eventName: string,
eventOptions: AddEventListenerOptions
): EventListener {
const eventListener = new EventListener(eventTarget, eventName, eventOptions)
if (this.started) {
eventListener.connect()
}
return eventListener
}
private fetchEventListenerMapForEventTarget(eventTarget: EventTarget): Map<string, EventListener> {
let eventListenerMap = this.eventListenerMaps.get(eventTarget)
if (!eventListenerMap) {
eventListenerMap = new Map()
this.eventListenerMaps.set(eventTarget, eventListenerMap)
}
return eventListenerMap
}
private cacheKey(eventName: string, eventOptions: any): string {
const parts = [eventName]
Object.keys(eventOptions)
.sort()
.forEach((key) => {
parts.push(`${eventOptions[key] ? "" : "!"}${key}`)
})
return parts.join(":")
}
}
================================================
FILE: src/core/error_handler.ts
================================================
export interface ErrorHandler {
handleError(error: Error, message: string, detail: object): void
}
================================================
FILE: src/core/event_listener.ts
================================================
import { Binding } from "./binding"
export class EventListener implements EventListenerObject {
readonly eventTarget: EventTarget
readonly eventName: string
readonly eventOptions: AddEventListenerOptions
private unorderedBindings: Set<Binding>
constructor(eventTarget: EventTarget, eventName: string, eventOptions: AddEventListenerOptions) {
this.eventTarget = eventTarget
this.eventName = eventName
this.eventOptions = eventOptions
this.unorderedBindings = new Set()
}
connect() {
this.eventTarget.addEventListener(this.eventName, this, this.eventOptions)
}
disconnect() {
this.eventTarget.removeEventListener(this.eventName, this, this.eventOptions)
}
// Binding observer delegate
bindingConnected(binding: Binding) {
this.unorderedBindings.add(binding)
}
bindingDisconnected(binding: Binding) {
this.unorderedBindings.delete(binding)
}
handleEvent(event: Event) {
// FIXME: Determine why TS won't recognize that the extended event has immediatePropagationStopped
const extendedEvent = extendEvent(event) as any
for (const binding of this.bindings) {
if (extendedEvent.immediatePropagationStopped) {
break
} else {
binding.handleEvent(extendedEvent)
}
}
}
hasBindings() {
return this.unorderedBindings.size > 0
}
get bindings(): Binding[] {
return Array.from(this.unorderedBindings).sort((left, right) => {
const leftIndex = left.index,
rightIndex = right.index
return leftIndex < rightIndex ? -1 : leftIndex > rightIndex ? 1 : 0
})
}
}
function extendEvent(event: Event) {
if ("immediatePropagationStopped" in event) {
return event
} else {
const { stopImmediatePropagation } = event
return Object.assign(event, {
immediatePropagationStopped: false,
stopImmediatePropagation() {
this.immediatePropagationStopped = true
stopImmediatePropagation.call(this)
},
})
}
}
================================================
FILE: src/core/guide.ts
================================================
import { Logger } from "./logger"
export class Guide {
readonly logger: Logger
readonly warnedKeysByObject: WeakMap<any, Set<string>> = new WeakMap()
constructor(logger: Logger) {
this.logger = logger
}
warn(object: any, key: string, message: string) {
let warnedKeys: Set<string> | undefined = this.warnedKeysByObject.get(object)
if (!warnedKeys) {
warnedKeys = new Set()
this.warnedKeysByObject.set(object, warnedKeys)
}
if (!warnedKeys.has(key)) {
warnedKeys.add(key)
this.logger.warn(message, object)
}
}
}
================================================
FILE: src/core/index.ts
================================================
export { ActionEvent } from "./action_event"
export { Application } from "./application"
export { Context } from "./context"
export { Controller, ControllerConstructor } from "./controller"
export { Definition } from "./definition"
export { Schema, defaultSchema } from "./schema"
================================================
FILE: src/core/inheritable_statics.ts
================================================
import { Constructor } from "./constructor"
export function readInheritableStaticArrayValues<T, U = string>(constructor: Constructor<T>, propertyName: string) {
const ancestors = getAncestorsForConstructor(constructor)
return Array.from(
ancestors.reduce((values, constructor) => {
getOwnStaticArrayValues(constructor, propertyName).forEach((name) => values.add(name))
return values
}, new Set() as Set<U>)
)
}
export function readInheritableStaticObjectPairs<T, U>(constructor: Constructor<T>, propertyName: string) {
const ancestors = getAncestorsForConstructor(constructor)
return ancestors.reduce((pairs, constructor) => {
pairs.push(...(getOwnStaticObjectPairs(constructor, propertyName) as any))
return pairs
}, [] as [string, U][])
}
function getAncestorsForConstructor<T>(constructor: Constructor<T>) {
const ancestors: Constructor<any>[] = []
while (constructor) {
ancestors.push(constructor)
constructor = Object.getPrototypeOf(constructor)
}
return ancestors.reverse()
}
function getOwnStaticArrayValues<T>(constructor: Constructor<T>, propertyName: string) {
const definition = (constructor as any)[propertyName]
return Array.isArray(definition) ? definition : []
}
function getOwnStaticObjectPairs<T, U>(constructor: Constructor<T>, propertyName: string) {
const definition = (constructor as any)[propertyName]
return definition ? Object.keys(definition).map((key) => [key, definition[key]] as [string, U]) : []
}
================================================
FILE: src/core/logger.ts
================================================
export interface Logger {
log(message: string, ...args: any[]): void
warn(message: string, ...args: any[]): void
error(message: string, ...args: any[]): void
groupCollapsed(groupTitle?: string, ...args: any[]): void
groupEnd(): void
}
================================================
FILE: src/core/module.ts
================================================
import { Application } from "./application"
import { Context } from "./context"
import { ControllerConstructor } from "./controller"
import { Definition, blessDefinition } from "./definition"
import { Scope } from "./scope"
export class Module {
readonly application: Application
readonly definition: Definition
private contextsByScope: WeakMap<Scope, Context>
private connectedContexts: Set<Context>
constructor(application: Application, definition: Definition) {
this.application = application
this.definition = blessDefinition(definition)
this.contextsByScope = new WeakMap()
this.connectedContexts = new Set()
}
get identifier(): string {
return this.definition.identifier
}
get controllerConstructor(): ControllerConstructor {
return this.definition.controllerConstructor
}
get contexts(): Context[] {
return Array.from(this.connectedContexts)
}
connectContextForScope(scope: Scope) {
const context = this.fetchContextForScope(scope)
this.connectedContexts.add(context)
context.connect()
}
disconnectContextForScope(scope: Scope) {
const context = this.contextsByScope.get(scope)
if (context) {
this.connectedContexts.delete(context)
context.disconnect()
}
}
private fetchContextForScope(scope: Scope): Context {
let context = this.contextsByScope.get(scope)
if (!context) {
context = new Context(this, scope)
this.contextsByScope.set(scope, context)
}
return context
}
}
================================================
FILE: src/core/outlet_observer.ts
================================================
import { Multimap } from "../multimap"
import { AttributeObserver, AttributeObserverDelegate } from "../mutation-observers"
import { SelectorObserver, SelectorObserverDelegate } from "../mutation-observers"
import { Context } from "./context"
import { Controller } from "./controller"
import { readInheritableStaticArrayValues } from "./inheritable_statics"
type OutletObserverDetails = { outletName: string }
export interface OutletObserverDelegate {
outletConnected(outlet: Controller, element: Element, outletName: string): void
outletDisconnected(outlet: Controller, element: Element, outletName: string): void
}
export class OutletObserver implements AttributeObserverDelegate, SelectorObserverDelegate {
started: boolean
readonly context: Context
readonly delegate: OutletObserverDelegate
readonly outletsByName: Multimap<string, Controller>
readonly outletElementsByName: Multimap<string, Element>
private selectorObserverMap: Map<string, SelectorObserver>
private attributeObserverMap: Map<string, AttributeObserver>
constructor(context: Context, delegate: OutletObserverDelegate) {
this.started = false
this.context = context
this.delegate = delegate
this.outletsByName = new Multimap()
this.outletElementsByName = new Multimap()
this.selectorObserverMap = new Map()
this.attributeObserverMap = new Map()
}
start() {
if (!this.started) {
this.outletDefinitions.forEach((outletName) => {
this.setupSelectorObserverForOutlet(outletName)
this.setupAttributeObserverForOutlet(outletName)
})
this.started = true
this.dependentContexts.forEach((context) => context.refresh())
}
}
refresh() {
this.selectorObserverMap.forEach((observer) => observer.refresh())
this.attributeObserverMap.forEach((observer) => observer.refresh())
}
stop() {
if (this.started) {
this.started = false
this.disconnectAllOutlets()
this.stopSelectorObservers()
this.stopAttributeObservers()
}
}
stopSelectorObservers() {
if (this.selectorObserverMap.size > 0) {
this.selectorObserverMap.forEach((observer) => observer.stop())
this.selectorObserverMap.clear()
}
}
stopAttributeObservers() {
if (this.attributeObserverMap.size > 0) {
this.attributeObserverMap.forEach((observer) => observer.stop())
this.attributeObserverMap.clear()
}
}
// Selector observer delegate
selectorMatched(element: Element, _selector: string, { outletName }: OutletObserverDetails) {
const outlet = this.getOutlet(element, outletName)
if (outlet) {
this.connectOutlet(outlet, element, outletName)
}
}
selectorUnmatched(element: Element, _selector: string, { outletName }: OutletObserverDetails) {
const outlet = this.getOutletFromMap(element, outletName)
if (outlet) {
this.disconnectOutlet(outlet, element, outletName)
}
}
selectorMatchElement(element: Element, { outletName }: OutletObserverDetails) {
const selector = this.selector(outletName)
const hasOutlet = this.hasOutlet(element, outletName)
const hasOutletController = element.matches(`[${this.schema.controllerAttribute}~=${outletName}]`)
if (selector) {
return hasOutlet && hasOutletController && element.matches(selector)
} else {
return false
}
}
// Attribute observer delegate
elementMatchedAttribute(_element: Element, attributeName: string) {
const outletName = this.getOutletNameFromOutletAttributeName(attributeName)
if (outletName) {
this.updateSelectorObserverForOutlet(outletName)
}
}
elementAttributeValueChanged(_element: Element, attributeName: string) {
const outletName = this.getOutletNameFromOutletAttributeName(attributeName)
if (outletName) {
this.updateSelectorObserverForOutlet(outletName)
}
}
elementUnmatchedAttribute(_element: Element, attributeName: string) {
const outletName = this.getOutletNameFromOutletAttributeName(attributeName)
if (outletName) {
this.updateSelectorObserverForOutlet(outletName)
}
}
// Outlet management
connectOutlet(outlet: Controller, element: Element, outletName: string) {
if (!this.outletElementsByName.has(outletName, element)) {
this.outletsByName.add(outletName, outlet)
this.outletElementsByName.add(outletName, element)
this.selectorObserverMap.get(outletName)?.pause(() => this.delegate.outletConnected(outlet, element, outletName))
}
}
disconnectOutlet(outlet: Controller, element: Element, outletName: string) {
if (this.outletElementsByName.has(outletName, element)) {
this.outletsByName.delete(outletName, outlet)
this.outletElementsByName.delete(outletName, element)
this.selectorObserverMap
.get(outletName)
?.pause(() => this.delegate.outletDisconnected(outlet, element, outletName))
}
}
disconnectAllOutlets() {
for (const outletName of this.outletElementsByName.keys) {
for (const element of this.outletElementsByName.getValuesForKey(outletName)) {
for (const outlet of this.outletsByName.getValuesForKey(outletName)) {
this.disconnectOutlet(outlet, element, outletName)
}
}
}
}
// Observer management
private updateSelectorObserverForOutlet(outletName: string) {
const observer = this.selectorObserverMap.get(outletName)
if (observer) {
observer.selector = this.selector(outletName)
}
}
private setupSelectorObserverForOutlet(outletName: string) {
const selector = this.selector(outletName)
const selectorObserver = new SelectorObserver(document.body, selector!, this, { outletName })
this.selectorObserverMap.set(outletName, selectorObserver)
selectorObserver.start()
}
private setupAttributeObserverForOutlet(outletName: string) {
const attributeName = this.attributeNameForOutletName(outletName)
const attributeObserver = new AttributeObserver(this.scope.element, attributeName, this)
this.attributeObserverMap.set(outletName, attributeObserver)
attributeObserver.start()
}
// Private
private selector(outletName: string) {
return this.scope.outlets.getSelectorForOutletName(outletName)
}
private attributeNameForOutletName(outletName: string) {
return this.scope.schema.outletAttributeForScope(this.identifier, outletName)
}
private getOutletNameFromOutletAttributeName(attributeName: string) {
return this.outletDefinitions.find((outletName) => this.attributeNameForOutletName(outletName) === attributeName)
}
private get outletDependencies() {
const dependencies = new Multimap<string, string>()
this.router.modules.forEach((module) => {
const constructor = module.definition.controllerConstructor
const outlets = readInheritableStaticArrayValues(constructor, "outlets")
outlets.forEach((outlet) => dependencies.add(outlet, module.identifier))
})
return dependencies
}
private get outletDefinitions() {
return this.outletDependencies.getKeysForValue(this.identifier)
}
private get dependentControllerIdentifiers() {
return this.outletDependencies.getValuesForKey(this.identifier)
}
private get dependentContexts() {
const identifiers = this.dependentControllerIdentifiers
return this.router.contexts.filter((context) => identifiers.includes(context.identifier))
}
private hasOutlet(element: Element, outletName: string) {
return !!this.getOutlet(element, outletName) || !!this.getOutletFromMap(element, outletName)
}
private getOutlet(element: Element, outletName: string) {
return this.application.getControllerForElementAndIdentifier(element, outletName)
}
private getOutletFromMap(element: Element, outletName: string) {
return this.outletsByName.getValuesForKey(outletName).find((outlet) => outlet.element === element)
}
private get scope() {
return this.context.scope
}
private get schema() {
return this.context.schema
}
private get identifier() {
return this.context.identifier
}
private get application() {
return this.context.application
}
private get router() {
return this.application.router
}
}
================================================
FILE: src/core/outlet_properties.ts
================================================
import { Constructor } from "./constructor"
import { Controller } from "./controller"
import { readInheritableStaticArrayValues } from "./inheritable_statics"
import { capitalize, namespaceCamelize } from "./string_helpers"
export function OutletPropertiesBlessing<T>(constructor: Constructor<T>) {
const outlets = readInheritableStaticArrayValues(constructor, "outlets")
return outlets.reduce((properties: any, outletDefinition: any) => {
return Object.assign(properties, propertiesForOutletDefinition(outletDefinition))
}, {} as PropertyDescriptorMap)
}
function getOutletController(controller: Controller, element: Element, identifier: string) {
return controller.application.getControllerForElementAndIdentifier(element, identifier)
}
function getControllerAndEnsureConnectedScope(controller: Controller, element: Element, outletName: string) {
let outletController = getOutletController(controller, element, outletName)
if (outletController) return outletController
controller.application.router.proposeToConnectScopeForElementAndIdentifier(element, outletName)
outletController = getOutletController(controller, element, outletName)
if (outletController) return outletController
}
function propertiesForOutletDefinition(name: string) {
const camelizedName = namespaceCamelize(name)
return {
[`${camelizedName}Outlet`]: {
get(this: Controller) {
const outletElement = this.outlets.find(name)
const selector = this.outlets.getSelectorForOutletName(name)
if (outletElement) {
const outletController = getControllerAndEnsureConnectedScope(this, outletElement, name)
if (outletController) return outletController
throw new Error(
`The provided outlet element is missing an outlet controller "${name}" instance for host controller "${this.identifier}"`
)
}
throw new Error(
`Missing outlet element "${name}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${selector}".`
)
},
},
[`${camelizedName}Outlets`]: {
get(this: Controller) {
const outlets = this.outlets.findAll(name)
if (outlets.length > 0) {
return outlets
.map((outletElement: Element) => {
const outletController = getControllerAndEnsureConnectedScope(this, outletElement, name)
if (outletController) return outletController
console.warn(
`The provided outlet element is missing an outlet controller "${name}" instance for host controller "${this.identifier}"`,
outletElement
)
})
.filter((controller) => controller) as Controller[]
}
return []
},
},
[`${camelizedName}OutletElement`]: {
get(this: Controller) {
const outletElement = this.outlets.find(name)
const selector = this.outlets.getSelectorForOutletName(name)
if (outletElement) {
return outletElement
} else {
throw new Error(
`Missing outlet element "${name}" for host controller "${this.identifier}". Stimulus couldn't find a matching outlet element using selector "${selector}".`
)
}
},
},
[`${camelizedName}OutletElements`]: {
get(this: Controller) {
return this.outlets.findAll(name)
},
},
[`has${capitalize(camelizedName)}Outlet`]: {
get(this: Controller) {
return this.outlets.has(name)
},
},
}
}
================================================
FILE: src/core/outlet_set.ts
================================================
import { Scope } from "./scope"
export class OutletSet {
readonly scope: Scope
readonly controllerElement: Element
constructor(scope: Scope, controllerElement: Element) {
this.scope = scope
this.controllerElement = controllerElement
}
get element() {
return this.scope.element
}
get identifier() {
return this.scope.identifier
}
get schema() {
return this.scope.schema
}
has(outletName: string) {
return this.find(outletName) != null
}
find(...outletNames: string[]) {
return outletNames.reduce(
(outlet, outletName) => outlet || this.findOutlet(outletName),
undefined as Element | undefined
)
}
findAll(...outletNames: string[]) {
return outletNames.reduce(
(outlets, outletName) => [...outlets, ...this.findAllOutlets(outletName)],
[] as Element[]
)
}
gitextract_o806b5yi/ ├── .eslintignore ├── .eslintrc ├── .github/ │ ├── scripts/ │ │ └── publish-dev-build │ └── workflows/ │ ├── build.yml │ ├── dev-builds.yml │ ├── lint.yml │ └── test.yml ├── .gitignore ├── .node-version ├── .npmignore ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── SECURITY.md ├── docs/ │ ├── handbook/ │ │ ├── 00_the_origin_of_stimulus.md │ │ ├── 01_introduction.md │ │ ├── 02_hello_stimulus.md │ │ ├── 03_building_something_real.md │ │ ├── 04_designing_for_resilience.md │ │ ├── 05_managing_state.md │ │ ├── 06_working_with_external_resources.md │ │ └── 07_installing_stimulus.md │ └── reference/ │ ├── actions.md │ ├── controllers.md │ ├── css_classes.md │ ├── lifecycle_callbacks.md │ ├── outlets.md │ ├── targets.md │ ├── using_typescript.md │ └── values.md ├── examples/ │ ├── .babelrc │ ├── controllers/ │ │ ├── clipboard_controller.js │ │ ├── content_loader_controller.js │ │ ├── hello_controller.js │ │ ├── slideshow_controller.js │ │ └── tabs_controller.js │ ├── index.js │ ├── package.json │ ├── public/ │ │ ├── examples.css │ │ └── main.css │ ├── server.js │ ├── views/ │ │ ├── clipboard.ejs │ │ ├── content-loader.ejs │ │ ├── hello.ejs │ │ ├── layout/ │ │ │ ├── head.ejs │ │ │ └── tail.ejs │ │ ├── slideshow.ejs │ │ └── tabs.ejs │ └── webpack.config.js ├── karma.conf.cjs ├── package.json ├── packages/ │ └── stimulus/ │ ├── .gitignore │ ├── .npmignore │ ├── index.d.ts │ ├── index.js │ ├── package.json │ ├── rollup.config.js │ ├── webpack-helpers.d.ts │ └── webpack-helpers.js ├── rollup.config.js ├── src/ │ ├── core/ │ │ ├── action.ts │ │ ├── action_descriptor.ts │ │ ├── action_event.ts │ │ ├── application.ts │ │ ├── binding.ts │ │ ├── binding_observer.ts │ │ ├── blessing.ts │ │ ├── class_map.ts │ │ ├── class_properties.ts │ │ ├── constructor.ts │ │ ├── context.ts │ │ ├── controller.ts │ │ ├── data_map.ts │ │ ├── definition.ts │ │ ├── dispatcher.ts │ │ ├── error_handler.ts │ │ ├── event_listener.ts │ │ ├── guide.ts │ │ ├── index.ts │ │ ├── inheritable_statics.ts │ │ ├── logger.ts │ │ ├── module.ts │ │ ├── outlet_observer.ts │ │ ├── outlet_properties.ts │ │ ├── outlet_set.ts │ │ ├── router.ts │ │ ├── schema.ts │ │ ├── scope.ts │ │ ├── scope_observer.ts │ │ ├── selectors.ts │ │ ├── string_helpers.ts │ │ ├── target_observer.ts │ │ ├── target_properties.ts │ │ ├── target_set.ts │ │ ├── utils.ts │ │ ├── value_observer.ts │ │ └── value_properties.ts │ ├── index.d.ts │ ├── index.js │ ├── index.ts │ ├── multimap/ │ │ ├── index.ts │ │ ├── indexed_multimap.ts │ │ ├── multimap.ts │ │ └── set_operations.ts │ ├── mutation-observers/ │ │ ├── attribute_observer.ts │ │ ├── element_observer.ts │ │ ├── index.ts │ │ ├── selector_observer.ts │ │ ├── string_map_observer.ts │ │ ├── token_list_observer.ts │ │ └── value_list_observer.ts │ └── tests/ │ ├── cases/ │ │ ├── application_test_case.ts │ │ ├── controller_test_case.ts │ │ ├── dom_test_case.ts │ │ ├── index.ts │ │ ├── log_controller_test_case.ts │ │ ├── observer_test_case.ts │ │ └── test_case.ts │ ├── controllers/ │ │ ├── class_controller.ts │ │ ├── default_value_controller.ts │ │ ├── log_controller.ts │ │ ├── outlet_controller.ts │ │ ├── target_controller.ts │ │ └── value_controller.ts │ ├── fixtures/ │ │ └── application_start/ │ │ ├── helpers.ts │ │ ├── index.html │ │ └── index.ts │ ├── index.ts │ └── modules/ │ ├── core/ │ │ ├── action_click_filter_tests.ts │ │ ├── action_keyboard_filter_tests.ts │ │ ├── action_ordering_tests.ts │ │ ├── action_params_case_insensitive_tests.ts │ │ ├── action_params_tests.ts │ │ ├── action_tests.ts │ │ ├── action_timing_tests.ts │ │ ├── application_start_tests.ts │ │ ├── application_tests.ts │ │ ├── class_tests.ts │ │ ├── data_tests.ts │ │ ├── default_value_tests.ts │ │ ├── error_handler_tests.ts │ │ ├── es6_tests.ts │ │ ├── event_options_tests.ts │ │ ├── extending_application_tests.ts │ │ ├── legacy_target_tests.ts │ │ ├── lifecycle_tests.ts │ │ ├── loading_tests.ts │ │ ├── memory_tests.ts │ │ ├── outlet_order_tests.ts │ │ ├── outlet_tests.ts │ │ ├── string_helpers_tests.ts │ │ ├── target_tests.ts │ │ ├── value_properties_tests.ts │ │ └── value_tests.ts │ └── mutation-observers/ │ ├── attribute_observer_tests.ts │ ├── selector_observer_tests.ts │ ├── token_list_observer_tests.ts │ └── value_list_observer_tests.ts ├── tsconfig.json └── tsconfig.test.json
SYMBOL INDEX (935 symbols across 93 files)
FILE: examples/controllers/clipboard_controller.js
method initialize (line 7) | initialize() {
method copy (line 13) | copy() {
FILE: examples/controllers/content_loader_controller.js
method connect (line 7) | connect() {
method itemTargetConnected (line 15) | itemTargetConnected(target) {
method itemTargetDisconnected (line 19) | itemTargetDisconnected(target) {
method disconnect (line 23) | disconnect() {
method load (line 27) | load() {
method startRefreshing (line 35) | startRefreshing() {
method stopRefreshing (line 41) | stopRefreshing() {
FILE: examples/controllers/hello_controller.js
method greet (line 6) | greet() {
method name (line 10) | get name() {
FILE: examples/controllers/slideshow_controller.js
method next (line 8) | next() {
method previous (line 14) | previous() {
method indexValueChanged (line 20) | indexValueChanged() {
method render (line 24) | render() {
method lastIndex (line 30) | get lastIndex() {
FILE: examples/controllers/tabs_controller.js
method next (line 8) | next() {
method previous (line 16) | previous() {
method open (line 24) | open(evt) {
method lastIndex (line 28) | get lastIndex() {
method indexValueChanged (line 32) | indexValueChanged(current, old) {
FILE: src/core/action.ts
class Action (line 9) | class Action {
method forToken (line 20) | static forToken(token: Token, schema: Schema) {
method constructor (line 24) | constructor(element: Element, index: number, descriptor: Partial<Actio...
method toString (line 36) | toString() {
method shouldIgnoreKeyboardEvent (line 42) | shouldIgnoreKeyboardEvent(event: KeyboardEvent): boolean {
method shouldIgnoreMouseEvent (line 65) | shouldIgnoreMouseEvent(event: MouseEvent): boolean {
method params (line 78) | get params() {
method eventTargetName (line 92) | private get eventTargetName() {
method keyMappings (line 96) | private get keyMappings() {
method keyFilterDissatisfied (line 100) | private keyFilterDissatisfied(event: KeyboardEvent | MouseEvent, filte...
function getDefaultEventNameForElement (line 117) | function getDefaultEventNameForElement(element: Element): string | undef...
function error (line 124) | function error(message: string): never {
function typecast (line 128) | function typecast(value: any): any {
FILE: src/core/action_descriptor.ts
type ActionDescriptorFilters (line 3) | type ActionDescriptorFilters = Record<string, ActionDescriptorFilter>
type ActionDescriptorFilter (line 4) | type ActionDescriptorFilter = (options: ActionDescriptorFilterOptions) =...
type ActionDescriptorFilterOptions (line 5) | type ActionDescriptorFilterOptions = {
method stop (line 14) | stop({ event, value }) {
method prevent (line 20) | prevent({ event, value }) {
method self (line 26) | self({ event, value, element }) {
type ActionDescriptor (line 35) | interface ActionDescriptor {
function parseActionDescriptorString (line 47) | function parseActionDescriptorString(descriptorString: string): Partial<...
function parseEventTarget (line 68) | function parseEventTarget(eventTargetName: string): EventTarget | undefi...
function parseEventOptions (line 76) | function parseEventOptions(eventOptions: string): AddEventListenerOptions {
function stringifyEventTarget (line 82) | function stringifyEventTarget(eventTarget: EventTarget) {
FILE: src/core/action_event.ts
type ActionEvent (line 1) | interface ActionEvent extends Event {
FILE: src/core/application.ts
class Application (line 10) | class Application implements ErrorHandler {
method start (line 19) | static start(element?: Element, schema?: Schema): Application {
method constructor (line 25) | constructor(element: Element = document.documentElement, schema: Schem...
method start (line 33) | async start() {
method stop (line 41) | stop() {
method register (line 48) | register(identifier: string, controllerConstructor: ControllerConstruc...
method registerActionOption (line 52) | registerActionOption(name: string, filter: ActionDescriptorFilter) {
method load (line 58) | load(head: Definition | Definition[], ...rest: Definition[]) {
method unload (line 69) | unload(head: string | string[], ...rest: string[]) {
method controllers (line 76) | get controllers(): Controller[] {
method getControllerForElementAndIdentifier (line 80) | getControllerForElementAndIdentifier(element: Element, identifier: str...
method handleError (line 87) | handleError(error: Error, message: string, detail: object) {
method logFormattedMessage (line 101) | private logFormattedMessage(identifier: string, functionName: string, ...
function domReady (line 110) | function domReady() {
FILE: src/core/binding.ts
class Binding (line 6) | class Binding {
method constructor (line 10) | constructor(context: Context, action: Action) {
method index (line 15) | get index(): number {
method eventTarget (line 19) | get eventTarget(): EventTarget {
method eventOptions (line 23) | get eventOptions(): AddEventListenerOptions {
method identifier (line 27) | get identifier(): string {
method handleEvent (line 31) | handleEvent(event: Event) {
method eventName (line 38) | get eventName(): string {
method method (line 42) | get method(): Function {
method applyEventModifiers (line 50) | private applyEventModifiers(event: Event): boolean {
method prepareActionEvent (line 70) | private prepareActionEvent(event: Event): ActionEvent {
method invokeWithEvent (line 74) | private invokeWithEvent(event: ActionEvent) {
method willBeInvokedByEvent (line 86) | private willBeInvokedByEvent(event: Event): boolean {
method controller (line 106) | private get controller(): Controller {
method methodName (line 110) | private get methodName(): string {
method element (line 114) | private get element(): Element {
method scope (line 118) | private get scope(): Scope {
FILE: src/core/binding_observer.ts
type BindingObserverDelegate (line 8) | interface BindingObserverDelegate extends ErrorHandler {
class BindingObserver (line 13) | class BindingObserver implements ValueListObserverDelegate<Action> {
method constructor (line 19) | constructor(context: Context, delegate: BindingObserverDelegate) {
method start (line 25) | start() {
method stop (line 32) | stop() {
method element (line 40) | get element() {
method identifier (line 44) | get identifier() {
method actionAttribute (line 48) | get actionAttribute() {
method schema (line 52) | get schema(): Schema {
method bindings (line 56) | get bindings(): Binding[] {
method connectAction (line 60) | private connectAction(action: Action) {
method disconnectAction (line 66) | private disconnectAction(action: Action) {
method disconnectAllActions (line 74) | private disconnectAllActions() {
method parseValueForToken (line 81) | parseValueForToken(token: Token): Action | undefined {
method elementMatchedValue (line 88) | elementMatchedValue(element: Element, action: Action) {
method elementUnmatchedValue (line 92) | elementUnmatchedValue(element: Element, action: Action) {
FILE: src/core/blessing.ts
type Blessing (line 4) | type Blessing<T> = (constructor: Constructor<T>) => PropertyDescriptorMap
type Blessable (line 6) | interface Blessable<T> extends Constructor<T> {
function bless (line 10) | function bless<T>(constructor: Blessable<T>): Constructor<T> {
function shadow (line 14) | function shadow<T>(constructor: Constructor<T>, properties: PropertyDesc...
function getBlessedProperties (line 21) | function getBlessedProperties<T>(constructor: Constructor<T>) {
function getShadowProperties (line 33) | function getShadowProperties(prototype: any, properties: PropertyDescrip...
function getShadowedDescriptor (line 43) | function getShadowedDescriptor(prototype: any, properties: PropertyDescr...
function extendWithReflect (line 65) | function extendWithReflect<T extends Constructor<any>>(constructor: T): T {
function testReflectExtension (line 78) | function testReflectExtension() {
FILE: src/core/class_map.ts
class ClassMap (line 4) | class ClassMap {
method constructor (line 7) | constructor(scope: Scope) {
method has (line 11) | has(name: string) {
method get (line 15) | get(name: string): string | undefined {
method getAll (line 19) | getAll(name: string) {
method getAttributeName (line 24) | getAttributeName(name: string) {
method getDataKey (line 28) | getDataKey(name: string) {
method data (line 32) | get data() {
FILE: src/core/class_properties.ts
function ClassPropertiesBlessing (line 6) | function ClassPropertiesBlessing<T>(constructor: Constructor<T>) {
function propertiesForClassDefinition (line 13) | function propertiesForClassDefinition(key: string) {
FILE: src/core/constructor.ts
type Constructor (line 1) | type Constructor<T> = new (...args: any[]) => T
FILE: src/core/context.ts
class Context (line 14) | class Context implements ErrorHandler, TargetObserverDelegate, OutletObs...
method constructor (line 23) | constructor(module: Module, scope: Scope) {
method connect (line 40) | connect() {
method refresh (line 54) | refresh() {
method disconnect (line 58) | disconnect() {
method application (line 72) | get application(): Application {
method identifier (line 76) | get identifier(): string {
method schema (line 80) | get schema(): Schema {
method dispatcher (line 84) | get dispatcher(): Dispatcher {
method element (line 88) | get element(): Element {
method parentElement (line 92) | get parentElement(): Element | null {
method handleError (line 98) | handleError(error: Error, message: string, detail: object = {}) {
method targetConnected (line 114) | targetConnected(element: Element, name: string) {
method targetDisconnected (line 118) | targetDisconnected(element: Element, name: string) {
method outletConnected (line 124) | outletConnected(outlet: Controller, element: Element, name: string) {
method outletDisconnected (line 128) | outletDisconnected(outlet: Controller, element: Element, name: string) {
method invokeControllerMethod (line 134) | invokeControllerMethod(methodName: string, ...args: any[]) {
FILE: src/core/controller.ts
type ControllerConstructor (line 9) | type ControllerConstructor = Constructor<Controller>
type DispatchOptions (line 11) | type DispatchOptions = Partial<{
class Controller (line 19) | class Controller<ElementType extends Element = Element> {
method shouldLoad (line 30) | static get shouldLoad() {
method afterLoad (line 34) | static afterLoad(_identifier: string, _application: Application) {
method constructor (line 40) | constructor(context: Context) {
method application (line 44) | get application() {
method scope (line 48) | get scope() {
method element (line 52) | get element() {
method identifier (line 56) | get identifier() {
method targets (line 60) | get targets() {
method outlets (line 64) | get outlets() {
method classes (line 68) | get classes() {
method data (line 72) | get data() {
method initialize (line 76) | initialize() {
method connect (line 80) | connect() {
method disconnect (line 84) | disconnect() {
method dispatch (line 88) | dispatch(
FILE: src/core/data_map.ts
class DataMap (line 4) | class DataMap {
method constructor (line 7) | constructor(scope: Scope) {
method element (line 11) | get element(): Element {
method identifier (line 15) | get identifier(): string {
method get (line 19) | get(key: string): string | null {
method set (line 24) | set(key: string, value: string): string | null {
method has (line 30) | has(key: string): boolean {
method delete (line 35) | delete(key: string): boolean {
method getAttributeNameForKey (line 45) | getAttributeNameForKey(key: string): string {
FILE: src/core/definition.ts
type Definition (line 4) | interface Definition {
function blessDefinition (line 9) | function blessDefinition(definition: Definition): Definition {
FILE: src/core/dispatcher.ts
class Dispatcher (line 6) | class Dispatcher implements BindingObserverDelegate {
method constructor (line 11) | constructor(application: Application) {
method start (line 17) | start() {
method stop (line 24) | stop() {
method eventListeners (line 31) | get eventListeners(): EventListener[] {
method bindingConnected (line 40) | bindingConnected(binding: Binding) {
method bindingDisconnected (line 44) | bindingDisconnected(binding: Binding, clearEventListeners = false) {
method handleError (line 51) | handleError(error: Error, message: string, detail: object = {}) {
method clearEventListenersForBinding (line 55) | private clearEventListenersForBinding(binding: Binding) {
method removeMappedEventListenerFor (line 63) | private removeMappedEventListenerFor(binding: Binding) {
method fetchEventListenerForBinding (line 72) | private fetchEventListenerForBinding(binding: Binding): EventListener {
method fetchEventListener (line 77) | private fetchEventListener(
method createEventListener (line 92) | private createEventListener(
method fetchEventListenerMapForEventTarget (line 104) | private fetchEventListenerMapForEventTarget(eventTarget: EventTarget):...
method cacheKey (line 113) | private cacheKey(eventName: string, eventOptions: any): string {
FILE: src/core/error_handler.ts
type ErrorHandler (line 1) | interface ErrorHandler {
FILE: src/core/event_listener.ts
class EventListener (line 3) | class EventListener implements EventListenerObject {
method constructor (line 9) | constructor(eventTarget: EventTarget, eventName: string, eventOptions:...
method connect (line 16) | connect() {
method disconnect (line 20) | disconnect() {
method bindingConnected (line 26) | bindingConnected(binding: Binding) {
method bindingDisconnected (line 30) | bindingDisconnected(binding: Binding) {
method handleEvent (line 34) | handleEvent(event: Event) {
method hasBindings (line 46) | hasBindings() {
method bindings (line 50) | get bindings(): Binding[] {
function extendEvent (line 59) | function extendEvent(event: Event) {
FILE: src/core/guide.ts
class Guide (line 3) | class Guide {
method constructor (line 7) | constructor(logger: Logger) {
method warn (line 11) | warn(object: any, key: string, message: string) {
FILE: src/core/inheritable_statics.ts
function readInheritableStaticArrayValues (line 3) | function readInheritableStaticArrayValues<T, U = string>(constructor: Co...
function readInheritableStaticObjectPairs (line 13) | function readInheritableStaticObjectPairs<T, U>(constructor: Constructor...
function getAncestorsForConstructor (line 21) | function getAncestorsForConstructor<T>(constructor: Constructor<T>) {
function getOwnStaticArrayValues (line 30) | function getOwnStaticArrayValues<T>(constructor: Constructor<T>, propert...
function getOwnStaticObjectPairs (line 35) | function getOwnStaticObjectPairs<T, U>(constructor: Constructor<T>, prop...
FILE: src/core/logger.ts
type Logger (line 1) | interface Logger {
FILE: src/core/module.ts
class Module (line 7) | class Module {
method constructor (line 13) | constructor(application: Application, definition: Definition) {
method identifier (line 20) | get identifier(): string {
method controllerConstructor (line 24) | get controllerConstructor(): ControllerConstructor {
method contexts (line 28) | get contexts(): Context[] {
method connectContextForScope (line 32) | connectContextForScope(scope: Scope) {
method disconnectContextForScope (line 38) | disconnectContextForScope(scope: Scope) {
method fetchContextForScope (line 46) | private fetchContextForScope(scope: Scope): Context {
FILE: src/core/outlet_observer.ts
type OutletObserverDetails (line 9) | type OutletObserverDetails = { outletName: string }
type OutletObserverDelegate (line 11) | interface OutletObserverDelegate {
class OutletObserver (line 16) | class OutletObserver implements AttributeObserverDelegate, SelectorObser...
method constructor (line 25) | constructor(context: Context, delegate: OutletObserverDelegate) {
method start (line 35) | start() {
method refresh (line 46) | refresh() {
method stop (line 51) | stop() {
method stopSelectorObservers (line 60) | stopSelectorObservers() {
method stopAttributeObservers (line 67) | stopAttributeObservers() {
method selectorMatched (line 76) | selectorMatched(element: Element, _selector: string, { outletName }: O...
method selectorUnmatched (line 84) | selectorUnmatched(element: Element, _selector: string, { outletName }:...
method selectorMatchElement (line 92) | selectorMatchElement(element: Element, { outletName }: OutletObserverD...
method elementMatchedAttribute (line 106) | elementMatchedAttribute(_element: Element, attributeName: string) {
method elementAttributeValueChanged (line 114) | elementAttributeValueChanged(_element: Element, attributeName: string) {
method elementUnmatchedAttribute (line 122) | elementUnmatchedAttribute(_element: Element, attributeName: string) {
method connectOutlet (line 132) | connectOutlet(outlet: Controller, element: Element, outletName: string) {
method disconnectOutlet (line 140) | disconnectOutlet(outlet: Controller, element: Element, outletName: str...
method disconnectAllOutlets (line 150) | disconnectAllOutlets() {
method updateSelectorObserverForOutlet (line 162) | private updateSelectorObserverForOutlet(outletName: string) {
method setupSelectorObserverForOutlet (line 170) | private setupSelectorObserverForOutlet(outletName: string) {
method setupAttributeObserverForOutlet (line 179) | private setupAttributeObserverForOutlet(outletName: string) {
method selector (line 190) | private selector(outletName: string) {
method attributeNameForOutletName (line 194) | private attributeNameForOutletName(outletName: string) {
method getOutletNameFromOutletAttributeName (line 198) | private getOutletNameFromOutletAttributeName(attributeName: string) {
method outletDependencies (line 202) | private get outletDependencies() {
method outletDefinitions (line 215) | private get outletDefinitions() {
method dependentControllerIdentifiers (line 219) | private get dependentControllerIdentifiers() {
method dependentContexts (line 223) | private get dependentContexts() {
method hasOutlet (line 228) | private hasOutlet(element: Element, outletName: string) {
method getOutlet (line 232) | private getOutlet(element: Element, outletName: string) {
method getOutletFromMap (line 236) | private getOutletFromMap(element: Element, outletName: string) {
method scope (line 240) | private get scope() {
method schema (line 244) | private get schema() {
method identifier (line 248) | private get identifier() {
method application (line 252) | private get application() {
method router (line 256) | private get router() {
FILE: src/core/outlet_properties.ts
function OutletPropertiesBlessing (line 6) | function OutletPropertiesBlessing<T>(constructor: Constructor<T>) {
function getOutletController (line 13) | function getOutletController(controller: Controller, element: Element, i...
function getControllerAndEnsureConnectedScope (line 17) | function getControllerAndEnsureConnectedScope(controller: Controller, el...
function propertiesForOutletDefinition (line 27) | function propertiesForOutletDefinition(name: string) {
FILE: src/core/outlet_set.ts
class OutletSet (line 3) | class OutletSet {
method constructor (line 7) | constructor(scope: Scope, controllerElement: Element) {
method element (line 12) | get element() {
method identifier (line 16) | get identifier() {
method schema (line 20) | get schema() {
method has (line 24) | has(outletName: string) {
method find (line 28) | find(...outletNames: string[]) {
method findAll (line 35) | findAll(...outletNames: string[]) {
method getSelectorForOutletName (line 42) | getSelectorForOutletName(outletName: string) {
method findOutlet (line 47) | private findOutlet(outletName: string) {
method findAllOutlets (line 52) | private findAllOutlets(outletName: string) {
method findElement (line 57) | private findElement(selector: string, outletName: string): Element | u...
method findAllElements (line 62) | private findAllElements(selector: string, outletName: string): Element...
method matchesElement (line 67) | private matchesElement(element: Element, selector: string, outletName:...
FILE: src/core/router.ts
class Router (line 9) | class Router implements ScopeObserverDelegate {
method constructor (line 15) | constructor(application: Application) {
method element (line 22) | get element() {
method schema (line 26) | get schema() {
method logger (line 30) | get logger() {
method controllerAttribute (line 34) | get controllerAttribute(): string {
method modules (line 38) | get modules() {
method contexts (line 42) | get contexts() {
method start (line 46) | start() {
method stop (line 50) | stop() {
method loadDefinition (line 54) | loadDefinition(definition: Definition) {
method unloadIdentifier (line 64) | unloadIdentifier(identifier: string) {
method getContextForElementAndIdentifier (line 71) | getContextForElementAndIdentifier(element: Element, identifier: string) {
method proposeToConnectScopeForElementAndIdentifier (line 78) | proposeToConnectScopeForElementAndIdentifier(element: Element, identif...
method handleError (line 90) | handleError(error: Error, message: string, detail: any) {
method createScopeForElementAndIdentifier (line 96) | createScopeForElementAndIdentifier(element: Element, identifier: strin...
method scopeConnected (line 100) | scopeConnected(scope: Scope) {
method scopeDisconnected (line 108) | scopeDisconnected(scope: Scope) {
method connectModule (line 118) | private connectModule(module: Module) {
method disconnectModule (line 124) | private disconnectModule(module: Module) {
FILE: src/core/schema.ts
type Schema (line 1) | interface Schema {
function objectFromEntries (line 36) | function objectFromEntries(array: [string, any][]): object {
FILE: src/core/scope.ts
class Scope (line 10) | class Scope {
method constructor (line 20) | constructor(schema: Schema, element: Element, identifier: string, logg...
method findElement (line 28) | findElement(selector: string): Element | undefined {
method findAllElements (line 32) | findAllElements(selector: string): Element[] {
method queryElements (line 43) | queryElements(selector: string): Element[] {
method controllerSelector (line 47) | private get controllerSelector(): string {
method isDocumentScope (line 51) | private get isDocumentScope() {
method documentScope (line 55) | private get documentScope(): Scope {
FILE: src/core/scope_observer.ts
type ScopeObserverDelegate (line 6) | interface ScopeObserverDelegate extends ErrorHandler {
class ScopeObserver (line 12) | class ScopeObserver implements ValueListObserverDelegate<Scope> {
method constructor (line 20) | constructor(element: Element, schema: Schema, delegate: ScopeObserverD...
method start (line 29) | start() {
method stop (line 33) | stop() {
method controllerAttribute (line 37) | get controllerAttribute() {
method parseValueForToken (line 43) | parseValueForToken(token: Token): Scope | undefined {
method parseValueForElementAndIdentifier (line 48) | parseValueForElementAndIdentifier(element: Element, identifier: string...
method elementMatchedValue (line 60) | elementMatchedValue(element: Element, value: Scope) {
method elementUnmatchedValue (line 68) | elementUnmatchedValue(element: Element, value: Scope) {
method fetchScopesByIdentifierForElement (line 78) | private fetchScopesByIdentifierForElement(element: Element) {
FILE: src/core/selectors.ts
function attributeValueContainsToken (line 1) | function attributeValueContainsToken(attributeName: string, token: strin...
FILE: src/core/string_helpers.ts
function camelize (line 1) | function camelize(value: string) {
function namespaceCamelize (line 5) | function namespaceCamelize(value: string) {
function capitalize (line 9) | function capitalize(value: string) {
function dasherize (line 13) | function dasherize(value: string) {
function tokenize (line 17) | function tokenize(value: string) {
FILE: src/core/target_observer.ts
type TargetObserverDelegate (line 5) | interface TargetObserverDelegate {
class TargetObserver (line 10) | class TargetObserver implements TokenListObserverDelegate {
method constructor (line 16) | constructor(context: Context, delegate: TargetObserverDelegate) {
method start (line 22) | start() {
method stop (line 29) | stop() {
method tokenMatched (line 39) | tokenMatched({ element, content: name }: Token) {
method tokenUnmatched (line 45) | tokenUnmatched({ element, content: name }: Token) {
method connectTarget (line 51) | connectTarget(element: Element, name: string) {
method disconnectTarget (line 58) | disconnectTarget(element: Element, name: string) {
method disconnectAllTargets (line 65) | disconnectAllTargets() {
method attributeName (line 75) | private get attributeName() {
method element (line 79) | private get element() {
method scope (line 83) | private get scope() {
FILE: src/core/target_properties.ts
function TargetPropertiesBlessing (line 6) | function TargetPropertiesBlessing<T>(constructor: Constructor<T>) {
function propertiesForTargetDefinition (line 13) | function propertiesForTargetDefinition(name: string) {
FILE: src/core/target_set.ts
class TargetSet (line 4) | class TargetSet {
method constructor (line 7) | constructor(scope: Scope) {
method element (line 11) | get element() {
method identifier (line 15) | get identifier() {
method schema (line 19) | get schema() {
method has (line 23) | has(targetName: string) {
method find (line 27) | find(...targetNames: string[]) {
method findAll (line 34) | findAll(...targetNames: string[]) {
method findTarget (line 45) | private findTarget(targetName: string) {
method findAllTargets (line 50) | private findAllTargets(targetName: string) {
method getSelectorForTargetName (line 55) | private getSelectorForTargetName(targetName: string) {
method findLegacyTarget (line 60) | private findLegacyTarget(targetName: string) {
method findAllLegacyTargets (line 65) | private findAllLegacyTargets(targetName: string) {
method getLegacySelectorForTargetName (line 70) | private getLegacySelectorForTargetName(targetName: string) {
method deprecate (line 75) | private deprecate<T>(element: T, targetName: string) {
method guide (line 90) | private get guide() {
FILE: src/core/utils.ts
function isSomething (line 1) | function isSomething(object: any): boolean {
function hasProperty (line 5) | function hasProperty(object: any, property: string): boolean {
FILE: src/core/value_observer.ts
class ValueObserver (line 6) | class ValueObserver implements StringMapObserverDelegate {
method constructor (line 12) | constructor(context: Context, receiver: any) {
method start (line 19) | start() {
method stop (line 24) | stop() {
method element (line 28) | get element() {
method controller (line 32) | get controller() {
method getStringMapKeyForAttribute (line 38) | getStringMapKeyForAttribute(attributeName: string) {
method stringMapKeyAdded (line 44) | stringMapKeyAdded(key: string, attributeName: string) {
method stringMapValueChanged (line 52) | stringMapValueChanged(value: string, name: string, oldValue: string) {
method stringMapKeyRemoved (line 64) | stringMapKeyRemoved(key: string, attributeName: string, oldValue: stri...
method invokeChangedCallbacksForDefaultValues (line 74) | private invokeChangedCallbacksForDefaultValues() {
method invokeChangedCallback (line 82) | private invokeChangedCallback(name: string, rawValue: string, rawOldVa...
method valueDescriptors (line 108) | private get valueDescriptors() {
method valueDescriptorNameMap (line 113) | private get valueDescriptorNameMap() {
method hasValue (line 124) | private hasValue(attributeName: string) {
FILE: src/core/value_properties.ts
function ValuePropertiesBlessing (line 7) | function ValuePropertiesBlessing<T>(constructor: Constructor<T>) {
function propertiesForValueDefinitionPair (line 26) | function propertiesForValueDefinitionPair<T>(
type ValueDescriptor (line 61) | type ValueDescriptor = {
type ValueDescriptorMap (line 71) | type ValueDescriptorMap = { [attributeName: string]: ValueDescriptor }
type ValueDefinitionMap (line 73) | type ValueDefinitionMap = { [token: string]: ValueTypeDefinition }
type ValueDefinitionPair (line 75) | type ValueDefinitionPair = [string, ValueTypeDefinition]
type ValueTypeConstant (line 77) | type ValueTypeConstant = typeof Array | typeof Boolean | typeof Number |...
type ValueTypeDefault (line 79) | type ValueTypeDefault = Array<any> | boolean | number | Object | string
type ValueTypeObject (line 81) | type ValueTypeObject = Partial<{ type: ValueTypeConstant; default: Value...
type ValueTypeDefinition (line 83) | type ValueTypeDefinition = ValueTypeConstant | ValueTypeDefault | ValueT...
type ValueType (line 85) | type ValueType = "array" | "boolean" | "number" | "object" | "string"
function parseValueDefinitionPair (line 87) | function parseValueDefinitionPair([token, typeDefinition]: ValueDefiniti...
function parseValueTypeConstant (line 95) | function parseValueTypeConstant(constant?: ValueTypeConstant) {
function parseValueTypeDefault (line 110) | function parseValueTypeDefault(defaultValue?: ValueTypeDefault) {
type ValueTypeObjectPayload (line 124) | type ValueTypeObjectPayload = {
function parseValueTypeObject (line 130) | function parseValueTypeObject(payload: ValueTypeObjectPayload) {
type ValueTypeDefinitionPayload (line 157) | type ValueTypeDefinitionPayload = {
function parseValueTypeDefinition (line 163) | function parseValueTypeDefinition(payload: ValueTypeDefinitionPayload): ...
function defaultValueForDefinition (line 181) | function defaultValueForDefinition(typeDefinition: ValueTypeDefinition):...
function valueDescriptorForTokenAndTypeDefinition (line 201) | function valueDescriptorForTokenAndTypeDefinition(payload: ValueTypeDefi...
method array (line 222) | get array() {
method object (line 227) | get object() {
type Reader (line 233) | type Reader = (value: string) => any
method array (line 236) | array(value: string): any[] {
method boolean (line 246) | boolean(value: string): boolean {
method number (line 250) | number(value: string): number {
method object (line 254) | object(value: string): object {
method string (line 264) | string(value: string): string {
type Writer (line 269) | type Writer = (value: any) => string
function writeJSON (line 277) | function writeJSON(value: any) {
function writeString (line 281) | function writeString(value: any) {
FILE: src/multimap/indexed_multimap.ts
class IndexedMultimap (line 4) | class IndexedMultimap<K, V> extends Multimap<K, V> {
method constructor (line 7) | constructor() {
method values (line 12) | get values(): V[] {
method add (line 16) | add(key: K, value: V) {
method delete (line 21) | delete(key: K, value: V) {
method hasValue (line 26) | hasValue(value: V): boolean {
method getKeysForValue (line 30) | getKeysForValue(value: V): K[] {
FILE: src/multimap/multimap.ts
class Multimap (line 3) | class Multimap<K, V> {
method constructor (line 6) | constructor() {
method keys (line 10) | get keys() {
method values (line 14) | get values(): V[] {
method size (line 19) | get size(): number {
method add (line 24) | add(key: K, value: V) {
method delete (line 28) | delete(key: K, value: V) {
method has (line 32) | has(key: K, value: V): boolean {
method hasKey (line 37) | hasKey(key: K): boolean {
method hasValue (line 41) | hasValue(value: V): boolean {
method getValuesForKey (line 46) | getValuesForKey(key: K): V[] {
method getKeysForValue (line 51) | getKeysForValue(value: V): K[] {
FILE: src/multimap/set_operations.ts
function add (line 1) | function add<K, V>(map: Map<K, Set<V>>, key: K, value: V) {
function del (line 5) | function del<K, V>(map: Map<K, Set<V>>, key: K, value: V) {
function fetch (line 10) | function fetch<K, V>(map: Map<K, Set<V>>, key: K): Set<V> {
function prune (line 19) | function prune<K, V>(map: Map<K, Set<V>>, key: K) {
FILE: src/mutation-observers/attribute_observer.ts
type AttributeObserverDelegate (line 3) | interface AttributeObserverDelegate {
class AttributeObserver (line 9) | class AttributeObserver implements ElementObserverDelegate {
method constructor (line 15) | constructor(element: Element, attributeName: string, delegate: Attribu...
method element (line 22) | get element(): Element {
method selector (line 26) | get selector(): string {
method start (line 30) | start() {
method pause (line 34) | pause(callback: () => void) {
method stop (line 38) | stop() {
method refresh (line 42) | refresh() {
method started (line 46) | get started(): boolean {
method matchElement (line 52) | matchElement(element: Element): boolean {
method matchElementsInTree (line 56) | matchElementsInTree(tree: Element): Element[] {
method elementMatched (line 62) | elementMatched(element: Element) {
method elementUnmatched (line 68) | elementUnmatched(element: Element) {
method elementAttributeChanged (line 74) | elementAttributeChanged(element: Element, attributeName: string) {
FILE: src/mutation-observers/element_observer.ts
type ElementObserverDelegate (line 1) | interface ElementObserverDelegate {
class ElementObserver (line 10) | class ElementObserver {
method constructor (line 19) | constructor(element: Element, delegate: ElementObserverDelegate) {
method start (line 28) | start() {
method pause (line 36) | pause(callback: () => void) {
method stop (line 50) | stop() {
method refresh (line 58) | refresh() {
method processMutations (line 76) | private processMutations(mutations: MutationRecord[]) {
method processMutation (line 84) | private processMutation(mutation: MutationRecord) {
method processAttributeChange (line 93) | private processAttributeChange(element: Element, attributeName: string) {
method processRemovedNodes (line 105) | private processRemovedNodes(nodes: NodeList) {
method processAddedNodes (line 114) | private processAddedNodes(nodes: NodeList) {
method matchElement (line 125) | private matchElement(element: Element): boolean {
method matchElementsInTree (line 129) | private matchElementsInTree(tree: Element = this.element): Element[] {
method processTree (line 133) | private processTree(tree: Element, processor: (element: Element) => vo...
method elementFromNode (line 139) | private elementFromNode(node: Node): Element | undefined {
method elementIsActive (line 145) | private elementIsActive(element: Element): boolean {
method addElement (line 155) | private addElement(element: Element) {
method removeElement (line 166) | private removeElement(element: Element) {
FILE: src/mutation-observers/selector_observer.ts
type SelectorObserverDelegate (line 4) | interface SelectorObserverDelegate {
class SelectorObserver (line 10) | class SelectorObserver implements ElementObserverDelegate {
method constructor (line 17) | constructor(element: Element, selector: string, delegate: SelectorObse...
method started (line 25) | get started(): boolean {
method selector (line 29) | get selector() {
method selector (line 33) | set selector(selector: string | null) {
method start (line 38) | start() {
method pause (line 42) | pause(callback: () => void) {
method stop (line 46) | stop() {
method refresh (line 50) | refresh() {
method element (line 54) | get element(): Element {
method matchElement (line 60) | matchElement(element: Element): boolean {
method matchElementsInTree (line 76) | matchElementsInTree(tree: Element): Element[] {
method elementMatched (line 88) | elementMatched(element: Element) {
method elementUnmatched (line 96) | elementUnmatched(element: Element) {
method elementAttributeChanged (line 104) | elementAttributeChanged(element: Element, _attributeName: string) {
method selectorMatched (line 121) | private selectorMatched(element: Element, selector: string) {
method selectorUnmatched (line 126) | private selectorUnmatched(element: Element, selector: string) {
FILE: src/mutation-observers/string_map_observer.ts
type StringMapObserverDelegate (line 1) | interface StringMapObserverDelegate {
class StringMapObserver (line 8) | class StringMapObserver {
method constructor (line 15) | constructor(element: Element, delegate: StringMapObserverDelegate) {
method start (line 23) | start() {
method stop (line 31) | stop() {
method refresh (line 39) | refresh() {
method processMutations (line 49) | private processMutations(mutations: MutationRecord[]) {
method processMutation (line 57) | private processMutation(mutation: MutationRecord) {
method refreshAttribute (line 66) | private refreshAttribute(attributeName: string, oldValue: string | nul...
method stringMapKeyAdded (line 88) | private stringMapKeyAdded(key: string, attributeName: string) {
method stringMapValueChanged (line 94) | private stringMapValueChanged(value: string | null, key: string, oldVa...
method stringMapKeyRemoved (line 100) | private stringMapKeyRemoved(key: string, attributeName: string, oldVal...
method knownAttributeNames (line 106) | private get knownAttributeNames() {
method currentAttributeNames (line 110) | private get currentAttributeNames() {
method recordedAttributeNames (line 114) | private get recordedAttributeNames() {
FILE: src/mutation-observers/token_list_observer.ts
type Token (line 4) | interface Token {
type TokenListObserverDelegate (line 11) | interface TokenListObserverDelegate {
class TokenListObserver (line 16) | class TokenListObserver implements AttributeObserverDelegate {
method constructor (line 21) | constructor(element: Element, attributeName: string, delegate: TokenLi...
method started (line 27) | get started(): boolean {
method start (line 31) | start() {
method pause (line 35) | pause(callback: () => void) {
method stop (line 39) | stop() {
method refresh (line 43) | refresh() {
method element (line 47) | get element(): Element {
method attributeName (line 51) | get attributeName(): string {
method elementMatchedAttribute (line 57) | elementMatchedAttribute(element: Element) {
method elementAttributeValueChanged (line 61) | elementAttributeValueChanged(element: Element) {
method elementUnmatchedAttribute (line 67) | elementUnmatchedAttribute(element: Element) {
method tokensMatched (line 71) | private tokensMatched(tokens: Token[]) {
method tokensUnmatched (line 75) | private tokensUnmatched(tokens: Token[]) {
method tokenMatched (line 79) | private tokenMatched(token: Token) {
method tokenUnmatched (line 84) | private tokenUnmatched(token: Token) {
method refreshTokensForElement (line 89) | private refreshTokensForElement(element: Element): [Token[], Token[]] {
method readTokensForElement (line 103) | private readTokensForElement(element: Element): Token[] {
function parseTokenString (line 110) | function parseTokenString(tokenString: string, element: Element, attribu...
function zip (line 118) | function zip<L, R>(left: L[], right: R[]): [L | undefined, R | undefined...
function tokensAreEqual (line 123) | function tokensAreEqual(left?: Token, right?: Token) {
FILE: src/mutation-observers/value_list_observer.ts
type ValueListObserverDelegate (line 3) | interface ValueListObserverDelegate<T> {
type ParseResult (line 9) | interface ParseResult<T> {
class ValueListObserver (line 14) | class ValueListObserver<T> implements TokenListObserverDelegate {
method constructor (line 20) | constructor(element: Element, attributeName: string, delegate: ValueLi...
method started (line 27) | get started(): boolean {
method start (line 31) | start() {
method stop (line 35) | stop() {
method refresh (line 39) | refresh() {
method element (line 43) | get element(): Element {
method attributeName (line 47) | get attributeName(): string {
method tokenMatched (line 51) | tokenMatched(token: Token) {
method tokenUnmatched (line 60) | tokenUnmatched(token: Token) {
method fetchParseResultForToken (line 69) | private fetchParseResultForToken(token: Token) {
method fetchValuesByTokenForElement (line 78) | private fetchValuesByTokenForElement(element: Element) {
method parseToken (line 87) | private parseToken(token: Token): ParseResult<T> {
FILE: src/tests/cases/application_test_case.ts
class TestApplication (line 5) | class TestApplication extends Application {
method handleError (line 6) | handleError(error: Error, _message: string, _detail: object) {
class ApplicationTestCase (line 11) | class ApplicationTestCase extends DOMTestCase {
method runTest (line 15) | async runTest(testName: string) {
method setupApplication (line 26) | setupApplication() {
FILE: src/tests/cases/controller_test_case.ts
class ControllerTests (line 5) | class ControllerTests<T extends Controller> extends ApplicationTestCase {
method setupApplication (line 10) | setupApplication() {
method controller (line 16) | get controller(): T {
method identifiers (line 25) | get identifiers(): string[] {
method controllers (line 33) | get controllers(): T[] {
function ControllerTestCase (line 40) | function ControllerTestCase<T extends Controller>(
FILE: src/tests/cases/dom_test_case.ts
type TriggerEventOptions (line 3) | interface TriggerEventOptions {
class DOMTestCase (line 13) | class DOMTestCase extends TestCase {
method runTest (line 17) | async runTest(testName: string) {
method renderFixture (line 22) | async renderFixture(fixtureHTML = this.fixtureHTML) {
method fixtureElement (line 27) | get fixtureElement(): Element {
method triggerEvent (line 36) | async triggerEvent(selectorOrTarget: string | EventTarget, type: strin...
method triggerMouseEvent (line 54) | async triggerMouseEvent(selectorOrTarget: string | EventTarget, type: ...
method triggerKeyboardEvent (line 63) | async triggerKeyboardEvent(selectorOrTarget: string | EventTarget, typ...
method setAttribute (line 72) | async setAttribute(selectorOrElement: string | Element, name: string, ...
method removeAttribute (line 79) | async removeAttribute(selectorOrElement: string | Element, name: strin...
method appendChild (line 86) | async appendChild<T extends Node>(selectorOrElement: T | string, child...
method remove (line 93) | async remove(selectorOrElement: Element | string) {
method findElement (line 100) | findElement(selector: string) {
method findElements (line 109) | findElements(...selectors: string[]) {
method nextFrame (line 113) | get nextFrame(): Promise<any> {
FILE: src/tests/cases/log_controller_test_case.ts
class LogControllerTestCase (line 5) | class LogControllerTestCase extends ControllerTestCase(LogController) {
method setup (line 8) | async setup() {
method assertActions (line 13) | assertActions(...actions: any[]) {
method assertNoActions (line 24) | assertNoActions() {
method actionLog (line 28) | get actionLog(): ActionLogEntry[] {
function slice (line 33) | function slice(object: any, keys: string[]): any {
function deepEqual (line 37) | function deepEqual(obj1: any, obj2: any): boolean {
FILE: src/tests/cases/observer_test_case.ts
type Observer (line 3) | interface Observer {
class ObserverTestCase (line 8) | class ObserverTestCase extends DOMTestCase {
method setup (line 13) | async setup() {
method teardown (line 19) | async teardown() {
method testCalls (line 23) | get testCalls() {
method recordCall (line 27) | recordCall(methodName: string, ...args: any[]) {
FILE: src/tests/cases/test_case.ts
class TestCase (line 1) | class TestCase {
method defineModule (line 4) | static defineModule(moduleName: string = this.name, qUnit: QUnit = QUn...
method getTest (line 15) | static getTest(testName: string) {
method runTest (line 19) | static runTest(testName: string, assert: Assert) {
method shouldSkipTest (line 24) | static shouldSkipTest(_testName: string): boolean {
method manifest (line 28) | static get manifest() {
method testNames (line 32) | static get testNames(): string[] {
method testPropertyNames (line 36) | static get testPropertyNames(): string[] {
method constructor (line 40) | constructor(assert: Assert) {
method runTest (line 44) | async runTest(testName: string) {
method runTestBody (line 53) | async runTestBody(testName: string) {
method setup (line 62) | async setup() {
method teardown (line 66) | async teardown() {
FILE: src/tests/controllers/class_controller.ts
class BaseClassController (line 3) | class BaseClassController extends Controller {
class ClassController (line 11) | class ClassController extends BaseClassController {
FILE: src/tests/controllers/default_value_controller.ts
class DefaultValueController (line 4) | class DefaultValueController extends Controller {
method initialize (line 71) | initialize() {
method connect (line 75) | connect() {
method defaultBooleanValueChanged (line 79) | defaultBooleanValueChanged() {
FILE: src/tests/controllers/log_controller.ts
type ActionLogEntry (line 4) | type ActionLogEntry = {
class LogController (line 15) | class LogController extends Controller {
method initialize (line 21) | initialize() {
method connect (line 25) | connect() {
method disconnect (line 29) | disconnect() {
method log (line 33) | log(event: ActionEvent) {
method log2 (line 37) | log2(event: ActionEvent) {
method log3 (line 41) | log3(event: ActionEvent) {
method logPassive (line 45) | logPassive(event: ActionEvent) {
method stop (line 54) | stop(event: ActionEvent) {
method actionLog (line 59) | get actionLog() {
method recordAction (line 63) | private recordAction(name: string, event: ActionEvent, passive?: boole...
FILE: src/tests/controllers/outlet_controller.ts
class BaseOutletController (line 3) | class BaseOutletController extends Controller {
class OutletController (line 13) | class OutletController extends BaseOutletController {
method connect (line 56) | connect() {
method alphaOutletConnected (line 60) | alphaOutletConnected(_outlet: Controller, element: Element) {
method alphaOutletDisconnected (line 65) | alphaOutletDisconnected(_outlet: Controller, element: Element) {
method betaOutletConnected (line 70) | betaOutletConnected(_outlet: Controller, element: Element) {
method betaOutletDisconnected (line 75) | betaOutletDisconnected(_outlet: Controller, element: Element) {
method gammaOutletConnected (line 80) | gammaOutletConnected(_outlet: Controller, element: Element) {
method namespacedEpsilonOutletConnected (line 85) | namespacedEpsilonOutletConnected(_outlet: Controller, element: Element) {
method namespacedEpsilonOutletDisconnected (line 90) | namespacedEpsilonOutletDisconnected(_outlet: Controller, element: Elem...
FILE: src/tests/controllers/target_controller.ts
class BaseTargetController (line 3) | class BaseTargetController extends Controller {
class TargetController (line 11) | class TargetController extends BaseTargetController {
method inputTargetConnected (line 39) | inputTargetConnected(element: Element) {
method inputTargetDisconnected (line 44) | inputTargetDisconnected(element: Element) {
method recursiveTargetConnected (line 49) | recursiveTargetConnected(element: Element) {
method recursiveTargetDisconnected (line 56) | recursiveTargetDisconnected(_element: Element) {
FILE: src/tests/controllers/value_controller.ts
class BaseValueController (line 4) | class BaseValueController extends Controller {
class ValueController (line 16) | class ValueController extends BaseValueController {
method numericValueChanged (line 33) | numericValueChanged(value: number, oldValue: any) {
method missingStringValueChanged (line 40) | missingStringValueChanged(value: string, oldValue: any) {
method optionsValueChanged (line 47) | optionsValueChanged(value: Object, oldValue: any) {
FILE: src/tests/fixtures/application_start/helpers.ts
function startApplication (line 3) | function startApplication() {
FILE: src/tests/modules/core/action_click_filter_tests.ts
class ActionClickFilterTests (line 3) | class ActionClickFilterTests extends LogControllerTestCase {
method "test ignoring clicks with unmatched modifier" (line 12) | async "test ignoring clicks with unmatched modifier"() {
FILE: src/tests/modules/core/action_keyboard_filter_tests.ts
class ActionKeyboardFilterTests (line 8) | class ActionKeyboardFilterTests extends LogControllerTestCase {
method "test ignore event handlers associated with modifiers other than Enter" (line 28) | async "test ignore event handlers associated with modifiers other than...
method "test ignore event handlers associated with modifiers other than Space" (line 38) | async "test ignore event handlers associated with modifiers other than...
method "test ignore event handlers associated with modifiers other than Tab" (line 48) | async "test ignore event handlers associated with modifiers other than...
method "test ignore event handlers associated with modifiers other than Escape" (line 58) | async "test ignore event handlers associated with modifiers other than...
method "test ignore event handlers associated with modifiers other than ArrowUp" (line 68) | async "test ignore event handlers associated with modifiers other than...
method "test ignore event handlers associated with modifiers other than ArrowDown" (line 78) | async "test ignore event handlers associated with modifiers other than...
method "test ignore event handlers associated with modifiers other than ArrowLeft" (line 88) | async "test ignore event handlers associated with modifiers other than...
method "test ignore event handlers associated with modifiers other than ArrowRight" (line 98) | async "test ignore event handlers associated with modifiers other than...
method "test ignore event handlers associated with modifiers other than Home" (line 108) | async "test ignore event handlers associated with modifiers other than...
method "test ignore event handlers associated with modifiers other than End" (line 118) | async "test ignore event handlers associated with modifiers other than...
method "test keyup" (line 128) | async "test keyup"() {
method "test global event" (line 138) | async "test global event"() {
method "test custom keymapping: a" (line 145) | async "test custom keymapping: a"() {
method "test custom keymapping: b" (line 152) | async "test custom keymapping: b"() {
method "test custom keymapping: unknown c" (line 159) | async "test custom keymapping: unknown c"() {
method "test ignore event handlers associated with modifiers other than shift+a" (line 166) | async "test ignore event handlers associated with modifiers other than...
method "test ignore event handlers associated with modifiers other than a" (line 173) | async "test ignore event handlers associated with modifiers other than...
method "test ignore event handlers associated with modifiers other than ctrol+shift+a" (line 180) | async "test ignore event handlers associated with modifiers other than...
method "test ignore filter syntax when not a keyboard event" (line 187) | async "test ignore filter syntax when not a keyboard event"() {
method "test ignore filter syntax when not a keyboard event (case2)" (line 194) | async "test ignore filter syntax when not a keyboard event (case2)"() {
FILE: src/tests/modules/core/action_ordering_tests.ts
class ActionOrderingTests (line 3) | class ActionOrderingTests extends LogControllerTestCase {
method "test adding an action to the right" (line 11) | async "test adding an action to the right"() {
method "test adding an action to the left" (line 24) | async "test adding an action to the left"() {
method "test removing an action from the right" (line 37) | async "test removing an action from the right"() {
method "test removing an action from the left" (line 49) | async "test removing an action from the left"() {
method "test replacing an action on the left" (line 61) | async "test replacing an action on the left"() {
method "test stopping an action" (line 76) | async "test stopping an action"() {
method "test disconnecting a controller disconnects its actions" (line 87) | async "test disconnecting a controller disconnects its actions"() {
method controllerValue (line 98) | set controllerValue(value: string) {
method actionValue (line 102) | set actionValue(value: string) {
method element (line 106) | get element() {
method buttonElement (line 110) | get buttonElement() {
FILE: src/tests/modules/core/action_params_case_insensitive_tests.ts
class ActionParamsCaseInsensitiveTests (line 3) | class ActionParamsCaseInsensitiveTests extends ActionParamsTests {
method "test clicking on the element does return its params" (line 32) | async "test clicking on the element does return its params"() {
method "test global event return element params where the action is defined" (line 40) | async "test global event return element params where the action is def...
method "test passing params to namespaced controller" (line 48) | async "test passing params to namespaced controller"() {
FILE: src/tests/modules/core/action_params_tests.ts
class ActionParamsTests (line 3) | class ActionParamsTests extends LogControllerTestCase {
method "test clicking on the element does return its params" (line 32) | async "test clicking on the element does return its params"() {
method "test global event return element params where the action is defined" (line 40) | async "test global event return element params where the action is def...
method "test passing params to namespaced controller" (line 48) | async "test passing params to namespaced controller"() {
method "test updating manually the params values" (line 56) | async "test updating manually the params values"() {
method "test clicking on a nested element does return the params of the actionable element" (line 84) | async "test clicking on a nested element does return the params of the...
method actionValue (line 92) | set actionValue(value: string) {
method element (line 96) | get element() {
method buttonElement (line 100) | get buttonElement() {
method nestedElement (line 104) | get nestedElement() {
FILE: src/tests/modules/core/action_tests.ts
class ActionTests (line 3) | class ActionTests extends LogControllerTestCase {
method "test default event" (line 19) | async "test default event"() {
method "test bubbling events" (line 24) | async "test bubbling events"() {
method "test non-bubbling events" (line 29) | async "test non-bubbling events"() {
method "test nested actions" (line 36) | async "test nested actions"() {
method "test global actions" (line 43) | async "test global actions"() {
method "test nested global actions" (line 48) | async "test nested global actions"() {
method "test multiple actions" (line 54) | async "test multiple actions"() {
method "test actions on svg elements" (line 64) | async "test actions on svg elements"() {
FILE: src/tests/modules/core/action_timing_tests.ts
class ActionTimingController (line 4) | class ActionTimingController extends Controller {
method connect (line 9) | connect() {
method record (line 13) | record(event: Event) {
class ActionTimingTests (line 18) | class ActionTimingTests extends ControllerTestCase(ActionTimingControlle...
method "test triggering an action on connect" (line 27) | async "test triggering an action on connect"() {
FILE: src/tests/modules/core/application_start_tests.ts
class ApplicationStartTests (line 3) | class ApplicationStartTests extends DOMTestCase {
method setup (line 6) | async setup() {
method "test starting an application when the document is loading" (line 12) | async "test starting an application when the document is loading"() {
method "test starting an application when the document is interactive" (line 18) | async "test starting an application when the document is interactive"() {
method "test starting an application when the document is complete" (line 24) | async "test starting an application when the document is complete"() {
method messageFromStartState (line 30) | private messageFromStartState(startState: string): Promise<any> {
method assertIn (line 45) | private assertIn(actual: any, expected: any[]) {
FILE: src/tests/modules/core/application_tests.ts
class AController (line 4) | class AController extends LogController {}
class BController (line 5) | class BController extends LogController {}
class ApplicationTests (line 7) | class ApplicationTests extends ApplicationTestCase {
method "test Application#register" (line 14) | async "test Application#register"() {
method "test Application#load" (line 23) | "test Application#load"() {
method "test Application#unload" (line 37) | "test Application#unload"() {
method controllers (line 48) | get controllers() {
FILE: src/tests/modules/core/class_tests.ts
class ClassTests (line 4) | class ClassTests extends ControllerTestCase(ClassController) {
method "test accessing a class property" (line 14) | "test accessing a class property"() {
method "test accessing a missing class property throws an error" (line 20) | "test accessing a missing class property throws an error"() {
method "test classes must be scoped by identifier" (line 26) | "test classes must be scoped by identifier"() {
method "test multiple classes map to array" (line 30) | "test multiple classes map to array"() {
method "test accessing a class property returns first class if multiple classes are used" (line 34) | "test accessing a class property returns first class if multiple class...
FILE: src/tests/modules/core/data_tests.ts
class DataTests (line 3) | class DataTests extends ControllerTestCase() {
method "test DataSet#get" (line 11) | "test DataSet#get"() {
method "test DataSet#set" (line 17) | "test DataSet#set"() {
method "test DataSet#has" (line 23) | "test DataSet#has"() {
method "test DataSet#delete" (line 29) | "test DataSet#delete"() {
FILE: src/tests/modules/core/default_value_tests.ts
class DefaultValueTests (line 4) | class DefaultValueTests extends ControllerTestCase(DefaultValueControlle...
method "test custom default boolean values" (line 17) | "test custom default boolean values"() {
method "test should be able to set a new value for custom default boolean values" (line 31) | "test should be able to set a new value for custom default boolean val...
method "test should override custom default boolean value with given data-attribute" (line 43) | "test should override custom default boolean value with given data-att...
method "test custom default string values" (line 51) | "test custom default string values"() {
method "test should be able to set a new value for custom default string values" (line 61) | "test should be able to set a new value for custom default string valu...
method "test should override custom default string value with given data-attribute" (line 73) | "test should override custom default string value with given data-attr...
method "test custom default number values" (line 81) | "test custom default number values"() {
method "test should be able to set a new value for custom default number values" (line 95) | "test should be able to set a new value for custom default number valu...
method "test should override custom default number value with given data-attribute" (line 107) | "test should override custom default number value with given data-attr...
method "test custom default array values" (line 115) | "test custom default array values"() {
method "test should be able to set a new value for custom default array values" (line 125) | "test should be able to set a new value for custom default array value...
method "test should override custom default array value with given data-attribute" (line 137) | "test should override custom default array value with given data-attri...
method "test custom default object values" (line 145) | "test custom default object values"() {
method "test should be able to set a new value for custom default object values" (line 155) | "test should be able to set a new value for custom default object valu...
method "test should override custom default object value with given data-attribute" (line 167) | "test should override custom default object value with given data-attr...
method "test [name]ValueChanged callbacks fire after initialize and before connect" (line 173) | "test [name]ValueChanged callbacks fire after initialize and before co...
method has (line 177) | has(name: string) {
method get (line 181) | get(name: string) {
method set (line 185) | set(name: string, value: string) {
method attr (line 189) | attr(name: string) {
method element (line 193) | get element() {
FILE: src/tests/modules/core/error_handler_tests.ts
class MockLogger (line 5) | class MockLogger {
method log (line 10) | log(event: any) {
method error (line 14) | error(event: any) {
method warn (line 18) | warn(event: any) {
method groupCollapsed (line 22) | groupCollapsed() {}
method groupEnd (line 23) | groupEnd() {}
class ErrorWhileConnectingController (line 26) | class ErrorWhileConnectingController extends Controller {
method connect (line 27) | connect() {
class TestApplicationWithDefaultErrorBehavior (line 32) | class TestApplicationWithDefaultErrorBehavior extends Application {}
class ErrorHandlerTests (line 34) | class ErrorHandlerTests extends ControllerTestCase(ErrorWhileConnectingC...
method setupApplication (line 37) | async setupApplication() {
method "test errors in connect are thrown and handled by built in logger" (line 52) | async "test errors in connect are thrown and handled by built in logge...
method "test errors in connect are thrown and handled by window.onerror" (line 60) | async "test errors in connect are thrown and handled by window.onerror"() {
FILE: src/tests/modules/core/es6_tests.ts
class ES6Tests (line 4) | class ES6Tests extends LogControllerTestCase {
method shouldSkipTest (line 5) | static shouldSkipTest(_testName: string) {
method renderFixture (line 19) | async renderFixture() {
method teardown (line 29) | async teardown() {
method "test ES6 controller classes" (line 34) | async "test ES6 controller classes"() {
function supportsES6Classes (line 40) | function supportsES6Classes() {
function supportsReflectConstruct (line 48) | function supportsReflectConstruct() {
FILE: src/tests/modules/core/event_options_tests.ts
class EventOptionsTests (line 4) | class EventOptionsTests extends LogControllerTestCase {
method "test different syntaxes for once action" (line 13) | async "test different syntaxes for once action"() {
method "test mix once and standard actions" (line 26) | async "test mix once and standard actions"() {
method "test stop propagation with once" (line 41) | async "test stop propagation with once"() {
method "test global once actions" (line 56) | async "test global once actions"() {
method "test edge case when updating action list with setAttribute preserves once history" (line 65) | async "test edge case when updating action list with setAttribute pres...
method "test default passive action" (line 82) | async "test default passive action"() {
method "test global passive actions" (line 89) | async "test global passive actions"() {
method "test passive false actions" (line 96) | async "test passive false actions"() {
method "test multiple options" (line 104) | async "test multiple options"() {
method "test wrong options are silently ignored" (line 113) | async "test wrong options are silently ignored"() {
method "test stop option with implicit event" (line 122) | async "test stop option with implicit event"() {
method "test stop option with explicit event" (line 131) | async "test stop option with explicit event"() {
method "test event propagation without stop option" (line 140) | async "test event propagation without stop option"() {
method "test prevent option with implicit event" (line 149) | async "test prevent option with implicit event"() {
method "test prevent option with explicit event" (line 157) | async "test prevent option with explicit event"() {
method "test self option" (line 165) | async "test self option"() {
method "test self option on parent" (line 173) | async "test self option on parent"() {
method "test custom action option callback params contain the controller instance" (line 181) | async "test custom action option callback params contain the controlle...
method "test custom option" (line 222) | async "test custom option"() {
method "test inverted custom option" (line 240) | async "test inverted custom option"() {
method "test custom action option callback event contains params" (line 258) | async "test custom action option callback event contains params"() {
method setAction (line 295) | setAction(element: Element, value: string) {
method toggleElement (line 300) | toggleElement(details: Element) {
method element (line 305) | get element() {
method buttonElement (line 309) | get buttonElement() {
method detailsElement (line 313) | get detailsElement() {
FILE: src/tests/modules/core/extending_application_tests.ts
class TestApplicationWithCustomBehavior (line 10) | class TestApplicationWithCustomBehavior extends Application {
method registerActionOption (line 11) | registerActionOption(name: string, filter: ActionDescriptorFilter): vo...
class ExtendingApplicationTests (line 17) | class ExtendingApplicationTests extends DOMTestCase {
method runTest (line 20) | async runTest(testName: string) {
method setup (line 30) | async setup() {
method teardown (line 34) | async teardown() {
method "test extended class method is supported when using MyApplication.start()" (line 38) | async "test extended class method is supported when using MyApplication....
FILE: src/tests/modules/core/legacy_target_tests.ts
class LegacyTargetTests (line 4) | class LegacyTargetTests extends ControllerTestCase(TargetController) {
method setupApplication (line 21) | async setupApplication() {
method "test TargetSet#find" (line 30) | "test TargetSet#find"() {
method "test TargetSet#find prefers scoped target attributes" (line 35) | "test TargetSet#find prefers scoped target attributes"() {
method "test TargetSet#findAll" (line 40) | "test TargetSet#findAll"() {
method "test TargetSet#findAll prioritizes scoped target attributes" (line 45) | "test TargetSet#findAll prioritizes scoped target attributes"() {
method "test TargetSet#findAll with multiple arguments" (line 50) | "test TargetSet#findAll with multiple arguments"() {
method "test TargetSet#has" (line 58) | "test TargetSet#has"() {
method "test TargetSet#find ignores child controller targets" (line 64) | "test TargetSet#find ignores child controller targets"() {
method "test linked target properties" (line 71) | "test linked target properties"() {
method "test inherited linked target properties" (line 78) | "test inherited linked target properties"() {
method "test singular linked target property throws an error when no target is found" (line 84) | "test singular linked target property throws an error when no target i...
FILE: src/tests/modules/core/lifecycle_tests.ts
class LifecycleTests (line 3) | class LifecycleTests extends LogControllerTestCase {
method setup (line 6) | async setup() {
method "test Controller#initialize" (line 10) | async "test Controller#initialize"() {
method "test Controller#connect" (line 18) | async "test Controller#connect"() {
method "test Controller#disconnect" (line 24) | async "test Controller#disconnect"() {
method reconnectControllerElement (line 31) | async reconnectControllerElement() {
method connectControllerElement (line 36) | async connectControllerElement() {
method disconnectControllerElement (line 41) | async disconnectControllerElement() {
FILE: src/tests/modules/core/loading_tests.ts
class UnloadableController (line 4) | class UnloadableController extends LogController {
method shouldLoad (line 5) | static get shouldLoad() {
class LoadableController (line 9) | class LoadableController extends LogController {
method shouldLoad (line 10) | static get shouldLoad() {
class AfterLoadController (line 15) | class AfterLoadController extends LogController {
method afterLoad (line 20) | static afterLoad(identifier: string, application: any) {
class ApplicationTests (line 33) | class ApplicationTests extends ApplicationTestCase {
method "test module with false shouldLoad should not load when registering" (line 36) | "test module with false shouldLoad should not load when registering"() {
method "test module with true shouldLoad should load when registering" (line 41) | "test module with true shouldLoad should load when registering"() {
method "test module with afterLoad method should be triggered when registered" (line 46) | "test module with afterLoad method should be triggered when registered...
method controllers (line 70) | get controllers() {
FILE: src/tests/modules/core/memory_tests.ts
class MemoryTests (line 3) | class MemoryTests extends ControllerTestCase() {
method setup (line 6) | async setup() {
method "test removing a controller clears dangling eventListeners" (line 17) | async "test removing a controller clears dangling eventListeners"() {
FILE: src/tests/modules/core/outlet_order_tests.ts
class OutletOrderController (line 6) | class OutletOrderController extends OutletController {
method connect (line 7) | connect() {
class OutletOrderTests (line 14) | class OutletOrderTests extends ControllerTestCase(OutletOrderController) {
method identifiers (line 22) | get identifiers() {
method "test can access outlets in connect() even if they are referenced before they are connected" (line 26) | async "test can access outlets in connect() even if they are reference...
FILE: src/tests/modules/core/outlet_tests.ts
class OutletTests (line 4) | class OutletTests extends ControllerTestCase(OutletController) {
method identifiers (line 39) | get identifiers() {
method "test OutletSet#find" (line 43) | "test OutletSet#find"() {
method "test OutletSet#findAll" (line 50) | "test OutletSet#findAll"() {
method "test OutletSet#findAll with multiple arguments" (line 59) | "test OutletSet#findAll with multiple arguments"() {
method "test OutletSet#has" (line 66) | "test OutletSet#has"() {
method "test OutletSet#has when attribute gets added later" (line 75) | "test OutletSet#has when attribute gets added later"() {
method "test OutletSet#has when no element with selector exists" (line 81) | "test OutletSet#has when no element with selector exists"() {
method "test OutletSet#has when selector matches but element doesn't have the right controller" (line 86) | "test OutletSet#has when selector matches but element doesn't have the...
method "test linked outlet properties" (line 91) | "test linked outlet properties"() {
method "test inherited linked outlet properties" (line 107) | "test inherited linked outlet properties"() {
method "test singular linked outlet property throws an error when no outlet is found" (line 121) | "test singular linked outlet property throws an error when no outlet i...
method "test outlet connected callback fires" (line 135) | async "test outlet connected callback fires"() {
method "test outlet connected callback fires for namespaced outlets" (line 142) | "test outlet connected callback fires for namespaced outlets"() {
method "test outlet connected callback when element is inserted" (line 150) | async "test outlet connected callback when element is inserted"() {
method "test outlet connected callback when present element adds matching outlet selector attribute" (line 171) | async "test outlet connected callback when present element adds matchi...
method "test outlet connected callback when present element already has connected controller and adds matching outlet selector attribute" (line 184) | async "test outlet connected callback when present element already has...
method "test outlet connect callback when an outlet present in the document adds a matching data-controller attribute" (line 196) | async "test outlet connect callback when an outlet present in the docu...
method "test outlet disconnected callback fires when calling disconnect() on the controller" (line 208) | async "test outlet disconnected callback fires when calling disconnect...
method "test outlet disconnected callback when element is removed" (line 225) | async "test outlet disconnected callback when element is removed"() {
method "test outlet disconnected callback when element is removed with namespaced outlet" (line 244) | async "test outlet disconnected callback when element is removed with ...
method "test outlet disconnected callback when an outlet present in the document removes the selector attribute" (line 263) | async "test outlet disconnected callback when an outlet present in the...
method "test outlet disconnected callback when an outlet present in the document removes the data-controller attribute" (line 282) | async "test outlet disconnected callback when an outlet present in the...
method "test outlet connect callback when the controlled element's outlet attribute is added" (line 301) | async "test outlet connect callback when the controlled element's outl...
method "test outlet connect callback doesn't get trigged when any attribute gets added to the controller element" (line 311) | async "test outlet connect callback doesn't get trigged when any attri...
method "test outlet connect callback when the controlled element's outlet attribute is changed" (line 330) | async "test outlet connect callback when the controlled element's outl...
method "test outlet disconnected callback when the controlled element's outlet attribute is removed" (line 351) | async "test outlet disconnected callback when the controlled element's...
FILE: src/tests/modules/core/string_helpers_tests.ts
class StringHelpersTests (line 4) | class StringHelpersTests extends TestCase {
method "test should camelize strings" (line 5) | "test should camelize strings"() {
method "test should namespace camelize strings" (line 19) | "test should namespace camelize strings"() {
method "test should dasherize strings" (line 33) | "test should dasherize strings"() {
method "test should capitalize strings" (line 38) | "test should capitalize strings"() {
method "test should tokenize strings" (line 43) | "test should tokenize strings"() {
FILE: src/tests/modules/core/target_tests.ts
class TargetTests (line 4) | class TargetTests extends ControllerTestCase(TargetController) {
method "test TargetSet#find" (line 19) | "test TargetSet#find"() {
method "test TargetSet#findAll" (line 23) | "test TargetSet#findAll"() {
method "test TargetSet#findAll with multiple arguments" (line 27) | "test TargetSet#findAll with multiple arguments"() {
method "test TargetSet#has" (line 34) | "test TargetSet#has"() {
method "test TargetSet#find ignores child controller targets" (line 39) | "test TargetSet#find ignores child controller targets"() {
method "test linked target properties" (line 45) | "test linked target properties"() {
method "test inherited linked target properties" (line 51) | "test inherited linked target properties"() {
method "test singular linked target property throws an error when no target is found" (line 56) | "test singular linked target property throws an error when no target i...
method "test target connected callback fires after initialize() and when calling connect()" (line 63) | "test target connected callback fires after initialize() and when call...
method "test target connected callback when element is inserted" (line 70) | async "test target connected callback when element is inserted"() {
method "test target connected callback when present element adds the target attribute" (line 87) | async "test target connected callback when present element adds the ta...
method "test target connected callback when element adds a token to an existing target attribute" (line 100) | async "test target connected callback when element adds a token to an ...
method "test target disconnected callback fires when calling disconnect() on the controller" (line 113) | async "test target disconnected callback fires when calling disconnect...
method "test target disconnected callback when element is removed" (line 130) | async "test target disconnected callback when element is removed"() {
method "test target disconnected callback when an element present in the document removes the target attribute" (line 150) | async "test target disconnected callback when an element present in th...
method "test target disconnected(), then connected() callback fired when the target name is present after the attribute change" (line 170) | async "test target disconnected(), then connected() callback fired whe...
method "test [target]Connected() and [target]Disconnected() do not loop infinitely" (line 192) | async "test [target]Connected() and [target]Disconnected() do not loop...
FILE: src/tests/modules/core/value_properties_tests.ts
class ValuePropertiesTests (line 12) | class ValuePropertiesTests extends ControllerTestCase(ValueController) {
method "test parseValueTypeConstant" (line 13) | "test parseValueTypeConstant"() {
method "test parseValueTypeDefault" (line 31) | "test parseValueTypeDefault"() {
method "test parseValueTypeObject" (line 57) | "test parseValueTypeObject"() {
method "test parseValueTypeDefinition booleans" (line 108) | "test parseValueTypeDefinition booleans"() {
method "test defaultValueForDefinition" (line 136) | "test defaultValueForDefinition"() {
FILE: src/tests/modules/core/value_tests.ts
class ValueTests (line 4) | class ValueTests extends ControllerTestCase(ValueController) {
method "test string values" (line 16) | "test string values"() {
method "test numeric values" (line 24) | "test numeric values"() {
method "test boolean values" (line 59) | "test boolean values"() {
method "test array values" (line 83) | "test array values"() {
method "test object values" (line 100) | "test object values"() {
method "test accessing a string value returns the empty string when the attribute is missing" (line 117) | "test accessing a string value returns the empty string when the attri...
method "test accessing a numeric value returns zero when the attribute is missing" (line 123) | "test accessing a numeric value returns zero when the attribute is mis...
method "test accessing a boolean value returns false when the attribute is missing" (line 129) | "test accessing a boolean value returns false when the attribute is mi...
method "test accessing an array value returns an empty array when the attribute is missing" (line 135) | "test accessing an array value returns an empty array when the attribu...
method "test accessing an object value returns an empty object when the attribute is missing" (line 144) | "test accessing an object value returns an empty object when the attri...
method "test changed callbacks" (line 153) | async "test changed callbacks"() {
method "test changed callbacks for object" (line 168) | async "test changed callbacks for object"() {
method "test default values trigger changed callbacks" (line 194) | async "test default values trigger changed callbacks"() {
method "test keys may be specified in kebab-case" (line 209) | "test keys may be specified in kebab-case"() {
method has (line 213) | has(name: string) {
method get (line 217) | get(name: string) {
method set (line 221) | set(name: string, value: string) {
method attr (line 225) | attr(name: string) {
method element (line 229) | get element() {
FILE: src/tests/modules/mutation-observers/attribute_observer_tests.ts
class AttributeObserverTests (line 4) | class AttributeObserverTests extends ObserverTestCase implements Attribu...
method "test elementMatchedAttribute" (line 9) | async "test elementMatchedAttribute"() {
method "test elementAttributeValueChanged" (line 13) | async "test elementAttributeValueChanged"() {
method "test elementUnmatchedAttribute" (line 23) | async "test elementUnmatchedAttribute"() {
method "test observes attribute changes to child elements" (line 33) | async "test observes attribute changes to child elements"() {
method "test ignores other attributes" (line 43) | async "test ignores other attributes"() {
method "test observes removal of nested matched element HTML" (line 50) | async "test observes removal of nested matched element HTML"() {
method "test ignores synchronously disconnected elements" (line 67) | async "test ignores synchronously disconnected elements"() {
method "test ignores synchronously moved elements" (line 77) | async "test ignores synchronously moved elements"() {
method outerElement (line 89) | get outerElement() {
method innerElement (line 93) | get innerElement() {
method elementMatchedAttribute (line 99) | elementMatchedAttribute(element: Element, attributeName: string) {
method elementAttributeValueChanged (line 103) | elementAttributeValueChanged(element: Element, attributeName: string) {
method elementUnmatchedAttribute (line 107) | elementUnmatchedAttribute(element: Element, attributeName: string) {
FILE: src/tests/modules/mutation-observers/selector_observer_tests.ts
class SelectorObserverTests (line 4) | class SelectorObserverTests extends ObserverTestCase implements Selector...
method "test should match when observer starts" (line 19) | async "test should match when observer starts"() {
method "test should match when element gets appended" (line 26) | async "test should match when element gets appended"() {
method "test should not match/unmatch when the attribute gets updated and matching selector persists" (line 45) | async "test should not match/unmatch when the attribute gets updated a...
method "test should match when attribute gets updated and start to matche selector" (line 53) | async "test should match when attribute gets updated and start to matc...
method "test should unmatch when attribute gets updated but matching attribute value gets removed" (line 61) | async "test should unmatch when attribute gets updated but matching at...
method "test should unmatch when attribute gets removed" (line 69) | async "test should unmatch when attribute gets removed"() {
method "test should unmatch when element gets removed" (line 81) | async "test should unmatch when element gets removed"() {
method "test should not match/unmatch when observer is paused" (line 98) | async "test should not match/unmatch when observer is paused"() {
method element (line 112) | get element(): Element {
method div1 (line 116) | get div1(): Element {
method div2 (line 120) | get div2(): Element {
method selectorMatched (line 126) | selectorMatched(element: Element, selector: string, details: object) {
method selectorUnmatched (line 130) | selectorUnmatched(element: Element, selector: string, details: object) {
FILE: src/tests/modules/mutation-observers/token_list_observer_tests.ts
class TokenListObserverTests (line 4) | class TokenListObserverTests extends ObserverTestCase implements TokenLi...
method "test tokenMatched" (line 9) | async "test tokenMatched"() {
method "test adding a token to the right" (line 16) | async "test adding a token to the right"() {
method "test inserting a token in the middle" (line 23) | async "test inserting a token in the middle"() {
method "test removing the leftmost token" (line 34) | async "test removing the leftmost token"() {
method "test removing the rightmost token" (line 45) | async "test removing the rightmost token"() {
method "test removing the only token" (line 52) | async "test removing the only token"() {
method element (line 64) | get element(): Element {
method tokenString (line 68) | set tokenString(value: string) {
method tokenMatched (line 74) | tokenMatched(token: Token) {
method tokenUnmatched (line 78) | tokenUnmatched(token: Token) {
FILE: src/tests/modules/mutation-observers/value_list_observer_tests.ts
type Value (line 4) | interface Value {
class ValueListObserverTests (line 9) | class ValueListObserverTests extends ObserverTestCase implements ValueLi...
method "test elementMatchedValue" (line 15) | async "test elementMatchedValue"() {
method "test adding a token to the right" (line 19) | async "test adding a token to the right"() {
method "test adding a token to the left" (line 26) | async "test adding a token to the left"() {
method "test removing a token from the right" (line 37) | async "test removing a token from the right"() {
method "test removing a token from the left" (line 49) | async "test removing a token from the left"() {
method "test removing the only token" (line 63) | async "test removing the only token"() {
method "test removing and re-adding a token produces a new value" (line 70) | async "test removing and re-adding a token produces a new value"() {
method element (line 82) | get element() {
method valueString (line 86) | set valueString(value: string) {
method parseValueForToken (line 92) | parseValueForToken(token: Token) {
method elementMatchedValue (line 96) | elementMatchedValue(element: Element, value: Value) {
method elementUnmatchedValue (line 100) | elementUnmatchedValue(element: Element, value: Value) {
Condensed preview — 163 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (416K chars).
[
{
"path": ".eslintignore",
"chars": 20,
"preview": "dist/\nnode_modules/\n"
},
{
"path": ".eslintrc",
"chars": 756,
"preview": "{\n \"root\": true,\n \"parser\": \"@typescript-eslint/parser\",\n \"plugins\": [\n \"@typescript-eslint\",\n \"prettier\"\n ],\n"
},
{
"path": ".github/scripts/publish-dev-build",
"chars": 1139,
"preview": "#!/usr/bin/env bash\nset -eux\n\nDEV_BUILD_REPO_NAME=\"hotwired/dev-builds\"\nDEV_BUILD_ORIGIN_URL=\"https://${1}@github.com/${"
},
{
"path": ".github/workflows/build.yml",
"chars": 545,
"preview": "name: Build\n\non: [push, pull_request]\n\njobs:\n build:\n name: Build\n runs-on: ubuntu-latest\n strategy:\n mat"
},
{
"path": ".github/workflows/dev-builds.yml",
"chars": 458,
"preview": "name: dev-builds\n\non:\n workflow_dispatch:\n push:\n branches:\n - main\n - 'builds/**'\n\njobs:\n build:\n ru"
},
{
"path": ".github/workflows/lint.yml",
"chars": 365,
"preview": "name: Lint\n\non: [push, pull_request]\n\njobs:\n lint:\n name: Lint\n runs-on: ubuntu-latest\n\n steps:\n - uses: "
},
{
"path": ".github/workflows/test.yml",
"chars": 483,
"preview": "name: Test\n\non: [push, pull_request]\n\njobs:\n test:\n name: Test\n runs-on: ubuntu-latest\n\n steps:\n - uses: "
},
{
"path": ".gitignore",
"chars": 60,
"preview": "coverage/\ndist/\nnode_modules/\ndocs/api/\n*.log\n*.tsbuildinfo\n"
},
{
"path": ".node-version",
"chars": 8,
"preview": "20.11.0\n"
},
{
"path": ".npmignore",
"chars": 39,
"preview": "src/tests/\ndist/tests/\ntsconfig*\n*.log\n"
},
{
"path": ".prettierignore",
"chars": 20,
"preview": "dist/\nnode_modules/\n"
},
{
"path": ".prettierrc.json",
"chars": 70,
"preview": " {\n \"singleQuote\": false,\n \"printWidth\": 120,\n \"semi\": false\n }\n"
},
{
"path": "CHANGELOG.md",
"chars": 101,
"preview": "# Changelog\n\nPlease see [our GitHub \"Releases\" page](https://github.com/hotwired/stimulus/releases).\n"
},
{
"path": "CODE_OF_CONDUCT.md",
"chars": 3233,
"preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, w"
},
{
"path": "LICENSE.md",
"chars": 1071,
"preview": "# MIT License\n\nCopyright © 2021 Basecamp, LLC.\n\nPermission is hereby granted, free of charge, to any person obtaining a "
},
{
"path": "README.md",
"chars": 3152,
"preview": "# <img src=\"assets/logo.svg?sanitize=true\" width=\"24\" height=\"24\" alt=\"Stimulus\"> Stimulus\n\n### A modest JavaScript fram"
},
{
"path": "SECURITY.md",
"chars": 1631,
"preview": "# Security Considerations\n\n### Q: Can I be confident that if my cross-site scripting countermeasures fail, there is no w"
},
{
"path": "docs/handbook/00_the_origin_of_stimulus.md",
"chars": 9260,
"preview": "---\npermalink: /handbook/origin.html\nnav_prefix: Preface\norder: 00\n---\n\n# The Origin of Stimulus\n\nWe write a lot of Java"
},
{
"path": "docs/handbook/01_introduction.md",
"chars": 2299,
"preview": "---\npermalink: /handbook/introduction.html\norder: 01\n---\n\n# Introduction\n\n## About Stimulus\n\nStimulus is a JavaScript fr"
},
{
"path": "docs/handbook/02_hello_stimulus.md",
"chars": 7247,
"preview": "---\npermalink: /handbook/hello-stimulus.html\norder: 02\n---\n\n# Hello, Stimulus\n\nThe best way to learn how Stimulus works "
},
{
"path": "docs/handbook/03_building_something_real.md",
"chars": 6851,
"preview": "---\npermalink: /handbook/building-something-real.html\norder: 03\n---\n\n# Building Something Real\n\nWe've implemented our fi"
},
{
"path": "docs/handbook/04_designing_for_resilience.md",
"chars": 3438,
"preview": "---\npermalink: /handbook/designing-for-resilience.html\norder: 04\n---\n\n# Designing For Resilience\n\nAlthough the clipboard"
},
{
"path": "docs/handbook/05_managing_state.md",
"chars": 8777,
"preview": "---\npermalink: /handbook/managing-state.html\norder: 05\n---\n\n# Managing State\n\nMost contemporary frameworks encourage you"
},
{
"path": "docs/handbook/06_working_with_external_resources.md",
"chars": 6689,
"preview": "---\npermalink: /handbook/working-with-external-resources.html\norder: 06\n---\n\n# Working With External Resources\n\nIn the l"
},
{
"path": "docs/handbook/07_installing_stimulus.md",
"chars": 5643,
"preview": "---\npermalink: /handbook/installing.html\norder: 07\n---\n\n# Installing Stimulus in Your Application\n\nTo install Stimulus i"
},
{
"path": "docs/reference/actions.md",
"chars": 12682,
"preview": "---\npermalink: /reference/actions.html\norder: 02\n---\n\n# Actions\n\n_Actions_ are how you handle DOM events in your control"
},
{
"path": "docs/reference/controllers.md",
"chars": 10911,
"preview": "---\npermalink: /reference/controllers.html\norder: 00\n---\n\n# Controllers\n\nA _controller_ is the basic organizational unit"
},
{
"path": "docs/reference/css_classes.md",
"chars": 5279,
"preview": "---\npermalink: /reference/css-classes.html\norder: 06\n---\n\n# CSS Classes\n\nIn HTML, a _CSS class_ defines a set of styles "
},
{
"path": "docs/reference/lifecycle_callbacks.md",
"chars": 4617,
"preview": "---\npermalink: /reference/lifecycle-callbacks.html\norder: 01\n---\n\n# Lifecycle Callbacks\n\nSpecial methods called _lifecyc"
},
{
"path": "docs/reference/outlets.md",
"chars": 6807,
"preview": "---\npermalink: /reference/outlets.html\norder: 04\n---\n\n# Outlets\n\n_Outlets_ let you reference Stimulus _controller instan"
},
{
"path": "docs/reference/targets.md",
"chars": 4671,
"preview": "---\npermalink: /reference/targets.html\norder: 03\n---\n\n# Targets\n\n_Targets_ let you reference important elements by name."
},
{
"path": "docs/reference/using_typescript.md",
"chars": 2762,
"preview": "---\npermalink: /reference/using-typescript.html\norder: 07\n---\n\n# Using Typescript\n\nStimulus itself is written in [TypeSc"
},
{
"path": "docs/reference/values.md",
"chars": 5105,
"preview": "---\npermalink: /reference/values.html\norder: 05\n---\n\n# Values\n\nYou can read and write [HTML data attributes](https://dev"
},
{
"path": "examples/.babelrc",
"chars": 158,
"preview": "{\n \"presets\": [\n [\"@babel/preset-env\"]\n ],\n \"plugins\": [\n [\"@babel/plugin-proposal-class-properties\"],\n [\"@b"
},
{
"path": "examples/controllers/clipboard_controller.js",
"chars": 367,
"preview": "import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n static targets = [ \"source\""
},
{
"path": "examples/controllers/content_loader_controller.js",
"chars": 889,
"preview": "import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n static targets = [\"item\"]\n "
},
{
"path": "examples/controllers/hello_controller.js",
"chars": 225,
"preview": "import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n static targets = [\"name\"]\n\n"
},
{
"path": "examples/controllers/slideshow_controller.js",
"chars": 639,
"preview": "import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n static targets = [ \"slide\" "
},
{
"path": "examples/controllers/tabs_controller.js",
"chars": 974,
"preview": "import { Controller } from \"@hotwired/stimulus\"\n\nexport default class extends Controller {\n static targets = [ \"tab\", \""
},
{
"path": "examples/index.js",
"chars": 722,
"preview": "import { Application } from \"@hotwired/stimulus\"\nimport \"@hotwired/turbo\"\n\nconst application = Application.start()\n\nimpo"
},
{
"path": "examples/package.json",
"chars": 507,
"preview": "{\n \"name\": \"@hotwired/stimulus-examples\",\n \"version\": \"3.1.0\",\n \"private\": true,\n \"dependencies\": {\n \"@babel/core"
},
{
"path": "examples/public/examples.css",
"chars": 1433,
"preview": "body {\n background: rgb(251, 247, 240);\n}\n\nmain {\n display: flex;\n flex-flow: row;\n align-items: flex-start;\n justi"
},
{
"path": "examples/public/main.css",
"chars": 1309,
"preview": "* {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n}\n\n*:not(:active) {\n color: #333;\n}\n\na:active {\n color: #333;"
},
{
"path": "examples/server.js",
"chars": 1236,
"preview": "const fs = require(\"fs\")\nconst path = require(\"path\")\nconst express = require(\"express\")\nconst webpack = require(\"webpac"
},
{
"path": "examples/views/clipboard.ejs",
"chars": 732,
"preview": "<%- include(\"layout/head\") %>\n\n<p data-controller=\"clipboard\" data-clipboard-supported-class=\"clipboard--supported\">\n <"
},
{
"path": "examples/views/content-loader.ejs",
"chars": 422,
"preview": "<%- include(\"layout/head\") %>\n\n<p data-controller=\"content-loader\"\n data-content-loader-url-value=\"/uptime\"></p>\n\n<p d"
},
{
"path": "examples/views/hello.ejs",
"chars": 279,
"preview": "<%- include(\"layout/head\") %>\n\n<form method=\"dialog\" data-controller=\"hello\">\n <p>\n <input data-hello-target=\"name\" "
},
{
"path": "examples/views/layout/head.ejs",
"chars": 756,
"preview": "<!DOCTYPE html>\n<html>\n\n<head>\n <meta charset=\"utf-8\">\n <title>Stimulus: <%= currentPage.title %> Example</title>\n <l"
},
{
"path": "examples/views/layout/tail.ejs",
"chars": 37,
"preview": " </div>\n </main>\n</body>\n</html>\n"
},
{
"path": "examples/views/slideshow.ejs",
"chars": 496,
"preview": "<%- include(\"layout/head\") %>\n\n<div data-controller=\"slideshow\" data-slideshow-current-slide-class=\"slide--current\">\n <"
},
{
"path": "examples/views/tabs.ejs",
"chars": 1083,
"preview": "<%- include(\"layout/head\") %>\n\n<div data-controller=\"tabs\" data-tabs-current-class=\"tabpanel--current\" aria-label=\"examp"
},
{
"path": "examples/webpack.config.js",
"chars": 665,
"preview": "const path = require(\"path\")\n\nmodule.exports = {\n entry: {\n main: \"./index.js\"\n },\n\n output: {\n filename: \"[nam"
},
{
"path": "karma.conf.cjs",
"chars": 875,
"preview": "const config = {\n basePath: \".\",\n\n browsers: [\"ChromeHeadless\", \"FirefoxHeadless\"],\n\n frameworks: [\"qunit\"],\n\n repor"
},
{
"path": "package.json",
"chars": 2431,
"preview": "{\n \"name\": \"@hotwired/stimulus\",\n \"version\": \"3.2.2\",\n \"license\": \"MIT\",\n \"description\": \"A modest JavaScript framew"
},
{
"path": "packages/stimulus/.gitignore",
"chars": 10,
"preview": "README.md\n"
},
{
"path": "packages/stimulus/.npmignore",
"chars": 23,
"preview": "rollup.config.js\n*.log\n"
},
{
"path": "packages/stimulus/index.d.ts",
"chars": 64,
"preview": "export * from \"@hotwired/stimulus\"\nexport as namespace Stimulus\n"
},
{
"path": "packages/stimulus/index.js",
"chars": 35,
"preview": "export * from \"@hotwired/stimulus\"\n"
},
{
"path": "packages/stimulus/package.json",
"chars": 1851,
"preview": "{\n \"name\": \"stimulus\",\n \"version\": \"3.2.2\",\n \"description\": \"Stimulus JavaScript framework\",\n \"homepage\": \"https://s"
},
{
"path": "packages/stimulus/rollup.config.js",
"chars": 683,
"preview": "import resolve from \"@rollup/plugin-node-resolve\"\n\nexport default [\n {\n input: \"index.js\",\n output: [\n {\n "
},
{
"path": "packages/stimulus/webpack-helpers.d.ts",
"chars": 94,
"preview": "export * from \"@hotwired/stimulus-webpack-helpers\"\nexport as namespace StimulusWebpackHelpers\n"
},
{
"path": "packages/stimulus/webpack-helpers.js",
"chars": 51,
"preview": "export * from \"@hotwired/stimulus-webpack-helpers\"\n"
},
{
"path": "rollup.config.js",
"chars": 969,
"preview": "import resolve from \"@rollup/plugin-node-resolve\"\nimport typescript from \"@rollup/plugin-typescript\"\nimport { terser } f"
},
{
"path": "src/core/action.ts",
"chars": 4117,
"preview": "import { ActionDescriptor, parseActionDescriptorString, stringifyEventTarget } from \"./action_descriptor\"\nimport { Token"
},
{
"path": "src/core/action_descriptor.ts",
"chars": 2420,
"preview": "import type { Controller } from \"./controller\"\n\nexport type ActionDescriptorFilters = Record<string, ActionDescriptorFil"
},
{
"path": "src/core/action_event.ts",
"chars": 80,
"preview": "export interface ActionEvent extends Event {\n params: { [key: string]: any }\n}\n"
},
{
"path": "src/core/application.ts",
"chars": 3779,
"preview": "import { Controller, ControllerConstructor } from \"./controller\"\nimport { Definition } from \"./definition\"\nimport { Disp"
},
{
"path": "src/core/binding.ts",
"chars": 3281,
"preview": "import { Action } from \"./action\"\nimport { ActionEvent } from \"./action_event\"\nimport { Context } from \"./context\"\nimpor"
},
{
"path": "src/core/binding_observer.ts",
"chars": 2568,
"preview": "import { Action } from \"./action\"\nimport { Binding } from \"./binding\"\nimport { Context } from \"./context\"\nimport { Error"
},
{
"path": "src/core/blessing.ts",
"chars": 3268,
"preview": "import { Constructor } from \"./constructor\"\nimport { readInheritableStaticArrayValues } from \"./inheritable_statics\"\n\nex"
},
{
"path": "src/core/class_map.ts",
"chars": 684,
"preview": "import { Scope } from \"./scope\"\nimport { tokenize } from \"./string_helpers\"\n\nexport class ClassMap {\n readonly scope: S"
},
{
"path": "src/core/class_properties.ts",
"chars": 1154,
"preview": "import { Constructor } from \"./constructor\"\nimport { Controller } from \"./controller\"\nimport { readInheritableStaticArra"
},
{
"path": "src/core/constructor.ts",
"chars": 55,
"preview": "export type Constructor<T> = new (...args: any[]) => T\n"
},
{
"path": "src/core/context.ts",
"chars": 4118,
"preview": "import { Application } from \"./application\"\nimport { BindingObserver } from \"./binding_observer\"\nimport { Controller } f"
},
{
"path": "src/core/controller.ts",
"chars": 2301,
"preview": "import { Application } from \"./application\"\nimport { ClassPropertiesBlessing } from \"./class_properties\"\nimport { Constr"
},
{
"path": "src/core/data_map.ts",
"chars": 1070,
"preview": "import { Scope } from \"./scope\"\nimport { dasherize } from \"./string_helpers\"\n\nexport class DataMap {\n readonly scope: S"
},
{
"path": "src/core/definition.ts",
"chars": 384,
"preview": "import { bless } from \"./blessing\"\nimport { ControllerConstructor } from \"./controller\"\n\nexport interface Definition {\n "
},
{
"path": "src/core/dispatcher.ts",
"chars": 3920,
"preview": "import { Application } from \"./application\"\nimport { Binding } from \"./binding\"\nimport { BindingObserverDelegate } from "
},
{
"path": "src/core/error_handler.ts",
"chars": 101,
"preview": "export interface ErrorHandler {\n handleError(error: Error, message: string, detail: object): void\n}\n"
},
{
"path": "src/core/event_listener.ts",
"chars": 1991,
"preview": "import { Binding } from \"./binding\"\n\nexport class EventListener implements EventListenerObject {\n readonly eventTarget:"
},
{
"path": "src/core/guide.ts",
"chars": 576,
"preview": "import { Logger } from \"./logger\"\n\nexport class Guide {\n readonly logger: Logger\n readonly warnedKeysByObject: WeakMap"
},
{
"path": "src/core/index.ts",
"chars": 281,
"preview": "export { ActionEvent } from \"./action_event\"\nexport { Application } from \"./application\"\nexport { Context } from \"./cont"
},
{
"path": "src/core/inheritable_statics.ts",
"chars": 1496,
"preview": "import { Constructor } from \"./constructor\"\n\nexport function readInheritableStaticArrayValues<T, U = string>(constructor"
},
{
"path": "src/core/logger.ts",
"chars": 245,
"preview": "export interface Logger {\n log(message: string, ...args: any[]): void\n warn(message: string, ...args: any[]): void\n e"
},
{
"path": "src/core/module.ts",
"chars": 1513,
"preview": "import { Application } from \"./application\"\nimport { Context } from \"./context\"\nimport { ControllerConstructor } from \"."
},
{
"path": "src/core/outlet_observer.ts",
"chars": 8255,
"preview": "import { Multimap } from \"../multimap\"\nimport { AttributeObserver, AttributeObserverDelegate } from \"../mutation-observe"
},
{
"path": "src/core/outlet_properties.ts",
"chars": 3608,
"preview": "import { Constructor } from \"./constructor\"\nimport { Controller } from \"./controller\"\nimport { readInheritableStaticArra"
},
{
"path": "src/core/outlet_set.ts",
"chars": 2183,
"preview": "import { Scope } from \"./scope\"\n\nexport class OutletSet {\n readonly scope: Scope\n readonly controllerElement: Element\n"
},
{
"path": "src/core/router.ts",
"chars": 3845,
"preview": "import { Application } from \"./application\"\nimport { Context } from \"./context\"\nimport { Definition } from \"./definition"
},
{
"path": "src/core/schema.ts",
"chars": 1181,
"preview": "export interface Schema {\n controllerAttribute: string\n actionAttribute: string\n targetAttribute: string\n targetAttr"
},
{
"path": "src/core/scope.ts",
"chars": 1904,
"preview": "import { ClassMap } from \"./class_map\"\nimport { DataMap } from \"./data_map\"\nimport { Guide } from \"./guide\"\nimport { Log"
},
{
"path": "src/core/scope_observer.ts",
"chars": 2851,
"preview": "import { ErrorHandler } from \"./error_handler\"\nimport { Schema } from \"./schema\"\nimport { Scope } from \"./scope\"\nimport "
},
{
"path": "src/core/selectors.ts",
"chars": 128,
"preview": "export function attributeValueContainsToken(attributeName: string, token: string) {\n return `[${attributeName}~=\"${toke"
},
{
"path": "src/core/string_helpers.ts",
"chars": 548,
"preview": "export function camelize(value: string) {\n return value.replace(/(?:[_-])([a-z0-9])/g, (_, char) => char.toUpperCase())"
},
{
"path": "src/core/target_observer.ts",
"chars": 2329,
"preview": "import { Multimap } from \"../multimap\"\nimport { Token, TokenListObserver, TokenListObserverDelegate } from \"../mutation-"
},
{
"path": "src/core/target_properties.ts",
"chars": 1140,
"preview": "import { Constructor } from \"./constructor\"\nimport { Controller } from \"./controller\"\nimport { readInheritableStaticArra"
},
{
"path": "src/core/target_set.ts",
"chars": 2731,
"preview": "import { Scope } from \"./scope\"\nimport { attributeValueContainsToken } from \"./selectors\"\n\nexport class TargetSet {\n re"
},
{
"path": "src/core/utils.ts",
"chars": 240,
"preview": "export function isSomething(object: any): boolean {\n return object !== null && object !== undefined\n}\n\nexport function "
},
{
"path": "src/core/value_observer.ts",
"chars": 3933,
"preview": "import { Context } from \"./context\"\nimport { StringMapObserver, StringMapObserverDelegate } from \"../mutation-observers\""
},
{
"path": "src/core/value_properties.ts",
"chars": 8395,
"preview": "import { Constructor } from \"./constructor\"\nimport { Controller } from \"./controller\"\nimport { readInheritableStaticObje"
},
{
"path": "src/index.d.ts",
"chars": 52,
"preview": "export * from \"./core\"\nexport as namespace Stimulus\n"
},
{
"path": "src/index.js",
"chars": 87,
"preview": "export * from \"./core\"\nexport * from \"./multimap\"\nexport * from \"./mutation-observers\"\n"
},
{
"path": "src/index.ts",
"chars": 87,
"preview": "export * from \"./core\"\nexport * from \"./multimap\"\nexport * from \"./mutation-observers\"\n"
},
{
"path": "src/multimap/index.ts",
"chars": 95,
"preview": "export * from \"./indexed_multimap\"\nexport * from \"./multimap\"\nexport * from \"./set_operations\"\n"
},
{
"path": "src/multimap/indexed_multimap.ts",
"chars": 719,
"preview": "import { Multimap } from \"./multimap\"\nimport { add, del } from \"./set_operations\"\n\nexport class IndexedMultimap<K, V> ex"
},
{
"path": "src/multimap/multimap.ts",
"chars": 1330,
"preview": "import { add, del } from \"./set_operations\"\n\nexport class Multimap<K, V> {\n private valuesByKey: Map<K, Set<V>>\n\n cons"
},
{
"path": "src/multimap/set_operations.ts",
"chars": 561,
"preview": "export function add<K, V>(map: Map<K, Set<V>>, key: K, value: V) {\n fetch(map, key).add(value)\n}\n\nexport function del<K"
},
{
"path": "src/mutation-observers/attribute_observer.ts",
"chars": 2151,
"preview": "import { ElementObserver, ElementObserverDelegate } from \"./element_observer\"\n\nexport interface AttributeObserverDelegat"
},
{
"path": "src/mutation-observers/element_observer.ts",
"chars": 4638,
"preview": "export interface ElementObserverDelegate {\n matchElement(element: Element): boolean\n matchElementsInTree(tree: Element"
},
{
"path": "src/mutation-observers/index.ts",
"chars": 222,
"preview": "export * from \"./attribute_observer\"\nexport * from \"./element_observer\"\nexport * from \"./selector_observer\"\nexport * fro"
},
{
"path": "src/mutation-observers/selector_observer.ts",
"chars": 3437,
"preview": "import { ElementObserver, ElementObserverDelegate } from \"./element_observer\"\nimport { Multimap } from \"../multimap\"\n\nex"
},
{
"path": "src/mutation-observers/string_map_observer.ts",
"chars": 3553,
"preview": "export interface StringMapObserverDelegate {\n getStringMapKeyForAttribute(attributeName: string): string | undefined\n "
},
{
"path": "src/mutation-observers/token_list_observer.ts",
"chars": 3694,
"preview": "import { AttributeObserver, AttributeObserverDelegate } from \"./attribute_observer\"\nimport { Multimap } from \"../multima"
},
{
"path": "src/mutation-observers/value_list_observer.ts",
"chars": 2655,
"preview": "import { Token, TokenListObserver, TokenListObserverDelegate } from \"./token_list_observer\"\n\nexport interface ValueListO"
},
{
"path": "src/tests/cases/application_test_case.ts",
"chars": 776,
"preview": "import { Application } from \"../../core/application\"\nimport { DOMTestCase } from \"./dom_test_case\"\nimport { Schema, defa"
},
{
"path": "src/tests/cases/controller_test_case.ts",
"chars": 1467,
"preview": "import { ApplicationTestCase } from \"./application_test_case\"\nimport { Constructor } from \"../../core/constructor\"\nimpor"
},
{
"path": "src/tests/cases/dom_test_case.ts",
"chars": 3776,
"preview": "import { TestCase } from \"./test_case\"\n\ninterface TriggerEventOptions {\n bubbles?: boolean\n setDefaultPrevented?: bool"
},
{
"path": "src/tests/cases/index.ts",
"chars": 219,
"preview": "export * from \"./application_test_case\"\nexport * from \"./controller_test_case\"\nexport * from \"./dom_test_case\"\nexport * "
},
{
"path": "src/tests/cases/log_controller_test_case.ts",
"chars": 1595,
"preview": "import { ControllerTestCase } from \"./controller_test_case\"\nimport { LogController, ActionLogEntry } from \"../controller"
},
{
"path": "src/tests/cases/observer_test_case.ts",
"chars": 578,
"preview": "import { DOMTestCase } from \"./dom_test_case\"\n\nexport interface Observer {\n start(): void\n stop(): void\n}\n\nexport clas"
},
{
"path": "src/tests/cases/test_case.ts",
"chars": 1828,
"preview": "export class TestCase {\n readonly assert: Assert\n\n static defineModule(moduleName: string = this.name, qUnit: QUnit = "
},
{
"path": "src/tests/controllers/class_controller.ts",
"chars": 562,
"preview": "import { Controller } from \"../../core/controller\"\n\nclass BaseClassController extends Controller {\n static classes = [\""
},
{
"path": "src/tests/controllers/default_value_controller.ts",
"chars": 2575,
"preview": "import { Controller } from \"../../core/controller\"\nimport { ValueDefinitionMap, ValueDescriptorMap } from \"../../core/va"
},
{
"path": "src/tests/controllers/log_controller.ts",
"chars": 1631,
"preview": "import { ActionEvent } from \"../../core/action_event\"\nimport { Controller } from \"../../core/controller\"\n\nexport type Ac"
},
{
"path": "src/tests/controllers/outlet_controller.ts",
"chars": 3310,
"preview": "import { Controller } from \"../../core/controller\"\n\nclass BaseOutletController extends Controller {\n static outlets = ["
},
{
"path": "src/tests/controllers/target_controller.ts",
"chars": 1673,
"preview": "import { Controller } from \"../../core/controller\"\n\nclass BaseTargetController extends Controller {\n static targets = ["
},
{
"path": "src/tests/controllers/value_controller.ts",
"chars": 1434,
"preview": "import { Controller } from \"../../core/controller\"\nimport { ValueDefinitionMap, ValueDescriptorMap } from \"../../core/va"
},
{
"path": "src/tests/fixtures/application_start/helpers.ts",
"chars": 597,
"preview": "import { Application, Controller } from \"../../../core\"\n\nexport function startApplication() {\n const startState = docum"
},
{
"path": "src/tests/fixtures/application_start/index.html",
"chars": 695,
"preview": "<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"utf-8\">\n <script src=\"/base/dist/tests/fixtures/application_start/index."
},
{
"path": "src/tests/fixtures/application_start/index.ts",
"chars": 163,
"preview": "import { startApplication } from \"./helpers\"\n\nstartApplication()\naddEventListener(\"DOMContentLoaded\", startApplication)\n"
},
{
"path": "src/tests/index.ts",
"chars": 187,
"preview": "const context = require.context(\"./modules\", true, /\\.js$/)\nconst modules = context.keys().map((key) => context(key).def"
},
{
"path": "src/tests/modules/core/action_click_filter_tests.ts",
"chars": 739,
"preview": "import { LogControllerTestCase } from \"../../cases/log_controller_test_case\"\n\nexport default class ActionClickFilterTest"
},
{
"path": "src/tests/modules/core/action_keyboard_filter_tests.ts",
"chars": 9170,
"preview": "import { TestApplication } from \"../../cases/application_test_case\"\nimport { LogControllerTestCase } from \"../../cases/l"
},
{
"path": "src/tests/modules/core/action_ordering_tests.ts",
"chars": 4241,
"preview": "import { LogControllerTestCase } from \"../../cases/log_controller_test_case\"\n\nexport default class ActionOrderingTests e"
},
{
"path": "src/tests/modules/core/action_params_case_insensitive_tests.ts",
"chars": 1981,
"preview": "import ActionParamsTests from \"./action_params_tests\"\n\nexport default class ActionParamsCaseInsensitiveTests extends Act"
},
{
"path": "src/tests/modules/core/action_params_tests.ts",
"chars": 3178,
"preview": "import { LogControllerTestCase } from \"../../cases/log_controller_test_case\"\n\nexport default class ActionParamsTests ext"
},
{
"path": "src/tests/modules/core/action_tests.ts",
"chars": 2500,
"preview": "import { LogControllerTestCase } from \"../../cases/log_controller_test_case\"\n\nexport default class ActionTests extends L"
},
{
"path": "src/tests/modules/core/action_timing_tests.ts",
"chars": 831,
"preview": "import { Controller } from \"../../../core/controller\"\nimport { ControllerTestCase } from \"../../cases/controller_test_ca"
},
{
"path": "src/tests/modules/core/application_start_tests.ts",
"chars": 1826,
"preview": "import { DOMTestCase } from \"../../cases\"\n\nexport default class ApplicationStartTests extends DOMTestCase {\n iframe!: H"
},
{
"path": "src/tests/modules/core/application_tests.ts",
"chars": 1823,
"preview": "import { ApplicationTestCase } from \"../../cases/application_test_case\"\nimport { LogController } from \"../../controllers"
},
{
"path": "src/tests/modules/core/class_tests.ts",
"chars": 1439,
"preview": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\nimport { ClassController } from \"../../controllers"
},
{
"path": "src/tests/modules/core/data_tests.ts",
"chars": 1304,
"preview": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\n\nexport default class DataTests extends Controller"
},
{
"path": "src/tests/modules/core/default_value_tests.ts",
"chars": 8343,
"preview": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\nimport { DefaultValueController } from \"../../cont"
},
{
"path": "src/tests/modules/core/error_handler_tests.ts",
"chars": 1969,
"preview": "import { Controller } from \"../../../core/controller\"\nimport { Application } from \"../../../core/application\"\nimport { C"
},
{
"path": "src/tests/modules/core/es6_tests.ts",
"chars": 1405,
"preview": "import { LogController } from \"../../controllers/log_controller\"\nimport { LogControllerTestCase } from \"../../cases/log_"
},
{
"path": "src/tests/modules/core/event_options_tests.ts",
"chars": 11205,
"preview": "import type { Controller } from \"src/core\"\nimport { LogControllerTestCase } from \"../../cases/log_controller_test_case\"\n"
},
{
"path": "src/tests/modules/core/extending_application_tests.ts",
"chars": 1749,
"preview": "import { Application } from \"../../../core/application\"\nimport { DOMTestCase } from \"../../cases/dom_test_case\"\nimport {"
},
{
"path": "src/tests/modules/core/legacy_target_tests.ts",
"chars": 3548,
"preview": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\nimport { TargetController } from \"../../controller"
},
{
"path": "src/tests/modules/core/lifecycle_tests.ts",
"chars": 1361,
"preview": "import { LogControllerTestCase } from \"../../cases/log_controller_test_case\"\n\nexport default class LifecycleTests extend"
},
{
"path": "src/tests/modules/core/loading_tests.ts",
"chars": 2670,
"preview": "import { ApplicationTestCase } from \"../../cases/application_test_case\"\nimport { LogController } from \"../../controllers"
},
{
"path": "src/tests/modules/core/memory_tests.ts",
"chars": 755,
"preview": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\n\nexport default class MemoryTests extends Controll"
},
{
"path": "src/tests/modules/core/outlet_order_tests.ts",
"chars": 1502,
"preview": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\nimport { OutletController } from \"../../controller"
},
{
"path": "src/tests/modules/core/outlet_tests.ts",
"chars": 16596,
"preview": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\nimport { OutletController } from \"../../controller"
},
{
"path": "src/tests/modules/core/string_helpers_tests.ts",
"chars": 2684,
"preview": "import { TestCase } from \"../../cases/test_case\"\nimport * as helpers from \"../../../core/string_helpers\"\n\nexport default"
},
{
"path": "src/tests/modules/core/target_tests.ts",
"chars": 8677,
"preview": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\nimport { TargetController } from \"../../controller"
},
{
"path": "src/tests/modules/core/value_properties_tests.ts",
"chars": 9012,
"preview": "import { ValueController } from \"../../controllers/value_controller\"\nimport { ControllerTestCase } from \"../../cases/con"
},
{
"path": "src/tests/modules/core/value_tests.ts",
"chars": 8703,
"preview": "import { ControllerTestCase } from \"../../cases/controller_test_case\"\nimport { ValueController } from \"../../controllers"
},
{
"path": "src/tests/modules/mutation-observers/attribute_observer_tests.ts",
"chars": 3855,
"preview": "import { AttributeObserver, AttributeObserverDelegate } from \"../../../mutation-observers/attribute_observer\"\nimport { O"
},
{
"path": "src/tests/modules/mutation-observers/selector_observer_tests.ts",
"chars": 4154,
"preview": "import { SelectorObserver, SelectorObserverDelegate } from \"../../../mutation-observers/selector_observer\"\nimport { Obse"
},
{
"path": "src/tests/modules/mutation-observers/token_list_observer_tests.ts",
"chars": 2691,
"preview": "import { Token, TokenListObserver, TokenListObserverDelegate } from \"../../../mutation-observers/token_list_observer\"\nim"
},
{
"path": "src/tests/modules/mutation-observers/value_list_observer_tests.ts",
"chars": 3070,
"preview": "import { Token, ValueListObserver, ValueListObserverDelegate } from \"../../../mutation-observers\"\nimport { ObserverTestC"
},
{
"path": "tsconfig.json",
"chars": 425,
"preview": "{\n \"compilerOptions\": {\n \"lib\": [ \"dom\", \"dom.iterable\", \"es2015\", \"scripthost\" ],\n \"module\": \"es2015\",\n \"modu"
},
{
"path": "tsconfig.test.json",
"chars": 83,
"preview": "{\n \"extends\": \"./tsconfig.json\",\n \"compilerOptions\": {\n \"target\": \"es5\"\n }\n}\n"
}
]
About this extraction
This page contains the full source code of the hotwired/stimulus GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 163 files (379.6 KB), approximately 92.9k tokens, and a symbol index with 935 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.