Skip to content

Commit

Permalink
Merge branch 'main' into outlets
Browse files Browse the repository at this point in the history
  • Loading branch information
dhh committed Nov 17, 2022
2 parents f967595 + 2fdac1a commit 73b41a3
Show file tree
Hide file tree
Showing 76 changed files with 1,388 additions and 424 deletions.
2 changes: 2 additions & 0 deletions .eslintignore
@@ -0,0 +1,2 @@
dist/
node_modules/
29 changes: 29 additions & 0 deletions .eslintrc
@@ -0,0 +1,29 @@
{
"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
}
}]
}
}
25 changes: 12 additions & 13 deletions .github/workflows/ci.yml
Expand Up @@ -8,18 +8,17 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '12.4.0'
- uses: actions/cache@v2
with:
path: |
node_modules
*/*/node_modules
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 16
cache: 'yarn'

- run: yarn install
- name: Install Dependencies
run: yarn install --frozen-lockfile

- name: Test
run: yarn test
- name: Test
run: yarn test

- name: Lint
run: yarn lint
2 changes: 1 addition & 1 deletion .node-version
@@ -1 +1 @@
16.7.0
16.15.0
2 changes: 2 additions & 0 deletions .prettierignore
@@ -0,0 +1,2 @@
dist/
node_modules/
5 changes: 5 additions & 0 deletions .prettierrc.json
@@ -0,0 +1,5 @@
{
"singleQuote": false,
"printWidth": 120,
"semi": false
}
13 changes: 12 additions & 1 deletion README.md
Expand Up @@ -44,16 +44,27 @@ You can use Stimulus with any asset packaging systems. And if you prefer no buil

See the [Installation Guide](https://stimulus.hotwired.dev/handbook/installing) for detailed instructions.

## Getting Help & Contributing Back
## 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](http://rubyonrails.org).
Expand Down
34 changes: 33 additions & 1 deletion docs/reference/controllers.md
Expand Up @@ -45,7 +45,7 @@ For example, this element has a controller which is an instance of the class def
<div data-controller="reference"></div>
```

The following is an example of how Stimulus will generate identifiers for controllers in it's require context:
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…
--------------------------------- | -----------------------
Expand Down Expand Up @@ -163,6 +163,38 @@ class UnloadableController extends ApplicationController {
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 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(".legacy-spinner-button").forEach((element) => {
element.setAttribute(controllerAttribute, identifier)
})
}

// called as soon as registered so DOM many 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. It gets called with 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:
Expand Down
13 changes: 12 additions & 1 deletion docs/reference/targets.md
Expand Up @@ -121,4 +121,15 @@ 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.
Always use camelCase to specify target names, since they map directly to properties on your controller:

```html
<span data-search-target="camelCase" />
<span data-search-target="do-not-do-this" />
```

```js
export default class extends Controller {
static targets = [ "camelCase" ]
}
```
18 changes: 13 additions & 5 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "@hotwired/stimulus",
"version": "3.1.0",
"version": "3.1.1",
"license": "MIT",
"description": "A modest JavaScript framework for the HTML you already have.",
"author": "Basecamp, LLC",
Expand Down Expand Up @@ -39,26 +39,34 @@
"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"
"test:watch": "yarn test --auto-watch --no-single-run",
"lint": "eslint . --ext .ts",
"format": "yarn lint --fix"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^13.0.0",
"@rollup/plugin-typescript": "^8.2.1",
"@rollup/plugin-typescript": "^8.5.0",
"@types/qunit": "^2.9.0",
"@types/webpack-env": "^1.14.0",
"@typescript-eslint/eslint-plugin": "^5.36.2",
"@typescript-eslint/parser": "^5.36.2",
"concurrently": "^6.2.1",
"eslint": "^8.23.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.2.1",
"karma": "^5.2.3",
"karma-chrome-launcher": "^3.1.0",
"karma-qunit": "^4.0.0",
"karma-sauce-launcher": "^4.3.6",
"karma-webpack": "^4.0.2",
"prettier": "^2.7.1",
"qunit": "^2.9.2",
"rimraf": "^3.0.2",
"rollup": "^2.53",
"rollup-plugin-terser": "^7.0.2",
"ts-loader": "^6.0.4",
"tslib": "^2.3.0",
"typescript": "^4.3.5",
"tslib": "^2.4.0",
"typescript": "^4.8.2",
"webpack": "^4.39.1"
}
}
4 changes: 2 additions & 2 deletions packages/stimulus/package.json
@@ -1,6 +1,6 @@
{
"name": "stimulus",
"version": "3.1.0",
"version": "3.1.1",
"description": "Stimulus JavaScript framework",
"repository": "https://stimulus.hotwired.dev",
"author": "Basecamp, LLC",
Expand Down Expand Up @@ -42,7 +42,7 @@
],
"license": "MIT",
"dependencies": {
"@hotwired/stimulus": "^3.1.0",
"@hotwired/stimulus": "^3.1.1",
"@hotwired/stimulus-webpack-helpers": "^1.0.0"
},
"devDependencies": {
Expand Down
8 changes: 4 additions & 4 deletions packages/stimulus/yarn.lock
Expand Up @@ -7,10 +7,10 @@
resolved "https://registry.yarnpkg.com/@hotwired/stimulus-webpack-helpers/-/stimulus-webpack-helpers-1.0.0.tgz#6bd7906a4a2b6e1cd8732203b60264f987bd1084"
integrity sha512-6oKDmJDSsV+zdlHnF485nneuekY/Zbl669wei4HIiwxUWHhVSU1XIVji4aj+Ws9AXghjTYBS8H5ralB97BVMDw==

"@hotwired/stimulus@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.0.0.tgz#45171e61417af60f0e546665c52fae5b67295cee"
integrity sha512-UFIuuf7GjKJoIYromuTmqfzT8gZ8eu5zIB5m2QoEsopymGeN7rfDSTRPyRfwHIqP0x+0vWo4O1LFozw+/sWXxg==
"@hotwired/stimulus@^3.1.1":
version "3.1.1"
resolved "https://registry.yarnpkg.com/@hotwired/stimulus/-/stimulus-3.1.1.tgz#652f08a8e1d5edcb407340e58818fcff463b5848"
integrity sha512-e0JpzIaYLsRRXevRDVs0yevabiCvieIWWCwh7VqVXjXM5AOHdjb7AjaKIj34zYFmY1N6HIRRfk915WVMYlHnDA==

"@rollup/plugin-node-resolve@^13.0.0":
version "13.0.5"
Expand Down
32 changes: 16 additions & 16 deletions src/core/action.ts
Expand Up @@ -15,13 +15,13 @@ export class Action {
}

constructor(element: Element, index: number, descriptor: Partial<ActionDescriptor>) {
this.element = element
this.index = index
this.eventTarget = descriptor.eventTarget || element
this.eventName = descriptor.eventName || getDefaultEventNameForElement(element) || error("missing event name")
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.identifier = descriptor.identifier || error("missing identifier")
this.methodName = descriptor.methodName || error("missing method name")
}

toString() {
Expand All @@ -30,14 +30,14 @@ export class Action {
}

get params() {
const params:{ [key: string]: any } = {}
const pattern = new RegExp(`^data-${this.identifier}-(.+)-param$`)
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)
params[camelize(key)] = typecast(value)
}
}
return params
Expand All @@ -49,13 +49,13 @@ export class Action {
}

const defaultEventNames: { [tagName: string]: (element: Element) => string } = {
"a": e => "click",
"button": e => "click",
"form": e => "submit",
"details": e => "toggle",
"input": e => e.getAttribute("type") == "submit" ? "click" : "input",
"select": e => "change",
"textarea": e => "input"
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 {
Expand Down
16 changes: 8 additions & 8 deletions src/core/action_descriptor.ts
Expand Up @@ -34,7 +34,7 @@ export const defaultActionDescriptorFilters: ActionDescriptorFilters = {
} else {
return true
}
}
},
}

// capture nos.: 12 23 4 43 1 5 56 7 768 9 98
Expand All @@ -44,11 +44,11 @@ export function parseActionDescriptorString(descriptorString: string): Partial<A
const source = descriptorString.trim()
const matches = source.match(descriptorPattern) || []
return {
eventTarget: parseEventTarget(matches[4]),
eventName: matches[2],
eventTarget: parseEventTarget(matches[4]),
eventName: matches[2],
eventOptions: matches[9] ? parseEventOptions(matches[9]) : {},
identifier: matches[5],
methodName: matches[7]
identifier: matches[5],
methodName: matches[7],
}
}

Expand All @@ -61,9 +61,9 @@ function parseEventTarget(eventTargetName: string): EventTarget | undefined {
}

function parseEventOptions(eventOptions: string): AddEventListenerOptions {
return eventOptions.split(":").reduce((options, token) =>
Object.assign(options, { [token.replace(/^!/, "")]: !/^!/.test(token) })
, {})
return eventOptions
.split(":")
.reduce((options, token) => Object.assign(options, { [token.replace(/^!/, "")]: !/^!/.test(token) }), {})
}

export function stringifyEventTarget(eventTarget: EventTarget) {
Expand Down
10 changes: 5 additions & 5 deletions src/core/application.ts
Expand Up @@ -14,7 +14,7 @@ export class Application implements ErrorHandler {
readonly router: Router
readonly actionDescriptorFilters: ActionDescriptorFilters
logger: Logger = console
debug: boolean = false
debug = false

static start(element?: Element, schema?: Schema): Application {
const application = new Application(element, schema)
Expand Down Expand Up @@ -57,7 +57,7 @@ export class Application implements ErrorHandler {
load(definitions: Definition[]): void
load(head: Definition | Definition[], ...rest: Definition[]) {
const definitions = Array.isArray(head) ? head : [head, ...rest]
definitions.forEach(definition => {
definitions.forEach((definition) => {
if ((definition.controllerConstructor as any).shouldLoad) {
this.router.loadDefinition(definition)
}
Expand All @@ -68,13 +68,13 @@ export class Application implements ErrorHandler {
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))
identifiers.forEach((identifier) => this.router.unloadIdentifier(identifier))
}

// Controllers

get controllers(): Controller[] {
return this.router.contexts.map(context => context.controller)
return this.router.contexts.map((context) => context.controller)
}

getControllerForElementAndIdentifier(element: Element, identifier: string): Controller | null {
Expand Down Expand Up @@ -108,7 +108,7 @@ export class Application implements ErrorHandler {
}

function domReady() {
return new Promise<void>(resolve => {
return new Promise<void>((resolve) => {
if (document.readyState == "loading") {
document.addEventListener("DOMContentLoaded", () => resolve())
} else {
Expand Down
2 changes: 1 addition & 1 deletion src/core/binding.ts
Expand Up @@ -72,7 +72,7 @@ export class Binding {
const actionEvent: ActionEvent = Object.assign(event, { params })
this.method.call(this.controller, actionEvent)
this.context.logDebugActivity(this.methodName, { event, target, currentTarget, action: this.methodName })
} catch (error) {
} 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)
Expand Down

0 comments on commit 73b41a3

Please sign in to comment.