chore(js): update coldwired and react

This commit is contained in:
Paul Chavard 2024-05-06 18:07:29 +02:00
parent 316a33f97b
commit 1e11ad4ce6
No known key found for this signature in database
24 changed files with 108 additions and 106 deletions

View file

@ -28,3 +28,7 @@ body {
.container { .container {
@extend %container; @extend %container;
} }
react-fragment {
display: block;
}

View file

@ -10,7 +10,7 @@
} }
} }
.form [data-react-component-value='MapEditor'] [data-reach-combobox-input] { .form react-fragment[data-component-name='MapEditor'] [data-reach-combobox-input] {
margin-bottom: 0; margin-bottom: 0;
} }

View file

@ -524,7 +524,7 @@
} }
} }
[data-react-component-value^="ComboMultiple"] { react-fragment[data-component-name^="ComboMultiple"] {
margin-bottom: $default-fields-spacer; margin-bottom: $default-fields-spacer;
[data-reach-combobox-input] { [data-reach-combobox-input] {

View file

@ -9,7 +9,7 @@
margin-left: 16px; margin-left: 16px;
} }
[data-react-component-value^="ComboMultiple"] { react-fragment[data-component-name^="ComboMultiple"] {
margin-bottom: 0; margin-bottom: 0;
[data-reach-combobox-token-list] { [data-reach-combobox-token-list] {

View file

@ -45,7 +45,7 @@
display: inline-block; display: inline-block;
} }
[data-react-component-value^="ComboMultiple"] { react-fragment[data-component-name^="ComboMultiple"] {
margin-bottom: $default-fields-spacer; margin-bottom: $default-fields-spacer;
[data-reach-combobox-token-list] { [data-reach-combobox-token-list] {

View file

@ -0,0 +1,14 @@
class ReactComponent < ApplicationComponent
erb_template <<-ERB
<% if content? %>
<react-component name=<%= @name %> props="<%= @props.to_json %>"><%= content %></react-component>
<% else %>
<react-component name=<%= @name %> props="<%= @props.to_json %>"></react-component>
<% end %>
ERB
def initialize(name, **props)
@name = name
@props = props
end
end

View file

@ -59,10 +59,6 @@ module ApplicationHelper
'alert' 'alert'
end end
def react_component(name, props = {}, html = {})
tag.div(**html.merge(data: { controller: 'react', react_component_value: name, react_props_value: props.to_json }))
end
def current_email def current_email
current_user&.email || current_user&.email ||
current_instructeur&.email || current_instructeur&.email ||

View file

@ -1,4 +1,4 @@
import React, { import {
useMemo, useMemo,
useState, useState,
useRef, useRef,

View file

@ -0,0 +1,12 @@
import { I18nProvider } from 'react-aria-components';
import { StrictMode, type ReactNode } from 'react';
export function Layout({ children }: { children: ReactNode }) {
const locale = document.documentElement.lang;
console.debug(`locale: ${locale}`);
return (
<I18nProvider locale={locale}>
<StrictMode>{children}</StrictMode>
</I18nProvider>
);
}

View file

@ -1,4 +1,4 @@
import React, { useState, useCallback, MouseEvent, ChangeEvent } from 'react'; import { useState, useCallback, MouseEvent, ChangeEvent } from 'react';
import type { FeatureCollection } from 'geojson'; import type { FeatureCollection } from 'geojson';
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';

View file

@ -1,4 +1,4 @@
import React, { useState, useId } from 'react'; import { useState, useId } from 'react';
import { fire } from '@utils'; import { fire } from '@utils';
import type { Feature, FeatureCollection } from 'geojson'; import type { Feature, FeatureCollection } from 'geojson';
import CoordinateInput from 'react-coordinate-input'; import CoordinateInput from 'react-coordinate-input';

View file

@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
import { Popup, LngLatBoundsLike, LngLatLike } from 'maplibre-gl'; import { Popup, LngLatBoundsLike, LngLatLike } from 'maplibre-gl';
import type { Feature, FeatureCollection, Point } from 'geojson'; import type { Feature, FeatureCollection, Point } from 'geojson';

View file

@ -1,4 +1,3 @@
import React from 'react';
import 'maplibre-gl/dist/maplibre-gl.css'; import 'maplibre-gl/dist/maplibre-gl.css';
import type { FeatureCollection } from 'geojson'; import type { FeatureCollection } from 'geojson';

View file

@ -1,4 +1,3 @@
import React from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';

View file

@ -1,4 +1,4 @@
import React, { import {
useState, useState,
useContext, useContext,
useRef, useRef,

View file

@ -1,4 +1,4 @@
import React, { useState, useId } from 'react'; import { useState, useId } from 'react';
import { Popover, RadioGroup } from '@headlessui/react'; import { Popover, RadioGroup } from '@headlessui/react';
import { usePopper } from 'react-popper'; import { usePopper } from 'react-popper';
import { MapIcon } from '@heroicons/react/outline'; import { MapIcon } from '@heroicons/react/outline';

View file

@ -1,72 +0,0 @@
import { Controller } from '@hotwired/stimulus';
import React, { lazy, Suspense, FunctionComponent } from 'react';
import { render, unmountComponentAtNode } from 'react-dom';
import invariant from 'tiny-invariant';
type Props = Record<string, unknown>;
type Loader = () => Promise<{ default: FunctionComponent<Props> }>;
const componentsRegistry = new Map<string, FunctionComponent<Props>>();
const components = import.meta.glob('../components/*.tsx');
for (const [path, loader] of Object.entries(components)) {
const [filename] = path.split('/').reverse();
const componentClassName = filename.replace(/\.(ts|tsx)$/, '');
console.debug(
`Registered lazy default export for "${componentClassName}" component`
);
componentsRegistry.set(
componentClassName,
LoadableComponent(loader as Loader)
);
}
// Initialize React components when their markup appears into the DOM.
//
// Example:
// <div data-controller="react" data-react-component-value="ComboMultiple" data-react-props-value="{}"></div>
//
export class ReactController extends Controller {
static values = {
component: String,
props: Object
};
declare readonly componentValue: string;
declare readonly propsValue: Props;
connect(): void {
this.mountComponent(this.element as HTMLElement);
}
disconnect(): void {
unmountComponentAtNode(this.element as HTMLElement);
}
private mountComponent(node: HTMLElement): void {
const componentName = this.componentValue;
const props = this.propsValue;
const Component = this.getComponent(componentName);
invariant(
Component,
`Cannot find a React component with class "${componentName}"`
);
render(<Component {...props} />, node);
}
private getComponent(componentName: string): FunctionComponent<Props> | null {
return componentsRegistry.get(componentName) ?? null;
}
}
const Spinner = () => <div className="spinner left" />;
function LoadableComponent(loader: Loader): FunctionComponent<Props> {
const LazyComponent = lazy(loader);
const Component: FunctionComponent<Props> = (props: Props) => (
<Suspense fallback={<Spinner />}>
<LazyComponent {...props} />
</Suspense>
);
return Component;
}

View file

@ -1,7 +1,9 @@
import { Actions } from '@coldwired/actions'; import { Actions } from '@coldwired/actions';
import { parseTurboStream } from '@coldwired/turbo-stream'; import { parseTurboStream } from '@coldwired/turbo-stream';
import { createRoot, createReactPlugin, type Root } from '@coldwired/react';
import invariant from 'tiny-invariant'; import invariant from 'tiny-invariant';
import { session as TurboSession, type StreamElement } from '@hotwired/turbo'; import { session as TurboSession, type StreamElement } from '@hotwired/turbo';
import type { ComponentType } from 'react';
import { ApplicationController } from './application_controller'; import { ApplicationController } from './application_controller';
@ -20,6 +22,7 @@ export class TurboController extends ApplicationController {
#submitting = false; #submitting = false;
#actions?: Actions; #actions?: Actions;
#root?: Root;
// `actions` instrface exposes all available actions as methods and also `applyActions` method // `actions` instrface exposes all available actions as methods and also `applyActions` method
// wich allows to apply a batch of actions. On top of regular `turbo-stream` actions we also // wich allows to apply a batch of actions. On top of regular `turbo-stream` actions we also
@ -32,6 +35,17 @@ export class TurboController extends ApplicationController {
} }
connect() { connect() {
this.#root = createRoot({
layoutComponentName: 'Layout/Layout',
loader,
schema: {
fragmentTagName: 'react-fragment',
componentTagName: 'react-component',
slotTagName: 'react-slot',
loadingClassName: 'loading'
}
});
const plugin = createReactPlugin(this.#root);
this.#actions = new Actions({ this.#actions = new Actions({
element: document.body, element: document.body,
schema: { schema: {
@ -40,6 +54,7 @@ export class TurboController extends ApplicationController {
focusDirectionAttribute: 'data-turbo-focus-direction', focusDirectionAttribute: 'data-turbo-focus-direction',
hiddenClassName: 'hidden' hiddenClassName: 'hidden'
}, },
plugins: [plugin],
debug: false debug: false
}); });
@ -73,6 +88,11 @@ export class TurboController extends ApplicationController {
}); });
} }
disconnect(): void {
this.#actions?.disconnect();
this.#root?.destroy();
}
private startSpinner() { private startSpinner() {
this.#submitting = true; this.#submitting = true;
this.actions.show({ targets: this.spinnerTargets }); this.actions.show({ targets: this.spinnerTargets });
@ -89,3 +109,24 @@ export class TurboController extends ApplicationController {
} }
} }
} }
type Loader = (exportName: string) => Promise<ComponentType<unknown>>;
const componentsRegistry: Record<string, Loader> = {};
const components = import.meta.glob('../components/*.tsx');
const loader: Loader = (name) => {
const [moduleName, exportName] = name.split('/');
const loader = componentsRegistry[moduleName];
invariant(loader, `Cannot find a React component with name "${name}"`);
return loader(exportName ?? 'default');
};
for (const [path, loader] of Object.entries(components)) {
const [filename] = path.split('/').reverse();
const componentClassName = filename.replace(/\.(ts|tsx)$/, '');
console.debug(`Registered lazy export for "${componentClassName}" component`);
componentsRegistry[componentClassName] = (exportName) =>
loader().then(
(m) => (m as Record<string, ComponentType<unknown>>)[exportName]
);
}

View file

@ -59,7 +59,5 @@
- else - else
= render 'footer' = render 'footer'
- if Rails.env.development?
= vite_typescript_tag 'axe-core'
= yield :charts_js = yield :charts_js
= render Attachment::ProgressBarComponent.new = render Attachment::ProgressBarComponent.new

View file

@ -11,6 +11,8 @@ by providing a `content_for(:javascript)` block.
<%= javascript_include_tag js_path %> <%= javascript_include_tag js_path %>
<% end %> <% end %>
<%= vite_client_tag %>
<%= vite_react_refresh_tag %>
<%= vite_typescript_tag 'manager' %> <%= vite_typescript_tag 'manager' %>
<%= yield :javascript %> <%= yield :javascript %>

BIN
bun.lockb

Binary file not shown.

View file

@ -1,9 +1,10 @@
{ {
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@coldwired/actions": "^0.11.2", "@coldwired/actions": "^0.13.0",
"@coldwired/turbo-stream": "^0.11.1", "@coldwired/react": "^0.15.0",
"@coldwired/utils": "^0.11.4", "@coldwired/turbo-stream": "^0.13.0",
"@coldwired/utils": "^0.13.0",
"@frsource/autoresize-textarea": "^2.0.75", "@frsource/autoresize-textarea": "^2.0.75",
"@gouvfr/dsfr": "^1.11.2", "@gouvfr/dsfr": "^1.11.2",
"@graphiql/plugin-explorer": "^3.0.2", "@graphiql/plugin-explorer": "^3.0.2",
@ -17,7 +18,6 @@
"@rails/actiontext": "^7.1.3-2", "@rails/actiontext": "^7.1.3-2",
"@rails/activestorage": "^7.1.3-2", "@rails/activestorage": "^7.1.3-2",
"@rails/ujs": "^7.1.3-2", "@rails/ujs": "^7.1.3-2",
"@reach/combobox": "^0.17.0",
"@reach/slider": "^0.17.0", "@reach/slider": "^0.17.0",
"@sentry/browser": "8.7.0", "@sentry/browser": "8.7.0",
"@tiptap/core": "^2.2.4", "@tiptap/core": "^2.2.4",
@ -50,31 +50,32 @@
"graphiql": "^3.2.3", "graphiql": "^3.2.3",
"graphql": "^16.8.1", "graphql": "^16.8.1",
"highcharts": "^10.3.3", "highcharts": "^10.3.3",
"is-hotkey": "^0.2.0",
"lightgallery": "^2.7.2", "lightgallery": "^2.7.2",
"maplibre-gl": "^1.15.2", "maplibre-gl": "^1.15.2",
"match-sorter": "^6.3.4", "match-sorter": "^6.3.4",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"react": "^18.2.0", "react": "^18.3.0",
"react-aria-components": "^1.2.0",
"react-coordinate-input": "^1.0.0", "react-coordinate-input": "^1.0.0",
"react-dom": "^18.2.0", "react-dom": "^18.3.0",
"react-popper": "^2.3.0", "react-popper": "^2.3.0",
"react-query": "^3.39.3", "react-use-event-hook": "^0.9.6",
"spectaql": "^2.3.1", "spectaql": "^2.3.1",
"stimulus-use": "^0.52.2", "stimulus-use": "^0.52.2",
"terser": "^5.31.0", "terser": "^5.31.0",
"tiny-invariant": "^1.3.3", "tiny-invariant": "^1.3.3",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"trix": "^1.2.3", "trix": "^1.2.3",
"use-debounce": "^9.0.4", "usehooks-ts": "^3.1.0",
"zod": "^3.20.2" "zod": "^3.20.2"
}, },
"devDependencies": { "devDependencies": {
"@esbuild/darwin-arm64": "=0.19.9", "@esbuild/darwin-arm64": "=0.19.9",
"@esbuild/linux-x64": "=0.19.9", "@esbuild/linux-x64": "=0.19.9",
"@esbuild/win32-x64": "=0.19.9", "@esbuild/win32-x64": "=0.19.9",
"@rollup/rollup-linux-x64-gnu": "=4.9.1", "@react-aria/optimize-locales-plugin": "^1.1.0",
"@rollup/rollup-darwin-arm64": "=4.9.1", "@rollup/rollup-darwin-arm64": "=4.9.1",
"@rollup/rollup-linux-x64-gnu": "=4.9.1",
"@rollup/rollup-win32-x64-msvc": "=4.9.1", "@rollup/rollup-win32-x64-msvc": "=4.9.1",
"@types/debounce": "^1.2.4", "@types/debounce": "^1.2.4",
"@types/geojson": "^7946.0.14", "@types/geojson": "^7946.0.14",
@ -82,8 +83,8 @@
"@types/mapbox__mapbox-gl-draw": "^1.2.5", "@types/mapbox__mapbox-gl-draw": "^1.2.5",
"@types/rails__activestorage": "^7.1.1", "@types/rails__activestorage": "^7.1.1",
"@types/rails__ujs": "^6.0.4", "@types/rails__ujs": "^6.0.4",
"@types/react": "^17.0.43", "@types/react": "^18.2.79",
"@types/react-dom": "^17.0.14", "@types/react-dom": "^18.2.25",
"@types/sortablejs": "^1.15.8", "@types/sortablejs": "^1.15.8",
"@typescript-eslint/eslint-plugin": "^7.11.0", "@typescript-eslint/eslint-plugin": "^7.11.0",
"@typescript-eslint/parser": "^7.11.0", "@typescript-eslint/parser": "^7.11.0",
@ -169,6 +170,7 @@
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended",
"plugin:react-hooks/recommended", "plugin:react-hooks/recommended",
"plugin:react/jsx-runtime",
"prettier" "prettier"
], ],
"rules": { "rules": {

View file

@ -5,7 +5,7 @@
"target": "ES2019", "target": "ES2019",
"moduleResolution": "node", "moduleResolution": "node",
"module": "es2020", "module": "es2020",
"jsx": "react", "jsx": "react-jsx",
"esModuleInterop": true, "esModuleInterop": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"isolatedModules": true, "isolatedModules": true,
@ -13,7 +13,7 @@
"skipLibCheck": true, "skipLibCheck": true,
"sourceMap": true, "sourceMap": true,
"strict": true, "strict": true,
"types": ["react/next", "react-dom/next", "vite/client"], "types": ["vite/client"],
"paths": { "paths": {
"~/*": ["./app/javascript/*"], "~/*": ["./app/javascript/*"],
"@utils": ["./app/javascript/shared/utils.ts"] "@utils": ["./app/javascript/shared/utils.ts"]

View file

@ -2,14 +2,21 @@ import { defineConfig } from 'vite';
import ViteReact from '@vitejs/plugin-react'; import ViteReact from '@vitejs/plugin-react';
import RubyPlugin from 'vite-plugin-ruby'; import RubyPlugin from 'vite-plugin-ruby';
import FullReload from 'vite-plugin-full-reload'; import FullReload from 'vite-plugin-full-reload';
import optimizeLocales from '@react-aria/optimize-locales-plugin';
const plugins = [ const plugins = [
RubyPlugin(), RubyPlugin(),
ViteReact({ jsxRuntime: 'classic' }), ViteReact(),
FullReload( FullReload(
['config/routes.rb', 'app/views/**/*', 'app/components/**/*.haml'], ['config/routes.rb', 'app/views/**/*', 'app/components/**/*.haml'],
{ delay: 200 } { delay: 200 }
) ),
{
...optimizeLocales.vite({
locales: ['en-GB', 'fr-FR']
}),
enforce: 'pre' as const
}
]; ];
export default defineConfig({ export default defineConfig({