Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fix @Watch can't work and add validate tag name #5

Merged
merged 5 commits into from
Nov 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Introduce

decoco is an efficient Web Component framework based on decorator-driven development. It provides a new way to build reusable components and simplifies common programming tasks such as event listening and state management through decorators.
Decoco is an efficient Web Component framework based on decorator-driven development. It provides a new way to build reusable components and simplifies common programming tasks such as event listening and state management through decorators.

## Packages

Expand Down
8 changes: 4 additions & 4 deletions lerna.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "0.2.3",
"npmClient": "pnpm"
}
"$schema": "node_modules/lerna/schemas/lerna-schema.json",
"version": "0.2.4-alpha.0",
"npmClient": "pnpm"
}
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "create-decoco",
"version": "0.2.3",
"version": "0.2.4-alpha.0",
"description": "Create a new Decoco project",
"main": "index.js",
"type": "module",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@decoco/core",
"version": "0.2.3",
"version": "0.2.4-alpha.0",
"description": "decoco is an efficient Web Component framework based on decorator-driven development",
"main": "./dist/index.esm.dev.js",
"types": "./dist/index.d.ts",
Expand Down
37 changes: 31 additions & 6 deletions packages/core/src/decorators/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createJob, queueJob } from '../runtime/scheduler';
import { doWatch } from './Watch';
import { DecoPlugin } from '../api/plugin';
import { isObjectAttribute } from '../utils/is';
import { warn } from '../utils/error';

