From 579293f6503ad9f9fd7bbb594dc98a4b2c8a658b Mon Sep 17 00:00:00 2001 From: joeldenning Date: Fri, 17 Nov 2023 14:30:14 -0700 Subject: [PATCH] Create new versioned docs for single-spa@6 --- versioned_docs/version-6.x/CODE_OF_CONDUCT.md | 47 + versioned_docs/version-6.x/api.md | 928 ++++++++++++ .../version-6.x/building-applications.md | 207 +++ .../version-6.x/contributing-overview.md | 87 ++ .../version-6.x/create-single-spa.md | 212 +++ versioned_docs/version-6.x/devtools.md | 72 + .../version-6.x/ecosystem-alpinejs.md | 254 ++++ .../version-6.x/ecosystem-angular.md | 1314 +++++++++++++++++ .../version-6.x/ecosystem-angularjs.md | 298 ++++ .../version-6.x/ecosystem-backbone.md | 174 +++ versioned_docs/version-6.x/ecosystem-css.md | 371 +++++ versioned_docs/version-6.x/ecosystem-cycle.md | 41 + versioned_docs/version-6.x/ecosystem-dojo.md | 67 + versioned_docs/version-6.x/ecosystem-ember.md | 170 +++ .../ecosystem-html-web-components.md | 60 + .../version-6.x/ecosystem-inferno.md | 37 + .../version-6.x/ecosystem-leaked-globals.md | 101 ++ .../version-6.x/ecosystem-preact.md | 39 + versioned_docs/version-6.x/ecosystem-react.md | 193 +++ versioned_docs/version-6.x/ecosystem-riot.md | 45 + .../version-6.x/ecosystem-snowpack.md | 106 ++ .../version-6.x/ecosystem-svelte.md | 44 + versioned_docs/version-6.x/ecosystem-vite.md | 125 ++ versioned_docs/version-6.x/ecosystem-vue.md | 403 +++++ versioned_docs/version-6.x/ecosystem.md | 34 + versioned_docs/version-6.x/examples.md | 50 + versioned_docs/version-6.x/faq.md | 172 +++ .../version-6.x/getting-started-overview.md | 168 +++ versioned_docs/version-6.x/glossary.md | 24 + versioned_docs/version-6.x/index.md | 0 versioned_docs/version-6.x/layout-api.md | 240 +++ .../version-6.x/layout-definition.md | 518 +++++++ versioned_docs/version-6.x/layout-overview.md | 101 ++ .../version-6.x/microfrontends-concept.md | 51 + .../version-6.x/migrating-existing-spas.md | 37 + versioned_docs/version-6.x/module-types.md | 77 + versioned_docs/version-6.x/parcels-api.md | 102 ++ .../version-6.x/parcels-overview.md | 229 +++ .../version-6.x/recommended-setup.md | 365 +++++ .../version-6.x/separating-applications.md | 66 + .../server-side-rendering-overview.md | 354 +++++ .../version-6.x/shared-webpack-configs.md | 323 ++++ .../version-6.x/single-spa-config.md | 197 +++ .../version-6.x/single-spa-playground.md | 11 + versioned_docs/version-6.x/testing/e2e.md | 36 + versioned_docs/version-6.x/testing/units.md | 109 ++ versioned_docs/version-6.x/videos.md | 17 + versioned_sidebars/version-6.x-sidebars.json | 281 ++++ versions.json | 1 + 49 files changed, 8958 insertions(+) create mode 100644 versioned_docs/version-6.x/CODE_OF_CONDUCT.md create mode 100644 versioned_docs/version-6.x/api.md create mode 100644 versioned_docs/version-6.x/building-applications.md create mode 100644 versioned_docs/version-6.x/contributing-overview.md create mode 100644 versioned_docs/version-6.x/create-single-spa.md create mode 100644 versioned_docs/version-6.x/devtools.md create mode 100644 versioned_docs/version-6.x/ecosystem-alpinejs.md create mode 100644 versioned_docs/version-6.x/ecosystem-angular.md create mode 100644 versioned_docs/version-6.x/ecosystem-angularjs.md create mode 100644 versioned_docs/version-6.x/ecosystem-backbone.md create mode 100644 versioned_docs/version-6.x/ecosystem-css.md create mode 100644 versioned_docs/version-6.x/ecosystem-cycle.md create mode 100644 versioned_docs/version-6.x/ecosystem-dojo.md create mode 100644 versioned_docs/version-6.x/ecosystem-ember.md create mode 100644 versioned_docs/version-6.x/ecosystem-html-web-components.md create mode 100644 versioned_docs/version-6.x/ecosystem-inferno.md create mode 100644 versioned_docs/version-6.x/ecosystem-leaked-globals.md create mode 100644 versioned_docs/version-6.x/ecosystem-preact.md create mode 100644 versioned_docs/version-6.x/ecosystem-react.md create mode 100644 versioned_docs/version-6.x/ecosystem-riot.md create mode 100644 versioned_docs/version-6.x/ecosystem-snowpack.md create mode 100644 versioned_docs/version-6.x/ecosystem-svelte.md create mode 100644 versioned_docs/version-6.x/ecosystem-vite.md create mode 100644 versioned_docs/version-6.x/ecosystem-vue.md create mode 100644 versioned_docs/version-6.x/ecosystem.md create mode 100644 versioned_docs/version-6.x/examples.md create mode 100644 versioned_docs/version-6.x/faq.md create mode 100644 versioned_docs/version-6.x/getting-started-overview.md create mode 100644 versioned_docs/version-6.x/glossary.md create mode 100644 versioned_docs/version-6.x/index.md create mode 100644 versioned_docs/version-6.x/layout-api.md create mode 100644 versioned_docs/version-6.x/layout-definition.md create mode 100644 versioned_docs/version-6.x/layout-overview.md create mode 100644 versioned_docs/version-6.x/microfrontends-concept.md create mode 100644 versioned_docs/version-6.x/migrating-existing-spas.md create mode 100644 versioned_docs/version-6.x/module-types.md create mode 100644 versioned_docs/version-6.x/parcels-api.md create mode 100644 versioned_docs/version-6.x/parcels-overview.md create mode 100644 versioned_docs/version-6.x/recommended-setup.md create mode 100644 versioned_docs/version-6.x/separating-applications.md create mode 100644 versioned_docs/version-6.x/server-side-rendering-overview.md create mode 100644 versioned_docs/version-6.x/shared-webpack-configs.md create mode 100644 versioned_docs/version-6.x/single-spa-config.md create mode 100644 versioned_docs/version-6.x/single-spa-playground.md create mode 100644 versioned_docs/version-6.x/testing/e2e.md create mode 100644 versioned_docs/version-6.x/testing/units.md create mode 100644 versioned_docs/version-6.x/videos.md create mode 100644 versioned_sidebars/version-6.x-sidebars.json diff --git a/versioned_docs/version-6.x/CODE_OF_CONDUCT.md b/versioned_docs/version-6.x/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..11308d361 --- /dev/null +++ b/versioned_docs/version-6.x/CODE_OF_CONDUCT.md @@ -0,0 +1,47 @@ +--- +id: code-of-conduct +title: Code of Conduct +sidebar_label: Code of Conduct +--- + +This code of conduct outlines our expectations for participants within the single-spa community, as well as steps to reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all and expect our code of conduct to be honored. Anyone who violates this code of conduct may be banned from the community. + +Our open source community strives to: + +* **Be friendly and patient.** +* **Be welcoming:** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, color, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. +* **Be considerate:** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into *account when making decisions. Remember that we’re a world-wide community, so you might not be communicating in someone else’s primary language. +* **Be respectful:** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration *to turn into a personal attack. It’s important to remember that a community where people feel uncomfortable or threatened is not a productive one. +* **Be careful in the words that you choose:** we are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do not insult or put down other participants. Harassment and other *exclusionary behavior aren’t acceptable. This includes, but is not limited to: + * Violent threats or language directed against another person. + * Discriminatory jokes and language. + * Posting sexually explicit or violent material. + * Posting (or threatening to post) other people’s personally identifying information (“doxing”). + * Personal insults, especially those using racist or sexist terms. + * Unwelcome sexual attention. + * Advocating for, or encouraging, any of the above behavior. + * Repeated harassment of others. In general, if someone asks you to stop, then stop. + * **When we disagree, try to understand why:** Disagreements, both social and technical, happen all the time. It is important that we resolve disagreements and differing views constructively. + * **Remember that we’re different.** The strength of our community comes from its diversity, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn’t mean that they’re wrong. Don’t forget that it is human to err and blaming each other doesn’t get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes. + +This code is not exhaustive or complete. It serves to distill our common understanding of a collaborative, shared environment, and goals. We expect it to be followed in spirit as much as in the letter. + +## Diversity Statement + +We encourage everyone to participate and are committed to building a community for all. Although we may not be able to satisfy everyone, we all agree that everyone is equal. Whenever a participant has made a mistake, we expect them to take responsibility for it. If someone has been harmed or offended, it is our responsibility to listen carefully and respectfully, and do our best to right the wrong. + +Although this list cannot be exhaustive, we explicitly honor diversity in age, gender, gender identity or expression, culture, ethnicity, language, national origin, political beliefs, profession, race, religion, sexual orientation, socioeconomic status, and technical ability. We will not tolerate discrimination based on any of the protected characteristics above, including participants with disabilities. + +## Reporting Issues + +If you experience or witness unacceptable behavior—or have any other concerns—please report it by sending a Twitter DM to https://twitter.com/Single_spa. All reports will be handled with discretion. In your report please include: + +* Your contact information. +* Names (real, nicknames, or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well. Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public IRC logger), please include a link. +* Any additional information that may be helpful. + +After filing a report, a representative will contact you personally. If the person who is harassing you is part of the response team, they will recuse themselves from handling your incident. A representative will then review the incident, follow up with any additional questions, and make a decision as to how to respond. We will respect confidentiality requests for the purpose of protecting victims of abuse. + +Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the representative may take any action they deem appropriate, up to and including a permanent ban from our community without warning. + +This Code Of Conduct follows the [template](https://github.com/todogroup/opencodeofconduct) established by the [TODO Group](http://todogroup.org/). \ No newline at end of file diff --git a/versioned_docs/version-6.x/api.md b/versioned_docs/version-6.x/api.md new file mode 100644 index 000000000..589ac819c --- /dev/null +++ b/versioned_docs/version-6.x/api.md @@ -0,0 +1,928 @@ +--- +id: api +title: Applications API +sidebar_label: Applications API +--- + +Single-spa exports named functions and variables rather than a single default export. +This means importing must happen in one of two ways: + +```js +import { registerApplication, start } from 'single-spa'; +// or +import * as singleSpa from 'single-spa'; +``` + +## registerApplication + +`registerApplication` is the most important API your root config will use. Use this function to register any application within single-spa. +Note that if an application is registered from within another application, that no hierarchy will be maintained between the applications. + +There are two ways of registering your application: + +### Simple arguments + +```js +singleSpa.registerApplication( + 'appName', + () => System.import('appName'), + location => location.pathname.startsWith('appName'), +); +``` + +

arguments

+ +
+
appName: string
+
App name that single-spa will register and reference this application with, and will be labelled with in dev tools.
+
applicationOrLoadingFn: () => <Function | Promise>
+
Must be a loading function that either returns the resolved application or a promise.
+
activityFn: (location) => boolean
+
Must be a pure function. The function is called with window.location as the first argument {/* TODO: any only? */} and should return a truthy value whenever the application should be active.
+
customProps?: Object | () => Object
+
Will be passed to the application during each lifecycle method.
+
+ +

returns

+ +`undefined` + +### Configuration object + +```js +singleSpa.registerApplication({ + name: 'appName', + app: () => System.import('appName'), + activeWhen: '/appName', + customProps: { + authToken: 'xc67f6as87f7s9d' + } +}) + +singleSpa.registerApplication({ + name: 'appName', + app: () => System.import('appName'), + activeWhen: '/appName', + // Dynamic custom props that can change based on route + customProps(appName, location) { + return { + authToken: 'xc67f6as87f7s9d' + } + } +}) +``` + +

arguments

+ +
+
name: string
+
App name that single-spa will register and reference this application with, and will be labelled with in dev tools.
+
app: Application | () => Application | Promise<Application>
+
Application object or a function that returns the resolved application (Promise or not)
+
activeWhen: string | (location) => boolean | (string | (location) => boolean)[]
+
Can be a path prefix which will match every URL starting with this path, + an activity function (as described in the simple arguments) or an array + containing both of them. If any of the criteria is true, it will keep the + application active. The path prefix also accepts dynamic values (they must + start with ':'), as some paths would receive url params and should still + trigger your application. + Examples: +
+
'/app1'
+
✅ https://app.com/app1
+
✅ https://app.com/app1/anything/everything
+
🚫 https://app.com/app2
+
'/users/:userId/profile'
+
✅ https://app.com/users/123/profile
+
✅ https://app.com/users/123/profile/sub-profile/
+
🚫 https://app.com/users//profile/sub-profile/
+
🚫 https://app.com/users/profile/sub-profile/
+
'/pathname/#/hash'
+
✅ https://app.com/pathname/#/hash
+
✅ https://app.com/pathname/#/hash/route/nested
+
🚫 https://app.com/pathname#/hash/route/nested
+
🚫 https://app.com/pathname#/another-hash
+
['/pathname/#/hash', '/app1']
+
✅ https://app.com/pathname/#/hash/route/nested
+
✅ https://app.com/app1/anything/everything
+
🚫 https://app.com/pathname/app1
+
🚫 https://app.com/app2
+
+
+
customProps?: Object | () => Object
+
Will be passed to the application during each lifecycle method.
+
+ +

returns

