Skip to content

Commit

Permalink
add destroy method and fix problem with dynamically loading modules o…
Browse files Browse the repository at this point in the history
…n slow connections
  • Loading branch information
rikschennink committed Sep 27, 2019
1 parent d214862 commit 35018c9
Show file tree
Hide file tree
Showing 8 changed files with 5,183 additions and 164 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ Bound modules are returned by the `hydrate` method. Each bound module object wra
| `onmount(boundModule)` | Callback that runs when the module has been mounted. Scoped to element. |
| `onmounterror(error, boundModule)` | Callback that runs when an error occurs during the mount process. Scoped to element. |
| `onunmount(boundModule)` | Callback that runs when the module has been unmounted. Scoped to element. |
| `ondestroy(boundModule)` | Callback that runs when the module has been destroyed. Scoped to element. |


### Context Monitor

Expand Down Expand Up @@ -215,6 +217,8 @@ We can link our plugins to the following hooks:
| `moduleDidMount(boundModule)` | Called after the module is mounted. |
| `moduleWillUnmount(boundModule)` | Called before the module is unmounted. |
| `moduleDidUnmount(boundModule)` | Called after the module is unmounted. |
| `moduleWillDestroy(boundModule)` | Called before the module is destroyed. |
| `moduleDidDestroy(boundModule)` | Called after the module is destroyed. |
| `moduleDidCatch(error, boundModule)` | Called when module import throws an error. |
| `monitor` | A collection of registered monitors. See monitor setup instructions below. |

