turbo-mount 0.4.3 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7f554d4713fb45b0eac6e3cc156f9e02b2ffee4f1e8fb19a1286a284975d329
4
- data.tar.gz: 076d2e1a64a804337ab30e9e19a71c04055c1ecaccfd3ef929719edf3f2638cc
3
+ metadata.gz: f1db02f73a566e2e07ec73668df700b52938dddd4f176e0ef84e765cf30cbe08
4
+ data.tar.gz: ab6d5ae65b2607be3684b55dcdeec255102eb71b25dfa29b9db7f339832bee00
5
5
  SHA512:
6
- metadata.gz: 8be457646dd2d8f48186f6c8235732f76485b74f771772ea03c7a3c7b0f22d36337df48ebccf193aa161292d9f57f059911fde3bd2b24ab5b7887c22225e8775
7
- data.tar.gz: c0e8df4c8dbf721ea75cd1eadca9cee99289db710300938504e0177fe5a21b139dc95cfed7bd365d21d7bea7472c5dce64a2ab044323e14c9441f4bb07d6860a
6
+ metadata.gz: '0670329edb09b9ee26f64a4231d61e4c54dc013baebafa6b926799a3943f04ae895552e54bcda5111229ec9a9fb17dc256120408244a9d9db6b694781ba80c90'
7
+ data.tar.gz: 7f0b12fa188f611f441405de7ea831d5c8ed617289a842090ada0501200ddc763c25b374291cb45c6a586a51ca10793e3ebecae20fa7b553da04a82261c4b8f3
data/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning].
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.4] - 2025-12-25
11
+
12
+ ### Fixed
13
+
14
+ - Fix turbo morph handling with custom controller names ([@ibrahima])
15
+
10
16
  ## [0.4.3] - 2025-06-11
11
17
 
12
18
  ### Added
@@ -103,10 +109,12 @@ and this project adheres to [Semantic Versioning].
103
109
  - Initial implementation. ([@skryukov])
104
110
 
105
111
  [@benngarcia]: https://github.com/benngarcia
112
+ [@ibrahima]: https://github.com/ibrahima
106
113
  [@jkogara]: https://github.com/jkogara
107
114
  [@skryukov]: https://github.com/skryukov
108
115
 
109
- [Unreleased]: https://github.com/skryukov/turbo-mount/compare/v0.4.3...HEAD
116
+ [Unreleased]: https://github.com/skryukov/turbo-mount/compare/v0.4.4...HEAD
117
+ [0.4.4]: https://github.com/skryukov/turbo-mount/compare/v0.4.3...v0.4.4
110
118
  [0.4.3]: https://github.com/skryukov/turbo-mount/compare/v0.4.2...v0.4.3
111
119
  [0.4.2]: https://github.com/skryukov/turbo-mount/compare/v0.4.1...v0.4.2
112
120
  [0.4.1]: https://github.com/skryukov/turbo-mount/compare/v0.4.0...v0.4.1
data/README.md CHANGED
@@ -137,9 +137,9 @@ Use the following helpers to mount components in your views:
137
137
  This will generate the following HTML:
138
138
 
139
139
  ```html
140
- <div data-controller="turbo-mount"
141
- data-turbo-mount-component-value="HexColorPicker"
142
- data-turbo-mount-props-value="{&quot;color&quot;:&quot;#034&quot;}"
140
+ <div data-controller="turbo-mount-hex-color-picker"
141
+ data-turbo-mount-hex-color-picker-component-value="HexColorPicker"
142
+ data-turbo-mount-hex-color-picker-props-value="{&quot;color&quot;:&quot;#034&quot;}"
143
143
  class="mb-5">
144
144
  </div>
145
145
  ```
