diff --git a/Contributing.md b/Contributing.md index 56ac8aa..b7db2ba 100644 --- a/Contributing.md +++ b/Contributing.md @@ -6,7 +6,8 @@ git clone https://github.com/EasyWebApp/WebCell.git ~/Desktop/WebCell cd ~/Desktop/WebCell -npm install +npm i pnpm -g +pnpm i npm test -npm run build +pnpm build ``` diff --git a/ReadMe.md b/ReadMe.md index 2c82cee..969e041 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -454,11 +454,28 @@ export default AsyncTag; #### `index.tsx` ```tsx +import { DOMRenderer } from 'dom-renderer'; +import { lazy } from 'web-cell'; + const AsyncTag = lazy(() => import('./AsyncTag')); new DOMRenderer().render(); ``` +### Animate CSS component + +```tsx +import { DOMRenderer } from 'dom-renderer'; +import { AnimateCSS } from 'web-cell'; + +new DOMRenderer().render( +

Fade In

} + /> +); +``` + ## Node.js usage ### Tool chain @@ -505,7 +522,7 @@ import 'web-cell/polyfill'; We recommend these libraries to use with WebCell: -- **State management**: [MobX][42] (also powered by **TypeScript** & **Decorator**) +- **State management**: [MobX][3] (also powered by **TypeScript** & **Decorator**) - **Router**: [Cell Router][43] - **UI components** @@ -520,14 +537,13 @@ We recommend these libraries to use with WebCell: ## Roadmap -- [x] [Extend **Build-in Elements** with Virtual DOM][51] -- [x] [Server-side Render][52] -- [x] [Async Component loading][53] +- [x] [Server-side Render][51] +- [x] [Async Component loading][52] ## More guides -1. [v2 to v3 migration][54] -2. [Development contribution][55] +1. [v2 to v3 migration][53] +2. [Development contribution][54] [1]: https://www.webcomponents.org/ [2]: https://facebook.github.io/jsx/ @@ -539,7 +555,7 @@ We recommend these libraries to use with WebCell: [8]: https://github.com/jaywcjlove/awesome-uikit [9]: https://tech-query.me/programming/web-components-practise/slide.html [10]: https://gitter.im/EasyWebApp/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge -[11]: https://codesandbox.io/s/webcell-demo-9gyll?autoresize=1&fontsize=14&hidenavigation=1&module=%2Fsrc%2FClock.tsx&theme=dark +[11]: https://codesandbox.io/p/devbox/9gyll?embed=1&file=%2Fsrc%2FClock.tsx [12]: https://nodei.co/npm/web-cell/ [13]: https://www.typescriptlang.org/ [14]: https://github.com/tc39/proposal-decorators @@ -570,7 +586,6 @@ We recommend these libraries to use with WebCell: [39]: https://github.com/EasyWebApp/scaffold [40]: https://github.com/EasyWebApp/DashBoard [41]: https://github.com/EasyWebApp/mark-wiki -[42]: https://github.com/mobxjs/mobx/blob/mobx4and5/docs/ [43]: https://web-cell.dev/cell-router/ [44]: https://bootstrap.web-cell.dev/ [45]: https://material.web-cell.dev/ @@ -579,8 +594,7 @@ We recommend these libraries to use with WebCell: [48]: https://web-cell.dev/web-utility/ [49]: https://web-cell.dev/iterable-observer/ [50]: https://github.com/EasyWebApp/MarkCell -[51]: https://github.com/snabbdom/snabbdom/pull/829 -[52]: https://web.dev/declarative-shadow-dom/ -[53]: https://reactjs.org/docs/react-api.html#reactlazy -[54]: https://github.com/EasyWebApp/WebCell/blob/main/Migrating.md -[55]: https://github.com/EasyWebApp/WebCell/blob/main/Contributing.md +[51]: https://web.dev/declarative-shadow-dom/ +[52]: https://reactjs.org/docs/react-api.html#reactlazy +[53]: https://github.com/EasyWebApp/WebCell/blob/main/Migrating.md +[54]: https://github.com/EasyWebApp/WebCell/blob/main/Contributing.md diff --git a/package.json b/package.json index 7af0442..641540d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "web-cell", - "version": "3.0.0-rc.5", + "version": "3.0.0-rc.7", "description": "Web Components engine based on VDOM, JSX, MobX & TypeScript", "keywords": [ "web", @@ -58,7 +58,7 @@ "lint-staged": "^15.2.0", "open-cli": "^8.0.0", "parcel": "~2.11.0", - "prettier": "^3.1.1", + "prettier": "^3.2.0", "rimraf": "^5.0.5", "ts-jest": "^29.1.1", "ts-node": "^10.9.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 341c7af..156ada0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,8 +80,8 @@ devDependencies: specifier: ~2.11.0 version: 2.11.0(@swc/helpers@0.5.3)(typescript@5.3.3) prettier: - specifier: ^3.1.1 - version: 3.1.1 + specifier: ^3.2.0 + version: 3.2.0 rimraf: specifier: ^5.0.5 version: 5.0.5 @@ -5007,8 +5007,8 @@ packages: engines: {node: '>= 0.8.0'} dev: true - /prettier@3.1.1: - resolution: {integrity: sha512-22UbSzg8luF4UuZtzgiUOfcGM8s4tjBv6dJRT7j275NXsy2jb4aJa4NNveul5x4eqlF1wuhuR2RElK71RvmVaw==} + /prettier@3.2.0: + resolution: {integrity: sha512-/vBUecTGaPlRVwyZVROVC58bYIScqaoEJzZmzQXXrZOzqn0TwWz0EnOozOlFO/YAImRnb7XsKpTCd3m1SjS2Ww==} engines: {node: '>=14'} hasBin: true dev: true diff --git a/preview/Home.tsx b/preview/Home.tsx index 74ae010..f38cd7e 100644 --- a/preview/Home.tsx +++ b/preview/Home.tsx @@ -1,18 +1,21 @@ import { formToJSON } from 'web-utility'; -import { FC, PropsWithChildren, lazy } from '../source'; +import { AnimateCSS, FC, WebCellProps, lazy } from '../source'; import { ClassClock, FunctionClock } from './Clock'; import { TestField } from './Field'; const Async = lazy(() => import('./Async')); -const Hello: FC = ({ children }) => ( -

Hello {children}!

+const Hello: FC = ({ className, children }) => ( +

Hello {children}!

); export const HomePage = () => ( <> - WebCell + WebCell} + />
We use the same configuration as Parcel to bundle this sandbox, you can find more info about Parcel diff --git a/source/Animation/index.tsx b/source/Animation/index.tsx new file mode 100644 index 0000000..80546b4 --- /dev/null +++ b/source/Animation/index.tsx @@ -0,0 +1,60 @@ +import { observable } from 'mobx'; +import { importCSS } from 'web-utility'; + +import { WebCellProps } from '../Async'; +import { animated } from '../MobX'; +import { WebCell, component } from '../WebCell'; +import { FC, attribute, observer, reaction } from '../decorator'; +import { AnimationType } from './type'; + +export * from './type'; + +export interface AnimateCSS extends WebCell {} + +export interface AnimateCSSProps { + type: AnimationType; + component: FC; +} + +@component({ tagName: 'animation-css' }) +@observer +export class AnimateCSS extends HTMLElement implements WebCell { + declare props: AnimateCSSProps; + + @attribute + @observable + accessor type: AnimationType; + + @attribute + @observable + accessor playing = false; + + component: FC; + + async connectedCallback() { + await importCSS('https://unpkg.com/animate.css@4/animate.min.css'); + + this.typeChanged(); + } + + @reaction(({ type }) => type) + async typeChanged() { + this.playing = true; + + await animated(this, '.animate__animated'); + + this.playing = false; + } + + render() { + const { type, playing, component: Tag } = this; + + return playing ? ( + + ) : type.includes('Out') ? ( + <> + ) : ( + + ); + } +} diff --git a/source/Animation/type.ts b/source/Animation/type.ts new file mode 100644 index 0000000..9fd98d2 --- /dev/null +++ b/source/Animation/type.ts @@ -0,0 +1,56 @@ +export type PositionY = 'Top' | 'Bottom'; +export type DirectionX = 'Left' | 'Right'; +export type DirectionY = 'Up' | 'Down'; +export type Direction = DirectionX | DirectionY; +export type AnimationMode = 'In' | 'Out'; + +export type AttentionSeekers = + | 'bounce' + | 'flash' + | 'pulse' + | 'rubberBand' + | `shake${'X' | 'Y'}` + | 'headShake' + | 'swing' + | 'tada' + | 'wobble' + | 'jello' + | 'heartBeat'; +export type BackEntrances = `backIn${Direction}`; +export type BackExits = `backOut${Direction}`; +export type BouncingEntrances = `bounceIn${'' | Direction}`; +export type BouncingExits = `bounceOut${'' | Direction}`; +export type FadingEntrances = + | `fadeIn${'' | `${Direction}${'' | 'Big'}`}` + | `fadeIn${PositionY}${DirectionX}`; +export type FadingExits = `fadeOut${ + | '' + | `${Direction}${'' | 'Big'}` + | `${PositionY}${DirectionX}`}`; +export type Flippers = `flip${'' | `${AnimationMode}${'X' | 'Y'}`}`; +export type Lightspeed = `lightSpeed${AnimationMode}${DirectionX}`; +export type RotatingEntrances = `rotateIn${'' | `${DirectionY}${DirectionX}`}`; +export type RotatingExits = `rotateOut${'' | `${DirectionY}${DirectionX}`}`; +export type Specials = 'hinge' | 'jackInTheBox' | `roll${'In' | 'Out'}`; +export type ZoomingEntrances = `zoomIn${'' | Direction}`; +export type ZoomingExits = `zoomOut${'' | Direction}`; +export type SlidingEntrances = `slideIn${Direction}`; +export type SlidingExits = `slideOut${Direction}`; + +export type AnimationType = + | AttentionSeekers + | BackEntrances + | BackExits + | BouncingEntrances + | BouncingExits + | FadingEntrances + | FadingExits + | Flippers + | Lightspeed + | RotatingEntrances + | RotatingExits + | Specials + | ZoomingEntrances + | ZoomingExits + | SlidingEntrances + | SlidingExits; diff --git a/source/Async.tsx b/source/Async.tsx index 04334d2..c80bb8d 100644 --- a/source/Async.tsx +++ b/source/Async.tsx @@ -1,5 +1,5 @@ -import { observable } from 'mobx'; import { JsxProps } from 'dom-renderer'; +import { observable } from 'mobx'; import { ClassComponent, WebCell, component } from './WebCell'; import { @@ -7,42 +7,39 @@ import { FunctionComponent, PropsWithChildren, WebCellComponent, - observer, - reaction + observer } from './decorator'; export type ComponentTag = string | WebCellComponent; export type WebCellProps = JsxProps; -export interface AsyncBoxProps extends WebCellProps { +export interface AsyncCellProps extends WebCellProps { loader: () => Promise; delegatedProps?: WebCellProps; } -export interface AsyncBox extends WebCell {} +export interface AsyncCell extends WebCell {} @component({ - tagName: 'async-box' + tagName: 'async-cell' }) @observer -export class AsyncBox extends HTMLElement { - declare props: AsyncBoxProps; +export class AsyncCell extends HTMLElement { + declare props: AsyncCellProps; - @observable - accessor loader: AsyncBoxProps['loader']; + loader: AsyncCellProps['loader']; @observable accessor component: FC; @observable - accessor delegatedProps: AsyncBoxProps['delegatedProps']; + accessor delegatedProps: AsyncCellProps['delegatedProps']; connectedCallback() { this.load(); } - @reaction((element: AsyncBox) => element.loader) protected async load() { this.component = undefined; @@ -72,7 +69,7 @@ export function lazy< T extends () => Promise<{ default: FunctionComponent | ClassComponent }> >(loader: T) { return (props: GetAsyncProps) => ( - (await loader()).default} /> diff --git a/source/MobX.ts b/source/MobX.ts index da8fe2e..7dcec68 100644 --- a/source/MobX.ts +++ b/source/MobX.ts @@ -1,5 +1,6 @@ import { DataObject } from 'dom-renderer'; import { ObservableValue } from 'mobx/dist/internal'; +import { delegate } from 'web-utility'; export function getMobxData(observable: T) { for (const key of Object.getOwnPropertySymbols(observable)) { @@ -13,3 +14,18 @@ export function getMobxData(observable: T) { ) as T; } } + +export const animated = ( + root: T, + targetSelector: string +) => + new Promise(resolve => { + const ended = delegate(targetSelector, (event: AnimationEvent) => { + root.removeEventListener('animationend', ended); + root.removeEventListener('animationcancel', ended); + resolve(event); + }); + + root.addEventListener('animationend', ended); + root.addEventListener('animationcancel', ended); + }); diff --git a/source/decorator.ts b/source/decorator.ts index dd2bd3a..f901aa0 100644 --- a/source/decorator.ts +++ b/source/decorator.ts @@ -52,6 +52,8 @@ function wrapClass(Component: T) { extends (Component as ClassComponent) implements CustomElement { + static observedAttributes = []; + protected disposers: IReactionDisposer[] = []; get props() { @@ -90,6 +92,16 @@ function wrapClass(Component: T) { this.disposers.length = 0; } + setAttribute(name: string, value: string) { + const old = super.getAttribute(name), + names: string[] = this.constructor['observedAttributes']; + + super.setAttribute(name, value); + + if (names.includes(name)) + this.attributeChangedCallback(name, old, value); + } + attributeChangedCallback(name: string, old: string, value: string) { this[toCamelCase(name)] = parseJSON(value); @@ -141,18 +153,10 @@ export function attribute( { name, addInitializer }: ClassAccessorDecoratorContext ) { addInitializer(function () { - const { constructor } = this; - var names = constructor['observedAttributes']; + const names: string[] = this.constructor['observedAttributes'], + attribute = toHyphenCase(name.toString()); - if (!names) { - names = []; - - Object.defineProperty(constructor, 'observedAttributes', { - configurable: true, - get: () => names - }); - } - names.push(toHyphenCase(name.toString())); + if (!names.includes(attribute)) names.push(attribute); }); } diff --git a/source/index.ts b/source/index.ts index a68df65..b23dd9f 100644 --- a/source/index.ts +++ b/source/index.ts @@ -3,3 +3,4 @@ export * from './MobX'; export * from './WebCell'; export * from './WebField'; export * from './Async'; +export * from './Animation';