Skip to content

Commit

Permalink
Merge pull request #3586 from abpframework/feat/3585
Browse files Browse the repository at this point in the history
Added a Method for Removing Content to DomInsertionService
  • Loading branch information
mehmet-erim authored Apr 14, 2020
2 parents 54ec92f + 58a98fe commit 6e97836
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 16 deletions.
53 changes: 48 additions & 5 deletions docs/en/UI/Angular/Dom-Insertion-Service.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,19 @@ class DemoComponent {
constructor(private domInsertionService: DomInsertionService) {}

ngOnInit() {
this.domInsertionService.insertContent(
const scriptElement = this.domInsertionService.insertContent(
CONTENT_STRATEGY.AppendScriptToBody('alert()')
);
}
}
```

In the example above, `<script>alert()</script>` element will place at the **end** of `<body>`.
In the example above, `<script>alert()</script>` element will place at the **end** of `<body>` and `scriptElement` will be an `HTMLScriptElement`.

Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy.

> Important Note: `DomInsertionService` does not insert the same content twice. In order to add a content again, you first should remove the old content using `removeContent` method.
### How to Insert Styles

If you pass a `StyleContentStrategy` instance as the first parameter of `insertContent` method, the `DomInsertionService` will create a `<style>` element with given `content` and place it in the designated DOM position.
Expand All @@ -60,27 +62,68 @@ class DemoComponent {
constructor(private domInsertionService: DomInsertionService) {}

ngOnInit() {
this.domInsertionService.insertContent(
const styleElement = this.domInsertionService.insertContent(
CONTENT_STRATEGY.AppendStyleToHead('body {margin: 0;}')
);
}
}
```

In the example above, `<style>body {margin: 0;}</style>` element will place at the **end** of `<head>`.
In the example above, `<style>body {margin: 0;}</style>` element will place at the **end** of `<head>` and `styleElement` will be an `HTMLStyleElement`.

Please refer to [ContentStrategy](./Content-Strategy.md) to see all available content strategies and how you can build your own content strategy.

> Important Note: `DomInsertionService` does not insert the same content twice. In order to add a content again, you first should remove the old content using `removeContent` method.
### How to Remove Inserted Scripts & Styles

If you pass the inserted `HTMLScriptElement` or `HTMLStyleElement` element as the first parameter of `removeContent` method, the `DomInsertionService` will remove the given element.

```js
import { DomInsertionService, CONTENT_STRATEGY } from '@abp/ng.core';

@Component({
/* class metadata here */
})
class DemoComponent {
private styleElement: HTMLStyleElement;

constructor(private domInsertionService: DomInsertionService) {}

ngOnInit() {
this.styleElement = this.domInsertionService.insertContent(
CONTENT_STRATEGY.AppendStyleToHead('body {margin: 0;}')
);
}

ngOnDestroy() {
this.domInsertionService.removeContent(this.styleElement);
}
}
```

In the example above, `<style>body {margin: 0;}</style>` element **will be removed** from `<head>` when the component is destroyed.

## API

### insertContent

```js
insertContent(contentStrategy: ContentStrategy): void
insertContent<T extends HTMLScriptElement | HTMLStyleElement>(
contentStrategy: ContentStrategy<T>,
): T
```

- `contentStrategy` parameter is the primary focus here and is explained above.
- returns `HTMLScriptElement` or `HTMLStyleElement` based on given strategy.

### removeContent

```js
removeContent(element: HTMLScriptElement | HTMLStyleElement): void
```

- `element` parameter is the inserted `HTMLScriptElement` or `HTMLStyleElement` element, which was returned by `insertContent` method.

## What's Next?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,23 @@ import { generateHash } from '../utils';
export class DomInsertionService {
readonly inserted = new Set<number>();

insertContent(contentStrategy: ContentStrategy) {
insertContent<T extends HTMLScriptElement | HTMLStyleElement>(
contentStrategy: ContentStrategy<T>,
): T {
const hash = generateHash(contentStrategy.content);

if (this.inserted.has(hash)) return;

contentStrategy.insertElement();
const element = contentStrategy.insertElement();
this.inserted.add(hash);

return element;
}

removeContent(element: HTMLScriptElement | HTMLStyleElement) {
const hash = generateHash(element.textContent);
this.inserted.delete(hash);

element.parentNode.removeChild(element);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ export abstract class ContentStrategy<T extends HTMLScriptElement | HTMLStyleEle

abstract createElement(): T;

insertElement() {
insertElement(): T {
const element = this.createElement();

this.contentSecurityStrategy.applyCSP(element);
this.domStrategy.insertElement(element);

return element;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {
CONTENT_SECURITY_STRATEGY,
CONTENT_STRATEGY,
StyleContentStrategy,
ScriptContentStrategy,
DOM_STRATEGY,
CONTENT_SECURITY_STRATEGY,
ScriptContentStrategy,
StyleContentStrategy,
} from '../strategies';
import { uuid } from '../utils';

Expand All @@ -26,8 +26,8 @@ describe('StyleContentStrategy', () => {
domStrategy.insertElement = jest.fn((el: HTMLScriptElement) => {}) as any;

const strategy = new StyleContentStrategy('', domStrategy, contentSecurityStrategy);
const element = strategy.createElement();
strategy.insertElement();
strategy.createElement();
const element = strategy.insertElement();

expect(contentSecurityStrategy.applyCSP).toHaveBeenCalledWith(element);
expect(domStrategy.insertElement).toHaveBeenCalledWith(element);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('DomInsertionService', () => {

beforeEach(() => (spectator = createService()));

afterEach(() => styleElements.forEach(element => element.remove()));
afterEach(() => (document.head.innerHTML = ''));

describe('#insertContent', () => {
it('should be able to insert given content', () => {
Expand All @@ -19,6 +19,11 @@ describe('DomInsertionService', () => {
expect(styleElements[0].textContent).toBe('.test {}');
});

it('should set a hash for the inserted content', () => {
spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}'));
expect(spectator.service.inserted.has(1437348290)).toBe(true);
});

it('should insert only once', () => {
expect(spectator.service.inserted.has(1437348290)).toBe(false);

Expand All @@ -37,9 +42,25 @@ describe('DomInsertionService', () => {
expect(spectator.service.inserted.has(1437348290)).toBe(true);
});

it('should be able to insert given content', () => {
spectator.service.insertContent(CONTENT_STRATEGY.AppendStyleToHead('.test {}'));
it('should return inserted element', () => {
const element = spectator.service.insertContent(
CONTENT_STRATEGY.AppendStyleToHead('.test {}'),
);
expect(element.tagName).toBe('STYLE');
});
});

describe('#removeContent', () => {
it('should remove inserted element and the hash for the content', () => {
expect(document.head.querySelector('style')).toBeNull();
const element = spectator.service.insertContent(
CONTENT_STRATEGY.AppendStyleToHead('.test {}'),
);
expect(spectator.service.inserted.has(1437348290)).toBe(true);

spectator.service.removeContent(element);
expect(spectator.service.inserted.has(1437348290)).toBe(false);
expect(document.head.querySelector('style')).toBeNull();
});
});
});

0 comments on commit 6e97836

Please sign in to comment.