Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add [key]Classes method to better handle multiple CSS classes #344

Merged
merged 8 commits into from
Mar 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions docs/reference/css_classes.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,14 +55,26 @@ Construct a CSS class attribute by joining together the controller identifier an

**Note:** CSS class attributes must be specified on the same element as the `data-controller` attribute.

<meta data-controller="callout" data-callout-text-value="data-search-loading-class=&quot;bg-gray-500 animate-spinner cursor-busy&quot;">

```html
<form data-controller="search"
data-search-loading-class="bg-gray-500 animate-spinner cursor-busy">
<input data-action="search#loadResults">
</form>
```

If you want to define multiple CSS classes for an attribute, separate the CSS class names with spaces.

## Properties

For each logical name defined in the `static classes` array, Stimulus adds the following _CSS class properties_ to your controller:

Name | Value
----------------------- | -----
`[logicalName]Class` | The value of the CSS class attribute corresponding to `logicalName`
`has[LogicalName]Class` | A boolean indicating whether or not the CSS class attribute is present
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 CSS class attributes, 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).

Expand All @@ -84,6 +96,22 @@ export default class extends Controller {

**Note:** Stimulus will throw an error if you attempt to access a CSS class property when no matching CSS class attribute is present.

If you want to use multiple classes, access the plural class property. You can apply the array of CSS classes to elements using [spread syntax](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax).

<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(/* … */)
}
}
```

## Naming Conventions

Use camelCase to specify logical names in CSS class definitions. Logical names map to camelCase CSS class properties:
Expand Down
8 changes: 7 additions & 1 deletion packages/@stimulus/core/src/class_map.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Scope } from "./scope"
import { tokenize } from "./string_helpers"

export class ClassMap {
readonly scope: Scope
Expand All @@ -12,7 +13,12 @@ export class ClassMap {
}

get(name: string) {
return this.data.get(this.getDataKey(name))
return this.getAll(name)[0]
}

getAll(name: string) {
const tokenString = this.data.get(this.getDataKey(name)) || ""
return tokenize(tokenString)
}

getAttributeName(name: string) {
Expand Down
12 changes: 8 additions & 4 deletions packages/@stimulus/core/src/class_properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,8 @@ export function ClassPropertiesBlessing<T>(constructor: Constructor<T>) {
}

function propertiesForClassDefinition(key: string) {
const name = `${key}Class`

return {
[name]: {
[`${key}Class`]: {
get(this: Controller) {
const { classes } = this
if (classes.has(key)) {
Expand All @@ -27,7 +25,13 @@ function propertiesForClassDefinition(key: string) {
}
},

[`has${capitalize(name)}`]: {
[`${key}Classes`]: {
get(this: Controller) {
return this.classes.getAll(key)
}
},

[`has${capitalize(key)}Class`]: {
get(this: Controller) {
return this.classes.has(key)
}
Expand Down
4 changes: 4 additions & 0 deletions packages/@stimulus/core/src/string_helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ export function capitalize(value: string) {
export function dasherize(value: string) {
return value.replace(/([A-Z])/g, (_, char) => `-${char.toLowerCase()}`)
}

export function tokenize(value: string) {
return value.trim().split(/\s+/).filter(content => content.length)
Copy link
Contributor

@tleish tleish Aug 31, 2021

Choose a reason for hiding this comment

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

A simpler (and slightly faster) method of doing this is

return value.match(/[^\s]+/g)

Copy link
Member

Choose a reason for hiding this comment

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

Please do add a PR 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

PR #430

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ class BaseClassController extends Controller {
static classes = [ "active" ]

readonly activeClass!: string
readonly activeClasses!: string[]
readonly hasActiveClass!: boolean
}

export class ClassController extends BaseClassController {
static classes = [ "enabled", "loading" ]
static classes = [ "enabled", "loading", "success" ]

readonly hasEnabledClass!: boolean
readonly enabledClass!: string
readonly enabledClasses!: string[]
readonly loadingClass!: string
readonly successClass!: string
readonly successClasses!: string[]
}
13 changes: 12 additions & 1 deletion packages/@stimulus/core/src/tests/modules/class_tests.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,37 @@
import { ControllerTestCase } from "../cases/controller_test_case"
import { ClassController } from "../controllers/class_controller"

export default class ValueTests extends ControllerTestCase(ClassController) {
export default class ClassTests extends ControllerTestCase(ClassController) {
fixtureHTML = `
<div data-controller="${this.identifier}"
data-${this.identifier}-active-class="test--active"
data-${this.identifier}-loading-class="busy"
data-${this.identifier}-success-class="bg-green-400 border border-green-600"
data-loading-class="xxx"
></div>
`

"test accessing a class property"() {
this.assert.ok(this.controller.hasActiveClass)
this.assert.equal(this.controller.activeClass, "test--active")
this.assert.deepEqual(this.controller.activeClasses, ["test--active"])
}

"test accessing a missing class property throws an error"() {
this.assert.notOk(this.controller.hasEnabledClass)
this.assert.raises(() => this.controller.enabledClass)
this.assert.equal(this.controller.enabledClasses.length, 0)
}

"test classes must be scoped by identifier"() {
this.assert.equal(this.controller.loadingClass, "busy")
}

"test multiple classes map to array"() {
this.assert.deepEqual(this.controller.successClasses, ["bg-green-400", "border", "border-green-600"])
}

"test accessing a class property returns first class if multiple classes are used"() {
this.assert.equal(this.controller.successClass, "bg-green-400");
}
}