Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
joeldenning committed Sep 22, 2015
0 parents commit 1b885d7
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
@@ -0,0 +1 @@
node_modules/
25 changes: 25 additions & 0 deletions package.json
@@ -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"
}
}
161 changes: 161 additions & 0 deletions src/single-spa.js
@@ -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);
}
}

0 comments on commit 1b885d7

Please sign in to comment.