Skip to content

Commit

Permalink
feat(styles): support external stylesheets #32
Browse files Browse the repository at this point in the history
Fixes #32
  • Loading branch information
asyncLiz committed Aug 23, 2018
1 parent e108c32 commit 13b4cad
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 75 deletions.
66 changes: 30 additions & 36 deletions shadycss/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,47 +53,40 @@ import '@polymer/paper-button/paper-button';
export class AppComponent {}
```

## IE11 Issue
## `@apply` mixins

Initial CSS custom properties must be defined on an `html` selector in a `ViewEncapsulation.None` component.

## Limitations

### External Stylesheets

This module cannot provide polyfill support for external stylesheets or any styles defined in the `angular.json` or `.angular-cli.json` `"styles"` array. A recommended workaround is to use `ViewEncapsulation.None` on the app's root component and use its component styles as "global" styles.
If using the deprecated `@apply` mixin proposal, import `ShadyCSSModule.usingApply()` instead.

```ts
import { Component, ViewEncapsulation } from '@angular/core';
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ShadyCSSModule } from '@codebakery/origami/shadycss';
import { AppComponent } from './app.component';

@Component({
selector: 'app-root',
templateUrl: './app-root.component.html',
// global.css may use CSS custom properties that will affect all children of
// the root element.
styleUrls: ['./global.css'],
encapsulation: ViewEncapsulation.None
@NgModule({
imports: [BrowserModule, ShadyCSSModule.usingApply()],
declarations: [AppComponent],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
bootstrap: [AppComponent]
})
export class AppRootComponent {}
export class AppModule {}
```

### IE11 New Properties
## Limitations

### Defining CSS Custom Properties

Properties that are defined in a Shadow DOM template (such as those provided by custom elements) will work with ShadyCSS in IE11. However, _newly_ defined properties in an Angular template must be defined with an `html` selector in a `ViewEncapsulation.None` component.
_Newly_ defined CSS custom properties must be defined in a root `html` or `:root` selector. It is recommended to define new properties in global CSS instead of component styles.

```ts
import { Component, ViewEncapsulation } from '@angular/core';
import { Component } from '@angular/core';

@Component({
selector: 'app-component',
styles: [
`
html {
/*
--my-color is not defined by an external element, it is our own
CSS property. To work with ShadyCSS, it must be defined on an html
root selector.
*/
/* This does not work */
:host {
--my-color: blue;
}
Expand All @@ -103,18 +96,19 @@ import { Component, ViewEncapsulation } from '@angular/core';
`
],
template: `
<div>I'm blue!</div>
`,
/*
In order for the above html selector to work, the component must have
ViewEncapsulation.None, since Angular cannot encapsulate the root html
element.
*/
encapsulation: ViewEncapsulation.None
<div>I'm not blue :(</div>
`
})
export class AppComponent {}
```

See [this issue](https://github.com/webcomponents/shadycss/issues/75) for more information.
The example `--my-color` property should be defined in a global CSS stylesheet.

Like the above limitation, it is recommended that all custom CSS properties an app defines should be declared in a root component that does not have view encapsulation.
```css
html {
/* AppComponent's <div> will now be blue */
--my-color: blue;
}
```

See [this issue](https://github.com/webcomponents/shadycss/issues/75) for more information.
1 change: 1 addition & 0 deletions shadycss/public_api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './src/models';
export * from './src/process-stylesheets';
export * from './src/shadycss.module';
export * from './src/shared-styles-host';
2 changes: 2 additions & 0 deletions shadycss/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export namespace ShadyCSS {
}

export interface ShadyCSS {
nativeCss: boolean;
nativeShadow: boolean;
CustomStyleInterface?: ShadyCSS.CustomStyleInterface;
}

Expand Down
55 changes: 55 additions & 0 deletions shadycss/src/process-stylesheets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { InjectionToken } from '@angular/core';

/**
* By default, Origami will not parse or register styles with ShadyCSS if the
* platform supports native CSS custom properties. However, ShadyCSS also
* supports the deprecated `@apply` mixin proposal. If a project is using
* `@apply` in CSS, this token should be provided with a true value.
*/
export const USING_APPLY = new InjectionToken<boolean>('usingApply');

/**
* Processes all current document stylesheets added by Angular and registers
* them with ShadyCSS.
*
* This function will also parse external `<link>` stylesheets if native
* CSS custom properties are not supported, or if `usingApply` is set to true.
*
* @param usingApply if true, parse stylesheets regardless of native support,
* since no browser supports `@apply` natively
*/
export function processStylesheets(usingApply?: boolean) {
const CustomStyleInterface =
window.ShadyCSS && window.ShadyCSS.CustomStyleInterface;
if (CustomStyleInterface && (!window.ShadyCSS.nativeCss || usingApply)) {
Array.from(document.styleSheets).forEach(stylesheet => {
const node = stylesheet.ownerNode;
if (isStyleNode(node) && !node.hasAttribute('scope')) {
CustomStyleInterface.addCustomStyle(node);
} else if (stylesheet.href && !(<any>stylesheet)._fetching) {
(<any>stylesheet)._fetching = true;
const xhr = new XMLHttpRequest();
xhr.addEventListener('load', () => {
const style = document.createElement('style');
style.innerHTML = xhr.responseText;
node.parentNode!.insertBefore(style, node);
node.parentNode!.removeChild(node);
CustomStyleInterface.addCustomStyle(style);
});

xhr.open('GET', stylesheet.href);
xhr.send();
}
});
}
}

/**
* Returns true if the provided node is a `<style>` node.
*
* @param node the node to test
* @returns true if the node is a `<style>` node
*/
export function isStyleNode(node: Node): node is HTMLStyleElement {
return node.localName === 'style';
}
25 changes: 23 additions & 2 deletions shadycss/src/shadycss.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NgModule } from '@angular/core';
import { ModuleWithProviders, NgModule } from '@angular/core';
import { ɵDomSharedStylesHost as DomSharedStylesHost } from '@angular/platform-browser';
import { WebComponentsReadyModule } from '@codebakery/origami/polyfills';
import { USING_APPLY } from './process-stylesheets';
import { ShadyCSSSharedStylesHost } from './shared-styles-host';

/**
Expand All @@ -10,11 +11,31 @@ import { ShadyCSSSharedStylesHost } from './shared-styles-host';
* The ShadyCSS polyfill must be imported separately. It may be imported from
* `@webcomponents/shadycss/entrypoints/custom-style-interface.js`
* or `@polymer/polymer/lib/elements/custom-style.js`.
*
* If using the deprecated `@apply` mixin proposal, import
* `ShadyCSSModule.usingApply()` instead.
*/
@NgModule({
imports: [WebComponentsReadyModule],
providers: [
{ provide: DomSharedStylesHost, useClass: ShadyCSSSharedStylesHost }
]
})
export class ShadyCSSModule {}
export class ShadyCSSModule {
/**
* Forces Origami to register all stylesheets with ShadyCSS regardless of
* native CSS custom property support. Import `ShadyCSSModule.usingApply()`
* when using `@apply` mixins.
*/
static usingApply(): ModuleWithProviders {
return {
ngModule: ShadyCSSModule,
providers: [
{
provide: USING_APPLY,
useValue: true
}
]
};
}
}
46 changes: 9 additions & 37 deletions shadycss/src/shared-styles-host.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Inject } from '@angular/core';
import { Inject, Optional } from '@angular/core';
import {
DOCUMENT,
ɵDomSharedStylesHost as DomSharedStylesHost
} from '@angular/platform-browser';
import { USING_APPLY, processStylesheets } from './process-stylesheets';

// First group is incorrect escape backslash, second group is rest of mixin detection
const MIXIN_REGEX = /(?:\\)(--\w[\w-_]*:\s*{[^}]*})(;)?/g;
Expand All @@ -13,11 +14,13 @@ const MIXIN_REGEX = /(?:\\)(--\w[\w-_]*:\s*{[^}]*})(;)?/g;
* properties in Angular styles on browsers that do not support them.
*/
export class ShadyCSSSharedStylesHost extends DomSharedStylesHost {
protected hostNodes = new Set<Node>();

constructor(@Inject(DOCUMENT) document: Document) {
constructor(
@Inject(DOCUMENT) document: Document,
@Optional()
@Inject(USING_APPLY)
private usingApply?: boolean
) {
super(document);
this.hostNodes.add(document.head);
}

addStyles(styles: string[]) {
Expand All @@ -43,39 +46,8 @@ export class ShadyCSSSharedStylesHost extends DomSharedStylesHost {
super.addStyles(styles.map(style => style.replace(MIXIN_REGEX, '$1')));
}

addHost(hostNode: Node) {
super.addHost(hostNode);
this.hostNodes.add(hostNode);
this.addStylesToShadyCSS();
}

onStylesAdded(additions: Set<string>) {
super.onStylesAdded(additions);
this.addStylesToShadyCSS();
}

removeHost(hostNode: Node) {
super.removeHost(hostNode);
this.hostNodes.delete(hostNode);
}

protected addStylesToShadyCSS() {
if (window.ShadyCSS && window.ShadyCSS.CustomStyleInterface) {
this.hostNodes.forEach(hostNode => {
Array.from(hostNode.childNodes).forEach(childNode => {
if (
this.isStyleElement(childNode) &&
!childNode.hasAttribute('scope')
) {
// ShadyCSS will handle <style> elements that have already been registered
window.ShadyCSS!.CustomStyleInterface!.addCustomStyle(childNode);
}
});
});
}
}

protected isStyleElement(element: any): element is HTMLStyleElement {
return element.tagName === 'STYLE';
processStylesheets(this.usingApply);
}
}

0 comments on commit 13b4cad

Please sign in to comment.