lupine.web 1.1.1 → 1.1.3
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/package.json +1 -1
- package/src/core/bind-styles.ts +12 -1
- package/src/core/index.ts +1 -0
- package/src/core/initialize.ts +9 -5
- package/src/core/render-component.ts +30 -12
- package/src/jsx.ts +2 -0
- package/src/lib/index.ts +1 -0
- package/src/lib/web-config.ts +77 -0
- package/src/lib/web-env.ts +47 -46
package/package.json
CHANGED
package/src/core/bind-styles.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { uniqueIdGenerator } from 'lupine.components';
|
|
1
2
|
import { CssProps } from '../jsx';
|
|
2
3
|
import { getCurrentTheme, themeCookieName } from './bind-theme';
|
|
3
4
|
import { camelToHyphens } from './camel-to-hyphens';
|
|
@@ -163,8 +164,18 @@ For example, it can be like this for all elements:
|
|
|
163
164
|
|
|
164
165
|
For themes like [data-theme="dark" i], the topUniqueClassName should be empty
|
|
165
166
|
*/
|
|
167
|
+
export const globalStyleUniqueId = uniqueIdGenerator('g'); // g means global style
|
|
168
|
+
const _globalStyleIds = new Map<CssProps, string>();
|
|
169
|
+
export const getGlobalStylesId = (style: CssProps): string => {
|
|
170
|
+
if (!_globalStyleIds.has(style)) {
|
|
171
|
+
const id = globalStyleUniqueId();
|
|
172
|
+
_globalStyleIds.set(style, id);
|
|
173
|
+
}
|
|
174
|
+
return _globalStyleIds.get(style)!;
|
|
175
|
+
};
|
|
176
|
+
|
|
166
177
|
const _globalStyle = new Map();
|
|
167
|
-
export const
|
|
178
|
+
export const bindGlobalStyle = (
|
|
168
179
|
topUniqueClassName: string,
|
|
169
180
|
style: CssProps,
|
|
170
181
|
forceUpdate = false,
|
package/src/core/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from './camel-to-hyphens';
|
|
|
10
10
|
export * from './mount-component';
|
|
11
11
|
export * from './page-loaded-events';
|
|
12
12
|
export * from './page-router';
|
|
13
|
+
export * from './render-component';
|
|
13
14
|
export * from './replace-innerhtml';
|
|
14
15
|
export * from './server-cookie';
|
|
15
16
|
|
package/src/core/initialize.ts
CHANGED
|
@@ -8,9 +8,10 @@ import { callPageLoadedEvent } from './page-loaded-events';
|
|
|
8
8
|
import { initServerCookies } from './server-cookie';
|
|
9
9
|
import { IToClientDelivery } from '../models';
|
|
10
10
|
import { getMetaDataObject, getMetaDataTags, getPageTitle } from './bind-meta';
|
|
11
|
-
import { initWebEnv
|
|
11
|
+
import { initWebEnv } from '../lib/web-env';
|
|
12
12
|
import { _lupineJs, PageProps, PageResultType, setRenderPageProps } from './export-lupine';
|
|
13
13
|
import { isFrontEnd } from '../lib/is-frontend';
|
|
14
|
+
import { WebConfig } from '../lib/web-config';
|
|
14
15
|
|
|
15
16
|
const logger = new Logger('initialize');
|
|
16
17
|
|
|
@@ -26,7 +27,8 @@ const generatePage = async (props: PageProps, toClientDelivery: IToClientDeliver
|
|
|
26
27
|
setRenderPageProps(props);
|
|
27
28
|
|
|
28
29
|
initWebEnv(toClientDelivery.getWebEnv());
|
|
29
|
-
|
|
30
|
+
WebConfig.initFromData(toClientDelivery.getWebSetting());
|
|
31
|
+
// initWebSetting(toClientDelivery.getWebSetting());
|
|
30
32
|
initServerCookies(toClientDelivery.getServerCookie());
|
|
31
33
|
// callPageResetEvent();
|
|
32
34
|
callPageLoadedEvent();
|
|
@@ -57,12 +59,14 @@ const generatePage = async (props: PageProps, toClientDelivery: IToClientDeliver
|
|
|
57
59
|
};
|
|
58
60
|
_lupineJs.generatePage = generatePage;
|
|
59
61
|
|
|
60
|
-
|
|
62
|
+
const _initSaved = {
|
|
63
|
+
pageInitialized: false,
|
|
64
|
+
};
|
|
61
65
|
// this is called in the FE when the document is loaded
|
|
62
66
|
// to avoid circular reference, bindLinks can't call initializePage directly
|
|
63
67
|
export const initializePage = async (newUrl?: string) => {
|
|
64
|
-
const currentPageInitialized =
|
|
65
|
-
|
|
68
|
+
const currentPageInitialized = _initSaved.pageInitialized;
|
|
69
|
+
_initSaved.pageInitialized = true;
|
|
66
70
|
logger.log('initializePage: ', newUrl);
|
|
67
71
|
if (newUrl) {
|
|
68
72
|
window.history.pushState({ urlPath: newUrl }, '', newUrl);
|
|
@@ -11,7 +11,7 @@ export const domUniqueId = uniqueIdGenerator('l'); // l means label
|
|
|
11
11
|
// domUniqueId(true);
|
|
12
12
|
// });
|
|
13
13
|
|
|
14
|
-
function renderChildren(html: string[], children: any, uniqueClassName?: string) {
|
|
14
|
+
function renderChildren(html: string[], children: any, uniqueClassName?: string, globalCssId?: string) {
|
|
15
15
|
if (typeof children === 'string') {
|
|
16
16
|
html.push(children);
|
|
17
17
|
} else if (children === false || children === null || typeof children === 'undefined') {
|
|
@@ -22,10 +22,10 @@ function renderChildren(html: string[], children: any, uniqueClassName?: string)
|
|
|
22
22
|
} else if (Array.isArray(children)) {
|
|
23
23
|
for (let i = 0; i < children.length; i++) {
|
|
24
24
|
const item = children[i];
|
|
25
|
-
renderChildren(html, item, uniqueClassName);
|
|
25
|
+
renderChildren(html, item, uniqueClassName, globalCssId);
|
|
26
26
|
}
|
|
27
27
|
} else if (children.type && children.props) {
|
|
28
|
-
renderComponent(children.type, children.props, uniqueClassName);
|
|
28
|
+
renderComponent(children.type, children.props, uniqueClassName, globalCssId);
|
|
29
29
|
html.push(...children.props._html);
|
|
30
30
|
children.props._html.length = 0;
|
|
31
31
|
} else {
|
|
@@ -57,7 +57,7 @@ const genUniqueId = (props: any) => {
|
|
|
57
57
|
return props._id;
|
|
58
58
|
};
|
|
59
59
|
// data-refid will be assigned with a ref.id
|
|
60
|
-
function renderAttribute(type: any, props: any, jsxNodes: any, uniqueClassName?: string) {
|
|
60
|
+
function renderAttribute(type: any, props: any, jsxNodes: any, uniqueClassName?: string, globalCssId?: string) {
|
|
61
61
|
const html = [];
|
|
62
62
|
// data-refid is used for nested components like this:
|
|
63
63
|
// <div class='class-name' ref={ref} ...>...
|
|
@@ -108,7 +108,21 @@ function renderAttribute(type: any, props: any, jsxNodes: any, uniqueClassName?:
|
|
|
108
108
|
// add as the first
|
|
109
109
|
classNameList.unshift(props._id);
|
|
110
110
|
}
|
|
111
|
-
if (
|
|
111
|
+
if (props['ref'] && props['ref'].globalCssId && !classNameList.includes(props['ref'].globalCssId)) {
|
|
112
|
+
// add as the first
|
|
113
|
+
classNameList.unshift(props['ref'].globalCssId);
|
|
114
|
+
}
|
|
115
|
+
if (globalCssId && uniqueClassName) {
|
|
116
|
+
// &xx -> globalCssId + xx and uniqueClassName+xx
|
|
117
|
+
classNameList = classNameList.flatMap((item: string) => {
|
|
118
|
+
if (item.includes('&')) {
|
|
119
|
+
return [item.replace(/&/g, globalCssId), item.replace(/&/g, uniqueClassName)];
|
|
120
|
+
}
|
|
121
|
+
return [item];
|
|
122
|
+
});
|
|
123
|
+
} else if (globalCssId) {
|
|
124
|
+
classNameList = classNameList.map((item: string) => item.replace(/&/g, globalCssId));
|
|
125
|
+
} else if (uniqueClassName) {
|
|
112
126
|
classNameList = classNameList.map((item: string) => item.replace(/&/g, uniqueClassName));
|
|
113
127
|
}
|
|
114
128
|
html.push(`class="${classNameList.join(' ')}"`);
|
|
@@ -125,11 +139,11 @@ function renderAttribute(type: any, props: any, jsxNodes: any, uniqueClassName?:
|
|
|
125
139
|
}
|
|
126
140
|
|
|
127
141
|
// The result has only one element
|
|
128
|
-
export const renderComponent = (type: any, props: any, uniqueClassName?: string) => {
|
|
142
|
+
export const renderComponent = (type: any, props: any, uniqueClassName?: string, globalCssId?: string) => {
|
|
129
143
|
// logger.log("==================renderComponent", type);
|
|
130
144
|
if (Array.isArray(props)) {
|
|
131
145
|
const jsxNodes = { type: 'Fragment', props: { children: props } } as any;
|
|
132
|
-
renderComponent(jsxNodes.type, jsxNodes.props, uniqueClassName);
|
|
146
|
+
renderComponent(jsxNodes.type, jsxNodes.props, uniqueClassName, globalCssId);
|
|
133
147
|
return;
|
|
134
148
|
}
|
|
135
149
|
|
|
@@ -147,7 +161,7 @@ export const renderComponent = (type: any, props: any, uniqueClassName?: string)
|
|
|
147
161
|
// }
|
|
148
162
|
// logger.log('==========props._result', props._result);
|
|
149
163
|
if (typeof props._result.type === 'function') {
|
|
150
|
-
renderComponent(props._result.type, props._result.props, uniqueClassName);
|
|
164
|
+
renderComponent(props._result.type, props._result.props, uniqueClassName, globalCssId);
|
|
151
165
|
if (props._result.props._html) {
|
|
152
166
|
props._html.push(...props._result.props._html);
|
|
153
167
|
props._result.props._html.length = 0;
|
|
@@ -172,7 +186,11 @@ export const renderComponent = (type: any, props: any, uniqueClassName?: string)
|
|
|
172
186
|
newProps['class'] = newUniqueClassName;
|
|
173
187
|
}
|
|
174
188
|
}
|
|
175
|
-
|
|
189
|
+
let newGlobalCssId = globalCssId;
|
|
190
|
+
if (newProps['ref'] && newProps['ref'].globalCssId) {
|
|
191
|
+
newGlobalCssId = newProps['ref'].globalCssId;
|
|
192
|
+
}
|
|
193
|
+
const attrs = renderAttribute(newType, newProps, { type, props }, newUniqueClassName, newGlobalCssId);
|
|
176
194
|
if (selfClosingTags.includes(newType.toLowerCase())) {
|
|
177
195
|
// for Fragment, only needs this tag when Ref is assigned
|
|
178
196
|
if (newType !== 'Fragment' || newProps.ref) {
|
|
@@ -191,12 +209,12 @@ export const renderComponent = (type: any, props: any, uniqueClassName?: string)
|
|
|
191
209
|
props._html.push(`<style id="sty-${newUniqueClassName}">${cssText}</style>`); // sty means style, and updateStyles has the same name
|
|
192
210
|
}
|
|
193
211
|
|
|
194
|
-
if (newProps.children) {
|
|
212
|
+
if (newProps.children || newProps.children === 0) {
|
|
195
213
|
// if (newProps._lb) {
|
|
196
214
|
// assignLabels(newProps._lb, newProps.children);
|
|
197
215
|
// }
|
|
198
216
|
|
|
199
|
-
renderChildren(props._html, newProps.children, newUniqueClassName);
|
|
217
|
+
renderChildren(props._html, newProps.children, newUniqueClassName, newGlobalCssId);
|
|
200
218
|
} else if (newProps['dangerouslySetInnerHTML']) {
|
|
201
219
|
props._html.push(newProps['dangerouslySetInnerHTML']);
|
|
202
220
|
} else {
|
|
@@ -208,7 +226,7 @@ export const renderComponent = (type: any, props: any, uniqueClassName?: string)
|
|
|
208
226
|
}
|
|
209
227
|
}
|
|
210
228
|
} else if (newType.name === 'Fragment') {
|
|
211
|
-
renderChildren(props._html, newProps.children, uniqueClassName);
|
|
229
|
+
renderChildren(props._html, newProps.children, uniqueClassName, globalCssId);
|
|
212
230
|
} else {
|
|
213
231
|
logger.warn('Unknown type: ', type, props, newType, newProps);
|
|
214
232
|
}
|
package/src/jsx.ts
CHANGED
|
@@ -20,6 +20,8 @@ export type Ref<T> = RefObject<T> | RefCallback<T>;
|
|
|
20
20
|
// Ref can only apply to a DOM element (not a function component)
|
|
21
21
|
export type RefProps = {
|
|
22
22
|
id?: string;
|
|
23
|
+
// if a component has global styles, and also has "&" classNames, then globalCssId is needed to replace "&"
|
|
24
|
+
globalCssId?: string;
|
|
23
25
|
current?: any; // Element | null,
|
|
24
26
|
onLoad?: (el: Element) => Promise<void>;
|
|
25
27
|
onUnload?: (el: Element) => Promise<void>;
|
package/src/lib/index.ts
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { getRenderPageProps } from 'lupine.web';
|
|
2
|
+
|
|
3
|
+
// for mobile app, it needs the url to fetch the config for the first time
|
|
4
|
+
export const bindWebConfigApi = (webConfigApi: string) => {
|
|
5
|
+
WebConfig.webConfigApi = webConfigApi;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export class WebConfig {
|
|
9
|
+
static webConfigApi = '';
|
|
10
|
+
static initialized = false;
|
|
11
|
+
static cfg: { [key: string]: string } = {};
|
|
12
|
+
|
|
13
|
+
// called from generatePage (SSR)
|
|
14
|
+
static initFromData(cfg: { [key: string]: string }) {
|
|
15
|
+
this.initialized = true;
|
|
16
|
+
this.cfg = cfg;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static async init(force?: boolean) {
|
|
20
|
+
if (this.initialized && !force) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
this.initialized = true;
|
|
24
|
+
|
|
25
|
+
// For web, it's injected in the html by SSR, but for mobile, it's fetched from api
|
|
26
|
+
if (typeof document === 'object' && !force) {
|
|
27
|
+
const json = document.querySelector('#web-setting')?.textContent;
|
|
28
|
+
if (json) {
|
|
29
|
+
this.cfg = JSON.parse(json);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!this.webConfigApi) {
|
|
35
|
+
console.error('WebConfig webConfigApi is not set');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const url = getRenderPageProps().renderPageFunctions.baseUrl(this.webConfigApi);
|
|
39
|
+
const data = await getRenderPageProps().renderPageFunctions.fetchData(url);
|
|
40
|
+
if (data && data.json && data.json.status === 'ok') {
|
|
41
|
+
this.cfg = data.json.result;
|
|
42
|
+
} else {
|
|
43
|
+
console.error(data?.json?.message || 'Failed to get web config');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static async get(key: string, defaultValue: number): Promise<number>;
|
|
48
|
+
static async get(key: string, defaultValue: string): Promise<string>;
|
|
49
|
+
static async get(key: string, defaultValue: boolean): Promise<boolean>;
|
|
50
|
+
static async get(key: string, defaultValue: object): Promise<object>;
|
|
51
|
+
static async get(key: string, defaultValue?: any): Promise<any> {
|
|
52
|
+
await WebConfig.init();
|
|
53
|
+
|
|
54
|
+
if (typeof WebConfig.cfg[key] === 'undefined') {
|
|
55
|
+
return defaultValue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (typeof defaultValue === 'number') {
|
|
59
|
+
return Number.parseInt(WebConfig.cfg[key]!);
|
|
60
|
+
}
|
|
61
|
+
if (typeof defaultValue === 'boolean') {
|
|
62
|
+
return WebConfig.cfg[key]!.toLocaleLowerCase() === 'true' || WebConfig.cfg[key] === '1';
|
|
63
|
+
}
|
|
64
|
+
if (typeof defaultValue === 'object') {
|
|
65
|
+
if (typeof WebConfig.cfg[key] === 'object') {
|
|
66
|
+
return WebConfig.cfg[key];
|
|
67
|
+
}
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(WebConfig.cfg[key]!);
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error(`WebConfig JSON.parse error: `, error);
|
|
72
|
+
}
|
|
73
|
+
return defaultValue;
|
|
74
|
+
}
|
|
75
|
+
return WebConfig.cfg[key] || defaultValue;
|
|
76
|
+
}
|
|
77
|
+
}
|
package/src/lib/web-env.ts
CHANGED
|
@@ -47,52 +47,53 @@ function initWebEnv(webEnv: { [key: string]: string }) {
|
|
|
47
47
|
_webEnvInitialized = true;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
// _webSetting is for
|
|
51
|
-
// _webSetting is
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
function webSetting(key: string, defaultValue:
|
|
56
|
-
function webSetting(key: string, defaultValue:
|
|
57
|
-
function webSetting(key: string, defaultValue:
|
|
58
|
-
function webSetting(key: string, defaultValue:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
50
|
+
// _webSetting is removed to WebConfig because for mobile app, it's async to load webconfig from an api
|
|
51
|
+
// // _webSetting is for dynamic settings that can be changed without redeploying the app
|
|
52
|
+
// // _webSetting is json format so the returning can be an object
|
|
53
|
+
// const _webSetting: { [key: string]: string } = {};
|
|
54
|
+
// let _webSettingInitialized = false;
|
|
55
|
+
// function webSetting(key: string, defaultValue: number): number;
|
|
56
|
+
// function webSetting(key: string, defaultValue: string): string;
|
|
57
|
+
// function webSetting(key: string, defaultValue: boolean): boolean;
|
|
58
|
+
// function webSetting(key: string, defaultValue: object): object;
|
|
59
|
+
// function webSetting(key: string, defaultValue: any): any {
|
|
60
|
+
// // for SSR, the webSetting should be initialized. But for the FE, it should be initialized by the webSetting script tag
|
|
61
|
+
// if (!_webSettingInitialized) {
|
|
62
|
+
// const json = document.querySelector('#web-setting')?.textContent;
|
|
63
|
+
// if (json) {
|
|
64
|
+
// _webSettingInitialized = true;
|
|
65
|
+
// initWebSetting(JSON.parse(json));
|
|
66
|
+
// }
|
|
67
|
+
// }
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
// !_webSettingInitialized && console.warn('webSetting has not been initialized yet!');
|
|
70
|
+
// if (typeof _webSetting[key] === 'undefined') {
|
|
71
|
+
// return defaultValue;
|
|
72
|
+
// }
|
|
72
73
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
}
|
|
92
|
-
// this is only called from the server side for SSR
|
|
93
|
-
function initWebSetting(webSetting: { [key: string]: string }) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
74
|
+
// if (typeof defaultValue === 'number') {
|
|
75
|
+
// return Number.parseInt(_webSetting[key]!);
|
|
76
|
+
// }
|
|
77
|
+
// if (typeof defaultValue === 'boolean') {
|
|
78
|
+
// return _webSetting[key]!.toLocaleLowerCase() === 'true' || _webSetting[key] === '1';
|
|
79
|
+
// }
|
|
80
|
+
// if (typeof defaultValue === 'object') {
|
|
81
|
+
// if (typeof _webSetting[key] === 'object') {
|
|
82
|
+
// return _webSetting[key];
|
|
83
|
+
// }
|
|
84
|
+
// try {
|
|
85
|
+
// return JSON.parse(_webSetting[key]!);
|
|
86
|
+
// } catch (error) {
|
|
87
|
+
// console.error(`webSetting JSON.parse error: `, error);
|
|
88
|
+
// }
|
|
89
|
+
// return defaultValue;
|
|
90
|
+
// }
|
|
91
|
+
// return _webSetting[key] || defaultValue;
|
|
92
|
+
// }
|
|
93
|
+
// // this is only called from the server side for SSR
|
|
94
|
+
// function initWebSetting(webSetting: { [key: string]: string }) {
|
|
95
|
+
// Object.assign(_webSetting, webSetting);
|
|
96
|
+
// _webSettingInitialized = true;
|
|
97
|
+
// }
|
|
97
98
|
|
|
98
|
-
export { initWebEnv, webEnv
|
|
99
|
+
export { initWebEnv, webEnv };
|