diff --git a/examples/basic/main.js b/examples/basic/main.js
index df831f3..92e059d 100644
--- a/examples/basic/main.js
+++ b/examples/basic/main.js
@@ -2,7 +2,8 @@ import { reactive, customElement, attribute, html } from 'pwc';
@customElement('child-element')
class Child extends HTMLElement {
- name = 'Child';
+ @reactive
+ accessor data = {};
@reactive
@attribute('data-class-name')
diff --git a/examples/condition/index.html b/examples/condition/index.html
new file mode 100644
index 0000000..fae5672
--- /dev/null
+++ b/examples/condition/index.html
@@ -0,0 +1,21 @@
+
+
+
+
+
+ Basic
+
+
+
+
+
+
+
+
diff --git a/examples/condition/main.js b/examples/condition/main.js
new file mode 100644
index 0000000..ae930f7
--- /dev/null
+++ b/examples/condition/main.js
@@ -0,0 +1,41 @@
+import { reactive, customElement, html } from 'pwc';
+
+@customElement('child-element')
+class ChildElement extends HTMLElement {
+ @reactive
+ accessor data = { foo: 0 }
+ get template() {
+ console.log('>>>');
+ return html`${this.data.foo}
`;
+ }
+}
+
+@customElement('custom-element')
+class CustomElement extends HTMLElement {
+ @reactive
+ accessor #condition = true;
+
+ @reactive
+ accessor #icon = '';
+
+ @reactive
+ accessor #data = { foo: 1 }
+
+
+ handleClick() {
+ console.log('click');
+ this.#condition = !this.#condition;
+ this.#icon += '!';
+ }
+
+ get template() {
+ const result = html`
+
Condition is ${this.#condition + ''}
+ ${this.#condition ? html`
True Condition${this.#icon}
` : html`
False Condition${this.#icon}
`}
+
+
`;
+
+ console.log(result);
+ return result;
+ }
+}
diff --git a/examples/condition/package.json b/examples/condition/package.json
new file mode 100644
index 0000000..19ad9cb
--- /dev/null
+++ b/examples/condition/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "basic",
+ "version": "0.0.0",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "devDependencies": {
+ "rollup": "^2.0.0",
+ "vite": "^2.7.2",
+ "vite-plugin-babel": "^1.0.0"
+ },
+ "dependencies": {
+ "pwc": "workspace:*"
+ },
+ "browserslist": "chrome > 60"
+}
diff --git a/examples/condition/vite.config.js b/examples/condition/vite.config.js
new file mode 100644
index 0000000..a8a7406
--- /dev/null
+++ b/examples/condition/vite.config.js
@@ -0,0 +1,33 @@
+import { defineConfig } from 'vite';
+import babel from 'vite-plugin-babel';
+
+export default defineConfig({
+ plugins: [
+ babel({
+ babelConfig: {
+ presets: [
+ [
+ '@babel/preset-env',
+ {
+ targets: {
+ chrome: 99,
+ },
+ modules: false,
+ },
+ ],
+ ],
+ plugins: [
+ [
+ '@babel/plugin-proposal-decorators',
+ {
+ version: '2021-12',
+ },
+ ],
+ '@babel/plugin-proposal-class-properties',
+ '@babel/plugin-proposal-class-static-block',
+ '@babel/plugin-proposal-private-methods'
+ ],
+ },
+ }),
+ ],
+});
diff --git a/examples/list/main.js b/examples/list/main.js
index 207fa2d..e0ac845 100644
--- a/examples/list/main.js
+++ b/examples/list/main.js
@@ -17,18 +17,18 @@ class CustomElement extends HTMLElement {
}
get template() {
- return html`
- ${html`${this.#list.map((item, index) => {
- if (item === 'item 2') {
- return null;
- }
- if (item === 'item 3') {
- return [1, 2, 3].map((insideItem) => {
- return html`
this.handleItemClick(index)}>inside list: ${insideItem}
`;
- });
- }
- return html`
this.handleItemClick(index)}>${item}
`;
- })}`}
-
`;
+ return html`
+ ${html`${this.#list.map((item, index) => {
+ if (item === 'item 2') {
+ return null;
+ }
+ if (item === 'item 3') {
+ return [1, 2, 3].map((insideItem) => {
+ return html`
this.handleItemClick(index)}>inside list: ${insideItem}
`;
+ });
+ }
+ return html`
this.handleItemClick(index)}>${item}
`;
+ })}`}
+
`;
}
}
diff --git a/jest.config.js b/jest.config.js
index 2f5af4d..fac9c6b 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,7 +1,14 @@
module.exports = {
coverageDirectory: './coverage/',
collectCoverage: true,
- collectCoverageFrom: ['packages/pwc/src/**/*.{js,ts}', 'packages/pwc-compiler/esm/**/*.{js,ts}', '!packages/**/*.d.ts', '!packages/**/type.ts', '!packages/*/src/index.{js,ts}'],
+ collectCoverageFrom: [
+ 'packages/pwc/src/**/*.{js,ts}',
+ 'packages/pwc-compiler/esm/**/*.{js,ts}',
+ '!packages/**/*.d.ts',
+ '!packages/**/type.ts',
+ '!packages/*/src/index.{js,ts}',
+ '!packages/*/src/utils/*.{js,ts}',
+ ],
coveragePathIgnorePatterns: ['/node_modules/'],
roots: ['/packages'],
testPathIgnorePatterns: ['/node_modules/', '/cjs/', '/esm/', '/es2017/', '/dist/', '.d.ts'],
diff --git a/packages/pwc/build.config.ts b/packages/pwc/build.config.ts
index e287d5f..ca526ba 100644
--- a/packages/pwc/build.config.ts
+++ b/packages/pwc/build.config.ts
@@ -1,7 +1,7 @@
import { defineConfig } from '@ice/pkg';
export default defineConfig({
- sourceMaps: 'inline',
+ sourceMaps: false,
transform: {
excludes: ['**/__tests__/**'],
diff --git a/packages/pwc/package.json b/packages/pwc/package.json
index ebc3ef3..2cdf3d6 100644
--- a/packages/pwc/package.json
+++ b/packages/pwc/package.json
@@ -4,7 +4,8 @@
"author": "Rax Team",
"homepage": "https://github.com/raxjs/pwc#readme",
"license": "MIT",
- "main": "esm/index.js",
+ "main": "cjs/index.js",
+ "module": "",
"files": [
"esm/",
"es2017/",
diff --git a/packages/pwc/src/constants.ts b/packages/pwc/src/constants.ts
index f72fbea..dcaf383 100644
--- a/packages/pwc/src/constants.ts
+++ b/packages/pwc/src/constants.ts
@@ -3,6 +3,8 @@ export const TEXT_COMMENT_DATA = '?pwc_t';
export const PLACEHOLDER_COMMENT_DATA = '?pwc_p';
export const enum ReactiveFlags {
RAW = '__p_raw__',
+ PROPERTY = '__p_property__',
+ IS_REACTIVE = '__p_is_reactive__',
}
export const TemplateString = 'templateString';
diff --git a/packages/pwc/src/elements/__tests__/HTMLElement.test.ts b/packages/pwc/src/elements/__tests__/HTMLElement.test.ts
index 82bcd93..ab39917 100644
--- a/packages/pwc/src/elements/__tests__/HTMLElement.test.ts
+++ b/packages/pwc/src/elements/__tests__/HTMLElement.test.ts
@@ -156,7 +156,8 @@ describe('Render HTMLElement', () => {
const container = document.getElementById('reactive-container');
container.click();
-
+ // @ts-ignore
+ expect(element._getChangedProperties()).toEqual(new Set(['#data', '#text', '#className']));
await nextTick();
expect(element.innerHTML).toEqual(
'hello? - jack!
',
@@ -264,32 +265,48 @@ describe('Render nested components', () => {
it('any reactive data should trigger the update of child components', async () => {
const parentBtn = document.getElementById('parent-btn');
- const childElement = document.getElementById('child-container');
- expect(childElement.innerHTML).toEqual(
+ const childContainer = document.getElementById('child-container');
+ expect(childContainer.innerHTML).toEqual(
'\n Hello - World -
\n ',
);
expect(mockChildFn).toBeCalledTimes(1);
+ const parentElement = element;
+ const childElement = document.getElementsByTagName('child-element')[0];
+ // @ts-ignore
+ expect(parentElement._getChangedProperties()).toEqual(new Set());
+ // @ts-ignore
+ expect(childElement._getChangedProperties()).toEqual(new Set());
+
// primity type
parentBtn.click();
+ // @ts-ignore
+ expect(parentElement._getChangedProperties()).toEqual(new Set(['#title']));
+
await nextTick();
- expect(childElement.innerHTML).toEqual(
+ expect(childContainer.innerHTML).toEqual(
'\n Hello! - World -
\n ',
);
expect(mockChildFn).toBeCalledTimes(2);
// object type
parentBtn.click();
+ // @ts-ignore
+ expect(parentElement._getChangedProperties()).toEqual(new Set(['#data']));
+
await nextTick();
- expect(childElement.innerHTML).toEqual(
+ expect(childContainer.innerHTML).toEqual(
'\n Hello! - World! -
\n ',
);
expect(mockChildFn).toBeCalledTimes(3);
// array type
parentBtn.click();
+ // @ts-ignore
+ expect(parentElement._getChangedProperties()).toEqual(new Set(['#items']));
+
await nextTick();
- expect(childElement.innerHTML).toEqual(
+ expect(childContainer.innerHTML).toEqual(
'\n Hello! - World! - 2
\n ',
);
expect(mockChildFn).toBeCalledTimes(4);
@@ -323,6 +340,65 @@ describe('Render nested components', () => {
);
expect(mockChildFn).toBeCalledTimes(6);
});
+
+ it('keep track of changed properties', async () => {
+ const mockFn1 = jest.fn();
+ const mockFn2 = jest.fn();
+ @customElement('child-element-1')
+ class ChildElement1 extends HTMLElement {
+ @reactive
+ accessor data = {}
+ get template() {
+ return mockFn1();
+ }
+ }
+
+ @customElement('child-element-2')
+ class ChildElement2 extends HTMLElement {
+ @reactive
+ accessor data = {}
+ get template() {
+ return mockFn2();
+ }
+ }
+
+
+ @customElement('track-element')
+ class ParentElement extends HTMLElement {
+ @reactive
+ accessor #data1 = { foo: 1 }
+
+ @reactive
+ accessor #data2 = { foo: 2};
+
+ handleClick() {
+ this.#data2.foo = 3;
+ }
+
+ get template() {
+ return html`
+ Click
+
+
+ `
+ }
+ }
+
+ const element = document.createElement('track-element');
+ document.body.append(element);
+ const btn = document.getElementById('track-btn');
+
+ expect(mockFn1).toBeCalledTimes(1);
+ expect(mockFn2).toBeCalledTimes(1);
+
+ btn.click();
+ // @ts-ignore
+ expect(element._getChangedProperties()).toEqual(new Set(['#data2']));
+
+ await nextTick();
+ expect(mockFn1).toBeCalledTimes(1);
+ expect(mockFn2).toBeCalledTimes(2);
+ });
});
describe('render multiple kinds template', () => {
@@ -443,7 +519,7 @@ describe('render multiple kinds template', () => {
document.body.appendChild(el2);
expect(el2.innerHTML).toEqual(
- '132',
+ '132',
);
@customElement('hybrid-list')
@@ -493,7 +569,7 @@ describe('render multiple kinds template', () => {
inside list: 2
inside list: 3
-
item4
`);
+ item4
`);
const item3 = document.getElementsByClassName('item3')[0];
item3.click();
await nextTick();
@@ -585,3 +661,5 @@ describe('render with rax', () => {
);
});
});
+
+
diff --git a/packages/pwc/src/elements/__tests__/commitAttributes.test.ts b/packages/pwc/src/elements/__tests__/part/commitAttributes.test.ts
similarity index 55%
rename from packages/pwc/src/elements/__tests__/commitAttributes.test.ts
rename to packages/pwc/src/elements/__tests__/part/commitAttributes.test.ts
index 2b0e5d9..b98c4ab 100644
--- a/packages/pwc/src/elements/__tests__/commitAttributes.test.ts
+++ b/packages/pwc/src/elements/__tests__/part/commitAttributes.test.ts
@@ -1,9 +1,9 @@
import { jest } from '@jest/globals'
-import { commitAttributes } from '../commitAttributes';
-import '../native/HTMLElement';
+import { commitAttributes } from '../../part';
+import '../../native/HTMLElement';
-describe('Set element attribute/property/event handler', () => {
- it('should set attribute at built-in element', () => {
+describe('Set/Update/Remove element attribute/property/event handler', () => {
+ it('should set/update/remove attribute at built-in element', () => {
const attrs = [
{
name: 'data-index',
@@ -20,6 +20,30 @@ describe('Set element attribute/property/event handler', () => {
expect(div.dataset.index).toEqual('1');
expect(div.getAttribute('class')).toEqual('container');
expect(div.classList.contains('container')).toBeTruthy();
+
+ const currentAttrs = [
+ // update
+ {
+ name: 'data-index',
+ value: 2
+ },
+ // add
+ {
+ name: 'id',
+ value: 'demo'
+ }
+ // remove class
+ ];
+ commitAttributes(div, [attrs, currentAttrs]);
+
+ expect(div.getAttribute('data-index')).toEqual('2');
+ expect(div.dataset.index).toEqual('2');
+
+ expect(div.getAttribute('class')).toBe(null);
+ expect(div.classList.length).toBe(0);
+
+ expect(div.getAttribute('id')).toEqual('demo');
+ expect(div.id).toEqual('demo');
});
it('should set attribute and event handler at built-in element', () => {
@@ -90,9 +114,10 @@ describe('Set element attribute/property/event handler', () => {
expect(childClickHandler).toBeCalledTimes(2);
});
- it('should set attribute and property at custom element', () => {
+ it('should set/update/remove attribute and property at custom element', () => {
class CustomElement extends HTMLElement {
description = 'default description';
+ number = 0;
}
window.customElements.define('custom-element', CustomElement);
@@ -108,6 +133,10 @@ describe('Set element attribute/property/event handler', () => {
name: 'class',
value: 'container',
},
+ {
+ name: 'title',
+ value: 'This is a title'
+ },
{
name: 'description',
value: 'This is custom element',
@@ -121,25 +150,86 @@ describe('Set element attribute/property/event handler', () => {
expect(customElement.getAttribute('class')).toEqual('container');
expect(customElement.classList.contains('container')).toBeTruthy();
// @ts-ignore
+ expect(customElement.title).toEqual('This is a title');
+ // @ts-ignore
expect(customElement.description).toEqual('This is custom element');
+
+ const currentAttrs = [
+ // update attribute
+ {
+ name: 'data-index',
+ value: 2
+ },
+ // add attribute
+ {
+ name: 'id',
+ value: 'demo'
+ },
+ // remove attribute
+ // update property
+ {
+ name: 'title',
+ value: 'Title Changed'
+ },
+ // add property
+ {
+ name: 'number',
+ value: 1
+ }
+ // remove property
+ ];
+
+ commitAttributes(customElement, [attrs, currentAttrs]);
+
+ expect(customElement.getAttribute('data-index')).toEqual('2');
+ expect(customElement.dataset.index).toEqual('2');
+
+ expect(customElement.getAttribute('class')).toBe(null);
+ expect(customElement.classList.length).toBe(0);
+
+ expect(customElement.getAttribute('id')).toEqual('demo');
+ expect(customElement.id).toEqual('demo');
+
+ // @ts-ignore
+ expect(customElement.title).toEqual('Title Changed');
+ // @ts-ignore
+ expect(customElement.number).toBe(1);
+ // @ts-ignore
+ expect(customElement.description).toBe(undefined);
});
- it('should only add event listener once with component update', () => {
- const mockClickHandler = jest.fn();
+ it('should add/update/remove event listener at element', () => {
+ const mockClickHandler1 = jest.fn();
const div = document.createElement('div');
const attrs = [
{
name: 'onclick',
- handler: mockClickHandler,
+ handler: mockClickHandler1,
capture: true,
}
];
- commitAttributes(div, attrs, { isInitial: true });
- div.click();
- expect(mockClickHandler).toBeCalledTimes(1);
commitAttributes(div, attrs);
div.click();
- expect(mockClickHandler).toBeCalledTimes(2);
+ expect(mockClickHandler1).toBeCalledTimes(1);
+
+ const mockClickHandler2 = jest.fn();
+ const changedAttrs = [
+ {
+ name: 'onclick',
+ handler: mockClickHandler2,
+ capture: true,
+ }
+ ];
+ commitAttributes(div, [attrs, changedAttrs]);
+ div.click();
+ expect(mockClickHandler1).toBeCalledTimes(1);
+ expect(mockClickHandler2).toBeCalledTimes(1);
+
+ const removeAttrs = [];
+ commitAttributes(div, [changedAttrs, removeAttrs]);
+ div.click();
+ expect(mockClickHandler1).toBeCalledTimes(1);
+ expect(mockClickHandler2).toBeCalledTimes(1);
});
it('Svg elements should be set as attributes', () => {
@@ -147,10 +237,22 @@ describe('Set element attribute/property/event handler', () => {
const attrs = [{
name: 'width',
value: '200'
+ }, {
+ name: 'height',
+ value: '200'
}];
- commitAttributes(svg, attrs, { isInitial: true });
-
+ commitAttributes(svg, attrs);
expect(svg.getAttribute('width')).toEqual('200');
+
+ const currentAttrs = [{
+ name: 'height',
+ value: '200'
+ }];
+
+ commitAttributes(svg, [attrs, currentAttrs]);
+ expect(svg.getAttribute('width')).toBe(null);
+ expect(svg.getAttribute('height')).toEqual('200');
});
});
+
diff --git a/packages/pwc/src/elements/__tests__/part/commitTemplates.test.ts b/packages/pwc/src/elements/__tests__/part/commitTemplates.test.ts
new file mode 100644
index 0000000..5d6524b
--- /dev/null
+++ b/packages/pwc/src/elements/__tests__/part/commitTemplates.test.ts
@@ -0,0 +1,12 @@
+import { TEXT_COMMENT_DATA } from "../../../constants";
+import { PWCElement } from "../../../type";
+import { html } from '../../../index';
+
+describe('commitTemplates', () => {
+ const commentNode = document.createComment(TEXT_COMMENT_DATA);
+ const rootElement = {} as PWCElement;
+
+ it('Simple commit', () => {
+
+ });
+});
\ No newline at end of file
diff --git a/packages/pwc/src/elements/commitAttributes.ts b/packages/pwc/src/elements/commitAttributes.ts
deleted file mode 100644
index e4f271f..0000000
--- a/packages/pwc/src/elements/commitAttributes.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { isEvent } from '../utils/isEvent';
-import type { Attributes, PWCElement } from '../type';
-import { toRaw } from '../utils';
-
-export function commitAttributes(element: Element, attrs: Attributes, opt?: {
- isInitial?: boolean;
- rootElement?: PWCElement;
- isSVG?: boolean;
-}) {
- const {
- isInitial = false,
- isSVG = false,
- rootElement,
- } = opt || {};
- for (const attr of attrs) {
- // Bind event
- if (isEvent(attr)) {
- const { name } = attr;
-
- // Only add event listener at the first render
- if (!isInitial) {
- continue;
- }
- const eventName = name.slice(2).toLowerCase();
- const { capture = false, handler } = attr;
- // If capture is true, the event should be triggered when capture stage
- // Bind the rootElement to ensure the handler context is the element itself
- element.addEventListener(eventName, handler.bind(rootElement), capture);
-
- continue;
- }
-
- const { name, value } = attr;
-
- if (isSVG) {
- // https://svgwg.org/svg2-draft/struct.html#InterfaceSVGSVGElement
- // Svg elements must be set as attributes, all properties is read only
- element.setAttribute(name, value);
- } else if (name in element) {
- // Verify that there is a target property on the element
- element[name] = toRaw(value);
- } else {
- element.setAttribute(name, value);
- }
- }
-}
diff --git a/packages/pwc/src/elements/part/attributes.ts b/packages/pwc/src/elements/part/attributes.ts
new file mode 100644
index 0000000..21a9e12
--- /dev/null
+++ b/packages/pwc/src/elements/part/attributes.ts
@@ -0,0 +1,65 @@
+import { Attributes, NormalAttribute, PWCElement } from '../../type';
+import { commitAttributes } from './utils/commitAttributes';
+import { BasePart } from './base';
+import { getProperties } from '../../reactivity/methods';
+
+export function genIsAttributeChanged(changedProperties: Set) {
+ return function (attr: NormalAttribute): boolean {
+ const { value } = attr;
+ const properties = getProperties(value);
+
+ for (let prop of properties) {
+ if (changedProperties.has(prop)) {
+ return true;
+ }
+ }
+ return false;
+ };
+}
+
+export class AttributesPart extends BasePart {
+ #el: Element;
+ #elIsCustom: boolean;
+ #elIsSvg: boolean;
+
+ constructor(commentNode: Comment, rootElement: PWCElement, initialValue: Attributes) {
+ super(commentNode, rootElement, initialValue);
+ this.#el = this.commentNode.nextSibling as Element;
+ this.#elIsCustom = Boolean(window.customElements.get(this.#el.localName));
+ this.#elIsSvg = this.#el instanceof SVGElement;
+ this.render(initialValue);
+ }
+
+ render(value: Attributes) {
+ if (this.#elIsCustom) {
+ // @ts-ignore
+ this.#el.__init_task__ = () => {
+ this.commitAttributes(value, true);
+ };
+ } else {
+ this.commitAttributes(value, true);
+ }
+ }
+
+ commitValue([prev, current]: [Attributes, Attributes]) {
+ const updated = this.commitAttributes([prev, current]);
+
+ // Any updating should trigger the child components's update method
+ if (this.#elIsCustom && (this.#el as PWCElement)._requestUpdate && updated) {
+ (this.#el as PWCElement)._requestUpdate();
+ }
+ }
+
+ commitAttributes(value: Attributes | [Attributes, Attributes], isInitial = false): boolean {
+ const changedProperties = this.rootElement._getChangedProperties();
+
+ const isAttributeChanged = genIsAttributeChanged(changedProperties);
+
+ return commitAttributes(this.#el, value, {
+ isInitial,
+ isSVG: this.#elIsSvg,
+ rootElement: this.rootElement,
+ isAttributeChanged,
+ });
+ }
+}
\ No newline at end of file
diff --git a/packages/pwc/src/elements/part/base.ts b/packages/pwc/src/elements/part/base.ts
new file mode 100644
index 0000000..e001744
--- /dev/null
+++ b/packages/pwc/src/elements/part/base.ts
@@ -0,0 +1,23 @@
+import { PWCElement, TemplateDataItemType } from '../../type';
+
+export class BasePart {
+ commentNode: Comment;
+ rootElement: PWCElement;
+
+ constructor(commentNode: Comment, rootElement: PWCElement, initialValue: TemplateDataItemType) {
+ this.commentNode = commentNode;
+ this.rootElement = rootElement;
+ }
+
+ // Initial values
+ init(...args: any[]) {}
+
+ // Remove node
+ remove() {}
+
+ // Render node
+ render(value: TemplateDataItemType) {}
+
+ // Trigger update
+ commitValue([prev, current]: [TemplateDataItemType, TemplateDataItemType]) {}
+}
diff --git a/packages/pwc/src/elements/part/index.ts b/packages/pwc/src/elements/part/index.ts
new file mode 100644
index 0000000..439e11f
--- /dev/null
+++ b/packages/pwc/src/elements/part/index.ts
@@ -0,0 +1,5 @@
+export * from './attributes';
+export * from './base';
+export * from './template';
+export * from './text';
+export * from './utils';
diff --git a/packages/pwc/src/elements/part/template.ts b/packages/pwc/src/elements/part/template.ts
new file mode 100644
index 0000000..fa8d0c4
--- /dev/null
+++ b/packages/pwc/src/elements/part/template.ts
@@ -0,0 +1,87 @@
+import { PLACEHOLDER_COMMENT_DATA, PWC_PREFIX, TEXT_COMMENT_DATA } from '../../constants';
+import { throwError } from '../../error';
+import { Attributes, PWCElement, PWCElementTemplate, RootElement, TemplateDataItemType } from '../../type';
+import { createTemplate, commitTemplates, renderTextCommentTemplate } from './utils';
+import { AttributesPart } from './attributes';
+import { BasePart } from './base';
+
+// Scan placeholder node, and commit dynamic data to component
+function renderElementTemplate(
+ fragment: RootElement | Node,
+ templateData: TemplateDataItemType[],
+ dynamicTree: DynamicNode[],
+ rootElement: PWCElement,
+) {
+ const nodeIterator = document.createNodeIterator(fragment, NodeFilter.SHOW_COMMENT, {
+ acceptNode(node) {
+ if ((node as Comment).data?.includes(PWC_PREFIX)) {
+ return NodeFilter.FILTER_ACCEPT;
+ }
+ return NodeFilter.FILTER_REJECT;
+ },
+ });
+ let placeholder: Node;
+ let index = 0;
+
+ while ((placeholder = nodeIterator.nextNode())) {
+ const value = templateData[index];
+ let node: DynamicNode;
+
+ // Insert dynamic text node
+ if ((placeholder as Comment).data === TEXT_COMMENT_DATA) {
+ node = renderTextCommentTemplate(placeholder as Comment, rootElement, value);
+ } else if ((placeholder as Comment).data === PLACEHOLDER_COMMENT_DATA) {
+ node = {
+ commentNode: placeholder as Comment,
+ part: new AttributesPart(placeholder as Comment, rootElement, value as Attributes),
+ };
+ }
+ dynamicTree.push(node);
+ index++;
+ }
+}
+
+export type DynamicNode = {
+ commentNode: Comment;
+ part?: BasePart;
+ children?: DynamicNode[];
+};
+export class TemplatePart extends BasePart {
+ childNodes: Node[];
+ dynamicNode: DynamicNode;
+
+ constructor(commentNode: Comment, rootElement: PWCElement, initialValue: PWCElementTemplate) {
+ super(commentNode, rootElement, initialValue);
+ this.childNodes = [];
+ this.dynamicNode = {
+ children: [],
+ part: this,
+ commentNode: this.commentNode,
+ };
+ this.render(initialValue);
+ }
+
+ remove() {
+ if (this.childNodes.length > 0) {
+ // Clear out-dated nodes
+ this.childNodes.forEach(childNode => {
+ childNode.parentNode.removeChild(childNode);
+ });
+ this.childNodes = [];
+ }
+ }
+
+ render(elementTemplate: PWCElementTemplate) {
+ this.remove();
+ const { templateString, templateData = [] } = elementTemplate;
+ const fragment = createTemplate(templateString);
+ renderElementTemplate(fragment, templateData, this.dynamicNode.children, this.rootElement);
+ // Cache all native nodes
+ this.childNodes = [...fragment.childNodes];
+ this.commentNode.parentNode.insertBefore(fragment, this.commentNode);
+ }
+
+ commitValue([prev, current]: [PWCElementTemplate, PWCElementTemplate]) {
+ commitTemplates([prev, current], this.dynamicNode, this.rootElement);
+ }
+}
\ No newline at end of file
diff --git a/packages/pwc/src/elements/part/text.ts b/packages/pwc/src/elements/part/text.ts
new file mode 100644
index 0000000..19f9890
--- /dev/null
+++ b/packages/pwc/src/elements/part/text.ts
@@ -0,0 +1,32 @@
+import { PWCElement } from '../../type';
+import { isFalsy } from '../../utils';
+import { BasePart } from './base';
+
+export class TextPart extends BasePart {
+ el: Text;
+
+ constructor(commentNode: Comment, rootElement: PWCElement, initialValue: string) {
+ super(commentNode, rootElement, initialValue);
+ this.render(initialValue);
+ }
+
+ commitValue([prev, current]: [string, string]) {
+ if (prev !== current) {
+ this.el.nodeValue = this.formatValue(current);
+ }
+ }
+
+ render(value: string) {
+ const textNode = document.createTextNode(this.formatValue(value));
+ this.el = textNode;
+ this.commentNode.parentNode.insertBefore(textNode, this.commentNode);
+ }
+
+ remove() {
+ this.commentNode.parentNode.removeChild(this.el);
+ }
+
+ formatValue(value: string): string {
+ return isFalsy(value) ? '' : value;
+ }
+}
diff --git a/packages/pwc/src/elements/part/utils/commitAttributes.ts b/packages/pwc/src/elements/part/utils/commitAttributes.ts
new file mode 100644
index 0000000..657ecf0
--- /dev/null
+++ b/packages/pwc/src/elements/part/utils/commitAttributes.ts
@@ -0,0 +1,193 @@
+import { toRaw } from '../../../reactivity/methods';
+import type { Attribute, Attributes, NormalAttribute, PWCElement } from '../../../type';
+import { is, isArray, isEvent } from '../../../utils';
+
+type IsAttributeChanged = (attr: NormalAttribute) => boolean;
+
+type Options = {
+ isInitial?: boolean;
+ rootElement?: PWCElement;
+ isSVG?: boolean;
+ isAttributeChanged?: IsAttributeChanged;
+};
+
+type Handler = (...args: any[]) => any;
+
+const handlerMap: WeakMap = new WeakMap();
+
+function returnTrue() {
+ return true;
+}
+
+function getHandler(handler: Handler, rootElement?: PWCElement): Handler {
+ if (handlerMap.has(handler)) {
+ return handlerMap.get(handler);
+ }
+ const newHandler = handler.bind(rootElement || null);
+ handlerMap.set(handler, newHandler);
+ return newHandler;
+}
+
+function isAttributes(attrs: Attributes | [Attributes, Attributes]): boolean {
+ return !isArray(attrs[0]);
+}
+
+
+enum DiffResult {
+ 'SAME',
+ 'CHANGED',
+ 'RESET',
+}
+
+function diffAttribute(prevAttr: Attribute, currentAttr: Attribute, isAttributeChanged: IsAttributeChanged): DiffResult {
+ if (isEvent(prevAttr)) {
+ if (isEvent(currentAttr)) {
+ const { handler: prevHandler, capture: prevCapture = false } = prevAttr;
+ const { handler: currentHandler, capture: currentCapture = false } = currentAttr;
+ if (prevHandler === currentHandler && prevCapture === currentCapture) {
+ return DiffResult.SAME;
+ }
+ }
+ // If attribute type is different, or event with same event name changed,
+ // it should be remove the old attribute and add the new one
+ return DiffResult.RESET;
+ }
+ if (!isEvent(currentAttr)) {
+ if (is(prevAttr.value, currentAttr.value) && !isAttributeChanged(currentAttr)) {
+ return DiffResult.SAME;
+ }
+ return DiffResult.CHANGED;
+ }
+
+ // Attribute type is different
+ return DiffResult.RESET;
+}
+
+function diffAttributes(prevAttrs: Attributes, currentAttrs: Attributes, isAttributeChanged: IsAttributeChanged): {
+ changed: Attributes;
+ removed: Attributes;
+} {
+ const currentMap: Map = new Map();
+ currentAttrs.forEach(attr => currentMap.set(attr.name, attr));
+ const changed: Attributes = [];
+ const removed: Attributes = [];
+
+ prevAttrs.forEach(prevAttr => {
+ const { name } = prevAttr;
+ if (currentMap.has(name)) {
+ const currentAttr = currentMap.get(name);
+ currentMap.delete(name);
+
+ const ret = diffAttribute(prevAttr, currentAttr, isAttributeChanged);
+ switch (ret) {
+ case DiffResult.CHANGED:
+ changed.push(currentAttr);
+ break;
+ case DiffResult.RESET:
+ removed.push(prevAttr);
+ changed.push(currentAttr);
+ case DiffResult.SAME:
+ default:
+ break;
+ }
+ } else {
+ removed.push(prevAttr);
+ }
+ });
+ for (let [, attr] of currentMap) {
+ changed.push(attr);
+ }
+ return {
+ changed,
+ removed,
+ };
+}
+
+function setAttributes(element: Element, attrs: Attributes, opt?: Options) {
+ const {
+ // isInitial = false,
+ isSVG = false,
+ rootElement,
+ } = opt || {};
+ for (const attr of attrs) {
+ // Bind event
+ if (isEvent(attr)) {
+ const { name } = attr;
+
+ const eventName = name.slice(2).toLowerCase();
+ const { capture = false, handler } = attr;
+ // If capture is true, the event should be triggered when capture stage
+ // Bind the rootElement to ensure the handler context is the element itself
+ const newHandler = getHandler(handler, rootElement);
+ element.addEventListener(eventName, newHandler, capture);
+
+ continue;
+ }
+
+ const { name, value } = attr;
+
+ if (isSVG) {
+ // https://svgwg.org/svg2-draft/struct.html#InterfaceSVGSVGElement
+ // Svg elements must be set as attributes, all properties is read only
+ element.setAttribute(name, value);
+ } else if (name in element) {
+ // Verify that there is a target property on the element
+ element[name] = toRaw(value);
+ } else {
+ element.setAttribute(name, value);
+ }
+ }
+}
+
+function removeAttributes(element: Element, attrs: Attributes, opt?: Options) {
+ const {
+ isSVG = false,
+ rootElement,
+ } = opt || {};
+ for (const attr of attrs) {
+ if (isEvent(attr)) {
+ const { name, capture = false, handler } = attr;
+ const eventName = name.slice(2).toLowerCase();
+ const newHandler = getHandler(handler, rootElement);
+ element.removeEventListener(eventName, newHandler, capture);
+ continue;
+ }
+ const { name } = attr;
+ if (isSVG) {
+ element.removeAttribute(name);
+ } else if (name in element) {
+ delete element[name];
+ } else {
+ element.removeAttribute(name);
+ }
+ }
+}
+
+// Commit attributes, return a boolean, means if updated
+export function commitAttributes(
+ element: Element,
+ attrs: Attributes | [Attributes, Attributes],
+ opt?: Options,
+): boolean {
+ if (attrs.length === 0) {
+ return false;
+ }
+ if (isAttributes(attrs)) {
+ setAttributes(element, attrs as Attributes, opt);
+ return attrs.length > 0;
+ }
+
+ const {
+ isAttributeChanged = returnTrue,
+ } = opt || {};
+
+ const [prevAttrs, currentAttrs] = attrs as [Attributes, Attributes];
+ const {
+ changed,
+ removed,
+ } = diffAttributes(prevAttrs, currentAttrs, isAttributeChanged);
+
+ removeAttributes(element, removed, opt);
+ setAttributes(element, changed, opt);
+ return changed.length + removed.length > 0;
+}
diff --git a/packages/pwc/src/elements/part/utils/commitTemplates.ts b/packages/pwc/src/elements/part/utils/commitTemplates.ts
new file mode 100644
index 0000000..c6f93d6
--- /dev/null
+++ b/packages/pwc/src/elements/part/utils/commitTemplates.ts
@@ -0,0 +1,121 @@
+import { Attributes, PWCElement, PWCElementTemplate, TemplateDataItemType } from '../../../type';
+import { AttributesPart, BasePart, DynamicNode, formatElementTemplate, TemplatePart, TextCommentDataType, TextPart, toTextCommentDataType } from '..';
+
+function remove(node: DynamicNode) {
+ if (node.part) {
+ node.part.remove();
+ } else {
+ for (let item of node.children) {
+ remove(item);
+ }
+ }
+}
+
+export function renderTextCommentTemplate(
+ commentNode: Comment,
+ rootElement: PWCElement,
+ value: TemplateDataItemType,
+): DynamicNode {
+ const node: DynamicNode = {
+ commentNode,
+ };
+ const dataType = toTextCommentDataType(value);
+ switch (dataType) {
+ case TextCommentDataType.Array: {
+ const children: DynamicNode[] = [];
+ (value as any[]).forEach(item => {
+ children.push(renderTextCommentTemplate(commentNode, rootElement, item));
+ });
+ node.children = children;
+ break;
+ }
+ case TextCommentDataType.Template: {
+ node.part = new TemplatePart(commentNode, rootElement, value as PWCElementTemplate);
+ break;
+ }
+ default: {
+ node.part = new TextPart(commentNode, rootElement, value as string);
+ break;
+ }
+ }
+ return node;
+}
+
+export function commitTemplates(
+ [prev, current]: [PWCElementTemplate, PWCElementTemplate],
+ dynamicNode: DynamicNode,
+ rootElement: PWCElement,
+): boolean {
+ const {
+ templateString: prevTemplateString,
+ templateData: prevTemplateData,
+ } = prev;
+ const {
+ templateString,
+ templateData,
+ } = current;
+ // If template strings is constant with prev ones,
+ // it should just update node values and attributes
+ if (prevTemplateString === templateString && dynamicNode.children) {
+ for (let index = 0; index < templateData.length; index++) {
+ const node = dynamicNode.children[index];
+ const prevData = prevTemplateData[index];
+ const data = templateData[index];
+
+ // Create new part
+ if (!node) {
+ dynamicNode.children[index] = renderTextCommentTemplate(dynamicNode.commentNode, rootElement, data);
+ continue;
+ }
+
+ // AttributesPart updated
+ if (node.part instanceof AttributesPart) {
+ node.part.commitValue([prevData as Attributes, data as Attributes]);
+ continue;
+ }
+
+ const prevDataType = toTextCommentDataType(prevData);
+ const dataType = toTextCommentDataType(data);
+
+ // If data type is different, it should re render parts
+ if (prevDataType !== dataType) {
+ remove(node);
+ dynamicNode.children[index] = renderTextCommentTemplate(node.commentNode, rootElement, data);
+ continue;
+ }
+
+ if (node.children) {
+ let cIndex = 0;
+ for (; cIndex < (data as any[]).length; cIndex++) {
+ if (node.children[cIndex]) {
+ commitTemplates(
+ [
+ formatElementTemplate(prevData[cIndex]),
+ formatElementTemplate(data[cIndex]),
+ ], node.children[cIndex], rootElement);
+ } else {
+ node.children.push(renderTextCommentTemplate(node.commentNode, rootElement, data[cIndex]));
+ }
+ }
+ for (; cIndex < node.children.length; cIndex++) {
+ remove(node.children[cIndex]);
+ }
+ continue;
+ }
+
+ if (node.part instanceof BasePart) {
+ node.part.commitValue([prevData, data]);
+ }
+ }
+ return;
+ }
+ // If template strings changed, it should rerender
+ remove(dynamicNode);
+ const { part, children } = renderTextCommentTemplate(dynamicNode.commentNode, rootElement, current);
+
+ // Passing valus
+ dynamicNode.part = part;
+ dynamicNode.children = children;
+
+ return true;
+}
diff --git a/packages/pwc/src/elements/createTemplate.ts b/packages/pwc/src/elements/part/utils/createTemplate.ts
similarity index 77%
rename from packages/pwc/src/elements/createTemplate.ts
rename to packages/pwc/src/elements/part/utils/createTemplate.ts
index 37d7ec0..1116c0b 100644
--- a/packages/pwc/src/elements/createTemplate.ts
+++ b/packages/pwc/src/elements/part/utils/createTemplate.ts
@@ -1,4 +1,4 @@
-import type { TemplateStringType } from '../type';
+import type { TemplateStringType } from '../../../type';
export function createTemplate(tplStr: TemplateStringType): Node {
const template = document.createElement('template');
diff --git a/packages/pwc/src/elements/elementTemplateManager.ts b/packages/pwc/src/elements/part/utils/elementTemplateManager.ts
similarity index 75%
rename from packages/pwc/src/elements/elementTemplateManager.ts
rename to packages/pwc/src/elements/part/utils/elementTemplateManager.ts
index fd9e29c..59b571e 100644
--- a/packages/pwc/src/elements/elementTemplateManager.ts
+++ b/packages/pwc/src/elements/part/utils/elementTemplateManager.ts
@@ -1,5 +1,5 @@
-import type { Fn } from '../type';
-import { isFalsy, isTemplate } from '../utils';
+import type { Fn } from '../../../type';
+import { isArray, isFalsy, isTemplate } from '../../../utils';
interface ManagerActions {
falsyAction: Fn;
@@ -18,7 +18,7 @@ export function elementTemplateManager(elementTemplate, {
falsyAction();
} else if (isTemplate(elementTemplate)) {
pwcElementTemplateAction();
- } else if (arrayAction) {
+ } else if (isArray(elementTemplate)) {
arrayAction();
} else {
textAction();
diff --git a/packages/pwc/src/elements/formatElementTemplate.ts b/packages/pwc/src/elements/part/utils/formatElementTemplate.ts
similarity index 71%
rename from packages/pwc/src/elements/formatElementTemplate.ts
rename to packages/pwc/src/elements/part/utils/formatElementTemplate.ts
index c0288c2..63a0bb4 100644
--- a/packages/pwc/src/elements/formatElementTemplate.ts
+++ b/packages/pwc/src/elements/part/utils/formatElementTemplate.ts
@@ -1,5 +1,5 @@
-import { TemplateData, TemplateString } from '../constants';
-import { ElementTemplate, PWCElementTemplate } from '../type';
+import { TemplateData, TemplateString, TEXT_COMMENT_DATA } from '../../../constants';
+import { ElementTemplate, PWCElementTemplate } from '../../../type';
import { elementTemplateManager } from './elementTemplateManager';
export function formatElementTemplate(elementTemplate: ElementTemplate): PWCElementTemplate {
@@ -18,6 +18,10 @@ export function formatElementTemplate(elementTemplate: ElementTemplate): PWCElem
textAction() {
templateString = elementTemplate;
},
+ arrayAction() {
+ templateString = ``;
+ templateData = [elementTemplate];
+ },
});
// TODO: xss
diff --git a/packages/pwc/src/elements/part/utils/index.ts b/packages/pwc/src/elements/part/utils/index.ts
new file mode 100644
index 0000000..5403bc2
--- /dev/null
+++ b/packages/pwc/src/elements/part/utils/index.ts
@@ -0,0 +1,6 @@
+export * from './commitAttributes';
+export * from './createTemplate';
+export * from './elementTemplateManager';
+export * from './formatElementTemplate';
+export * from './toTextCommentDataType';
+export * from './commitTemplates';
diff --git a/packages/pwc/src/elements/part/utils/toTextCommentDataType.ts b/packages/pwc/src/elements/part/utils/toTextCommentDataType.ts
new file mode 100644
index 0000000..985a175
--- /dev/null
+++ b/packages/pwc/src/elements/part/utils/toTextCommentDataType.ts
@@ -0,0 +1,18 @@
+import { TemplateDataItemType } from '../../../type';
+import { isArray, isTemplate } from '../../../utils';
+
+export enum TextCommentDataType {
+ 'Array',
+ 'Template',
+ 'Text',
+}
+
+export function toTextCommentDataType(value: TemplateDataItemType) {
+ if (isArray(value)) {
+ return TextCommentDataType.Array;
+ }
+ if (isTemplate(value)) {
+ return TextCommentDataType.Template;
+ }
+ return TextCommentDataType.Text;
+}
\ No newline at end of file
diff --git a/packages/pwc/src/elements/reactiveElementFactory.ts b/packages/pwc/src/elements/reactiveElementFactory.ts
index 79a253b..8a3f46f 100644
--- a/packages/pwc/src/elements/reactiveElementFactory.ts
+++ b/packages/pwc/src/elements/reactiveElementFactory.ts
@@ -1,9 +1,8 @@
-import type { ElementTemplate, PWCElement, ReflectProperties, RootElement, ReactiveNode, PWCElementTemplate } from '../type';
+import type { ElementTemplate, PWCElement, ReflectProperties, RootElement, PWCElementTemplate } from '../type';
import { Reactive } from '../reactivity/reactive';
-import { TemplateNode, TemplatesNode } from './reactiveNode';
-import { generateUid, isArray } from '../utils';
+import { TemplatePart, formatElementTemplate } from './part';
+import { generateUid } from '../utils';
import { enqueueJob } from './sheduler';
-import { formatElementTemplate } from './formatElementTemplate';
import { TEXT_COMMENT_DATA } from '../constants';
export default (Definition: PWCElement) => {
@@ -14,9 +13,9 @@ export default (Definition: PWCElement) => {
// The root element
#root: RootElement;
// Template info
- #currentTemplate: ElementTemplate | ElementTemplate[];
- // Reactive nodes
- #reactiveNode: ReactiveNode;
+ #currentTemplate: PWCElementTemplate;
+ //
+ #dynamicPart: TemplatePart;
// Reactive instance
#reactive: Reactive = new Reactive(this);
// Reflect properties
@@ -34,19 +33,14 @@ export default (Definition: PWCElement) => {
// @ts-ignore
this.__init_task__();
}
- let currentTemplate = this.template;
+ this.#currentTemplate = formatElementTemplate(this.template);
this.#root = this.shadowRoot || this;
// This pwc element root base comment node
const commentNode = document.createComment(TEXT_COMMENT_DATA);
this.appendChild(commentNode);
- if (isArray(currentTemplate)) {
- this.#reactiveNode = new TemplatesNode(commentNode, this, currentTemplate as PWCElementTemplate[]);
- } else {
- currentTemplate = formatElementTemplate(currentTemplate);
- this.#reactiveNode = new TemplateNode(commentNode, this, currentTemplate as PWCElementTemplate);
- }
- this.#currentTemplate = currentTemplate;
+ this.#dynamicPart = new TemplatePart(commentNode, this, this.#currentTemplate as PWCElementTemplate);
this.#initialized = true;
+ this.#reactive.clearChangedProperties();
}
}
disconnectedCallback() {}
@@ -59,11 +53,11 @@ export default (Definition: PWCElement) => {
}
#performUpdate() {
- const nextElementTemplate = this.template;
- const newPWCElementTemplate = formatElementTemplate(nextElementTemplate);
+ const newPWCElementTemplate = formatElementTemplate(this.template);
// The root reactive node must be TemplateNode
- this.#reactiveNode.commitValue([this.#currentTemplate, newPWCElementTemplate]);
+ this.#dynamicPart.commitValue([this.#currentTemplate, newPWCElementTemplate]);
this.#currentTemplate = newPWCElementTemplate;
+ this.#reactive.clearChangedProperties();
}
_requestUpdate(): void {
@@ -91,5 +85,9 @@ export default (Definition: PWCElement) => {
_getReflectProperties() {
return this.#reflectProperties;
}
+
+ _getChangedProperties(): Set {
+ return this.#reactive.getChangedProperties();
+ }
};
};
diff --git a/packages/pwc/src/elements/reactiveNode.ts b/packages/pwc/src/elements/reactiveNode.ts
deleted file mode 100644
index a11475e..0000000
--- a/packages/pwc/src/elements/reactiveNode.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-import type { Attributes, PWCElementTemplate, PWCElement, ReactiveNode, ReactiveNodeMapType, ReactiveNodeValue, ElementTemplate } from '../type';
-import { commitAttributes } from './commitAttributes';
-import { isFalsy, shallowEqual } from '../utils';
-import { renderElementTemplate } from './renderElementTemplate';
-import { NodeType } from '../constants';
-import { formatElementTemplate } from './formatElementTemplate';
-import { createTemplate } from './createTemplate';
-import { elementTemplateManager } from './elementTemplateManager';
-
-class BaseNode {
- commentNode: Comment;
- rootElement: PWCElement;
- value: ReactiveNodeValue;
- reactiveNodes: ReactiveNode[] = [];
- constructor(commentNode: Comment, rootElement: PWCElement, initialValue: ReactiveNodeValue) {
- this.commentNode = commentNode;
- this.rootElement = rootElement;
- this.value = initialValue;
- }
-}
-
-export class TextNode extends BaseNode implements ReactiveNode {
- #el: Text;
-
- constructor(commentNode: Comment, rootElement: PWCElement, initialValue: string) {
- super(commentNode, rootElement, initialValue);
- this.render();
- }
-
- commitValue(value: string) {
- this.value = this.formatValue(value);
- this.#el.nodeValue = this.value;
- }
-
- render() {
- this.value = this.formatValue(this.value as string);
- const textNode = document.createTextNode(this.value);
- this.#el = textNode;
- this.commentNode.parentNode.insertBefore(textNode, this.commentNode);
- }
-
- formatValue(value: string): string {
- return isFalsy(value) ? '' : value;
- }
-}
-
-export class AttributedNode extends BaseNode implements ReactiveNode {
- #el: Element;
- #elIsCustom: boolean;
- #elIsSvg: boolean;
-
- constructor(commentNode: Comment, rootElement: PWCElement, initialAttrs: Attributes) {
- super(commentNode, rootElement, initialAttrs);
- this.#el = commentNode.nextSibling as Element;
- this.#elIsCustom = Boolean(window.customElements.get(this.#el.localName));
- this.#elIsSvg = this.#el instanceof SVGElement;
- this.render();
- }
-
- render() {
- if (this.#elIsCustom) {
- // @ts-ignore
- this.#el.__init_task__ = () => {
- this.#commitAttributes(this.value as Attributes, true);
- };
- } else {
- this.#commitAttributes(this.value as Attributes, true);
- }
- }
-
- commitValue(value: Attributes) {
- this.#commitAttributes(value);
-
- // Any updating should trigger the child components's update method
- if (this.#elIsCustom && (this.#el as PWCElement)._requestUpdate) {
- (this.#el as PWCElement)._requestUpdate();
- }
- }
-
- #commitAttributes(value: Attributes, isInitial = false) {
- commitAttributes(this.#el, value, {
- isInitial,
- isSVG: this.#elIsSvg,
- rootElement: this.rootElement,
- });
- }
-}
-
-export class TemplateNode extends BaseNode implements ReactiveNode {
- childNodes: Node[];
- constructor(commentNode: Comment, rootElement: PWCElement, elementTemplate: PWCElementTemplate) {
- super(commentNode, rootElement, elementTemplate);
- this.render();
- }
- render() {
- const { templateString, templateData = [] } = formatElementTemplate(this.value as ElementTemplate);
- const fragment = createTemplate(templateString);
- // Cache all native nodes
- this.childNodes = [...fragment.childNodes];
- renderElementTemplate(fragment, templateData, this.reactiveNodes, this.rootElement, ReactiveNodeMap);
- this.commentNode.parentNode.insertBefore(fragment, this.commentNode);
- }
- commitValue([prev, current]: [PWCElementTemplate, PWCElementTemplate]) {
- updateView(prev, current, this.reactiveNodes);
- }
-}
-
-export class TemplatesNode extends BaseNode implements ReactiveNode {
- childNodes: Node[];
- constructor(commentNode: Comment, rootElement: PWCElement, elementTemplates: PWCElementTemplate[]) {
- super(commentNode, rootElement, elementTemplates);
- this.render([, elementTemplates]);
- }
-
- commitValue([prev, current]: [ElementTemplate[], ElementTemplate[]]) {
- // Delete reactive children nodes
- this.deleteChildren(this);
- // Rebuild
- this.render([prev, current]);
- }
- render([, current]: [ElementTemplate[], ElementTemplate[]]) {
- for (let elementTemplate of current) {
- let ReactiveNodeCtor;
- elementTemplateManager(elementTemplate, {
- falsyAction() {
- ReactiveNodeCtor = TextNode;
- elementTemplate = '';
- },
- pwcElementTemplateAction() {
- ReactiveNodeCtor = TemplateNode;
- },
- textAction() {
- ReactiveNodeCtor = TextNode;
- },
- arrayAction() {
- ReactiveNodeCtor = TemplatesNode;
- },
- });
-
- this.reactiveNodes.push(new ReactiveNodeCtor(this.commentNode, this.rootElement, elementTemplate));
- }
- }
-
- deleteChildren(targetReactiveNode: ReactiveNode) {
- for (const reactiveNode of targetReactiveNode.reactiveNodes) {
- (reactiveNode as TemplateNode).childNodes?.forEach(childNode => {
- const parent = childNode.parentNode;
- parent.removeChild(childNode);
- });
- if (reactiveNode.reactiveNodes.length > 0) {
- this.deleteChildren(reactiveNode);
- }
- }
- targetReactiveNode.reactiveNodes = [];
- }
-}
-
-export function updateView(
- oldElementTemplate: PWCElementTemplate,
- newElementTemplate: PWCElementTemplate,
- reactiveNodes: ReactiveNode[],
-) {
- const {
- templateString: oldTemplateString,
- templateData: oldTemplateData,
- } = oldElementTemplate;
- const {
- templateString,
- templateData,
- } = newElementTemplate;
- // While template strings is constant with prev ones,
- // it should just update node values and attributes
- if (oldTemplateString === templateString) {
- for (let index = 0; index < oldTemplateData.length; index++) {
- const reactiveNode = reactiveNodes[index];
- // Avoid html fragment effect
- if (reactiveNode instanceof TemplateNode || reactiveNode instanceof TemplatesNode) {
- // TODO more diff
- reactiveNode.commitValue(
- [
- oldTemplateData[index] as (PWCElementTemplate & PWCElementTemplate[]),
- templateData[index] as (PWCElementTemplate & PWCElementTemplate[]),
- ],
- );
- } else if (!shallowEqual(oldTemplateData[index], templateData[index])) {
- reactiveNode.commitValue(templateData[index]);
- }
- }
- }
-}
-
-export const ReactiveNodeMap: ReactiveNodeMapType = {
- [NodeType.TEXT]: TextNode,
- [NodeType.ATTRIBUTE]: AttributedNode,
- [NodeType.TEMPLATE]: TemplateNode,
- [NodeType.TEMPLATES]: TemplatesNode,
-};
diff --git a/packages/pwc/src/elements/renderElementTemplate.ts b/packages/pwc/src/elements/renderElementTemplate.ts
deleted file mode 100644
index cbcea4a..0000000
--- a/packages/pwc/src/elements/renderElementTemplate.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import type { PWCElement, ReactiveNode, ReactiveNodeMapType, RootElement, TemplateDataItemType } from '../type';
-import { NodeType, PLACEHOLDER_COMMENT_DATA, PWC_PREFIX, TEXT_COMMENT_DATA } from '../constants';
-import { isArray, isTemplate } from '../utils';
-import { throwError } from '../error';
-
-// Scan placeholder node, and commit dynamic data to component
-export function renderElementTemplate(
- fragment: RootElement | Node,
- templateData: TemplateDataItemType[],
- reactiveNodes: ReactiveNode[],
- rootElement: PWCElement,
- ReactiveNodeMap: ReactiveNodeMapType,
-) {
- const nodeIterator = document.createNodeIterator(fragment, NodeFilter.SHOW_COMMENT, {
- acceptNode(node) {
- if ((node as Comment).data?.includes(PWC_PREFIX)) {
- return NodeFilter.FILTER_ACCEPT;
- }
- return NodeFilter.FILTER_REJECT;
- },
- });
- let placeholder: Node;
- let index = 0;
-
- while ((placeholder = nodeIterator.nextNode())) {
- const value = templateData[index];
- let type;
-
- // Insert dynamic text node
- if ((placeholder as Comment).data === TEXT_COMMENT_DATA) {
- if (isArray(value)) {
- type = NodeType.TEMPLATES;
- } else if (isTemplate(value)) {
- type = NodeType.TEMPLATE;
- } else {
- type = NodeType.TEXT;
- }
- } else if ((placeholder as Comment).data === PLACEHOLDER_COMMENT_DATA) {
- type = NodeType.ATTRIBUTE;
- }
-
- if (__DEV__) {
- if (!type) {
- throwError('It is an invalid element template!');
- }
- }
-
- reactiveNodes.push(new ReactiveNodeMap[type](placeholder, rootElement, value));
-
- index++;
- }
-}
diff --git a/packages/pwc/src/index.ts b/packages/pwc/src/index.ts
index a48b82f..c7008d1 100644
--- a/packages/pwc/src/index.ts
+++ b/packages/pwc/src/index.ts
@@ -2,6 +2,6 @@ import './elements';
export { nextTick } from './elements/sheduler';
export * from './decorators';
-export { toRaw } from './utils';
+export { toRaw } from './reactivity/methods';
export { compileTemplateInRuntime as html } from '@pwc/compiler/compileTemplateInRuntime';
diff --git a/packages/pwc/src/reactivity/__tests__/createReactive/Map.test.ts b/packages/pwc/src/reactivity/__tests__/createReactive/Map.test.ts
new file mode 100644
index 0000000..e6b4c72
--- /dev/null
+++ b/packages/pwc/src/reactivity/__tests__/createReactive/Map.test.ts
@@ -0,0 +1,111 @@
+import { jest } from '@jest/globals';
+import { createReactive } from '../../createReactive';
+
+describe('createReactive/Map', () => {
+ const propName = 'prop';
+ function foo() {}
+ it('instanceof', () => {
+ const mockCallback = jest.fn(foo);
+ const original = new Map();
+ const reactived = createReactive(original, propName, mockCallback);
+
+ expect(original instanceof Map).toBe(true);
+ expect(reactived instanceof Map).toBe(true);
+ });
+ it('set/clear/delete should trigger handlers', () => {
+ const mockCallback = jest.fn(foo);
+ const original = new Map();
+ const reactived = createReactive(original, propName, mockCallback);
+
+ // set
+ reactived.set('a', { foo: 1 });
+ reactived.set('b', { foo: 2 });
+ expect(mockCallback.mock.calls.length).toBe(2);
+
+ // delete
+ reactived.delete('a');
+ expect(mockCallback.mock.calls.length).toBe(3);
+
+ // clear
+ reactived.clear();
+ expect(mockCallback.mock.calls.length).toBe(4);
+ });
+
+ it('should observe for "of"', () => {
+ const mockCallback = jest.fn(foo);
+ const original = new Map([
+ [{ key: 'a' }, { value: 'a' }],
+ [{ key: 'b' }, { value: 'a' }],
+ ]);
+ const reactived = createReactive(original, propName, mockCallback);
+
+ let index = 1;
+ for(let [key, value] of reactived) {
+ key.key += '!';
+ expect(mockCallback.mock.calls.length).toBe(index++);
+ value.value += '!';
+ expect(mockCallback.mock.calls.length).toBe(index++);
+ }
+ });
+
+ it('should observe for "forEach"', () => {
+ const mockCallback = jest.fn(foo);
+ const original = new Map([
+ [{ key: 'a' }, { value: 'a' }],
+ [{ key: 'b' }, { value: 'a' }],
+ ]);
+ const reactived = createReactive(original, propName, mockCallback);
+
+ let index = 1;
+ reactived.forEach((key, value) => {
+ key.key += '!';
+ expect(mockCallback.mock.calls.length).toBe(index++);
+ value.value += '!';
+ expect(mockCallback.mock.calls.length).toBe(index++);
+ });
+ });
+
+ it('should observe for "keys"', () => {
+ const mockCallback = jest.fn(foo);
+ const original = new Map([
+ [{ key: 'a' }, { value: 'a' }],
+ [{ key: 'b' }, { value: 'a' }],
+ ]);
+ const reactived = createReactive(original, propName, mockCallback);
+
+ let index = 1;
+ const keys = reactived.keys();
+ for (let key of keys) {
+ key.key += '!';
+ expect(mockCallback.mock.calls.length).toBe(index++);
+ }
+ });
+
+ it('should observe for "values"', () => {
+ const mockCallback = jest.fn(foo);
+ const original = new Map([
+ [{ key: 'a' }, { value: 'a' }],
+ [{ key: 'b' }, { value: 'a' }],
+ ]);
+ const reactived = createReactive(original, propName, mockCallback);
+
+ let index = 1;
+ const values = reactived.values();
+ for (let value of values) {
+ value.value += '!';
+ expect(mockCallback.mock.calls.length).toBe(index++);
+ }
+ });
+
+ it('should observe for "get"', () => {
+ const mockCallback = jest.fn(foo);
+ const original = new Map([
+ ['a', { value: 'a' }],
+ ]);
+ const reactived = createReactive(original, propName, mockCallback);
+
+ const value = reactived.get('a');
+ value.value = 'b';
+ expect(mockCallback.mock.calls.length).toBe(1);
+ });
+});
\ No newline at end of file
diff --git a/packages/pwc/src/reactivity/__tests__/createReactive/Set.test.ts b/packages/pwc/src/reactivity/__tests__/createReactive/Set.test.ts
new file mode 100644
index 0000000..ef82306
--- /dev/null
+++ b/packages/pwc/src/reactivity/__tests__/createReactive/Set.test.ts
@@ -0,0 +1,81 @@
+import { jest } from '@jest/globals';
+import { createReactive } from '../../createReactive';
+
+describe('createReactive/Set', () => {
+ const propName = 'prop';
+ function foo() {}
+ it('instanceof', () => {
+ const mockCallback = jest.fn(foo);
+ const original = new Set();
+ const reactived = createReactive(original, propName, mockCallback);
+
+ expect(original instanceof Set).toBe(true);
+ expect(reactived instanceof Set).toBe(true);
+ });
+ it('add/clear/delete should trigger handlers', () => {
+ const mockCallback = jest.fn(foo);
+ const original = new Set();
+ const reactived = createReactive(original, propName, mockCallback);
+
+ // add
+ const a = { foo: 1 };
+ const b = { foo: 2 };
+ reactived.add(a);
+ reactived.add(b);
+ expect(mockCallback.mock.calls.length).toBe(2);
+
+ // delete
+ reactived.delete(a);
+ expect(mockCallback.mock.calls.length).toBe(3);
+
+ // clear
+ reactived.clear();
+ expect(mockCallback.mock.calls.length).toBe(4);
+ });
+
+ it('should observe for "of"', () => {
+ const mockCallback = jest.fn(foo);
+ const original = new Set([
+ { value: 'a' },
+ { value: 'b' },
+ ]);
+ const reactived = createReactive(original, propName, mockCallback);
+
+ let index = 1;
+ for(let value of reactived) {
+ value.value += '!';
+ expect(mockCallback.mock.calls.length).toBe(index++);
+ }
+ });
+
+ it('should observe for "forEach"', () => {
+ const mockCallback = jest.fn(foo);
+ const original = new Set([
+ { value: 'a' },
+ { value: 'b' },
+ ]);
+ const reactived = createReactive(original, propName, mockCallback);
+
+ let index = 1;
+ reactived.forEach((value) => {
+ value.value += '!';
+ expect(mockCallback.mock.calls.length).toBe(index++);
+ });
+ });
+
+ it('should observe for "values"', () => {
+ const mockCallback = jest.fn(foo);
+ const original = new Set([
+ { value: 'a' },
+ { value: 'b' },
+ ]);
+ const reactived = createReactive(original, propName, mockCallback);
+
+ let index = 1;
+ const values = reactived.values();
+ for (let value of values) {
+ value.value += '!';
+ expect(mockCallback.mock.calls.length).toBe(index++);
+ }
+ });
+});
\ No newline at end of file
diff --git a/packages/pwc/src/reactivity/__tests__/createReactive/WeakMap.test.ts b/packages/pwc/src/reactivity/__tests__/createReactive/WeakMap.test.ts
new file mode 100644
index 0000000..47fd279
--- /dev/null
+++ b/packages/pwc/src/reactivity/__tests__/createReactive/WeakMap.test.ts
@@ -0,0 +1,36 @@
+import { jest } from '@jest/globals';
+import { createReactive } from '../../createReactive';
+
+describe('createReactive/WeakMap', () => {
+ const propName = 'prop';
+ function foo() {}
+ it('instanceof', () => {
+ const mockCallback = jest.fn(foo);
+ const original = new WeakMap();
+ const reactived = createReactive(original, propName, mockCallback);
+
+ expect(original instanceof WeakMap).toBe(true);
+ expect(reactived instanceof WeakMap).toBe(true);
+ });
+ it('set/delete should trigger handlers', () => {
+ const mockCallback = jest.fn(foo);
+ const original = new WeakMap();
+ const reactived = createReactive(original, propName, mockCallback);
+
+ const a = {
+ key: 'a'
+ };
+ const b = {
+ key: 'b'
+ };
+
+ // set
+ reactived.set(a, { foo: 1 });
+ reactived.set(b, { foo: 2 });
+ expect(mockCallback.mock.calls.length).toBe(2);
+
+ // delete
+ reactived.delete(a);
+ expect(mockCallback.mock.calls.length).toBe(3);
+ });
+});
\ No newline at end of file
diff --git a/packages/pwc/src/reactivity/__tests__/createReactive/WeakSet.test.ts b/packages/pwc/src/reactivity/__tests__/createReactive/WeakSet.test.ts
new file mode 100644
index 0000000..93f99d5
--- /dev/null
+++ b/packages/pwc/src/reactivity/__tests__/createReactive/WeakSet.test.ts
@@ -0,0 +1,31 @@
+import { jest } from '@jest/globals';
+import { createReactive } from '../../createReactive';
+
+describe('createReactive/Set', () => {
+ const propName = 'prop';
+ function foo() {}
+ it('instanceof', () => {
+ const mockCallback = jest.fn(foo);
+ const original = new WeakSet();
+ const reactived = createReactive(original, propName, mockCallback);
+
+ expect(original instanceof WeakSet).toBe(true);
+ expect(reactived instanceof WeakSet).toBe(true);
+ });
+ it('add/clear/delete should trigger handlers', () => {
+ const mockCallback = jest.fn(foo);
+ const original = new WeakSet();
+ const reactived = createReactive(original, propName, mockCallback);
+
+ // add
+ const a = { foo: 1 };
+ const b = { foo: 2 };
+ reactived.add(a);
+ reactived.add(b);
+ expect(mockCallback.mock.calls.length).toBe(2);
+
+ // delete
+ reactived.delete(a);
+ expect(mockCallback.mock.calls.length).toBe(3);
+ });
+});
\ No newline at end of file
diff --git a/packages/pwc/src/reactivity/__tests__/createReactive/base.test.ts b/packages/pwc/src/reactivity/__tests__/createReactive/base.test.ts
new file mode 100644
index 0000000..eada53a
--- /dev/null
+++ b/packages/pwc/src/reactivity/__tests__/createReactive/base.test.ts
@@ -0,0 +1,44 @@
+import { jest } from '@jest/globals';
+import { createReactive } from '../../createReactive';
+
+describe('createReactive', () => {
+ const propName = 'prop';
+ it('Obecjt', () => {
+ const mockCallback = jest.fn(() => {});
+ const source = { foo: 1 };
+ const reactived = createReactive(source, propName, mockCallback);
+
+ reactived.foo = 2;
+ expect(mockCallback.mock.calls.length).toBe(1);
+
+ // assign new object
+ reactived.newObj = {
+ foo: 1
+ };
+ expect(mockCallback.mock.calls.length).toBe(2);
+
+ // nesting changed
+ reactived.newObj.foo = 2;
+ expect(mockCallback.mock.calls.length).toBe(3);
+ });
+ it('Array', () => {
+ const mockCallback = jest.fn(() => {});
+ const source: any[] = ['foo', { foo: 1 }];
+ const reactived = createReactive(source, propName, mockCallback);
+
+ reactived[0] = 'newItem';
+ expect(mockCallback.mock.calls.length).toBe(1);
+
+ // nesting changed
+ reactived[1].foo = 2;
+ expect(mockCallback.mock.calls.length).toBe(2);
+
+ // methods
+ reactived.push({ foo: 2 });
+ expect(mockCallback.mock.calls.length).toBe(3);
+
+ // new item changed
+ reactived[2].foo = 3;
+ expect(mockCallback.mock.calls.length).toBe(4);
+ });
+});
diff --git a/packages/pwc/src/reactivity/__tests__/methods.test.ts b/packages/pwc/src/reactivity/__tests__/methods.test.ts
new file mode 100644
index 0000000..79a7711
--- /dev/null
+++ b/packages/pwc/src/reactivity/__tests__/methods.test.ts
@@ -0,0 +1,33 @@
+import { jest } from '@jest/globals';
+import { createReactive } from '../createReactive';
+import { toRaw, getProperties, isReactive } from '../methods';
+
+describe('reactive methods', () => {
+ const propName = 'prop';
+ it('toRaw', () => {
+ const mockCallback = jest.fn(() => {});
+ const source = { foo: 1 };
+ const reactived = createReactive(source, propName, mockCallback);
+
+ const raw = toRaw(reactived);
+ expect(raw).toBe(source);
+ });
+
+ it('getProperties', () => {
+ const mockCallback = jest.fn(() => {});
+ const source = { obj: { foo: 1 } };
+ const reactived = createReactive(source, propName, mockCallback);
+
+ expect(getProperties(reactived)).toEqual(new Set([propName]));
+ expect(getProperties(reactived.obj)).toEqual(new Set([propName]));
+ });
+
+ it('isReactive', () => {
+ const mockCallback = jest.fn(() => {});
+ const source = { obj: { foo: 1 } };
+ const reactived = createReactive(source, propName, mockCallback);
+
+ expect(isReactive(reactived)).toBe(true);
+ expect(isReactive(source)).toBe(false);
+ });
+});
\ No newline at end of file
diff --git a/packages/pwc/src/reactivity/__tests__/reactive.test.ts b/packages/pwc/src/reactivity/__tests__/reactive.test.ts
index 920fa56..71517d2 100644
--- a/packages/pwc/src/reactivity/__tests__/reactive.test.ts
+++ b/packages/pwc/src/reactivity/__tests__/reactive.test.ts
@@ -1,5 +1,5 @@
+import { toRaw } from '../methods';
import { Reactive } from '../reactive';
-import { toRaw } from '../../utils';
class MockReactiveElement {
#initialized = false;
diff --git a/packages/pwc/src/reactivity/baseProxy.ts b/packages/pwc/src/reactivity/baseProxy.ts
new file mode 100644
index 0000000..dcbcb74
--- /dev/null
+++ b/packages/pwc/src/reactivity/baseProxy.ts
@@ -0,0 +1,70 @@
+import { ReactiveFlags } from '../constants';
+import { hasOwnProperty, isArray, isObject } from '../utils';
+import { toReactive } from './createReactive';
+import { toRaw } from './methods';
+import { forwardTracks, getPropertyNames, runHandlers } from './track';
+
+function get(
+ target: any,
+ key: string,
+ receiver: any,
+): any {
+ if (key === ReactiveFlags.PROPERTY) {
+ return getPropertyNames(target);
+ }
+ if (key === ReactiveFlags.RAW) {
+ return target;
+ }
+ if (key === ReactiveFlags.IS_REACTIVE) {
+ return true;
+ }
+ const result = Reflect.get(target, key, receiver);
+ if (isObject(result)) {
+ forwardTracks(target, result);
+ return toReactive(result);
+ }
+ return result;
+}
+
+function set(
+ target: any,
+ key: string,
+ value: unknown,
+ receiver: any,
+) {
+ const result = Reflect.set(target, key, value, receiver);
+ const originTarget = toRaw(receiver);
+
+ // Ignore the set which happened in a prototype chain
+ if (originTarget !== target) {
+ return result;
+ }
+
+ // Ignore the array.length changes
+ if (!isArray(target) || key !== 'length') {
+ runHandlers(target);
+ }
+
+ return result;
+}
+
+function deleteProperty(
+ target: any,
+ key: string,
+): boolean {
+ const hadKey = hasOwnProperty(target, key);
+ const result = Reflect.deleteProperty(target, key);
+
+ if (result && hadKey) {
+ runHandlers(target);
+ }
+ return result;
+}
+
+export function createBaseProxy(target: any) {
+ return new Proxy(target, {
+ get,
+ set,
+ deleteProperty,
+ });
+}
\ No newline at end of file
diff --git a/packages/pwc/src/reactivity/collectionProxy.ts b/packages/pwc/src/reactivity/collectionProxy.ts
new file mode 100644
index 0000000..08321c4
--- /dev/null
+++ b/packages/pwc/src/reactivity/collectionProxy.ts
@@ -0,0 +1,220 @@
+import { ReactiveFlags } from '../constants';
+import { hasOwnProperty, isObject, isMap } from '../utils';
+import { toReactive } from './createReactive';
+import { forwardTracks, getPropertyNames, runHandlers } from './track';
+import { toRaw } from './methods';
+
+export type CollectionTypes = IterableCollections | WeakCollections;
+
+type IterableCollections = Map | Set;
+type WeakCollections = WeakMap | WeakSet;
+type MapTypes = Map | WeakMap;
+type SetTypes = Set | WeakSet;
+
+interface Iterable {
+ [Symbol.iterator](): Iterator;
+}
+
+interface Iterator {
+ next(value?: any): IterationResult;
+}
+
+interface IterationResult {
+ value: any;
+ done: boolean;
+}
+
+function get(
+ this: MapTypes,
+ key: unknown,
+) {
+ const raw = toRaw(this);
+ const rawKey = toRaw(key);
+
+ const result = raw.get(rawKey);
+ if (result && isObject(result)) {
+ forwardTracks(raw, result);
+ return toReactive(result);
+ }
+ return result;
+}
+
+function has(
+ this: CollectionTypes,
+ key: unknown,
+) {
+ const raw = toRaw(this);
+ const rawKey = toRaw(key);
+
+ return raw.has(rawKey);
+}
+
+function size(
+ target: IterableCollections,
+) {
+ const raw = toRaw(target);
+ return raw.size;
+}
+
+function set(
+ this: MapTypes,
+ key: unknown,
+ value: unknown,
+) {
+ const raw = toRaw(this);
+ const rawKey = toRaw(key);
+ const rawValue = toRaw(value);
+
+ const hadKey = raw.has(rawKey);
+ const isEqual = raw.get(rawKey) === rawValue;
+ if (!hadKey || !isEqual) {
+ raw.set(rawKey, rawValue);
+ runHandlers(raw);
+ }
+
+ return this;
+}
+
+function add(
+ this: SetTypes,
+ value: unknown,
+) {
+ const raw = toRaw(this);
+ const hadKey = raw.has(value);
+ if (!hadKey) {
+ raw.add(value);
+ runHandlers(raw);
+ }
+ return this;
+}
+
+function deleteItem(
+ this: CollectionTypes,
+ key: unknown,
+) {
+ const raw = toRaw(this);
+ const rawKey = toRaw(key);
+
+ const hadKey = raw.has(rawKey);
+
+ const result = raw.delete(key);
+ if (hadKey) {
+ runHandlers(raw);
+ }
+ return result;
+}
+
+function clear(
+ this: IterableCollections,
+) {
+ const raw = toRaw(this);
+ const hadItems = raw.size > 0;
+ const result = raw.clear();
+ if (hadItems) {
+ runHandlers(raw);
+ }
+ return result;
+}
+
+function forEach(
+ this: IterableCollections,
+ callback: Function,
+ thisArg?: unknown,
+) {
+ const raw = toRaw(this);
+ return raw.forEach((value, key) => {
+ forwardTracks(raw, value);
+ forwardTracks(raw, key);
+ return callback.call(thisArg, toReactive(value), toReactive(key), this);
+ });
+}
+
+function createIterableMethod(
+ method: string | symbol,
+) {
+ return function (
+ this: IterableCollections,
+ ...args: unknown[]
+ ): Iterable & Iterator {
+ const raw = toRaw(this);
+ const innerIterator = raw[method](...args);
+ const targetIsMap = isMap(raw);
+ const isPair =
+ method === 'entries' || (method === Symbol.iterator && targetIsMap);
+ return {
+ next() {
+ const { value, done } = innerIterator.next();
+ if (done) {
+ return {
+ value,
+ done,
+ };
+ }
+ if (isPair) {
+ forwardTracks(raw, value[0]);
+ forwardTracks(raw, value[1]);
+ return {
+ value: [toReactive(value[0]), toReactive(value[1])],
+ done,
+ };
+ }
+ forwardTracks(raw, value);
+ return {
+ value: toReactive(value),
+ done,
+ };
+ },
+ [Symbol.iterator]() {
+ return this;
+ },
+ };
+ };
+}
+
+function createInstrumentations() {
+ const instrumentations = {
+ get,
+ has,
+ add,
+ set,
+ delete: deleteItem,
+ clear,
+ forEach,
+ get size() {
+ return size(this);
+ },
+ };
+ const iteratorMethods = ['keys', 'values', 'entries', Symbol.iterator];
+ iteratorMethods.forEach(method => {
+ instrumentations[method as string] = createIterableMethod(method);
+ });
+
+ return instrumentations;
+}
+
+const instrumentations = createInstrumentations();
+
+export function createCollectionProxy(target: any) {
+ return new Proxy(target, {
+ get(
+ target: CollectionTypes,
+ key: string,
+ receiver: CollectionTypes,
+ ) {
+ if (key === ReactiveFlags.PROPERTY) {
+ return getPropertyNames(target);
+ }
+ if (key === ReactiveFlags.RAW) {
+ return target;
+ }
+ if (key === ReactiveFlags.IS_REACTIVE) {
+ return true;
+ }
+
+ if (hasOwnProperty(instrumentations, key) && key in target) {
+ return Reflect.get(instrumentations, key, receiver);
+ }
+ return Reflect.get(target, key, receiver);
+ },
+ });
+}
\ No newline at end of file
diff --git a/packages/pwc/src/reactivity/createReactive.ts b/packages/pwc/src/reactivity/createReactive.ts
new file mode 100644
index 0000000..0447b92
--- /dev/null
+++ b/packages/pwc/src/reactivity/createReactive.ts
@@ -0,0 +1,45 @@
+import { isObject, toRawType } from '../utils';
+import { toRaw } from './methods';
+import { createBaseProxy } from './baseProxy';
+import { createCollectionProxy } from './collectionProxy';
+import { keepTrack } from './track';
+
+// store the collection of original object and reactive object
+const proxyMap = new WeakMap();
+
+export function createReactive(target: any, prop: string, handler: any) {
+ if (!isObject(target)) {
+ return target;
+ }
+
+ const raw = toRaw(target);
+ keepTrack(target, prop, handler);
+ return toReactive(raw);
+}
+
+export function toReactive(target: any) {
+ let proxy = proxyMap.get(target);
+ if (proxy) {
+ return proxy;
+ }
+
+ const rawType = toRawType(target);
+ switch (rawType) {
+ case 'Object':
+ case 'Array':
+ proxy = createBaseProxy(target);
+ break;
+ case 'Map':
+ case 'Set':
+ case 'WeakMap':
+ case 'WeakSet':
+ proxy = createCollectionProxy(target);
+ break;
+ default:
+ return target;
+ }
+ // cache proxy
+ proxyMap.set(target, proxy);
+
+ return proxy;
+}
diff --git a/packages/pwc/src/reactivity/handler.ts b/packages/pwc/src/reactivity/handler.ts
deleted file mode 100644
index bfcdf93..0000000
--- a/packages/pwc/src/reactivity/handler.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { hasOwnProperty, isArray, toRaw } from '../utils';
-import { ReactiveFlags } from '../constants';
-
-function get(
- target: object,
- key: string,
- receiver: object,
-): any {
- if (key === ReactiveFlags.RAW) {
- return target;
- }
- return Reflect.get(target, key, receiver);
-}
-
-function createSetter(trigger) {
- return function set(
- target: object,
- key: string,
- value: unknown,
- receiver: object,
- ) {
- const result = Reflect.set(target, key, value, receiver);
- const originTarget = toRaw(receiver);
-
- // Ignore the set which happened in a prototype chain
- if (originTarget !== target) {
- return result;
- }
-
- // Ignore the array.length changes
- if (!isArray(target) || key !== 'length') {
- trigger();
- }
-
- return result;
- };
-}
-
-function createDeleteProperty(trigger) {
- return function deleteProperty(
- target: object,
- key: string,
- ): boolean {
- const hadKey = hasOwnProperty(target, key);
- const result = Reflect.deleteProperty(target, key);
-
- if (result && hadKey) {
- trigger();
- }
- return result;
- };
-}
-export function getProxyHandler(callback) {
- const set = createSetter(callback);
- const deleteProperty = createDeleteProperty(callback);
- return {
- get,
- set,
- deleteProperty,
- };
-}
diff --git a/packages/pwc/src/reactivity/methods.ts b/packages/pwc/src/reactivity/methods.ts
new file mode 100644
index 0000000..5061fa5
--- /dev/null
+++ b/packages/pwc/src/reactivity/methods.ts
@@ -0,0 +1,15 @@
+import { ReactiveFlags } from '../constants';
+
+export function toRaw(observed: T): T {
+ const raw = observed && observed[ReactiveFlags.RAW];
+ return raw ? toRaw(raw) : observed;
+}
+
+// get the propNames which the reactive obj created from
+export function getProperties(observed): Set {
+ return observed && observed[ReactiveFlags.PROPERTY] ? observed[ReactiveFlags.PROPERTY] : new Set();
+}
+
+export function isReactive(observed): boolean {
+ return !!(observed && observed[ReactiveFlags.IS_REACTIVE]);
+}
diff --git a/packages/pwc/src/reactivity/reactive.ts b/packages/pwc/src/reactivity/reactive.ts
index 60f3d00..a9a0625 100644
--- a/packages/pwc/src/reactivity/reactive.ts
+++ b/packages/pwc/src/reactivity/reactive.ts
@@ -1,5 +1,5 @@
import { isArray, isPlainObject, isPrivate, shallowClone } from '../utils';
-import { getProxyHandler } from './handler';
+import { createReactive } from './createReactive';
interface ReactiveType {
initValue: (prop: string, value: unknown) => void;
@@ -9,22 +9,24 @@ interface ReactiveType {
getValue: (prop: string) => unknown;
// If the reactive property changes, it will request a update
- requestUpdate: () => void;
+ requestUpdate: (prop: string) => void;
}
export class Reactive implements ReactiveType {
+ #changedProperties: Set = new Set();
+
static getKey(key: string): string {
return `#_${key}`;
}
#element: any;
- #proxyHandler = getProxyHandler(this.requestUpdate.bind(this));
constructor(elementInstance) {
this.#element = elementInstance;
}
- requestUpdate() {
+ requestUpdate(prop: string) {
+ this.#changedProperties.add(prop);
this.#element?._requestUpdate();
}
@@ -46,10 +48,20 @@ export class Reactive implements ReactiveType {
}
if (forceUpdate) {
- this.requestUpdate();
+ this.requestUpdate(prop);
+ } else {
+ this.#changedProperties.add(prop);
}
}
+ getChangedProperties() {
+ return this.#changedProperties;
+ }
+
+ clearChangedProperties() {
+ this.#changedProperties.clear();
+ }
+
#setReactiveValue(prop: string, value: unknown) {
if (isArray(value) || isPlainObject(value)) {
this.#createReactiveProperty(prop, value);
@@ -65,6 +77,6 @@ export class Reactive implements ReactiveType {
#createReactiveProperty(prop: string, initialValue: any) {
const key = Reactive.getKey(prop);
- this.#element[key] = new Proxy(initialValue, this.#proxyHandler);
+ this.#element[key] = createReactive(initialValue, prop, this.requestUpdate.bind(this, prop));
}
}
diff --git a/packages/pwc/src/reactivity/track.ts b/packages/pwc/src/reactivity/track.ts
new file mode 100644
index 0000000..2a171e7
--- /dev/null
+++ b/packages/pwc/src/reactivity/track.ts
@@ -0,0 +1,61 @@
+
+type Handler = () => void;
+
+// The propMap stores the connection of target and propNames.
+// PropName is a custom element's property.
+const propMap = new WeakMap>();
+
+// The handlerMap stores the connection of target and handlers.
+// Handler is the callback that should triggered when the target changed.
+const handlerMap = new WeakMap>();
+
+export function getPropertyNames(target: any) {
+ return propMap.get(target);
+}
+
+export function setPropertyNames(target: any, prop: string) {
+ const props = getPropertyNames(target);
+ if (props) {
+ props.add(prop);
+ } else {
+ propMap.set(target, new Set([prop]));
+ }
+}
+
+export function getHandlers(target: any) {
+ return handlerMap.get(target);
+}
+
+export function setHandlers(target: any, handler: () => void) {
+ const handlers = getHandlers(target);
+ if (handlers) {
+ handlers.add(handler);
+ } else {
+ handlerMap.set(target, new Set([handler]));
+ }
+}
+
+export function forwardTracks(source: any, target: any) {
+ const props = getPropertyNames(source);
+ const handlers = getHandlers(source);
+
+ if (props) {
+ propMap.set(target, props);
+ }
+ if (handlers) {
+ handlerMap.set(target, handlers);
+ }
+}
+
+export function keepTrack(target, prop, handler) {
+ setPropertyNames(target, prop);
+ setHandlers(target, handler);
+}
+
+export function runHandlers(target: any) {
+ const handlers = getHandlers(target);
+
+ if (handlers) {
+ handlers.forEach(handler => handler());
+ }
+}
\ No newline at end of file
diff --git a/packages/pwc/src/type.ts b/packages/pwc/src/type.ts
index cf9b151..b354c6b 100644
--- a/packages/pwc/src/type.ts
+++ b/packages/pwc/src/type.ts
@@ -1,18 +1,16 @@
-import { NodeType } from './constants';
-
export interface PWCElement extends Element {
connectedCallback(): void;
disconnectedCallback(): void;
attributeChangedCallback(name: string, oldValue: any, newValue: any): void;
adoptedCallback(): void;
_requestUpdate(): void;
+ _getChangedProperties(): Set;
prototype: PWCElement;
new(): PWCElement;
}
export type RootElement = PWCElement | ShadowRoot;
-
export interface NormalAttribute {
name: string;
value: any;
@@ -57,19 +55,3 @@ export type ReflectProperties = Map;
-export interface ReactiveNode {
- reactiveNodes?: ReactiveNode[];
- commitValue: (value: any) => void;
-}
-
-export type ReactiveNodeValue = string | Attributes | PWCElementTemplate[] | PWCElementTemplate;
-
-interface ReactiveNodeCtor {
- new(
- commentNode: Comment,
- rootElement: PWCElement,
- initialValue?: ReactiveNodeValue,
- ): ReactiveNode;
-}
-
-export type ReactiveNodeMapType = Record;
diff --git a/packages/pwc/src/utils/checkTypes.ts b/packages/pwc/src/utils/checkTypes.ts
index 3053203..ae690f3 100644
--- a/packages/pwc/src/utils/checkTypes.ts
+++ b/packages/pwc/src/utils/checkTypes.ts
@@ -19,6 +19,10 @@ export function isFunction(value: unknown) {
return typeof value === 'function';
}
+export function isObject(value: unknown) {
+ return typeof value === 'object';
+}
+
export function isPlainObject(value: unknown) {
return Object.prototype.toString.call(value) === '[object Object]';
}
@@ -47,3 +51,6 @@ export function isTemplate(value: unknown): boolean {
export function isFalsy(value: unknown) {
return !value && value !== 0;
}
+export function toRawType(value: unknown): string {
+ return Object.prototype.toString.call(value).slice(8, -1);
+}
diff --git a/packages/pwc/src/utils/index.ts b/packages/pwc/src/utils/index.ts
index 15caca6..3652fb7 100644
--- a/packages/pwc/src/utils/index.ts
+++ b/packages/pwc/src/utils/index.ts
@@ -1,8 +1,6 @@
export * from './common';
export * from './isEvent';
-export * from './shallowEqual';
export * from './generateUid';
export * from './checkTypes';
export * from './shallowClone';
-export * from './toRaw';
diff --git a/packages/pwc/src/utils/shallowEqual.ts b/packages/pwc/src/utils/shallowEqual.ts
deleted file mode 100644
index d1fc83a..0000000
--- a/packages/pwc/src/utils/shallowEqual.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-import { hasOwnProperty, is } from './common';
-import { isEvent } from './isEvent';
-import { isPrimitive } from './checkTypes';
-
-export function shallowEqual(valueA: any, valueB: any) {
- if (typeof valueA !== typeof valueB) {
- return false;
- }
- // text node
- if (isPrimitive(valueB)) {
- return valueA === valueB;
- }
- // attribute node
- const keysA = Object.keys(valueA);
- const keysB = Object.keys(valueB);
- if (keysA.length !== keysB.length) {
- return false;
- }
-
- for (const val of keysA) {
- if (!hasOwnProperty(valueB, val) || !isEvent(valueA[val]) || !is(valueA[val], valueB[val])) {
- return false;
- }
- }
-
- return true;
-}
diff --git a/packages/pwc/src/utils/toRaw.ts b/packages/pwc/src/utils/toRaw.ts
deleted file mode 100644
index e55ac3f..0000000
--- a/packages/pwc/src/utils/toRaw.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { ReactiveFlags } from '../constants';
-
-export function toRaw(observed: T): T {
- const raw = observed && observed[ReactiveFlags.RAW];
- return raw ? toRaw(raw) : observed;
-}
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8cd4483..d03e0f3 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -81,6 +81,19 @@ importers:
vite: 2.9.9
vite-plugin-babel: 1.0.0_@babel+core@7.17.10+vite@2.9.9
+ examples/condition:
+ specifiers:
+ pwc: workspace:*
+ rollup: ^2.0.0
+ vite: ^2.7.2
+ vite-plugin-babel: ^1.0.0
+ dependencies:
+ pwc: link:../../packages/pwc
+ devDependencies:
+ rollup: 2.72.1
+ vite: 2.9.9
+ vite-plugin-babel: 1.0.0_@babel+core@7.17.10+vite@2.9.9
+
examples/edit-word:
specifiers:
'@ice/pkg': ^1.0.0-rc.4