Skip to content

Commit

Permalink
feat: state-model as observable
Browse files Browse the repository at this point in the history
  • Loading branch information
nattallius committed Jun 14, 2021
1 parent 052b965 commit 48caeeb
Show file tree
Hide file tree
Showing 11 changed files with 52 additions and 78 deletions.
31 changes: 7 additions & 24 deletions src/core/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import {attr, ESLBaseElement} from '@exadel/esl/modules/esl-base-element/core';
import {UIPRoot} from './root';
import {bind} from '@exadel/esl/modules/esl-utils/decorators/bind';
import {EventUtils} from '@exadel/esl/modules/esl-utils/dom/events';

/**
* Base class for UI Playground plugins.
Expand All @@ -17,22 +15,23 @@ export abstract class UIPPlugin extends ESLBaseElement {
@attr() public label: string;

/** @returns UIPRoot - playground root element */
protected get root(): UIPRoot | null{
protected get root(): UIPRoot | null {
return this._root;
}
protected set root(root: UIPRoot | null) {
this._root && this._root.removeEventListener('state:change', this._onRootStateChange);
this._root?.removeStateListener(this._onRootStateChange);
this._root = root;
this._root && this._root.addEventListener('state:change', this._onRootStateChange);
this._root?.addStateListener(this._onRootStateChange);
}

protected connectedCallback() {
super.connectedCallback();
this.classList.add('uip-plugin');
this.root = this.closest(`${UIPRoot.is}`) as UIPRoot;
this.root && this.handleChange();
this.root && this._onRootStateChange();
}
protected disconnectedCallback() {
this._root?.removeStateListener(this._onRootStateChange);
this.root = null;
super.disconnectedCallback();
}
Expand All @@ -41,22 +40,6 @@ export abstract class UIPPlugin extends ESLBaseElement {
if (attrName === 'label') this.setAttribute('aria-label', newVal);
}

/** Handles root state change event. Delegate non self triggered events to the {@link handleChange}*/
@bind
protected _onRootStateChange(e: CustomEvent) {
if (e.detail.origin === this) return;
this.handleChange();
}

/** Dispatch change state */
protected dispatchChange(markup: string) {
const detail = {
markup,
type: (this.constructor as typeof UIPPlugin).is
};
EventUtils.dispatch(this, 'request:change', {detail});
}

/** Handles root state change. Will not process self triggered changes */
protected abstract handleChange(): void;
/** Handles root state change*/
protected abstract _onRootStateChange(): void;
}
35 changes: 7 additions & 28 deletions src/core/root.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,24 @@
import {bind} from '@exadel/esl';
import {ObserverCallback} from '@exadel/esl';
import {ESLBaseElement} from '@exadel/esl/modules/esl-base-element/core';
import {EventUtils} from '@exadel/esl/modules/esl-utils/dom/events';
import {UIPStateModel} from '../utils/state-model/state-model';
import {UIPStateModel} from './state-model';

