Skip to content

Commit

Permalink
Add vite helper to register components via a glob
Browse files Browse the repository at this point in the history
  • Loading branch information
skryukov committed May 12, 2024
1 parent 99cd746 commit b473907
Show file tree
Hide file tree
Showing 8 changed files with 122 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning].
### Added

- Add a mount target to the base controller. ([@skryukov])
- Add `registerComponents` helper for vite. ([@skryukov])

## [0.2.2] - 2024-05-09

Expand Down
65 changes: 52 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,25 +16,25 @@ If bundler is not being used to manage dependencies, install the gem by executin

## Usage

First, you need to initialize `TurboMount` and register the components you want to use:
### Initialization

First, initialize `TurboMount` and register the components you wish to use:

```js
import { Application } from "@hotwired/stimulus"

const application = Application.start()

import { TurboMount } from "turbo-mount"
import plugin from "turbo-mount/react"
import { SketchPicker } from 'react-color'

// Initialize TurboMount and register the react stimulus controller
const application = Application.start();
const turboMount = new TurboMount({application, plugin});

// Register the components you want to use
import { SketchPicker } from 'react-color'
turboMount.register('SketchPicker', SketchPicker);
```

Now you can use view helpers to mount the components:
### View Helpers

Use the following helpers to mount components in your views:

```erb
<%= turbo_mount_component("SketchPicker", framework: "react", props: {color: "#034"}) %>
Expand All @@ -44,11 +44,21 @@ Now you can use view helpers to mount the components:
<%= turbo_mount_react_component("SketchPicker", props: {color: "#430"}) %>
```

In case you need to customize the component's behavior, or pass functions as props, you can create a custom controller:
### Supported Frameworks

```js
// javascript/controllers/turbo_mount_react_sketch_picker_controller.js
`TurboMount` supports the following frameworks:

- React `"turbo-mount/react"`
- Vue `"turbo-mount/vue"`
- Svelte `"turbo-mount/svelte"`

It's possible to add support for other frameworks by creating custom controller class extending `TurboMountController` and providing a 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"

export default class extends TurboMountReactController {
Expand All @@ -68,12 +78,41 @@ export default class extends TurboMountReactController {
Then pass this controller the register method:

```js
turboMount.register('SketchPicker', SketchPicker, TurboMountReactController);
import SketchController from "controllers/turbo_mount/sketch_picker_controller"

turboMount.register('SketchPicker', SketchPicker, SketchController);
```

### Vite Integration

`TurboMount` includes a `registerComponents` function that automates the loading of components. It also accepts an optional `controllers` property to autoload customized controllers:

```js
import { application } from "./application"
import { registerControllers } from "stimulus-vite-helpers";
import { TurboMount } from "turbo-mount";
import { registerComponents } from "turbo-mount/vite";
import plugin from "turbo-mount/react";

const controllers = import.meta.glob("./**/*_controller.js", { eager: true });
const components = import.meta.glob(`/components/**/*.jsx`, {eager: true});

registerControllers(application, controllers);

const turboMount = new TurboMount({application, plugin});
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}`

### Mount target

You can add a mount target, which will be used to mount the component:
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| %>
Expand Down
9 changes: 8 additions & 1 deletion packages/turbo-mount/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"./react": "./dist/plugins/react.js",
"./svelte": "./dist/plugins/svelte.js",
"./vue": "./dist/plugins/vue.js",
"./vite": "./dist/vite.js",
".": "./dist/turbo-mount.js"
},
"files": [
Expand All @@ -40,17 +41,20 @@
"react-dom": ">= 17.0",
"rollup": "^2.79.1",
"rollup-plugin-terser": "^7.0.2",
"stimulus-vite-helpers": ">= 3.0",
"svelte": ">= 3.0",
"tslib": "^2.6.2",
"typescript": "^5.4.5",
"vite": "^5.2.11",
"vue": ">= 3.0"
},
"dependencies": {
"@hotwired/stimulus": ">= 3.0"
"@hotwired/stimulus": "^3.2.2"
},
"peerDependencies": {
"react": ">= 17.0",
"react-dom": ">= 17.0",
"stimulus-vite-helpers": ">= 3.0",
"svelte": ">= 3.0",
"vue": ">= 3.0"
},
Expand All @@ -66,6 +70,9 @@
},
"vue": {
"optional": true
},
"stimulus-vite-helpers": {
"optional": true
}
}
}
6 changes: 5 additions & 1 deletion packages/turbo-mount/rollup.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ const external = [
"react",
"react-dom/client",
"vue",
'turbo-mount'
"turbo-mount",
"stimulus-vite-helpers"
]

const plugins = [
Expand All @@ -31,6 +32,9 @@ const entrypoints = fs.readdirSync(pluginsPath).map(plugin => {
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")
})

const config = entrypoints.flatMap(({input, output}) => (
Expand Down
3 changes: 3 additions & 0 deletions packages/turbo-mount/src/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const camelToKebabCase = (str: string) => {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
8 changes: 3 additions & 5 deletions packages/turbo-mount/src/turbo-mount.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {Application, ControllerConstructor} from '@hotwired/stimulus';

import {camelToKebabCase} from "./helpers";

export interface ApplicationWithTurboMount<T> extends Application {
turboMount: { [framework: string]: TurboMount<T> };
}
Expand Down Expand Up @@ -37,7 +39,7 @@ export class TurboMount<T> {
this.components.set(name, component);

if (controller) {
const controllerName = `turbo-mount-${this.framework}-${this.camelToKebabCase(name)}`;
const controllerName = `turbo-mount-${this.framework}-${camelToKebabCase(name)}`;
this.application.register(controllerName, controller);
}
}
Expand All @@ -49,8 +51,4 @@ export class TurboMount<T> {
}
return component;
}

camelToKebabCase(str: string) {
return str.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
}
}
45 changes: 45 additions & 0 deletions packages/turbo-mount/src/vite.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {definitionsFromGlob} from "stimulus-vite-helpers";
import {Definition} from "@hotwired/stimulus"

import {TurboMount} from "turbo-mount";

import {camelToKebabCase} from "./helpers";

type RegisterComponentsProps = {
turboMount: TurboMount<any>;
components: Record<string, any>;
controllers?: Record<string, Definition>;
}

const identifierNames = (name: string, turboMount: TurboMount<any>) => {
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}`
];
}

export const registerComponents = ({turboMount, components, controllers}: RegisterComponentsProps) => {
const controllerModules = controllers ? definitionsFromGlob(controllers) : [];

for (const [componentPath, componentModule] of Object.entries(components)) {
const name = componentPath.replace(/\.\w*$/, "")
.replace(/^[.\/]*components\//, '');

const identifiers = identifierNames(name, turboMount);

const controller = controllerModules.find(({identifier}) => identifiers.includes(identifier));
const component = componentModule.default ?? componentModule;

if (controller) {
turboMount.register(name, component, controller.controllerConstructor);
} else {
turboMount.register(name, component);
}
}
}
7 changes: 5 additions & 2 deletions packages/turbo-mount/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"lib": [
"es2015",
"es2017",
"dom"
],
"target": "es2017",
Expand All @@ -17,7 +17,10 @@
"declaration": false,
"paths": {
"turbo-mount": ["src/index"]
}
},
"types": [
"vite/client"
]
},
"exclude": [
"dist"
Expand Down

0 comments on commit b473907

Please sign in to comment.