@servicetitan/ko-bridge 23.1.0 → 23.2.1
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/dist/html-comment.d.ts +1 -1
- package/dist/html-comment.d.ts.map +1 -1
- package/dist/knockout-component.d.ts +1 -1
- package/dist/knockout-component.d.ts.map +1 -1
- package/dist/ko-binding-handlers.d.ts +5 -11
- package/dist/ko-binding-handlers.d.ts.map +1 -1
- package/dist/ko-binding-handlers.js +57 -19
- package/dist/ko-binding-handlers.js.map +1 -1
- package/package.json +4 -2
- package/src/__tests__/ko-binding-handlers.test.tsx +218 -0
- package/src/html-comment.tsx +1 -1
- package/src/knockout-component.tsx +1 -1
- package/src/ko-binding-handlers.tsx +100 -0
- package/src/ko-binding-handlers.ts +0 -34
package/dist/html-comment.d.ts
CHANGED
|
@@ -12,6 +12,6 @@ export declare class HtmlComment extends Component<CommentProps> {
|
|
|
12
12
|
componentDidMount(): void;
|
|
13
13
|
componentDidUpdate(): void;
|
|
14
14
|
componentWillUnmount(): void;
|
|
15
|
-
render():
|
|
15
|
+
render(): JSX.Element;
|
|
16
16
|
}
|
|
17
17
|
//# sourceMappingURL=html-comment.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html-comment.d.ts","sourceRoot":"","sources":["../src/html-comment.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7C,MAAM,WAAW,YAAY;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,WAAY,SAAQ,SAAS,CAAC,YAAY,CAAC;IACpD,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,GAAG,CAA4B;IAEvC,iBAAiB;IAUjB,kBAAkB;IAWlB,oBAAoB;IAWpB,MAAM;
|
|
1
|
+
{"version":3,"file":"html-comment.d.ts","sourceRoot":"","sources":["../src/html-comment.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAa,SAAS,EAAE,MAAM,OAAO,CAAC;AAE7C,MAAM,WAAW,YAAY;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,qBAAa,WAAY,SAAQ,SAAS,CAAC,YAAY,CAAC;IACpD,OAAO,CAAC,UAAU,CAAqB;IACvC,OAAO,CAAC,WAAW,CAAqB;IACxC,OAAO,CAAC,QAAQ,CAAqB;IACrC,OAAO,CAAC,GAAG,CAA4B;IAEvC,iBAAiB;IAUjB,kBAAkB;IAWlB,oBAAoB;IAWpB,MAAM,IAAI,GAAG,CAAC,OAAO;CAIxB"}
|
|
@@ -16,7 +16,7 @@ export declare class KnockoutComponent extends Component<KnockoutComponentProps>
|
|
|
16
16
|
getSnapshotBeforeUpdate(): null;
|
|
17
17
|
componentDidUpdate(): void;
|
|
18
18
|
componentWillUnmount(): void;
|
|
19
|
-
render():
|
|
19
|
+
render(): JSX.Element;
|
|
20
20
|
private isModelValid;
|
|
21
21
|
private initNode;
|
|
22
22
|
private initOrigVisibility;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"knockout-component.d.ts","sourceRoot":"","sources":["../src/knockout-component.tsx"],"names":[],"mappings":"AACA,OAAO,EACH,SAAS,EAET,yBAAyB,EACzB,eAAe,EACf,aAAa,EAChB,MAAM,OAAO,CAAC;AAIf,MAAM,WAAW,sBAAsB;IACnC,OAAO,EAAE,GAAG,CAAC;IACb,UAAU,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAClC,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,aAAK,gBAAgB,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,GAAG,EAAE,IAE/C,yBAAyB,CAAC,eAAe,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;AAErE,qBAAa,iBAAkB,SAAQ,SAAS,CAAC,sBAAsB,CAAC;IACpE,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAElD;IACF,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,GAAG,CAA4B;IACvC,OAAO,CAAC,IAAI,CAA4B;IACxC,OAAO,CAAC,cAAc,CAAC,CAAS;IAChC,iBAAiB;IAMjB,qBAAqB,CAAC,SAAS,EAAE,sBAAsB;IAkBvD,uBAAuB;IAIvB,kBAAkB;IAIlB,oBAAoB;IAGpB,MAAM;
|
|
1
|
+
{"version":3,"file":"knockout-component.d.ts","sourceRoot":"","sources":["../src/knockout-component.tsx"],"names":[],"mappings":"AACA,OAAO,EACH,SAAS,EAET,yBAAyB,EACzB,eAAe,EACf,aAAa,EAChB,MAAM,OAAO,CAAC;AAIf,MAAM,WAAW,sBAAsB;IACnC,OAAO,EAAE,GAAG,CAAC;IACb,UAAU,EAAE,gBAAgB,CAAC,GAAG,CAAC,CAAC;IAClC,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,aAAK,gBAAgB,CAAC,CAAC,SAAS,WAAW,EAAE,CAAC,GAAG,EAAE,IAE/C,yBAAyB,CAAC,eAAe,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC;AAErE,qBAAa,iBAAkB,SAAQ,SAAS,CAAC,sBAAsB,CAAC;IACpE,MAAM,CAAC,YAAY,EAAE,OAAO,CAAC,sBAAsB,CAAC,CAElD;IACF,OAAO,CAAC,gBAAgB,CAAS;IACjC,OAAO,CAAC,GAAG,CAA4B;IACvC,OAAO,CAAC,IAAI,CAA4B;IACxC,OAAO,CAAC,cAAc,CAAC,CAAS;IAChC,iBAAiB;IAMjB,qBAAqB,CAAC,SAAS,EAAE,sBAAsB;IAkBvD,uBAAuB;IAIvB,kBAAkB;IAIlB,oBAAoB;IAGpB,MAAM,IAAI,GAAG,CAAC,OAAO;IAKrB,OAAO,CAAC,YAAY;IAGpB,OAAO,CAAC,QAAQ;IAYhB,OAAO,CAAC,kBAAkB;IAG1B,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,YAAY;CASvB"}
|
|
@@ -1,14 +1,8 @@
|
|
|
1
|
+
/// <reference types="knockout" />
|
|
2
|
+
declare type KO = typeof import('knockout');
|
|
1
3
|
export declare const koBindingHandlers: {
|
|
2
|
-
reactUncontrolled: (ko:
|
|
3
|
-
|
|
4
|
-
controlsDescendantBindings: boolean;
|
|
5
|
-
};
|
|
6
|
-
};
|
|
7
|
-
react: (ko: any) => {
|
|
8
|
-
init: (el: any) => {
|
|
9
|
-
controlsDescendantBindings: boolean;
|
|
10
|
-
};
|
|
11
|
-
update: (el: Element, valueAccessor: any, allBindings: any, viewModel: any) => void;
|
|
12
|
-
};
|
|
4
|
+
reactUncontrolled: (ko: KO) => KnockoutBindingHandler;
|
|
5
|
+
react: (ko: KO) => KnockoutBindingHandler;
|
|
13
6
|
};
|
|
7
|
+
export {};
|
|
14
8
|
//# sourceMappingURL=ko-binding-handlers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ko-binding-handlers.d.ts","sourceRoot":"","sources":["../src/ko-binding-handlers.
|
|
1
|
+
{"version":3,"file":"ko-binding-handlers.d.ts","sourceRoot":"","sources":["../src/ko-binding-handlers.tsx"],"names":[],"mappings":";AAIA,aAAK,EAAE,GAAG,cAAc,UAAU,CAAC,CAAC;AAapC,eAAO,MAAM,iBAAiB;4BAEF,EAAE,KAAG,sBAAsB;gBAsBvC,EAAE,KAAG,sBAAsB;CAoB1C,CAAC"}
|
|
@@ -1,34 +1,72 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Fragment, useEffect } from 'react';
|
|
3
|
+
import ReactDOM from 'react-dom';
|
|
4
|
+
let createRoot;
|
|
5
|
+
try {
|
|
6
|
+
/**
|
|
7
|
+
* Note, the webpack.IgnorePlugin must be configured to ignore this dependency.
|
|
8
|
+
* @see {@link file://./../../startup/src/webpack/configs/plugins/ignore-plugin/ignore-plugin.ts}
|
|
9
|
+
*/
|
|
10
|
+
createRoot = require('react-dom/client').createRoot;
|
|
11
|
+
}
|
|
12
|
+
catch (e) {
|
|
13
|
+
// ignore
|
|
14
|
+
}
|
|
4
15
|
export const koBindingHandlers = {
|
|
5
16
|
// reactUncontrolled
|
|
6
17
|
reactUncontrolled: (ko) => ({
|
|
7
|
-
init: (el, valueAccessor,
|
|
8
|
-
|
|
9
|
-
ko.
|
|
10
|
-
|
|
11
|
-
});
|
|
12
|
-
render(createElement(component), el, () => {
|
|
13
|
-
ko.applyBindingsToDescendants(bindingContext, el);
|
|
14
|
-
});
|
|
18
|
+
init: (el, valueAccessor, allBindings, _viewModel, bindingContext) => {
|
|
19
|
+
initRoot(ko, el, allBindings);
|
|
20
|
+
const Component = ko.unwrap(valueAccessor());
|
|
21
|
+
render(ko, el, _jsxs(Fragment, { children: [_jsx(Component, {}), _jsx(ApplyBindings, { bindingContext: bindingContext, el: el, ko: ko })] }));
|
|
15
22
|
return { controlsDescendantBindings: true };
|
|
16
23
|
},
|
|
17
24
|
}),
|
|
18
25
|
// React component renderer for knockout
|
|
19
26
|
react: (ko) => ({
|
|
20
|
-
init: (el) => {
|
|
21
|
-
ko
|
|
22
|
-
unmountComponentAtNode(el);
|
|
23
|
-
});
|
|
27
|
+
init: (el, _valueAccessor, allBindings) => {
|
|
28
|
+
initRoot(ko, el, allBindings);
|
|
24
29
|
return { controlsDescendantBindings: true }; // important
|
|
25
30
|
},
|
|
26
31
|
update: (el, valueAccessor, allBindings, viewModel) => {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
render(createElement(component, props), el);
|
|
32
|
+
const Component = ko.unwrap(valueAccessor());
|
|
33
|
+
const props = allBindings.get('props') || viewModel || null;
|
|
34
|
+
render(ko, el, _jsx(Component, Object.assign({}, props)));
|
|
31
35
|
},
|
|
32
36
|
}),
|
|
33
37
|
};
|
|
38
|
+
function ApplyBindings({ bindingContext, el, ko }) {
|
|
39
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
40
|
+
useEffect(() => ko.applyBindingsToDescendants(bindingContext, el), []);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const ROOT_KEY = 'ko-binding-handler-root';
|
|
44
|
+
function disposalCallback(el, ko) {
|
|
45
|
+
const root = ko.utils.domData.get(el, ROOT_KEY);
|
|
46
|
+
if (root) {
|
|
47
|
+
return () => {
|
|
48
|
+
root.unmount();
|
|
49
|
+
ko.utils.domData.clear(el);
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// eslint-disable-next-line react/no-deprecated
|
|
53
|
+
return () => ReactDOM.unmountComponentAtNode(el);
|
|
54
|
+
}
|
|
55
|
+
function initRoot(ko, el, allBindings) {
|
|
56
|
+
if (createRoot && allBindings.get('legacyRoot') !== true) {
|
|
57
|
+
const root = createRoot(el);
|
|
58
|
+
ko.utils.domData.set(el, ROOT_KEY, root);
|
|
59
|
+
}
|
|
60
|
+
ko.utils.domNodeDisposal.addDisposeCallback(el, disposalCallback(el, ko));
|
|
61
|
+
}
|
|
62
|
+
function render(ko, container, element) {
|
|
63
|
+
const root = ko.utils.domData.get(container, ROOT_KEY);
|
|
64
|
+
if (root) {
|
|
65
|
+
root.render(element);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
// eslint-disable-next-line react/no-deprecated
|
|
69
|
+
ReactDOM.render(element, container);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
34
72
|
//# sourceMappingURL=ko-binding-handlers.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ko-binding-handlers.js","sourceRoot":"","sources":["../src/ko-binding-handlers.
|
|
1
|
+
{"version":3,"file":"ko-binding-handlers.js","sourceRoot":"","sources":["../src/ko-binding-handlers.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@servicetitan/ko-bridge",
|
|
3
|
-
"version": "23.1
|
|
3
|
+
"version": "23.2.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"homepage": "https://docs.st.dev/docs/frontend/ko-bridge",
|
|
6
6
|
"repository": {
|
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
"src"
|
|
17
17
|
],
|
|
18
18
|
"devDependencies": {
|
|
19
|
+
"@testing-library/jest-dom": "^6.4.2",
|
|
20
|
+
"@testing-library/react": "^14.2.1",
|
|
19
21
|
"@types/knockout": "~3.4.71",
|
|
20
22
|
"@types/react": "~18.2.55",
|
|
21
23
|
"@types/react-dom": "~18.2.19",
|
|
@@ -36,5 +38,5 @@
|
|
|
36
38
|
"cli": {
|
|
37
39
|
"webpack": false
|
|
38
40
|
},
|
|
39
|
-
"gitHead": "
|
|
41
|
+
"gitHead": "b9322f0b223f6c4539ff2e90d2c18ffa97d349da"
|
|
40
42
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|
|
2
|
+
import { act, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import ko from 'knockout';
|
|
4
|
+
import { ComponentType } from 'react';
|
|
5
|
+
import ReactDOM from 'react-dom';
|
|
6
|
+
|
|
7
|
+
import { koBindingHandlers } from '../ko-binding-handlers';
|
|
8
|
+
|
|
9
|
+
describe('[ko-bridge] koBindingHandlers', () => {
|
|
10
|
+
let el: HTMLElement;
|
|
11
|
+
let mockAllBindings: any;
|
|
12
|
+
let mockBindingContext: any;
|
|
13
|
+
let valueAccessor: () => ComponentType;
|
|
14
|
+
let viewModel: Record<string, any>;
|
|
15
|
+
|
|
16
|
+
interface MockButtonProps {
|
|
17
|
+
title?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function MockButton({ title }: Readonly<MockButtonProps>) {
|
|
21
|
+
return <button>{title ?? 'MockButton'}</button>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function itRendersComponent(subject: Function) {
|
|
25
|
+
test('renders Component', async () => {
|
|
26
|
+
subject();
|
|
27
|
+
|
|
28
|
+
await waitFor(() => {
|
|
29
|
+
expect(screen.getByRole('button', { name: 'MockButton' })).toBeInTheDocument();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function itRegistersUnmountCallback(subject: Function) {
|
|
35
|
+
test('registers callback that unmounts Component', () => {
|
|
36
|
+
const callbackSpy = jest.spyOn(ko.utils.domNodeDisposal, 'addDisposeCallback');
|
|
37
|
+
subject();
|
|
38
|
+
|
|
39
|
+
const root = ko.utils.domData.get(el, 'ko-binding-handler-root'); // Beware: this relies on an implementation detail
|
|
40
|
+
const unmountSpy = jest.spyOn(root, 'unmount');
|
|
41
|
+
|
|
42
|
+
const [, disposeCallback] = callbackSpy.mock.calls[0];
|
|
43
|
+
act(() => disposeCallback());
|
|
44
|
+
|
|
45
|
+
expect(unmountSpy).toHaveBeenCalled();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function itRendersLegacyComponent(subject: Function) {
|
|
50
|
+
// eslint-disable-next-line react/no-deprecated
|
|
51
|
+
const originalRender = ReactDOM.render;
|
|
52
|
+
|
|
53
|
+
test('uses legacy API to render Component', async () => {
|
|
54
|
+
const renderSpy = jest
|
|
55
|
+
.spyOn(ReactDOM, 'render')
|
|
56
|
+
.mockImplementation((...args: Parameters<ReactDOM.Renderer>) =>
|
|
57
|
+
originalRender(...args)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
subject();
|
|
61
|
+
|
|
62
|
+
await waitFor(() => {
|
|
63
|
+
expect(screen.getByRole('button', { name: 'MockButton' })).toBeInTheDocument();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(renderSpy).toHaveBeenCalled();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function itRegistersLegacyUnmountCallback(subject: Function) {
|
|
71
|
+
test('uses legacy API to unmount Component', () => {
|
|
72
|
+
const callbackSpy = jest.spyOn(ko.utils.domNodeDisposal, 'addDisposeCallback');
|
|
73
|
+
|
|
74
|
+
subject();
|
|
75
|
+
|
|
76
|
+
jest.spyOn(ReactDOM, 'unmountComponentAtNode');
|
|
77
|
+
|
|
78
|
+
const [, disposeCallback] = callbackSpy.mock.calls[0];
|
|
79
|
+
disposeCallback();
|
|
80
|
+
|
|
81
|
+
// eslint-disable-next-line react/no-deprecated
|
|
82
|
+
expect(ReactDOM.unmountComponentAtNode).toHaveBeenCalledWith(el);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
jest.clearAllMocks();
|
|
88
|
+
|
|
89
|
+
el = document.createElement('div');
|
|
90
|
+
document.body.appendChild(el);
|
|
91
|
+
|
|
92
|
+
mockAllBindings = new Map();
|
|
93
|
+
mockBindingContext = {};
|
|
94
|
+
valueAccessor = () => MockButton;
|
|
95
|
+
viewModel = {};
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
afterEach(() => {
|
|
99
|
+
el.remove();
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function init(handler: KnockoutBindingHandler) {
|
|
103
|
+
let result: ReturnType<Required<KnockoutBindingHandler>['init']> = undefined;
|
|
104
|
+
|
|
105
|
+
act(() => {
|
|
106
|
+
result = handler.init!(
|
|
107
|
+
el,
|
|
108
|
+
valueAccessor,
|
|
109
|
+
mockAllBindings,
|
|
110
|
+
viewModel,
|
|
111
|
+
mockBindingContext
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function update(handler: KnockoutBindingHandler) {
|
|
119
|
+
let result: ReturnType<Required<KnockoutBindingHandler>['update']> = undefined;
|
|
120
|
+
|
|
121
|
+
act(() => {
|
|
122
|
+
result = handler.update!(
|
|
123
|
+
el,
|
|
124
|
+
valueAccessor,
|
|
125
|
+
mockAllBindings,
|
|
126
|
+
viewModel,
|
|
127
|
+
mockBindingContext
|
|
128
|
+
);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
describe('reactUncontrolled', () => {
|
|
135
|
+
const handler: KnockoutBindingHandler = koBindingHandlers.reactUncontrolled(ko);
|
|
136
|
+
|
|
137
|
+
describe('init', () => {
|
|
138
|
+
const subject = () => init(handler);
|
|
139
|
+
|
|
140
|
+
itRendersComponent(subject);
|
|
141
|
+
itRegistersUnmountCallback(subject);
|
|
142
|
+
|
|
143
|
+
test('applies bindings to descendants', async () => {
|
|
144
|
+
jest.spyOn(ko, 'applyBindingsToDescendants');
|
|
145
|
+
|
|
146
|
+
expect(subject()).toEqual({ controlsDescendantBindings: true });
|
|
147
|
+
await waitFor(() =>
|
|
148
|
+
expect(ko.applyBindingsToDescendants).toHaveBeenCalledWith(
|
|
149
|
+
mockBindingContext,
|
|
150
|
+
el
|
|
151
|
+
)
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('with {legacyRoot: true}', () => {
|
|
156
|
+
beforeEach(() => mockAllBindings.set('legacyRoot', true));
|
|
157
|
+
|
|
158
|
+
itRendersLegacyComponent(subject);
|
|
159
|
+
itRegistersLegacyUnmountCallback(subject);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
describe('react', () => {
|
|
165
|
+
const handler: KnockoutBindingHandler = koBindingHandlers.react(ko);
|
|
166
|
+
|
|
167
|
+
describe('init', () => {
|
|
168
|
+
const subject = () => init(handler);
|
|
169
|
+
|
|
170
|
+
itRegistersUnmountCallback(subject);
|
|
171
|
+
|
|
172
|
+
describe('with {legacyRoot: true}', () => {
|
|
173
|
+
beforeEach(() => mockAllBindings.set('legacyRoot', true));
|
|
174
|
+
|
|
175
|
+
itRegistersLegacyUnmountCallback(subject);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('update', () => {
|
|
180
|
+
const subject = () => {
|
|
181
|
+
init(handler);
|
|
182
|
+
update(handler);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
itRendersComponent(subject);
|
|
186
|
+
|
|
187
|
+
describe('with {legacyRoot: true}', () => {
|
|
188
|
+
beforeEach(() => mockAllBindings.set('legacyRoot', true));
|
|
189
|
+
|
|
190
|
+
itRendersLegacyComponent(subject);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('with props', () => {
|
|
194
|
+
const props = { title: 'Foo' };
|
|
195
|
+
|
|
196
|
+
beforeEach(() => mockAllBindings.set('props', props));
|
|
197
|
+
|
|
198
|
+
test('renders Component with props', () => {
|
|
199
|
+
subject();
|
|
200
|
+
|
|
201
|
+
expect(screen.getByRole('button', { name: props.title })).toBeInTheDocument();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('with viewModel', () => {
|
|
206
|
+
beforeEach(() => (viewModel = { title: 'Bar' }));
|
|
207
|
+
|
|
208
|
+
test('renders Component with viewModel as props', () => {
|
|
209
|
+
subject();
|
|
210
|
+
|
|
211
|
+
expect(
|
|
212
|
+
screen.getByRole('button', { name: viewModel.title })
|
|
213
|
+
).toBeInTheDocument();
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
package/src/html-comment.tsx
CHANGED
|
@@ -62,7 +62,7 @@ export class KnockoutComponent extends Component<KnockoutComponentProps> {
|
|
|
62
62
|
componentWillUnmount() {
|
|
63
63
|
this.cleanNode();
|
|
64
64
|
}
|
|
65
|
-
render() {
|
|
65
|
+
render(): JSX.Element {
|
|
66
66
|
const { koTemplate: KoTemplate } = this.props;
|
|
67
67
|
|
|
68
68
|
return <KoTemplate ref={this.ref} />;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ComponentType, Fragment, ReactElement, useEffect } from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom';
|
|
3
|
+
import type ReactDOMClient from 'react-dom/client';
|
|
4
|
+
|
|
5
|
+
type KO = typeof import('knockout');
|
|
6
|
+
|
|
7
|
+
let createRoot: typeof ReactDOMClient.createRoot;
|
|
8
|
+
try {
|
|
9
|
+
/**
|
|
10
|
+
* Note, the webpack.IgnorePlugin must be configured to ignore this dependency.
|
|
11
|
+
* @see {@link file://./../../startup/src/webpack/configs/plugins/ignore-plugin/ignore-plugin.ts}
|
|
12
|
+
*/
|
|
13
|
+
createRoot = require('react-dom/client').createRoot;
|
|
14
|
+
} catch (e) {
|
|
15
|
+
// ignore
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const koBindingHandlers = {
|
|
19
|
+
// reactUncontrolled
|
|
20
|
+
reactUncontrolled: (ko: KO): KnockoutBindingHandler => ({
|
|
21
|
+
init: (
|
|
22
|
+
el: Element,
|
|
23
|
+
valueAccessor: () => any,
|
|
24
|
+
allBindings: KnockoutAllBindingsAccessor,
|
|
25
|
+
_viewModel: any,
|
|
26
|
+
bindingContext: KnockoutBindingContext
|
|
27
|
+
) => {
|
|
28
|
+
initRoot(ko, el, allBindings);
|
|
29
|
+
const Component = ko.unwrap<ComponentType>(valueAccessor());
|
|
30
|
+
render(
|
|
31
|
+
ko,
|
|
32
|
+
el,
|
|
33
|
+
<Fragment>
|
|
34
|
+
<Component />
|
|
35
|
+
<ApplyBindings bindingContext={bindingContext} el={el} ko={ko} />
|
|
36
|
+
</Fragment>
|
|
37
|
+
);
|
|
38
|
+
return { controlsDescendantBindings: true };
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
// React component renderer for knockout
|
|
42
|
+
react: (ko: KO): KnockoutBindingHandler => ({
|
|
43
|
+
init: (
|
|
44
|
+
el: Element,
|
|
45
|
+
_valueAccessor: () => any,
|
|
46
|
+
allBindings: KnockoutAllBindingsAccessor
|
|
47
|
+
) => {
|
|
48
|
+
initRoot(ko, el, allBindings);
|
|
49
|
+
return { controlsDescendantBindings: true }; // important
|
|
50
|
+
},
|
|
51
|
+
update: (
|
|
52
|
+
el: Element,
|
|
53
|
+
valueAccessor: () => any,
|
|
54
|
+
allBindings: KnockoutAllBindingsAccessor,
|
|
55
|
+
viewModel: any
|
|
56
|
+
) => {
|
|
57
|
+
const Component = ko.unwrap<ComponentType>(valueAccessor());
|
|
58
|
+
const props = allBindings.get('props') || viewModel || null;
|
|
59
|
+
render(ko, el, <Component {...props} />);
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function ApplyBindings({ bindingContext, el, ko }: { bindingContext: any; el: Element; ko: KO }) {
|
|
65
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
66
|
+
useEffect(() => ko.applyBindingsToDescendants(bindingContext, el), []);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const ROOT_KEY = 'ko-binding-handler-root';
|
|
71
|
+
|
|
72
|
+
function disposalCallback(el: Element, ko: KO) {
|
|
73
|
+
const root = ko.utils.domData.get(el, ROOT_KEY);
|
|
74
|
+
if (root) {
|
|
75
|
+
return () => {
|
|
76
|
+
root.unmount();
|
|
77
|
+
ko.utils.domData.clear(el);
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// eslint-disable-next-line react/no-deprecated
|
|
81
|
+
return () => ReactDOM.unmountComponentAtNode(el);
|
|
82
|
+
}
|
|
83
|
+
function initRoot(ko: KO, el: Element, allBindings: KnockoutAllBindingsAccessor) {
|
|
84
|
+
if (createRoot && allBindings.get('legacyRoot') !== true) {
|
|
85
|
+
const root = createRoot(el);
|
|
86
|
+
ko.utils.domData.set(el, ROOT_KEY, root);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
ko.utils.domNodeDisposal.addDisposeCallback(el, disposalCallback(el, ko));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function render(ko: KO, container: Element, element: ReactElement) {
|
|
93
|
+
const root = ko.utils.domData.get(container, ROOT_KEY);
|
|
94
|
+
if (root) {
|
|
95
|
+
root.render(element);
|
|
96
|
+
} else {
|
|
97
|
+
// eslint-disable-next-line react/no-deprecated
|
|
98
|
+
ReactDOM.render(element, container);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { createElement } from 'react';
|
|
2
|
-
// eslint-disable-next-line react/no-deprecated
|
|
3
|
-
import { render, unmountComponentAtNode } from 'react-dom';
|
|
4
|
-
|
|
5
|
-
export const koBindingHandlers = {
|
|
6
|
-
// reactUncontrolled
|
|
7
|
-
reactUncontrolled: (ko: any) => ({
|
|
8
|
-
init: (el: Element, valueAccessor: any, _1: any, _2: any, bindingContext: any) => {
|
|
9
|
-
const component = ko.unwrap(valueAccessor());
|
|
10
|
-
ko.utils.domNodeDisposal.addDisposeCallback(el, () => {
|
|
11
|
-
unmountComponentAtNode(el);
|
|
12
|
-
});
|
|
13
|
-
render(createElement(component), el, () => {
|
|
14
|
-
ko.applyBindingsToDescendants(bindingContext, el);
|
|
15
|
-
});
|
|
16
|
-
return { controlsDescendantBindings: true };
|
|
17
|
-
},
|
|
18
|
-
}),
|
|
19
|
-
// React component renderer for knockout
|
|
20
|
-
react: (ko: any) => ({
|
|
21
|
-
init: (el: any) => {
|
|
22
|
-
ko.utils.domNodeDisposal.addDisposeCallback(el, () => {
|
|
23
|
-
unmountComponentAtNode(el);
|
|
24
|
-
});
|
|
25
|
-
return { controlsDescendantBindings: true }; // important
|
|
26
|
-
},
|
|
27
|
-
update: (el: Element, valueAccessor: any, allBindings: any, viewModel: any) => {
|
|
28
|
-
const component = ko.unwrap(valueAccessor());
|
|
29
|
-
let props = allBindings.get('props');
|
|
30
|
-
props = props ? props : viewModel ? viewModel : null;
|
|
31
|
-
render(createElement(component, props), el);
|
|
32
|
-
},
|
|
33
|
-
}),
|
|
34
|
-
};
|