Skip to content

Commit

Permalink
Trigger transition based on change in computed style (#1)
Browse files Browse the repository at this point in the history
* Watch for a difference in computedStyle to trigger transitio

* Update README for new computedStyle

* Update README with new functioning of destroy

* Clarify that inspiration is only for the transition
  • Loading branch information
robbevp authored Apr 3, 2021
1 parent 5523b0c commit 2f60ced
Show file tree
Hide file tree
Showing 2 changed files with 80 additions and 38 deletions.
39 changes: 26 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Stimulus Transition

Enter/Leave transitions for Stimulus - based on the syntax from Vue and Alpine.
The controller watches for changes to the `hidden`-attribute to automatically run the transitions.
Enter/Leave transitions for Stimulus - based on the syntax from Vue and Alpine.
The controller watches for changes to computed display style to automatically run the transitions. This could be an added/removed class, a changed is the element's `style`-attribute or the `hidden`-attribute.

## Install

Expand Down Expand Up @@ -32,9 +32,11 @@ Add the `transition` controller to each element you want to transition and add c
</div>
```

The controller watch for changes to the `hidden`-attribute on the exact element. Add, remove or change the attribute to trigger the enter or leave transition.
The controller watch for changes to the computed display style on the exact element. You can trigger this by changing the classList, the element's style or with the `hidden`-attribute. If the change would cause the element to appear/disappear, the transition will run.

For example another controller might contain:
During the transition, the effect of your change will be canceled out and be reset afterwards. This controller will not change the display style itself.

All of the below should trigger a transition.

```javascript
export default class extends Controller {
Expand All @@ -47,8 +49,21 @@ export default class extends Controller {
hideOptions() {
this.optionsTarget.hidden = true;
}

addClass() {
this.optionsTarget.classList.add("hidden")
}

removeClass() {
this.optionsTarget.classList.add("hidden")
}

setDisplayNone() {
this.optionsTarget.style.setProperty("display", "none")
}
}
```

### Optional classes
If you don't need one of the classes, you can omit the attributes. The following will just transition on enter:
```HTML
Expand All @@ -60,7 +75,7 @@ If you don't need one of the classes, you can omit the attributes. The following
</div>
```
### Initial transition
If you want to run the transition when the element, you should add the `data-transition-initial-value`-attribute to the element. The value you enter is not used.
If you want to run the transition when the element in entered in the DOM, you should add the `data-transition-initial-value`-attribute to the element. The value you enter is not used.
```HTML
<div data-controller="transition"
data-transition-initial-value
Expand All @@ -70,20 +85,19 @@ If you want to run the transition when the element, you should add the `data-tra
<!-- content -->
</div>
```
### Manual triggers
### Destroy after leave

You can also destroy the element after running the leave transition by adding `data-transition-destroy-value`

You can also manually trigger the transitions, by calling `enter`, `leave`, `destroy` inside `data-action`
```HTML
<div data-controller="transition"
data-transition-destroy-value
data-transition-enter-active="enter-class"
data-transition-enter-from="enter-from-class"
data-transition-enter-to="enter-to-class"
data-transition-leave-active="or-use multiple classes"
data-transition-leave-from="or-use multiple classes"
data-transition-leave-to="or-use multiple classes"
data-action="click->transition#enter">
<button data-action="transition#leave">Run leave transition and hide element</button>
<button data-action="transition#destroy">Run leave transition and remove element from DOM</button>
data-transition-leave-to="or-use multiple classes">
</div>
```

Expand All @@ -92,7 +106,6 @@ You can also manually trigger the transitions, by calling `enter`, `leave`, `des
If you want to run another action after the transition is completed, you can listen for the following events on the element.
* `transition:end-enter`
* `transition:end-leave`
* `transition:end-destroy` (This actually runs right after `transition:end-leave` and right before destroying the element)

This would look something like:
```HTML
Expand All @@ -114,4 +127,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/robbev
This package is available as open source under the terms of the MIT License.

## Credits
This implementation is inspired by [the following article from Sebastian De Deyne](https://sebastiandedeyne.com/javascript-framework-diet/enter-leave-transitions/) - it's an interesting read to understand what is happening in these transitions.
This implementation of the transition is inspired by [the following article from Sebastian De Deyne](https://sebastiandedeyne.com/javascript-framework-diet/enter-leave-transitions/) - it's an interesting read to understand what is happening in these transitions.
79 changes: 54 additions & 25 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,46 @@
import { Controller } from "stimulus";

export default class extends Controller {
static values = { initial: String };
static values = { initial: Boolean, destroy: Boolean };
hasDestroyValue: boolean;
hasInitialValue: boolean;
observer: MutationObserver;
currentDisplayStyle: string;

initialize(): void {
this.observer = new MutationObserver(() => {
this.observer = new MutationObserver((mutations) => {
this.observer.disconnect();
this.triggerTransition.call(this);
this.verifyChange.call(this, mutations);
});
if (this.hasInitialValue) this.enter();
else this.startObserver();
}

triggerTransition(): void {
(this.element as HTMLElement).hidden ? this.leave() : this.enter();
}

async enter(): Promise<void> {
(this.element as HTMLElement).hidden = false;
await this.runTransition("enter");
this.dispatchEnd("enter");
this.startObserver();
}

async leave(): Promise<void> {
(this.element as HTMLElement).hidden = false;
async leave(attribute?: string | null): Promise<void> {
// Cancel out the display style
if (attribute === "hidden") (this.element as HTMLElement).hidden = false;
else this.displayStyle = this.currentDisplayStyle;

await this.runTransition("leave");
(this.element as HTMLElement).hidden = true;
this.dispatchEnd("leave");
this.startObserver();
}

async destroy(): Promise<void> {
await this.leave();
this.dispatchEnd("destroy");
this.element.remove();
}
// Restore the display style to previous value
if (attribute === "hidden") (this.element as HTMLElement).hidden = true;
else this.displayStyle = attribute === "style" ? "none" : undefined;

// Private functions
private startObserver(): void {
if (this.element.isConnected)
this.observer.observe(this.element, { attributeFilter: ["hidden"] });
this.dispatchEnd("leave");

// Destroy element, or restart observer
if (this.hasDestroyValue) this.element.remove();
else this.startObserver();
}

// Helpers for transition
private nextFrame(): Promise<number> {
return new Promise((resolve) => {
requestAnimationFrame(() => {
Expand Down Expand Up @@ -93,10 +89,43 @@ export default class extends Controller {
);
}

private dispatchEnd(name: "enter" | "leave" | "destroy") {
const type = `transition:end-${name}`;
private dispatchEnd(dir: "enter" | "leave") {
const type = `transition:end-${dir}`;
const event = new CustomEvent(type, { bubbles: true, cancelable: true });
this.element.dispatchEvent(event);
return event;
}

private get display(): string {
return getComputedStyle(this.element)["display"];
}

private set displayStyle(v: string | undefined) {
v
? (this.element as HTMLElement).style.setProperty("display", v)
: (this.element as HTMLElement).style.removeProperty("display");
}

// Helpers for observer
private verifyChange(mutations: MutationRecord[]): void {
const newDisplayStyle = this.display;

// Make sure there is a new computed displayStyle && the it was or will be "none"
if (
newDisplayStyle !== this.currentDisplayStyle &&
(newDisplayStyle === "none" || this.currentDisplayStyle === "none")
)
newDisplayStyle === "none"
? this.leave(mutations[0].attributeName)
: this.enter();
else this.startObserver();
}

private startObserver(): void {
this.currentDisplayStyle = this.display;
if (this.element.isConnected)
this.observer.observe(this.element, {
attributeFilter: ["class", "hidden", "style"],
});
}
}

0 comments on commit 2f60ced

Please sign in to comment.