Skip to content

Commit

Permalink
Introduce new API
Browse files Browse the repository at this point in the history
  • Loading branch information
skryukov committed May 31, 2024
1 parent a8afe2d commit 0f8e455
Show file tree
Hide file tree
Showing 32 changed files with 335 additions and 322 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
79 changes: 22 additions & 57 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
<div data-controller="turbo-mount-react-sketch-picker"
data-turbo-mount-react-sketch-picker-component-value="SketchPicker"
data-turbo-mount-react-sketch-picker-props-value="{&quot;color&quot;:&quot;#034&quot;}">
<div data-controller="turbo-mount-hex-color-picker"
data-turbo-mount-hex-color-picker-component-value="HexColorPicker"
data-turbo-mount-hex-color-picker-props-value="{&quot;color&quot;:&quot;#034&quot;}"
class="mb-5">
</div>
```

Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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 });
Expand All @@ -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}`

Expand All @@ -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| %>
<h3>Color picker</h3>
<div data-<%= controller_name %>-target="mount"></div>
<% end %>
Expand Down
79 changes: 53 additions & 26 deletions app/assets/javascripts/turbo-mount.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
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));
}
disconnect() {
this.umountComponent();
}
propsValueChanged() {
if (this.skipPropsChangeCallback) {
this.skipPropsChangeCallback = false;
return;
}
this.umountComponent();
this._umountComponentCallback || (this._umountComponentCallback = this.mountComponent(this.mountElement, this.resolvedComponent, this.componentProps));
}
Expand All @@ -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 = {
Expand All @@ -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);
}
}
Expand All @@ -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 };
2 changes: 1 addition & 1 deletion app/assets/javascripts/turbo-mount.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 0f8e455

Please sign in to comment.