Skip to content

Commit

Permalink
feat(react): Next.js Support (#445)
Browse files Browse the repository at this point in the history
* feat(react-output-target): generate functional components and ES modules (#432)

* feat: migrate to lit/react component wrappers

* exploration nextjs support

* update Stencil with support for DSD

* get rid of hydration errors

* enhance output target

* add tests

* fix dep

* remove type property in package.json

* remove hydrate dir

* move ssr support into custom export

* clear hydrate folder again

* skip lib check

* skip lib check

* better resolve light dom

* remove unused import

* match default dir with stencil

* more improvements on light dom rendering

* remove duplicate hydrate ot

* remove style prop

* fail if outdir is not set

* import hydration script and ssr runtime only within the server component

* validate hydrate output target to be set if hydrateModule option is set

* improve implementation

* separate files for server and client components

* properly create server and client side components

* don't parse children

* use own runtime

* validate that external runtime is set to 'true'

* adjust test

* update Stencil dev build

* recognise that externalRuntime default is true

* bring back light DOM rendering for better hydration results

* revert light dom approach

* minor formatting

* update stencil

* explicitly type component exports

* unit test tweak

* fix build

* use latest Stencil release

* fix test

---------

Co-authored-by: Sean Perkins <[email protected]>
  • Loading branch information
christian-bromann and sean-perkins authored Aug 27, 2024
1 parent 24de01e commit 399fe25
Show file tree
Hide file tree
Showing 41 changed files with 5,402 additions and 2,348 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ build/
.npmrc
*.tgz
*.lerna_backup
packages/example-project/component-library/hydrate
58 changes: 41 additions & 17 deletions packages/example-project/component-library-react/src/components.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
'use client';

/**
* This file was automatically generated by the Stencil React Output Target.
* Changes to this file may cause incorrect behavior and will be lost if the code is regenerated.
*/

/* eslint-disable */

import type { EventName } from '@stencil/react-output-target/runtime';
import type { EventName, StencilReactComponent } from '@stencil/react-output-target/runtime';
import { createComponent } from '@stencil/react-output-target/runtime';
import {
type CheckboxChangeEventDetail,
Expand Down Expand Up @@ -58,7 +60,10 @@ type MyButtonEvents = {
onMyBlur: EventName<CustomEvent<void>>;
};

export const MyButton = /*@__PURE__*/ createComponent<MyButtonElement, MyButtonEvents>({
export const MyButton: StencilReactComponent<MyButtonElement, MyButtonEvents> = /*@__PURE__*/ createComponent<
MyButtonElement,
MyButtonEvents
>({
tagName: 'my-button',
elementClass: MyButtonElement,
react: React,
Expand All @@ -75,7 +80,10 @@ type MyCheckboxEvents = {
onMyBlur: EventName<CustomEvent<void>>;
};

export const MyCheckbox = /*@__PURE__*/ createComponent<MyCheckboxElement, MyCheckboxEvents>({
export const MyCheckbox: StencilReactComponent<MyCheckboxElement, MyCheckboxEvents> = /*@__PURE__*/ createComponent<
MyCheckboxElement,
MyCheckboxEvents
>({
tagName: 'my-checkbox',
elementClass: MyCheckboxElement,
react: React,
Expand All @@ -89,7 +97,10 @@ export const MyCheckbox = /*@__PURE__*/ createComponent<MyCheckboxElement, MyChe

type MyComponentEvents = { onMyCustomEvent: EventName<CustomEvent<number>> };

export const MyComponent = /*@__PURE__*/ createComponent<MyComponentElement, MyComponentEvents>({
export const MyComponent: StencilReactComponent<MyComponentElement, MyComponentEvents> = /*@__PURE__*/ createComponent<
MyComponentElement,
MyComponentEvents
>({
tagName: 'my-component',
elementClass: MyComponentElement,
react: React,
Expand All @@ -104,7 +115,10 @@ type MyInputEvents = {
onMyFocus: EventName<CustomEvent<void>>;
};

export const MyInput = /*@__PURE__*/ createComponent<MyInputElement, MyInputEvents>({
export const MyInput: StencilReactComponent<MyInputElement, MyInputEvents> = /*@__PURE__*/ createComponent<
MyInputElement,
MyInputEvents
>({
tagName: 'my-input',
elementClass: MyInputElement,
react: React,
Expand All @@ -120,11 +134,14 @@ export const MyInput = /*@__PURE__*/ createComponent<MyInputElement, MyInputEven
type MyPopoverEvents = {
onMyPopoverDidPresent: EventName<CustomEvent<void>>;
onMyPopoverWillPresent: EventName<CustomEvent<void>>;
onMyPopoverWillDismiss: EventName<MyPopoverCustomEvent<OverlayEventDetail<any>>>;
onMyPopoverDidDismiss: EventName<MyPopoverCustomEvent<OverlayEventDetail<any>>>;
onMyPopoverWillDismiss: EventName<MyPopoverCustomEvent<OverlayEventDetail>>;
onMyPopoverDidDismiss: EventName<MyPopoverCustomEvent<OverlayEventDetail>>;
};

export const MyPopover = /*@__PURE__*/ createComponent<MyPopoverElement, MyPopoverEvents>({
export const MyPopover: StencilReactComponent<MyPopoverElement, MyPopoverEvents> = /*@__PURE__*/ createComponent<
MyPopoverElement,
MyPopoverEvents
>({
tagName: 'my-popover',
elementClass: MyPopoverElement,
react: React,
Expand All @@ -143,7 +160,10 @@ type MyRadioEvents = {
onMySelect: EventName<CustomEvent<void>>;
};

export const MyRadio = /*@__PURE__*/ createComponent<MyRadioElement, MyRadioEvents>({
export const MyRadio: StencilReactComponent<MyRadioElement, MyRadioEvents> = /*@__PURE__*/ createComponent<
MyRadioElement,
MyRadioEvents
>({
tagName: 'my-radio',
elementClass: MyRadioElement,
react: React,
Expand All @@ -157,21 +177,25 @@ export const MyRadio = /*@__PURE__*/ createComponent<MyRadioElement, MyRadioEven

type MyRadioGroupEvents = { onMyChange: EventName<MyRadioGroupCustomEvent<RadioGroupChangeEventDetail>> };

export const MyRadioGroup = /*@__PURE__*/ createComponent<MyRadioGroupElement, MyRadioGroupEvents>({
tagName: 'my-radio-group',
elementClass: MyRadioGroupElement,
react: React,
events: { onMyChange: 'myChange' } as MyRadioGroupEvents,
defineCustomElement: defineMyRadioGroup,
});
export const MyRadioGroup: StencilReactComponent<MyRadioGroupElement, MyRadioGroupEvents> =
/*@__PURE__*/ createComponent<MyRadioGroupElement, MyRadioGroupEvents>({
tagName: 'my-radio-group',
elementClass: MyRadioGroupElement,
react: React,
events: { onMyChange: 'myChange' } as MyRadioGroupEvents,
defineCustomElement: defineMyRadioGroup,
});

type MyRangeEvents = {
onMyChange: EventName<MyRangeCustomEvent<RangeChangeEventDetail>>;
onMyFocus: EventName<CustomEvent<void>>;
onMyBlur: EventName<CustomEvent<void>>;
};

export const MyRange = /*@__PURE__*/ createComponent<MyRangeElement, MyRangeEvents>({
export const MyRange: StencilReactComponent<MyRangeElement, MyRangeEvents> = /*@__PURE__*/ createComponent<
MyRangeElement,
MyRangeEvents
>({
tagName: 'my-range',
elementClass: MyRangeElement,
react: React,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,16 @@ export const defineContainer = <Props, VModelType = string | number | boolean>(
if (routerLink === EMPTY_PROP) return;

if (navManager !== undefined) {
/**
* This prevents the browser from
* performing a page reload when pressing
* an Ionic component with routerLink.
* The page reload interferes with routing
* and causes ion-back-button to disappear
* since the local history is wiped on reload.
*/
ev.preventDefault();

let navigationPayload: any = { event: ev };
for (const key in props) {
const value = props[key];
Expand Down Expand Up @@ -185,6 +195,17 @@ export const defineContainer = <Props, VModelType = string | number | boolean>(
}
}

// If router link is defined, add href to props
// in order to properly render an anchor tag inside
// of components that should become activatable and
// focusable with router link.
if (props[ROUTER_LINK_VALUE] !== EMPTY_PROP) {
propsToAdd = {
...propsToAdd,
href: props[ROUTER_LINK_VALUE],
};
}

/**
* vModelDirective is only needed on components that support v-model.
* As a result, we conditionally call withDirectives with v-model components.
Expand Down
3 changes: 2 additions & 1 deletion packages/example-project/component-library-vue/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"sourceMap": true,
"jsx": "react",
"target": "esnext",
"types": ["jest"]
"types": ["jest"],
"skipLibCheck": true
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["./__tests__/**", "node_modules", "setupTests.ts"],
Expand Down
4 changes: 2 additions & 2 deletions packages/example-project/component-library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"type": "git",
"url": "https://github.com/ionic-team/stencil-ds-output-targets.git"
},
"main": "dist/index.js",
"main": "dist/index.cjs.js",
"module": "dist/index.mjs",
"es2015": "dist/esm/index.mjs",
"es2017": "dist/esm/index.mjs",
Expand All @@ -31,7 +31,7 @@
},
"devDependencies": {
"@stencil/angular-output-target": "workspace:*",
"@stencil/core": "^4.16.0",
"@stencil/core": "^4.21.0",
"@stencil/react-output-target": "workspace:*",
"@stencil/vue-output-target": "workspace:*",
"@types/puppeteer": "2.0.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
:host {
display: block;
color: green;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
| ---------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
| `accept` | `accept` | If the value of the type attribute is `"file"`, then this attribute will indicate the types of files that the server accepts, otherwise it will be ignored. The value must be a comma-separated list of unique content type specifiers. | `string` | `undefined` |
| `autocapitalize` | `autocapitalize` | Indicates whether and how the text value should be automatically capitalized as it is entered/edited by the user. | `string` | `'off'` |
| `autocomplete` | `autocomplete` | Indicates whether the value of the control can be automatically completed by the browser. | `"name" \| "on" \| "off" \| "honorific-prefix" \| "given-name" \| "additional-name" \| "family-name" \| "honorific-suffix" \| "nickname" \| "email" \| "username" \| "new-password" \| "current-password" \| "one-time-code" \| "organization-title" \| "organization" \| "street-address" \| "address-line1" \| "address-line2" \| "address-line3" \| "address-level4" \| "address-level3" \| "address-level2" \| "address-level1" \| "country" \| "country-name" \| "postal-code" \| "cc-name" \| "cc-given-name" \| "cc-additional-name" \| "cc-family-name" \| "cc-number" \| "cc-exp" \| "cc-exp-month" \| "cc-exp-year" \| "cc-csc" \| "cc-type" \| "transaction-currency" \| "transaction-amount" \| "language" \| "bday" \| "bday-day" \| "bday-month" \| "bday-year" \| "sex" \| "tel" \| "tel-country-code" \| "tel-national" \| "tel-area-code" \| "tel-local" \| "tel-extension" \| "impp" \| "url" \| "photo"` | `'off'` |
| `autocomplete` | `autocomplete` | Indicates whether the value of the control can be automatically completed by the browser. | `"off" \| "on" \| "name" \| "honorific-prefix" \| "given-name" \| "additional-name" \| "family-name" \| "honorific-suffix" \| "nickname" \| "email" \| "username" \| "new-password" \| "current-password" \| "one-time-code" \| "organization-title" \| "organization" \| "street-address" \| "address-line1" \| "address-line2" \| "address-line3" \| "address-level4" \| "address-level3" \| "address-level2" \| "address-level1" \| "country" \| "country-name" \| "postal-code" \| "cc-name" \| "cc-given-name" \| "cc-additional-name" \| "cc-family-name" \| "cc-number" \| "cc-exp" \| "cc-exp-month" \| "cc-exp-year" \| "cc-csc" \| "cc-type" \| "transaction-currency" \| "transaction-amount" \| "language" \| "bday" \| "bday-day" \| "bday-month" \| "bday-year" \| "sex" \| "tel" \| "tel-country-code" \| "tel-national" \| "tel-area-code" \| "tel-local" \| "tel-extension" \| "impp" \| "url" \| "photo"` | `'off'` |
| `autocorrect` | `autocorrect` | Whether auto correction should be enabled when the user is entering/editing the text value. | `"off" \| "on"` | `'off'` |
| `autofocus` | `autofocus` | This Boolean attribute lets you specify that a form control should have input focus when the page loads. | `boolean` | `false` |
| `clearInput` | `clear-input` | If `true`, a clear icon will appear in the input when there is a value. Clicking it clears the input. | `boolean` | `false` |
Expand Down
9 changes: 9 additions & 0 deletions packages/example-project/component-library/stencil.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,19 +61,28 @@ export const config: Config = {
reactOutputTarget({
outDir: '../component-library-react/src',
}),
reactOutputTarget({
outDir: '../next-app/src/app',
hydrateModule: 'component-library/hydrate'
}),
vueOutputTarget({
componentCorePackage: 'component-library',
proxiesFile: '../component-library-vue/src/proxies.ts',
componentModels: vueComponentModels,
}),
{
type: 'dist-custom-elements',
externalRuntime: false,
dir: 'components'
},
{
type: 'dist',
esmLoaderPath: '../loader',
},
{
type: 'dist-hydrate-script',
dir: './hydrate',
},
{
type: 'docs-readme',
},
Expand Down
Loading

0 comments on commit 399fe25

Please sign in to comment.