Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 1b885d7
Showing
3 changed files
with
187 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
node_modules/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
{ | ||
"name": "single-spa", | ||
"version": "1.0.0", | ||
"description": "Multiple applications, one page", | ||
"main": "dist/single-spa.js", | ||
"scripts": { | ||
"build": "babel src/single-spa.js --out-file dist/single-spa.js" | ||
}, | ||
"keywords": [ | ||
"single", | ||
"page", | ||
"application", | ||
"spa", | ||
"multiple", | ||
"lifecycle" | ||
], | ||
"author": "Joel Denning", | ||
"license": "MIT", | ||
"dependencies": { | ||
"es6-module-loader": "0.17.7" | ||
}, | ||
"devDependencies": { | ||
"babel": "^5.8.23" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
let appLocationToApp = {}; | ||
let unhandledRouteHandlers = []; | ||
let mountedApp; | ||
const nativeAddEventListener = window.addEventListener; | ||
|
||
window.singlespa = function(element) { | ||
window.history.pushState(undefined, '', element.getAttribute('href')); | ||
setTimeout(function() { | ||
triggerAppChange(); | ||
}, 10); | ||
return false; | ||
} | ||
|
||
export function declareChildApplication(appLocation, activeWhen) { | ||
if (typeof appLocation !== 'string' || appLocation.length === 0) | ||
throw new Error(`The first argument must be a non-empty string 'appLocation'`); | ||
if (typeof activeWhen !== 'function') | ||
throw new Error(`The second argument must be a function 'activeWhen'`); | ||
if (appLocationToApp[appLocation]) | ||
throw new Error(`There is already an app declared at location ${appLocation}`); | ||
|
||
appLocationToApp[appLocation] = { | ||
appLocation: appLocation, | ||
activeWhen: activeWhen, | ||
parentApp: mountedApp ? mountedApp.appLocation : null | ||
}; | ||
|
||
triggerAppChange(); | ||
} | ||
|
||
export function addUnhandledRouteHandler(handler) { | ||
if (typeof handler !== 'function') { | ||
throw new Error(`The first argument must be a handler function`); | ||
} | ||
unhandledRouteHandlers.push(handler); | ||
} | ||
|
||
export function updateApplicationSourceCode(appName) { | ||
if (!appLocationToApp[appName]) { | ||
throw new Error(`No such app '${appName}'`); | ||
} | ||
let app = appLocationToApp[appName]; | ||
app.lifecycleFunctions.activeApplicationSourceWillUpdate().then(() => { | ||
//TODO reload the app | ||
app.lifecycleFunctions.activeApplicationSourceWasUpdated(); | ||
}); | ||
} | ||
|
||
function loadAppForFirstTime(appLocation) { | ||
return new Promise(function(resolve, reject) { | ||
System.import(appLocation).then(function(restOfApp) { | ||
if (restOfApp.default) { | ||
restOfApp = restOfApp.default; | ||
} | ||
registerApplication(appLocation, restOfApp); | ||
let app = appLocationToApp[appLocation]; | ||
app.entryWillBeInstalled().then(() => { | ||
System.import(app.entry).then(() => { | ||
resolve(); | ||
}) | ||
}) | ||
}) | ||
}) | ||
} | ||
|
||
function registerApplication(appLocation, partialApp) { | ||
let app = appLocationToApp[appLocation]; | ||
for (propertyName in partialApp) { | ||
app[propertyName] = partialApp[propertyName]; | ||
} | ||
app.hashChangeFunctions = []; | ||
app.popStateFunctions = []; | ||
} | ||
|
||
nativeAddEventListener('popstate', triggerAppChange); | ||
|
||
function triggerAppChange() { | ||
let newApp = appForCurrentURL(); | ||
if (!newApp) { | ||
unhandledRouteHandlers.forEach((handler) => { | ||
handler(mountedApp); | ||
}); | ||
} | ||
|
||
if (newApp !== mountedApp) { | ||
let appWillUnmountPromise = mountedApp ? mountedApp.applicationWillUnmount() : new Promise((resolve) => resolve()); | ||
|
||
appWillUnmountPromise.then(function() { | ||
let appLoadedPromise = newApp.entry ? new Promise((resolve) => resolve()) : loadAppForFirstTime(newApp.appLocation); | ||
appLoadedPromise.then(function() { | ||
let appMountedPromise = new Promise(function(resolve) { | ||
if (mountedApp) { | ||
mountedApp.unmountApplication().then(() => { | ||
finishUnmountingApp(mountedApp); | ||
resolve(); | ||
}); | ||
} else { | ||
resolve(); | ||
} | ||
}); | ||
appMountedPromise.then(function() { | ||
newApp.applicationWillMount().then(function() { | ||
appWillBeMounted(newApp); | ||
newApp.mountApplication().then(function() { | ||
mountedApp = newApp; | ||
}); | ||
}) | ||
}); | ||
}) | ||
}) | ||
} | ||
} | ||
|
||
function appForCurrentURL() { | ||
let appsForCurrentUrl = []; | ||
for (let appName in appLocationToApp) { | ||
let app = appLocationToApp[appName]; | ||
if (app.activeWhen(window.location)) { | ||
appsForCurrentUrl.push(app); | ||
} | ||
} | ||
switch (appsForCurrentUrl.length) { | ||
case 0: | ||
return undefined; | ||
case 1: | ||
return appsForCurrentUrl[0]; | ||
default: | ||
appNames = appsForCurrentUrl.map((app) => app.name); | ||
throw new Error(`The following applications all claim to own the location ${window.location.href} -- ${appnames.toString()}`) | ||
} | ||
} | ||
|
||
function finishUnmountingApp(app) { | ||
app.hashChangeFunctions.forEach((hashChangeFunction) => { | ||
window.removeEventListener('hashchange', hashChangeFunction); | ||
}); | ||
app.popStateFunctions.forEach((popStateFunction) => { | ||
window.removeEventListener('popstate', popStateFunctions); | ||
}); | ||
//TODO clean up the dom??? | ||
} | ||
|
||
function appWillBeMounted(app) { | ||
app.hashChangeFunctions.forEach((hashChangeFunction) => { | ||
nativeAddEventListener('hashchange', hashChangeFunction); | ||
}); | ||
app.popStateFunctions.forEach((popStateFunction) => { | ||
nativeAddEventListener('popstate', popStateFunctions); | ||
}); | ||
} | ||
|
||
window.addEventListener = function(name, fn) { | ||
if (mountedApp) { | ||
if (name === 'popstate') { | ||
mountedApp.popStateFunctions.push(fn); | ||
} else if (name === 'hashchange') { | ||
mountedApp.hashChangeFunctions.push(fn); | ||
} | ||
nativeAddEventListener.apply(this, arguments); | ||
} | ||
} |