+ +`undefined` + +:::note +It is described in detail inside of the [Configuration docs](configuration#registering-applications) +::: + +## start + +```js +singleSpa.start(); + +// Optionally, you can provide configuration +singleSpa.start({ + urlRerouteOnly: true, +}); +``` + +Must be called by your single spa config. Before `start` is called, applications will be loaded, but will never be bootstrapped, mounted or unmounted. The reason for `start` is to give you control over the performance of your single page application. For example, you may want to declare registered applications immediately (to start downloading the code for the active ones), but not actually mount the registered applications until an initial AJAX request (maybe to get information about the logged in user) has been completed. In that case, the best performance is achieved by calling `registerApplication` immediately, but calling `start` after the AJAX request is completed. + +

arguments

+ +The `start(opts)` api optionally accepts a single `opts` object, with the following properties. If the opts object is omitted, all defaults will be applied. + +- `urlRerouteOnly`: A boolean that defaults to false. If set to true, calls to `history.pushState()` and `history.replaceState()` will not trigger a single-spa reroute unless the client side route was changed. Setting this to true can be better for performance in some situations. For more information, read [original issue](https://github.com/single-spa/single-spa/issues/484). + +

returns

+ +`undefined` + +## triggerAppChange + +```js +singleSpa.triggerAppChange(); +``` + +Returns a Promise that will resolve/reject when all apps have mounted/unmounted. This was built for testing single-spa and is likely not +needed in a production application. + +

arguments

+ +none + +

returns

+ +
+
Promise
+
Returns a Promise that will resolve/reject when all apps have mounted.
+
+ +## navigateToUrl + +```js +// Three ways of using navigateToUrl +singleSpa.navigateToUrl('/new-url'); +singleSpa.navigateToUrl(document.querySelector('a')); +document.querySelector('a').addEventListener(singleSpa.navigateToUrl); +``` + +```html + +My link +``` + +Use this utility function to easily perform url navigation between registered applications without needing to deal with `event.preventDefault()`, `pushState`, `triggerAppChange()`, etc. + +

arguments

+ +
+
navigationObj: string | context | DOMEvent
+
+ navigationObj must be one of the following types: +
    +
  • a url string.
  • +
  • a context / thisArg that has an href property; useful for calling singleSpaNavigate.call(anchorElement) with a reference to the anchor element or other context.
  • +
  • a DOMEvent object for a click event on a DOMElement that has an href attribute; ideal for the <a onclick="singleSpaNavigate"></a> use case.
  • +
+
+
+ +

returns

+ +`undefined` + +## getMountedApps + +```js +const mountedAppNames = singleSpa.getMountedApps(); +console.log(mountedAppNames); // ['app1', 'app2', 'navbar'] +``` + +

arguments

+ +none + +

returns

+ +
+
appNames: string[]
+
Each string is the name of a registered application that is currently MOUNTED.
+
+ +## getAppNames + +```js +const appNames = singleSpa.getAppNames(); +console.log(appNames); // ['app1', 'app2', 'app3', 'navbar'] +``` + +

arguments

+ +none + +

returns

+ +
+
appNames: string[]
+
Each string is the name of a registered application regardless of app status.
+
+ +## getAppStatus + +```js +const status = singleSpa.getAppStatus('app1'); +console.log(status); // one of many statuses (see list below). e.g. MOUNTED +``` + +

arguments

+ +
+
appName: string
+
Registered application name.
+
+ +

returns

+ +
+
appStatus: <string | null>
+
+ Will be one of the following string constants, or null if the app does not exist. +
+
+
NOT_LOADED
+
app has been registered with single-spa but has not yet been loaded.
+
+
+
LOADING_SOURCE_CODE
+
app's source code is being fetched.
+
+
+
NOT_BOOTSTRAPPED
+
app has been loaded but not bootstrapped.
+
+
+
BOOTSTRAPPING
+
the bootstrap lifecycle function has been called but has not finished.
+
+
+
NOT_MOUNTED
+
app has been loaded and bootstrapped but not yet mounted.
+
+
+
MOUNTING
+
app is being mounted but not finished.
+
+
+
MOUNTED
+
app is currently active and is mounted to the DOM.
+
+
+
UNMOUNTING
+
app is currently being unmounted but has not finished.
+
+
+
UNLOADING
+
app is currently being unloaded but has not finished.
+
+
+
SKIP_BECAUSE_BROKEN
+
app threw an error during load, bootstrap, mount, or unmount and has been siloed because it is misbehaving and has been skipped. Other apps will continue on normally.
+
+
+
LOAD_ERROR
+
+ The app's loading function returned a promise that rejected. This is often due to a network error attempting to download the JavaScript bundle for the application. Single-spa will retry loading the application after the user navigates away from the current route and then comes back to it. +
+
+
+
+
+ +### Handling LOAD_ERROR status to retry module + +If a module fails to load (for example, due to network error), single-spa will handle the error but SystemJS will not automatically retry to download the module later. To do so, add a single-spa errorHandler that deletes the module from the SystemJS registry and re-attempt to download the module when `System.import()` on an application in `LOAD_ERROR` status is called again. + +```js +singleSpa.addErrorHandler(err => { + if (singleSpa.getAppStatus(err.appOrParcelName) === singleSpa.LOAD_ERROR) { + System.delete(System.resolve(err.appOrParcelName)); + } +}); +``` + +## unloadApplication + +```js +// Unload the application right now, without waiting for it to naturally unmount. +singleSpa.unloadApplication('app1'); + +// Unload the application only after it naturally unmounts due to a route change. +singleSpa.unloadApplication('app1', { waitForUnmount: true }); +``` + +The purpose of unloading a registered application is to set it back to a NOT_LOADED status, which means that it will be re-bootstrapped the next time it needs to mount. The main use-case for this was to allow for the hot-reloading of entire registered applications, but `unloadApplication` can be useful whenever you want to re-bootstrap your application. + +Single-spa performs the following steps when unloadApplication is called. + +1. Call the [unload lifecyle](api.md#unload) on the registered application that is being unloaded. +2. Set the app status to NOT_LOADED +3. Trigger a reroute, during which single-spa will potentially mount the application that was just unloaded. + +Because a registered application might be mounted when `unloadApplication` is called, you can specify whether you want to immediately unload or if you want to wait until the application is no longer mounted. This is done with the `waitForUnmount` option. + +

arguments

+ +
+
appName: string
+
Registered application name.
+
options?: {waitForUnmount: boolean = false}
+
The options must be an object that has a waitForUnmount property. When waitForUnmount is false, single-spa immediately unloads the specified registered application even if the app is currently mounted. If it is true, single-spa will unload the registered application as soon as it is safe to do so (when the app status is not MOUNTED).
+
+ +

returns

+ +
+
Promise
+
This promise will be resolved when the registered application has been successfully unloaded.
+
+ +## unregisterApplication + +```js +import { unregisterApplication } from 'single-spa'; + +unregisterApplication('app1').then(() => { + console.log('app1 is now unmounted, unloaded, and no longer registered!'); +}); +``` + +The `unregisterApplication` function will unmount, unload, and unregister an application. Once it is no longer registered, the application will never again be mounted. + +This api was introduced in single-spa@5.8.0. A few notes about this api: + +- Unregistering an application does not delete it from the SystemJS module registry. +- Unregistering an application does not delete its code or javascript frameworks from browser memory. +- An alternative to unregistering applications is to perform permission checks inside of the application's activity function. This has a similar effect of preventing the application from ever mounting. + +

arguments

+ +
+
appName: string
+
+ +

returns

+ +
+
Promise
+
This promise will be resolved when the application has been successfully unregistered.
+
+ +## checkActivityFunctions + +```js +const appsThatShouldBeActive = singleSpa.checkActivityFunctions(); +console.log(appsThatShouldBeActive); // ['app1'] + +const appsForACertainRoute = singleSpa.checkActivityFunctions(new URL('/app2', window.location.href)); +console.log(appsForACertainRoute); // ['app2'] +``` + +Will call every app's activity function with `url` and give you list of which applications should be mounted with that location. + +

arguments

+ +
+
location: Location
+
A Location object that will be used instead of window.location when calling every application's activity function to test if they return true.
+
+ +

returns

+ +
+
appNames: string[]
+
Each string is the name of a registered application that matches the provided url.
+
+ +## addErrorHandler + +```js +singleSpa.addErrorHandler(err => { + console.log(err); + console.log(err.appOrParcelName); + console.log(singleSpa.getAppStatus(err.appOrParcelName)); +}); +``` + +Adds a handler that will be called every time an application throws an error during a lifecycle function or activity function. When there are no error handlers, single-spa throws the error to the window. + +
+
errorHandler: Function(error: Error)
+
Must be a function. Will be called with an Error object that additionally has a message and appOrParcelName property.
+
+ +

returns

+ +`undefined` + +## removeErrorHandler + +```js +singleSpa.addErrorHandler(handleErr); +singleSpa.removeErrorHandler(handleErr); + +function handleErr(err) { + console.log(err); +} +``` + +Removes the given error handler function. + +

arguments

+ +
+
errorHandler: Function
+
Reference to the error handling function.
+
+ +

returns

+ +
+
boolean
+
true if the error handler was removed, and false if it was not.
+
+ +## mountRootParcel + +```js +// Synchronous mounting +const parcel = singleSpa.mountRootParcel(parcelConfig, { + prop1: 'value1', + domElement: document.getElementById('a-div'), +}); +parcel.mountPromise.then(() => { + console.log('finished mounting the parcel!'); +}); + +// Asynchronous mounting. Feel free to use webpack code splits or SystemJS dynamic loading +const parcel2 = singleSpa.mountRootParcel(() => import('./some-parcel.js'), { + prop1: 'value1', + domElement: document.getElementById('a-div'), +}); +``` + +Will create and mount a [single-spa parcel](parcels-overview.md). + +:::caution Parcels do not automatically unmount +Unmounting will need to be triggered manually. +::: + +

arguments

+ +
+
parcelConfig: Object or Loading Function
+
[parcelConfig](parcels-api.md#parcel-configuration)
+
parcelProps: Object with a domElement property
+
[parcelProps](parcels-api.md#parcel-props)
+
+ +

returns

+ +
+
Parcel object
+
See Parcels API for more detail.
+
+ +## pathToActiveWhen + +The `pathToActiveWhen` function converts a string URL path into an [activity function](/docs/configuration/#activity-function). The string path may contain route parameters that single-spa will match any characters to. By default, pathToActiveWhen assumes that the string provided is a **prefix**; however, this can be altered with the `exactMatch` parameter. + +This function is used by single-spa when a string is passed into `registerApplication` as the `activeWhen` argument. + +**_Arguments_** + +1. `path` (string): The URL prefix that. +2. `exactMatch` (boolean, optional, defaults to `false`, requires single-spa@>=5.9.0): A boolean that controls whether trailing characters after the path should be allowed. When `false`, trailing characters are allowed. When `true`, trailing characters are not allowed. + +**_Return Value_** + +`(url: URL) => boolean` + +A function that accepts a `URL` object as an argument and returns a boolean indicating whether the path matches that URL. + +**_Examples:_** + +```js +let activeWhen = singleSpa.pathToActiveWhen('/settings'); +activeWhen(new URL('http://localhost/settings')); // true +activeWhen(new URL('http://localhost/settings/password')); // true +activeWhen(new URL('http://localhost/')); // false + +activeWhen = singleSpa.pathToActiveWhen('/users/:id/settings'); +activeWhen(new URL('http://localhost/users/6f7dsdf8g9df8g9dfg/settings')); // true +activeWhen(new URL('http://localhost/users/1324/settings')); // true +activeWhen(new URL('http://localhost/users/1324/settings/password')); // true +activeWhen(new URL('http://localhost/users/1/settings')); // true +activeWhen(new URL('http://localhost/users/1')); // false +activeWhen(new URL('http://localhost/settings')); // false +activeWhen(new URL('http://localhost/')); // false + +activeWhen = singleSpa.pathToActiveWhen('/page#/hash'); +activeWhen(new URL('http://localhost/page#/hash')); // true +activeWhen(new URL('http://localhost/#/hash')); // false +activeWhen(new URL('http://localhost/page')); // false +``` + +## ensureJQuerySupport + +```js +singleSpa.ensureJQuerySupport(jQuery); +``` + +jQuery uses [event delegation](https://learn.jquery.com/events/event-delegation/) so single-spa must monkey-patch each version of jQuery on the page. single-spa will attempt to do this automatically by looking for `window.jQuery` or `window.$`. Use this explicit method if multiple versions are included on your page or if jQuery is bound to a different global variable. + +

arguments

+ +
+
jQuery?: JQueryFn = window.jQuery
+
A reference to the global variable that jQuery has been bound to.
+
+ +

returns

+ +`undefined` + +## setBootstrapMaxTime + +```js +// After three seconds, show a console warning while continuing to wait. +singleSpa.setBootstrapMaxTime(3000); + +// After three seconds, move the application to SKIP_BECAUSE_BROKEN status. +singleSpa.setBootstrapMaxTime(3000, true); + +// don't do a console warning for slow lifecycles until 10 seconds have elapsed +singleSpa.setBootstrapMaxTime(3000, true, 10000); +``` + +Sets the global configuration for bootstrap timeouts. + +

arguments

+ +
+
millis: number
+
Number of milliseconds to wait for bootstrap to complete before timing out.
+
dieOnTimeout: boolean = false
+
+

If false, registered applications that are slowing things down will cause nothing more than some warnings in the console up until millis is reached.

+

If true, registered applications that are slowing things down will be siloed into a SKIP_BECAUSE_BROKEN status where they will never again be given the chance to break everything.

+

Each registered application can override this behavior for itself.

+
+
warningMillis: number = 1000
+
Number of milliseconds to wait between console warnings that occur before the final timeout.
+
+ +

returns

+ +`undefined` + +## setMountMaxTime + +```js +// After three seconds, show a console warning while continuing to wait. +singleSpa.setMountMaxTime(3000); + +// After three seconds, move the application to SKIP_BECAUSE_BROKEN status. +singleSpa.setMountMaxTime(3000, true); + +// don't do a console warning for slow lifecycles until 10 seconds have elapsed +singleSpa.setMountMaxTime(3000, true, 10000); +``` + +Sets the global configuration for mount timeouts. + +

arguments

+ +
+
millis: number
+
Number of milliseconds to wait for mount to complete before timing out.
+
dieOnTimeout: boolean = false
+
+

If false, registered applications that are slowing things down will cause nothing more than some warnings in the console up until millis is reached.

+

If true, registered applications that are slowing things down will be siloed into a SKIP_BECAUSE_BROKEN status where they will never again be given the chance to break everything.

+

Each registered application can override this behavior for itself.

+
+
warningMillis: number = 1000
+
Number of milliseconds to wait between console warnings that occur before the final timeout.
+
+ +

returns

+ +`undefined` + +## setUnmountMaxTime + +```js +// After three seconds, show a console warning while continuing to wait. +singleSpa.setUnmountMaxTime(3000); + +// After three seconds, move the application to SKIP_BECAUSE_BROKEN status. +singleSpa.setUnmountMaxTime(3000, true); + +// don't do a console warning for slow lifecycles until 10 seconds have elapsed +singleSpa.setUnmountMaxTime(3000, true, 10000); +``` + +Sets the global configuration for unmount timeouts. + +

arguments

+ +
+
millis: number
+
Number of milliseconds to wait for unmount to complete before timing out.
+
dieOnTimeout: boolean = false
+
+

If false, registered applications that are slowing things down will cause nothing more than some warnings in the console up until millis is reached.

+

If true, registered applications that are slowing things down will be siloed into a SKIP_BECAUSE_BROKEN status where they will never again be given the chance to break everything.

+

Each registered application can override this behavior for itself.

+
+
warningMillis: number = 1000
+
Number of milliseconds to wait between console warnings that occur before the final timeout.
+
+ +

returns

+ +`undefined` + +--- + +## setUnloadMaxTime + +```js +// After three seconds, show a console warning while continuing to wait. +singleSpa.setUnloadMaxTime(3000); + +// After three seconds, move the application to SKIP_BECAUSE_BROKEN status. +singleSpa.setUnloadMaxTime(3000, true); + +// don't do a console warning for slow lifecycles until 10 seconds have elapsed +singleSpa.setUnloadMaxTime(3000, true, 10000); +``` + +Sets the global configuration for unload timeouts. + +

arguments

+ +
+
millis: number
+
Number of milliseconds to wait for unload to complete before timing out.
+
dieOnTimeout: boolean = false
+
+

If false, registered applications that are slowing things down will cause nothing more than some warnings in the console up until millis is reached.

+

If true, registered applications that are slowing things down will be siloed into a SKIP_BECAUSE_BROKEN status where they will never again be given the chance to break everything.

+

Each registered application can override this behavior for itself.

+
+
warningMillis: number = 1000
+
Number of milliseconds to wait between console warnings that occur before the final timeout.
+
+ +

returns

+ +`undefined` + +## Events + +single-spa fires Events to the `window` as a way for your code to hook into URL transitions. + +### PopStateEvent + +single-spa fires [PopStateEvent](https://developer.mozilla.org/en-US/docs/Web/API/PopStateEvent) events when it wants to instruct all active applications to re-render. This occurs when one application calls [history.pushState](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState), [history.replaceState](https://developer.mozilla.org/en-US/docs/Web/API/History/replaceState), or [triggerAppChange](#triggerAppChange). Single-spa deviates from the browser's default behavior in some cases, as described in [this Github issue](https://github.com/single-spa/single-spa/issues/484#issuecomment-601279869). + +```js +window.addEventListener('popstate', evt => { + if (evt.singleSpa) { + console.log( + 'This event was fired by single-spa to forcibly trigger a re-render', + ); + console.log(evt.singleSpaTrigger); // pushState | replaceState + } else { + console.log('This event was fired by native browser behavior'); + } +}); +``` + +### Canceling navigation + +Canceling navigation refers to the URL changing and then immediately changing back to what it was before. This is done before any mounting, unmounting, or loading that would otherwise take place. This can be used in conjunction with Vue router and Angular router's built-in navigation guards that allow for cancelation of a navigation event. + +To cancel a navigation event, listen to the `single-spa:before-routing-event` event: + +```js +window.addEventListener( + 'single-spa:before-routing-event', + ({ detail: { oldUrl, newUrl, cancelNavigation } }) => { + if ( + new URL(oldUrl).pathname === '/route1' && + new URL(newUrl).pathname === '/route2' + ) { + cancelNavigation(); + } + }, +); +``` + +When a navigation is canceled, no applications will be mounted, unmounted, loaded, or unloaded. All single-spa routing events will fire for the canceled navigation, but they will each have the `navigationIsCanceled` property set to `true` on the `event.detail` (Details below in Custom Events section). + +Navigation cancelation is sometimes used as a mechanism for preventing users from accessing routes for which they are unauthorized. However, we generally recommend permission checks on each route as the proper way to guard routes, instead of navigation cancelation. + +### Custom Events + +single-spa fires a series of [custom events](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent) whenever it reroutes. A reroute occurs whenever the browser URL changes in any way or a `triggerAppChange` is called. The custom events are fired to the `window`. Each custom event has a [`detail` property](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail) with the following properties: + +```js +window.addEventListener('single-spa:before-routing-event', evt => { + const { + originalEvent, + newAppStatuses, + appsByNewStatus, + totalAppChanges, + oldUrl, + newUrl, + navigationIsCanceled, + cancelNavigation, + } = evt.detail; + console.log( + 'original event that triggered this single-spa event', + originalEvent, + ); // PopStateEvent | HashChangeEvent | undefined + console.log( + 'the new status for all applications after the reroute finishes', + newAppStatuses, + ); // { app1: MOUNTED, app2: NOT_MOUNTED } + console.log( + 'the applications that changed, grouped by their status', + appsByNewStatus, + ); // { MOUNTED: ['app1'], NOT_MOUNTED: ['app2'] } + console.log( + 'number of applications that changed status so far during this reroute', + totalAppChanges, + ); // 2 + console.log('the URL before the navigationEvent', oldUrl); // http://localhost:8080/old-route + console.log('the URL after the navigationEvent', newUrl); // http://localhost:8080/new-route + console.log('has the navigation been canceled', navigationIsCanceled); // false + + // The cancelNavigation function is only defined in the before-routing-event + evt.detail.cancelNavigation(); +}); +``` + +The following table shows the order in which the custom events are fired during a reroute: + +| Event order | Event Name | Condition for firing | +| ----------- | ------------------------------------------------------------------- | --------------------------------------------------- | +| 1 | `single-spa:before-app-change` or `single-spa:before-no-app-change` | Will any applications change status? | +| 2 | `single-spa:before-routing-event` | — | +| 3 | `single-spa:before-mount-routing-event` | — | +| 4 | `single-spa:before-first-mount` | Is this the first time any application is mounting? | +| 5 | `single-spa:first-mount` | Is this the first time any application was mounted? | +| 6 | `single-spa:app-change` or `single-spa:no-app-change` | Did any applications change status? | +| 7 | `single-spa:routing-event` | — | + +### before-app-change event + +```js +window.addEventListener('single-spa:before-app-change', evt => { + console.log('single-spa is about to mount/unmount applications!'); + console.log(evt.detail.originalEvent); // PopStateEvent + console.log(evt.detail.newAppStatuses); // { app1: MOUNTED } + console.log(evt.detail.appsByNewStatus); // { MOUNTED: ['app1'], NOT_MOUNTED: [] } + console.log(evt.detail.totalAppChanges); // 1 +}); +``` + +A `single-spa:before-app-change` event is fired before reroutes that will result in at least one application changing status. + +### before-no-app-change + +```js +window.addEventListener('single-spa:before-no-app-change', evt => { + console.log('single-spa is about to do a no-op reroute'); + console.log(evt.detail.originalEvent); // PopStateEvent + console.log(evt.detail.newAppStatuses); // { } + console.log(evt.detail.appsByNewStatus); // { MOUNTED: [], NOT_MOUNTED: [] } + console.log(evt.detail.totalAppChanges); // 0 +}); +``` + +A `single-spa:before-no-app-change` event is fired before reroutes that will result in zero applications changing status. + +### before-routing-event + +```js +window.addEventListener('single-spa:before-routing-event', evt => { + console.log('single-spa is about to mount/unmount applications!'); + console.log(evt.detail.originalEvent); // PopStateEvent + console.log(evt.detail.newAppStatuses); // { } + console.log(evt.detail.appsByNewStatus); // { MOUNTED: [], NOT_MOUNTED: [] } + console.log(evt.detail.totalAppChanges); // 0 +}); +``` + +A `single-spa:before-routing-event` event is fired before every routing event occurs, which is after each hashchange, popstate, or triggerAppChange, even if no changes to registered applications were necessary. + +### before-mount-routing-event + +```js +window.addEventListener('single-spa:before-mount-routing-event', evt => { + console.log('single-spa is about to mount/unmount applications!'); + console.log(evt.detail); + console.log(evt.detail.originalEvent); // PopStateEvent + console.log(evt.detail.newAppStatuses); // { app1: MOUNTED } + console.log(evt.detail.appsByNewStatus); // { MOUNTED: ['app1'], NOT_MOUNTED: [] } + console.log(evt.detail.totalAppChanges); // 1 +}); +``` + +A `single-spa:before-mount-routing-event` event is fired after `before-routing-event` and before `routing-event`. It is guaranteed to fire after all single-spa applications have been unmounted, but before any new applications have been mounted. + +### before-first-mount + +```js +window.addEventListener('single-spa:before-first-mount', () => { + console.log( + 'single-spa is about to mount the very first application for the first time', + ); +}); +``` + +Before the first of any single-spa applications is mounted, single-spa fires a `single-spa:before-first-mount` event; therefore it will only be fired once ever. More specifically, it fires after the application is already loaded but before mounting. + +:::tip Suggested use case +remove a loader bar that the user is seeing right before the first app will be mounted. +::: + +### first-mount + +```js +window.addEventListener('single-spa:first-mount', () => { + console.log('single-spa just mounted the very first application'); +}); +``` + +After the first of any single-spa applications is mounted, single-spa fires a `single-spa:first-mount` event; therefore it will only be fired once ever. + +:::tip Suggested use case +log the time it took before the user sees any of the apps mounted. +::: + +### app-change event + +```js +window.addEventListener('single-spa:app-change', evt => { + console.log( + 'A routing event occurred where at least one application was mounted/unmounted', + ); + console.log(evt.detail.originalEvent); // PopStateEvent + console.log(evt.detail.newAppStatuses); // { app1: MOUNTED, app2: NOT_MOUNTED } + console.log(evt.detail.appsByNewStatus); // { MOUNTED: ['app1'], NOT_MOUNTED: ['app2'] } + console.log(evt.detail.totalAppChanges); // 2 +}); +``` + +A `single-spa:app-change` event is fired every time that one or more apps were loaded, bootstrapped, mounted, unmounted, or unloaded. It is similar to `single-spa:routing-event` except that it will not fire unless one or more apps were actually loaded, bootstrapped, mounted, or unmounted. A hashchange, popstate, or triggerAppChange that does not result in one of those changes will not cause the event to be fired. + +### no-app-change event + +```js +window.addEventListener('single-spa:no-app-change', evt => { + console.log( + 'A routing event occurred where zero applications were mounted/unmounted', + ); + console.log(evt.detail.originalEvent); // PopStateEvent + console.log(evt.detail.newAppStatuses); // { } + console.log(evt.detail.appsByNewStatus); // { MOUNTED: [], NOT_MOUNTED: [] } + console.log(evt.detail.totalAppChanges); // 0 +}); +``` + +When no applications were loaded, bootstrapped, mounted, unmounted, or unloaded, single-spa fires a `single-spa:no-app-change` event. This is the inverse of the `single-spa:app-change` event. Only one will be fired for each routing event. + +### routing-event + +```js +window.addEventListener('single-spa:routing-event', evt => { + console.log('single-spa finished mounting/unmounting applications!'); + console.log(evt.detail.originalEvent); // PopStateEvent + console.log(evt.detail.newAppStatuses); // { app1: MOUNTED, app2: NOT_MOUNTED } + console.log(evt.detail.appsByNewStatus); // { MOUNTED: ['app1'], NOT_MOUNTED: ['app2'] } + console.log(evt.detail.totalAppChanges); // 2 +}); +``` + +A `single-spa:routing-event` event is fired every time that a routing event has occurred, which is after each hashchange, popstate, or triggerAppChange, even if no changes to registered applications were necessary; and after single-spa verified that all apps were correctly loaded, bootstrapped, mounted, and unmounted. diff --git a/versioned_docs/version-6.x/building-applications.md b/versioned_docs/version-6.x/building-applications.md new file mode 100644 index 000000000..25e3be833 --- /dev/null +++ b/versioned_docs/version-6.x/building-applications.md @@ -0,0 +1,207 @@ +--- +id: building-applications +title: Building single-spa applications +sidebar_label: single-spa applications +--- + +A single-spa registered application is everything that a normal SPA is, except that it doesn't have an HTML page. In a single-spa world, your SPA contains many registered applications, where each has its own framework. Registered applications have their own client-side routing and their own frameworks/libraries. They render their own HTML and have full freedom to do whatever they want, whenever they are _mounted_. The concept of being _mounted_ refers to whether a registered application is putting content on the DOM or not. What determines if a registered application is mounted is its [activity function](/docs/configuration/#activity-function). Whenever a registered application is _not mounted_, it should remain completely dormant until mounted. + +## Creating a registered application + +To create a registered application, first [register the application with single-spa](/docs/configuration/#registering-applications). Once registered, the registered application must correctly implement **all** of the following lifecycle functions inside of its main entry point. + +## Registered application lifecycle + +During the course of a single-spa page, registered applications are loaded, bootstrapped (initialized), mounted, unmounted, and unloaded. single-spa provides hooks into each phase via `lifecycles`. + +A lifecycle function is a function or array of functions that single-spa will call on a registered application. single-spa calls these by finding specific named exports from the registered application's main file. + +Notes: + +- Implementing `bootstrap`, `mount`, and `unmount` is required. But implementing `unload` is optional. +- Each lifecycle function must either return a `Promise` or be an `async function`. +- If an array of functions is exported (instead of just one function), the functions will be called + one-after-the-other, waiting for the resolution of one function's promise before calling the next. +- If single-spa is [not started](api.md#start), applications will be loaded, + but will not be bootstrapped, mounted or unmounted. + +:::info +Framework-specific helper libraries exist in the [single-spa ecosystem](ecosystem.md) to implement these required lifecycle methods. This documentation is helpful for understanding what those helpers are doing, or for implementing your own. +::: + +## Lifecycle props + +Lifecycle functions are called with a `props` argument, which is an object with some guaranteed information and additional custom information. + +```js +function bootstrap(props) { + const { + name, // The name of the application + singleSpa, // The singleSpa instance + mountParcel, // Function for manually mounting + customProps, // Additional custom information + } = props; // Props are given to every lifecycle + return Promise.resolve(); +} +``` + +#### Built-in props + +Each lifecycle function is guaranteed to be called with the following props: + +- `name`: The string name that was registered to single-spa. +- `singleSpa`: A reference to the singleSpa instance, itself. This is intended to allow applications and helper libraries to call singleSpa + APIs without having to import it. This is useful in situations where there are multiple webpack configs that are not set up to ensure + that only one instance of singleSpa is loaded. +- `mountParcel`: The [mountParcel function](/docs/parcels-api#mountparcel). + +#### Custom props + +In addition to the built-in props that are provided by single-spa, you may optionally specify custom props to be passed to an application. These _customProps_ will be passed into each lifecycle method. The custom props are an object, and you can provide either the object or a function that returns the object. Custom prop functions are called with the application name and current window.location as arguments. + +```js title="root-config.js" +singleSpa.registerApplication({ + name: 'app1', + activeWhen, + app, + customProps: { authToken: 'd83jD63UdZ6RS6f70D0' }, +}); + +singleSpa.registerApplication({ + name: 'app1', + activeWhen, + app, + customProps: (name, location) => { + return { authToken: 'd83jD63UdZ6RS6f70D0' }; + }, +}); +``` + +```js title="app1.js" +export function mount(props) { + // do something with the common authToken in app1 + console.log(props.authToken); + return reactLifecycles.mount(props); +} +``` + +Some use cases could be to: + +- share a common access token with all child apps +- pass down some initialization information, like the rendering target +- pass a reference to a common event bus so each app may talk to each other + +Note that when no _customProps_ are provided during registration, `props.customProps` defaults to an empty object. + +### Lifecycle helpers + +Some helper libraries that implement lifecycle functions for ease of use are available for many popular frameworks/libraries. Learn more on the [Ecosystem page](ecosystem.md). + +### Load + +When registered applications are being lazily loaded, this refers to when the code for a registered application is fetched from the server and executed. This will happen once the registered application's [activity function](/docs/configuration/#activity-function) returns a truthy value for the first time. It is best practice to do as little as possible / nothing at all during `load`, but instead to wait until the bootstrap lifecycle function to do anything. If you need to do something during `load`, simply put the code into a registered application's main entry point, but not inside of an exported function. For example: + +```js +console.log("The registered application has been loaded!"); + +export async function bootstrap(props) {...} +export async function mount(props) {...} +export async function unmount(props) {...} +``` + +### Bootstrap + +This lifecycle function will be called once, right before the registered application is mounted for the first time. + +```js +export function bootstrap(props) { + return Promise.resolve().then(() => { + // One-time initialization code goes here + console.log('bootstrapped!'); + }); +} +``` + +### Mount + +This lifecycle function will be called whenever the registered application is not mounted, but its [activity function](/docs/configuration/#activity-function) returns a truthy value. When called, this function should look at the URL to determine the active route and then create DOM elements, DOM event listeners, etc. to render content to the user. Any subsequent routing events (such as `hashchange` and `popstate`) will **not** trigger more calls to `mount`, but instead should be handled by the application itself. + +```js +export function mount(props) { + return Promise.resolve().then(() => { + // Do framework UI rendering here + console.log('mounted!'); + }); +} +``` + +### Unmount + +This lifecycle function will be called whenever the registered application is mounted, but its [activity function](/docs/configuration/#activity-function) returns a falsy value. When called, this function should clean up all DOM elements, DOM event listeners, leaked memory, globals, observable subscriptions, etc. that were created at any point when the registered application was mounted. + +```js +export function unmount(props) { + return Promise.resolve().then(() => { + // Do framework UI unrendering here + console.log('unmounted!'); + }); +} +``` + +### Unload + +The `unload` lifecycle is an optionally implemented lifecycle function. It will be called whenever an application should be `unloaded`. This will not ever happen unless someone calls the [`unloadApplication`](api.md#unloadapplication) API. If a registered application does not implement the unload lifecycle, then it assumed that unloading the app is a no-op. + +The purpose of the `unload` lifecycle is to perform logic right before a single-spa application is unloaded. Once the application is unloaded, the application status will be NOT_LOADED and the application will be re-bootstrapped. + +The motivation for `unload` was to implement the hot-loading of entire registered applications, but it is useful in other scenarios as well when you want to re-bootstrap applications, but perform some logic before applications are re-bootstrapped. + +```js +export function unload(props) { + return Promise.resolve().then(() => { + // Hot-reloading implementation goes here + console.log('unloaded!'); + }); +} +``` + +## Timeouts + +By default, registered applications obey the [global timeout configuration](/docs/api#setbootstrapmaxtime), but can override that behavior for their specific application. This is done by exporting a `timeouts` object from the main entry point of the registered application. Example: + +```js title="app-1.js" +export function bootstrap(props) {...} +export function mount(props) {...} +export function unmount(props) {...} + +export const timeouts = { + bootstrap: { + millis: 5000, + dieOnTimeout: true, + warningMillis: 2500, + }, + mount: { + millis: 5000, + dieOnTimeout: false, + warningMillis: 2500, + }, + unmount: { + millis: 5000, + dieOnTimeout: true, + warningMillis: 2500, + }, + unload: { + millis: 5000, + dieOnTimeout: true, + warningMillis: 2500, + }, +}; +``` + +Note that `millis` refers to the number of milliseconds for the final console warning, and `warningMillis` refers to the number of milliseconds at which a warning will be printed to the console (on an interval) leading up to the final console warning. + +## Transitioning between applications + +If you find yourself wanting to add transitions as applications are mounted and unmounted, then you'll probably want to tie into the `bootstrap`, `mount`, and `unmount` lifecycle methods. This [single-spa transitions](https://github.com/frehner/singlespa-transitions) repo is a small proof-of-concept of how you can tie into these lifecycle methods to add transitions as your apps mount and unmount. + +Transitions for pages within a mounted application can be handled entirely by the application itself. For example, using [react-transition-group](https://github.com/reactjs/react-transition-group) for React-based projects. diff --git a/versioned_docs/version-6.x/contributing-overview.md b/versioned_docs/version-6.x/contributing-overview.md new file mode 100644 index 000000000..4a3915780 --- /dev/null +++ b/versioned_docs/version-6.x/contributing-overview.md @@ -0,0 +1,87 @@ +--- +id: contributing-overview +title: Contributing to Single-spa +sidebar_label: Overview +--- + +[List of current contributors](/contributors) + +Thanks for checking out single-spa! We're excited to hear and learn from you. + +We've put together the following guidelines to help you figure out where you can best be helpful. + +## Table of Contents + +1. [Types of contributions we're looking for](#types-of-contributions-were-looking-for) +1. [Ground rules & expectations](#ground-rules-expectations) +1. [How to contribute](#how-to-contribute) +1. [Setting up your environment](#setting-up-your-environment) +1. [Community](#community) + +## Types of contributions we're looking for + +There are many ways you can directly contribute to the guides (in descending order of need): + +- Examples +- Helper Libraries (like single-spa-react) for missing frameworks +- Bug fixes +- Answering questions in the slack channel +- new helper packages for frameworks + +Interested in making a contribution? Read on! + +## Ground rules & expectations + +Before we get started, here are a few things we expect from you (and that you should expect from others): + +- Be kind and thoughtful in your conversations around this project. We all come from different backgrounds and projects, which means we likely have different perspectives on "how open source is done." Try to listen to others rather than convince them that your way is correct. +- Please read the single-spa [Contributor Code of Conduct](/docs/code-of-conduct/). By participating in this project, you agree to abide by its terms. +- If you open a pull request, please ensure that your contribution passes all tests. If there are test failures, you will need to address them before we can merge your contribution. +- When adding content, please consider if it is widely valuable. Please don't add references or links to things you or your employer have created as others will do so if they appreciate it. + +## How to contribute + +If you'd like to contribute, start by searching through the [issues](https://github.com/single-spa/single-spa/issues) and [pull requests](https://github.com/single-spa/single-spa/pulls) to see whether someone else has raised a similar idea or question. + +If you don't see your idea listed, and you think it fits into the goals of this guide, do one of the following: + +- **If your contribution is minor,** such as a small typo or bug fix, open a pull request. +- **If your contribution is major,** such as a new feature, start by opening an issue first. That way, other people can weigh in on the discussion before you do any work. + +## Setting up your environment + +### Prerequisites + +1. Git +1. Node: install version 8.4 or greater +1. Yarn: See [Yarn website for installation instructions](https://yarnpkg.com/lang/en/docs/install/) +1. A fork of the [single-spa repo](https://github.com/single-spa/single-spa) +1. A clone of the repo on your local machine + +### Installation + +1. `cd single-spa` to go into the project root +1. `yarn` to install single-spa's dependencies + +### Create a branch + +1. `git checkout master` from any folder in your local `single-spa` repository +1. `git pull origin master` to ensure you have the latest main code +1. `git checkout -b the-name-of-my-branch` (replacing `the-name-of-my-branch` with a suitable name) to create a branch + +### Test the change + +1. Run `yarn test` from the project root. + +### Push it + +1. `git add . && git commit -m "My message"` (replacing `My message` with a commit message, such as `Fixed application lifecycles`) to stage and commit your changes +1. `git push my-fork-name the-name-of-my-branch` +1. Go to the [single-spa repo](https://github.com/single-spa/single-spa) and you should see recently pushed branches. +1. Follow GitHub's instructions to submit a new Pull Request. + +## Community + +Discussions about single-spa take place on the single-spa repository's [Issues](https://github.com/single-spa/single-spa/issues) and [Pull Requests](https://github.com/single-spa/single-spa/pulls) sections. Anybody is welcome to join these conversations. There is also a [Slack workspace](https://join.slack.com/t/single-spa/shared_invite/zt-21skfl7l3-leF7JkoKwKaRIPX~N6jXJQ) for regular updates. + +Wherever possible, do not take these conversations to private channels, including contacting the maintainers directly. Keeping communication public means everybody can benefit and learn from the conversation. diff --git a/versioned_docs/version-6.x/create-single-spa.md b/versioned_docs/version-6.x/create-single-spa.md new file mode 100644 index 000000000..c00e46893 --- /dev/null +++ b/versioned_docs/version-6.x/create-single-spa.md @@ -0,0 +1,212 @@ +--- +id: create-single-spa +title: create-single-spa +sidebar_label: create-single-spa +--- + +Single-spa offers a CLI for those who prefer autogenerated and managed configurations for webpack, babel, jest, etc. You do not have to use the CLI in order to use single-spa. + +The CLI is called `create-single-spa` ([Github link](https://github.com/single-spa/create-single-spa/)). It is primarily intended for the creation of new projects, but may also be useful for migrating existing projects (especially migrating CRA or frameworkless projects). + +## Installation and Usage + +If you wish to have create-single-spa globally available, run the following in a terminal + +```sh +npm install --global create-single-spa + +# or +yarn global add create-single-spa +``` + +Then run the following: + +```sh +create-single-spa +``` + +Alternatively, you may use create-single-spa without global installation: + +```sh +npm init single-spa + +# or +npx create-single-spa + +# or +yarn create single-spa +``` + +This will open up a CLI prompt asking you what kind of project you want to create or update. + +## CLI arguments + +You may pass arguments to create-single-spa like so: + +```sh +# Different ways of doing the same thing +create-single-spa --framework react +npm init single-spa --framework react +npx create-single-spa --framework react +yarn create single-spa --framework react +``` + +Here are the available CLI options: + +### --dir + +You may specify which directory create-single-spa runs in through either of the following ways: + +```sh +# Two ways of doing the same thing +create-single-spa my-dir +create-single-spa --dir my-dir +``` + +### --moduleType + +You can specify which kind of microfrontend you are creating with the `--moduleType` CLI argument: + +```sh +create-single-spa --moduleType root-config +create-single-spa --moduleType app-parcel +create-single-spa --moduleType util-module +``` + +### --framework + +You can specify which framework you're using with the `--framework` CLI argument. Note that if you specify a framework that you may omit the `--moduleType`, as it is inferred to be `app-parcel`. + +```sh +create-single-spa --framework react +create-single-spa --framework vue +create-single-spa --framework angular +``` + +### --layout + +When generating a root config, the `--layout` CLI argument indicates that you want to use [single-spa-layout](/docs/layout-overview) in your root config. + +### --skipInstall + +This option skips npm/yarn/pnpm installation during project creation. + +## Project types + +create-single-spa asks you if you'd like to create a single-spa application, a utility module, or a root-config. All three module types assume that you are using the [recommended setup](/docs/recommended-setup). + +If you select that you'd like to create a single-spa application, you will be prompted for which framework you'd like to choose. React is implemented with premade configurations for babel + webpack + jest. Angular is implemented with Angular CLI and [single-spa-angular](/docs/ecosystem-angular). Vue is implemented with Vue CLI and [vue-cli-plugin-single-spa](/docs/ecosystem-vue#vue-cli). + +# NPM packages + +Within the create-single-spa repo, there are several NPM packages. The following sections document each package: + +## create-single-spa + +[Github project](https://github.com/single-spa/create-single-spa/tree/master/packages/create-single-spa) + +The core CLI, which invokes [generator-single-spa](#generator-single-spa). + +## generator-single-spa + +[Github project](https://github.com/single-spa/create-single-spa/tree/master/packages/generator-single-spa) + +A [Yeoman generator](https://yeoman.io/) that prompts the user and then creates files. This is primarily invoked via the create-single-spa CLI, but can also be [composed](https://yeoman.io/authoring/composability.html) if you'd like to customize it. + +## single-spa-web-server-utils + +The `single-spa-web-server-utils` package is a collection of functions that help when implementing a web server for an index.html file. This package can be used to inline import maps into an HTML, which helps with the performance of your application. Additionally, it can be used to modify a browser import map so that it's suitable for usage in NodeJS for dynamic module loading and server rendering ([Dynamic Module Loading](/docs/ssr-overview#a-module-loading) and [Server Rendering](/docs/ssr-overview#intro-to-ssr))). + +The web server utils poll the import map from a URL and generate a `browserImportMap` and `nodeImportMap` from the response. + +### Installation + +```sh +npm install --save single-spa-web-server-utils + +# alternatively +yarn add single-spa-web-server-utils +``` + +### getImportMaps + +The `getImportMaps` function accepts an object parameter and returns a promise that resolves with an object with two import maps: `browserImportMap` and `nodeImportMap`. Note that import maps are polled at the specified interval forever until either `reset()` or `clearAllIntervals()` is called. Import Maps are stored in memory in a javascript variable that exists outside of the `getImportMaps` function, so subsequent calls to `getImportMaps` will all use the same cache. + +```js +const { getImportMaps } = require('single-spa-web-server-utils'); +const http = require('http'); +const ejs = require('ejs'); +const fs = require('fs'); +const path = require('path'); + +const htmlTemplate = ejs.compile( + fs.readFileSync(path.resolve(process.cwd(), 'views/index.html'), 'utf-8'), +); + +http.createServer((req, res) => { + getImportMaps({ + // required + // The URL at which the server + url: 'https://my-cdn.com/live.importmap', + + // optional - defaults to 30000 + // The ms to wait when polling the import map + pollInterval: 30000, + + // optional - defaults to false + // Whether to allow for import-map-overrides via cookies sent in the request. + // More details about overrides via cookies at + // https://github.com/joeldenning/import-map-overrides/blob/master/docs/api.md#node + allowOverrides: true, + + // optional - only needed when allowOverrides is true + // The IncomingMessage from an http server. This is used to gather + // cookies for import-map-overrides + req, + + // optional + // This allows you to remove entries from the downloaded import map + // from the returned `nodeImportMap`. This is useful for customizing + // an import map that is used in the browser so that it can be used + // for dynamic NodeJS module loading. Each key is a string import specifier. + // Keys that you return `true` for are preserved in the nodeImportMap. + nodeKeyFilter(key) { + return true; + }, + }).then(({ browserImportMap, nodeImportMap }) => { + console.log(browserImportMap, nodeImportMap); + + // Example of how to inline a browser import map + const htmlWithInlinedImportMap = htmlTemplate({ + importMap: browserImportMap, + }); + res.setResponseHeader('Content-Type', 'text/html'); + res.status(200).send(htmlWithInlinedImportMap); + + // Example of how to apply a NodeJS import map + // More info at https://github.com/node-loader/node-loader-import-maps + global.nodeLoader.setImportMapPromise(Promise.resolve(nodeImportMap)); + import('module-in-import-map'); + }); +}); +``` + +### clearAllIntervals + +This clears all import map polling intervals that were created via `setInterval()` inside of `getImportMaps()`. This is useful for tests and for cleaning up memory. + +```js +import { clearAllIntervals } from 'single-spa-web-server-utils'; + +clearAllIntervals(); +``` + +### reset + +This clears all intervals (see [clearAllIntervals](#clearallintervals)), and also clears the in-memory cache of all import maps. In other words, after `reset()` is called, `getImportMaps()` will always result in a new network request to fetch the import map. + +```js +import { reset } from 'single-spa-web-server-utils'; + +reset(); +``` diff --git a/versioned_docs/version-6.x/devtools.md b/versioned_docs/version-6.x/devtools.md new file mode 100644 index 000000000..b49b91d33 --- /dev/null +++ b/versioned_docs/version-6.x/devtools.md @@ -0,0 +1,72 @@ +--- +id: devtools +title: single-spa-inspector +sidebar_label: Overview +--- + +The single-spa-inspector is a Firefox/Chrome devtools extension to provide utilities for helping with [single-spa](https://single-spa.js.org) applications. [Github project](https://github.com/single-spa/single-spa-inspector). + +Requires >= single-spa@4.1. + +## Installation links + +- [Firefox](https://addons.mozilla.org/en-US/firefox/addon/single-spa-inspector/) +- [Chrome](https://chrome.google.com/webstore/detail/single-spa-inspector/emldbibkihanfiaiaghebffnbahjcgcp) + +Note: you can also build and run this locally. See [how to contribute](/docs/contributing-overview/#how-to-contribute). + +## Features + +- List all registered applications (mounted at top) +- Show all application statuses (statii) +- Force mount and unmount an application +- Show app overlays (see [configuring app overlays](#configuring-app-overlays) to enable this feature) +- Provides an interface for adding [import-map overrides](#import-map-overrides) + +## Configuring app overlays + +App overlays allow you to hover over a mounted app's name and have an "inspect element" type experience which shows where the app is in the DOM. This is especially useful for when multiple apps are mounted at the same time (e.g. in some places Canopy has upwards of 4 apps mounted on a single page/URL!). + +To add app overlays, find the file where you export your lifecycle functions (e.g. `bootstrap`, `mount`, `unmount`) and add another exported object with the following shape: + +```js +// must be called "devtools" +export const devtools = { + overlays: { + // selectors is required for overlays to work + selectors: [ + // an array of CSS selector strings, meant to be unique ways to identify the outermost container of your app + // you can have more than one, for cases like parcels or different containers for differet views + '#my-app', + '.some-container .app', + ], + // options is optional + options: { + // these options allow you some control over how the overlay div looks/behaves + // the listed values below are the defaults + + width: '100%', + height: '100%', + zIndex: 40, + position: 'absolute', + top: 0, + left: 0, + color: '#000', // the default for this is actually based on the app's name, so it's dynamic. can be a hex or a CSS color name + background: '#000', // the default for this is actually based on the app's name, so it's dynamic. can be a hex or a CSS color name + textBlocks: [ + // allows you to add additional text to the overlay. for example, you can add the name of the team/squad that owns this app + // each string in this array will be in a new div + // 'blue squad', 'is awesome' + // turns into: + //
blue squad
is awesome
+ ], + }, + }, +}; +``` + +## import-map-overrides + +If your environment uses [import-maps](https://github.com/WICG/import-maps), single-spa Inspector provides an interface for adding import-map overrides when utilizing the [import-map-overrides](https://github.com/joeldenning/import-map-overrides) library. Once the [installation requirements](https://github.com/joeldenning/import-map-overrides#installation) for import-map-overrides are completed, you can add, remove, and refresh the page with your overrides. + +![Example of single-spa Inspector extension with import-maps overrides](/img/demo-with-importmapoverrides.png) diff --git a/versioned_docs/version-6.x/ecosystem-alpinejs.md b/versioned_docs/version-6.x/ecosystem-alpinejs.md new file mode 100644 index 000000000..e9df25b05 --- /dev/null +++ b/versioned_docs/version-6.x/ecosystem-alpinejs.md @@ -0,0 +1,254 @@ +--- +id: ecosystem-alpinejs +title: single-spa-alpinejs +sidebar_label: AlpineJS +--- + +[single-spa-alpinejs](https://github.com/single-spa/single-spa-alpinejs) is a helper library for mounting [alpinejs](https://github.com/alpinejs/alpine/) components as +single-spa applications or parcels. + +## Installation + +```sh +npm install --save single-spa-alpinejs + +# or +yarn add single-spa-alpinejs +``` + +Alternatively, you can use single-spa-alpinejs from a CDN as a global variable: + +```html + +``` + +Note that you might want to lock down the package to a specific version. See [here](https://cdn.jsdelivr.net/npm/single-spa-alpinejs) for +how to do that. + +## Usage + +- There are three ways the you can define AlpineJS components as single-spa applications or parcels. + +### _1 - Template Only_ + +- The simplest way where the template contains all the required data and initialization logic (including `x-data` and `x-init`) as part of the dom. The template is provided via the options attribute `template` + +### _2 - Template with externally defined `x-data`_ + +- You could also provide `x-data` externally and the helper will add it to the component. + - The `x-data` can be provided in the following forms (via the options attribute `xData`) + - an object + - a function that returns an object + - a function that returns a promise which resolves with an object. + +### _3 - Template with externally defined `x-data` with `x-init`_ + +- You can also provide `x-init` externally along with the `x-data` and the helper will add it to the component. + +- The `x-init` can be provided in the following forms (via the options attribute `xInit`) and needs to be a function. +- Please note the `xData` attribute _must_ be provided otherwise the `xInit` attribute will be ignored. +- The sample below references the example from the [Alpine Toolbox - Alpine JS and fetch()](https://www.alpinetoolbox.com/examples/) and demonstrates how you can use the `xInit` and `xData` attributes to create an AlpineJS application . + +### Usage Examples + +#### _1 - Template Only_ + +```js +import singleSpaAlpinejs from 'single-spa-alpinejs'; + +const alpinejslifecycles = singleSpaAlpinejs({ + template: ` +
+
Example for x-show attribute
+ +
+ Hey, I'm open +
+
`, +}); + +export const bootstrap = alpinejslifecycles.bootstrap; +export const mount = alpinejslifecycles.mount; +export const unmount = alpinejslifecycles.unmount; +``` + +#### Via cdn + +Example usage when installed via CDN: + +- The usage is similar and once the library is loaded it will be available globally and accessed via the `window` object. + +```js +const alpinejsApp = window.singleSpaAlpinejs({ + template: ` +
+
Example for x-show attribute
+ +
+ Hey, I'm open +
+
`, +}); + +singleSpa.registerApplication({ + name: 'name', + app: alpinejsApp, + activeWhen: () => true, +}); +``` + +#### _2 - Template with externally defined `x-data`_ + +```js +import singleSpaAlpinejs from 'single-spa-alpinejs'; + +const alpinejslifecycles = singleSpaAlpinejs({ + template: ` +
+
Example for x-show attribute
+ +
+ Hey, I'm open +
+
`, + xData: { open: false }, +}); + +export const bootstrap = alpinejslifecycles.bootstrap; +export const mount = alpinejslifecycles.mount; +export const unmount = alpinejslifecycles.unmount; +``` + +#### _3 - Template with externally defined `x-data` with `x-init`_ + +```js +import singleSpaAlpinejs from 'single-spa-alpinejs'; + +const appTemplate = ` +
+

