Skip to content

Commit

Permalink
Dynamic import for bootstrap components
Browse files Browse the repository at this point in the history
  • Loading branch information
Oksydan committed Oct 21, 2023
1 parent 3564065 commit fe6b1d5
Show file tree
Hide file tree
Showing 7 changed files with 461 additions and 173 deletions.
14 changes: 0 additions & 14 deletions _dev/js/theme/components/dynamic-bootstrap-components.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import DOMReady from '../utils/DOMReady';
import useBootstrapComponentDynamicImport from '../utils/dynamicImports/useBootstrapComponentDynamicImport';

DOMReady(() => {
/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "import" }] */

const { init: initDynamicImportForModal } = useBootstrapComponentDynamicImport(
() => [
import('bootstrap/js/src/modal'),
Expand All @@ -22,18 +20,6 @@ DOMReady(() => {

initDynamicImportForModal();

// const modal = new bootstrap.Modal('#testModal', {
// keyboard: false,
// });
//
// const handleTestModal = () => {
// modal.toggle();
// console.log('toggle');
// setTimeout(handleTestModal, 4000);
// }
//
// handleTestModal();

const { init: initDynamicImportForOffcanvas } = useBootstrapComponentDynamicImport(
() => [
import('bootstrap/js/src/offcanvas'),
Expand Down
26 changes: 26 additions & 0 deletions _dev/js/theme/utils/dynamicImports/componentProxyFactory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const handleProxyGet = (element, proxyMethodHandler) => (target, prop, receiver) => {
if (target[prop] !== undefined) {
return target[prop];
}

return (...args) => proxyMethodHandler(target, prop, receiver, element, args);
};

const componentProxyFactory = (
element,
options,
proxyMethodHandler = () => {},
) => {
const pluginObject = {
_element: element,
_options: options,
};

const proxyHandler = {
get: handleProxyGet(element, proxyMethodHandler),
};

return new Proxy(pluginObject, proxyHandler);
};

export default componentProxyFactory;
278 changes: 128 additions & 150 deletions _dev/js/theme/utils/dynamicImports/useBootstrapComponentDynamicImport.js
Original file line number Diff line number Diff line change
@@ -1,210 +1,188 @@
import useEvent from '../../components/event/useEvent';

const { on, off } = useEvent();
import useInstanceMap from './useInstanceMap';
import useDynamicImportEventsHandler from './useDynamicImportEventsHandler';
import useFunctionCallstackMap from './useFunctionCallstackMap';
import componentProxyFactory from './componentProxyFactory';

const isJQueryEnabled = () => {
try {
return !!window.jQuery;
} catch (e) {
return false;
}
};

const useBootstrapComponentDynamicImport = (importFiles, {
componentName = '',
componentName,
events = [],
onLoad = () => {},
}) => {
let filesLoaded = false;
const callStack = [];
const jQueryCallStack = [];
const instancesMap = new Map();

const setInstanceInMap = (element, instance) => {
if (!instancesMap.has(element)) {
instancesMap.set(element, instance);
}
};

const getInstanceFromMap = (element) => {
if (!instancesMap.has(element)) {
return null;
}

return instancesMap.get(element);
};

if (!componentName) {
throw new Error('Component name is required');
}

const getJQueryComponentName = () => componentName.charAt(0).toLowerCase() + componentName.slice(1);

const isJQueryEnabled = () => {
try {
return !!window.jQuery;
} catch (e) {
return false;
let filesLoaded = false;
let filesLoading = false;
const {
getCallbacksForElement,
setCallbackForElement,
removeCallbacksForElement,
createCallbackObject,
} = useFunctionCallstackMap(componentName);
const {
setCallbackForElement: jQuerySetCallbackForElement,
removeCallbacksForElement: jQueryRemoveCallbacksForElement,
createCallbackObject: jQueryCreateCallbackObject,
getAllCallbacksForComponent: jQueryGetAllCallbacksForComponent,
} = useFunctionCallstackMap(`jQuery_${componentName}`);
const {
getInstanceFromMap,
setInstanceInMap,
removeInstanceFromMap,
getAllInstancesForComponent,
} = useInstanceMap(componentName);

const getJQueryComponentName = () => componentName.toLowerCase();

const loadFiles = () => Promise.all(importFiles()).then((files) => {
for (let i = 0; i < files.length; i += 1) {
const file = files[i];

if (file.default) {
window.bootstrap[componentName] = file.default;
break;
}
}
};

const loadFiles = () => {
filesLoaded = true;
filesLoading = false;
});

return Promise.all(importFiles()).then((files) => {
files.forEach((file) => {
if (file.default) {
window.bootstrap[componentName] = file.default;
}
});
});
};
const executeComponentInitializationAndCallbackCalls = () => {
const instances = getAllInstancesForComponent();

const executeCallStack = () => {
callStack.forEach(({ args, instanceMethodCall, componentInstance }, i) => {
componentInstance = new window.bootstrap[componentName](args);
if (instances) {
instances.forEach((proxy, element) => {
const {
_options: options,
} = proxy;

callStack[i].componentInstance = componentInstance;
const instance = new window.bootstrap[componentName](element, options);
setInstanceInMap(element, instance);

setInstanceInMap(componentInstance._element, componentInstance);
const callbacks = getCallbacksForElement(element);

instanceMethodCall.forEach(({ prop, methodArgs }) => {
componentInstance[prop](...methodArgs);
});
});
if (callbacks) {
callbacks.forEach((callbackObject) => {
const {
prop,
args,
} = callbackObject;

if (isJQueryEnabled()) {
jQueryCallStack.forEach(({ elem, args }) => {
window.jQuery(elem)[getJQueryComponentName()](args);
instance[prop](...args);
});

removeCallbacksForElement(element);
}
});
}
};

const handleEvent = async (e) => {
e.preventDefault();

// DISABLE FOR NOW BEFORE REFACTORING
/* eslint-disable */
await handleComponentLoad();
/* eslint-enable */

const { currentTarget, type } = e;

currentTarget.dispatchEvent(new Event(type));
};
if (isJQueryEnabled()) {
const allCallbacks = jQueryGetAllCallbacksForComponent();

const unbindEvents = () => {
events.forEach(({
name = '',
selector = '',
}) => {
if (!name || !selector) {
throw new Error('Event name and selector are required');
}
if (allCallbacks) {
allCallbacks.forEach((callbacks, jqueryObject) => {
callbacks.forEach((callbackObject) => {
const {
prop,
args,
} = callbackObject;

off(
document,
name,
selector,
handleEvent,
);
});
};
jqueryObject[prop](...args);
});

const bindEvents = () => {
events.forEach(({
name = '',
selector = '',
}) => {
if (!name || !selector) {
throw new Error('Event name and selector are required');
jQueryRemoveCallbacksForElement(jqueryObject);
});
}

on(
document,
name,
selector,
handleEvent,
);
});
}
};

const handleComponentLoad = async () => {
if (filesLoaded) {
if (filesLoaded || filesLoading) {
return;
}

filesLoading = true;

// eslint-disable-next-line no-use-before-define
unbindEvents();
await loadFiles();
onLoad();
executeCallStack();
executeComponentInitializationAndCallbackCalls();
};

const proxyFactory = (pluginInstance) => {
const pluginObject = {};
const proxyHandler = {
get(target, prop, receiver) {
return (...args) => {
if (!filesLoaded) {
pluginInstance.instanceMethodCall.push({
prop,
args,
});

handleComponentLoad();
}
const handleEvent = async (e) => {
e.preventDefault();

if (pluginInstance.componentInstance !== null) {
pluginInstance.componentInstance[prop](...args);
}
await handleComponentLoad();

return receiver;
};
},
};
const { currentTarget, type } = e;

return new Proxy(pluginObject, proxyHandler);
currentTarget.dispatchEvent(new Event(type));
};

const getComponentInstance = (element) => getInstanceFromMap(element);

function ComponentObjectConstructorFunction(args) {
const pluginInstance = {
args,
instanceMethodCall: [],
componentInstance: null,
};
const { bindEvents, unbindEvents } = useDynamicImportEventsHandler(events, handleEvent);

pluginInstance.proxyInstance = proxyFactory(pluginInstance);
const getComponentInstance = (element) => getInstanceFromMap(element);

callStack.push(pluginInstance);
const proxyMethodCallHandler = (target, prop, receiver, element, args) => {
const instance = getInstanceFromMap(element);

return pluginInstance.proxyInstance;
}
if (!filesLoaded) {
setCallbackForElement(element, createCallbackObject(prop, args));

const getOrCreateInstance = (element) => {
const pluginInstance = getComponentInstance(element);
handleComponentLoad();
} else {
if (instance !== null) {
instance[prop](...args);
}

if (pluginInstance) {
return pluginInstance.proxyInstance;
if (prop === 'dispose' && instance !== null) {
removeInstanceFromMap(element);
}
}

const proxyInstance = new ComponentObjectConstructorFunction(element);
return receiver;
};

setInstanceInMap(element, proxyInstance);
function ComponentObjectConstructorFunction(selectorOrElement, options = {}) {
const element = selectorOrElement instanceof Element ? selectorOrElement : document.querySelector(selectorOrElement);
const componentInstanceProxy = componentProxyFactory(element, options, proxyMethodCallHandler);

return proxyInstance;
};
setInstanceInMap(element, componentInstanceProxy);

ComponentObjectConstructorFunction.getOrCreateInstance = getOrCreateInstance;
ComponentObjectConstructorFunction.getInstance = getComponentInstance;
return componentInstanceProxy;
}

const handleJQueryPluginCall = (args) => {
jQueryCallStack.push({
elem: this,
args,
});
ComponentObjectConstructorFunction.getOrCreateInstance = (element, options) => {
const componentInstance = getComponentInstance(element);

if (componentInstance) {
return componentInstance;
}

handleComponentLoad();
return new ComponentObjectConstructorFunction(element, options);
};

ComponentObjectConstructorFunction.getInstance = getComponentInstance;

window.bootstrap = window.bootstrap || {};
window.bootstrap[componentName] = ComponentObjectConstructorFunction;
window.bootstrap[componentName].NAME = componentName.toLowerCase();

if (isJQueryEnabled()) {
window.jQuery.fn[getJQueryComponentName()] = handleJQueryPluginCall;
window.jQuery.fn[getJQueryComponentName()] = function jqueryFunctionCallback(...args) {
jQuerySetCallbackForElement(this, jQueryCreateCallbackObject(getJQueryComponentName(), args));

handleComponentLoad();
};
}

const init = () => {
Expand Down
Loading

0 comments on commit fe6b1d5

Please sign in to comment.