piral-core 0.14.8-beta.3504 → 0.14.8-beta.3513

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.
Files changed (58) hide show
  1. package/esm/Piral.js +2 -0
  2. package/esm/Piral.js.map +1 -1
  3. package/esm/RootListener.d.ts +2 -0
  4. package/esm/RootListener.js +23 -0
  5. package/esm/RootListener.js.map +1 -0
  6. package/esm/components/ExtensionSlot.js +2 -2
  7. package/esm/components/ExtensionSlot.js.map +1 -1
  8. package/esm/modules/api.d.ts +3 -4
  9. package/esm/modules/api.js +1 -106
  10. package/esm/modules/api.js.map +1 -1
  11. package/esm/modules/core.d.ts +3 -0
  12. package/esm/modules/core.js +48 -0
  13. package/esm/modules/core.js.map +1 -0
  14. package/esm/modules/element.d.ts +5 -0
  15. package/esm/modules/element.js +83 -0
  16. package/esm/modules/element.js.map +1 -0
  17. package/esm/modules/index.d.ts +1 -0
  18. package/esm/modules/index.js +1 -0
  19. package/esm/modules/index.js.map +1 -1
  20. package/esm/types/extension.d.ts +7 -3
  21. package/esm/utils/extension.d.ts +13 -0
  22. package/esm/utils/extension.js +32 -0
  23. package/esm/utils/extension.js.map +1 -1
  24. package/lib/Piral.js +2 -0
  25. package/lib/Piral.js.map +1 -1
  26. package/lib/RootListener.d.ts +2 -0
  27. package/lib/RootListener.js +27 -0
  28. package/lib/RootListener.js.map +1 -0
  29. package/lib/components/ExtensionSlot.js +2 -2
  30. package/lib/components/ExtensionSlot.js.map +1 -1
  31. package/lib/modules/api.d.ts +3 -4
  32. package/lib/modules/api.js +3 -109
  33. package/lib/modules/api.js.map +1 -1
  34. package/lib/modules/core.d.ts +3 -0
  35. package/lib/modules/core.js +52 -0
  36. package/lib/modules/core.js.map +1 -0
  37. package/lib/modules/element.d.ts +5 -0
  38. package/lib/modules/element.js +87 -0
  39. package/lib/modules/element.js.map +1 -0
  40. package/lib/modules/index.d.ts +1 -0
  41. package/lib/modules/index.js +1 -0
  42. package/lib/modules/index.js.map +1 -1
  43. package/lib/types/extension.d.ts +7 -3
  44. package/lib/utils/extension.d.ts +13 -0
  45. package/lib/utils/extension.js +34 -1
  46. package/lib/utils/extension.js.map +1 -1
  47. package/package.json +4 -4
  48. package/src/Piral.tsx +2 -0
  49. package/src/RootListener.tsx +26 -0
  50. package/src/components/ExtensionSlot.tsx +2 -1
  51. package/src/modules/api.test.ts +15 -15
  52. package/src/modules/api.ts +3 -125
  53. package/src/modules/core.test.ts +148 -0
  54. package/src/modules/core.ts +50 -0
  55. package/src/modules/element.ts +103 -0
  56. package/src/modules/index.ts +1 -0
  57. package/src/types/extension.ts +7 -2
  58. package/src/utils/extension.tsx +41 -0
@@ -1,129 +1,7 @@
1
- import { isfunc, PiletApiCreator, PiletApiExtender, initializeApi, mergeApis } from 'piral-base';
1
+ import { isfunc, PiletApiCreator, initializeApi, mergeApis } from 'piral-base';
2
2
  import { __assign } from 'tslib';
3
- import { withApi } from '../state';
4
- import { ExtensionSlot } from '../components';
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
+ }
@@ -1,2 +1,3 @@
1
1
  export * from './api';
2
2
  export * from './dependencies';
3
+ export * from './element';
@@ -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?(): React.ReactNode;
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<React.ReactNode>): React.ReactElement<any, any> | null;
26
+ render?(nodes: Array<ReactNode>): ReactElement<any, any> | null;
22
27
  /**
23
28
  * The custom parameters for the given extension.
24
29
  */
@@ -1,6 +1,47 @@
1
1
  import * as React from 'react';
2
2
  import { ExtensionComponentProps, WrappedComponent } from '../types';
3
3
 
4
+ function removeAll(nodes: Array<ChildNode>) {
5
+ nodes.forEach((node) => node.remove());
6
+ }
7
+
8
+ interface SlotCarrierProps {
9
+ nodes: Array<ChildNode>;
10
+ }
11
+
12
+ const SlotCarrier: React.FC<SlotCarrierProps> = ({ nodes }) => {
13
+ const host = React.useRef<HTMLSlotElement>();
14
+
15
+ React.useEffect(() => {
16
+ host.current?.append(...nodes);
17
+ return () => removeAll(nodes);
18
+ }, [nodes]);
19
+
20
+ if (nodes.length) {
21
+ return <slot ref={host} />;
22
+ }
23
+
24
+ return null;
25
+ };
26
+
27
+ /**
28
+ * Transforms the given component to an extension component.
29
+ * @param Component The component to transform.
30
+ * @returns The extension component (receiving its props via params).
31
+ */
4
32
  export function toExtension<T>(Component: React.ComponentType<T>): WrappedComponent<ExtensionComponentProps<T>> {
5
33
  return (props) => <Component {...props.params} />;
6
34
  }
35
+
36
+ /**
37
+ * Reactifies the list of child nodes to a React Node by removing the
38
+ * nodes from the DOM and carrying it in a React Node, where it would be
39
+ * attached at a slot.
40
+ * @param childNodes The child nodes to reactify.
41
+ * @returns The React Node.
42
+ */
43
+ export function reactifyContent(childNodes: NodeListOf<ChildNode>): React.ReactNode {
44
+ const nodes: Array<ChildNode> = Array.prototype.filter.call(childNodes, Boolean);
45
+ removeAll(nodes);
46
+ return <SlotCarrier nodes={nodes} />;
47
+ }