diff --git a/README.md b/README.md index 6431994..88fed24 100644 --- a/README.md +++ b/README.md @@ -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 @@ -32,9 +32,11 @@ Add the `transition` controller to each element you want to transition and add c ``` -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 { @@ -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 @@ -60,7 +75,7 @@ If you don't need one of the classes, you can omit the attributes. The following ``` ### 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
``` -### 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
- - + data-transition-leave-to="or-use multiple classes">
``` @@ -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 @@ -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. diff --git a/src/index.ts b/src/index.ts index 7749228..2a5645e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 { - (this.element as HTMLElement).hidden = false; await this.runTransition("enter"); this.dispatchEnd("enter"); this.startObserver(); } - async leave(): Promise { - (this.element as HTMLElement).hidden = false; + async leave(attribute?: string | null): Promise { + // 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 { - 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 { return new Promise((resolve) => { requestAnimationFrame(() => { @@ -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"], + }); + } }