Skip to content

Commit

Permalink
Sync doc updates
Browse files Browse the repository at this point in the history
  • Loading branch information
dhh committed Sep 4, 2023
1 parent 35f81d0 commit 9f54ee6
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 73 deletions.
10 changes: 5 additions & 5 deletions _source/handbook/00_the_origin_of_stimulus.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ order: 00

# The Origin of Stimulus

We write a lot of JavaScript at [37signals](https://37signals.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.
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://m.signalvnoise.com/the-majestic-monolith-29166d022228). 37signals 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.
This is the way of the [majestic monolith](https://m.signalvnoise.com/the-majestic-monolith-29166d022228). 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.

Expand Down Expand Up @@ -71,13 +71,13 @@ If, on the other hand, you have nagging sense that what you’re working on does

### Stimulus and related ideas were extracted from the wild

At 37signals 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.
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 37signals 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.
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 37signals, 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.
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.

Expand Down
45 changes: 24 additions & 21 deletions _source/reference/actions.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,20 +82,22 @@ 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
[a-z] | [a-z]
[0-9] | [0-9]
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.

Expand Down Expand Up @@ -129,7 +131,7 @@ The list of supported modifier keys is shown below.

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 modifer) in an action descriptor to install the event listener on `window` or `document`, respectively, as in the following example:
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">

Expand Down Expand Up @@ -213,12 +215,13 @@ 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
element | [Element]: The element where the action descriptor is declared
| 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
Expand Down
15 changes: 13 additions & 2 deletions _source/reference/controllers.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,13 +169,15 @@ If you want to trigger some behaviour once a controller has been registered you

```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(".legacy-spinner-button").forEach((element) => {
document.querySelector(this.legacySelector).forEach((element) => {
element.setAttribute(controllerAttribute, identifier)
})
}
Expand All @@ -193,7 +195,7 @@ class SpinnerButton extends 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.
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

Expand Down Expand Up @@ -229,6 +231,15 @@ class EffectsController extends Controller {
}
```

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
Expand Down
91 changes: 55 additions & 36 deletions _source/reference/outlets.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@ The use of Outlets helps with cross-controller communication and coordination as

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-search-result-outlet=".result"'>
<meta data-controller="callout" data-callout-text-value='class="result"'>
<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 data-controller="search" data-search-result-outlet=".result">
<div>
<div class="online-user" data-controller="user-status">...</div>
<div class="online-user" data-controller="user-status">...</div>
...
</div>

...

<div id="results">
<div class="result" data-controller="result">...</div>
<div class="result" data-controller="result">...</div>
<div data-controller="chat" data-chat-user-status-outlet=".online-user">
...
</div>
```
Expand All @@ -33,35 +33,36 @@ While a **target** is a specifically marked element **within the scope** of its

## Attributes and Names

The `data-search-result-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 `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.

```html
data-[identifier]-[outlet]-outlet="[selector]"
```

<meta data-controller="callout" data-callout-text-value='data-search-result-outlet=".result"'>
<meta data-controller="callout" data-callout-text-value='data-chat-user-status-outlet=".online-user"'>


```html
<div data-controller="search" data-search-result-outlet=".result"></div>
<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='"result"'>
<meta data-controller="callout" data-callout-text-value='"user-status"'>
<meta data-controller="callout" data-callout-text-value='userStatus'>


```js
// search_controller.js
// chat_controller.js

export default class extends Controller {
static outlets = [ "result" ]
static outlets = [ "user-status" ]

connect () {
this.resultOutlets.forEach(result => ...)
this.userStatusOutlets.forEach(status => ...)
}
}
```
Expand All @@ -78,44 +79,62 @@ For each outlet defined in the `static outlets` array, Stimulus adds five proper
| 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.resultOutlet.idValue
this.resultOutlet.imageTarget
this.resultOutlet.activeClasses
this.userStatusOutlet.idValue
this.userStatusOutlet.imageTarget
this.userStatusOutlet.activeClasses
```

You are also able to invoke any function the outlet controller may define:

```js
// result_controller.js
// user_status_controller.js

export default class extends Controller {
markAsSelected(event) {
// ...
}
}

// search_controller.js
// chat_controller.js

export default class extends Controller {
static outlets = [ "result" ]
static outlets = [ "user-status" ]

selectAll(event) {
this.resultOutlets.forEach(result => result.markAsSelected(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.resultOutletElement.dataset.value
this.resultOutletElement.getAttribute("id")
this.resultOutletElements.map(result => result.hasAttribute("selected"))
this.userStatusOutletElement.dataset.value
this.userStatusOutletElement.getAttribute("id")
this.userStatusOutletElements.map(status => status.hasAttribute("selected"))
```

## Outlet Callbacks
Expand All @@ -125,16 +144,16 @@ Outlet callbacks are specially named functions called by Stimulus to let you res
To observe outlet changes, define a function named `[name]OutletConnected()` or `[name]OutletDisconnected()`.

```js
// search_controller.js
// chat_controller.js

export default class extends Controller {
static outlets = [ "result" ]
static outlets = [ "user-status" ]

resultOutletConnected(outlet, element) {
userStatusOutletConnected(outlet, element) {
// ...
}

resultOutletDisconnected(outlet, element) {
userStatusOutletDisconnected(outlet, element) {
// ...
}
}
Expand All @@ -145,35 +164,35 @@ export default class extends Controller {
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 "result" for "search" controller
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.hasResultOutlet) {
this.resultOutlet.safelyCallSomethingOnTheOutlet()
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-search-result-outlet="#result"'>
<meta data-controller="callout" data-callout-text-value='id="result"'>
<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="search" data-search-result-outlet="#result"></div>
<div data-controller="chat" data-chat-user-status-outlet="#user-column"></div>

<div id="result"></div>
<div id="user-column"></div>
```

Would result in:
```html
Missing "data-controller=result" attribute on outlet element for
"search" controller`
Missing "data-controller=user-status" attribute on outlet element for
"chat" controller`
```
19 changes: 10 additions & 9 deletions _source/reference/values.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ You can read and write [HTML data attributes](https://developer.mozilla.org/en-U
<meta data-controller="callout" data-callout-text-value="data-loader-url-value=&quot;/messages&quot;">

```html
<div data-controller="loader"
data-loader-url-value="/messages">
<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">

Expand Down Expand Up @@ -53,13 +54,13 @@ export default class extends Controller {

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)`
Object | `JSON.stringify(object)` | `JSON.parse(value)`
String | Itself | Itself
| 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

Expand Down

1 comment on commit 9f54ee6

@kwhandy
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May I know the reason behind your action that loop-changing 37signals to Basecamp and so on? From legal view I thought the correct one should be 37signals instead Basecamp because 37signals LLC practically a company that owns Basecamp or is this marketing reasons behind?

Please sign in to comment.