export interface DecoWebComponent {
[K: string | symbol]: any;
Expand Down Expand Up @@ -58,6 +59,18 @@ export default function Component(tagOrOptions: string | LegacyComponentOptions,

const observedAttributes = target.prototype.__propKeys || [];

if (customElements.get(tag)) {
warn(`custom element ${tag} already exists`);
return;
}

// Validate tag name
const tagNamePattern = /^[a-zA-Z][a-zA-Z0-9-]*$/;
if (!tagNamePattern.test(tag)) {
warn(
`Invalid tag name: ${tag}. Tag names must start with a letter and contain only letters, digits, and hyphens.`,
);
}
customElements.define(String(tag), getCustomElementWrapper(target, { tag, style, observedAttributes }));
};
}
Expand Down Expand Up @@ -142,14 +155,25 @@ function getCustomElementWrapper(target: any, { tag, style, observedAttributes }
if (stateKeys && propKeys) {
for (const propKey of propKeys.values()) {
if (stateKeys.has(propKey)) {
throw new Error(
warn(
`${String(tag)} ${propKey} can only be one state or prop, please change it to another name.`,
);
}
}
}
}

validateSomeInStateOrProps(validateKeys: Array<string>) {
const stateKeys: Set<string> = Reflect.getMetadata('stateKeys', this);
const propKeys: Set<string> = Reflect.getMetadata('propKeys', this);

for (const key of validateKeys) {
if (!stateKeys.has(key) && !propKeys.has(key)) {
return key;
}
}
}

initRefs() {
const refs = Reflect.getMetadata('refs', this);

Expand Down Expand Up @@ -211,11 +235,12 @@ function getCustomElementWrapper(target: any, { tag, style, observedAttributes }
for (const item of watchers) {
const { watchKeys, watchMethodName } = item;
watchKeys.forEach((watchKey: string) => {
const { ctx, property } = expToPath(watchKey, this);

const watchEffect = new Effect(() => this[watchKey]);

doWatch(this, watchMethodName, watchEffect, property, statePool, item.options);
const { ctx, property } = expToPath(watchKey, this) || {};
if (!ctx || !property) {
warn(`invalid watchKey ${watchKey}`);
return;
}
doWatch(this, watchMethodName, ctx, property, statePool, item.options);
});
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/decorators/State.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export default function State() {
return function (target: any, value: unknown) {
// if ((context as DecoratorContextObject).static) {
// throw new Error('@State decorator must be used on a instance property');
// throw new Error('@State.md decorator must be used on a instance property');
// }

// const metadata = context.metadata;
Expand Down
11 changes: 6 additions & 5 deletions packages/core/src/decorators/Watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,23 @@ export default function Watch(watchKeys: any[] | never[], options: WatchOptions
}

export function doWatch(
ctx: DecoWebComponent,
instance: DecoWebComponent,
watchMethodName: keyof DecoWebComponent,
watchEffect: Effect,
propertyCtx: any,
property: string | symbol,
statePool: StatePool,
watchOptions: WatchOptions,
) {
let oldValue: any;
const watchEffect = new Effect(() => propertyCtx[property]);
const job = createJob(() => {
const newValue = watchEffect.run();

const cleanup = () => {
statePool.delete(ctx, property, watchEffect);
statePool.delete(propertyCtx, property, watchEffect);
};

ctx[watchMethodName].call(ctx, newValue, oldValue, cleanup);
instance[watchMethodName].call(instance, newValue, oldValue, cleanup);

oldValue = newValue;

Expand All @@ -45,7 +46,7 @@ export function doWatch(
}
}, watchEffect.id);
watchEffect.scheduler = () => queueJob(job);
watchEffect.captureSelf(ctx, property);
watchEffect.captureSelf(propertyCtx, property, instance);

if (watchOptions.immediate) {
job();
Expand Down
14 changes: 12 additions & 2 deletions packages/core/src/reactive/effect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ export class Effect {
id = uid++;
effect: (...args: any[]) => unknown;
scheduler?: Function;
cleanup?: Function;
cleanup: Set<Function> = new Set();

constructor(effect: (...args: any[]) => unknown, options: EffectOptions = {}) {
const { scheduler, cleanup } = options;
this.effect = effect;
this.scheduler = scheduler;
this.cleanup = cleanup;

// todo
if (cleanup) {
this.cleanup.add(cleanup);
}
}

run(...args: any[]) {
Expand All @@ -36,4 +40,10 @@ export class Effect {
const statePool: StatePool = Reflect.getMetadata('statePool', instance || target);
statePool.set(target, name, this);
}

execCleanup() {
for (const cleanup of this.cleanup.values()) {
cleanup();
}
}
}
22 changes: 16 additions & 6 deletions packages/core/src/reactive/observe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createJob, queueJob } from '../runtime/scheduler';
import { ObserverOptions } from '../types';
import { isArray, isObject, isPlainObject } from '../utils/is';
import { Effect } from './effect';
import { warn } from '../utils/error';

const proxyMap = new WeakMap<object, object>();

Expand Down Expand Up @@ -29,6 +30,7 @@ export function createReactive(targetElement: any, target: unknown, options: Obs
set(target, key, value, receiver) {
Reflect.set(target, key, autoDeepReactive ? createReactive(targetElement, value) : value, receiver);
statePool.notify(target, key);
statePool.delete(target, key);

return true;
},
Expand Down Expand Up @@ -72,7 +74,7 @@ export function observe(
options: ObserverOptions = { deep: true },
) {
let value = originValue;
const { lazy, deep, isProp, autoDeepReactive = true } = options;
const { lazy, deep = true, isProp, autoDeepReactive = true } = options;
const statePool = Reflect.getMetadata('statePool', target as any);

const isDeepReactive = (isPlainObject(value) || isArray(value)) && deep;
Expand All @@ -98,10 +100,10 @@ export function observe(
}
}

statePool.delete(this, name);
value = autoDeepReactive ? createReactive(target, newValue, options) : newValue;

statePool.notify(target, name);
statePool.delete(this, name);

return true;
},
},
Expand Down Expand Up @@ -156,18 +158,26 @@ export class StatePool {
delete(target: object, name: string | symbol, effect?: Effect) {
const depKeyMap = this.store.get(target);
if (!depKeyMap) {
console.error(new Error(`${target} has no state ${String(name)}`));
warn(`${target} has no state ${String(name)}`);
return;
}
const deps = depKeyMap.get(name);
if (!deps) {
return;
}
if (!effect) {
// effect?.cleanup?.();
deps.clear();
deps.values().forEach((effect: Effect) => {
effect.execCleanup();
});
return;
} else {
if (!deps.has(effect)) {
warn(`Not find effect. This possibly is a bug. Please report it.`);
}
deps.delete(effect);
effect.execCleanup();
}
deps.delete(effect);
}

notify(target: object, name: string | symbol) {
Expand Down
4 changes: 1 addition & 3 deletions packages/core/src/utils/error.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
export function warn(message: string) {
if (process.env.NODE_ENV !== 'production') {
throw new Error(message);
console.error(new Error(message));
}

console.error(message);
}
6 changes: 4 additions & 2 deletions packages/core/src/utils/share.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { warn } from './error';

export function expToPath(exp: string, ctx: object) {
const paths = exp.split('.');
const paths = exp.replace(/\[(\w+)\]/g, '.$1').split('.');
if (paths.length === 1) {
return { ctx, property: paths[0] };
}
Expand All @@ -15,6 +17,6 @@ export function expToPath(exp: string, ctx: object) {

return { ctx: prevObj, property: key };
} catch (err) {
throw new Error(`${exp} is not a valid path`, { cause: err });
warn(`${exp} is not a valid path`);
}
}
39 changes: 22 additions & 17 deletions packages/docs/.gitignore
100755 → 100644
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
# Dependencies
/node_modules

# Production
/build

# Generated files
.docusaurus
.cache-loader

# Misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*

node_modules
dist
dist-ssr
*.local

# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
doc_build
41 changes: 0 additions & 41 deletions packages/docs/README.md

This file was deleted.

3 changes: 0 additions & 3 deletions packages/docs/babel.config.js

This file was deleted.

12 changes: 0 additions & 12 deletions packages/docs/blog/2019-05-28-first-blog-post.md

This file was deleted.

Loading