Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(framework): dynamic custom elements scoping #2091

Merged
merged 35 commits into from
Aug 19, 2020
Merged

Conversation

vladitasev
Copy link
Contributor

@vladitasev vladitasev commented Aug 13, 2020

❗ What changes for component developers

Important information for component developers:

  • All components may now exist with suffixed names, f.e. ui5-button-mytest instead of ui5-button if the application calls setCustomElementsScopingSuffix("mytest"); so we can no longer rely on the tag name.
  • All components always have an attribute with the name of the original tag, f.e. <ui5-button-mytest icon="add" ui5-button>Hello world</ui5-button-mytest> and this attribute can be used reliably.
  • In the .js files never write selectors by tag for the custom elements, only by the new attribute: f.e.

this.shadowRoot.querySelector("ui5-list")
✔️ this.shadowRoot.querySelector("[ui5-list]")

  • In the .css files never write selectors by tag for the custom elements, only by the new attribute: f.e.

ui5-button ui5-icon {}
✔️ [ui5-button] [ui5-icon] {}

  • There are no changes to the way you write .hbs files - continue to use the tags normally, f.e. <ui5-icon>. The template processing logic will replace the tags with the suffixed tags at runtime. However, for this to work, you should properly declare all dependencies for your component (see the next point):

  • In UI5Element.js there is a new dependencies static getter, f.e. the DatePicker.js implementation is:

static get dependencies() {
	return [
		Icon,
		ResponsivePopover,
		Calendar,
		Input,
		Button,
	];
}

Add all the component's dependencies there, instead of inside onDefine.

  • Dependencies are now defined automatically by the framework, based on the information in the dependencies static getter when the component itself is defined, it is no longer necessary to define them manually in onDefine.
    Only use onDefine for other resources, f.e.
static async onDefine() {
	await fetchI18nBundle("@ui5/webcomponents");
}

or

static async onDefine() {
	await Promise.all([
		fetchCldr(getLocale().getLanguage(), getLocale().getRegion(), getLocale().getScript()),
		fetchI18nBundle("@ui5/webcomponents"),
	]);
}
  • There is a new script command: startWithScopе that is the same as start, but also creates scoped bundles and scoped test pages for easier testing.

Note: You should continue working with yarn start as before, yarn startWithScopе is slower and creates 8 bundles instead of 4, so it is intended for use only when you are specifically testing the scoping functionality

yarn startWithScopе

and then:

----- Normal pages Scoped pages
main http://localhost:8080/test-resources/pages/ http://localhost:8080/test-resources/scoped/
fiori http://localhost:8081/test-resources/pages/ http://localhost:8081/test-resources/scoped/

Goal of the change

  • Allow several runtimes of UI5 Web Components to completely co-exist on a single page, even on HTML-tag level
  • Allow micro-frontend/library authors to ensure no other component on the target HTML page has already upgraded the custom elements.

Example:

<body>

	<!-- Application code, using version 1.0.1 -->
	<div class="application-container">
		<ui5-title>This is the new app with many third-party extensions!</ui5-title>
		<ui5-card>
			<ui5-button>Hello</ui5-button>
			<ui5-input></ui5-input>
		</ui5-card>
	</div>

	<!-- Code inserted by "common help" library, using version 1.2.0 -->
	<div class="common-help-container">
		<ui5-button-chlp new-button-prop="1">Help Menu</ui5-button-chlp>
		<ui5-input-chlp value="Type your question"></ui5-input-chlp>
	</div>

	<!-- Code inserted by "global footer" library, using version @latest -->
	<footer class="global-footer-container">
		<global-footer-main>
			<ui5-button-glob-foot new-button-prop="2" even-newer-button-prop="3">Profile</ui5-button-glob-foot>
			<ui5-link-glob-foot>Contacts</ui5-link-glob-foot>
		</global-footer-main>
	</footer>

</body>

Integration scenario: on the same page 3 separately bundled and separately loaded pieces of code co-exist:

  • App code, using the oldest version (which upgraded the ui5-card, ui5-button and ui5-input tags already)
  • Code of "common help" library based on version 1.2.0, using scoping suffix "chlp"
  • Code of "global footer" library based on latest version, using scoping suffix "glob-foot"

The 2 libraries use scoping suffixes so they can use newer features of the ui5-button for example (properties such as newButtonProp and evenNewerButtonProp that are not found in older versions).

For applications

Applications, but mostly micro-frontend authors, have new APIs available:

import { setCustomElementsScopingSuffix, setCustomElementsScopingRules } from "@ui5/webcomponents-base/dist/CustomElementsScope.js";
setCustomElementsScopingSuffix("demo");
setCustomElementsScopingRules({include: [/^ui5-/], exclude: [/^ui5-my-/, /-test-/]});

that allow to scope all (or some) custom elements with the provided suffix. Then the usage becomes:

<ui5-button-demo>Hello</ui5-button-demo>

and ui5-button is no longer available, only ui5-button-demo.

Components that instantiate other components in the shadow root, also use the scoping suffix transitively:

image

For component developers

  • Using custom elements tag names in JS/CSS is not scoping-friendly and will lead to runtime errors. Use classes/ids preferably. When nothing else is possible, use the new special attribute with the name of the "pure" tag, for example:
<ui5-button-demo class="btn" icon="home" ui5-button></ui5-button-demo>
::slotted(ui5-button) {} // Not OK
::slotted([ui5-button]) {} // OK, safe for scoping
this.shadowRoot.querySelector("ui5-list") // Not OK
this.shadowRoot.querySelector("[ui5-list]") // OK, safe for scoping

image

  • A new dependencies static getter exists on framework level that lists all components that the current component depends on. This information is used for one-time template processing at runtime to replace their tag names with the suffixed ones.
  • It is no longer necessary to define dependencies in onDefine, this is now done by the framework.

Changes to lit-html logic

Until lit-html 2.0 is released with the support of static bindings, we are going to pre-process all strings once and replace tags by monkey-patching the html and svg functions. For that purpose, executing templates now requires also information about the custom tags that may be found in the template (and need to be suffixed) as well as the suffix itself. The whole logic is moved to a separate module.

Changes to bundle*.js files and package.json files

  • bundles are now optimized, no more code is copy/pasted. The result of the main project's bundle.esm.js file is now exported and reused by all other bundles (configuration and other global functions for the test pages).
  • components packages (main and fiori) now have a custom ui5 entry in package.json. This is used by the tools to analyze which packages export UI5 Web Components.
  • there is a new startWithScoping command available on top level and component-package level. It is the same as start, but also includes 2 more bundles for each package and a new set of test pages found in dist/test-resources/scoped/ (exact copy of dist/test-resources/pages/ but with the scoped bundles and scoped tags). This also includes a scope-lint task that scans the source files for scoping-unfriendly selectors (by tag name).

closes: #2080

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Create scoped custom elements for micro-frontend authors
2 participants