export class UIPRoot extends ESLBaseElement {
public static is = 'uip-root';
private _model: UIPStateModel;
private _model = new UIPStateModel();

public get model(): UIPStateModel {
return this._model;
}

public set model(model: UIPStateModel) {
this._model = model;
}

protected connectedCallback() {
super.connectedCallback();
this.bindEvents();
this.model = new UIPStateModel();
}

protected disconnectedCallback() {
this.unbindEvents();
super.disconnectedCallback();
}

protected bindEvents() {
this.addEventListener('request:change', this._onStateChange);
}

protected unbindEvents() {
this.removeEventListener('request:change', this._onStateChange);
public addStateListener(listener: ObserverCallback) {
this._model.addListener(listener);
}

@bind
protected _onStateChange(e: CustomEvent) {
this.model.html = e.detail.markup;
const detail = Object.assign({
origin: e.target
}, e.detail);
EventUtils.dispatch(this, 'state:change', {detail});
public removeStateListener(listener: ObserverCallback) {
this._model.removeListener(listener);
}
}
28 changes: 23 additions & 5 deletions src/utils/state-model/state-model.ts → src/core/state-model.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
export class UIPStateModel {
import {Observable} from '@exadel/esl';

export class UIPStateModel extends Observable {
protected root: Element;

constructor() {
super();
this.root = new DOMParser().parseFromString('', 'text/html').body;
}

public set html(markup: string) {
this.root = new DOMParser().parseFromString(markup, 'text/html').body;
const root = new DOMParser().parseFromString(markup, 'text/html').body;
if (root.innerHTML !== this.root.innerHTML) {
this.root = root;
this.fire({markup: this.html});
}
}

public get html(): string {
return this.root? this.root.innerHTML : '';
return this.root ? this.root.innerHTML : '';
}

public getAttribute(target: string, name: string): (string | null)[] {
Expand All @@ -15,19 +26,26 @@ export class UIPStateModel {

public setAttribute(target: string, name: string, value: string | boolean): void {
const elements = Array.from(this.root.querySelectorAll(target));

if (!elements.length) return;

if (typeof value === 'string') {
elements.forEach(el => el.setAttribute(name, value));
} else {
elements.forEach(el => value ? el.setAttribute(name, '') : el.removeAttribute(name));
}

this.fire({markup: this.html});
}

public transformAttribute(target: string, name: string, transform: (current: string | null) => string | null) {
Array.from(this.root.querySelectorAll(target)).forEach(el => {
const elements = Array.from(this.root.querySelectorAll(target));
if (!elements.length) return;

elements.forEach(el => {
const transformed = transform(el.getAttribute(name));
transformed === null ? el.removeAttribute(name) : el.setAttribute(name, transformed);
});

this.fire({markup: this.html});
}
}
6 changes: 4 additions & 2 deletions src/editor/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ export class UIPEditor extends UIPPlugin {
}

protected onChange = debounce(() => {
this.dispatchChange(this.editor.getValue());
this.root!.model.html = this.editor.getValue();
}, 1000);

@bind
protected handleChange(): void {
protected _onRootStateChange(): void {
const markup = this.root!.model.html;
if (this.editor && markup === this.editor.getValue()) return; // check for self triggered changes

const $inner = document.createElement('div');
$inner.classList.add('uip-editor-inner');

Expand Down
2 changes: 1 addition & 1 deletion src/options/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class UIPOptions extends UIPPlugin {
this.unbindEvents();
}

protected handleChange() {}
protected _onRootStateChange() {}

protected bindEvents() {
this.addEventListener('click', this._onOptionChange);
Expand Down
2 changes: 1 addition & 1 deletion src/preview/preview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export class UIPPreview extends UIPPlugin {
}

@bind
protected handleChange(): void {
protected _onRootStateChange(): void {
this.$inner.innerHTML = this.root!.model.html;
this.innerHTML = '';
this.appendChild(this.$inner);
Expand Down
2 changes: 1 addition & 1 deletion src/settings/setting/bool-setting/bool-setting.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {attr} from '@exadel/esl/modules/esl-base-element/core';

import {UIPSetting} from '../setting';
import {UIPStateModel} from '../../../utils/state-model/state-model';
import {UIPStateModel} from '../../../core/state-model';
import TokenListUtils from '../../../utils/array-utils/token-list-utils';
import {WARN} from '../../../utils/warn-messages/warn';

Expand Down
2 changes: 1 addition & 1 deletion src/settings/setting/select-setting/select-setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {ESLSelect} from '@exadel/esl';
import {generateUId} from '@exadel/esl/modules/esl-utils/misc/uid';

import {UIPSetting} from '../setting';
import {UIPStateModel} from '../../../utils/state-model/state-model';
import {UIPStateModel} from '../../../core/state-model';
import TokenListUtils from '../../../utils/array-utils/token-list-utils';
import {WARN} from '../../../utils/warn-messages/warn';

Expand Down
2 changes: 1 addition & 1 deletion src/settings/setting/setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {attr, ESLBaseElement} from '@exadel/esl/modules/esl-base-element/core';
import {EventUtils} from '@exadel/esl/modules/esl-utils/dom/events';
import {bind} from '@exadel/esl/modules/esl-utils/decorators/bind';

import {UIPStateModel} from '../../utils/state-model/state-model';
import {UIPStateModel} from '../../core/state-model';
import {UIPSettings} from '../settings';
import {WARN} from '../../utils/warn-messages/warn';

Expand Down
11 changes: 2 additions & 9 deletions src/settings/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,16 @@ export class UIPSettings extends UIPPlugin {

protected _onSettingChanged(e: any) {
if (!this.root) return;

(e.target as UIPSetting).applyTo(this.root.model);
this.settings.forEach(setting => setting.updateFrom(this.root!.model));
this.dispatchChange(this.root.model.html);
}

protected get settings(): UIPSetting[] {
return Array.from(this.getElementsByClassName(UIPSetting.is)) as UIPSetting[];
}

@bind
protected handleChange(): void {
const model = this.root!.model;

for (const setting of this.settings) {
setting.updateFrom(model);
}
protected _onRootStateChange(): void {
this.settings.forEach(setting => setting.updateFrom(this.root!.model));
}
}

9 changes: 4 additions & 5 deletions src/snippets/snippets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class UIPSnippets extends UIPPlugin {
this.removeEventListener('click', this._onClick);
}

protected handleChange() {}
protected _onRootStateChange() {}

protected render(): void {
const $inner = document.createElement('div');
Expand All @@ -66,7 +66,7 @@ export class UIPSnippets extends UIPPlugin {
this.innerHTML = $inner.outerHTML + $scroll.outerHTML;
}

protected createListItem(snippet : HTMLTemplateElement) {
protected createListItem(snippet: HTMLTemplateElement) {
const li = document.createElement('li');
li.classList.add('snippets-list-item');
const label = snippet.getAttribute('label');
Expand All @@ -78,9 +78,8 @@ export class UIPSnippets extends UIPPlugin {

protected applyActive(): void {
const tmpl = this.$active?.querySelector('template[uip-snippet]');
if (!tmpl) return;

this.dispatchChange(tmpl.innerHTML);
if (!tmpl || !this.root) return;
this.root.model.html = tmpl.innerHTML;
}

@bind
Expand Down

0 comments on commit 48caeeb

Please sign in to comment.