+

+
+ + + +
+
+ `; + +const appDataFn = ({ title, name }) => ({ + title, + intro: + 'Implement a simple fetch() request to render a list of items using Alpine.js :)', + users: [], + open: false, + name, +}); + +const appXInitFn = (id) => { + return fetch('https://jsonplaceholder.typicode.com/users') + .then((response) => response.json()) + .then((data) => (document.querySelector(`#${id}`).__x.$data.users = data)); +}; + +const opts = { + template: appTemplate, + xData: (data) => appDataFn(data), // pass props to x-data + xInit: appXInitFn, +}; + +const alpinejslifecycles = singleSpaAlpinejs(opts); + +export const bootstrap = alpinejslifecycles.bootstrap; +export const mount = alpinejslifecycles.mount; +export const unmount = alpinejslifecycles.unmount; +``` + +## API / Options + +single-spa-html is called with an object that has the following properties: + +- `template` (required): An HTML string or a function that returns a string. The function will be called with the single-spa custom props. The returned string is injected into the DOM during the single-spa mount lifecycle. +- `domElementGetter` (optional): A function that returns the dom element container into which the HTML will be injected. If omitted, + a default implementation is provided that wraps the template in a `
` that is appended to `document.body`. +- `xData` (optional): An object or a function or a function that returns a promise.The returned string is injected into the DOM as the `x-data` attribute during the single-spa mount lifecycle. +- `xInit` (optional): A function or a function that returns a promise. The function provided is added to the global scope and the function initiation along with the root dom element id as a parameter is injected into the DOM as the `x-init` attribute during the single-spa mount lifecycle. Please note the `xData` attribute _must_ be provided otherwise the `xInit` attribute will be ignored. The function you provide `xInit` + +### xData and xInit Handling + +- This section covers the details of how `xData` and `xInit` option attributes are processed by the single spa helper. + +- Consider the example below + +```js +const appDataFn = () => { open: false, loading: true } +const appXInitFn = (domId) => { + console.log('Hello from appXInitFn'); + // domId provides access to the parent dom element where x-data and x-init are defined + document.querySelector(`#${domId}`).__x.$data.loading = false +} + +const opts = { + template: appTemplate, // base template + xData: (data) => appDataFn(data), // pass props to x-data + xInit: appXInitFn, // x-Init function +}; + +const alpinejsApp = singleSpaAlpinejs(opts); + +singleSpa.registerApplication({ + name: 'myapp', + app: alpinejsApp, + activeWhen: () => true, +}); + +``` + +- The helper does the following + - Adds the template to the dom wrapped in `parent dom element` with and id that has a prefix of `alpine`. In this case it will be `id='alpine-myapp'` + - Attaches a resolved `xData` as a string `x-data="{ "name": "myapp" ,"open": false }"` to the `parent dom element`. + - It will make the user defined `appXInitFn` available globally as an attribute of `window.singleSpaAlpineXInit` and will be accessible via variable `window.singleSpaAlpineXInit.myapp` + - Attaches a resolved `xInit` as a string that calls the globally defined variable `x-init="singleSpaAlpineXInit.myapp('alpine-myapp')"` to the `parent dom element`. + - **Note** that this also passes `id` of the `parent dom element` which can then be used to access the alpine data elements to update the state as required. + + #### Special characters in the application names + + - You may have special characters in the application name for example `@my/app`. See the example below + + ```js + singleSpa.registerApplication({ + name: '@my/app', + app: alpinejsApp, + activeWhen: () => true, + }); + ``` + + - The single spa helper converts these to valid `global` function names by `replacing` `all the special characters` with underscores (`_`). This does not require any special handling from the user as the helper takes care of this internally + + - In the above case the `xInit` dom element would look like the following `x-init="singleSpaAlpineXInit._my_app('alpine-@my/app')"` where the `xInit` function is available as a `global` variable `_my_app`. diff --git a/versioned_docs/version-6.x/ecosystem-angular.md b/versioned_docs/version-6.x/ecosystem-angular.md new file mode 100644 index 000000000..5fda2428f --- /dev/null +++ b/versioned_docs/version-6.x/ecosystem-angular.md @@ -0,0 +1,1314 @@ +--- +id: ecosystem-angular +title: single-spa-angular +sidebar_label: Angular +--- + +### Project Status + +The single-spa-angular project is overseen by the single-spa core team, but largely maintained by the community using it. This is because the single-spa core team is stronger in other frameworks and often doesn't have the expertise to fix bugs in a timely manner. There are a few community members who help us actively maintain the project, which we greatly appreciate. If you have Angular experience, we'd appreciate your help in maintaining the repo. To do so, set up notifications by watching the single-spa-angular Github repository. Also, please join the `#maintainers` and `#angular` channels in Slack. + +## Introduction + +[single-spa-angular](https://github.com/single-spa/single-spa-angular/) is a library for creating Angular microfrontends. + +Each microfrontend ([single-spa application](/docs/building-applications)) is an Angular CLI project that can +use its own version of Angular and be deployed separately from any other. They all come together into a single +web page where one or more single-spa applications is active at any time. + +The documentation here is extensive, so use the sidenav on the right. 👉👉👉 + +### Community + +Join the `#angular` channel in [single-spa's Slack workspace](https://join.slack.com/t/single-spa/shared_invite/zt-21skfl7l3-leF7JkoKwKaRIPX~N6jXJQ). + +### Demo + +https://coexisting-angular-microfrontends.surge.sh + +### Starter repo + +https://github.com/joeldenning/coexisting-angular-microfrontends + +### Contributing + +For instructions on how to test this locally before creating a pull request, see the [Contributing docs](https://github.com/single-spa/single-spa-angular/blob/master/CONTRIBUTING.md). + +## Angular versions + +### Angular 1 (AngularJS) + +AngularJS is supported by [single-spa-angularjs](https://github.com/single-spa/single-spa-angularjs), instead of single-spa-angular. +See [AngularJS docs](/docs/ecosystem-angularjs). + +### Angular 2 + +Angular 2 is supported by single-spa-angular@3. + +The [single-spa-angular schematics](#schematics) are not supported by Angular 2, so you'll have to +follow the [steps for manual installation](#manual-installation). The +[single-spa helpers](#the-single-spa-helpers) work with Angular 2. + +### Angular 3 + +Angular 3 [never existed](https://www.infoworld.com/article/3150716/forget-angular-3-google-skips-straight-to-angular-4.html). + +### Angular 4 + +Angular 4 is supported by single-spa-angular@3. + +The [single-spa-angular schematics](#schematics) are not supported by Angular 4, so you'll have to +follow the [steps for manual installation](#manual-installation). The +[single-spa helpers](#the-single-spa-helpers) work with Angular 4. + +### Angular 5 + +Angular 5 is supported by single-spa-angular@3. + +The [single-spa-angular schematics](#schematics) are not supported by Angular 5, so you'll have to +follow the [steps for manual installation](#manual-installation). The +[single-spa helpers](#the-single-spa-helpers) work with Angular 5. + +### Angular 6 + +Angular 6 is supported by single-spa-angular@3. + +The [single-spa-angular schematics](#schematics) are not supported by Angular 6, so you'll have to +follow the [steps for manual installation](#manual-installation). The +[single-spa helpers](#the-single-spa-helpers) work with Angular 6. + +### Angular 7 + +Angular 7 is supported by single-spa-angular@3. + +Both the [single-spa-angular schematics](#schematics) and the [single-spa helpers](#the-single-spa-helpers) +work with Angular 7. Follow the [Angular CLI instructions](#angular-cli). + +Note that the schematics for Angular 7 use an [Angular Builder](#use-angular-builder) that is no longer +used in the Angular 8 schematics. + +### Angular 8 + +Angular 8 is supported by single-spa-angular@3. + +Both the [single-spa-angular schematics](#schematics) and the [single-spa helpers](#the-single-spa-helpers) +work with Angular 8. Follow the [Angular CLI instructions](#angular-cli). + +Note that the schematics for Angular 8 [do not use the custom Angular builder](#angular-builder), but instead use +[@angular-builders/custom-webpack](https://www.npmjs.com/package/@angular-builders/custom-webpack). + +### Angular 9 + +Angular 9 is supported by single-spa-angular@4. + +Both the [single-spa-angular schematics](#schematics) and the [single-spa helpers](#the-single-spa-helpers) +work with Angular 9. Follow the [Angular CLI instructions](#angular-cli). + +Note that the schematics for Angular 9 [do not use the custom Angular builder](#angular-builder), but instead use +[@angular-builders/custom-webpack](https://www.npmjs.com/package/@angular-builders/custom-webpack). + +### Angular 10 + +Angular 10 is supported by single-spa-angular@4. + +Both the [single-spa-angular schematics](#schematics) and the [single-spa helpers](#the-single-spa-helpers) +work with Angular 10. Follow the [Angular CLI instructions](#angular-cli). + +Note that the schematics for Angular 10 [do not use the custom Angular builder](#angular-builder), but instead use +[@angular-builders/custom-webpack](https://www.npmjs.com/package/@angular-builders/custom-webpack). + +### Angular 11 + +Angular 11 is supported by single-spa-angular@4. + +Both the [single-spa-angular schematics](#schematics) and the [single-spa helpers](#the-single-spa-helpers) +work with Angular 11. Follow the [Angular CLI instructions](#angular-cli). + +Note that the schematics for Angular 11 [do not use the custom Angular builder](#angular-builder), but instead use +[@angular-builders/custom-webpack](https://www.npmjs.com/package/@angular-builders/custom-webpack). + +### Angular 12 + +Angular 12 is supported by single-spa-angular@5. + +Both the [single-spa-angular schematics](#schematics) and the [single-spa helpers](#the-single-spa-helpers) +work with Angular 12. Follow the [Angular CLI instructions](#angular-cli). + +### Angular 13 + +Angular 13 is supported by single-spa-angular@6. + +Both the [single-spa-angular schematics](#schematics) and the [single-spa helpers](#the-single-spa-helpers) +work with Angular 13. Follow the [Angular CLI instructions](#angular-cli). + +### Angular 14 + +Angular 14 is supported by single-spa-angular@7. + +Both the [single-spa-angular schematics](#schematics) and the [single-spa helpers](#the-single-spa-helpers) +work with Angular 14. Follow the [Angular CLI instructions](#angular-cli). + +### Angular 15 + +Angular 15 is supported by single-spa-angular@8. + +Both the [single-spa-angular schematics](#schematics) and the [single-spa helpers](#the-single-spa-helpers) +work with Angular 15. Follow the [Angular CLI instructions](#angular-cli). + +## Angular CLI + +You may use Angular CLI and single-spa together with any version of Angular. However, the [Angular CLI schematics](#schematics) +only work if you're using Angular >= 7. If you're using an older version of Angular, follow +the [manual installation instructions](#manual-installation). + +### Installation + +First, create an angular application. This requires installing [Angular CLI](https://cli.angular.io/). Note that the `--prefix` +is important so that when you have multiple angular applications their component selectors won't have the same names. + +```sh +ng new my-app --routing --prefix my-app +cd my-app +``` + +In the root of your Angular CLI application run the following: + +```sh +# If you use any Angular version lower than 14. +ng add single-spa-angular +# If you use Angular 14 you have to specify the project name. +# This is because the `defaultProject` option has been deprecated by Angular CLI. +ng add single-spa-angular --project my-cool-app +``` + +### Schematics + +[Angular schematics](https://angular.io/guide/schematics) are processed when you run `ng add single-spa-angular` or `ng add single-spa-angular --project my-cool-app`. + +The single-spa-angular schematics perform the following tasks: + +- Install single-spa-angular. +- Generate a `main.single-spa.ts` in your project `src/`. +- Generate `single-spa-props.ts` in `src/single-spa/` +- Generate `asset-url.ts` in `src/single-spa/` +- Generate an EmptyRouteComponent in `src/app/empty-route/`, to be used in app-routing.module.ts. +- Add an npm script `npm run build:single-spa`. +- Add an npm script `npm run serve:single-spa`. +- For Angular 7 only, create a new entry in the project's architect called `single-spa`, which is + a preconfigured [Angular Builder](#angular-builder). + +### Finish installation + +Now you must [configure routes](#configure-routes). Then you can [serve](#serving) and [build](#building). + +## Manual Installation + +The manual installation instructions should be used if you are not using Angular CLI or if you are using Angular 6 or older. + +### Installation + +```sh +npm install single-spa-angular +# Or if you're using yarn +yarn add single-spa-angular +# Or if you're using pnpm +pnpm install single-spa-angular +``` + +### Manually apply schematics + +Since the single-spa-angular schematics didn't run, you'll need to make the following changes: + +1. Create all of the files that would have been created by the schematic. + [See schematics files](https://github.com/single-spa/single-spa-angular/tree/master/schematics/ng-add/_files). + Be sure to get the files in the subdirectories, too. +2. Add `build:single-spa` and `serve:single-spa` to the [scripts](https://docs.npmjs.com/misc/scripts) in your package.json. + [See `addNPMScripts` function](https://github.com/single-spa/single-spa-angular/blob/master/schematics/ng-add/index.ts#L161). +3. Use the angular builder, as described in the next section. + +### Use Angular Builder + +**Note that this only applies to Angular versions pre Angular 8**. Up until Angular 8, we maintained an angular builder +that allowed us to control the webpack config, but since Angular 8 we use +[@angular-builders/custom-webpack](https://www.npmjs.com/package/@angular-builders/custom-webpack) instead. See [documentation](#use-custom-webpack) for +using the custom webpack builder with single-spa-angular and Angular 8+. + +**If you installed this library with Angular 7 using the Angular Schematic, this is already configured and +you don't need to change it. Otherwise, you might need to do this manually.** + +**If you don't use Angular CLI, skip this section.** + +To build your Angular CLI application as a single-spa app do the following. + +- Open `angular.json` +- Locate the project you wish to update. +- Navigate to the `architect > build` property. +- Set the `builder` property to `single-spa-angular:build`. +- Run `ng build` and verify your dist contains one asset, `main.js`. + +Example Configuration: + +```json +{ + "architect": { + "build": { + "builder": "single-spa-angular:build", + "options": { + "libraryName": "hello" + } + }, + "serve": { + "builder": "single-spa-angular:dev-server", + "options": {} + } + } +} +``` + +##### ng build options + +Configuration options are provided to the `architect.build.options` section of your angular.json. + +| Name | Description | Default Value | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------ | +| libraryName | (optional) Specify the name of the module | Angular CLI project name | +| libraryTarget | (optional) The type of library to build [see available options](https://github.com/webpack/webpack/blob/master/declarations/WebpackOptions.d.ts#L1111) | "UMD" | +| singleSpaWebpackConfigPath | (optional) Path to partial webpack config to be merged with angular's config. Example: `extra-webpack.config.js` | undefined | + +##### ng serve options + +Configuration options are provided to the `architect.serve.options` section of your angular.json. + +| Name | Description | Default Value | +| -------------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------- | +| singleSpaWebpackConfigPath | (optional) Path to partial webpack config to be merged with angular's config. Example: `extra-webpack.config.js` | undefined | + +### Use Custom Webpack + +Starting with Angular 8, single-spa-angular's schematics install and use [`@angular-builders/custom-webpack`](https://github.com/just-jeb/angular-builders/tree/master/packages/custom-webpack) to modify the webpack config. The schematics also create an `extra-webpack.config.js` file in your project where you can modify the configuration further. + +The extra-webpack.config.js file should include the following: + +```js +const singleSpaAngularWebpack = require('single-spa-angular/lib/webpack') + .default; + +module.exports = (config, options) => { + const singleSpaWebpackConfig = singleSpaAngularWebpack(config, options); + + // Feel free to modify this webpack config however you'd like to + return singleSpaWebpackConfig; +}; +``` + +Older versions of single-spa-angular@3 and single-spa-angular@4 created extra-webpack.config.js files that did not pass `options` into `singleSpaAngularWebpack`. When you upgrade to newer versions, you'll need to pass in the options as shown above. + +In addition to modifying the webpack config directly, you may alter some of single-spa-angular's behavior by changing the angular.json. Configuration options are provided to the `architect.build.options.customWebpackConfig` section of your angular.json. + +| Name | Description | Default Value | +| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------ | +| path | (required) Path to the the above `extra-webpack.config.js` file. | N/A | +| libraryName | (optional) Specify the name of the module | Angular CLI project name | +| libraryTarget | (optional) The type of library to build [see available options](https://github.com/webpack/webpack/blob/master/declarations/WebpackOptions.d.ts#L1111) | "UMD" | +| excludeAngularDependencies | (optional) Excludes Angular dependencies from the bundle by adding them to Webpack `externals` | false | + +If you're using SystemJS, you may want to consider changing the [webpack output.libraryTarget](https://webpack.js.org/configuration/output/#outputlibrarytarget) to be `"system"`, for better interop with SystemJS. + +## Routing + +### Configure routes + +To get single-spa working, you'll need to manually modify a few files. + +1. Add `providers: [{ provide: APP_BASE_HREF, useValue: '/' }]` to `app-routing.module.ts`. See + [angular docs](https://angular.io/api/common/APP_BASE_HREF) for more details about APP_BASE_HREF. +2. Add `{ path: '**', component: EmptyRouteComponent }` to your `app-routing.module.ts` routes. The EmptyRouteComponent is part of the + single-spa-angular schematics. This route makes sure that when single-spa is transitioning between routes that your Angular application + doesn't try to show a 404 page or throw an error. See [angular docs](https://angular.io/guide/router#configuration) for more details about routes. +3. Add a declaration for EmptyRouteComponent in `app.module.ts`. See [angular docs](https://angular.io/guide/ngmodules#the-basic-ngmodule) for + more details about app.module.ts. + +:::caution +**APP_BASE_HREF** should have the same value that the used url for mount the Angular app defined in the single-spa root application. But doing this causes strange behaviours in Angular Router when navigate between registered apps. + +In order to avoid this is recommended using **'/'** as **APP_BASE_HREF** and repeat the url prefix for your Angular app in every route component and router links. If you set **/angular** in your Angular app activity function for mount when the url starts with this value you'll have to add **/angular** prefix in all links. + +You can see several discussions about this issue in **single-spa-angular** GitHub repo: [Router not working without APP_BASE_HREF](https://github.com/single-spa/single-spa-angular/issues/64) and [How to handle router links between different single-spa application subrouters](https://github.com/single-spa/single-spa-angular/issues/62) +::: + +### Linking between applications + +To link between applications, simply use [`routerLink`](https://angular.io/api/router/RouterLink) like normal. + +```html + + Link to other app + +``` + +### Nested routes + +Nested routes work exactly the same as they normally do. To create a nested route, add it to your app-routing.module.ts. +To link to a nested route, use [`routerLink`](https://angular.io/api/router/RouterLink) the same way you normally do. + +```html + + Link to nested route + +``` + +### Enabling hash mode + +You need to firstly enable hash mode in root-config. + +If you are using layout html with `single-spa-router`, add `mode="hash"` + +```html + + ... + +``` + +If you are registering each route manually, use `location.hash` + +```js +registerApplication({ + name: '@orgName/app1', + app: () => System.import('@orgName/app1'), + activeWhen: location => location.hash.startsWith('#/app1'), +}); +``` + +Then, enable hash mode in your routing module of angular micro frontend application. + +```ts +@NgModule({ + imports: [RouterModule.forRoot(routes, {useHash: true})], + exports: [RouterModule] +}) +``` + +## Serving + +Run the following: + +```sh +npm run serve:single-spa +``` + +This **will not** open up an HTML file, since single-spa applications all [share one html file](/docs/configuration). Instead, go to +http://single-spa-playground.org and follow the instructions there to verify everything is working and for instructions on creating the shared HTML file. + +## Building + +Run `npm run build:single-spa`, which will create a `dist` directory with your compiled code. + +In order for the [webpack public path](https://webpack.js.org/guides/public-path/#root) to be correctly set for your assets, you should use Angular CLI's `--deploy-url` option. For more information, see [this Stack Overflow answer](https://stackoverflow.com/questions/47885451/angular-cli-build-using-base-href-and-deploy-url-to-access-assets-on-cdn) which shows a few options for how to do that. + +## The single-spa helpers + +### Introduction + +"single-spa helpers" refers to the in-browser portion of single-spa-angular. The helpers are used by all versions of Angular and +regardless of whether you are using Angular CLI or not. This is the core of the single-spa-angular library that makes it possible +for Angular applications to bootstrap, mount, and unmount. See +[single-spa lifecycles](/docs/building-applications#registered-application-lifecycle) for more information. + +### Migrating from single-spa-angular@3.x to single-spa-angular@4.x + +​ +Migrating from 3.x to 4.x requires only few API updates. +​ + +#### Packages + +​ + +```sh +npm install single-spa-angular@4.0.0 +# Or if you're using yarn +yarn add single-spa-angular@4.0.0 +# Or if you're using pnpm +pnpm install single-spa-angular@4.0.0 +``` + +​ + +#### API Updates + +​ +`single-spa-angular` doesn't have a default export anymore, instead you have to import a named `singleSpaAngular` function. Given the following code: +​ + +```js +import singleSpaAngular from 'single-spa-angular'; // single-spa-angular@3.x +import { singleSpaAngular } from 'single-spa-angular'; // single-spa-angular@4.x +``` + +​ +Also, if your application uses routing then you have to import the `getSingleSpaExtraProviders` function. Let's look at the following example, this is how it was in `single-spa-angular@3.x`: +​ + +```js +import { NgZone } from '@angular/core'; +import { Router, NavigationStart } from '@angular/router'; +import singleSpaAngular, { getSingleSpaExtraProviders } from 'single-spa-angular'; +​ +const lifecycles = singleSpaAngular({ + bootstrapFunction: singleSpaProps => { + singleSpaPropsSubject.next(singleSpaProps); + return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule); + }, + template: '', + Router, + NavigationStart, + NgZone, +}); +``` + +​ +And this is how it should be in `single-spa-angular@4.x`: +​ + +```js +import { NgZone } from '@angular/core'; +import { Router, NavigationStart } from '@angular/router'; +import { singleSpaAngular, getSingleSpaExtraProviders } from 'single-spa-angular'; +​ +const lifecycles = singleSpaAngular({ + bootstrapFunction: singleSpaProps => { + singleSpaPropsSubject.next(singleSpaProps); + return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule); + }, + template: '', + Router, + NavigationStart, + NgZone, +}); +``` + +## Basic usage + +```ts +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { NgZone } from '@angular/core'; +import { Router, NavigationStart } from '@angular/router'; +import { + singleSpaAngular, + getSingleSpaExtraProviders, +} from 'single-spa-angular'; + +import { AppModule } from './app/app.module'; + +const lifecycles = singleSpaAngular({ + bootstrapFunction: singleSpaProps => { + return platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule( + AppModule, + ); + }, + template: '', + Router, + NavigationStart, + NgZone, +}); + +export const bootstrap = lifecycles.bootstrap; +export const mount = lifecycles.mount; +export const unmount = lifecycles.unmount; +``` + +### Full Example + +See [this schematic file](https://github.com/single-spa/single-spa-angular/blob/master/schematics/ng-add/_files/src/main.single-spa.ts.template#L16) +for a good example of how to use the single-spa helpers. + +### Options + +Options are passed to single-spa-angular via the `opts` parameter when calling `singleSpaAngular(opts)`. This happens inside of your `main.single-spa.ts` file. + +The following options are available: + +- `bootstrapFunction`: (required) A function that is given custom props as an argument and returns a promise that resolves with a resolved Angular module that is bootstrapped. Usually, your implementation will look like this: `bootstrapFunction: (customProps) => platformBrowserDynamic().bootstrapModule()`. + See [custom props documentation](https://single-spa.js.org/docs/building-applications#custom-props) for more info on the argument passed to the function. +- `template`: (required) An HTML string that will be put into the DOM Element returned by `domElementGetter`. This template can be anything, + but it is recommended that you keeping it simple by making it only one Angular component. For example, `` is recommended, + but `
Hello
` is allowed. Note that `innerHTML` is used to put the template + onto the DOM. Also note that when using multiple angular applications simultaneously, you will want to make sure that the component + selectors provided are unique to avoid collisions. When migrating to single-spa, this template is what is inside of your index.html file's + `` element. +- `Router`: (optional) The angular router class. This is required when you are using `@angular/router`. +- `AnimationModule`: (optional) The animation module class. This is required when you are using BrowserAnimationsModule. + Example way to import this: `import { eAnimationEngine as AnimationModule } from '@angular/animations/browser';`. + See [Issue 48](https://github.com/single-spa/single-spa-angular/issues/48) for more details. Note that AnimationModule is no longer needed in Angular 12, so this option can be ignored in Angular >= 12. +- `domElementGetter`: (optional) A function that takes in no arguments and returns a DOMElement. This dom element is where the Angular + application will be bootstrapped, mounted, and unmounted. It's recommended to omit this and let single-spa-angular's defaults create and use + a container div. + +## Concepts + +### ZoneJS + +[ZoneJS](https://github.com/angular/zone.js/) is the library that Angular uses for change detection. You absolutely must have exactly +one instance of the ZoneJS library on the page. ZoneJS will throw errors if you have more than one instance of ZoneJS on the page. + +The preferred way to ensure only one instance of ZoneJS is loaded on your page is with a script tag in your root-config's HTML file. You should load ZoneJS upfront a single time, before loading SystemJS or any of your microfrontends. + +```html + +``` + +Note that having only one instance of ZoneJS is different than having only one zone within that instance. single-spa-angular +automatically will ensure that each of your Angular applications has its own isolated, separate zone. + +### Multiple applications + +When you have multiple apps running side by side, you'll need to make sure that their +[component selectors](https://angular.io/api/core/Directive#selector) are unique. When creating a new +project, you can have angular-cli do this for you by passing in the `--prefix` option: + +```sh +ng new --prefix app2 +``` + +If you did not use the `--prefix` option, you should set the prefix manually: + +1. For an application called app2, add `"prefix": "app2"` to `projects.app2` inside of the angular.json. +2. Go to `app.component.ts`. Modify `selector` to be `app2-root`. +3. Go to `main.single-spa.ts`. Modify `template` to be ``. + +Additionally, make sure that `reflect-metadata` is only imported once in the root application and is not imported again in the child applications. +Otherwise, you might see an `No NgModule metadata found` error. +See [issue thread](https://github.com/single-spa/single-spa-angular/issues/2#issuecomment-347864894) for more details. + +### Custom Props + +[Custom props](https://single-spa.js.org/docs/building-applications#custom-props) are a way of passing auth or other data to your single-spa +applications. The custom props are available inside of the [bootstrapFunction](#options) passed to singleSpaAngular(). Additionally, if you use the +angular cli schematic, you may subscribe to the singleSpaPropsSubject in your component, as shown below: + +```ts +// An example showing where you get access to the single-spa props: +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { singleSpaAngular } from 'single-spa-angular'; + +const lifecycles = singleSpaAngular({ + bootstrapFunction(singleSpaProps) { + // Here are the custom props + console.log(singleSpaProps); + return platformBrowserDynamic().bootstrapModule(AppModule); + }, + // add the other options to singleSpaAngular, too. See "Basic usage" for more info +}); +``` + +```ts +// If you're using the singleSpaPropsSubject generated by the single-spa-angular schematics, +// here's an example component that uses the custom props +import { Component, OnInit, OnDestroy } from '@angular/core'; +import { Subscription } from 'rxjs'; +import { + singleSpaPropsSubject, + SingleSpaProps, +} from 'src/single-spa/single-spa-props'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'], +}) +export class AppComponent implements OnInit, OnDestroy { + singleSpaProps: SingleSpaProps; + subscription: Subscription; + + ngOnInit(): void { + this.subscription = singleSpaPropsSubject.subscribe( + props => (this.singleSpaProps = props), + ); + } + + ngOnDestroy(): void { + this.subscription.unsubscribe(); + } + + // OR if you don't need to access `singleSpaProps` inside the component + // then create `Observable` property and use it in template with `async` pipe. + singleSpaProps$ = singleSpaPropsSubject.asObservable(); +} +``` + +### Angular assets + +[Angular assets](https://angular.io/guide/file-structure#application-source-files) are handled differently within single-spa than within +other Angular applications. The schematics file called `asset-url.ts` helps you do load assets in a way that works both ways. + +**This won't work** + +```ts +// Doesn't work with single-spa +const imageUrl = '/assets/yoshi.png'; +``` + +**Do this instead** + +```js +import { assetUrl } from 'src/single-spa/asset-url'; + +// Works great with single-spa +const imageUrl = assetUrl('yoshi.png'); +``` + +#### Within HTML templates + +##### Option 1 + +Add the asset url to your component's class and reference it from the template. +See [here](https://github.com/joeldenning/coexisting-angular-microfrontends/blob/0fb9a557705b46349deae5c71b393b71d887e18d/app1/src/app/app.component.ts#L11) +and [here](https://github.com/joeldenning/coexisting-angular-microfrontends/blob/master/app1/src/app/app.component.html#L9). + +##### Option 2 + +Create an [Angular Pipe](https://angular.io/guide/pipes) that lets you calculate the asset url inside of an HTML template: + +```ts +import { Pipe, PipeTransform } from '@angular/core'; +import { assetUrl } from 'src/single-spa/public-path'; + +@Pipe({ name: 'assetUrl' }) +export class AssetUrlPipe implements PipeTransform { + transform(value: string): string { + return assetUrl(value); + } +} +``` + +Then use it in your template: + +```html + +``` + +### Scripts + +[Scripts in your angular.json](https://angular.io/guide/workspace-config#additional-build-and-test-options) are not loaded by single-spa. +This is because single-spa applications have to all share an HTML file. +([read more](http://single-spa-playground.org/playground/html-file)). You can remove the scripts from your angular.json because they +have no impact on your single-spa build. + +#### Option 1 + +Add the script tags directly into your [root HTML file](http://single-spa-playground.org/playground/html-file). This way is easiest. +The downside is that all of the scripts get loaded even for routes that don't need them. However, that is generally okay and this +is the preferred way to do it. + +#### Option 2 + +If you want the scripts to only be loaded when needed, you can add a custom +[bootstrap lifecycle](/docs/building-applications#bootstrap) to your code. + +Note that lazy loading these scripts can actually be worse for performance _if you always need them_, since +they will start downloading later than if you put them right into the root HTML file. + +```ts +// main.single-spa.ts + +// Modify the bootstrap function like so +export const bootstrap = [ + () => + Promise.all([ + loadScript( + 'https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js', + ), + loadScript( + 'https://cdnjs.cloudflare.com/ajax/libs/datepicker/0.6.5/datepicker.min.js', + ), + ]), + lifecycles.bootstrap, +]; + +function loadScript(url: string) { + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.src = url; + + function onLoad() { + resolve(); + cleanup(); + } + + function onError(event: Event) { + reject(event); + cleanup(); + } + + function cleanup() { + script.removeEventListener('load', onLoad); + script.removeEventListener('error', onError); + } + + script.addEventListener('load', onLoad); + script.addEventListener('error', onError); + document.head.appendChild(script); + }); +} +``` + +### Styles + +[Styles in your angular.json](https://angular.io/guide/workspace-config#additional-build-and-test-options) will automatically +be loaded by single-spa-angular's webpack config, without you having to configure anything. + +Your component styles will also be loaded like normal without you having to configure anything. + +### Polyfills + +[Polyfills in your angular.json](https://angular.io/guide/browser-support) are JavaScript code that make your project work in older browsers, +such as IE11. + +**The polyfills that you specify in your angular.json file will not be loaded automatically**. This is because we should only load +polyfills once in the root HTML file, instead of once per application. + +To load polyfills, you'll need to follow the instructions in the [Angular documentation for non-CLI users](https://angular.io/guide/browser-support#polyfills-for-non-cli-users). +Even if you are using Angular CLI, you will need to follow those instructions, since your [single-spa root HTML file](https://single-spa-playground.org/playground/html-file) +is not using Angular CLI and that's where the polyfills need to go. + +If you're looking for a quick one-liner, try adding this line near the top of your index.html. + +```html + +``` + +To correct the error `It looks like your application or one of its dependencies is using i18n`. + +Install `@angular/localize` in your root-config module + +```sh +npm install @angular/localize +# Or if you're using yarn +yarn add @angular/localize +# Or if you're using pnpm +pnpm install @angular/localize + +``` + +Add following import to your root-config.js + +```ts +import '@angular/localize/init'; +``` + +### Internet Explorer + +If you need to support IE11 or older, do the following: + +- [Add core-js polyfill](#polyfills) +- Remove arrow functions from index.html ([example](https://github.com/joeldenning/coexisting-angular-microfrontends/commit/22cbb2dc1c15165c39b10aa4019fe517fa88af32#diff-07a3141209aa56f89a0f47490866f94eR34)) +- Change angular.json `target` to `es5` ([example](https://github.com/joeldenning/coexisting-angular-microfrontends/commit/22cbb2dc1c15165c39b10aa4019fe517fa88af32#diff-acbfc718bf309f27dd3699a4ad80a2d1R13)) + +[Full example commit to get IE11 support](https://github.com/joeldenning/coexisting-angular-microfrontends/commit/22cbb2dc1c15165c39b10aa4019fe517fa88af32) + +## Shared Angular + +Sharing one or more instances of Angular between microfrontends provides the following benefits: + +1. Performance improvement, due to reduced amount of javascript to load. +1. [Cross-microfrontend imports](/docs/recommended-setup#cross-microfrontend-imports) of angular components are possible. (Without a shared instance of Angular, you can still use cross-microfrontend imports of [single-spa parcels](#parcels)) + +There are two techniques for sharing Angular: [SystemJS in-browser modules](/docs/recommended-setup#systemjs) and [Module Federation](/docs/recommended-setup#module-federation). + +The below guide uses SystemJS for sharing dependencies. + +> :warning: Angular 13 has few breaking changes. It dropped View Engine support, and this means there's a single template compiler right now (Ivy). They also introduced changes to the Angular Package Format. UMD bundles are no longer generated when building libraries. The `ng-packagr` now emits only ES2015 and ES2020 bundles. See this article for more info: https://blog.angular.io/angular-v13-is-now-available-cce66f7bc296. + +You can use the following esm-bundle packages, which provide Ivy-compatible versions that can be shared via SystemJS: + +- https://github.com/esm-bundle/angular__core +- https://github.com/esm-bundle/angular__common +- https://github.com/esm-bundle/angular__compiler +- https://github.com/esm-bundle/angular__platform-browser +- https://github.com/esm-bundle/angular__platform-browser-dynamic +- https://github.com/esm-bundle/angular__animations +- https://github.com/esm-bundle/angular__router +- https://github.com/esm-bundle/angular__forms +- https://github.com/esm-bundle/angular__elements +- https://github.com/esm-bundle/angular__upgrade +- https://github.com/esm-bundle/angular__service-worker +- https://github.com/esm-bundle/angular__localize + +The single-spa-angular is also available in SystemJS format: + +- https://github.com/esm-bundle/single-spa-angular + +Let's imagine that we have 2 Angular applications and the root-config application. Angular applications want to share dependencies. First of all, we need to exclude Angular dependencies from their bundles, this can be done via Webpack `externals` property, but `single-spa-angular@6.2.0+` has an option that will exclude `rxjs`, `@angular/*` and `single-spa-angular/*` packages: + +```json +"build": { + "builder": "@angular-builders/custom-webpack:browser", + "options": { + "customWebpackConfig": { + "libraryTarget": "system", + "excludeAngularDependencies": true, + "path": "..." + } + } +} +``` + +Note that we set the `libraryTarget` to `system` and `excludeAngularDependencies` to `true`. + +Next we need to replace `enableProdMode` from `@angular/core` with `enableProdMode` from `single-spa-angular` in our applications: + +```js +import { enableProdMode } from 'single-spa-angular'; + +if (environment.production) { + enableProdMode(); +} +``` + +This is because Angular's `enableProdMode` throws an error when it's called multiple times, but it will be called multiple times when dependencies are shared. `single-spa-angular` calls the original `enableProdMode` but swallows the error. + +We also have to consider the platform injector that will be re-used between applications. Angular creates a platform injector when it bootstraps an application; it creates it only once and then re-uses it. Each Angular application has its own platform injector when dependencies are NOT shared and will have a single one when dependencies are shared. This means that `providedIn: 'platform'` services will be a part of the single injector and will be shared between applications. + +`single-spa-angular` exports the `getSingleSpaExtraProviders` function that adds `SingleSpaPlatformLocation` to the platform injector. This function should be called in each application (even if that application doesn't use routing). We also have to share the `single-spa-angular` package because applications should reference the same `SingleSpaPlatformLocation` class. Assume that `app1` is created earlier than `app2`, `app1` doesn't call `getSingleSpaExtraProviders()` when bootstrapping the root module, but `app2` does. The platform injector is created eagerly when the `platformBrowser()` is called for the first time. The `app2` will then try to retrieve the `SingleSpaPlatformLocation`, but there's no such injectee within the platform injector since `app1` didn't declare this dependency. + +The root-config should have a SystemJS import map with all of the required packages: + +```json +{ + "imports": { + "app1": "http://localhost:4200/main.js", + "app2": "http://localhost:4201/main.js", + + "single-spa": "https://cdnjs.cloudflare.com/ajax/libs/single-spa/5.9.3/system/single-spa.dev.js", + "rxjs": "https://cdn.jsdelivr.net/npm/@esm-bundle/rxjs/system/es2015/rxjs.min.js", + "rxjs/operators": "https://cdn.jsdelivr.net/npm/@esm-bundle/rxjs/system/es2015/rxjs-operators.min.js", + "@angular/compiler": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular__compiler/system/es2020/ivy/angular-compiler.js", + "@angular/core": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular__core/system/es2020/ivy/angular-core.js", + "@angular/common": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular__common/system/es2020/ivy/angular-common.js", + "@angular/common/http": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular__common/system/es2020/ivy/angular-http.js", + "@angular/animations": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular__animations/system/es2020/ivy/angular-animations.js", + "@angular/animations/browser": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular__animations/system/es2020/ivy/angular-browser.js", + "@angular/platform-browser": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular__platform-browser/system/es2020/ivy/angular-platform-browser.js", + "@angular/platform-browser/animations": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular__platform-browser/system/es2020/ivy/angular-animations.js", + "@angular/platform-browser-dynamic": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular__platform-browser-dynamic/system/es2020/ivy/angular-platform-browser-dynamic.js", + "@angular/router": "https://cdn.jsdelivr.net/npm/@esm-bundle/angular__router/system/es2020/ivy/angular-router.js", + "single-spa-angular/internals": "https://cdn.jsdelivr.net/npm/@esm-bundle/single-spa-angular@6.2.0/system/es2020/ivy/angular-single-spa-angular-internals.js", + "single-spa-angular": "https://cdn.jsdelivr.net/npm/@esm-bundle/single-spa-angular@6.2.0/system/es2020/ivy/angular-single-spa-angular.js" + } +} +``` + +Production bundles also don't use `@angular/compiler` and `@angular/platform-browser-dynamic` packages. You would want to use minified packages when the root-config is built in production mode. One approach could be to use `if-else` conditions within the root-config `.ejs` template: + +```html +<% if (htmlWebpackPlugin.options.isDevelopment) { %> + +<%} else { %> + +<% } %> +``` + +Note: the `isDevelopment` can be provided when creating the `HtmlWebpackPlugin`: + +```js +const HtmlWebpackPlugin = require('html-webpack-plugin'); + +module.exports = (env, { mode }) => { + const isDevelopment = mode !== 'production'; + + return { + plugins: [ + new HtmlWebpackPlugin({ + template: '...', + isDevelopment, + }), + ], + }; +}; +``` + +The `mode` will equal production when you run `webpack --mode production`. + +The root-config then can load `single-spa`, register applications and start the `single-spa` in order for applications to be mounted: + +```js +System.import('single-spa').then(({ registerApplication, start }) => { + registerApplication({ + name: 'app1', + app: () => System.import('app1'), + activeWhen: location => location.pathname.startsWith('/app1'), + }); + + registerApplication({ + name: 'app2', + app: () => System.import('app2'), + activeWhen: location => location.pathname.startsWith('/app2'), + }); + + start(); +}); +``` + +## Angular Elements + +:::info +This feature is available starting from `single-spa-angular@4.4.0`. You also may need to become familiar with [Angular Elements documentation](https://angular.io/guide/elements). +::: + +Let's start with installing the `@angular/elements`: + +```sh +npm install @angular/elements +# Or if you're using yarn +yarn add @angular/elements +# Or if you're using pnpm +pnpm install @angular/elements +``` + +The next step is to edit `main.single-spa.ts`: + +```ts +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; +import { singleSpaAngularElements } from 'single-spa-angular/elements'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +const lifecycles = singleSpaAngularElements({ + template: '', + // We can actually not rely on the `zone.js` library, our custom element + // will behave itself as a zone-less application. + bootstrapFunction: () => + platformBrowserDynamic().bootstrapModule(AppModule, { ngZone: 'noop' }), +}); + +export const bootstrap = lifecycles.bootstrap; +export const mount = lifecycles.mount; +export const unmount = lifecycles.unmount; +``` + +Note that the `app-custom-element` selector will be used when defining our [custom element](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements). + +After that, you'll have to edit `app.module.ts` and define a custom tag: + +```ts +import { NgModule, Injector, DoBootstrap } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { createCustomElement } from '@angular/elements'; + +import { AppComponent } from './app.component'; + +@NgModule({ + imports: [BrowserModule], + declarations: [AppComponent], +}) +export class AppModule implements DoBootstrap { + constructor(private injector: Injector) {} + + ngDoBootstrap(): void { + customElements.define( + // This tag we've have provided in `options.template` when called `singleSpaAngularElements`. + 'app-custom-element', + createCustomElement(AppComponent, { injector: this.injector }), + ); + } +} +``` + +The following options are available to be passed when calling `singleSpaAngularElements`: + +- `bootstrapFunction` (required) +- `template` (required) +- `domElementGetter` (optional) + +See [options](#options) for detailed explanation. + +## Parcels + +We encourage you to get familiar with the documentation, namely [Parcels overview](/docs/parcels-overview) and [Parcels API](/docs/parcels-api). This documentation will give you a basic understanding of what parcels are. + +Additionally, single-spa-angular provides a `` component to make using framework agnostic single-spa parcels easier. This allows you to put the parcel into your component's template, instead of calling `mountRootParcel()` by yourselves. + +`single-spa-angular/parcel` package exports the `ParcelModule` which exports the `` component: + +```ts +// Inside of src/app/app.module.ts +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { ParcelModule } from 'single-spa-angular/parcel'; + +import { AppComponent } from './app.component'; + +@NgModule({ + imports: [BrowserModule, ParcelModule], + declarations: [AppComponent], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + +The example below shows how you can render React parcels: + +```ts +// Inside of src/app/app.component.ts +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { mountRootParcel } from 'single-spa'; + +import { config } from './ReactWidget/ReactWidget'; + +@Component({ + selector: 'app-root', + template: + '', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppComponent { + config = config; + mountRootParcel = mountRootParcel; +} +``` + +For React, you will need to create a file with the extension `.tsx`: + +```tsx +// Inside of src/app/ReactWidget/ReactWidget.tsx +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import singleSpaReact from 'single-spa-react'; + +const ReactWidget = () =>
Hello from React!
; + +export const config = singleSpaReact({ + React, + ReactDOM, + rootComponent: ReactWidget, +}); +``` + +### Loading External Components Asynchronously + +If your code is part of an import map and you include a global reference to +systemjs, you can dynamically import the code using an `async` method. + +For example, if your import map includes the `ReactComponent`. + +```javascript +{ + imports: { + "@org/react-component": '/org-react-component.js' + } +} +``` + +You can dynamically load the component by setting the `config` as an +asynchronous method that fetches the component. + +Since some versions of webpack use SystemJS under the hood, you'll need to +reference the global version. + +```ts +// Inside of src/app/app.component.ts +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { mountRootParcel } from 'single-spa'; + +@Component({ + selector: 'app-root', + template: + '', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppComponent { + async config() { + return window.System.import('@org/react-component'); + } + mountRootParcel = mountRootParcel; +} +``` + +### Passing Custom Props + +You can pass any custom props to the parcel by passing an object of props using +the `customProps` attribute. + +For example, if you're rendering a React parcel from an Angular component, you can pass a click handler from Angular into the React parcel: + +```tsx +// Inside of src/app/ReactWidget/ReactWidget.tsx +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; +import singleSpaReact from 'single-spa-react'; + +const ReactWidget = ({ handleClick }) => ( + +); + +export const parcelConfig = singleSpaReact({ + React, + ReactDOM, + rootComponent: ReactWidget, +}); +``` + +You can pass a function (or any other value) as a custom prop. To ensure that the functions you pass to the parcel are bound with the correct javascript context, use the `handleClick = () => {` syntax when defining your functions. + +```ts +// Inside of src/app/app.component.ts +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { mountRootParcel } from 'single-spa'; + +import { config } from './ReactWidget/ReactWidget'; + +@Component({ + selector: 'app-root', + template: + '', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class AppComponent { + config = config; + mountRootParcel = mountRootParcel; + + handleClick = () => { + alert('Hello World'); + }; + + parcelProps = { + handleClick = this.handleClick, + }; +} +``` + +## Zone-less applications + +:::info +This feature is available starting from `single-spa-angular@4.1`. +::: + +It's possible to develop Angular applications that don't rely on `zone.js` library, these applications are called _zone-less_. You have to run change detection manually in _zone-less_ applications through `ApplicationRef.tick()` or `ChangeDetectorRef.detectChanges()`. You can find more info in [Angular NoopZone docs](https://angular.io/guide/zone#noopzone). + +The point is that you do not need to load `zone.js` library in your root HTML file. As Angular docs mention that you should have a comprehensive knowledge of change detection to develop such applications. Let's start by _nooping_ zone when bootstrapping module: + +```ts +import { singleSpaAngular } from 'single-spa-angular'; + +const lifecycles = singleSpaAngular({ + bootstrapFunction: () => + platformBrowserDynamic().bootstrapModule(AppModule, { ngZone: 'noop' }), + template: '', + NgZone: 'noop', +}); +``` + +:::caution +We must specify `noop` _twice_: when bootstrapping `AppModule`, and setting `NgZone` property to `noop`. This tells Angular and single-spa-angular that we're not going to use zones. +::: + +### Routing in zone-less applications + +Since routing is managed by single-spa and there is no zone that tells Angular that some asynchronous event has occured, then we need to tell Angular when to run change detection if routing occurs. Let's look at the below code: + +```js +import { ApplicationRef } from '@angular/core'; +import { Router, NavigationStart } from '@angular/router'; +import { singleSpaAngular } from 'single-spa-angular'; + +const lifecycles = singleSpaAngular({ + bootstrapFunction: async () => { + const ngModuleRef = await platformBrowserDynamic().bootstrapModule( + AppModule, + { ngZone: 'noop' }, + ); + + const appRef = ngModuleRef.injector.get(ApplicationRef); + const listener = () => appRef.tick(); + window.addEventListener('popstate', listener); + + ngModuleRef.onDestroy(() => { + window.removeEventListener('popstate', listener); + }); + + return ngModuleRef; + }, + template: '', + NgZone: 'noop', + Router, + NavigationStart, +}); +``` + +:::caution +`single-spa-angular@4.x` **requires** calling `getSingleSpaExtraProviders` function in applications that have routing. Do not call this function in _zone-less_ application. +::: + +## Inter-app communication via RxJS + +First of all, check out this [Inter-app communication guide](/docs/recommended-setup#inter-app-communication). + +It's possible to setup a communication between microfrontends via RxJS using [cross microfrontend imports](/docs/recommended-setup#cross-microfrontend-imports). + +We can not create complex abstractions, but simply export the `Subject`: + +```ts +// Inside of @org/api +import { ReplaySubject } from 'rxjs'; +import { User } from '@org/models'; + +// `1` means that we want to buffer the last emitted value +export const userSubject$ = new ReplaySubject(1); +``` + +And then you just need to import this `Subject` into the microfrontend application: + +```ts +// Inside of @org/app1 single-spa application +import { userSubject$ } from '@org/api'; +import { User } from '@org/models'; + +userSubject$.subscribe(user => { + // ... +}); + +userSubject$.next(newUser); +``` + +Also, you should remember that `@org/api` should be an "isolated" dependency, for example the Nrwl Nx library, where each library is in the "libs" folder and you import it via TypeScript paths. + +Every application that uses this library should add it to its Webpack config as an external dependency: + +```js +const singleSpaAngularWebpack = require('single-spa-angular/lib/webpack') + .default; + +module.exports = (config, options) => { + const singleSpaWebpackConfig = singleSpaAngularWebpack(config, options); + singleSpaWebpackConfig.externals = [/^@org\/api$/]; + return singleSpaWebpackConfig; +}; +``` + +But this library should be part of root application bundle and [shared with import maps](/docs/recommended-setup/#sharing-with-import-maps), for example: + +```json +{ + "imports": { + "@org/api": "http://localhost:8080/api.js" + } +} +``` diff --git a/versioned_docs/version-6.x/ecosystem-angularjs.md b/versioned_docs/version-6.x/ecosystem-angularjs.md new file mode 100644 index 000000000..69c7a343c --- /dev/null +++ b/versioned_docs/version-6.x/ecosystem-angularjs.md @@ -0,0 +1,298 @@ +--- +id: ecosystem-angularjs +title: single-spa-angularjs +sidebar_label: AngularJS +--- + +single-spa-angularjs is a helper library that helps implement [single-spa registered application](configuration#registering-applications) [lifecycle functions](building-applications.md#registered-application-lifecycle) (bootstrap, mount and unmount) for use with [AngularJS](https://angularjs.org/). Check out the [single-spa-angularjs github](https://github.com/single-spa/single-spa-angularjs). + +## Installation +```sh +npm install --save single-spa-angularjs +``` + +Note that you can alternatively ` + +``` + +2. Change your angularjs application to not mount to the DOM. This is generally done removing the `ng-app` attribute in your main html file. +3. In one of the first / main scripts loaded for your angularjs application, create your single-spa application as a global variable. See [this code](#as-a-global-variable). +4. In your main HTML file, add the following: +```html + +``` +5. Confirm that your application now is mounting again and works properly. Also, check that it's in `MOUNTED` status as a single-spa microfrontend: + +```js +// in the browser console, check that it's in `MOUNTED` status +console.log('legacyAngularjsApp status', singleSpa.getAppStatus('legacyAngularjsApp')); +``` + +### Step 2: Convert to SystemJS module: + +This step is not required unless you want to do [cross microfrontend imports](/docs/recommended-setup#cross-microfrontend-imports) between your angularjs microfrontend and other microfrontends. + +1. Add systemjs to your index.html file: + +```html + + + +``` + +2. Remove the global single-spa script: + +```diff +- +``` + +3. Modify your main / first angularjs script file to create a systemjs module instead of global variable. See [this code](/docs/ecosystem-angularjs#as-a-systemjs-module). + +4. Remove the ` +``` + +5. Modify the ` +``` +3. Start a new microfrontend on the port in the import map. Go to the route for the new microfrontend and verify it is loaded. + +## Examples + +- [polyglot microfrontends account settings](https://github.com/polyglot-microfrontends/account-settings): Gulp + angularjs@1.7 project integrated with Vue microfrontends. +- [single-spa-es5-angularjs](https://github.com/joeldenning/single-spa-es5-angularjs): No build process - just global variables. diff --git a/versioned_docs/version-6.x/ecosystem-backbone.md b/versioned_docs/version-6.x/ecosystem-backbone.md new file mode 100644 index 000000000..d1a124d3f --- /dev/null +++ b/versioned_docs/version-6.x/ecosystem-backbone.md @@ -0,0 +1,174 @@ +--- +id: ecosystem-backbone +title: single-spa-backbone +sidebar_label: Backbone +--- + +A single-spa helper library which provides lifecycle events for building single-spa applications using [Backbone](http://backbonejs.org/). + +[![npm Package](https://img.shields.io/npm/v/@emtecinc/single-spa-backbone.svg)](https://www.npmjs.com/package/@emtecinc/single-spa-backbone) +[![License](https://img.shields.io/npm/l/@emtecinc/single-spa-backbone.svg)](https://github.com/emtecinc/single-spa-backbone/blob/master/LICENSE) + +There are mostly three styles of creating backbone applications + +1. Using [RequireJS](https://requirejs.org/) which will loads the application and all it's dependencies, including the templates loaded using [Handlebars](https://handlebarsjs.com/), [RequireJS:Text](https://github.com/requirejs/text) or any other engine. + + If your application is written using this style, then you will have to pass the `AppWithRequire` parameter as options in the plugin, and choose the flavour to load the app, either through `data-main` attribute or without it. + +2. Using [Backbone](http://backbonejs.org/) and ApplicationPath (Entry point of application) directly as script elements and optionally loading other dependencies. + +3. Loading a single application bundle which includes application dependencies like i.e. Backbone, Require, Underscore, Jquery etc. + +## NPM package + +npm install --save @emtecinc/single-spa-backbone + +The npm package can be [found here](https://www.npmjs.com/package/@emtecinc/single-spa-backbone). + +## Quickstart + +### Option 1: Using `RequireJS` with `data-main` + +First, in the [single-spa application](https://github.com/single-spa/single-spa/blob/master/docs/applications.md#registered-applications), run `npm install --save @emtec/single-spa-backbone`. Then, create an entry file for application like below, assuming the application has some `BasePath` with `AppPath` and `RequireJsPath' being relative to the base path. + +```js +import singleSpaBackbone from '@emtecinc/single-spa-backbone'; + +const backBoneLifecycles = singleSpaBackbone({ + BasePath: 'appBasePath', + AppWithRequire: + { + IsDataMain: true, + AppPath: 'src/app', + RequireJsPath: 'lib/require.js' + }, + DomElementSetter: domElementSetter +}); + +export const bootstrap = [ + backBoneLifecycles.bootstrap, +]; + +export const mount = [ + backBoneLifecycles.mount, +]; + +export const unmount = [ + backBoneLifecycles.unmount, +]; + + +function domElementSetter() { + + //use the same element id to render into, in the backbone app + let el = document.getElementById('shell-container'); + if (!el) { + el = document.createElement('div'); + el.id = 'shell-container'; + document.body.appendChild(el); + } + +} +``` + +`DomElementSetter` gives you a provision to hook up your callback, and can be used to create a container element in the dom which will be used to load the app. + +Please note that this hook up is just a setter and don't expect you to return a value. You need to explicitly use the same element #id in the backbone application to use it as app region or container. + + +### Option 2: Using `RequireJS` without `data-main` + +`IsDataMain` will be set to `false` in this case + +```js +import singleSpaBackbone from '@emtecinc/single-spa-backbone'; + +const backBoneLifecycles = singleSpaBackbone({ + BasePath: 'appBasePath', + AppWithBackboneJs: + { + AppPath: 'src/app', + BackboneJsPath: 'lib/backbone.js' + }, + DomElementSetter: domElementSetter +}); + +export const bootstrap = backBoneLifecycles.bootstrap; + +export const mount = backBoneLifecycles.mount; + +export const unmount = backBoneLifecycles.unmount; + +function domElementSetter() { + + //use the same element id to render into, in the backbone app + let el = document.getElementById('shell-container'); + if (!el) { + el = document.createElement('div'); + el.id = 'shell-container'; + document.body.appendChild(el); + } + +} +``` + +### Option 3: Load Backbone app using production build + + +```js +import singleSpaBackbone from '@emtecinc/single-spa-backbone'; + +const backBoneLifecycles = singleSpaBackbone({ + BasePath: 'appBasePath', + App: + { + AppPath: 'src/app' + }, + DomElementSetter: domElementSetter +}); + +export const bootstrap = backBoneLifecycles.bootstrap; + +export const mount = backBoneLifecycles.mount; + +export const unmount = backBoneLifecycles.unmount; + + +function domElementSetter() { + + //use the same element id to render into, in the backbone app + let el = document.getElementById('shell-container'); + if (!el) { + el = document.createElement('div'); + el.id = 'shell-container'; + document.body.appendChild(el); + } + +} +``` + + +## Options + +All options are passed to single-spa-backbone via the `userOptions` parameter when calling `singleSpaBackbone(userOptions)`. The following properties are available: + +* `BasePath` (required) : The base path of the backbone application. Mostly it will be the public path from where the app is server and other paths will be relative to this. This parameter expects a string type. +optional + +* `AppWithRequire` (required) : This parameter takes an object and expects below properties: + * `IsDataMain` (optional) : This parameter takes a boolean value and is used to specify whether require js will use `data-main` to load the app. + * `AppPath` (required) : This parameter takes a string value and specifies the path of the JavaScript file, which is entry point of your application and will be booted up using RequireJs. The path is relative to BasePath. + * `RequireJsPath` (required) : This parameter takes a string value and takes the path of the RequireJs file and is relative to BasePath. + * `DependenciesJsPaths` (optional) : This is an optional parameter takes an array of strings. It can be used to optionally provide a list of JavaScript paths which you want to load in the browser. + +* `AppWithBackboneJs` (optional) : This parameter takes an object and expects below properties: + * `AppPath` (required) : This parameter takes a string value and specifies the path of the JavaScript file, which is entry point of your application and will be booted up using Backbone Js. The path is relative to BasePath. + * `BackboneJsPath` (required) : This parameter takes a string value and takes the path of the Backbone Js file and is relative to BasePath. + * `DependenciesJsPaths` (optional) : This is an optional parameter takes an array of strings. It can be used to optionally provide a list of JavaScript paths which you want to load in the browser. + +* `App` (optional) : This parameter takes an object and expects below properties: + * `AppPath` (required) : This parameter takes a string value and specifies the path of the JavaScript file, which is the production build of your backbone application. The path is relative to BasePath. + +### Note : Out of AppWithRequire, AppWithBackboneJs and App only one is required + +* `DomElementSetter` (optional) : This is an optional parameter and can be mostly used to create a dom element, whose id can be later used in the backbone app to load the application. However, you can freely use this callback for any other purpose. It is called before anything else. \ No newline at end of file diff --git a/versioned_docs/version-6.x/ecosystem-css.md b/versioned_docs/version-6.x/ecosystem-css.md new file mode 100644 index 000000000..7d961f36c --- /dev/null +++ b/versioned_docs/version-6.x/ecosystem-css.md @@ -0,0 +1,371 @@ +--- +id: ecosystem-css +title: CSS +sidebar_label: CSS +--- + +In a microfrontends architecture, it's important to have both shared CSS and microfrontend-specific CSS. There should only be one copy of all shared CSS, and CSS specific to a microfrontend should be scoped so that class names do not collide between microfrontends. + +## Shared CSS + +It is best for both performance and developer experience to have some shared CSS. Often, the shared CSS is part of a "styleguide" or "design system." + +Sometimes the design system is created in-house by a company, and other times it's an open source design system that is available on npm (Material UI, Bootstrap, Semantic UI, etc). For both cases, it's important that there is only a single copy of the CSS on the page at any time. When using [the recommended setup](/docs/recommended-setup), this is accomplished by following [the techniques in this documentation](/docs/recommended-setup#sharing-with-import-maps). + +Besides sharing component styles, the styleguide or design system also usually includes CSS resets and utility classes. + +### In-House Design System + +Our recommendation for in-house design systems is to create a [utility microfrontend](/docs/module-types#utilities) (often named `@your-org-name/styleguide`). Contained within the utility microfrontend are shared CSS and Javascript components that are available for all other microfrontends to use. + +Other microfrontends can access shared Javascript components via [cross-microfrontend imports](/docs/recommended-setup#cross-microfrontend-imports), and apply shared, global CSS classes to their components in the normal way (`
`). + +Here are some examples: + +- https://github.com/react-microfrontends/styleguide +- https://github.com/vue-microfrontends/styleguide +- https://github.com/polyglot-microfrontends/styleguide + +The alternative to creating a utility microfrontend for your styleguide is to publish it to npm. The drawback to this approach is that it makes it easier to have duplicate copies of the styleguide, and also easier to have different versions of the styleguide. Npm packages are not independently deployable, nor are they singletons, but for a styleguide it's often desirable to have it centrally managed and can be deployed separately from the microfrontends that use them. + +### Third Party Design System + +When using a third-party design system, such as Material UI, Bootstrap, Semantic, etc, it is important that only one copy and version of the design system is loaded on the page. To accomplish this, here are two implementation options. + +1. Add the design system libraries to your SystemJS import map, then mark them as external ([full documentation](/docs/recommended-setup#sharing-with-import-maps)). Alternatively, do the equivalent with [module federation](/docs/recommended-setup#sharing-with-module-federation). +1. Create a utility microfrontend (often called `@your-org-name/styleguide`) that contains all shared CSS and Javascript components. Re-export the components from the design system so that all other microfrontends can access them via [cross microfrontend imports](/docs/recommended-setup#cross-microfrontend-imports) (`import { Button } from '@your-org-name/styleguide';`). + +Once the design system is properly shared, all its CSS and Javascript components will only be included one time on the web page. The code using the design system's components remains unchanged. + +### Global CSS versus shared Javascript components + +It's possible to share CSS via global CSS classes, Javascript components, or both. No method is clearly superior than others in every way, and you should choose an approach that fits your situation. + +Some organizations scope the CSS for their shared Javascript components as a way of ensuring that the look and feel requires that you use the Javascript components. However, other organizations choose to publish global CSS in addition to their Javascript components, to allow for additional flexibility in their look and feel and make it easier to support multiple frameworks. + +To share Javascript components, use [cross microfrontend imports](/docs/recommended-setup#cross-microfrontend-imports). + +### CSS Custom Properties + +Browsers support [CSS custom properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) (sometimes called CSS Variables), which facilitate sharing CSS between microfrontends in an easy way. Any CSS variable applied to the `:root` pseudoelement is accessible to any other microfrontend. + +```css +/* In your styleguide / design system */ +:root { + --blue: #0000FF; +} +``` + +```css +/* In an individual microfrontend */ +.settings { + color: var(--blue); +} +``` + +No extra configuration is needed for this to work, as this is built into the browser. + +## Scoped CSS + +For all CSS specific to a particular microfrontend or component, it is preferred to scope the CSS. In general, CSS classes are global by default, but "scoping" refers to encapsulating the CSS such that it only applies to one component or microfrontend. The code snippets below demonstrate some ways that this is possible: + +```css +/* + GLOBAL: this css class is not scoped + NOT RECOMMENDED + +
+*/ +.settings { + color: blue; +} + +/* + Scoped by suffixing all css classes with a unique hash. This is often done by build tools, + particularly CSS Modules via Webpack's css-loader (https://webpack.js.org/loaders/css-loader/). + +
+*/ +.settings-67f89dd87sf89ds { + color: blue; +} + +/* + Scoped by suffixing all CSS classes with a unique hash, and also adding a unique prefix + (such as the microfrontend name) to classes. This is a variant of the above, except it + ensures no collision of generated hashes. See the localIdentName option to css-loader + https://webpack.js.org/loaders/css-loader/#localidentname + +
+*/ +.app1__settings-67f89dd87sf89ds { + color: blue; +} + +/* + Scoped via data attribute. This can often be done automatically by build tools (including Vue CLI, Angular, Svelte). + Only one component or microfrontend adds this specific data attribute, effectively + making the settings class "scoped" to that microfrontend + +
+*/ +.settings[data-df65s76dfs] { + color: blue; +} + +/* + Scoped via container selector. Single-spa applications are generally wrapped in a + div that looks like this:
+ + We can make our CSS class only apply to one microfrontend by prefixing it with that id. + + Run CSS.escape("single-spa-application:@org-name/project-name"); in the browser console + to escape any special characters in the ID, to ensure that the container selector works. + +
+
+
+*/ +#single-spa-application\:\@org-name\/project-name .settings { + color: blue; +} +``` + +### UI Frameworks + +Many popular UI frameworks have scoping built-in, or large ecosystems of open source libraries that help with scoping: + +#### React + +React CSS is quite diverse, with hundreds of options. Here are a few popular options that each result in component-scoped CSS: + +- [CSS Modules](https://github.com/css-modules/css-modules) +- [Styled Components](https://styled-components.com/) +- [Emotion](https://emotion.sh/docs/introduction) + +Also, in the single-spa community created Kremling, which scopes CSS while also unmounting it from the DOM when the React component unmounts: + +- [Kremling](https://github.com/CanopyTax/kremling) + +#### Angular + +[Angular Component Styles](https://angular.io/guide/component-styles) are built into Angular and facilitate scoping CSS to a component (and therefore, to its containing microfrontend). + +#### Vue + +Vue [Single File Components (SFC)](https://vue-loader.vuejs.org/spec.html) have built-in support for [Scoped CSS](https://vue-loader.vuejs.org/guide/scoped-css.html). + +#### Svelte + +Svelte scopes CSS classes by default ([Docs](https://svelte.dev/tutorial/styling)). + +### PostCSS Prefix Selector + +[PostCSS](https://postcss.org/) is a build tool that processes your CSS. It's often used via Webpack with [postcss-loader](https://www.npmjs.com/package/postcss-loader). + +A particular PostCSS plugin called [postcss-prefix-selector](https://github.com/RadValentin/postcss-prefix-selector) can be very helpful to scope CSS to a microfrontend. +With single-spa, each application is wrapped in a `
`, which can be used as a prefix to all CSS classes and selectors. +Run `CSS.escape("single-spa-application:@org-name/project-name")` in the browser console to make sure the HTML id is escaped, then prefix it with `#` so that it matches the id. The resulting string is what you pass into postcss-prefix-selector. + +The example code above in the [Scoped CSS](#scoped-css) section shows the mechanics of how selector prefixing can accomplish scoping, and postcss-prefix-selector can do this automatically to +all of your CSS. Below is an example PostCSS configuration file: + +```js +// postcss.config.js +const prefixer = require('postcss-prefix-selector'); + +module.exports = { + plugins: [ + prefixer({ + prefix: "#single-spa-application\\:\\@org-name\\/project-name" + }) + ] +} +``` + +### Shadow DOM + +[Shadow DOM](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) is a browser API for scoping CSS. It is designed to be used by [Web Components](https://developer.mozilla.org/en-US/docs/Web/Web_Components), and is mentioned here as another viable option for scoping CSS. + +Below are some notes about Shadow DOM that may be relevant to microfrontends: + +- Shadow DOM prevents any global CSS from cascading into the Shadow Root, which means you can't easily have global, shared CSS. +- CSS custom properties from outside the Shadow Root can be used within the Shadow Root. +- The HTML elements within the Shadow DOM are not reachable by CSS selectors outside of the Shadow Root. +- Events that propagate from a Shadow Root are retargeted at each shadow boundary. + +## Lazy Loading + +"Loading" CSS refers to downloading the CSS by inserting a `` element into the DOM, or by downloading a Javascript file that inserts a `` element into the DOM. + +"Lazy Loading" refers to only inserting the `` or `