Skip to content

Commit

Permalink
feat: SSR for react
Browse files Browse the repository at this point in the history
  • Loading branch information
divdavem committed Apr 18, 2024
1 parent 1337a26 commit be09940
Show file tree
Hide file tree
Showing 9 changed files with 99 additions and 51 deletions.
2 changes: 1 addition & 1 deletion core/src/components/accordion/accordion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ function createAccordionItem(
'aria-expanded': computed(() => `${itemVisible$()}`),
'aria-disabled': computed(() => `${itemDisabled$()}`),
'aria-controls': computed(() => `${itemId$()}-body-container`),
disabled: itemDisabled$(),
disabled: itemDisabled$,
},
classNames: {collapsed: computed(() => !itemVisible$())},
events: {click: clickAction},
Expand Down
5 changes: 2 additions & 3 deletions e2e/ssr.ssr-e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import {expect, test} from './fixture';
import {htmlSnapshot} from './htmlSnapshot';

test.fixme(({framework}) => framework === 'react', 'SSR test is not yet fully working for React');
test.fixme(({framework}) => framework === 'svelte', 'SSR test is not yet fully working for Svelte');

test.describe('SSR without rehydration', () => {
test.fixme(({framework}) => framework === 'svelte', 'SSR test is not yet fully working for Svelte');

test.use({javaScriptEnabled: false});
test('Markup', async ({page}) => {
await page.goto('.', {waitUntil: 'networkidle'});
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

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

2 changes: 1 addition & 1 deletion react/bootstrap/src/components/progressbar/progressbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function DefaultSlotContent(slotContext: ProgressbarContext) {
const {striped, animated, type} = slotContext.state;
const classes = classNames('progress-bar', {'progress-bar-striped': striped}, {'progress-bar-animated': animated}, {[`text-bg-${type}`]: !!type});
return (
<div className="progress" style={{height: slotContext.state.height}}>
<div className="progress" style={{height: slotContext.state.height || undefined}}>
<div className={classes} style={{width: `${slotContext.state.percentage}%`}}>
<Slot slotContent={slotContext.state.slotDefault} props={slotContext} />
</div>
Expand Down
1 change: 1 addition & 0 deletions react/bootstrap/src/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export function Select<Item>(props: Partial<SelectProps<Item>>) {
<Badges slotContext={slotContext}></Badges>
<input
id={id}
suppressHydrationWarning={true}
aria-label={ariaLabel}
className="au-select-input flex-grow-1 border-0"
type="text"
Expand Down
1 change: 1 addition & 0 deletions react/headless/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
},
"peerDependencies": {
"@amadeus-it-group/tansu": "*",
"esm-env": "*",
"react": "*",
"react-dom": "*"
},
Expand Down
124 changes: 78 additions & 46 deletions react/headless/src/utils/directive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,56 +21,39 @@ export const useClassDirective = (className: string) => {
return directive;
};

export function useDirective(directive: Directive<void>): {ref: RefCallback<HTMLElement>};
export function useDirective<T>(directive: Directive<T>, args: T): {ref: RefCallback<HTMLElement>};
/**
* The useDirective function.
*
* Allows to attach a provided directive to the current react component.
*
* @param directive - the directive
* @param args - the args to pass to the directive
* @returns the ref callback
*/
export function useDirective<T>(directive: Directive<T>, args?: T): {ref: RefCallback<HTMLElement>} {
const instance = useRef<ReturnType<typeof directive>>();
const propsRef = useRef<T>();
const ref = useCallback(
(element: HTMLElement | null) => {
instance.current?.destroy?.();
instance.current = undefined;
if (element) {
instance.current = directive(element, propsRef.current as T);
}
},
[directive],
);
propsRef.current = args;
instance.current?.update?.(args as T);
return {ref};
}

export function useDirectives(directives: Directive<void>[]): {ref: RefCallback<HTMLElement>};
export function useDirectives<T>(directives: Directive<T>[], args: T): {ref: RefCallback<HTMLElement>};
/**
* The useDirectives function.
*
* Allows to attach multiple directives to the current react component.
*
* @param directives - directives
* @param args - the args to pass to the directives
* @returns the ref callback
*/
export function useDirectives<T>(directives: Directive<T>[], args?: T): {ref: RefCallback<HTMLElement>} {
const mergedDirectives = useMemo(() => mergeDirectives(...directives), directives);
return useDirective(mergedDirectives, args as any);
}

const attributesMap = new Map([
['tabindex', 'tabIndex'],
['for', 'htmlFor'],
]);

// For boolean attributes, the presence of the attribute means true, but react wants a boolean value
// cf the list in https://github.com/facebook/react/blob/bf40b024421a0e1f2f882fd7171ea39cd74c88df/packages/react-dom-bindings/src/client/ReactDOMComponent.js#L665
const booleanAttributes = new Set([
'inert',
'allowFullScreen',
'async',
'autoPlay',
'controls',
'default',
'defer',
'disabled',
'disablePictureInPicture',
'disableRemotePlayback',
'formNoValidate',
'hidden',
'loop',
'noModule',
'noValidate',
'open',
'playsInline',
'readOnly',
'required',
'reversed',
'scoped',
'seamless',
'itemScope',
]);

/**
* Returns an object with the key/value attributes for JSX, derived from a list of directives.
*
Expand All @@ -82,7 +65,7 @@ export function directiveAttributes<T extends any[]>(...directives: {[K in keyof
const {attributes, style, classNames} = attributesData(...directives);

for (const [name, value] of Object.entries(attributes)) {
reactAttributes[attributesMap.get(name) ?? name] = value;
reactAttributes[attributesMap.get(name) ?? name] = booleanAttributes.has(name) ? true : value;
}

if (classNames?.length) {
Expand All @@ -102,3 +85,52 @@ export function directiveAttributes<T extends any[]>(...directives: {[K in keyof
* @returns JSON object with name/value for the attributes
*/
export const ssrAttributes: typeof directiveAttributes = BROWSER ? () => ({}) : directiveAttributes;

/**
* The useDirective function.
*
* Allows to attach a provided directive to the current react component.
*
* @param directive - the directive
* @param args - the args to pass to the directive
* @returns the ref callback
*/
export const useDirective: {
(directive: Directive): {ref: RefCallback<HTMLElement>};
<T>(directive: Directive<T>, args: T): {ref: RefCallback<HTMLElement>};
} = BROWSER
? <T>(directive: Directive<T>, args?: T): {ref: RefCallback<HTMLElement>; suppressHydrationWarning: true} => {
const instance = useRef<ReturnType<typeof directive>>();
const propsRef = useRef<T>();
const ref = useCallback(
(element: HTMLElement | null) => {
instance.current?.destroy?.();
instance.current = undefined;
if (element) {
instance.current = directive(element, propsRef.current as T);
}
},
[directive],
);
propsRef.current = args;
instance.current?.update?.(args as T);
return {ref, suppressHydrationWarning: true};
}
: <T>(directive: Directive<T>, args?: T): {ref: RefCallback<HTMLElement>} => ssrAttributes([directive, args as T]) as any;

Check warning on line 119 in react/headless/src/utils/directive.ts

View check run for this annotation

Codecov / codecov/patch

react/headless/src/utils/directive.ts#L119

Added line #L119 was not covered by tests

/**
* The useDirectives function.
*
* Allows to attach multiple directives to the current react component.
*
* @param directives - directives
* @param args - the args to pass to the directives
* @returns the ref callback
*/
export const useDirectives: {
(directives: Directive[]): {ref: RefCallback<HTMLElement>};
<T>(directives: Directive<T>[], args: T): {ref: RefCallback<HTMLElement>};
} = <T>(directives: Directive<T>[], args?: T): {ref: RefCallback<HTMLElement>} => {
const mergedDirectives = useMemo(() => mergeDirectives(...directives), directives);
return useDirective(mergedDirectives, args as any);
};
10 changes: 10 additions & 0 deletions react/ssr-app/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.check.json",
"compilerOptions": {
"paths": {
"@agnos-ui/core/*": ["./core/src/*"],
"@agnos-ui/react-headless/*": ["./react/headless/src/*", "./react/headless/src/generated/*"],
"@agnos-ui/react/*": ["./react/lib/src/*", "./react/lib/src/generated/*"]
}
}
}
4 changes: 4 additions & 0 deletions react/ssr-app/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ export default defineConfig((config) => ({
resolve: {
alias: config.mode === 'production' ? {} : alias,
},
define: {
// make sure we always have react warnings even on the CI with "npm run preview"
'process.env.NODE_ENV': '"development"',
},
}));

0 comments on commit be09940

Please sign in to comment.