Compare commits

...

1 Commits

Author SHA1 Message Date
Kshitij Sobti
40ab53d793 feat: A prototype of dynamically loading themes 2022-04-22 11:26:22 +05:30
5 changed files with 102 additions and 1 deletions

1
.env
View File

@@ -20,3 +20,4 @@ REFRESH_ACCESS_TOKEN_ENDPOINT=''
SEGMENT_KEY=''
SITE_NAME=''
USER_INFO_COOKIE_NAME=''
THEME_LOADER_URL=''

View File

@@ -21,3 +21,4 @@ REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=''
SITE_NAME=localhost
USER_INFO_COOKIE_NAME='edx-user-info'
THEME_LOADER_URL='https://xitij2000.github.io/frontend-theme-prototype/themes.js'

View File

@@ -7,6 +7,7 @@ import {
import { PostActionsBar } from '../../components';
import { ALL_ROUTES, DiscussionProvider, Routes } from '../../data/constants';
import { useTheme } from '../../theme-hooks';
import { DiscussionContext } from '../common/context';
import {
useCourseDiscussionData, useIsOnDesktop, useRedirectToThread, useSidebarVisible,
@@ -21,6 +22,7 @@ import DiscussionSidebar from './DiscussionSidebar';
export default function DiscussionsHome() {
const location = useLocation();
const ready = useTheme('red_theme');
const postEditorVisible = useSelector(
(state) => state.threads.postEditorVisible,
);
@@ -59,6 +61,9 @@ export default function DiscussionsHome() {
postMessageToParent('discussions.navigate', { path });
}
}, [path]);
if (!ready) {
return null;
}
return (
<DiscussionContext.Provider value={{

View File

@@ -14,7 +14,6 @@ import appMessages from './i18n';
import store from './store';
import './assets/favicon.ico';
import './index.scss';
subscribe(APP_READY, () => {
ReactDOM.render(
@@ -38,6 +37,7 @@ initialize({
config() {
mergeConfig({
POST_MARK_AS_READ_DELAY: process.env.POST_MARK_AS_READ_DELAY || 2000,
THEME_LOADER_URL: process.env.THEME_LOADER_URL || 'http://localhost:8111/themes.js',
});
},
},

94
src/theme-hooks.js Normal file
View File

@@ -0,0 +1,94 @@
import { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform';
export async function loadComponent(scope, module) {
// Initializes the share scope. This fills it with known provided modules from this build and all remotes
// Webpack module federation allows sharing common dependencies, like `react`, `react-dom` etc
// between modules so that they are no loaded multiple times. The following line will initialise
// the system for sharing common modules.
// Since there are no shared module here, you can safely comment out the next three lines and this
// will still work.
// eslint-disable-next-line no-undef
await __webpack_init_sharing__('default');
const container = window[scope];
// eslint-disable-next-line no-undef
await container.init(__webpack_share_scopes__.default);
const factory = await window[scope].get(module);
return factory();
} // The hook loads the supplied theme, and if the current theme changes it will
// Given a script URL this hook will add the script to the body. When the url
// changes it will unload the previous script.
// For theming, if we know where the script will come from in advance we can just
// include it in the HTML and not load it at runtime. However, that would
// require supplying that as a build-time value. Keeping this dynamic allows us
// to use it
function useDynamicScript(url) {
const [ready, setReady] = useState(false);
const [failed, setFailed] = useState(false);
useEffect(() => {
if (!url) {
return undefined;
}
const element = document.createElement('script');
element.src = url;
element.type = 'text/javascript';
element.async = true;
setReady(false);
setFailed(false);
element.onload = () => {
console.log(`Dynamic Script Loaded: ${url}`);
setReady(true);
};
element.onerror = () => {
console.error(`Dynamic Script Error: ${url}`);
setReady(false);
setFailed(true);
};
document.head.appendChild(element);
return () => {
console.log(`Dynamic Script Removed: ${url}`);
document.head.removeChild(element);
};
}, [url]);
return {
ready,
failed,
};
}
// unload the previous theme and load the new one.
export function useTheme(theme) {
const { ready } = useDynamicScript(getConfig().THEME_LOADER_URL);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
if (!ready) {
return undefined;
}
let styles = null;
(async () => {
const themeComponent = await loadComponent(theme, './theme');
styles = themeComponent.styles;
themeComponent.styles.use();
setLoaded(true);
})();
// eslint-disable-next-line no-unused-expressions
return () => {
if (styles) {
styles.unuse();
}
setLoaded(false);
};
}, [ready]);
return loaded;
}