Expand Down
124 changes: 68 additions & 56 deletions conditioner-core.esm.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* conditioner-core 2.3.1 */
/* conditioner-core 2.3.2 */
// links the module to the element and exposes a callback api object
const bindModule = element => {
const bindModule = (element, unbind) => {
// gets the name of the module from the element, we assume the name is an alias
const alias = runPlugin('moduleGetName', element);

Expand All @@ -27,13 +27,11 @@ const bindModule = element => {
// is the module currently mounted?
mounted: false,

// unload is empty function so we can blindly call it if initial context does not match
// unmounts the module
unmount: () => {
// can't be unmounted if no destroy method has been supplied
// can't be unmounted if not mounted
if (!state.destruct || !boundModule.mounted) {
return;
}
if (!state.destruct || !boundModule.mounted) return;

// about to unmount the module
eachPlugins('moduleWillUnmount', boundModule);
Expand All @@ -47,36 +45,24 @@ const bindModule = element => {
// done unmounting the module
eachPlugins('moduleDidUnmount', boundModule);

// done destroying
// done unmounting
boundModule.onunmount.apply(element);
},

// requests and loads the module
mount: () => {
// can't mount an already mounted module
// can't mount a module that is currently mounting
if (boundModule.mounted || state.mounting) {
return;
}
if (boundModule.mounted || state.mounting) return;

// now mounting module
state.mounting = true;

// about to mount the module
eachPlugins('moduleWillMount', boundModule);

// get the module
runPlugin('moduleImport', name)
.catch(error => {
// failed to mount so no longer mounting
state.mounting = false;

// failed to mount the module
eachPlugins('moduleDidCatch', error, boundModule);

// callback for this specific module
boundModule.onmounterror.apply(element, [error, boundModule]);

// let dev know
throw new Error(`Conditioner: ${error}`);
})
.then(module => {
// initialise the module, module can return a destroy mehod
state.destruct = runPlugin(
Expand All @@ -86,23 +72,55 @@ const bindModule = element => {
)
);

// module is now mounted
boundModule.mounted = true;

// no longer mounting
state.mounting = false;

// module is now mounted
boundModule.mounted = true;

// did mount the module
eachPlugins('moduleDidMount', boundModule);

// module has now loaded lets fire the onload event so everyone knows about it
boundModule.onmount.apply(element, [boundModule]);
})
.catch(error => {
// failed to mount so no longer mounting
state.mounting = false;

// failed to mount the module
eachPlugins('moduleDidCatch', error, boundModule);

// callback for this specific module
boundModule.onmounterror.apply(element, [error, boundModule]);

// let dev know
throw new Error(`Conditioner: ${error}`);
});

// return state object
return boundModule;
},

// unmounts the module and destroys the attached monitors
destroy: function() {

// about to destroy the module
eachPlugins('moduleWillDestroy', boundModule);

// not implemented yet
boundModule.unmount();

// did destroy the module
eachPlugins('moduleDidDestroy', boundModule);

// call public ondestroy so dev can handle it as well
boundModule.ondestroy.apply(element);

// call the destroy callback so monitor can be removed as well
unbind();
},

// called when fails to bind the module
onmounterror: function() {},

Expand All @@ -112,8 +130,8 @@ const bindModule = element => {
// called when the module is unloaded, scope is set to element
onunmount: function() {},

// unmounts the module and destroys the attached monitors
destroy: function() {}
// called when the module is destroyed
ondestroy: function() {}
};

// done!
Expand Down Expand Up @@ -148,9 +166,7 @@ const getContextMonitor = (element, name, context) => {
const monitor = getPlugins('monitor').find(monitor => monitor.name === name);
// @exclude
if (!monitor) {
throw new Error(
`Conditioner: Cannot find monitor with name "@${name}". Only the "@media" monitor is always available. Custom monitors can be added with the \`addPlugin\` method using the \`monitors\` key. The name of the custom monitor should not include the "@" symbol.`
);
throw new Error(`Conditioner: Cannot find monitor with name "@${name}". Only the "@media" monitor is always available. Custom monitors can be added with the \`addPlugin\` method using the \`monitors\` key. The name of the custom monitor should not include the "@" symbol.`);
}
// @endexclude
return monitor.create(context, element);
Expand All @@ -161,22 +177,16 @@ const matchMonitors = monitors =>
monitors.reduce(
(matches, monitor) => {
// an earlier monitor returned false, so current context will no longer be suitable
if (!matches) {
return false;
}
if (!matches) return false;

// get current match state, takes "not" into account
const matched = monitor.invert ? !monitor.matches : monitor.matches;

// mark monitor as has been matched in the past
if (matched) {
monitor.matched = true;
}
if (matched) monitor.matched = true;

// if retain is enabled with "was" and the monitor has been matched in the past, there's a match
if (monitor.retain && monitor.matched) {
return true;
}
if (monitor.retain && monitor.matched) return true;

// return current match state
return matched;
Expand All @@ -194,9 +204,7 @@ export const monitor = (query, element) => {
onchange: function() {},
start: () => {
// cannot be activated when already active
if (contextMonitor.active) {
return;
}
if (contextMonitor.active) return;

// now activating
contextMonitor.active = true;
Expand All @@ -217,9 +225,7 @@ export const monitor = (query, element) => {
monitorSets.forEach(monitorSet =>
monitorSet.forEach(monitor => {
// stop listening (if possible)
if (!monitor.removeListener) {
return;
}
if (!monitor.removeListener) return;
monitor.removeListener(onMonitorEvent);
})
);
Expand All @@ -242,10 +248,10 @@ export const monitor = (query, element) => {
// if all monitors return true for .matches getter, we mount the module
const onMonitorEvent = () => {
// will keep returning false if one of the monitors does not match, else checks matches property
const matches = monitorSets.reduce((matches, monitorSet) => {
const matches = monitorSets.reduce((matches, monitorSet) =>
// if one of the sets is true, it's all fine, no need to match the other sets
return matches ? true : matchMonitors(monitorSet);
}, false);
matches ? true : matchMonitors(monitorSet)
, false);

// store new state
contextMonitor.matches = matches;
Expand All @@ -261,24 +267,32 @@ export const monitor = (query, element) => {
const createContextualModule = (query, boundModule) => {
// setup query monitor
const moduleMonitor = monitor(query, boundModule.element);
moduleMonitor.onchange = matches => (matches ? boundModule.mount() : boundModule.unmount());
moduleMonitor.onchange = matches => matches ? boundModule.mount() : boundModule.unmount();

// start monitoring
moduleMonitor.start();

return boundModule;
// export monitor
return moduleMonitor;
};

// pass in an element and outputs a bound module object, will wrap bound module in a contextual module if required
const createModule = element => {

// called when the module is destroyed
const unbindModule = () => monitor && monitor.destroy();

// bind the module to the element and receive the module wrapper API
const boundModule = bindModule(element);
const boundModule = bindModule(element, unbindModule);

// get context requirements for this module (if any have been defined)
const query = runPlugin('moduleGetContext', element);

// wait for the right context or load the module immidiately if no context supplied
return query ? createContextualModule(query, boundModule) : boundModule.mount();
const monitor = createContextualModule(query, boundModule);

// return module
return query ? boundModule : boundModule.mount();
};

// parse a certain section of the DOM and load bound modules
Expand Down Expand Up @@ -320,9 +334,7 @@ addPlugin({
// load the referenced module, by default searches global scope for module name
moduleImport: name =>
new Promise((resolve, reject) => {
if (self[name]) {
return resolve(self[name]);
}
if (self[name]) return resolve(self[name]);
// @exclude
reject(
`Cannot find module with name "${name}". By default Conditioner will import modules from the global scope, make sure a function named "${name}" is defined on the window object. The scope of a function defined with \`let\` or \`const\` is limited to the <script> block in which it is defined.`
Expand Down
Loading

2 comments on commit 35018c9

@andyfitch
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi Rik,

This commit produces an error if you do not provide a data-context="", which is the case in many of our projects. I can see that you committed it today so maybe you already know about this.

We love this library at @parallax!

Here is the error since 2.3.2:

Uncaught TypeError: Cannot read property 'split' of undefined
at monitor (conditioner-core.js?a945:316)
at createContextualModule (conditioner-core.js?a945:345)
at createModule (conditioner-core.js?a945:372)
at Array.map ()
at Object.hydrate (conditioner-core.js?a945:380)
at eval (main.js?c45b:28)
at Object. (main.js:520)
at webpack_require (main.js:50)
at main.js:145
at main.js:148

Cheers,
Andy

@rikschennink
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick headsup! Fixed in 2.3.3

Please sign in to comment.