@@ -84,10 +84,17 @@ class TurboMount {
84
84
  document.addEventListener("turbo:before-morph-element", (event) => {
85
85
  const turboMorphEvent = event;
86
86
  const { target, detail } = turboMorphEvent;
87
- if (target.getAttribute("data-controller")?.includes("turbo-mount")) {
88
- target.setAttribute("data-turbo-mount-props-value", detail.newElement.getAttribute("data-turbo-mount-props-value") ||
89
- "{}");
90
- event.preventDefault();
87
+ const controllerAttr = target.getAttribute("data-controller");
88
+ if (controllerAttr?.includes("turbo-mount")) {
89
+ const turboMountController = controllerAttr
90
+ .split(/\s+/)
91
+ .find((name) => name.startsWith("turbo-mount"));
92
+ if (turboMountController) {
93
+ const propsAttrName = `data-${turboMountController}-props-value`;
94
+ const newPropsValue = detail.newElement.getAttribute(propsAttrName) || "{}";
95
+ target.setAttribute(propsAttrName, newPropsValue);
96
+ event.preventDefault();
97
+ }
91
98
  }
92
99
  });
93
100
  }
@@ -1,2 +1,2 @@
1
- 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=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"];const e=t=>t.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/_/g,"-").replace(/\//g,"--").toLowerCase(),r=t=>t.replace(/\.\w*$/,"").replace(/^[./]*components\//,""),s=t=>{if(t.endsWith("/index")){return t.replace(/\/index$/,"")||null}return null};class i{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=>{const o=t,{target:n,detail:e}=o;n.getAttribute("data-controller")?.includes("turbo-mount")&&(n.setAttribute("data-turbo-mount-props-value",e.newElement.getAttribute("data-turbo-mount-props-value")||"{}"),t.preventDefault())}))}register(t,o,r,s){if(s||(s=n),this.components.has(o))throw new Error(`Component '${o}' is already registered.`);if(this.components.set(o,{component:r,plugin:t}),s){const t=`turbo-mount-${e(o)}`;this.application.register(t,s)}}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 u(t){return(o,n,e,r)=>{o.register(t,n,e,r)}}const l=({plugin:t,turboMount:o,components:n,controllers:e=[]})=>{const i=new Set,u=[];for(const{filename:l,module:a}of n){const n=r(l),m=a.default??a;p({plugin:t,turboMount:o,availableControllers:e,componentName:n,component:m}),i.add(n);const c=s(n);c&&u.push({name:c,module:a})}for(const{name:n,module:r}of u)if(!i.has(n)){const s=r.default??r;p({plugin:t,turboMount:o,availableControllers:e,componentName:n,component:s}),i.add(n)}},p=({plugin:t,turboMount:o,availableControllers:n,componentName:r,component:s})=>{const i=(t=>{const o=e(t);return[`turbo-mount--${o}`,`turbo-mount-${o}`]})(r),u=n.find((({identifier:t})=>i.includes(t)));u?o.register(t,r,s,u.controllerConstructor):o.register(t,r,s)};export{i as TurboMount,n as TurboMountController,u as buildRegisterFunction,l as registerComponentsBase};
1
+ 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=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"];const e=t=>t.replace(/([a-z])([A-Z])/g,"$1-$2").replace(/_/g,"-").replace(/\//g,"--").toLowerCase(),r=t=>t.replace(/\.\w*$/,"").replace(/^[./]*components\//,""),s=t=>{if(t.endsWith("/index")){return t.replace(/\/index$/,"")||null}return null};class i{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=>{const o=t,{target:n,detail:e}=o,r=n.getAttribute("data-controller");if(r?.includes("turbo-mount")){const o=r.split(/\s+/).find(t=>t.startsWith("turbo-mount"));if(o){const r=`data-${o}-props-value`,s=e.newElement.getAttribute(r)||"{}";n.setAttribute(r,s),t.preventDefault()}}})}register(t,o,r,s){if(s||(s=n),this.components.has(o))throw new Error(`Component '${o}' is already registered.`);if(this.components.set(o,{component:r,plugin:t}),s){const t=`turbo-mount-${e(o)}`;this.application.register(t,s)}}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 u(t){return(o,n,e,r)=>{o.register(t,n,e,r)}}const l=({plugin:t,turboMount:o,components:n,controllers:e=[]})=>{const i=new Set,u=[];for(const{filename:l,module:a}of n){const n=r(l),m=a.default??a;p({plugin:t,turboMount:o,availableControllers:e,componentName:n,component:m}),i.add(n);const c=s(n);c&&u.push({name:c,module:a})}for(const{name:n,module:r}of u)if(!i.has(n)){const s=r.default??r;p({plugin:t,turboMount:o,availableControllers:e,componentName:n,component:s}),i.add(n)}},p=({plugin:t,turboMount:o,availableControllers:n,componentName:r,component:s})=>{const i=(t=>{const o=e(t);return[`turbo-mount--${o}`,`turbo-mount-${o}`]})(r),u=n.find(({identifier:t})=>i.includes(t));u?o.register(t,r,s,u.controllerConstructor):o.register(t,r,s)};export{i as TurboMount,n as TurboMountController,u as buildRegisterFunction,l as registerComponentsBase};
2
2
  //# sourceMappingURL=turbo-mount.min.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"turbo-mount.min.js","sources":["../src/turbo-mount-controller.ts","../src/helpers.ts","../src/turbo-mount.ts","../src/registerComponentsBase.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?.();\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","export const camelToKebabCase = (str: string) => {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1-$2\")\n .replace(/_/g, \"-\")\n .replace(/\\//g, \"--\")\n .toLowerCase();\n};\n\n// Normalizes a component filename into a standardized component name.\n// Example: './components/users/UserProfile.tsx' -> 'users/UserProfile'\n// Example: 'global/utility/debounce_button.js' -> 'global/utility/debounce_button'\nexport const normalizeFilenameToComponentName = (filename: string): string => {\n return filename.replace(/\\.\\w*$/, \"\").replace(/^[./]*components\\//, \"\");\n};\n\nexport const generateStimulusIdentifiers = (\n componentName: string,\n): string[] => {\n const kebabCaseName = camelToKebabCase(componentName);\n\n return [`turbo-mount--${kebabCaseName}`, `turbo-mount-${kebabCaseName}`];\n};\n\nexport const getShortNameForIndexComponent = (\n componentName: string,\n): string | null => {\n if (componentName.endsWith(\"/index\")) {\n const shortName = componentName.replace(/\\/index$/, \"\");\n return shortName || null;\n }\n return null;\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<T> = {\n el: Element;\n Component: T;\n props: object;\n};\n\nexport type Plugin<T> = {\n mountComponent: (props: MountComponentProps<T>) => () => void;\n};\n\nexport type TurboMountProps = {\n application?: Application;\n};\n\ntype TurboMountComponents<T> = Map<string, { component: T; plugin: Plugin<T> }>;\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<any>;\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<T>(\n plugin: Plugin<T>,\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<T>(plugin: Plugin<T>) {\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","import { Definition } from \"@hotwired/stimulus\";\n\nimport { TurboMount, Plugin } from \"./turbo-mount\";\nimport {\n getShortNameForIndexComponent,\n normalizeFilenameToComponentName,\n generateStimulusIdentifiers,\n} from \"./helpers\";\n\nexport type ComponentModule = { default: never } | never;\n\nexport type ComponentDefinition = {\n filename: string;\n module: ComponentModule;\n};\n\ntype RegisterComponentsProps<T> = {\n plugin: Plugin<T>;\n turboMount: TurboMount;\n components: ComponentDefinition[];\n controllers?: Definition[];\n};\n\n// Registers multiple components with TurboMount, potentially linking them\n// to Stimulus controllers based on naming conventions. Handles index\n// components by registering them under both their full path and the parent\n// directory name if available.\nexport const registerComponentsBase = <T>({\n plugin,\n turboMount,\n components,\n controllers = [],\n}: RegisterComponentsProps<T>) => {\n const registeredNames = new Set<string>();\n const indexComponentsToRegisterLater: Array<{\n name: string;\n module: ComponentModule;\n }> = [];\n\n for (const { filename, module } of components) {\n const componentName = normalizeFilenameToComponentName(filename);\n const component = module.default ?? module;\n\n registerSingleComponent({\n plugin,\n turboMount,\n availableControllers: controllers,\n componentName,\n component,\n });\n registeredNames.add(componentName);\n\n // If component path ends with /index, prepare for possible registration\n // under the shorter directory name in the second pass.\n const shortName = getShortNameForIndexComponent(componentName);\n if (shortName) {\n indexComponentsToRegisterLater.push({ name: shortName, module });\n }\n }\n\n // Second Pass: Register 'index' components using their shorter directory name\n // This pass ensures that an explicit component (e.g., 'button.js') takes\n // precedence over an index component (e.g., 'button/index.js') if both\n // would resolve to the same short name ('button').\n for (const { name: shortName, module } of indexComponentsToRegisterLater) {\n if (!registeredNames.has(shortName)) {\n const component = module.default ?? module;\n\n registerSingleComponent({\n plugin,\n turboMount,\n availableControllers: controllers,\n componentName: shortName,\n component,\n });\n registeredNames.add(shortName);\n }\n }\n};\n\nconst registerSingleComponent = <T>({\n plugin,\n turboMount,\n availableControllers,\n componentName,\n component,\n}: {\n plugin: Plugin<T>;\n turboMount: TurboMount;\n availableControllers: Definition[];\n componentName: string;\n component: T;\n}) => {\n const potentialIdentifiers = generateStimulusIdentifiers(componentName);\n\n const controllerDefinition = availableControllers.find(({ identifier }) =>\n potentialIdentifiers.includes(identifier),\n );\n\n if (controllerDefinition) {\n turboMount.register(\n plugin,\n componentName,\n component,\n controllerDefinition.controllerConstructor,\n );\n } else {\n turboMount.register(plugin, componentName, component);\n }\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","camelToKebabCase","str","replace","toLowerCase","normalizeFilenameToComponentName","filename","getShortNameForIndexComponent","componentName","endsWith","TurboMount","components","Map","findOrStartApplication","register","document","addEventListener","event","turboMorphEvent","target","detail","getAttribute","includes","setAttribute","newElement","preventDefault","name","controller","has","Error","set","controllerName","get","hydratedApp","window","Stimulus","Application","start","buildRegisterFunction","registerComponentsBase","controllers","registeredNames","Set","indexComponentsToRegisterLater","module","default","registerSingleComponent","availableControllers","add","shortName","push","potentialIdentifiers","kebabCaseName","generateStimulusIdentifiers","controllerDefinition","find","identifier","controllerConstructor"],"mappings":"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"}
1
+ {"version":3,"file":"turbo-mount.min.js","sources":["../src/turbo-mount-controller.ts","../src/helpers.ts","../src/turbo-mount.ts","../src/registerComponentsBase.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?.();\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","export const camelToKebabCase = (str: string) => {\n return str\n .replace(/([a-z])([A-Z])/g, \"$1-$2\")\n .replace(/_/g, \"-\")\n .replace(/\\//g, \"--\")\n .toLowerCase();\n};\n\n// Normalizes a component filename into a standardized component name.\n// Example: './components/users/UserProfile.tsx' -> 'users/UserProfile'\n// Example: 'global/utility/debounce_button.js' -> 'global/utility/debounce_button'\nexport const normalizeFilenameToComponentName = (filename: string): string => {\n return filename.replace(/\\.\\w*$/, \"\").replace(/^[./]*components\\//, \"\");\n};\n\nexport const generateStimulusIdentifiers = (\n componentName: string,\n): string[] => {\n const kebabCaseName = camelToKebabCase(componentName);\n\n return [`turbo-mount--${kebabCaseName}`, `turbo-mount-${kebabCaseName}`];\n};\n\nexport const getShortNameForIndexComponent = (\n componentName: string,\n): string | null => {\n if (componentName.endsWith(\"/index\")) {\n const shortName = componentName.replace(/\\/index$/, \"\");\n return shortName || null;\n }\n return null;\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<T> = {\n el: Element;\n Component: T;\n props: object;\n};\n\nexport type Plugin<T> = {\n mountComponent: (props: MountComponentProps<T>) => () => void;\n};\n\nexport type TurboMountProps = {\n application?: Application;\n};\n\ntype TurboMountComponents<T> = Map<string, { component: T; plugin: Plugin<T> }>;\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<any>;\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 const controllerAttr = target.getAttribute(\"data-controller\");\n if (controllerAttr?.includes(\"turbo-mount\")) {\n const turboMountController = controllerAttr\n .split(/\\s+/)\n .find((name) => name.startsWith(\"turbo-mount\"));\n\n if (turboMountController) {\n const propsAttrName = `data-${turboMountController}-props-value`;\n const newPropsValue =\n detail.newElement.getAttribute(propsAttrName) || \"{}\";\n target.setAttribute(propsAttrName, newPropsValue);\n event.preventDefault();\n }\n }\n });\n }\n\n register<T>(\n plugin: Plugin<T>,\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<T>(plugin: Plugin<T>) {\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","import { Definition } from \"@hotwired/stimulus\";\n\nimport { TurboMount, Plugin } from \"./turbo-mount\";\nimport {\n getShortNameForIndexComponent,\n normalizeFilenameToComponentName,\n generateStimulusIdentifiers,\n} from \"./helpers\";\n\nexport type ComponentModule = { default: never } | never;\n\nexport type ComponentDefinition = {\n filename: string;\n module: ComponentModule;\n};\n\ntype RegisterComponentsProps<T> = {\n plugin: Plugin<T>;\n turboMount: TurboMount;\n components: ComponentDefinition[];\n controllers?: Definition[];\n};\n\n// Registers multiple components with TurboMount, potentially linking them\n// to Stimulus controllers based on naming conventions. Handles index\n// components by registering them under both their full path and the parent\n// directory name if available.\nexport const registerComponentsBase = <T>({\n plugin,\n turboMount,\n components,\n controllers = [],\n}: RegisterComponentsProps<T>) => {\n const registeredNames = new Set<string>();\n const indexComponentsToRegisterLater: Array<{\n name: string;\n module: ComponentModule;\n }> = [];\n\n for (const { filename, module } of components) {\n const componentName = normalizeFilenameToComponentName(filename);\n const component = module.default ?? module;\n\n registerSingleComponent({\n plugin,\n turboMount,\n availableControllers: controllers,\n componentName,\n component,\n });\n registeredNames.add(componentName);\n\n // If component path ends with /index, prepare for possible registration\n // under the shorter directory name in the second pass.\n const shortName = getShortNameForIndexComponent(componentName);\n if (shortName) {\n indexComponentsToRegisterLater.push({ name: shortName, module });\n }\n }\n\n // Second Pass: Register 'index' components using their shorter directory name\n // This pass ensures that an explicit component (e.g., 'button.js') takes\n // precedence over an index component (e.g., 'button/index.js') if both\n // would resolve to the same short name ('button').\n for (const { name: shortName, module } of indexComponentsToRegisterLater) {\n if (!registeredNames.has(shortName)) {\n const component = module.default ?? module;\n\n registerSingleComponent({\n plugin,\n turboMount,\n availableControllers: controllers,\n componentName: shortName,\n component,\n });\n registeredNames.add(shortName);\n }\n }\n};\n\nconst registerSingleComponent = <T>({\n plugin,\n turboMount,\n availableControllers,\n componentName,\n component,\n}: {\n plugin: Plugin<T>;\n turboMount: TurboMount;\n availableControllers: Definition[];\n componentName: string;\n component: T;\n}) => {\n const potentialIdentifiers = generateStimulusIdentifiers(componentName);\n\n const controllerDefinition = availableControllers.find(({ identifier }) =>\n potentialIdentifiers.includes(identifier),\n );\n\n if (controllerDefinition) {\n turboMount.register(\n plugin,\n componentName,\n component,\n controllerDefinition.controllerConstructor,\n );\n } else {\n turboMount.register(plugin, componentName, component);\n }\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","camelToKebabCase","str","replace","toLowerCase","normalizeFilenameToComponentName","filename","getShortNameForIndexComponent","componentName","endsWith","TurboMount","components","Map","findOrStartApplication","register","document","addEventListener","event","turboMorphEvent","target","detail","controllerAttr","getAttribute","includes","turboMountController","split","find","name","startsWith","propsAttrName","newPropsValue","newElement","setAttribute","preventDefault","controller","has","Error","set","controllerName","get","hydratedApp","window","Stimulus","Application","start","buildRegisterFunction","registerComponentsBase","controllers","registeredNames","Set","indexComponentsToRegisterLater","module","default","registerSingleComponent","availableControllers","add","shortName","push","potentialIdentifiers","kebabCaseName","generateStimulusIdentifiers","controllerDefinition","identifier","controllerConstructor"],"mappings":"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"}
@@ -26,7 +26,7 @@ module TurboMount
26
26
  if importmap?
27
27
  "bin/importmap pin #{dependencies.join(" ")}"
28
28
  else
29
- "#{@package_manager} add #{dependencies.join(" ")}#{@generator.options[:verbose] ? "" : " --silent"}"
29
+ "#{@package_manager} add #{dependencies.join(" ")}#{" --silent" unless @generator.options[:verbose]}"
30
30
  end
31
31
  @generator.in_root { @generator.run cmd }
32
32
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Turbo
4
4
  module Mount
5
- VERSION = "0.4.3"
5
+ VERSION = "0.4.4"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: turbo-mount
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.3
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Svyatoslav Kryukov
@@ -82,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
82
82
  - !ruby/object:Gem::Version
83
83
  version: '0'
84
84
  requirements: []
85
- rubygems_version: 3.6.7
85
+ rubygems_version: 4.0.3
86
86
  specification_version: 4
87
87
  summary: Use React, Vue, Svelte, and other components with Hotwire.
88
88
  test_files: []