Skip to content

Commit 1b885d7

Browse files
author
Joel Denning
committed
Initial commit
0 parents  commit 1b885d7

File tree

3 files changed

+187
-0
lines changed

3 files changed

+187
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules/

package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "single-spa",
3+
"version": "1.0.0",
4+
"description": "Multiple applications, one page",
5+
"main": "dist/single-spa.js",
6+
"scripts": {
7+
"build": "babel src/single-spa.js --out-file dist/single-spa.js"
8+
},
9+
"keywords": [
10+
"single",
11+
"page",
12+
"application",
13+
"spa",
14+
"multiple",
15+
"lifecycle"
16+
],
17+
"author": "Joel Denning",
18+
"license": "MIT",
19+
"dependencies": {
20+
"es6-module-loader": "0.17.7"
21+
},
22+
"devDependencies": {
23+
"babel": "^5.8.23"
24+
}
25+
}

src/single-spa.js

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
let appLocationToApp = {};
2+
let unhandledRouteHandlers = [];
3+
let mountedApp;
4+
const nativeAddEventListener = window.addEventListener;
5+
6+
window.singlespa = function(element) {
7+
window.history.pushState(undefined, '', element.getAttribute('href'));
8+
setTimeout(function() {
9+
triggerAppChange();
10+
}, 10);
11+
return false;
12+
}
13+
14+
export function declareChildApplication(appLocation, activeWhen) {
15+
if (typeof appLocation !== 'string' || appLocation.length === 0)
16+
throw new Error(`The first argument must be a non-empty string 'appLocation'`);
17+
if (typeof activeWhen !== 'function')
18+
throw new Error(`The second argument must be a function 'activeWhen'`);
19+
if (appLocationToApp[appLocation])
20+
throw new Error(`There is already an app declared at location ${appLocation}`);
21+
22+
appLocationToApp[appLocation] = {
23+
appLocation: appLocation,
24+
activeWhen: activeWhen,
25+
parentApp: mountedApp ? mountedApp.appLocation : null
26+
};
27+
28+
triggerAppChange();
29+
}
30+
31+
export function addUnhandledRouteHandler(handler) {
32+
if (typeof handler !== 'function') {
33+
throw new Error(`The first argument must be a handler function`);
34+
}
35+
unhandledRouteHandlers.push(handler);
36+
}
37+
38+
export function updateApplicationSourceCode(appName) {
39+
if (!appLocationToApp[appName]) {
40+
throw new Error(`No such app '${appName}'`);
41+
}
42+
let app = appLocationToApp[appName];
43+
app.lifecycleFunctions.activeApplicationSourceWillUpdate().then(() => {
44+
//TODO reload the app
45+
app.lifecycleFunctions.activeApplicationSourceWasUpdated();
46+
});
47+
}
48+
49+
function loadAppForFirstTime(appLocation) {
50+
return new Promise(function(resolve, reject) {
51+
System.import(appLocation).then(function(restOfApp) {
52+
if (restOfApp.default) {
53+
restOfApp = restOfApp.default;
54+
}
55+
registerApplication(appLocation, restOfApp);
56+
let app = appLocationToApp[appLocation];
57+
app.entryWillBeInstalled().then(() => {
58+
System.import(app.entry).then(() => {
59+
resolve();
60+
})
61+
})
62+
})
63+
})
64+
}
65+
66+
function registerApplication(appLocation, partialApp) {
67+
let app = appLocationToApp[appLocation];
68+
for (propertyName in partialApp) {
69+
app[propertyName] = partialApp[propertyName];
70+
}
71+
app.hashChangeFunctions = [];
72+
app.popStateFunctions = [];
73+
}
74+
75+
nativeAddEventListener('popstate', triggerAppChange);
76+
77+
function triggerAppChange() {
78+
let newApp = appForCurrentURL();
79+
if (!newApp) {
80+
unhandledRouteHandlers.forEach((handler) => {
81+
handler(mountedApp);
82+
});
83+
}
84+
85+
if (newApp !== mountedApp) {
86+
let appWillUnmountPromise = mountedApp ? mountedApp.applicationWillUnmount() : new Promise((resolve) => resolve());
87+
88+
appWillUnmountPromise.then(function() {
89+
let appLoadedPromise = newApp.entry ? new Promise((resolve) => resolve()) : loadAppForFirstTime(newApp.appLocation);
90+
appLoadedPromise.then(function() {
91+
let appMountedPromise = new Promise(function(resolve) {
92+
if (mountedApp) {
93+
mountedApp.unmountApplication().then(() => {
94+
finishUnmountingApp(mountedApp);
95+
resolve();
96+
});
97+
} else {
98+
resolve();
99+
}
100+
});
101+
appMountedPromise.then(function() {
102+
newApp.applicationWillMount().then(function() {
103+
appWillBeMounted(newApp);
104+
newApp.mountApplication().then(function() {
105+
mountedApp = newApp;
106+
});
107+
})
108+
});
109+
})
110+
})
111+
}
112+
}
113+
114+
function appForCurrentURL() {
115+
let appsForCurrentUrl = [];
116+
for (let appName in appLocationToApp) {
117+
let app = appLocationToApp[appName];
118+
if (app.activeWhen(window.location)) {
119+
appsForCurrentUrl.push(app);
120+
}
121+
}
122+
switch (appsForCurrentUrl.length) {
123+
case 0:
124+
return undefined;
125+
case 1:
126+
return appsForCurrentUrl[0];
127+
default:
128+
appNames = appsForCurrentUrl.map((app) => app.name);
129+
throw new Error(`The following applications all claim to own the location ${window.location.href} -- ${appnames.toString()}`)
130+
}
131+
}
132+
133+
function finishUnmountingApp(app) {
134+
app.hashChangeFunctions.forEach((hashChangeFunction) => {
135+
window.removeEventListener('hashchange', hashChangeFunction);
136+
});
137+
app.popStateFunctions.forEach((popStateFunction) => {
138+
window.removeEventListener('popstate', popStateFunctions);
139+
});
140+
//TODO clean up the dom???
141+
}
142+
143+
function appWillBeMounted(app) {
144+
app.hashChangeFunctions.forEach((hashChangeFunction) => {
145+
nativeAddEventListener('hashchange', hashChangeFunction);
146+
});
147+
app.popStateFunctions.forEach((popStateFunction) => {
148+
nativeAddEventListener('popstate', popStateFunctions);
149+
});
150+
}
151+
152+
window.addEventListener = function(name, fn) {
153+
if (mountedApp) {
154+
if (name === 'popstate') {
155+
mountedApp.popStateFunctions.push(fn);
156+
} else if (name === 'hashchange') {
157+
mountedApp.hashChangeFunctions.push(fn);
158+
}
159+
nativeAddEventListener.apply(this, arguments);
160+
}
161+
}

0 commit comments

Comments
 (0)