diff --git a/CHANGELOG.md b/CHANGELOG.md index d1a3bae..d307dc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning]. ## [Unreleased] +### Added + +- Installation script. ([@skryukov]) + +### Changed + +- [BREAKING] New API without controller inheritance. ([@skryukov]) + To migrate to the new API: + - Replace `new TurboMountReact()` (or any other framework specific constructor) with `new TurboMount()` + - Replace `turboMount.register(...)` with `registerComponent(turboMount, ...)` + - Replace `turbo_mount_react_component` (or any other framework specific helper) with `turbo_mount` + - Also see the new API for plugins and custom controllers in the README. + ## [0.2.3] - 2024-05-12 ### Added diff --git a/README.md b/README.md index c2d04b3..76c9b18 100644 --- a/README.md +++ b/README.md @@ -88,69 +88,35 @@ Note: Importmap-only mode is quite limited in terms of JavaScript dependencies. ### Initialization -To begin using `TurboMount`, start by initializing the library and registering the components you intend to use. Below are the steps to set up `TurboMount` with different configurations. - -#### Standard Initialization - -Import the necessary modules and initialize `TurboMount` with your application and the desired plugin. Here's how to set it up with a React plugin: - -```js -import { Application } from "@hotwired/stimulus"; -import { TurboMount } from "turbo-mount"; -import plugin from "turbo-mount/react"; -import { SketchPicker } from 'react-color'; - -const application = Application.start(); -const turboMount = new TurboMount({ application, plugin }); - -turboMount.register('SketchPicker', SketchPicker); -``` - -#### Simplified Initialization - -If you prefer not to specify the `application` explicitly, `TurboMount` can automatically detect or initialize it. This approach uses the `window.Stimulus` if available; otherwise, it initializes a new Stimulus application: +To begin using `TurboMount`, start by initializing the library and registering the components you intend to use. Here's how to set it up with a React plugin: ```js import { TurboMount } from "turbo-mount"; -import plugin from "turbo-mount/react"; -import { SketchPicker } from 'react-color'; +import { registerComponent } from "turbo-mount/react"; +import { HexColorPicker } from 'react-colorful'; -const turboMount = new TurboMount({ plugin }); +const turboMount = new TurboMount(); // or new TurboMount({ application }) -turboMount.register('SketchPicker', SketchPicker); +registerComponent(turboMount, "HexColorPicker", HexColorPicker); ``` -#### Plugin-Specific Initialization - -For a more streamlined setup, you can directly import a specialized version of `TurboMount`: - -```js -import { TurboMountReact } from "turbo-mount/react"; -import { SketchPicker } from 'react-color'; - -const turboMount = new TurboMountReact(); - -turboMount.register('SketchPicker', SketchPicker); -``` +If you prefer not to specify the `application` explicitly, `TurboMount` can automatically detect or initialize it. Turbo Mount uses the `window.Stimulus` if available; otherwise, it initializes a new Stimulus application. ### View Helpers Use the following helpers to mount components in your views: ```erb -<%= turbo_mount_component("SketchPicker", framework: "react", props: {color: "#034"}) %> - -<%# or using alias %> - -<%= turbo_mount_react_component("SketchPicker", props: {color: "#430"}) %> +<%= turbo_mount("HexColorPicker", props: {color: "#034"}, class: "mb-5") %> ``` This will generate the following HTML: ```html -
+
``` @@ -162,16 +128,16 @@ This will generate the following HTML: - Vue: `"turbo-mount/vue"` - Svelte: `"turbo-mount/svelte"` -To add support for other frameworks, create a custom controller class extending `TurboMountController` and provide a plugin. See included plugins for examples. +To add support for other frameworks, create a custom plugin. See included plugins for examples. ### Custom Controllers To customize component behavior or pass functions as props, create a custom controller: ```js -import { TurboMountReactController } from "turbo-mount"; +import { TurboMountController } from "turbo-mount"; -export default class extends TurboMountReactController { +export default class extends TurboMountController { get componentProps() { return { ...this.propsValue, @@ -180,17 +146,19 @@ export default class extends TurboMountReactController { } onChange = (color) => { - this.propsValue = { ...this.propsValue, color: color.hex }; + // same as this.propsValue = { ...this.propsValue, color }; + // but skips the rerendering of the component: + this.componentProps = { ...this.propsValue, color }; }; } ``` -Then pass this controller to the register method: +Then pass this controller to the `registerComponent` method: ```js -import SketchController from "controllers/turbo_mount/sketch_picker_controller"; +import HexColorPickerController from "controllers/turbo_mount/hex_color_picker_controller"; -turboMount.register('SketchPicker', SketchPicker, SketchController); +registerComponent(turboMount, "HexColorPicker", HexColorPicker, HexColorPickerController); ``` ### Vite Integration @@ -199,7 +167,7 @@ turboMount.register('SketchPicker', SketchPicker, SketchController); ```js import { TurboMount } from "turbo-mount/react"; -import { registerComponents } from "turbo-mount/vite"; +import { registerComponents } from "turbo-mount/registerComponents/react"; const controllers = import.meta.glob("./**/*_controller.js", { eager: true }); const components = import.meta.glob("/components/**/*.jsx", { eager: true }); @@ -209,9 +177,6 @@ registerComponents({ turboMount, components, controllers }); ``` The `registerComponents` helper searches for controllers in the following paths: -- `controllers/turbo-mount/${framework}/${controllerName}` -- `controllers/turbo-mount/${framework}-${controllerName}` -- `controllers/turbo-mount-${framework}-${controllerName}` - `controllers/turbo-mount/${controllerName}` - `controllers/turbo-mount-${controllerName}` @@ -220,7 +185,7 @@ The `registerComponents` helper searches for controllers in the following paths: To specify a non-root mount target, use the `data-<%= controller_name %>-target="mount"` attribute: ```erb -<%= turbo_mount_react_component("SketchPicker", props: {color: "#430"}) do |controller_name| %> +<%= turbo_mount("HexColorPicker", props: {color: "#430"}) do |controller_name| %>

Color picker

-target="mount">
<% end %> diff --git a/app/assets/javascripts/turbo-mount.js b/app/assets/javascripts/turbo-mount.js index d32ff3c..f84f56c 100644 --- a/app/assets/javascripts/turbo-mount.js +++ b/app/assets/javascripts/turbo-mount.js @@ -1,6 +1,10 @@ import { Controller, Application } from '@hotwired/stimulus'; class TurboMountController extends Controller { + constructor() { + super(...arguments); + this.skipPropsChangeCallback = false; + } connect() { this._umountComponentCallback || (this._umountComponentCallback = this.mountComponent(this.mountElement, this.resolvedComponent, this.componentProps)); } @@ -8,6 +12,10 @@ class TurboMountController extends Controller { this.umountComponent(); } propsValueChanged() { + if (this.skipPropsChangeCallback) { + this.skipPropsChangeCallback = false; + return; + } this.umountComponent(); this._umountComponentCallback || (this._umountComponentCallback = this.mountComponent(this.mountElement, this.resolvedComponent, this.componentProps)); } @@ -18,15 +26,25 @@ class TurboMountController extends Controller { return this.hasMountTarget ? this.mountTarget : this.element; } get resolvedComponent() { - return this.resolveComponent(this.componentValue); + return this.resolveMounted(this.componentValue).component; + } + get resolvedPlugin() { + return this.resolveMounted(this.componentValue).plugin; } umountComponent() { this._umountComponentCallback && this._umountComponentCallback(); this._umountComponentCallback = undefined; } - resolveComponent(component) { + mountComponent(el, Component, props) { + return this.resolvedPlugin.mountComponent({ el, Component, props }); + } + resolveMounted(component) { const app = this.application; - return app.turboMount[this.framework].resolve(component); + return app.turboMount.resolve(component); + } + setComponentProps(props) { + this.skipPropsChangeCallback = true; + this.propsValue = props; } } TurboMountController.values = { @@ -40,34 +58,30 @@ const camelToKebabCase = (str) => { }; class TurboMount { - constructor({ application, plugin }) { - var _a; + constructor(props = {}) { this.components = new Map(); - this.application = this.findOrStartApplication(application); - this.framework = plugin.framework; - this.baseController = plugin.controller; - (_a = this.application).turboMount || (_a.turboMount = {}); - this.application.turboMount[this.framework] = this; - if (this.baseController) { - this.application.register(`turbo-mount-${this.framework}`, this.baseController); - } + this.application = this.findOrStartApplication(props.application); + this.application.turboMount = this; + this.application.register("turbo-mount", TurboMountController); + document.addEventListener("turbo:before-morph-element", (event) => { + var _a; + const turboMorphEvent = event; + const { target, detail } = turboMorphEvent; + if ((_a = target.getAttribute("data-controller")) === null || _a === void 0 ? void 0 : _a.includes("turbo-mount")) { + target.setAttribute("data-turbo-mount-props-value", detail.newElement.getAttribute("data-turbo-mount-props-value") || + "{}"); + event.preventDefault(); + } + }); } - findOrStartApplication(hydratedApp) { - let application = hydratedApp || window.Stimulus; - if (!application) { - application = Application.start(); - window.Stimulus = application; - } - return application; - } - register(name, component, controller) { - controller || (controller = this.baseController); + register(plugin, name, component, controller) { + controller || (controller = TurboMountController); if (this.components.has(name)) { throw new Error(`Component '${name}' is already registered.`); } - this.components.set(name, component); + this.components.set(name, { component, plugin }); if (controller) { - const controllerName = `turbo-mount-${this.framework}-${camelToKebabCase(name)}`; + const controllerName = `turbo-mount-${camelToKebabCase(name)}`; this.application.register(controllerName, controller); } } @@ -78,6 +92,19 @@ class TurboMount { } return component; } + findOrStartApplication(hydratedApp) { + let application = hydratedApp || window.Stimulus; + if (!application) { + application = Application.start(); + window.Stimulus = application; + } + return application; + } +} +function buildRegisterFunction(plugin) { + return (turboMount, name, component, controller) => { + turboMount.register(plugin, name, component, controller); + }; } -export { TurboMount, TurboMountController }; +export { TurboMount, TurboMountController, buildRegisterFunction }; diff --git a/app/assets/javascripts/turbo-mount.min.js b/app/assets/javascripts/turbo-mount.min.js index 25fc46f..c5d7575 100644 --- a/app/assets/javascripts/turbo-mount.min.js +++ b/app/assets/javascripts/turbo-mount.min.js @@ -1,2 +1,2 @@ -import{Controller as t,Application as o}from"@hotwired/stimulus";class n extends t{connect(){this._umountComponentCallback||(this._umountComponentCallback=this.mountComponent(this.mountElement,this.resolvedComponent,this.componentProps))}disconnect(){this.umountComponent()}propsValueChanged(){this.umountComponent(),this._umountComponentCallback||(this._umountComponentCallback=this.mountComponent(this.mountElement,this.resolvedComponent,this.componentProps))}get componentProps(){return this.propsValue}get mountElement(){return this.hasMountTarget?this.mountTarget:this.element}get resolvedComponent(){return this.resolveComponent(this.componentValue)}umountComponent(){this._umountComponentCallback&&this._umountComponentCallback(),this._umountComponentCallback=void 0}resolveComponent(t){return this.application.turboMount[this.framework].resolve(t)}}n.values={props:Object,component:String},n.targets=["mount"];class e{constructor({application:t,plugin:o}){var n;this.components=new Map,this.application=this.findOrStartApplication(t),this.framework=o.framework,this.baseController=o.controller,(n=this.application).turboMount||(n.turboMount={}),this.application.turboMount[this.framework]=this,this.baseController&&this.application.register(`turbo-mount-${this.framework}`,this.baseController)}findOrStartApplication(t){let n=t||window.Stimulus;return n||(n=o.start(),window.Stimulus=n),n}register(t,o,n){if(n||(n=this.baseController),this.components.has(t))throw new Error(`Component '${t}' is already registered.`);if(this.components.set(t,o),n){const o=`turbo-mount-${this.framework}-${e=t,e.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}`;this.application.register(o,n)}var e}resolve(t){const o=this.components.get(t);if(!o)throw new Error(`Unknown component: ${t}`);return o}}export{e as TurboMount,n as TurboMountController}; +import{Controller as t,Application as o}from"@hotwired/stimulus";class n extends t{constructor(){super(...arguments),this.skipPropsChangeCallback=!1}connect(){this._umountComponentCallback||(this._umountComponentCallback=this.mountComponent(this.mountElement,this.resolvedComponent,this.componentProps))}disconnect(){this.umountComponent()}propsValueChanged(){this.skipPropsChangeCallback?this.skipPropsChangeCallback=!1:(this.umountComponent(),this._umountComponentCallback||(this._umountComponentCallback=this.mountComponent(this.mountElement,this.resolvedComponent,this.componentProps)))}get componentProps(){return this.propsValue}get mountElement(){return this.hasMountTarget?this.mountTarget:this.element}get resolvedComponent(){return this.resolveMounted(this.componentValue).component}get resolvedPlugin(){return this.resolveMounted(this.componentValue).plugin}umountComponent(){this._umountComponentCallback&&this._umountComponentCallback(),this._umountComponentCallback=void 0}mountComponent(t,o,n){return this.resolvedPlugin.mountComponent({el:t,Component:o,props:n})}resolveMounted(t){return this.application.turboMount.resolve(t)}setComponentProps(t){this.skipPropsChangeCallback=!0,this.propsValue=t}}n.values={props:Object,component:String},n.targets=["mount"];class e{constructor(t={}){this.components=new Map,this.application=this.findOrStartApplication(t.application),this.application.turboMount=this,this.application.register("turbo-mount",n),document.addEventListener("turbo:before-morph-element",(t=>{var o;const n=t,{target:e,detail:s}=n;(null===(o=e.getAttribute("data-controller"))||void 0===o?void 0:o.includes("turbo-mount"))&&(e.setAttribute("data-turbo-mount-props-value",s.newElement.getAttribute("data-turbo-mount-props-value")||"{}"),t.preventDefault())}))}register(t,o,e,s){if(s||(s=n),this.components.has(o))throw new Error(`Component '${o}' is already registered.`);if(this.components.set(o,{component:e,plugin:t}),s){const t=`turbo-mount-${r=o,r.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase()}`;this.application.register(t,s)}var r}resolve(t){const o=this.components.get(t);if(!o)throw new Error(`Unknown component: ${t}`);return o}findOrStartApplication(t){let n=t||window.Stimulus;return n||(n=o.start(),window.Stimulus=n),n}}function s(t){return(o,n,e,s)=>{o.register(t,n,e,s)}}export{e as TurboMount,n as TurboMountController,s as buildRegisterFunction}; //# sourceMappingURL=turbo-mount.min.js.map diff --git a/app/assets/javascripts/turbo-mount.min.js.map b/app/assets/javascripts/turbo-mount.min.js.map index f567b2d..4e09d65 100644 --- a/app/assets/javascripts/turbo-mount.min.js.map +++ b/app/assets/javascripts/turbo-mount.min.js.map @@ -1 +1 @@ -{"version":3,"file":"turbo-mount.min.js","sources":["../src/turbo-mount-controller.ts","../src/turbo-mount.ts","../src/helpers.ts"],"sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\nimport { ApplicationWithTurboMount } from \"./turbo-mount\";\n\nexport abstract class TurboMountController extends Controller {\n static values = {\n props: Object,\n component: String,\n };\n static targets = [\"mount\"];\n\n declare readonly propsValue: object;\n declare readonly componentValue: string;\n declare readonly hasMountTarget: boolean;\n declare readonly mountTarget: Element;\n\n abstract framework: string;\n\n abstract mountComponent(el: Element, Component: T, props: object): () => void;\n\n _umountComponentCallback?: () => void;\n\n connect() {\n this._umountComponentCallback ||= this.mountComponent(\n this.mountElement,\n this.resolvedComponent,\n this.componentProps,\n );\n }\n\n disconnect() {\n this.umountComponent();\n }\n\n propsValueChanged() {\n this.umountComponent();\n this._umountComponentCallback ||= this.mountComponent(\n this.mountElement,\n this.resolvedComponent,\n this.componentProps,\n );\n }\n\n get componentProps() {\n return this.propsValue;\n }\n\n get mountElement() {\n return this.hasMountTarget ? this.mountTarget : this.element;\n }\n\n get resolvedComponent() {\n return this.resolveComponent(this.componentValue);\n }\n\n umountComponent() {\n this._umountComponentCallback && this._umountComponentCallback();\n this._umountComponentCallback = undefined;\n }\n\n resolveComponent(component: string): T {\n const app = this.application as ApplicationWithTurboMount;\n return app.turboMount[this.framework].resolve(component);\n }\n}\n","import { Application, ControllerConstructor } from \"@hotwired/stimulus\";\n\nimport { camelToKebabCase } from \"./helpers\";\n\ndeclare global {\n interface Window {\n Stimulus?: Application;\n }\n}\n\nexport interface ApplicationWithTurboMount extends Application {\n turboMount: { [framework: string]: TurboMount };\n}\n\nexport type Plugin = {\n framework: string;\n controller: ControllerConstructor;\n};\n\nexport type TurboMountProps = {\n application?: Application;\n plugin: Plugin;\n};\n\nexport class TurboMount {\n components: Map;\n application: ApplicationWithTurboMount;\n framework: string;\n baseController?: ControllerConstructor;\n\n constructor({ application, plugin }: TurboMountProps) {\n this.components = new Map();\n this.application = this.findOrStartApplication(application);\n this.framework = plugin.framework;\n this.baseController = plugin.controller;\n\n this.application.turboMount ||= {};\n this.application.turboMount[this.framework] = this;\n\n if (this.baseController) {\n this.application.register(\n `turbo-mount-${this.framework}`,\n this.baseController,\n );\n }\n }\n\n private findOrStartApplication(hydratedApp?: Application) {\n let application = hydratedApp || window.Stimulus;\n\n if (!application) {\n application = Application.start();\n window.Stimulus = application;\n }\n return application as ApplicationWithTurboMount;\n }\n\n register(name: string, component: T, controller?: ControllerConstructor) {\n controller ||= this.baseController;\n if (this.components.has(name)) {\n throw new Error(`Component '${name}' is already registered.`);\n }\n this.components.set(name, component);\n\n if (controller) {\n const controllerName = `turbo-mount-${this.framework}-${camelToKebabCase(name)}`;\n this.application.register(controllerName, controller);\n }\n }\n\n resolve(name: string) {\n const component = this.components.get(name);\n if (!component) {\n throw new Error(`Unknown component: ${name}`);\n }\n return component;\n }\n}\n","export const camelToKebabCase = (str: string) => {\n return str.replace(/([a-z])([A-Z])/g, \"$1-$2\").toLowerCase();\n};\n"],"names":["TurboMountController","Controller","connect","this","_umountComponentCallback","mountComponent","mountElement","resolvedComponent","componentProps","disconnect","umountComponent","propsValueChanged","propsValue","hasMountTarget","mountTarget","element","resolveComponent","componentValue","undefined","component","application","turboMount","framework","resolve","values","props","Object","String","targets","TurboMount","constructor","plugin","components","Map","findOrStartApplication","baseController","controller","_a","register","hydratedApp","window","Stimulus","Application","start","name","has","Error","set","controllerName","str","replace","toLowerCase","get"],"mappings":"iEAGM,MAAgBA,UAAgCC,EAkBpD,OAAAC,GACEC,KAAKC,2BAALD,KAAKC,yBAA6BD,KAAKE,eACrCF,KAAKG,aACLH,KAAKI,kBACLJ,KAAKK,gBAER,CAED,UAAAC,GACEN,KAAKO,iBACN,CAED,iBAAAC,GACER,KAAKO,kBACLP,KAAKC,2BAALD,KAAKC,yBAA6BD,KAAKE,eACrCF,KAAKG,aACLH,KAAKI,kBACLJ,KAAKK,gBAER,CAED,kBAAIA,GACF,OAAOL,KAAKS,UACb,CAED,gBAAIN,GACF,OAAOH,KAAKU,eAAiBV,KAAKW,YAAcX,KAAKY,OACtD,CAED,qBAAIR,GACF,OAAOJ,KAAKa,iBAAiBb,KAAKc,eACnC,CAED,eAAAP,GACEP,KAAKC,0BAA4BD,KAAKC,2BACtCD,KAAKC,8BAA2Bc,CACjC,CAED,gBAAAF,CAAiBG,GAEf,OADYhB,KAAKiB,YACNC,WAAWlB,KAAKmB,WAAWC,QAAQJ,EAC/C,EA1DMnB,EAAAwB,OAAS,CACdC,MAAOC,OACPP,UAAWQ,QAEN3B,EAAA4B,QAAU,CAAC,eCgBPC,EAMX,WAAAC,EAAYV,YAAEA,EAAWW,OAAEA,UACzB5B,KAAK6B,WAAa,IAAIC,IACtB9B,KAAKiB,YAAcjB,KAAK+B,uBAAuBd,GAC/CjB,KAAKmB,UAAYS,EAAOT,UACxBnB,KAAKgC,eAAiBJ,EAAOK,YAE7BC,EAAAlC,KAAKiB,aAAYC,aAAAgB,EAAAhB,WAAe,CAAA,GAChClB,KAAKiB,YAAYC,WAAWlB,KAAKmB,WAAanB,KAE1CA,KAAKgC,gBACPhC,KAAKiB,YAAYkB,SACf,eAAenC,KAAKmB,YACpBnB,KAAKgC,eAGV,CAEO,sBAAAD,CAAuBK,GAC7B,IAAInB,EAAcmB,GAAeC,OAAOC,SAMxC,OAJKrB,IACHA,EAAcsB,EAAYC,QAC1BH,OAAOC,SAAWrB,GAEbA,CACR,CAED,QAAAkB,CAASM,EAAczB,EAAciB,GAEnC,GADAA,IAAAA,EAAejC,KAAKgC,gBAChBhC,KAAK6B,WAAWa,IAAID,GACtB,MAAM,IAAIE,MAAM,cAAcF,6BAIhC,GAFAzC,KAAK6B,WAAWe,IAAIH,EAAMzB,GAEtBiB,EAAY,CACd,MAAMY,EAAiB,eAAe7C,KAAKmB,aCjEhB2B,EDiE8CL,EChEtEK,EAAIC,QAAQ,kBAAmB,SAASC,gBDiE3ChD,KAAKiB,YAAYkB,SAASU,EAAgBZ,EAC3C,CCnE2B,IAACa,CDoE9B,CAED,OAAA1B,CAAQqB,GACN,MAAMzB,EAAYhB,KAAK6B,WAAWoB,IAAIR,GACtC,IAAKzB,EACH,MAAM,IAAI2B,MAAM,sBAAsBF,KAExC,OAAOzB,CACR"} \ No newline at end of file +{"version":3,"file":"turbo-mount.min.js","sources":["../src/turbo-mount-controller.ts","../src/turbo-mount.ts","../src/helpers.ts"],"sourcesContent":["import { Controller } from \"@hotwired/stimulus\";\nimport { ApplicationWithTurboMount } from \"./turbo-mount\";\n\nexport class TurboMountController extends Controller {\n static values = {\n props: Object,\n component: String,\n };\n static targets = [\"mount\"];\n\n private skipPropsChangeCallback = false;\n\n declare propsValue: object;\n declare componentValue: string;\n declare readonly hasMountTarget: boolean;\n declare readonly mountTarget: Element;\n\n _umountComponentCallback?: () => void;\n\n connect() {\n this._umountComponentCallback ||= this.mountComponent(\n this.mountElement,\n this.resolvedComponent,\n this.componentProps,\n );\n }\n\n disconnect() {\n this.umountComponent();\n }\n\n propsValueChanged() {\n // Prevent re-mounting the component if the props are being set by the component itself\n if (this.skipPropsChangeCallback) {\n this.skipPropsChangeCallback = false;\n return;\n }\n\n this.umountComponent();\n this._umountComponentCallback ||= this.mountComponent(\n this.mountElement,\n this.resolvedComponent,\n this.componentProps,\n );\n }\n\n get componentProps() {\n return this.propsValue;\n }\n\n get mountElement() {\n return this.hasMountTarget ? this.mountTarget : this.element;\n }\n\n get resolvedComponent() {\n return this.resolveMounted(this.componentValue).component;\n }\n\n get resolvedPlugin() {\n return this.resolveMounted(this.componentValue).plugin;\n }\n\n umountComponent() {\n this._umountComponentCallback && this._umountComponentCallback();\n this._umountComponentCallback = undefined;\n }\n\n mountComponent(el: Element, Component: unknown, props: object) {\n return this.resolvedPlugin.mountComponent({ el, Component, props });\n }\n\n resolveMounted(component: string) {\n const app = this.application as ApplicationWithTurboMount;\n return app.turboMount.resolve(component);\n }\n\n setComponentProps(props: object) {\n this.skipPropsChangeCallback = true;\n this.propsValue = props;\n }\n}\n","import { Application, ControllerConstructor } from \"@hotwired/stimulus\";\n\nimport { camelToKebabCase } from \"./helpers\";\nimport { TurboMountController } from \"./turbo-mount-controller\";\n\ndeclare global {\n interface Window {\n Stimulus?: Application;\n }\n}\n\nexport interface ApplicationWithTurboMount extends Application {\n turboMount: TurboMount;\n}\n\nexport type MountComponentProps = {\n el: Element;\n Component: T;\n props: object;\n};\n\nexport type Plugin = {\n mountComponent: (props: MountComponentProps) => () => void;\n};\n\nexport type TurboMountProps = {\n application?: Application;\n};\n\ntype TurboMountComponents = Map }>;\n\ninterface TurboMorphEvent extends CustomEvent {\n target: Element;\n detail: {\n newElement: Element;\n };\n}\n\nexport class TurboMount {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n components: TurboMountComponents;\n application: ApplicationWithTurboMount;\n\n constructor(props: TurboMountProps = {}) {\n this.components = new Map();\n this.application = this.findOrStartApplication(props.application);\n this.application.turboMount = this;\n this.application.register(\"turbo-mount\", TurboMountController);\n\n document.addEventListener(\"turbo:before-morph-element\", (event) => {\n const turboMorphEvent = event as unknown as TurboMorphEvent;\n const { target, detail } = turboMorphEvent;\n\n if (target.getAttribute(\"data-controller\")?.includes(\"turbo-mount\")) {\n target.setAttribute(\n \"data-turbo-mount-props-value\",\n detail.newElement.getAttribute(\"data-turbo-mount-props-value\") ||\n \"{}\",\n );\n event.preventDefault();\n }\n });\n }\n\n register(\n plugin: Plugin,\n name: string,\n component: T,\n controller?: ControllerConstructor,\n ) {\n controller ||= TurboMountController;\n if (this.components.has(name)) {\n throw new Error(`Component '${name}' is already registered.`);\n }\n this.components.set(name, { component, plugin });\n\n if (controller) {\n const controllerName = `turbo-mount-${camelToKebabCase(name)}`;\n this.application.register(controllerName, controller);\n }\n }\n\n resolve(name: string) {\n const component = this.components.get(name);\n if (!component) {\n throw new Error(`Unknown component: ${name}`);\n }\n return component;\n }\n\n private findOrStartApplication(hydratedApp?: Application) {\n let application = hydratedApp || window.Stimulus;\n\n if (!application) {\n application = Application.start();\n window.Stimulus = application;\n }\n return application as ApplicationWithTurboMount;\n }\n}\n\nexport function buildRegisterFunction(plugin: Plugin) {\n return (\n turboMount: TurboMount,\n name: string,\n component: T,\n controller?: ControllerConstructor,\n ) => {\n turboMount.register(plugin, name, component, controller);\n };\n}\n","export const camelToKebabCase = (str: string) => {\n return str.replace(/([a-z])([A-Z])/g, \"$1-$2\").toLowerCase();\n};\n"],"names":["TurboMountController","Controller","constructor","this","skipPropsChangeCallback","connect","_umountComponentCallback","mountComponent","mountElement","resolvedComponent","componentProps","disconnect","umountComponent","propsValueChanged","propsValue","hasMountTarget","mountTarget","element","resolveMounted","componentValue","component","resolvedPlugin","plugin","undefined","el","Component","props","application","turboMount","resolve","setComponentProps","values","Object","String","targets","TurboMount","components","Map","findOrStartApplication","register","document","addEventListener","event","turboMorphEvent","target","detail","_a","getAttribute","includes","setAttribute","newElement","preventDefault","name","controller","has","Error","set","controllerName","str","replace","toLowerCase","get","hydratedApp","window","Stimulus","Application","start","buildRegisterFunction"],"mappings":"iEAGM,MAAOA,UAA6BC,EAA1C,WAAAC,uBAOUC,KAAuBC,yBAAG,CAsEnC,CA7DC,OAAAC,GACEF,KAAKG,2BAALH,KAAKG,yBAA6BH,KAAKI,eACrCJ,KAAKK,aACLL,KAAKM,kBACLN,KAAKO,gBAER,CAED,UAAAC,GACER,KAAKS,iBACN,CAED,iBAAAC,GAEMV,KAAKC,wBACPD,KAAKC,yBAA0B,GAIjCD,KAAKS,kBACLT,KAAKG,2BAALH,KAAKG,yBAA6BH,KAAKI,eACrCJ,KAAKK,aACLL,KAAKM,kBACLN,KAAKO,iBAER,CAED,kBAAIA,GACF,OAAOP,KAAKW,UACb,CAED,gBAAIN,GACF,OAAOL,KAAKY,eAAiBZ,KAAKa,YAAcb,KAAKc,OACtD,CAED,qBAAIR,GACF,OAAON,KAAKe,eAAef,KAAKgB,gBAAgBC,SACjD,CAED,kBAAIC,GACF,OAAOlB,KAAKe,eAAef,KAAKgB,gBAAgBG,MACjD,CAED,eAAAV,GACET,KAAKG,0BAA4BH,KAAKG,2BACtCH,KAAKG,8BAA2BiB,CACjC,CAED,cAAAhB,CAAeiB,EAAaC,EAAoBC,GAC9C,OAAOvB,KAAKkB,eAAed,eAAe,CAAEiB,KAAIC,YAAWC,SAC5D,CAED,cAAAR,CAAeE,GAEb,OADYjB,KAAKwB,YACNC,WAAWC,QAAQT,EAC/B,CAED,iBAAAU,CAAkBJ,GAChBvB,KAAKC,yBAA0B,EAC/BD,KAAKW,WAAaY,CACnB,EA3EM1B,EAAA+B,OAAS,CACdL,MAAOM,OACPZ,UAAWa,QAENjC,EAAAkC,QAAU,CAAC,eC8BPC,EAKX,WAAAjC,CAAYwB,EAAyB,IACnCvB,KAAKiC,WAAa,IAAIC,IACtBlC,KAAKwB,YAAcxB,KAAKmC,uBAAuBZ,EAAMC,aACrDxB,KAAKwB,YAAYC,WAAazB,KAC9BA,KAAKwB,YAAYY,SAAS,cAAevC,GAEzCwC,SAASC,iBAAiB,8BAA+BC,UACvD,MAAMC,EAAkBD,GAClBE,OAAEA,EAAMC,OAAEA,GAAWF,GAEe,QAAtCG,EAAAF,EAAOG,aAAa,0BAAkB,IAAAD,OAAA,EAAAA,EAAEE,SAAS,kBACnDJ,EAAOK,aACL,+BACAJ,EAAOK,WAAWH,aAAa,iCAC7B,MAEJL,EAAMS,iBACP,GAEJ,CAED,QAAAZ,CACEjB,EACA8B,EACAhC,EACAiC,GAGA,GADAA,IAAAA,EAAerD,GACXG,KAAKiC,WAAWkB,IAAIF,GACtB,MAAM,IAAIG,MAAM,cAAcH,6BAIhC,GAFAjD,KAAKiC,WAAWoB,IAAIJ,EAAM,CAAEhC,YAAWE,WAEnC+B,EAAY,CACd,MAAMI,EAAiB,eC7EIC,ED6E4BN,EC5EpDM,EAAIC,QAAQ,kBAAmB,SAASC,gBD6E3CzD,KAAKwB,YAAYY,SAASkB,EAAgBJ,EAC3C,CC/E2B,IAACK,CDgF9B,CAED,OAAA7B,CAAQuB,GACN,MAAMhC,EAAYjB,KAAKiC,WAAWyB,IAAIT,GACtC,IAAKhC,EACH,MAAM,IAAImC,MAAM,sBAAsBH,KAExC,OAAOhC,CACR,CAEO,sBAAAkB,CAAuBwB,GAC7B,IAAInC,EAAcmC,GAAeC,OAAOC,SAMxC,OAJKrC,IACHA,EAAcsC,EAAYC,QAC1BH,OAAOC,SAAWrC,GAEbA,CACR,EAGG,SAAUwC,EAAyB7C,GACvC,MAAO,CACLM,EACAwB,EACAhC,EACAiC,KAEAzB,EAAWW,SAASjB,EAAQ8B,EAAMhC,EAAWiC,EAAW,CAE5D"} \ No newline at end of file diff --git a/app/assets/javascripts/turbo-mount/react.js b/app/assets/javascripts/turbo-mount/react.js index 390e6c7..7588ca6 100644 --- a/app/assets/javascripts/turbo-mount/react.js +++ b/app/assets/javascripts/turbo-mount/react.js @@ -1,29 +1,18 @@ -import { TurboMountController, TurboMount } from 'turbo-mount'; +import { buildRegisterFunction } from 'turbo-mount'; +export { TurboMount } from 'turbo-mount'; import { createElement } from 'react'; import { createRoot } from 'react-dom/client'; -class TurboMountReactController extends TurboMountController { - constructor() { - super(...arguments); - this.framework = "react"; - } - mountComponent(el, Component, props) { +const plugin = { + mountComponent: (mountProps) => { + const { el, Component, props } = mountProps; const root = createRoot(el); root.render(createElement(Component, props)); return () => { root.unmount(); }; - } -} - -const plugin = { - framework: "react", - controller: TurboMountReactController, + }, }; -class TurboMountReact extends TurboMount { - constructor(props) { - super(Object.assign(Object.assign({}, props), { plugin })); - } -} +const registerComponent = buildRegisterFunction(plugin); -export { TurboMountReact as TurboMount, TurboMountReact, plugin as default }; +export { plugin as default, registerComponent }; diff --git a/app/assets/javascripts/turbo-mount/react.min.js b/app/assets/javascripts/turbo-mount/react.min.js index 516c748..fa48a5b 100644 --- a/app/assets/javascripts/turbo-mount/react.min.js +++ b/app/assets/javascripts/turbo-mount/react.min.js @@ -1,2 +1,2 @@ -import{TurboMountController as r,TurboMount as t}from"turbo-mount";import{createElement as o}from"react";import{createRoot as e}from"react-dom/client";const n={framework:"react",controller:class extends r{constructor(){super(...arguments),this.framework="react"}mountComponent(r,t,n){const s=e(r);return s.render(o(t,n)),()=>{s.unmount()}}}};class s extends t{constructor(r){super(Object.assign(Object.assign({},r),{plugin:n}))}}export{s as TurboMount,s as TurboMountReact,n as default}; +import{buildRegisterFunction as o}from"turbo-mount";export{TurboMount}from"turbo-mount";import{createElement as t}from"react";import{createRoot as r}from"react-dom/client";const n={mountComponent:o=>{const{el:n,Component:m,props:e}=o,u=r(n);return u.render(t(m,e)),()=>{u.unmount()}}},m=o(n);export{n as default,m as registerComponent}; //# sourceMappingURL=react.min.js.map diff --git a/app/assets/javascripts/turbo-mount/react.min.js.map b/app/assets/javascripts/turbo-mount/react.min.js.map index 64ce64a..55e0d47 100644 --- a/app/assets/javascripts/turbo-mount/react.min.js.map +++ b/app/assets/javascripts/turbo-mount/react.min.js.map @@ -1 +1 @@ -{"version":3,"file":"react.min.js","sources":["../../src/plugins/react/index.ts","../../src/plugins/react/turbo-mount-react-controller.ts"],"sourcesContent":["import { Plugin, TurboMount, TurboMountProps } from \"turbo-mount\";\n\nimport { TurboMountReactController } from \"./turbo-mount-react-controller\";\n\nconst plugin: Plugin = {\n framework: \"react\",\n controller: TurboMountReactController,\n};\n\nexport class TurboMountReact extends TurboMount {\n constructor(props: Omit) {\n super({ ...props, plugin });\n }\n}\n\nexport { TurboMountReact as TurboMount };\n\nexport default plugin;\n","import { ComponentType, createElement } from \"react\";\nimport { createRoot } from \"react-dom/client\";\nimport { TurboMountController } from \"turbo-mount\";\n\nexport class TurboMountReactController extends TurboMountController {\n framework = \"react\";\n\n mountComponent(el: Element, Component: ComponentType, props: object) {\n const root = createRoot(el);\n root.render(createElement(Component, props));\n\n return () => {\n root.unmount();\n };\n }\n}\n"],"names":["plugin","framework","controller","TurboMountController","constructor","this","mountComponent","el","Component","props","root","createRoot","render","createElement","unmount","TurboMountReact","TurboMount","super","Object","assign"],"mappings":"uJAIA,MAAMA,EAAiB,CACrBC,UAAW,QACXC,WCFI,cAAyCC,EAA/C,WAAAC,uBACEC,KAASJ,UAAG,OAUb,CARC,cAAAK,CAAeC,EAAaC,EAA0BC,GACpD,MAAMC,EAAOC,EAAWJ,GAGxB,OAFAG,EAAKE,OAAOC,EAAcL,EAAWC,IAE9B,KACLC,EAAKI,SAAS,CAEjB,IDLG,MAAOC,UAA2BC,EACtC,WAAAZ,CAAYK,GACVQ,MAAWC,OAAAC,OAAAD,OAAAC,OAAA,GAAAV,GAAO,CAAAT,WACnB"} \ No newline at end of file +{"version":3,"file":"react.min.js","sources":["../../src/plugins/react/index.ts"],"sourcesContent":["import { buildRegisterFunction, Plugin, TurboMount } from \"turbo-mount\";\nimport { ComponentType, createElement } from \"react\";\nimport { createRoot } from \"react-dom/client\";\n\nconst plugin: Plugin = {\n mountComponent: (mountProps) => {\n const { el, Component, props } = mountProps;\n const root = createRoot(el);\n root.render(createElement(Component, props));\n\n return () => {\n root.unmount();\n };\n },\n};\n\nconst registerComponent = buildRegisterFunction(plugin);\n\nexport { TurboMount, registerComponent };\n\nexport default plugin;\n"],"names":["plugin","mountComponent","mountProps","el","Component","props","root","createRoot","render","createElement","unmount","registerComponent","buildRegisterFunction"],"mappings":"4KAIA,MAAMA,EAAgC,CACpCC,eAAiBC,IACf,MAAMC,GAAEA,EAAEC,UAAEA,EAASC,MAAEA,GAAUH,EAC3BI,EAAOC,EAAWJ,GAGxB,OAFAG,EAAKE,OAAOC,EAAcL,EAAWC,IAE9B,KACLC,EAAKI,SAAS,CACf,GAICC,EAAoBC,EAAsBZ"} \ No newline at end of file diff --git a/app/assets/javascripts/turbo-mount/svelte.js b/app/assets/javascripts/turbo-mount/svelte.js index 3f357fb..ce0bb4e 100644 --- a/app/assets/javascripts/turbo-mount/svelte.js +++ b/app/assets/javascripts/turbo-mount/svelte.js @@ -1,26 +1,15 @@ -import { TurboMountController, TurboMount } from 'turbo-mount'; +import { buildRegisterFunction } from 'turbo-mount'; +export { TurboMount } from 'turbo-mount'; -class TurboMountSvelteController extends TurboMountController { - constructor() { - super(...arguments); - this.framework = "svelte"; - } - mountComponent(el, Component, props) { +const plugin = { + mountComponent: (mountProps) => { + const { el, Component, props } = mountProps; const component = new Component({ target: el, props }); return () => { component.$destroy(); }; - } -} - -const plugin = { - framework: "svelte", - controller: TurboMountSvelteController, + }, }; -class TurboMountSvelte extends TurboMount { - constructor(props) { - super(Object.assign(Object.assign({}, props), { plugin })); - } -} +const registerComponent = buildRegisterFunction(plugin); -export { TurboMountSvelte as TurboMount, TurboMountSvelte, plugin as default }; +export { plugin as default, registerComponent }; diff --git a/app/assets/javascripts/turbo-mount/svelte.min.js b/app/assets/javascripts/turbo-mount/svelte.min.js index fc8fa73..d6bea7b 100644 --- a/app/assets/javascripts/turbo-mount/svelte.min.js +++ b/app/assets/javascripts/turbo-mount/svelte.min.js @@ -1,2 +1,2 @@ -import{TurboMountController as t,TurboMount as s}from"turbo-mount";const e={framework:"svelte",controller:class extends t{constructor(){super(...arguments),this.framework="svelte"}mountComponent(t,s,e){const o=new s({target:t,props:e});return()=>{o.$destroy()}}}};class o extends s{constructor(t){super(Object.assign(Object.assign({},t),{plugin:e}))}}export{o as TurboMount,o as TurboMountSvelte,e as default}; +import{buildRegisterFunction as o}from"turbo-mount";export{TurboMount}from"turbo-mount";const t={mountComponent:o=>{const{el:t,Component:r,props:n}=o,e=new r({target:t,props:n});return()=>{e.$destroy()}}},r=o(t);export{t as default,r as registerComponent}; //# sourceMappingURL=svelte.min.js.map diff --git a/app/assets/javascripts/turbo-mount/svelte.min.js.map b/app/assets/javascripts/turbo-mount/svelte.min.js.map index 3adaf68..bc60d2d 100644 --- a/app/assets/javascripts/turbo-mount/svelte.min.js.map +++ b/app/assets/javascripts/turbo-mount/svelte.min.js.map @@ -1 +1 @@ -{"version":3,"file":"svelte.min.js","sources":["../../src/plugins/svelte/index.ts","../../src/plugins/svelte/turbo-mount-svelte-controller.ts"],"sourcesContent":["import { Plugin, TurboMount, TurboMountProps } from \"turbo-mount\";\n\nimport { TurboMountSvelteController } from \"./turbo-mount-svelte-controller\";\n\nconst plugin: Plugin = {\n framework: \"svelte\",\n controller: TurboMountSvelteController,\n};\n\nexport class TurboMountSvelte extends TurboMount {\n constructor(props: Omit) {\n super({ ...props, plugin });\n }\n}\n\nexport { TurboMountSvelte as TurboMount };\nexport default plugin;\n","import { ComponentType } from \"svelte\";\nimport { TurboMountController } from \"turbo-mount\";\n\nexport class TurboMountSvelteController extends TurboMountController {\n framework = \"svelte\";\n\n mountComponent(el: Element, Component: ComponentType, props: object) {\n const component = new Component({ target: el, props });\n\n return () => {\n component.$destroy();\n };\n }\n}\n"],"names":["plugin","framework","controller","TurboMountController","constructor","this","mountComponent","el","Component","props","component","target","$destroy","TurboMountSvelte","TurboMount","super","Object","assign"],"mappings":"mEAIA,MAAMA,EAAiB,CACrBC,UAAW,SACXC,WCHI,cAA0CC,EAAhD,WAAAC,uBACEC,KAASJ,UAAG,QASb,CAPC,cAAAK,CAAeC,EAAaC,EAA0BC,GACpD,MAAMC,EAAY,IAAIF,EAAU,CAAEG,OAAQJ,EAAIE,UAE9C,MAAO,KACLC,EAAUE,UAAU,CAEvB,IDHG,MAAOC,UAA4BC,EACvC,WAAAV,CAAYK,GACVM,MAAWC,OAAAC,OAAAD,OAAAC,OAAA,GAAAR,GAAO,CAAAT,WACnB"} \ No newline at end of file +{"version":3,"file":"svelte.min.js","sources":["../../src/plugins/svelte/index.ts"],"sourcesContent":["import { buildRegisterFunction, Plugin, TurboMount } from \"turbo-mount\";\nimport { ComponentType } from \"svelte\";\n\nconst plugin: Plugin = {\n mountComponent: (mountProps) => {\n const { el, Component, props } = mountProps;\n const component = new Component({ target: el, props });\n\n return () => {\n component.$destroy();\n };\n },\n};\n\nconst registerComponent = buildRegisterFunction(plugin);\n\nexport { TurboMount, registerComponent };\n\nexport default plugin;\n"],"names":["plugin","mountComponent","mountProps","el","Component","props","component","target","$destroy","registerComponent","buildRegisterFunction"],"mappings":"wFAGA,MAAMA,EAAgC,CACpCC,eAAiBC,IACf,MAAMC,GAAEA,EAAEC,UAAEA,EAASC,MAAEA,GAAUH,EAC3BI,EAAY,IAAIF,EAAU,CAAEG,OAAQJ,EAAIE,UAE9C,MAAO,KACLC,EAAUE,UAAU,CACrB,GAICC,EAAoBC,EAAsBV"} \ No newline at end of file diff --git a/app/assets/javascripts/turbo-mount/vue.js b/app/assets/javascripts/turbo-mount/vue.js index 104c25e..d8eb055 100644 --- a/app/assets/javascripts/turbo-mount/vue.js +++ b/app/assets/javascripts/turbo-mount/vue.js @@ -1,28 +1,17 @@ -import { TurboMountController, TurboMount } from 'turbo-mount'; +import { buildRegisterFunction } from 'turbo-mount'; +export { TurboMount } from 'turbo-mount'; import { createApp } from 'vue'; -class TurboMountVueController extends TurboMountController { - constructor() { - super(...arguments); - this.framework = "vue"; - } - mountComponent(el, Component, props) { +const plugin = { + mountComponent: (mountProps) => { + const { el, Component, props } = mountProps; const app = createApp(Component, props); app.mount(el); return () => { app.unmount(); }; - } -} - -const plugin = { - framework: "vue", - controller: TurboMountVueController, + }, }; -class TurboMountVue extends TurboMount { - constructor(props) { - super(Object.assign(Object.assign({}, props), { plugin })); - } -} +const registerComponent = buildRegisterFunction(plugin); -export { TurboMountVue as TurboMount, TurboMountVue, plugin as default }; +export { plugin as default, registerComponent }; diff --git a/app/assets/javascripts/turbo-mount/vue.min.js b/app/assets/javascripts/turbo-mount/vue.min.js index 966f1a4..48a1777 100644 --- a/app/assets/javascripts/turbo-mount/vue.min.js +++ b/app/assets/javascripts/turbo-mount/vue.min.js @@ -1,2 +1,2 @@ -import{TurboMountController as o,TurboMount as t}from"turbo-mount";import{createApp as r}from"vue";const n={framework:"vue",controller:class extends o{constructor(){super(...arguments),this.framework="vue"}mountComponent(o,t,n){const s=r(t,n);return s.mount(o),()=>{s.unmount()}}}};class s extends t{constructor(o){super(Object.assign(Object.assign({},o),{plugin:n}))}}export{s as TurboMount,s as TurboMountVue,n as default}; +import{buildRegisterFunction as o}from"turbo-mount";export{TurboMount}from"turbo-mount";import{createApp as t}from"vue";const n={mountComponent:o=>{const{el:n,Component:r,props:u}=o,m=t(r,u);return m.mount(n),()=>{m.unmount()}}},r=o(n);export{n as default,r as registerComponent}; //# sourceMappingURL=vue.min.js.map diff --git a/app/assets/javascripts/turbo-mount/vue.min.js.map b/app/assets/javascripts/turbo-mount/vue.min.js.map index ebc9cf0..9cc6a29 100644 --- a/app/assets/javascripts/turbo-mount/vue.min.js.map +++ b/app/assets/javascripts/turbo-mount/vue.min.js.map @@ -1 +1 @@ -{"version":3,"file":"vue.min.js","sources":["../../src/plugins/vue/index.ts","../../src/plugins/vue/turbo-mount-vue-controller.ts"],"sourcesContent":["import { Plugin, TurboMount, TurboMountProps } from \"turbo-mount\";\n\nimport { TurboMountVueController } from \"./turbo-mount-vue-controller\";\n\nconst plugin: Plugin = {\n framework: \"vue\",\n controller: TurboMountVueController,\n};\n\nexport class TurboMountVue extends TurboMount {\n constructor(props: Omit) {\n super({ ...props, plugin });\n }\n}\n\nexport { TurboMountVue as TurboMount };\n\nexport default plugin;\n","import { createApp, App } from \"vue\";\nimport { TurboMountController } from \"turbo-mount\";\n\nexport class TurboMountVueController extends TurboMountController {\n framework = \"vue\";\n\n mountComponent(el: Element, Component: App, props: object) {\n const app = createApp(Component, props as Record);\n app.mount(el);\n\n return () => {\n app.unmount();\n };\n }\n}\n"],"names":["plugin","framework","controller","TurboMountController","constructor","this","mountComponent","el","Component","props","app","createApp","mount","unmount","TurboMountVue","TurboMount","super","Object","assign"],"mappings":"mGAIA,MAAMA,EAAiB,CACrBC,UAAW,MACXC,WCHI,cAAuCC,EAA7C,WAAAC,uBACEC,KAASJ,UAAG,KAUb,CARC,cAAAK,CAAeC,EAAaC,EAAgBC,GAC1C,MAAMC,EAAMC,EAAUH,EAAWC,GAGjC,OAFAC,EAAIE,MAAML,GAEH,KACLG,EAAIG,SAAS,CAEhB,IDJG,MAAOC,UAAyBC,EACpC,WAAAX,CAAYK,GACVO,MAAWC,OAAAC,OAAAD,OAAAC,OAAA,GAAAT,GAAO,CAAAT,WACnB"} \ No newline at end of file +{"version":3,"file":"vue.min.js","sources":["../../src/plugins/vue/index.ts"],"sourcesContent":["import { buildRegisterFunction, Plugin, TurboMount } from \"turbo-mount\";\nimport { App, createApp } from \"vue\";\n\nconst plugin: Plugin = {\n mountComponent: (mountProps) => {\n const { el, Component, props } = mountProps;\n const app = createApp(Component, props as Record);\n app.mount(el);\n\n return () => {\n app.unmount();\n };\n },\n};\n\nconst registerComponent = buildRegisterFunction(plugin);\n\nexport { TurboMount, registerComponent };\n\nexport default plugin;\n"],"names":["plugin","mountComponent","mountProps","el","Component","props","app","createApp","mount","unmount","registerComponent","buildRegisterFunction"],"mappings":"wHAGA,MAAMA,EAAsB,CAC1BC,eAAiBC,IACf,MAAMC,GAAEA,EAAEC,UAAEA,EAASC,MAAEA,GAAUH,EAC3BI,EAAMC,EAAUH,EAAWC,GAGjC,OAFAC,EAAIE,MAAML,GAEH,KACLG,EAAIG,SAAS,CACd,GAICC,EAAoBC,EAAsBX"} \ No newline at end of file diff --git a/lib/generators/turbo_mount/install/turbo-mount.js.tt b/lib/generators/turbo_mount/install/turbo-mount.js.tt index fcfc4cb..c04765a 100644 --- a/lib/generators/turbo_mount/install/turbo-mount.js.tt +++ b/lib/generators/turbo_mount/install/turbo-mount.js.tt @@ -1,15 +1,17 @@ -import { TurboMount<%= framework.capitalize %> } from "turbo-mount/<%= framework %>"; +import { TurboMount } from "turbo-mount"; +import { registerComponent } from "turbo-mount/<%= framework %>"; -const turboMount = new TurboMount<%= framework.capitalize %>(); +const turboMount = new TurboMount(); // to register a component use: -// turboMount.register("Hello", Hello); // where Hello is the imported the component +// registerComponent(turboMount, "Hello", Hello); // where Hello is the imported the component // to override the default controller use: -// turboMount.register("Hello", Hello, HelloController); // where HelloController is a Stimulus controller extended from TurboMount<%= framework.capitalize %>Controller +// registerComponent(turboMount, "Hello", Hello, HelloController); // where HelloController is a Stimulus controller extended from TurboMountController <%- if vite? -%> // If you want to automatically register components use: +// import { registerComponents } from "turbo-mount/registerComponents/<%= framework %>"; // const controllers = import.meta.glob("/controllers/**/*_controller.js", { eager: true }); // const components = import.meta.glob("/components/**/*.jsx", { eager: true }); // registerComponents({ turboMount, components, controllers }); diff --git a/lib/generators/turbo_mount/install_generator.rb b/lib/generators/turbo_mount/install_generator.rb index 2df7475..dbaacad 100644 --- a/lib/generators/turbo_mount/install_generator.rb +++ b/lib/generators/turbo_mount/install_generator.rb @@ -65,8 +65,8 @@ def install_importmap say "Pinning Turbo Mount to the importmap" append_to_file "config/importmap.rb", %(pin "turbo-mount", to: "turbo-mount.min.js"\n) - append_to_file "config/importmap.rb", %(pin "turbo-mount-initializer"\n) append_to_file "config/importmap.rb", %(pin "turbo-mount/#{framework}", to: "turbo-mount/#{framework}.min.js"\n) + append_to_file "config/importmap.rb", %(pin "turbo-mount-initializer"\n) say "Pinning framework dependencies to the importmap" run "bin/importmap pin #{FRAMEWORKS[framework][:pins]}" diff --git a/lib/turbo/mount/helpers.rb b/lib/turbo/mount/helpers.rb index 4726411..391840e 100644 --- a/lib/turbo/mount/helpers.rb +++ b/lib/turbo/mount/helpers.rb @@ -3,10 +3,10 @@ module Turbo module Mount module Helpers - def turbo_mount_component(component_name, framework:, props: {}, tag: "div", **attrs, &block) + def turbo_mount(component_name, props: {}, tag: "div", **attrs, &block) raise TypeError, "Component name expected" unless component_name.is_a? String - controller_name = "turbo-mount-#{framework}-#{component_name.underscore.dasherize}" + controller_name = "turbo-mount-#{component_name.underscore.dasherize}" attrs["data-controller"] = controller_name prefix = "data-#{controller_name}" attrs["#{prefix}-component-value"] = component_name @@ -16,18 +16,7 @@ def turbo_mount_component(component_name, framework:, props: {}, tag: "div", **a content_tag(tag, nil, attrs) { capture(controller_name, &block) } end - - def turbo_mount_react_component(component_name, **attrs, &block) - turbo_mount_component(component_name, framework: "react", **attrs, &block) - end - - def turbo_mount_svelte_component(component_name, **attrs, &block) - turbo_mount_component(component_name, framework: "svelte", **attrs, &block) - end - - def turbo_mount_vue_component(component_name, **attrs, &block) - turbo_mount_component(component_name, framework: "vue", **attrs, &block) - end + alias_method :turbo_mount_component, :turbo_mount end end end diff --git a/packages/turbo-mount/package.json b/packages/turbo-mount/package.json index c52e572..7b9c330 100644 --- a/packages/turbo-mount/package.json +++ b/packages/turbo-mount/package.json @@ -28,7 +28,10 @@ "./react": "./dist/plugins/react.js", "./svelte": "./dist/plugins/svelte.js", "./vue": "./dist/plugins/vue.js", - "./vite": "./dist/vite.js", + "./registerComponents/react": "./dist/registerComponents/react.js", + "./registerComponents/svelte": "./dist/registerComponents/svelte.js", + "./registerComponents/vue": "./dist/registerComponents/vue.js", + "./registerComponents": "./dist/registerComponents.js", ".": "./dist/turbo-mount.js" }, "files": [ diff --git a/packages/turbo-mount/rollup.config.js b/packages/turbo-mount/rollup.config.js index bc2cc2b..00f3192 100644 --- a/packages/turbo-mount/rollup.config.js +++ b/packages/turbo-mount/rollup.config.js @@ -4,13 +4,17 @@ import alias from '@rollup/plugin-alias'; import typescript from "@rollup/plugin-typescript"; import {terser} from "rollup-plugin-terser"; +const pluginsPath = path.resolve(__dirname, 'src', 'plugins'); + const external = [ "@hotwired/stimulus", "react", "react-dom/client", "vue", "turbo-mount", - "stimulus-vite-helpers" + "turbo-mount/registerComponents", + "stimulus-vite-helpers", + ...fs.readdirSync(pluginsPath).map(plugin => `turbo-mount/${plugin}`) ] const plugins = [ @@ -22,19 +26,25 @@ const plugins = [ }), ] -const pluginsPath = path.resolve(__dirname, 'src', 'plugins'); -const entrypoints = fs.readdirSync(pluginsPath).map(plugin => { - const input = path.join(pluginsPath, plugin, 'index.ts'); - const output = path.join('dist', 'plugins', `${plugin}.js`); - return {input, output} +const entrypoints = fs.readdirSync(pluginsPath).flatMap(plugin => { + return [ + { + input: path.join(pluginsPath, plugin, 'index.ts'), + output: path.join('dist', 'plugins', `${plugin}.js`) + }, + { + input: path.join(pluginsPath, plugin, 'registerComponents.ts'), + output: path.join('dist', 'registerComponents', `${plugin}.js`) + } + ] }); entrypoints.unshift({ input: path.join("src", "index.ts"), output: path.join("dist", "turbo-mount.js") -},{ - input: path.join("src", "vite.ts"), - output: path.join("dist", "vite.js") +}, { + input: path.join("src", "registerComponents.ts"), + output: path.join("dist", "registerComponents.js") }) const config = entrypoints.flatMap(({input, output}) => ( diff --git a/packages/turbo-mount/src/plugins/react/index.ts b/packages/turbo-mount/src/plugins/react/index.ts index 13d8c90..1d2693c 100644 --- a/packages/turbo-mount/src/plugins/react/index.ts +++ b/packages/turbo-mount/src/plugins/react/index.ts @@ -1,18 +1,21 @@ -import { Plugin, TurboMount, TurboMountProps } from "turbo-mount"; +import { buildRegisterFunction, Plugin, TurboMount } from "turbo-mount"; +import { ComponentType, createElement } from "react"; +import { createRoot } from "react-dom/client"; -import { TurboMountReactController } from "./turbo-mount-react-controller"; +const plugin: Plugin = { + mountComponent: (mountProps) => { + const { el, Component, props } = mountProps; + const root = createRoot(el); + root.render(createElement(Component, props)); -const plugin: Plugin = { - framework: "react", - controller: TurboMountReactController, + return () => { + root.unmount(); + }; + }, }; -export class TurboMountReact extends TurboMount { - constructor(props: Omit) { - super({ ...props, plugin }); - } -} +const registerComponent = buildRegisterFunction(plugin); -export { TurboMountReact as TurboMount }; +export { TurboMount, registerComponent }; export default plugin; diff --git a/packages/turbo-mount/src/plugins/react/registerComponents.ts b/packages/turbo-mount/src/plugins/react/registerComponents.ts new file mode 100644 index 0000000..1087464 --- /dev/null +++ b/packages/turbo-mount/src/plugins/react/registerComponents.ts @@ -0,0 +1,6 @@ +import { buildRegisterComponentsFunction } from "turbo-mount/registerComponents"; +import plugin from "turbo-mount/react"; + +const registerComponents = buildRegisterComponentsFunction(plugin); + +export { registerComponents }; diff --git a/packages/turbo-mount/src/plugins/react/turbo-mount-react-controller.ts b/packages/turbo-mount/src/plugins/react/turbo-mount-react-controller.ts deleted file mode 100644 index a83a63a..0000000 --- a/packages/turbo-mount/src/plugins/react/turbo-mount-react-controller.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { ComponentType, createElement } from "react"; -import { createRoot } from "react-dom/client"; -import { TurboMountController } from "turbo-mount"; - -export class TurboMountReactController extends TurboMountController { - framework = "react"; - - mountComponent(el: Element, Component: ComponentType, props: object) { - const root = createRoot(el); - root.render(createElement(Component, props)); - - return () => { - root.unmount(); - }; - } -} diff --git a/packages/turbo-mount/src/plugins/svelte/index.ts b/packages/turbo-mount/src/plugins/svelte/index.ts index ec91879..2e07f8b 100644 --- a/packages/turbo-mount/src/plugins/svelte/index.ts +++ b/packages/turbo-mount/src/plugins/svelte/index.ts @@ -1,17 +1,19 @@ -import { Plugin, TurboMount, TurboMountProps } from "turbo-mount"; +import { buildRegisterFunction, Plugin, TurboMount } from "turbo-mount"; +import { ComponentType } from "svelte"; -import { TurboMountSvelteController } from "./turbo-mount-svelte-controller"; +const plugin: Plugin = { + mountComponent: (mountProps) => { + const { el, Component, props } = mountProps; + const component = new Component({ target: el, props }); -const plugin: Plugin = { - framework: "svelte", - controller: TurboMountSvelteController, + return () => { + component.$destroy(); + }; + }, }; -export class TurboMountSvelte extends TurboMount { - constructor(props: Omit) { - super({ ...props, plugin }); - } -} +const registerComponent = buildRegisterFunction(plugin); + +export { TurboMount, registerComponent }; -export { TurboMountSvelte as TurboMount }; export default plugin; diff --git a/packages/turbo-mount/src/plugins/svelte/registerComponents.ts b/packages/turbo-mount/src/plugins/svelte/registerComponents.ts new file mode 100644 index 0000000..d4c243a --- /dev/null +++ b/packages/turbo-mount/src/plugins/svelte/registerComponents.ts @@ -0,0 +1,6 @@ +import { buildRegisterComponentsFunction } from "turbo-mount/registerComponents"; +import plugin from "turbo-mount/svelte"; + +const registerComponents = buildRegisterComponentsFunction(plugin); + +export { registerComponents }; diff --git a/packages/turbo-mount/src/plugins/svelte/turbo-mount-svelte-controller.ts b/packages/turbo-mount/src/plugins/svelte/turbo-mount-svelte-controller.ts deleted file mode 100644 index af14e1d..0000000 --- a/packages/turbo-mount/src/plugins/svelte/turbo-mount-svelte-controller.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ComponentType } from "svelte"; -import { TurboMountController } from "turbo-mount"; - -export class TurboMountSvelteController extends TurboMountController { - framework = "svelte"; - - mountComponent(el: Element, Component: ComponentType, props: object) { - const component = new Component({ target: el, props }); - - return () => { - component.$destroy(); - }; - } -} diff --git a/packages/turbo-mount/src/plugins/vue/index.ts b/packages/turbo-mount/src/plugins/vue/index.ts index 28112b4..efd152b 100644 --- a/packages/turbo-mount/src/plugins/vue/index.ts +++ b/packages/turbo-mount/src/plugins/vue/index.ts @@ -1,18 +1,20 @@ -import { Plugin, TurboMount, TurboMountProps } from "turbo-mount"; +import { buildRegisterFunction, Plugin, TurboMount } from "turbo-mount"; +import { App, createApp } from "vue"; -import { TurboMountVueController } from "./turbo-mount-vue-controller"; +const plugin: Plugin = { + mountComponent: (mountProps) => { + const { el, Component, props } = mountProps; + const app = createApp(Component, props as Record); + app.mount(el); -const plugin: Plugin = { - framework: "vue", - controller: TurboMountVueController, + return () => { + app.unmount(); + }; + }, }; -export class TurboMountVue extends TurboMount { - constructor(props: Omit) { - super({ ...props, plugin }); - } -} +const registerComponent = buildRegisterFunction(plugin); -export { TurboMountVue as TurboMount }; +export { TurboMount, registerComponent }; export default plugin; diff --git a/packages/turbo-mount/src/plugins/vue/registerComponents.ts b/packages/turbo-mount/src/plugins/vue/registerComponents.ts new file mode 100644 index 0000000..b71517a --- /dev/null +++ b/packages/turbo-mount/src/plugins/vue/registerComponents.ts @@ -0,0 +1,6 @@ +import { buildRegisterComponentsFunction } from "turbo-mount/registerComponents"; +import plugin from "turbo-mount/vue"; + +const registerComponents = buildRegisterComponentsFunction(plugin); + +export { registerComponents }; diff --git a/packages/turbo-mount/src/plugins/vue/turbo-mount-vue-controller.ts b/packages/turbo-mount/src/plugins/vue/turbo-mount-vue-controller.ts deleted file mode 100644 index d380717..0000000 --- a/packages/turbo-mount/src/plugins/vue/turbo-mount-vue-controller.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createApp, App } from "vue"; -import { TurboMountController } from "turbo-mount"; - -export class TurboMountVueController extends TurboMountController { - framework = "vue"; - - mountComponent(el: Element, Component: App, props: object) { - const app = createApp(Component, props as Record); - app.mount(el); - - return () => { - app.unmount(); - }; - } -} diff --git a/packages/turbo-mount/src/vite.ts b/packages/turbo-mount/src/registerComponents.ts similarity index 54% rename from packages/turbo-mount/src/vite.ts rename to packages/turbo-mount/src/registerComponents.ts index 30be994..db2e2a1 100644 --- a/packages/turbo-mount/src/vite.ts +++ b/packages/turbo-mount/src/registerComponents.ts @@ -1,36 +1,31 @@ import { definitionsFromGlob } from "stimulus-vite-helpers"; import { Definition } from "@hotwired/stimulus"; -import { TurboMount } from "turbo-mount"; +import { TurboMount, Plugin } from "turbo-mount"; import { camelToKebabCase } from "./helpers"; type ComponentModule = { default: never } | never; -type RegisterComponentsProps = { - turboMount: TurboMount; +type RegisterComponentsProps = { + plugin: Plugin; + turboMount: TurboMount; components: Record; controllers?: Record; }; -const identifierNames = (name: string, turboMount: TurboMount) => { +const identifierNames = (name: string) => { const controllerName = camelToKebabCase(name); - const framework = turboMount.framework; - - return [ - `turbo-mount--${framework}--${controllerName}`, - `turbo-mount--${framework}-${controllerName}`, - `turbo-mount-${framework}-${controllerName}`, - `turbo-mount--${controllerName}`, - `turbo-mount-${controllerName}`, - ]; + + return [`turbo-mount--${controllerName}`, `turbo-mount-${controllerName}`]; }; -export const registerComponents = ({ +export const registerComponents = ({ + plugin, turboMount, components, controllers, -}: RegisterComponentsProps) => { +}: RegisterComponentsProps) => { const controllerModules = controllers ? definitionsFromGlob(controllers) : []; for (const [componentPath, componentModule] of Object.entries(components)) { @@ -38,7 +33,7 @@ export const registerComponents = ({ .replace(/\.\w*$/, "") .replace(/^[./]*components\//, ""); - const identifiers = identifierNames(name, turboMount); + const identifiers = identifierNames(name); const controller = controllerModules.find(({ identifier }) => identifiers.includes(identifier), @@ -46,9 +41,19 @@ export const registerComponents = ({ const component = componentModule.default ?? componentModule; if (controller) { - turboMount.register(name, component, controller.controllerConstructor); + turboMount.register( + plugin, + name, + component, + controller.controllerConstructor, + ); } else { - turboMount.register(name, component); + turboMount.register(plugin, name, component); } } }; + +export const buildRegisterComponentsFunction = (plugin: Plugin) => { + return (props: Omit, "plugin">) => + registerComponents({ plugin, ...props }); +}; diff --git a/packages/turbo-mount/src/turbo-mount-controller.ts b/packages/turbo-mount/src/turbo-mount-controller.ts index 3f58d21..c2dcac7 100644 --- a/packages/turbo-mount/src/turbo-mount-controller.ts +++ b/packages/turbo-mount/src/turbo-mount-controller.ts @@ -1,22 +1,20 @@ import { Controller } from "@hotwired/stimulus"; import { ApplicationWithTurboMount } from "./turbo-mount"; -export abstract class TurboMountController extends Controller { +export class TurboMountController extends Controller { static values = { props: Object, component: String, }; static targets = ["mount"]; - declare readonly propsValue: object; - declare readonly componentValue: string; + private skipPropsChangeCallback = false; + + declare propsValue: object; + declare componentValue: string; declare readonly hasMountTarget: boolean; declare readonly mountTarget: Element; - abstract framework: string; - - abstract mountComponent(el: Element, Component: T, props: object): () => void; - _umountComponentCallback?: () => void; connect() { @@ -32,6 +30,12 @@ export abstract class TurboMountController extends Controller { } propsValueChanged() { + // Prevent re-mounting the component if the props are being set by the component itself + if (this.skipPropsChangeCallback) { + this.skipPropsChangeCallback = false; + return; + } + this.umountComponent(); this._umountComponentCallback ||= this.mountComponent( this.mountElement, @@ -49,7 +53,11 @@ export abstract class TurboMountController extends Controller { } get resolvedComponent() { - return this.resolveComponent(this.componentValue); + return this.resolveMounted(this.componentValue).component; + } + + get resolvedPlugin() { + return this.resolveMounted(this.componentValue).plugin; } umountComponent() { @@ -57,8 +65,17 @@ export abstract class TurboMountController extends Controller { this._umountComponentCallback = undefined; } - resolveComponent(component: string): T { - const app = this.application as ApplicationWithTurboMount; - return app.turboMount[this.framework].resolve(component); + mountComponent(el: Element, Component: unknown, props: object) { + return this.resolvedPlugin.mountComponent({ el, Component, props }); + } + + resolveMounted(component: string) { + const app = this.application as ApplicationWithTurboMount; + return app.turboMount.resolve(component); + } + + setComponentProps(props: object) { + this.skipPropsChangeCallback = true; + this.propsValue = props; } } diff --git a/packages/turbo-mount/src/turbo-mount.ts b/packages/turbo-mount/src/turbo-mount.ts index 8e3d86a..eb03971 100644 --- a/packages/turbo-mount/src/turbo-mount.ts +++ b/packages/turbo-mount/src/turbo-mount.ts @@ -1,6 +1,7 @@ import { Application, ControllerConstructor } from "@hotwired/stimulus"; import { camelToKebabCase } from "./helpers"; +import { TurboMountController } from "./turbo-mount-controller"; declare global { interface Window { @@ -8,62 +9,73 @@ declare global { } } -export interface ApplicationWithTurboMount extends Application { - turboMount: { [framework: string]: TurboMount }; +export interface ApplicationWithTurboMount extends Application { + turboMount: TurboMount; } -export type Plugin = { - framework: string; - controller: ControllerConstructor; +export type MountComponentProps = { + el: Element; + Component: T; + props: object; +}; + +export type Plugin = { + mountComponent: (props: MountComponentProps) => () => void; }; export type TurboMountProps = { application?: Application; - plugin: Plugin; }; -export class TurboMount { - components: Map; - application: ApplicationWithTurboMount; - framework: string; - baseController?: ControllerConstructor; +type TurboMountComponents = Map }>; + +interface TurboMorphEvent extends CustomEvent { + target: Element; + detail: { + newElement: Element; + }; +} + +export class TurboMount { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + components: TurboMountComponents; + application: ApplicationWithTurboMount; - constructor({ application, plugin }: TurboMountProps) { + constructor(props: TurboMountProps = {}) { this.components = new Map(); - this.application = this.findOrStartApplication(application); - this.framework = plugin.framework; - this.baseController = plugin.controller; - - this.application.turboMount ||= {}; - this.application.turboMount[this.framework] = this; - - if (this.baseController) { - this.application.register( - `turbo-mount-${this.framework}`, - this.baseController, - ); - } - } + this.application = this.findOrStartApplication(props.application); + this.application.turboMount = this; + this.application.register("turbo-mount", TurboMountController); - private findOrStartApplication(hydratedApp?: Application) { - let application = hydratedApp || window.Stimulus; + document.addEventListener("turbo:before-morph-element", (event) => { + const turboMorphEvent = event as unknown as TurboMorphEvent; + const { target, detail } = turboMorphEvent; - if (!application) { - application = Application.start(); - window.Stimulus = application; - } - return application as ApplicationWithTurboMount; + if (target.getAttribute("data-controller")?.includes("turbo-mount")) { + target.setAttribute( + "data-turbo-mount-props-value", + detail.newElement.getAttribute("data-turbo-mount-props-value") || + "{}", + ); + event.preventDefault(); + } + }); } - register(name: string, component: T, controller?: ControllerConstructor) { - controller ||= this.baseController; + register( + plugin: Plugin, + name: string, + component: T, + controller?: ControllerConstructor, + ) { + controller ||= TurboMountController; if (this.components.has(name)) { throw new Error(`Component '${name}' is already registered.`); } - this.components.set(name, component); + this.components.set(name, { component, plugin }); if (controller) { - const controllerName = `turbo-mount-${this.framework}-${camelToKebabCase(name)}`; + const controllerName = `turbo-mount-${camelToKebabCase(name)}`; this.application.register(controllerName, controller); } } @@ -75,4 +87,25 @@ export class TurboMount { } return component; } + + private findOrStartApplication(hydratedApp?: Application) { + let application = hydratedApp || window.Stimulus; + + if (!application) { + application = Application.start(); + window.Stimulus = application; + } + return application as ApplicationWithTurboMount; + } +} + +export function buildRegisterFunction(plugin: Plugin) { + return ( + turboMount: TurboMount, + name: string, + component: T, + controller?: ControllerConstructor, + ) => { + turboMount.register(plugin, name, component, controller); + }; } diff --git a/packages/turbo-mount/tsconfig.json b/packages/turbo-mount/tsconfig.json index fa960fe..60f7eb4 100644 --- a/packages/turbo-mount/tsconfig.json +++ b/packages/turbo-mount/tsconfig.json @@ -16,7 +16,9 @@ "noEmit": false, "declaration": false, "paths": { - "turbo-mount": ["src/index"] + "turbo-mount": ["src/index"], + "turbo-mount/registerComponents": ["src/registerComponents"], + "turbo-mount/*": ["src/plugins/*"] }, "types": [ "vite/client"