piral-core 0.14.8-beta.3500 → 0.14.8-beta.3511
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/Piral.d.ts +2 -2
- package/esm/Piral.js +3 -1
- package/esm/Piral.js.map +1 -1
- package/esm/RootListener.d.ts +2 -0
- package/esm/RootListener.js +23 -0
- package/esm/RootListener.js.map +1 -0
- package/esm/components/ExtensionSlot.js +2 -2
- package/esm/components/ExtensionSlot.js.map +1 -1
- package/esm/components/Mediator.js +11 -8
- package/esm/components/Mediator.js.map +1 -1
- package/esm/components/ResponsiveLayout.js +3 -3
- package/esm/components/ResponsiveLayout.js.map +1 -1
- package/esm/components/SetComponent.js +2 -2
- package/esm/components/SetComponent.js.map +1 -1
- package/esm/components/SetError.js +2 -2
- package/esm/components/SetError.js.map +1 -1
- package/esm/components/SetProvider.js +2 -2
- package/esm/components/SetProvider.js.map +1 -1
- package/esm/components/SetRedirect.js +2 -2
- package/esm/components/SetRedirect.js.map +1 -1
- package/esm/components/SetRoute.js +2 -2
- package/esm/components/SetRoute.js.map +1 -1
- package/esm/createInstance.d.ts +2 -2
- package/esm/createInstance.js +3 -1
- package/esm/createInstance.js.map +1 -1
- package/esm/modules/api.d.ts +3 -4
- package/esm/modules/api.js +1 -106
- package/esm/modules/api.js.map +1 -1
- package/esm/modules/core.d.ts +3 -0
- package/esm/modules/core.js +48 -0
- package/esm/modules/core.js.map +1 -0
- package/esm/modules/element.d.ts +5 -0
- package/esm/modules/element.js +83 -0
- package/esm/modules/element.js.map +1 -0
- package/esm/modules/index.d.ts +1 -0
- package/esm/modules/index.js +1 -0
- package/esm/modules/index.js.map +1 -1
- package/esm/state/withApi.js +2 -3
- package/esm/state/withApi.js.map +1 -1
- package/esm/types/extension.d.ts +7 -3
- package/esm/types/instance.d.ts +18 -3
- package/esm/utils/extension.d.ts +13 -0
- package/esm/utils/extension.js +32 -0
- package/esm/utils/extension.js.map +1 -1
- package/lib/Piral.d.ts +2 -2
- package/lib/Piral.js +3 -1
- package/lib/Piral.js.map +1 -1
- package/lib/RootListener.d.ts +2 -0
- package/lib/RootListener.js +27 -0
- package/lib/RootListener.js.map +1 -0
- package/lib/components/ExtensionSlot.js +2 -2
- package/lib/components/ExtensionSlot.js.map +1 -1
- package/lib/components/Mediator.js +10 -7
- package/lib/components/Mediator.js.map +1 -1
- package/lib/components/ResponsiveLayout.js +2 -2
- package/lib/components/ResponsiveLayout.js.map +1 -1
- package/lib/components/SetComponent.js +1 -1
- package/lib/components/SetComponent.js.map +1 -1
- package/lib/components/SetError.js +1 -1
- package/lib/components/SetError.js.map +1 -1
- package/lib/components/SetProvider.js +1 -1
- package/lib/components/SetProvider.js.map +1 -1
- package/lib/components/SetRedirect.js +1 -1
- package/lib/components/SetRedirect.js.map +1 -1
- package/lib/components/SetRoute.js +1 -1
- package/lib/components/SetRoute.js.map +1 -1
- package/lib/createInstance.d.ts +2 -2
- package/lib/createInstance.js +3 -1
- package/lib/createInstance.js.map +1 -1
- package/lib/modules/api.d.ts +3 -4
- package/lib/modules/api.js +3 -109
- package/lib/modules/api.js.map +1 -1
- package/lib/modules/core.d.ts +3 -0
- package/lib/modules/core.js +52 -0
- package/lib/modules/core.js.map +1 -0
- package/lib/modules/element.d.ts +5 -0
- package/lib/modules/element.js +87 -0
- package/lib/modules/element.js.map +1 -0
- package/lib/modules/index.d.ts +1 -0
- package/lib/modules/index.js +1 -0
- package/lib/modules/index.js.map +1 -1
- package/lib/state/withApi.js +1 -2
- package/lib/state/withApi.js.map +1 -1
- package/lib/types/extension.d.ts +7 -3
- package/lib/types/instance.d.ts +18 -3
- package/lib/utils/extension.d.ts +13 -0
- package/lib/utils/extension.js +34 -1
- package/lib/utils/extension.js.map +1 -1
- package/package.json +4 -4
- package/src/Piral.tsx +5 -3
- package/src/RootListener.tsx +26 -0
- package/src/actions/app.ts +1 -1
- package/src/components/ExtensionSlot.tsx +2 -1
- package/src/components/Mediator.test.tsx +4 -3
- package/src/components/Mediator.tsx +15 -8
- package/src/components/ResponsiveLayout.test.tsx +15 -5
- package/src/components/ResponsiveLayout.tsx +3 -3
- package/src/components/SetComponent.tsx +2 -2
- package/src/components/SetError.tsx +2 -2
- package/src/components/SetProvider.tsx +2 -2
- package/src/components/SetRedirect.tsx +2 -2
- package/src/components/SetRoute.tsx +2 -2
- package/src/createInstance.tsx +5 -2
- package/src/modules/api.test.ts +15 -15
- package/src/modules/api.ts +3 -125
- package/src/modules/core.test.ts +148 -0
- package/src/modules/core.ts +50 -0
- package/src/modules/element.ts +103 -0
- package/src/modules/index.ts +1 -0
- package/src/state/withApi.tsx +2 -3
- package/src/types/extension.ts +7 -2
- package/src/types/instance.ts +19 -3
- package/src/utils/extension.tsx +41 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { Redirect } from 'react-router';
|
|
3
|
-
import {
|
|
3
|
+
import { useGlobalStateContext, useSetter } from '../hooks';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The props for the SetRedirect component.
|
|
@@ -20,7 +20,7 @@ export interface SetRedirectProps {
|
|
|
20
20
|
* The component capable of setting a global redirect route at mounting.
|
|
21
21
|
*/
|
|
22
22
|
export function SetRedirect({ from, to }: SetRedirectProps): React.ReactElement {
|
|
23
|
-
const setRoute =
|
|
23
|
+
const { setRoute } = useGlobalStateContext();
|
|
24
24
|
useSetter(() => setRoute(from, () => <Redirect to={to} />));
|
|
25
25
|
// tslint:disable-next-line:no-null-keyword
|
|
26
26
|
return null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import { RouteComponentProps } from 'react-router';
|
|
3
|
-
import {
|
|
3
|
+
import { useGlobalStateContext, useSetter } from '../hooks';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The props for the SetRoute component.
|
|
@@ -20,7 +20,7 @@ export interface SetRouteProps<T = {}> {
|
|
|
20
20
|
* The component capable of setting a global route at mounting.
|
|
21
21
|
*/
|
|
22
22
|
export function SetRoute<T = {}>({ path, component }: SetRouteProps<T>): React.ReactElement {
|
|
23
|
-
const setRoute =
|
|
23
|
+
const { setRoute } = useGlobalStateContext();
|
|
24
24
|
useSetter(() => component && setRoute(path, component));
|
|
25
25
|
// tslint:disable-next-line:no-null-keyword
|
|
26
26
|
return null;
|
package/src/createInstance.tsx
CHANGED
|
@@ -3,7 +3,8 @@ import { blazingStrategy, standardStrategy, createListener, isfunc } from 'piral
|
|
|
3
3
|
import { defaultApiFactory, defaultDependencySelector, defaultModuleRequester } from './modules';
|
|
4
4
|
import { createGlobalState, createActions, includeActions } from './state';
|
|
5
5
|
import { createPiletOptions } from './helpers';
|
|
6
|
-
import
|
|
6
|
+
import { generateId } from './utils';
|
|
7
|
+
import type { PiralInstanceOptions, PiralInstance } from './types';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Creates a new PiralInstance component, which can be used for
|
|
@@ -25,8 +26,9 @@ const app = (
|
|
|
25
26
|
render(app, document.querySelector('#app'));
|
|
26
27
|
```
|
|
27
28
|
*/
|
|
28
|
-
export function createInstance(config:
|
|
29
|
+
export function createInstance(config: PiralInstanceOptions = {}): PiralInstance {
|
|
29
30
|
const {
|
|
31
|
+
id = generateId(),
|
|
30
32
|
state,
|
|
31
33
|
actions,
|
|
32
34
|
availablePilets = [],
|
|
@@ -71,6 +73,7 @@ export function createInstance(config: PiralConfiguration = {}): PiralInstance {
|
|
|
71
73
|
context.options = options;
|
|
72
74
|
|
|
73
75
|
return __assign(events, {
|
|
76
|
+
id,
|
|
74
77
|
createApi,
|
|
75
78
|
context,
|
|
76
79
|
root,
|
package/src/modules/api.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createElement, FC } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { defaultApiFactory } from './api';
|
|
3
3
|
|
|
4
4
|
jest.mock('../hooks');
|
|
5
5
|
|
|
@@ -21,21 +21,21 @@ function createMockContainer() {
|
|
|
21
21
|
off: jest.fn(),
|
|
22
22
|
emit: jest.fn(),
|
|
23
23
|
converters: {},
|
|
24
|
+
apis: {},
|
|
24
25
|
readState() {
|
|
25
26
|
return undefined;
|
|
26
27
|
},
|
|
27
28
|
} as any,
|
|
28
|
-
api: {} as any,
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function createApi(container) {
|
|
33
|
-
|
|
34
|
-
return
|
|
32
|
+
function createApi(container, apis = []) {
|
|
33
|
+
const api = defaultApiFactory(container.context, apis);
|
|
34
|
+
return api(moduleMetadata);
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
describe('API Module', () => {
|
|
38
|
-
it('
|
|
38
|
+
it('defaultApiFactory can register and unregister a page', () => {
|
|
39
39
|
const container = createMockContainer();
|
|
40
40
|
container.context.registerPage = jest.fn();
|
|
41
41
|
container.context.unregisterPage = jest.fn();
|
|
@@ -48,7 +48,7 @@ describe('API Module', () => {
|
|
|
48
48
|
expect(container.context.unregisterPage.mock.calls[0][0]).toBe(container.context.registerPage.mock.calls[0][0]);
|
|
49
49
|
});
|
|
50
50
|
|
|
51
|
-
it('
|
|
51
|
+
it('defaultApiFactory can dispose a registered page', () => {
|
|
52
52
|
const container = createMockContainer();
|
|
53
53
|
container.context.registerPage = jest.fn();
|
|
54
54
|
container.context.unregisterPage = jest.fn();
|
|
@@ -61,7 +61,7 @@ describe('API Module', () => {
|
|
|
61
61
|
expect(container.context.unregisterPage.mock.calls[0][0]).toBe(container.context.registerPage.mock.calls[0][0]);
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
-
it('
|
|
64
|
+
it('defaultApiFactory can register and unregister an extension', () => {
|
|
65
65
|
const container = createMockContainer();
|
|
66
66
|
container.context.registerExtension = jest.fn();
|
|
67
67
|
container.context.unregisterExtension = jest.fn();
|
|
@@ -76,7 +76,7 @@ describe('API Module', () => {
|
|
|
76
76
|
);
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
it('
|
|
79
|
+
it('defaultApiFactory can dispose an registered extension', () => {
|
|
80
80
|
const container = createMockContainer();
|
|
81
81
|
container.context.registerExtension = jest.fn();
|
|
82
82
|
container.context.unregisterExtension = jest.fn();
|
|
@@ -91,7 +91,7 @@ describe('API Module', () => {
|
|
|
91
91
|
);
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
it('
|
|
94
|
+
it('defaultApiFactory read data by its name', () => {
|
|
95
95
|
const container = createMockContainer();
|
|
96
96
|
container.context.readDataValue = jest.fn((name) => name);
|
|
97
97
|
const api = createApi(container);
|
|
@@ -100,7 +100,7 @@ describe('API Module', () => {
|
|
|
100
100
|
expect(container.context.readDataValue).toHaveBeenCalled();
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
it('
|
|
103
|
+
it('defaultApiFactory write data without options shall pass, but memory should not emit events', () => {
|
|
104
104
|
const container = createMockContainer();
|
|
105
105
|
container.context.tryWriteDataItem = jest.fn(() => true);
|
|
106
106
|
const api = createApi(container);
|
|
@@ -109,7 +109,7 @@ describe('API Module', () => {
|
|
|
109
109
|
expect(container.context.emit).not.toHaveBeenCalled();
|
|
110
110
|
});
|
|
111
111
|
|
|
112
|
-
it('
|
|
112
|
+
it('defaultApiFactory write data with empty options shall pass, but memory should not emit events', () => {
|
|
113
113
|
const container = createMockContainer();
|
|
114
114
|
container.context.tryWriteDataItem = jest.fn(() => true);
|
|
115
115
|
const api = createApi(container);
|
|
@@ -118,7 +118,7 @@ describe('API Module', () => {
|
|
|
118
118
|
expect(container.context.emit).not.toHaveBeenCalled();
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
it('
|
|
121
|
+
it('defaultApiFactory write data by the simple option should not pass, never emitting events', () => {
|
|
122
122
|
const container = createMockContainer();
|
|
123
123
|
container.context.tryWriteDataItem = jest.fn(() => false);
|
|
124
124
|
const api = createApi(container);
|
|
@@ -127,7 +127,7 @@ describe('API Module', () => {
|
|
|
127
127
|
expect(container.context.emit).not.toHaveBeenCalled();
|
|
128
128
|
});
|
|
129
129
|
|
|
130
|
-
it('
|
|
130
|
+
it('defaultApiFactory write data by the simple option shall pass with remote', () => {
|
|
131
131
|
const container = createMockContainer();
|
|
132
132
|
container.context.tryWriteDataItem = jest.fn(() => true);
|
|
133
133
|
const api = createApi(container);
|
|
@@ -135,7 +135,7 @@ describe('API Module', () => {
|
|
|
135
135
|
expect(container.context.tryWriteDataItem).toHaveBeenCalled();
|
|
136
136
|
});
|
|
137
137
|
|
|
138
|
-
it('
|
|
138
|
+
it('defaultApiFactory write data by the object options shall pass with remote', () => {
|
|
139
139
|
const container = createMockContainer();
|
|
140
140
|
container.context.tryWriteDataItem = jest.fn(() => true);
|
|
141
141
|
const api = createApi(container);
|
package/src/modules/api.ts
CHANGED
|
@@ -1,129 +1,7 @@
|
|
|
1
|
-
import { isfunc, PiletApiCreator,
|
|
1
|
+
import { isfunc, PiletApiCreator, initializeApi, mergeApis } from 'piral-base';
|
|
2
2
|
import { __assign } from 'tslib';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { createDataOptions, getDataExpiration, renderInDom, tryParseJson, changeDomPortal, noop } from '../utils';
|
|
6
|
-
import { Disposable, GlobalStateContext, PiletCoreApi, PiralPlugin } from '../types';
|
|
7
|
-
|
|
8
|
-
interface Updatable {
|
|
9
|
-
(newProps: any): void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
if (typeof window !== 'undefined' && 'customElements' in window) {
|
|
13
|
-
class PiralExtension extends HTMLElement {
|
|
14
|
-
dispose: Disposable = noop;
|
|
15
|
-
update: Updatable = noop;
|
|
16
|
-
|
|
17
|
-
getProps() {
|
|
18
|
-
const name = this.getAttribute('name');
|
|
19
|
-
const params = tryParseJson(this.getAttribute('params'));
|
|
20
|
-
return { name, params };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
connectedCallback() {
|
|
24
|
-
if (this.isConnected) {
|
|
25
|
-
this.dispatchEvent(
|
|
26
|
-
new CustomEvent('render-html', {
|
|
27
|
-
bubbles: true,
|
|
28
|
-
detail: {
|
|
29
|
-
target: this,
|
|
30
|
-
props: this.getProps(),
|
|
31
|
-
},
|
|
32
|
-
}),
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
disconnectedCallback() {
|
|
38
|
-
this.dispose();
|
|
39
|
-
this.dispose = noop;
|
|
40
|
-
this.update = noop;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
attributeChangedCallback() {
|
|
44
|
-
this.update(this.getProps());
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
static get observedAttributes() {
|
|
48
|
-
return ['name', 'params'];
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
customElements.define('piral-extension', PiralExtension);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function render(context: GlobalStateContext, element: HTMLElement | ShadowRoot, props: any): [Disposable, Updatable] {
|
|
56
|
-
let [id, portal] = renderInDom(context, element, ExtensionSlot, props);
|
|
57
|
-
const evName = 'extension-props-changed';
|
|
58
|
-
const handler = (ev: CustomEvent) => update(ev.detail);
|
|
59
|
-
const dispose: Disposable = () => {
|
|
60
|
-
context.hidePortal(id, portal);
|
|
61
|
-
element.removeEventListener(evName, handler);
|
|
62
|
-
};
|
|
63
|
-
const update: Updatable = (newProps) => {
|
|
64
|
-
[id, portal] = changeDomPortal(id, portal, context, element, ExtensionSlot, newProps);
|
|
65
|
-
};
|
|
66
|
-
element.addEventListener(evName, handler);
|
|
67
|
-
return [dispose, update];
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function createCoreApi(context: GlobalStateContext): PiletApiExtender<PiletCoreApi> {
|
|
71
|
-
if (typeof document !== 'undefined') {
|
|
72
|
-
document.body.addEventListener(
|
|
73
|
-
'render-html',
|
|
74
|
-
(ev: CustomEvent) => {
|
|
75
|
-
ev.stopPropagation();
|
|
76
|
-
const container = ev.detail.target;
|
|
77
|
-
const [dispose, update] = render(context, container, ev.detail.props);
|
|
78
|
-
container.dispose = dispose;
|
|
79
|
-
container.update = update;
|
|
80
|
-
},
|
|
81
|
-
false,
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return (api, target) => {
|
|
86
|
-
const pilet = target.name;
|
|
87
|
-
return {
|
|
88
|
-
getData(name) {
|
|
89
|
-
return context.readDataValue(name);
|
|
90
|
-
},
|
|
91
|
-
setData(name, value, options) {
|
|
92
|
-
const { target = 'memory', expires } = createDataOptions(options);
|
|
93
|
-
const expiration = getDataExpiration(expires);
|
|
94
|
-
return context.tryWriteDataItem(name, value, pilet, target, expiration);
|
|
95
|
-
},
|
|
96
|
-
registerPage(route, arg, meta) {
|
|
97
|
-
context.registerPage(route, {
|
|
98
|
-
pilet,
|
|
99
|
-
meta,
|
|
100
|
-
component: withApi(context, arg, api, 'page'),
|
|
101
|
-
});
|
|
102
|
-
return () => api.unregisterPage(route);
|
|
103
|
-
},
|
|
104
|
-
unregisterPage(route) {
|
|
105
|
-
context.unregisterPage(route);
|
|
106
|
-
},
|
|
107
|
-
registerExtension(name, arg, defaults) {
|
|
108
|
-
context.registerExtension(name as string, {
|
|
109
|
-
pilet,
|
|
110
|
-
component: withApi(context, arg, api, 'extension'),
|
|
111
|
-
reference: arg,
|
|
112
|
-
defaults,
|
|
113
|
-
});
|
|
114
|
-
return () => api.unregisterExtension(name, arg);
|
|
115
|
-
},
|
|
116
|
-
unregisterExtension(name, arg) {
|
|
117
|
-
context.unregisterExtension(name as string, arg);
|
|
118
|
-
},
|
|
119
|
-
renderHtmlExtension(element, props) {
|
|
120
|
-
const [dispose] = render(context, element, props);
|
|
121
|
-
return dispose;
|
|
122
|
-
},
|
|
123
|
-
Extension: ExtensionSlot,
|
|
124
|
-
};
|
|
125
|
-
};
|
|
126
|
-
}
|
|
3
|
+
import { createCoreApi } from './core';
|
|
4
|
+
import { GlobalStateContext, PiralPlugin } from '../types';
|
|
127
5
|
|
|
128
6
|
export function createExtenders(context: GlobalStateContext, apis: Array<PiralPlugin>) {
|
|
129
7
|
const creators: Array<PiralPlugin> = [createCoreApi, ...apis.filter(isfunc)];
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { createElement, FC } from 'react';
|
|
2
|
+
import { createCoreApi } from './core';
|
|
3
|
+
|
|
4
|
+
jest.mock('../hooks');
|
|
5
|
+
|
|
6
|
+
const StubComponent: FC = (props) => createElement('div', props);
|
|
7
|
+
StubComponent.displayName = 'StubComponent';
|
|
8
|
+
|
|
9
|
+
const moduleMetadata = {
|
|
10
|
+
name: 'my-module',
|
|
11
|
+
version: '1.0.0',
|
|
12
|
+
link: undefined,
|
|
13
|
+
custom: undefined,
|
|
14
|
+
hash: '123',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function createMockContainer() {
|
|
18
|
+
return {
|
|
19
|
+
context: {
|
|
20
|
+
on: jest.fn(),
|
|
21
|
+
off: jest.fn(),
|
|
22
|
+
emit: jest.fn(),
|
|
23
|
+
converters: {},
|
|
24
|
+
readState() {
|
|
25
|
+
return undefined;
|
|
26
|
+
},
|
|
27
|
+
} as any,
|
|
28
|
+
api: {} as any,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createApi(container) {
|
|
33
|
+
Object.assign(container.api, createCoreApi(container.context)(container.api, moduleMetadata));
|
|
34
|
+
return container.api;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('Core API Module', () => {
|
|
38
|
+
it('createCoreApi can register and unregister a page', () => {
|
|
39
|
+
const container = createMockContainer();
|
|
40
|
+
container.context.registerPage = jest.fn();
|
|
41
|
+
container.context.unregisterPage = jest.fn();
|
|
42
|
+
const api = createApi(container);
|
|
43
|
+
api.registerPage('/route', StubComponent);
|
|
44
|
+
expect(container.context.registerPage).toHaveBeenCalledTimes(1);
|
|
45
|
+
expect(container.context.unregisterPage).toHaveBeenCalledTimes(0);
|
|
46
|
+
api.unregisterPage('/route');
|
|
47
|
+
expect(container.context.unregisterPage).toHaveBeenCalledTimes(1);
|
|
48
|
+
expect(container.context.unregisterPage.mock.calls[0][0]).toBe(container.context.registerPage.mock.calls[0][0]);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('createCoreApi can dispose a registered page', () => {
|
|
52
|
+
const container = createMockContainer();
|
|
53
|
+
container.context.registerPage = jest.fn();
|
|
54
|
+
container.context.unregisterPage = jest.fn();
|
|
55
|
+
const api = createApi(container);
|
|
56
|
+
const dispose = api.registerPage('/route', StubComponent);
|
|
57
|
+
expect(container.context.registerPage).toHaveBeenCalledTimes(1);
|
|
58
|
+
expect(container.context.unregisterPage).toHaveBeenCalledTimes(0);
|
|
59
|
+
dispose();
|
|
60
|
+
expect(container.context.unregisterPage).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(container.context.unregisterPage.mock.calls[0][0]).toBe(container.context.registerPage.mock.calls[0][0]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('createCoreApi can register and unregister an extension', () => {
|
|
65
|
+
const container = createMockContainer();
|
|
66
|
+
container.context.registerExtension = jest.fn();
|
|
67
|
+
container.context.unregisterExtension = jest.fn();
|
|
68
|
+
const api = createApi(container);
|
|
69
|
+
api.registerExtension('ext', StubComponent);
|
|
70
|
+
expect(container.context.registerExtension).toHaveBeenCalledTimes(1);
|
|
71
|
+
expect(container.context.unregisterExtension).toHaveBeenCalledTimes(0);
|
|
72
|
+
api.unregisterExtension('ext', StubComponent);
|
|
73
|
+
expect(container.context.unregisterExtension).toHaveBeenCalledTimes(1);
|
|
74
|
+
expect(container.context.unregisterExtension.mock.calls[0][0]).toBe(
|
|
75
|
+
container.context.registerExtension.mock.calls[0][0],
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('createCoreApi can dispose an registered extension', () => {
|
|
80
|
+
const container = createMockContainer();
|
|
81
|
+
container.context.registerExtension = jest.fn();
|
|
82
|
+
container.context.unregisterExtension = jest.fn();
|
|
83
|
+
const api = createApi(container);
|
|
84
|
+
const dispose = api.registerExtension('ext', StubComponent);
|
|
85
|
+
expect(container.context.registerExtension).toHaveBeenCalledTimes(1);
|
|
86
|
+
expect(container.context.unregisterExtension).toHaveBeenCalledTimes(0);
|
|
87
|
+
dispose();
|
|
88
|
+
expect(container.context.unregisterExtension).toHaveBeenCalledTimes(1);
|
|
89
|
+
expect(container.context.unregisterExtension.mock.calls[0][0]).toBe(
|
|
90
|
+
container.context.registerExtension.mock.calls[0][0],
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('createCoreApi read data by its name', () => {
|
|
95
|
+
const container = createMockContainer();
|
|
96
|
+
container.context.readDataValue = jest.fn((name) => name);
|
|
97
|
+
const api = createApi(container);
|
|
98
|
+
const result = api.getData('foo');
|
|
99
|
+
expect(result).toBe('foo');
|
|
100
|
+
expect(container.context.readDataValue).toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('createCoreApi write data without options shall pass, but memory should not emit events', () => {
|
|
104
|
+
const container = createMockContainer();
|
|
105
|
+
container.context.tryWriteDataItem = jest.fn(() => true);
|
|
106
|
+
const api = createApi(container);
|
|
107
|
+
api.setData('foo', 5);
|
|
108
|
+
expect(container.context.tryWriteDataItem).toHaveBeenCalled();
|
|
109
|
+
expect(container.context.emit).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('createCoreApi write data with empty options shall pass, but memory should not emit events', () => {
|
|
113
|
+
const container = createMockContainer();
|
|
114
|
+
container.context.tryWriteDataItem = jest.fn(() => true);
|
|
115
|
+
const api = createApi(container);
|
|
116
|
+
api.setData('foo', 5, {});
|
|
117
|
+
expect(container.context.tryWriteDataItem).toHaveBeenCalled();
|
|
118
|
+
expect(container.context.emit).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('createCoreApi write data by the simple option should not pass, never emitting events', () => {
|
|
122
|
+
const container = createMockContainer();
|
|
123
|
+
container.context.tryWriteDataItem = jest.fn(() => false);
|
|
124
|
+
const api = createApi(container);
|
|
125
|
+
api.setData('foo', 5, 'remote');
|
|
126
|
+
expect(container.context.tryWriteDataItem).toHaveBeenCalled();
|
|
127
|
+
expect(container.context.emit).not.toHaveBeenCalled();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('createCoreApi write data by the simple option shall pass with remote', () => {
|
|
131
|
+
const container = createMockContainer();
|
|
132
|
+
container.context.tryWriteDataItem = jest.fn(() => true);
|
|
133
|
+
const api = createApi(container);
|
|
134
|
+
api.setData('foo', 5, 'remote');
|
|
135
|
+
expect(container.context.tryWriteDataItem).toHaveBeenCalled();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('createCoreApi write data by the object options shall pass with remote', () => {
|
|
139
|
+
const container = createMockContainer();
|
|
140
|
+
container.context.tryWriteDataItem = jest.fn(() => true);
|
|
141
|
+
const api = createApi(container);
|
|
142
|
+
api.setData('foo', 15, {
|
|
143
|
+
expires: 10,
|
|
144
|
+
target: 'local',
|
|
145
|
+
});
|
|
146
|
+
expect(container.context.tryWriteDataItem).toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { PiletApiExtender } from 'piral-base';
|
|
2
|
+
import { renderElement } from './element';
|
|
3
|
+
import { withApi } from '../state';
|
|
4
|
+
import { ExtensionSlot } from '../components';
|
|
5
|
+
import { createDataOptions, getDataExpiration } from '../utils';
|
|
6
|
+
import { GlobalStateContext, PiletCoreApi } from '../types';
|
|
7
|
+
|
|
8
|
+
export function createCoreApi(context: GlobalStateContext): PiletApiExtender<PiletCoreApi> {
|
|
9
|
+
return (api, meta) => {
|
|
10
|
+
const pilet = meta.name;
|
|
11
|
+
return {
|
|
12
|
+
getData(name) {
|
|
13
|
+
return context.readDataValue(name);
|
|
14
|
+
},
|
|
15
|
+
setData(name, value, options) {
|
|
16
|
+
const { target = 'memory', expires } = createDataOptions(options);
|
|
17
|
+
const expiration = getDataExpiration(expires);
|
|
18
|
+
return context.tryWriteDataItem(name, value, pilet, target, expiration);
|
|
19
|
+
},
|
|
20
|
+
registerPage(route, arg, meta) {
|
|
21
|
+
context.registerPage(route, {
|
|
22
|
+
pilet,
|
|
23
|
+
meta,
|
|
24
|
+
component: withApi(context, arg, api, 'page'),
|
|
25
|
+
});
|
|
26
|
+
return () => api.unregisterPage(route);
|
|
27
|
+
},
|
|
28
|
+
unregisterPage(route) {
|
|
29
|
+
context.unregisterPage(route);
|
|
30
|
+
},
|
|
31
|
+
registerExtension(name, arg, defaults) {
|
|
32
|
+
context.registerExtension(name as string, {
|
|
33
|
+
pilet,
|
|
34
|
+
component: withApi(context, arg, api, 'extension'),
|
|
35
|
+
reference: arg,
|
|
36
|
+
defaults,
|
|
37
|
+
});
|
|
38
|
+
return () => api.unregisterExtension(name, arg);
|
|
39
|
+
},
|
|
40
|
+
unregisterExtension(name, arg) {
|
|
41
|
+
context.unregisterExtension(name as string, arg);
|
|
42
|
+
},
|
|
43
|
+
renderHtmlExtension(element, props) {
|
|
44
|
+
const [dispose] = renderElement(context, element, props);
|
|
45
|
+
return dispose;
|
|
46
|
+
},
|
|
47
|
+
Extension: ExtensionSlot,
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { ExtensionSlot } from '../components';
|
|
2
|
+
import { tryParseJson, noop, reactifyContent, renderInDom, changeDomPortal } from '../utils';
|
|
3
|
+
import { Disposable, GlobalStateContext } from '../types';
|
|
4
|
+
|
|
5
|
+
export interface Updatable {
|
|
6
|
+
(newProps: any): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (typeof window !== 'undefined' && 'customElements' in window) {
|
|
10
|
+
class PiralExtension extends HTMLElement {
|
|
11
|
+
dispose: Disposable = noop;
|
|
12
|
+
update: Updatable = noop;
|
|
13
|
+
props = {
|
|
14
|
+
name: this.getAttribute('name'),
|
|
15
|
+
params: tryParseJson(this.getAttribute('params')),
|
|
16
|
+
empty: undefined,
|
|
17
|
+
children: reactifyContent(this.childNodes),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
get params() {
|
|
21
|
+
return this.props.params;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
set params(value) {
|
|
25
|
+
this.props.params = value;
|
|
26
|
+
this.update(this.props);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get name() {
|
|
30
|
+
return this.props.name;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
set name(value) {
|
|
34
|
+
this.props.name = value;
|
|
35
|
+
this.update(this.props);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get empty() {
|
|
39
|
+
return this.props.empty;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
set empty(value) {
|
|
43
|
+
this.props.empty = value;
|
|
44
|
+
this.update(this.props);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
connectedCallback() {
|
|
48
|
+
if (this.isConnected) {
|
|
49
|
+
this.dispatchEvent(
|
|
50
|
+
new CustomEvent('render-html', {
|
|
51
|
+
bubbles: true,
|
|
52
|
+
detail: {
|
|
53
|
+
target: this,
|
|
54
|
+
props: this.props,
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
disconnectedCallback() {
|
|
62
|
+
this.dispose();
|
|
63
|
+
this.dispose = noop;
|
|
64
|
+
this.update = noop;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
attributeChangedCallback(name: string, _: any, newValue: any) {
|
|
68
|
+
switch (name) {
|
|
69
|
+
case 'name':
|
|
70
|
+
this.name = newValue;
|
|
71
|
+
break;
|
|
72
|
+
case 'params':
|
|
73
|
+
this.params = tryParseJson(newValue);
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
static get observedAttributes() {
|
|
79
|
+
return ['name', 'params'];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
customElements.define('piral-extension', PiralExtension);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function renderElement(
|
|
87
|
+
context: GlobalStateContext,
|
|
88
|
+
element: HTMLElement | ShadowRoot,
|
|
89
|
+
props: any,
|
|
90
|
+
): [Disposable, Updatable] {
|
|
91
|
+
let [id, portal] = renderInDom(context, element, ExtensionSlot, props);
|
|
92
|
+
const evName = 'extension-props-changed';
|
|
93
|
+
const handler = (ev: CustomEvent) => update(ev.detail);
|
|
94
|
+
const dispose: Disposable = () => {
|
|
95
|
+
context.hidePortal(id, portal);
|
|
96
|
+
element.removeEventListener(evName, handler);
|
|
97
|
+
};
|
|
98
|
+
const update: Updatable = (newProps) => {
|
|
99
|
+
[id, portal] = changeDomPortal(id, portal, context, element, ExtensionSlot, newProps);
|
|
100
|
+
};
|
|
101
|
+
element.addEventListener(evName, handler);
|
|
102
|
+
return [dispose, update];
|
|
103
|
+
}
|
package/src/modules/index.ts
CHANGED
package/src/state/withApi.tsx
CHANGED
|
@@ -2,7 +2,7 @@ import * as React from 'react';
|
|
|
2
2
|
import { isfunc } from 'piral-base';
|
|
3
3
|
import { __RouterContext } from 'react-router';
|
|
4
4
|
import { PiralError, PiralLoadingIndicator, ErrorBoundary, ErrorBoundaryOptions, PortalRenderer } from '../components';
|
|
5
|
-
import {
|
|
5
|
+
import { useGlobalStateContext } from '../hooks';
|
|
6
6
|
import { defaultRender, convertComponent, none } from '../utils';
|
|
7
7
|
import {
|
|
8
8
|
AnyComponent,
|
|
@@ -107,8 +107,7 @@ function wrapForeignComponent<T>(
|
|
|
107
107
|
Wrapper: React.ComponentType<any>,
|
|
108
108
|
) {
|
|
109
109
|
return React.memo((props: T) => {
|
|
110
|
-
const { destroyPortal } =
|
|
111
|
-
const { state, readState } = useGlobalStateContext();
|
|
110
|
+
const { state, readState, destroyPortal } = useGlobalStateContext();
|
|
112
111
|
const router = React.useContext(__RouterContext);
|
|
113
112
|
const id = React.useMemo(() => (portalIdBase++).toString(26), none);
|
|
114
113
|
const context = React.useMemo(() => ({ router, state, readState }), [router, state]);
|
package/src/types/extension.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReactNode, ReactElement } from 'react';
|
|
1
2
|
import type { PiralCustomExtensionSlotMap } from './custom';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -9,16 +10,20 @@ export interface PiralExtensionSlotMap extends PiralCustomExtensionSlotMap {}
|
|
|
9
10
|
* The basic props for defining an extension slot.
|
|
10
11
|
*/
|
|
11
12
|
export interface BaseExtensionSlotProps<TName, TParams> {
|
|
13
|
+
/**
|
|
14
|
+
* The children to transport, if any.
|
|
15
|
+
*/
|
|
16
|
+
children?: ReactNode;
|
|
12
17
|
/**
|
|
13
18
|
* Defines what should be rendered when no components are available
|
|
14
19
|
* for the specified extension.
|
|
15
20
|
*/
|
|
16
|
-
empty?():
|
|
21
|
+
empty?(): ReactNode;
|
|
17
22
|
/**
|
|
18
23
|
* Defines how the provided nodes should be rendered.
|
|
19
24
|
* @param nodes The rendered extension nodes.
|
|
20
25
|
*/
|
|
21
|
-
render?(nodes: Array<
|
|
26
|
+
render?(nodes: Array<ReactNode>): ReactElement<any, any> | null;
|
|
22
27
|
/**
|
|
23
28
|
* The custom parameters for the given extension.
|
|
24
29
|
*/
|