@gondel/plugin-react
Advanced tools
Comparing version 1.1.2 to 1.2.0
@@ -6,2 +6,13 @@ # Change Log | ||
# [1.2.0](https://github.com/namics/gondel/compare/v1.1.2...v1.2.0) (2020-02-19) | ||
### Features | ||
* **react plugin:** provide a factory function to connect Gondel and React ([c6ac867](https://github.com/namics/gondel/commit/c6ac867ad9841f09d90dda18a9fbb77fb83f6dce)) | ||
## [1.1.2](https://github.com/namics/gondel/compare/v1.1.1...v1.1.2) (2020-01-16) | ||
@@ -8,0 +19,0 @@ |
@@ -1,2 +0,2 @@ | ||
import React, { Component } from "react"; | ||
import { Component } from "react"; | ||
export interface Props<S> extends React.ComponentLifecycle<null, S> { | ||
@@ -9,4 +9,4 @@ children?: (props: S) => JSX.Element; | ||
constructor(props: Props<TConfig>); | ||
render(): JSX.Element | (((props: TConfig) => JSX.Element) & string) | (((props: TConfig) => JSX.Element) & number) | (((props: TConfig) => JSX.Element) & false) | (((props: TConfig) => JSX.Element) & true) | (((props: TConfig) => JSX.Element) & React.ReactNodeArray) | undefined; | ||
render(): JSX.Element | (((props: TConfig) => JSX.Element) & string) | (((props: TConfig) => JSX.Element) & number) | (((props: TConfig) => JSX.Element) & false) | (((props: TConfig) => JSX.Element) & true) | (((props: TConfig) => JSX.Element) & import("react").ReactNodeArray) | undefined; | ||
} | ||
export declare function createRenderableAppWrapper<TConfig>(props: Props<TConfig>): JSX.Element; | ||
export declare function createRenderableAppWrapper<TConfig>(props: Props<TConfig>): import("react").CElement<Props<unknown>, AppWrapper<unknown>>; |
@@ -14,14 +14,3 @@ var __extends = (this && this.__extends) || (function () { | ||
})(); | ||
var __assign = (this && this.__assign) || function () { | ||
__assign = Object.assign || function(t) { | ||
for (var s, i = 1, n = arguments.length; i < n; i++) { | ||
s = arguments[i]; | ||
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) | ||
t[p] = s[p]; | ||
} | ||
return t; | ||
}; | ||
return __assign.apply(this, arguments); | ||
}; | ||
import React, { Component } from "react"; | ||
import { Component, createElement } from "react"; | ||
var AppWrapper = /** @class */ (function (_super) { | ||
@@ -47,3 +36,3 @@ __extends(AppWrapper, _super); | ||
_this[reactHook] = function () { | ||
this.props[reactHook].apply(this, arguments); | ||
return this.props[reactHook].apply(this, arguments); | ||
}; | ||
@@ -63,4 +52,4 @@ }); | ||
export function createRenderableAppWrapper(props) { | ||
return React.createElement(AppWrapper, __assign({}, props)); | ||
return createElement(AppWrapper, props); | ||
} | ||
//# sourceMappingURL=AppWrapper.js.map |
import React, { StatelessComponent, ComponentClass, ComponentLifecycle } from "react"; | ||
import { GondelBaseComponent } from "@gondel/core"; | ||
import { KeysMatching, UnwrapPromise } from "./utils"; | ||
declare type RenderableReactComponent<State> = StatelessComponent<State> | ComponentClass<State, any>; | ||
export declare class GondelReactComponent<State = {}, TElement extends HTMLElement = HTMLDivElement> extends GondelBaseComponent<TElement> implements ComponentLifecycle<null, State> { | ||
static readonly AppPromiseMap: WeakMap<Promise<React.ComponentType<any>>, React.ComponentType<any>>; | ||
declare type StateOfComponent<T> = T extends RenderableReactComponent<infer V> ? V : never; | ||
interface ConstructableGondelReactComponent<State> { | ||
new (...args: any[]): GondelReactComponent<State>; | ||
} | ||
/** | ||
* Create a GondelReactComponent class which is directly linked with a loader | ||
* for default exports | ||
* | ||
* @example | ||
* const loader = () => import('./App'); | ||
* class MyComponent extends GondelReactComponent.create(loader) { | ||
* } | ||
* | ||
*/ | ||
export declare function createGondelReactLoader<State extends {}>(loader: () => Promise<RenderableReactComponent<State>>): ConstructableGondelReactComponent<State>; | ||
/** | ||
* Create a GondelReactComponent class which is directly linked with a loader | ||
* | ||
* @example | ||
* const loader = () => <span>Hello world</span>; | ||
* class MyComponent extends GondelReactComponent.create(loader) { | ||
* } | ||
* | ||
*/ | ||
export declare function createGondelReactLoader<State extends {}>(loader: () => RenderableReactComponent<State>): ConstructableGondelReactComponent<State>; | ||
/** | ||
* Create a GondelReactComponent class which is directly linked with a loader | ||
* for named exports like `export const App = () => <span>Hello world</span>` | ||
* | ||
* @example | ||
* const loader = () => import('./App'); | ||
* class MyComponent extends GondelReactComponent.create(loader, "App") { | ||
* } | ||
* | ||
*/ | ||
export declare function createGondelReactLoader<State extends StateOfComponent<UnwrapPromise<Module>[ExportName]>, Module extends Promise<{ | ||
[key: string]: unknown; | ||
}>, ExportName extends KeysMatching<UnwrapPromise<Module>, RenderableReactComponent<any>>>(loader: () => Module, exportName: ExportName): ConstructableGondelReactComponent<State>; | ||
export declare class GondelReactComponent<State extends {} = {}, TElement extends HTMLElement = HTMLDivElement> extends GondelBaseComponent<TElement> implements ComponentLifecycle<null, State> { | ||
static readonly AppPromiseMap: WeakMap<Promise<RenderableReactComponent<any>>, RenderableReactComponent<any>>; | ||
/** | ||
* Create a GondelReactComponent class which is directly linked with a loader | ||
*/ | ||
static create: typeof createGondelReactLoader; | ||
_setInternalState: (config: State) => void | undefined; | ||
@@ -7,0 +50,0 @@ App?: RenderableReactComponent<State> | Promise<RenderableReactComponent<State>>; |
@@ -14,6 +14,36 @@ var __extends = (this && this.__extends) || (function () { | ||
})(); | ||
import React from "react"; | ||
import { createElement } from "react"; | ||
import { GondelBaseComponent } from "@gondel/core"; | ||
import { createRenderableAppWrapper } from "./AppWrapper"; | ||
import { isPromise } from "./utils"; | ||
export function createGondelReactLoader(loader, exportName) { | ||
var unifiedLoader = function () { | ||
var loaderResult = loader(); | ||
if (!isPromise(loaderResult)) { | ||
return loaderResult; | ||
} | ||
return loaderResult.then(function (lazyLoadModule) { | ||
if (exportName) { | ||
var mod = lazyLoadModule; | ||
/* istanbul ignore else */ | ||
if (mod[exportName]) { | ||
return mod[exportName]; | ||
} | ||
else { | ||
throw new Error("export " + exportName + " not found"); | ||
} | ||
} | ||
return lazyLoadModule; | ||
}); | ||
}; | ||
return /** @class */ (function (_super) { | ||
__extends(GondelReactWrapperComponent, _super); | ||
function GondelReactWrapperComponent() { | ||
var _this = _super !== null && _super.apply(this, arguments) || this; | ||
_this.App = unifiedLoader(); | ||
return _this; | ||
} | ||
return GondelReactWrapperComponent; | ||
}(GondelReactComponent)); | ||
} | ||
var GondelReactComponent = /** @class */ (function (_super) { | ||
@@ -26,5 +56,6 @@ __extends(GondelReactComponent, _super); | ||
var ReactDOMPromise = import( | ||
/* webpackPrefetch: true, webpackChunkName: 'ReactDom' */ "react-dom").then(function (ReactDOM) { return ReactDOM.default; }); | ||
/* webpackPrefetch: true, webpackChunkName: 'ReactDom' */ "react-dom").then(function (ReactDOM) { return ReactDOM.default || ReactDOM; }); | ||
var configScript = ctx.querySelector("script[type='text/json']"); | ||
_this.state = configScript ? JSON.parse(configScript.innerHTML) : {}; | ||
var unmountComponentAtNode; | ||
_this.start = function () { | ||
@@ -48,2 +79,4 @@ var _this = this; | ||
var ReactDOM = _a[0], App = _a[1]; | ||
// Store unmountComponentAtNode for stopping the app | ||
unmountComponentAtNode = ReactDOM.unmountComponentAtNode; | ||
// Store unwrapped promise for this.App | ||
@@ -61,4 +94,8 @@ if (App && isPromise(_this.App)) { | ||
componentWillUnmount: function () { | ||
var args = []; | ||
for (var _i = 0; _i < arguments.length; _i++) { | ||
args[_i] = arguments[_i]; | ||
} | ||
delete _this._setInternalState; | ||
_this.componentWillUnmount && _this.componentWillUnmount(); | ||
_this.componentWillUnmount && _this.componentWillUnmount.apply(_this, args); | ||
}, | ||
@@ -76,2 +113,13 @@ componentDidMount: _this.componentDidMount && _this.componentDidMount.bind(_this), | ||
}; | ||
// Make sure that the stop method will tear down the react app | ||
var originalStop = _this.stop; | ||
_this.stop = function () { | ||
var returnValue = originalStop && originalStop.apply(this, arguments); | ||
// check if during this components start method unmountComponentAtNode | ||
// was set - if not we don't need to unmound the app | ||
if (unmountComponentAtNode) { | ||
unmountComponentAtNode(this._ctx); | ||
} | ||
return returnValue; | ||
}; | ||
return _this; | ||
@@ -95,5 +143,9 @@ } | ||
} | ||
return React.createElement(App, this.state); | ||
return createElement(App, this.state); | ||
}; | ||
GondelReactComponent.AppPromiseMap = new WeakMap(); | ||
/** | ||
* Create a GondelReactComponent class which is directly linked with a loader | ||
*/ | ||
GondelReactComponent.create = createGondelReactLoader; | ||
return GondelReactComponent; | ||
@@ -100,0 +152,0 @@ }(GondelBaseComponent)); |
@@ -5,1 +5,12 @@ /** | ||
export declare function isPromise<T>(obj: {} | Promise<T> | undefined | string | number | null): obj is Promise<T>; | ||
/** | ||
* Extracts all key from a type T which are assignable to V | ||
* @see https://stackoverflow.com/a/54520829/159319 | ||
*/ | ||
export declare type KeysMatching<T, V> = { | ||
[K in keyof T]: T[K] extends V ? K : never; | ||
}[keyof T]; | ||
/** | ||
* Returns the resolved Promise value type | ||
*/ | ||
export declare type UnwrapPromise<T> = T extends Promise<infer V> ? V : never; |
{ | ||
"name": "@gondel/plugin-react", | ||
"version": "1.1.2", | ||
"version": "1.2.0", | ||
"description": "Gondel Plugin to boot react widgets and apps", | ||
@@ -29,3 +29,3 @@ "bugs": "https://github.com/namics/gondel/issues", | ||
"devDependencies": { | ||
"@gondel/core": "^1.1.0", | ||
"@gondel/core": "^1.2.0", | ||
"jest": "24.9.0", | ||
@@ -60,4 +60,3 @@ "npm-run-all": "4.1.5", | ||
] | ||
}, | ||
"gitHead": "40d0471a610ffa353af7956bac7a9d53e847a419" | ||
} | ||
} |
@@ -114,24 +114,12 @@ # React Plugin | ||
const loader = () => import('./App'); | ||
@Component('DemoWidget') | ||
export class DemoWidget extends GondelReactComponent { | ||
App = import('./App').then(({ App }) => App); | ||
export class DemoWidget extends GondelReactComponent.create(loader, "App") { | ||
} | ||
``` | ||
### Async blocking linking example (code splitting) | ||
To use a react App with a default export the second parameter of `create` can be skipped. | ||
You can use a async start method to lazy load a component and tell Gondel to wait until the JavaScript of the component has been loaded. | ||
This will guarantee that the sync method of all Gondel components will be delayed until the React component was completely loaded. | ||
**HTML** | ||
```html | ||
<div data-g-name="DemoWidget"> | ||
<script type="text/json">{ "foo":"bar" }</script> | ||
Loading.. | ||
</div> | ||
``` | ||
**JavaScript** | ||
```js | ||
@@ -141,11 +129,10 @@ import { GondelReactComponent } from '@gondel/plugin-react'; | ||
const loader = () => import('./App'); | ||
@Component('DemoWidget') | ||
export class DemoWidget extends GondelReactComponent { | ||
async start() { | ||
this.App = (await import('./App')).App; | ||
} | ||
export class DemoWidget extends GondelReactComponent.create(loader) { | ||
} | ||
``` | ||
## Manipulating state | ||
@@ -171,3 +158,3 @@ | ||
const DemoApp = ({ theme }) => ( | ||
const DemoApp = ({ theme }: {theme: 'light' | 'dark'}) => ( | ||
<h1 className={theme === 'dark' ? 'dark' : 'light'}> | ||
@@ -179,5 +166,3 @@ Hello World | ||
@Component('DemoWidget') | ||
export class DemoWidget extends GondelReactComponent<{theme: 'light' | 'dark'}> { | ||
App = DemoApp; | ||
export class DemoWidget extends GondelReactComponent.create(() => DemoApp) { | ||
setTheme(theme: 'light' | 'dark') { | ||
@@ -184,0 +169,0 @@ this.setState({ theme }); |
@@ -1,5 +0,4 @@ | ||
import { Component } from "@gondel/core"; | ||
import { TestApp } from "../fixtures/TestApp"; | ||
import { GondelReactComponent } from "./GondelReactComponent"; | ||
import { isPromise } from "./utils"; | ||
import { Component, getComponentByDomNode, startComponents } from "@gondel/core"; | ||
import { createElement } from "react"; | ||
import { createGondelReactLoader, GondelReactComponent } from "./GondelReactComponent"; | ||
@@ -32,20 +31,2 @@ const createComponentStateHTML = (initialState: object = {}) => { | ||
it("should not expose certain react lifecycle methods", () => { | ||
class TestComponent extends GondelReactComponent { | ||
_componentName = "TestComponent"; | ||
} | ||
const root = document.createElement("div"); | ||
const c = new TestComponent(root, "stub"); | ||
expect(c.componentWillMount).toBeUndefined(); | ||
expect(c.componentDidMount).toBeUndefined(); | ||
expect(c.componentWillReceiveProps).toBeUndefined(); | ||
expect(c.shouldComponentUpdate).toBeUndefined(); | ||
expect(c.componentWillUpdate).toBeUndefined(); | ||
expect(c.componentDidUpdate).toBeUndefined(); | ||
expect(c.componentWillUnmount).toBeUndefined(); | ||
expect(c.componentDidCatch).toBeUndefined(); | ||
}); | ||
it("should read child script config", () => { | ||
@@ -140,20 +121,196 @@ const root = createComponentStateHTML({ theme: "light", loaded: true }); | ||
describe("render", () => { | ||
// TODO: This test fails, strange behaviour, we need to investigate a bit here | ||
it.skip("should be able to render React apps", async () => { | ||
@Component("test") | ||
class TestComponent extends GondelReactComponent { | ||
App = TestApp; | ||
} | ||
it("should be able to render React apps syncronously", async () => { | ||
const root = document.createElement("div"); | ||
const component = new TestComponent(root, "test"); | ||
component.setState({ a: 1 }); | ||
expect(typeof (component as any).start).toBeTruthy(); | ||
const startPromise = (component as any).start() as Promise<any>; | ||
expect(isPromise(startPromise)).toBeTruthy(); | ||
const startResponse = await startPromise; | ||
// const out = await (c as any).start(); | ||
// expect(typeof (c as any).start === 'function').toBeTruthy() | ||
// expect(c.render()).toEqual('') | ||
root.innerHTML = `<div data-g-name="Greeter"><script type="text/json">{ "title": "Hello World"}</script></div>`; | ||
await new Promise(resolve => { | ||
function TestTitleSpan(props: { title: string }) { | ||
return createElement("span", null, props.title); | ||
} | ||
const loader = () => TestTitleSpan; | ||
const GondelReactLoaderComponent = createGondelReactLoader(loader); | ||
@Component("Greeter") | ||
class Greeter extends GondelReactLoaderComponent { | ||
start() {} | ||
componentDidMount() { | ||
resolve(); | ||
} | ||
} | ||
startComponents(root); | ||
}); | ||
expect(root.innerHTML).toBe('<div data-g-name="Greeter"><span>Hello World</span></div>'); | ||
}); | ||
it("should be able to render React apps asyncronously", async () => { | ||
const root = document.createElement("div"); | ||
root.innerHTML = `<div data-g-name="Greeter"><script type="text/json">{ "title": "Hello World"}</script></div>`; | ||
await new Promise(resolve => { | ||
function TestTitleSpan(props: { title: string }) { | ||
return createElement("span", null, props.title); | ||
} | ||
const loader = async () => TestTitleSpan; | ||
const GondelReactLoaderComponent = createGondelReactLoader(loader); | ||
@Component("Greeter") | ||
class Greeter extends GondelReactLoaderComponent { | ||
componentDidMount() { | ||
resolve(); | ||
} | ||
} | ||
startComponents(root); | ||
}); | ||
expect(root.innerHTML).toBe('<div data-g-name="Greeter"><span>Hello World</span></div>'); | ||
}); | ||
it("should be able to render React apps named asyncronously", async () => { | ||
const root = document.createElement("div"); | ||
root.innerHTML = `<div data-g-name="Greeter"><script type="text/json">{ "title": "Hello World"}</script></div>`; | ||
await new Promise(resolve => { | ||
function TestTitleSpan(props: { title: string }) { | ||
return createElement("span", null, props.title); | ||
} | ||
const loader = async () => ({ TestTitleSpan } as const); | ||
const GondelReactLoaderComponent = createGondelReactLoader(loader, "TestTitleSpan"); | ||
@Component("Greeter") | ||
class Greeter extends GondelReactLoaderComponent { | ||
componentDidMount() { | ||
resolve(); | ||
} | ||
} | ||
startComponents(root); | ||
}); | ||
expect(root.innerHTML).toBe('<div data-g-name="Greeter"><span>Hello World</span></div>'); | ||
}); | ||
it("should execute hooks during rendering", async () => { | ||
const root = document.createElement("div"); | ||
root.innerHTML = `<div data-g-name="Greeter"></div>`; | ||
const hooks: string[] = []; | ||
await new Promise(resolve => { | ||
const loader = () => () => createElement("span", null, "Hello World"); | ||
const GondelReactLoaderComponent = createGondelReactLoader(loader); | ||
@Component("Greeter") | ||
class Greeter extends GondelReactLoaderComponent { | ||
componentDidMount() { | ||
hooks.push("componentDidMount"); | ||
setTimeout(() => { | ||
this.stop(); | ||
}); | ||
} | ||
componentWillUnmount() { | ||
hooks.push("componentWillUnmount"); | ||
resolve(); | ||
} | ||
} | ||
startComponents(root); | ||
}); | ||
expect(hooks).toEqual(["componentDidMount", "componentWillUnmount"]); | ||
}); | ||
it("should render after the start method is done", async () => { | ||
const root = document.createElement("div"); | ||
root.innerHTML = `<div data-g-name="Greeter"></div>`; | ||
await new Promise(resolve => { | ||
function TestTitleSpan(props: { title: string }) { | ||
return createElement("span", null, props.title); | ||
} | ||
const loader = () => TestTitleSpan; | ||
const GondelReactLoaderComponent = createGondelReactLoader(loader); | ||
@Component("Greeter") | ||
class Greeter extends GondelReactLoaderComponent { | ||
start() { | ||
return new Promise(resolve => { | ||
setTimeout(() => { | ||
this.setState({ | ||
title: "Lazy loaded data" | ||
}); | ||
resolve(); | ||
}); | ||
}); | ||
} | ||
componentDidMount() { | ||
resolve(); | ||
} | ||
} | ||
startComponents(root); | ||
}); | ||
expect(root.innerHTML).toBe( | ||
'<div data-g-name="Greeter"><span>Lazy loaded data</span></div>' | ||
); | ||
}); | ||
it("should render after the start method is done using a callback", async () => { | ||
const root = document.createElement("div"); | ||
root.innerHTML = `<div data-g-name="Greeter"></div>`; | ||
await new Promise(resolve => { | ||
function TestTitleSpan(props: { title: string }) { | ||
return createElement("span", null, props.title); | ||
} | ||
const loader = () => TestTitleSpan; | ||
const GondelReactLoaderComponent = createGondelReactLoader(loader); | ||
@Component("Greeter") | ||
class Greeter extends GondelReactLoaderComponent { | ||
start(resolve: () => void) { | ||
setTimeout(() => { | ||
this.setState({ | ||
title: "Lazy loaded data" | ||
}); | ||
resolve(); | ||
}); | ||
} | ||
componentDidMount() { | ||
resolve(); | ||
} | ||
} | ||
startComponents(root); | ||
}); | ||
expect(root.innerHTML).toBe( | ||
'<div data-g-name="Greeter"><span>Lazy loaded data</span></div>' | ||
); | ||
}); | ||
it("should rerender once setState is called", async () => { | ||
const root = document.createElement("div"); | ||
root.innerHTML = `<div data-g-name="Greeter"></div>`; | ||
await new Promise(resolve => { | ||
function TestTitleSpan(props: { title: string }) { | ||
return createElement("span", null, props.title || ""); | ||
} | ||
const GondelReactLoaderComponent = createGondelReactLoader(() => TestTitleSpan); | ||
@Component("Greeter") | ||
class Greeter extends GondelReactLoaderComponent { | ||
componentDidMount() { | ||
resolve(); | ||
} | ||
componentDidUpdate() {} | ||
shouldComponentUpdate() { | ||
return true; | ||
} | ||
} | ||
startComponents(root); | ||
}); | ||
const component = getComponentByDomNode<any>(root.firstElementChild!); | ||
component.setState({ title: "update using getComponentByDomNode" }); | ||
expect(root.innerHTML).toBe( | ||
'<div data-g-name="Greeter"><span>update using getComponentByDomNode</span></div>' | ||
); | ||
}); | ||
it("base class should throw an error if no app provided", () => { | ||
@@ -160,0 +317,0 @@ const root = document.createElement("div"); |
@@ -9,1 +9,12 @@ /** | ||
} | ||
/** | ||
* Extracts all key from a type T which are assignable to V | ||
* @see https://stackoverflow.com/a/54520829/159319 | ||
*/ | ||
export type KeysMatching<T, V> = { [K in keyof T]: T[K] extends V ? K : never }[keyof T]; | ||
/** | ||
* Returns the resolved Promise value type | ||
*/ | ||
export type UnwrapPromise<T> = T extends Promise<infer V> ? V : never; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
69690
1251
240