├── .browserslistrc
├── .build
├── webpack.wechat.config.js
└── wechat-exports-loader.js
├── .eslintrc.js
├── .examples
├── app
│ ├── index.html
│ ├── src
│ │ ├── components
│ │ │ ├── header
│ │ │ │ ├── header.css
│ │ │ │ └── header.jsx
│ │ │ ├── icon
│ │ │ │ └── icon.jsx
│ │ │ ├── nav-bar
│ │ │ │ ├── nav-bar.css
│ │ │ │ └── nav-bar.jsx
│ │ │ └── tab
│ │ │ │ ├── tab.css
│ │ │ │ └── tab.jsx
│ │ ├── index.js
│ │ ├── modules
│ │ │ ├── home
│ │ │ │ ├── home.controller.js
│ │ │ │ ├── home.css
│ │ │ │ ├── home.jsx
│ │ │ │ └── home.service.js
│ │ │ └── hot
│ │ │ │ ├── hot.css
│ │ │ │ └── hot.jsx
│ │ └── navigation.js
│ └── webpack.config.js
├── controller
│ └── some.jsx
├── dev
│ ├── app.jsx
│ ├── index.html
│ ├── index.js
│ └── webpack.config.js
├── event-stream
│ ├── app.jsx
│ └── component.jsx
├── hooks-in-class-component
│ └── some.jsx
├── router-i18n
│ ├── app.jsx
│ ├── detail
│ │ ├── i18n
│ │ │ └── index.js
│ │ ├── index.jsx
│ │ └── router
│ │ │ └── index.js
│ ├── home.jsx
│ └── list.jsx
└── two-way-binding
│ ├── app.jsx
│ └── component.jsx
├── .github
└── workflows
│ └── publish.yml
├── .gitignore
├── .npmignore
├── .scripts
├── files-for-wechat.js
└── get-wechat-bind.js
├── .vscode
└── settings.json
├── CHANGELOG.md
├── README.md
├── babel.config.js
├── docs
├── CNAME
├── README.md
├── api
│ ├── animation.md
│ ├── create-async-component.md
│ ├── create-two-way-binding.md
│ └── storage.md
├── assets
│ ├── nautil-lifecircle.jpg
│ └── nautil-logo.png
├── beginning
│ ├── install.md
│ └── package-usage.md
├── cli
│ ├── command.md
│ ├── config.md
│ ├── env-vars.md
│ ├── install.md
│ └── ts.md
├── component
│ ├── api.md
│ ├── attrs.md
│ ├── class-or-function.md
│ ├── event-stream.md
│ ├── lifecircle.md
│ ├── props-type.md
│ ├── props.md
│ ├── stylesheet.md
│ └── two-way-binding.md
├── components
│ ├── async.md
│ ├── each.md
│ ├── for.md
│ ├── if-else.md
│ ├── observer.md
│ ├── prepare.md
│ ├── static.md
│ └── switch-case.md
├── concepts
│ ├── css-module.md
│ ├── data-type-system.md
│ ├── mvc.md
│ ├── observer-pattern.md
│ ├── stream.md
│ └── two-way-binding.md
├── controller
│ └── controller.md
├── decorators
│ ├── decorate.md
│ ├── evolve.md
│ ├── initialize.md
│ ├── inject.md
│ ├── nest.md
│ ├── observe.md
│ └── pipe.md
├── docsify.js
├── docsify.min.js
├── elements
│ ├── audio.md
│ ├── button.md
│ ├── checkbox.md
│ ├── form.md
│ ├── image.md
│ ├── input.md
│ ├── line.md
│ ├── list-section.md
│ ├── radio.md
│ ├── scroll-section.md
│ ├── section.md
│ ├── select.md
│ ├── swipe-section.md
│ ├── text.md
│ ├── textarea.md
│ ├── video.md
│ └── webview.md
├── hooks
│ ├── use-controller.md
│ ├── use-data-source.md
│ ├── use-force-update.md
│ ├── use-model.md
│ ├── use-service.md
│ ├── use-shallow-latest.md
│ ├── use-two-way-binding.md
│ └── use-unique-keys.md
├── i18n
│ └── i18n.md
├── index.html
├── index.md
├── module
│ ├── create-bootstrap.md
│ ├── import-module.md
│ ├── module.md
│ └── router.md
├── renderers
│ ├── dom.md
│ ├── native.md
│ ├── web-component.md
│ └── wechat.md
├── services
│ ├── data-service.md
│ ├── event-service.md
│ ├── queue-service.md
│ └── service.md
├── store
│ ├── consumer.md
│ ├── provider.md
│ └── store.md
└── view
│ └── view.md
├── dom.d.ts
├── index.d.ts
├── native.d.ts
├── package-lock.json
├── package.json
├── src
├── dom
│ ├── elements
│ │ ├── audio.jsx
│ │ ├── button.jsx
│ │ ├── checkbox.jsx
│ │ ├── form.jsx
│ │ ├── image.jsx
│ │ ├── input.jsx
│ │ ├── line.jsx
│ │ ├── list-section.jsx
│ │ ├── radio.jsx
│ │ ├── scroll-section.jsx
│ │ ├── section.jsx
│ │ ├── select.jsx
│ │ ├── swipe-section.jsx
│ │ ├── text.jsx
│ │ ├── textarea.jsx
│ │ ├── video.jsx
│ │ └── webview.jsx
│ ├── i18n
│ │ └── language-detector.js
│ ├── index.js
│ ├── render.js
│ ├── router
│ │ └── router.jsx
│ ├── storage
│ │ └── storage.js
│ └── style
│ │ └── transform.js
├── index.js
├── lib
│ ├── animate
│ │ ├── animation.jsx
│ │ ├── easings.js
│ │ ├── transition.js
│ │ └── tween.js
│ ├── components
│ │ ├── async.jsx
│ │ ├── for-each.jsx
│ │ ├── if-else.jsx
│ │ ├── observer.jsx
│ │ ├── prepare.jsx
│ │ ├── static.jsx
│ │ └── switch-case.jsx
│ ├── core
│ │ ├── component.js
│ │ ├── controller.js
│ │ ├── module.jsx
│ │ ├── service.js
│ │ ├── stream.js
│ │ └── view.jsx
│ ├── decorators
│ │ ├── combiners.js
│ │ └── decorators.js
│ ├── elements
│ │ ├── audio.jsx
│ │ ├── button.jsx
│ │ ├── checkbox.jsx
│ │ ├── form.jsx
│ │ ├── image.jsx
│ │ ├── input.jsx
│ │ ├── line.jsx
│ │ ├── list-section.jsx
│ │ ├── radio.jsx
│ │ ├── scroll-section.jsx
│ │ ├── section.jsx
│ │ ├── select.jsx
│ │ ├── swipe-section.jsx
│ │ ├── text.jsx
│ │ ├── textarea.jsx
│ │ ├── video.jsx
│ │ └── webview.jsx
│ ├── hooks
│ │ ├── controller.js
│ │ ├── force-update.js
│ │ ├── model.js
│ │ ├── service.js
│ │ ├── shallow-latest.js
│ │ ├── two-way-binding.js
│ │ └── unique-keys.js
│ ├── i18n
│ │ ├── i18n.class.js
│ │ ├── i18n.jsx
│ │ └── language-detector.js
│ ├── router
│ │ ├── history.js
│ │ └── router.jsx
│ ├── services
│ │ ├── data-service.js
│ │ ├── event-service.js
│ │ └── queue-service.js
│ ├── storage
│ │ └── storage.js
│ ├── store
│ │ ├── context.jsx
│ │ ├── shared.js
│ │ └── store.js
│ ├── style
│ │ ├── classname.js
│ │ ├── style.js
│ │ └── transform.js
│ └── utils.js
├── native
│ ├── elements
│ │ ├── audio.jsx
│ │ ├── button.jsx
│ │ ├── checkbox.jsx
│ │ ├── form.jsx
│ │ ├── image.jsx
│ │ ├── input.jsx
│ │ ├── line.jsx
│ │ ├── list-section.jsx
│ │ ├── radio.jsx
│ │ ├── scroll-section.jsx
│ │ ├── section.jsx
│ │ ├── select.jsx
│ │ ├── swipe-section.jsx
│ │ ├── text.jsx
│ │ ├── textarea.jsx
│ │ ├── video.jsx
│ │ └── webview.jsx
│ ├── i18n
│ │ └── language-detector.js
│ ├── index.js
│ ├── register.js
│ ├── router
│ │ └── router.jsx
│ ├── storage
│ │ └── storage.js
│ └── style
│ │ ├── style.js
│ │ └── transform.js
├── ssr
│ ├── client
│ │ └── render.js
│ ├── navigation
│ │ └── navigation.js
│ └── server
│ │ ├── core
│ │ └── component.js
│ │ ├── create.js
│ │ └── index.js
├── web-component
│ ├── define.js
│ ├── index.js
│ └── retarget-events.js
└── wechat
│ ├── components
│ └── dynamic
│ │ ├── dynamic.js
│ │ ├── dynamic.json
│ │ ├── dynamic.wxml
│ │ └── fns.wxs
│ ├── elements
│ ├── audio.jsx
│ ├── button.jsx
│ ├── checkbox.jsx
│ ├── form.jsx
│ ├── image.jsx
│ ├── input.jsx
│ ├── line.jsx
│ ├── list-section.jsx
│ ├── radio.jsx
│ ├── scroll-section.jsx
│ ├── section.jsx
│ ├── select.jsx
│ ├── swipe-section.jsx
│ ├── text.jsx
│ ├── textarea.jsx
│ ├── video.jsx
│ └── webview.jsx
│ ├── i18n
│ └── language-detector.js
│ ├── index.js
│ ├── render.js
│ ├── router
│ └── router.jsx
│ ├── storage
│ └── storage.js
│ └── style
│ └── transform.js
├── web-component.d.ts
└── wechat.d.ts
/.browserslistrc:
--------------------------------------------------------------------------------
1 | ## supports proxy
2 | chrome 49
3 | firefox 18
4 | edge 12
5 | safari 10
6 | opera 36
7 |
--------------------------------------------------------------------------------
/.build/webpack.wechat.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const babelConfig = require('../babel.config.js')
3 |
4 | babelConfig.presets[0][1] = { modules: false }
5 |
6 | const main = {
7 | mode: 'production',
8 | target: 'node',
9 | entry: path.resolve(__dirname, '../src/index.js'),
10 | output: {
11 | path: path.join(__dirname, '../miniprogram_dist'),
12 | filename: 'index.js',
13 | library: 'nautil',
14 | libraryTarget: 'commonjs2',
15 | },
16 | resolve: {
17 | alias: {
18 | 'ts-fns': path.resolve(__dirname, '../node_modules/ts-fns/es'),
19 | scopex: path.resolve(__dirname, '../node_modules/scopex'),
20 | tyshemo: path.resolve(__dirname, '../node_modules/tyshemo/src'),
21 | immer: path.resolve(__dirname, '../node_modules/immer'),
22 | algeb: path.resolve(__dirname, '../node_modules/algeb/src'),
23 | },
24 | },
25 | externals: {
26 | './wechat': true,
27 | },
28 | module: {
29 | rules: [
30 | {
31 | test: /\.js|jsx$/,
32 | exclude: {
33 | and: [
34 | /node_modules/,
35 | ],
36 | not: [
37 | /ts\-fns/,
38 | /tyshemo/,
39 | ],
40 | },
41 | use: [
42 | {
43 | loader: 'babel-loader',
44 | options: babelConfig,
45 | },
46 | {
47 | loader: path.resolve(__dirname, 'wechat-exports-loader.js'),
48 | },
49 | ],
50 | },
51 | ],
52 | },
53 | optimization: {
54 | minimize: true,
55 | usedExports: true,
56 | sideEffects: true,
57 | },
58 | }
59 |
60 | const wechat = {
61 | ...main,
62 | entry: path.resolve(__dirname, '../src/wechat/index.js'),
63 | output: {
64 | path: path.join(__dirname, '../miniprogram_dist/wechat'),
65 | filename: 'index.js',
66 | library: 'nautil/wechat',
67 | libraryTarget: 'commonjs2',
68 | },
69 | externals: undefined,
70 | }
71 |
72 | const dynamic = {
73 | ...wechat,
74 | entry: path.resolve(__dirname, '../src/wechat/components/dynamic/dynamic.js'),
75 | output: {
76 | path: path.join(__dirname, '../miniprogram_dist/wechat/components/dynamic'),
77 | filename: 'dynamic.js',
78 | libraryTarget: 'commonjs2',
79 | },
80 | }
81 |
82 | module.exports = [main, wechat, dynamic]
83 |
--------------------------------------------------------------------------------
/.build/wechat-exports-loader.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const fs = require('fs')
3 |
4 | const libFile = path.resolve(__dirname, '../src/index.js')
5 | const wxFile = path.resolve(__dirname, '../src/wechat/index.js')
6 |
7 | const libContent = fs.readFileSync(libFile).toString()
8 | const wxContent = fs.readFileSync(wxFile).toString()
9 |
10 | module.exports = function(content) {
11 | if (this.resourcePath === libFile) {
12 | return `export * from './wechat'`
13 | }
14 |
15 | if (this.resourcePath === wxFile) {
16 | const wxLines = wxContent.split('\n')
17 |
18 | const contents = libContent.split('\n').map((line) => {
19 | if (line.indexOf("from './lib") === -1) {
20 | return line
21 | }
22 | return line.replace('./lib/', '../lib/')
23 | })
24 |
25 | wxLines.forEach((line) => {
26 | if (line.indexOf("from './") === -1) {
27 | return
28 | }
29 |
30 | const [_, file] = line.match(/from '(.*?)'/)
31 | if (fs.existsSync(path.resolve(__dirname, '../src/lib', file))) {
32 | contents.unshift(`import '${file}'`)
33 | }
34 | else {
35 | contents.push(line)
36 | }
37 | })
38 |
39 | return contents.join('\n')
40 | }
41 |
42 | return content
43 | }
44 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | commonjs: true,
5 | es6: true,
6 | },
7 | globals: {
8 | importScripts: 'readonly',
9 | describe: 'readonly',
10 | test: 'readonly',
11 | expect: 'readonly',
12 | jest: 'readonly',
13 | process: 'readonly',
14 | __dirname: 'readonly',
15 | },
16 | parser: '@typescript-eslint/parser',
17 | parserOptions: {
18 | ecmaFeatures: {
19 | experimentalObjectRestSpread: true,
20 | jsx: true,
21 | },
22 | sourceType: 'module',
23 | },
24 | plugins: [
25 | 'react',
26 | ],
27 | extends: "eslint:recommended",
28 | rules: {
29 | indent: ['error', 2],
30 | semi: ['error', 'never', {
31 | beforeStatementContinuationChars: 'always',
32 | }],
33 | 'comma-dangle': ['error', 'always-multiline'],
34 | 'object-curly-spacing': ['error', 'always'],
35 | 'array-bracket-spacing': ['error', 'never'],
36 | 'no-unused-vars': ['error', {
37 | argsIgnorePattern: '^_',
38 | varsIgnorePattern: '^_',
39 | }],
40 | },
41 | }
42 |
--------------------------------------------------------------------------------
/.examples/app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.examples/app/src/components/header/header.css:
--------------------------------------------------------------------------------
1 | .header {
2 | padding: 10px 15px;
3 | border-bottom: solid #eee 1px;
4 | display: flex;
5 | align-items: center;
6 | background-color: #fff;
7 | }
8 | .header__logo {
9 | width: 45px;
10 | height: 38px;
11 | }
12 | .header__logo-img {
13 | width: 45px;
14 | height: 38px;
15 | }
16 | .header__search {
17 | flex: 1;
18 | text-align: right;
19 | margin: 0 5px;
20 | }
21 | .header__search-input {
22 | overflow: visible;
23 | border: none;
24 | box-shadow: none;
25 | outline: none;
26 | color: #666;
27 | outline-offset: -2px;
28 | background-color: #f9f9f9;
29 | padding: 8px;
30 | border: #dedede solid 1px;
31 | }
32 | .header__menus {
33 | position: relative;
34 | }
35 | .header__menus-holder {
36 | color: #007fff;
37 | }
38 | .header__menus-list {
39 | position: absolute;
40 | top: 100%;
41 | left: 0;
42 |
43 | margin-top: 10px;
44 | width: 90px;
45 | background-color: #fff;
46 | border: #eee solid 1px;
47 | border-radius: 5px;
48 | }
49 | .header__menus-item {
50 | display: block;
51 | padding: 5px 10px;
52 | text-decoration: none;
53 | color: #71777c;
54 | }
55 | .header__menus-item--active {
56 | color: #007fff;
57 | }
58 | .header__login-button {
59 | appearance: none;
60 | border-radius: 2px;
61 | outline: none;
62 | transition: background-color .3s,color .3s;
63 | cursor: pointer;
64 | border: #007fff solid 1px;
65 | padding: .5px 10px;
66 | color: #007fff;
67 | line-height: 1.9rem;
68 | background-color: #fff;
69 | }
70 |
--------------------------------------------------------------------------------
/.examples/app/src/components/icon/icon.jsx:
--------------------------------------------------------------------------------
1 | import { React } from 'nautil'
2 |
3 | export function Icon(props) {
4 | return
5 | }
6 | export default Icon
7 |
--------------------------------------------------------------------------------
/.examples/app/src/components/nav-bar/nav-bar.css:
--------------------------------------------------------------------------------
1 | .nav-bar {
2 | display: flex;
3 | border-bottom: #eee solid 1px;
4 | padding: 0 5px;
5 | background-color: #fff;
6 | box-shadow: 0 1px 2px #f1f1f1;
7 | }
8 | .nav-bar__item {
9 | padding: 10px;
10 | }
11 | .nav-bar__item-link {
12 | text-decoration: none;
13 | color: #585858;
14 | font-size: 14px;
15 | }
16 |
--------------------------------------------------------------------------------
/.examples/app/src/components/nav-bar/nav-bar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Component, Section, Each, Link } from 'nautil'
3 | import Styles from './nav-bar.css'
4 |
5 | const navItems = [
6 | {
7 | name: 'recommend',
8 | text: '推荐',
9 | },
10 | {
11 | name: 'frontend',
12 | text: '前端',
13 | },
14 | {
15 | name: 'backend',
16 | text: '后端',
17 | },
18 | ]
19 |
20 | export class NavBar extends Component {
21 | render() {
22 | return (
23 |
30 | )
31 | }
32 | }
33 | export default NavBar
34 |
--------------------------------------------------------------------------------
/.examples/app/src/components/tab/tab.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangshuang/nautil/1cdd52330d481aef417b7560796c9e68d3a42167/.examples/app/src/components/tab/tab.css
--------------------------------------------------------------------------------
/.examples/app/src/components/tab/tab.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Component } from 'nautil'
3 |
--------------------------------------------------------------------------------
/.examples/app/src/index.js:
--------------------------------------------------------------------------------
1 | import { createApp } from 'nautil'
2 | import { mount } from 'nautil/dom'
3 | import navigation from './navigation.js'
4 |
5 | const App = createApp({
6 | navigation,
7 | })
8 |
9 | mount('#root', App)
10 |
--------------------------------------------------------------------------------
/.examples/app/src/modules/home/home.controller.js:
--------------------------------------------------------------------------------
1 | import { Controller } from 'nautil'
2 | import { HomeService } from './home.service.js'
3 |
4 | export class HomeController extends Controller {
5 | static service = HomeService
6 |
7 | get articles() {
8 | return this.service.get(this.service.articles)
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.examples/app/src/modules/home/home.css:
--------------------------------------------------------------------------------
1 | .header {
2 | padding: 10px 15px;
3 | border-bottom: solid #dedede 1px;
4 | display: flex;
5 | align-items: center;
6 | }
7 | .header__logo {
8 | width: 45px;
9 | height: 38px;
10 | }
11 | .header__logo-img {
12 | width: 45px;
13 | height: 38px;
14 | }
15 | .header__search {
16 | flex: 1;
17 | text-align: right;
18 | margin: 0 5px;
19 | }
20 | .header__search-input {
21 | overflow: visible;
22 | border: none;
23 | box-shadow: none;
24 | outline: none;
25 | color: #666;
26 | outline-offset: -2px;
27 | background-color: #f9f9f9;
28 | padding: 8px;
29 | border: #dedede solid 1px;
30 | }
31 | .header__menus {
32 | position: relative;
33 | }
34 | .header__menus-holder {
35 | color: #007fff;
36 | }
37 | .header__menus-list {
38 | position: absolute;
39 | top: 100%;
40 | left: 0;
41 |
42 | margin-top: 10px;
43 | width: 90px;
44 | background-color: #fff;
45 | border: #dedede solid 1px;
46 | border-radius: 5px;
47 | }
48 | .header__menus-item {
49 | display: block;
50 | padding: 5px 10px;
51 | text-decoration: none;
52 | color: #71777c;
53 | }
54 | .header__menus-item--active {
55 | color: #007fff;
56 | }
57 | .header__login-button {
58 | appearance: none;
59 | border-radius: 2px;
60 | outline: none;
61 | transition: background-color .3s,color .3s;
62 | cursor: pointer;
63 | border: #007fff solid 1px;
64 | padding: .5px 10px;
65 | color: #007fff;
66 | line-height: 1.9rem;
67 | background-color: #fff;
68 | }
69 |
--------------------------------------------------------------------------------
/.examples/app/src/modules/home/home.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | React,
3 | Component,
4 | Section,
5 | Text,
6 | } from 'nautil'
7 | import Header from '../../components/header/header.jsx'
8 | import NavBar from '../../components/nav-bar/nav-bar.jsx'
9 | import { HomeController } from './home.controller.js'
10 |
11 | export class Home extends Component {
12 | controller = new HomeController()
13 |
14 | HomeCover = this.controller.reactive(() => {
15 | const items = this.controller.articles
16 | return (
17 |
18 | Home
19 | {JSON.stringify(items)}
20 |
21 | )
22 | })
23 |
24 | render() {
25 | const { HomeCover } = this
26 | return (
27 | <>
28 |
29 |
30 |
31 | >
32 | )
33 | }
34 | }
35 | export default Home
36 |
--------------------------------------------------------------------------------
/.examples/app/src/modules/home/home.service.js:
--------------------------------------------------------------------------------
1 | import { DataService } from 'nautil'
2 |
3 | export class HomeService extends DataService {
4 | articles = this.source(() => new Promise((r) => setTimeout(() => r([{ id: 1, title: 'Article 1' }]), 1000)), [])
5 | }
6 |
--------------------------------------------------------------------------------
/.examples/app/src/modules/hot/hot.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangshuang/nautil/1cdd52330d481aef417b7560796c9e68d3a42167/.examples/app/src/modules/hot/hot.css
--------------------------------------------------------------------------------
/.examples/app/src/modules/hot/hot.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | React,
3 | Component,
4 | Section,
5 | Text,
6 | } from 'nautil'
7 | import Header from '../../components/header/header.jsx'
8 | import NavBar from '../../components/nav-bar/nav-bar.jsx'
9 |
10 | export class Hot extends Component {
11 | render() {
12 | return (
13 | <>
14 |
15 |
16 |
19 | >
20 | )
21 | }
22 | }
23 | export default Hot
24 |
--------------------------------------------------------------------------------
/.examples/app/src/navigation.js:
--------------------------------------------------------------------------------
1 | import { Navigation } from 'nautil'
2 | import Home from './modules/home/home.jsx'
3 | import Hot from './modules/hot/hot.jsx'
4 |
5 | const navigation = new Navigation({
6 | mode: 'history',
7 | routes: [
8 | {
9 | name: 'home',
10 | path: '/home',
11 | component: Home,
12 | },
13 | {
14 | name: 'hot',
15 | path: '/hot',
16 | component: Hot,
17 | },
18 | ],
19 | })
20 |
21 | export default navigation
22 |
--------------------------------------------------------------------------------
/.examples/controller/some.jsx:
--------------------------------------------------------------------------------
1 | import { React, Component, Controller, Model, Meta, Text, Button, Section } from 'nautil'
2 |
3 | class Title extends Meta {
4 | default = ''
5 | type = String
6 | maxLength = 50
7 | }
8 |
9 | class Price extends Meta {
10 | default = 0
11 | type = Number
12 | min = 0
13 | }
14 |
15 | class SomeModel extends Model {
16 | static title = Title
17 | static price = Price
18 | }
19 |
20 | class SomeController extends Controller {
21 | static some = SomeModel
22 |
23 | static increase$(stream) {
24 | stream.subscribe(() => this.some.price ++)
25 | }
26 |
27 | Title() {
28 | return (
29 | {this.some.title}
30 | )
31 | }
32 |
33 | Price() {
34 | return (
35 | {this.some.price}
36 | )
37 | }
38 |
39 | IncreasePriceButton(props) {
40 | return (
41 | {props.children || 'Increase Price'}
42 | )
43 | }
44 | }
45 |
46 | export default class Some extends Component {
47 | controller = new SomeController()
48 |
49 | render() {
50 | const { Title, Price, IncreasePriceButton } = this.controller
51 | return (
52 |
57 | )
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/.examples/dev/app.jsx:
--------------------------------------------------------------------------------
1 | import { Router, createBootstrap, Link } from 'nautil'
2 |
3 | function Home() {
4 | return go to detail
5 | }
6 |
7 | function Detail() {
8 | return (
9 | <>
10 | go to home
11 | go to content
12 | >
13 | )
14 | }
15 |
16 | function Content() {
17 | return 'content'
18 | }
19 |
20 | const { Outlet } = new Router({
21 | routes: [
22 | {
23 | path: '',
24 | redirect: 'home',
25 | },
26 | {
27 | path: 'home',
28 | component: Home,
29 | },
30 | {
31 | path: 'detail',
32 | component: Detail,
33 | },
34 | ],
35 | })
36 |
37 | const bootstrap = createBootstrap({
38 | router: {
39 | mode: '/',
40 | },
41 | })
42 |
43 | function App() {
44 | return
45 | }
46 |
47 | export default bootstrap(App)
48 |
--------------------------------------------------------------------------------
/.examples/dev/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/.examples/dev/index.js:
--------------------------------------------------------------------------------
1 | import { mount } from 'nautil/dom'
2 | import App from './app.jsx'
3 |
4 |
5 | mount('#root', App)
6 |
--------------------------------------------------------------------------------
/.examples/event-stream/app.jsx:
--------------------------------------------------------------------------------
1 | import { React, Component, Stream } from 'nautil'
2 | import Some from './component.jsx'
3 |
4 | export default class App extends Component {
5 | change$ = new Stream()
6 |
7 | state = {
8 | value: '',
9 | }
10 |
11 | onInit() {
12 | this.on('change', e => this.setState({ value: e.target.value }))
13 | }
14 |
15 | render() {
16 | return
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/.examples/event-stream/component.jsx:
--------------------------------------------------------------------------------
1 | import { React, Component, Input } from 'nautil'
2 |
3 | export default class Some extends Component {
4 | static props = {
5 | value: String,
6 | onChange: true, // declare a event property
7 | }
8 |
9 | render() {
10 | const { value } = this.attrs
11 | return this.emit('change', e)}
15 | />
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/.examples/hooks-in-class-component/some.jsx:
--------------------------------------------------------------------------------
1 | import { React, Component, Button } from 'nautil'
2 |
3 | export default class Some extends Component {
4 | // notice, use uppercase `Render`
5 | Render(props) {
6 | const [state, setState] = React.useState(props.count)
7 | return setState(state + 1)}>{state}
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/.examples/router-i18n/app.jsx:
--------------------------------------------------------------------------------
1 | import { Component, Router, createAsyncComponent, createBootstrap, LanguageDetector, I18n } from 'nautil'
2 |
3 | const Home = createAsyncComponent(() => import('./home'))
4 | const List = createAsyncComponent(() => import('./list'))
5 | const Detail = createAsyncComponent(() => import('./detail'))
6 |
7 | const { Outlet, Link } = new Router({
8 | routes: [
9 | {
10 | path: '',
11 | redirect: 'home',
12 | },
13 | {
14 | path: 'home',
15 | component: Home,
16 | },
17 | {
18 | path: 'list',
19 | component: List,
20 | },
21 | {
22 | path: 'detail/:id',
23 | component: Detail,
24 | },
25 | ],
26 | })
27 |
28 | const { T } = new I18n({
29 | resources: {
30 | zh: async () => {},
31 | en: async () => {},
32 | },
33 | })
34 |
35 | class App extends Component {
36 | render() {
37 | const id = '1' // mock
38 |
39 | return (
40 |
41 |
42 | Home
43 | List
44 | Detail
45 |
46 |
47 |
48 | )
49 | }
50 | }
51 |
52 | const bootstrap = createBootstrap({
53 | router: {
54 | mode: '/',
55 | },
56 | i18n: {
57 | lang: LanguageDetector,
58 | },
59 | })
60 |
61 | export default bootstrap(App)
62 |
--------------------------------------------------------------------------------
/.examples/router-i18n/detail/i18n/index.js:
--------------------------------------------------------------------------------
1 | import { I18n } from 'nautil'
2 |
3 | export const { T, useLang, setLang, useLocale, useTranslate } = new I18n({
4 | resources: {
5 | zh: async () => {},
6 | en: async () => {},
7 | },
8 | })
9 |
--------------------------------------------------------------------------------
/.examples/router-i18n/detail/index.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'nautil'
2 | import { T } from './i18n'
3 | import { Outlet, Link } from './router'
4 |
5 | export default class Detail extends Component {
6 | state = {
7 | content: '',
8 | }
9 | shouldAffect(props) {
10 | return [props.id]
11 | }
12 | async onAffect() {
13 | const { id } = this.props
14 | const content = await fetch(`/api/article/${id}`).then(res => res.json())
15 | this.setState({
16 | content,
17 | })
18 | }
19 | render() {
20 | return (
21 |
22 |
Detail
23 |
{this.state.content}
24 |
25 | Basic
26 | Extra
27 |
28 |
29 |
30 | )
31 | }
32 | }
33 |
34 | function Basic() {
35 | return 'basic'
36 | }
37 |
38 | function Extra() {
39 | return 'extra'
40 | }
41 |
42 | function NotFound() {
43 | return 'not found'
44 | }
45 |
--------------------------------------------------------------------------------
/.examples/router-i18n/detail/router/index.js:
--------------------------------------------------------------------------------
1 | import { Router } from 'nautil'
2 |
3 | export const { Outlet, Link, useMatch, useLocation, useParams, useRouteNavigate, useListen } = new Router({
4 | routes: [
5 | {
6 | path: 'basic',
7 | component: Basic,
8 | },
9 | {
10 | path: 'extra',
11 | component: Extra,
12 | },
13 | {
14 | path: '',
15 | redirect: '/basic',
16 | },
17 | {
18 | path: '!',
19 | component: NotFound,
20 | },
21 | ],
22 | })
23 |
24 | function Basic() {
25 | return 'basic'
26 | }
27 |
28 | function Extra() {
29 | return 'extra'
30 | }
31 |
32 | function NotFound() {
33 | return 'not found'
34 | }
35 |
--------------------------------------------------------------------------------
/.examples/router-i18n/home.jsx:
--------------------------------------------------------------------------------
1 | export default function Home() {
2 | return 'home'
3 | }
4 |
--------------------------------------------------------------------------------
/.examples/router-i18n/list.jsx:
--------------------------------------------------------------------------------
1 | export default function List() {
2 | return 'list'
3 | }
4 |
--------------------------------------------------------------------------------
/.examples/two-way-binding/app.jsx:
--------------------------------------------------------------------------------
1 | import { React, Component, createTwoWayBinding } from 'nautil'
2 | import Toggler from './component.jsx'
3 |
4 | export default class App extends Component {
5 | state = {
6 | show: true,
7 | }
8 |
9 | render() {
10 | const $show = createTwoWayBinding(this.state.show, (show) => this.setState({ show }))
11 | return
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.examples/two-way-binding/component.jsx:
--------------------------------------------------------------------------------
1 | import { React, Component, Button, Section } from 'nautil'
2 |
3 | export default class Toggler extends Component {
4 | static props = {
5 | $show: Boolean,
6 | }
7 | render() {
8 | return (
9 | <>
10 | {this.attrs.show ? : null}
11 | this.attrs.show = !this.attrs.show}>toggle
12 | >
13 | )
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 | on:
3 | push:
4 | ## only works on master branch
5 | branches:
6 | - master
7 | jobs:
8 | publish:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v2
12 | - name: Use Node.js 16.14.x
13 | uses: actions/setup-node@v1
14 | with:
15 | node-version: 16.14.x
16 | registry-url: https://registry.npmjs.org/
17 | - run: npm set config loglevel=info
18 | - run: npx can-npm-publish
19 | - name: Install
20 | run: npm i
21 | - name: Publish
22 | ## prepublishOnly has done build and test tasks
23 | uses: JS-DevTools/npm-publish@v1
24 | with:
25 | token: "${{ secrets.NPM_AUTH_TOKEN }}"
26 | - name: Git-tag
27 | uses: butlerlogic/action-autotag@stable
28 | with:
29 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
30 | tag_prefix: "v"
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | /index.js
3 | /*.map
4 | /lib/
5 | /dom/
6 | /native/
7 | /web-component/
8 | /wechat-miniprogram/
9 | /ssr/
10 | .test/
11 | .tmp/
12 | /dist/
13 | /wechat/
14 | /miniprogram_dist/
15 | .shell
16 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | docs/
3 | .build/
4 | .demo/
5 | .test/
6 | .examples/
7 | .babelrc
8 | .browserslistrc
9 | .tmp/
10 | .shell/
11 |
--------------------------------------------------------------------------------
/.scripts/get-wechat-bind.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 |
3 | const content = fs.readFileSync(__dirname + '/../src/wechat/components/dynamic/dynamic.wxml').toString()
4 | const matches = content.match(/bind(\w+)="/g)
5 | .map(item => item.replace('="', ''))
6 | .filter((item, i, arr) => arr.indexOf(item) === i)
7 | console.log(matches)
8 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.validate": [
3 | "javascript",
4 | "javascriptreact"
5 | ],
6 | "editor.codeActionsOnSave": {
7 | "source.fixAll.eslint": true,
8 | "source.fixAll.stylelint": true
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 2021-12-13
2 |
3 | - Rename `Controller.turn` to `Controller.reactive`
4 | - Use `algeb` as `DataService` driver
5 |
6 | ## 0.31.23 (Jun. 16, 2021)
7 | - supports wechat miniprogram by a React renderer
8 |
9 | ## 0.31.5 (May. 15, 2021)
10 |
11 | - add `evolve` decorator
12 | - add `Controller.turn`
13 |
14 | ## 0.29.11 (Apr. 30, 2021)
15 |
16 | - Redefine nautil to `Powerful Cross Platform Business System Frontend Framework` which emphasize MVC
17 | - fix some bugs
18 | - update Consumer with better performance
19 | - provide DataService, QueueService and HyperJSONService
20 |
21 | ## 0.23.0
22 |
23 | - Support HyperJSON (without export)
24 |
25 | ## 0.17.0 (Feb. 15, 2021)
26 |
27 | - Enhance useUniqueKeys
28 | - Support shared store by `applyStore`
29 | - Add `connect` as HOC generator of `Consumer`
30 | - Enhance `Component.update`
31 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env'],
4 | ['@babel/preset-react', { runtime: 'automatic' }],
5 | ],
6 | plugins: [
7 | ['@babel/plugin-transform-runtime', { regenerator: true }],
8 | ["@babel/plugin-proposal-class-properties"],
9 | ],
10 | }
11 |
--------------------------------------------------------------------------------
/docs/CNAME:
--------------------------------------------------------------------------------
1 | nautil.js.org
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # Nautil
2 |
3 |
4 |
5 | Enterprise Level Business System Frontend Framework
6 |
7 | - [Documents](https://nautil.js.org)
8 | - [中文文档](https://www.tangshuang.net/7273.html)
9 |
10 | ## What is all the Nautil?
11 |
12 | Nautil\[ˈnɔːtɪl] is a modern javascript frontend framework which helps you to build enterprise level business system by using familiar React syntax.
13 |
14 | The purpose of Nautil is to make complex business system development more systematic, easy and efficient.
15 |
16 | Nautil is built on React and is a framework, not a UI library. Developers can use React components in Nautil applications directly as possible. *Nautil is absolutely React, however, Nautil is more than React.* As a framework, it provides MVC architecture, router, state management, model management, event stream management, internationalization and ability of cross-platform.
17 |
18 | Without importing all the ecosystem of React, without complex redux, without any more choice of third part libraries, you will begin and build your application quickly with Nautil. Feel happy and relaxing when you writing with Nautil. It will work as what you think. You do not need to learn more than react. The only thing you need to know is some feature level api. There is no syntax level or higher knowledge to learn. Try it, I belive, you will fall in love with Nautil in 5 minutes.
19 |
20 | ## Ready for More?
21 |
22 | We've briefly introduced the most basic features of nautil.js core - the rest of this document will cover them and other advanced features with much finer details, so make sure to read through it all!
23 |
--------------------------------------------------------------------------------
/docs/api/animation.md:
--------------------------------------------------------------------------------
1 | # Animation
2 |
3 | ```js
4 | import { Animation } from 'nautil'
5 |
6 |
7 | ...
8 |
9 | ```
10 |
11 | ## props
12 |
13 | - show: Boolean,
14 | - enter: String,
15 | - leave: String,
16 | - loop: ifexist(Boolean), // when you pass loop=true, you should pass $show instead
17 | - onEnterStart
18 | - onEnterUpdate
19 | - onEnterStop
20 | - onLeaveStart
21 | - onLeaveUpdate
22 | - onLeaveStop
23 |
24 |
25 | `enter` and `leave` is a description for enter animation `...effect:params duration ease`.
26 |
27 | - fade:(from)/(to): in,out,
28 | - move:(from)/(to): left,right,top,bottom,(x,y)/(x,y)
29 | - rotate:(from)deg/(to)deg
30 | - scale:(from)/(to)
--------------------------------------------------------------------------------
/docs/api/create-async-component.md:
--------------------------------------------------------------------------------
1 | # createAsyncComponent
2 |
3 | ```js
4 | import { createAsyncComponent } from 'nautil'
5 |
6 | const Home = createAsyncComponent(() => import('./home.jsx'))
7 | const Detail = createAsyncComponent(() => import('./detail.jsx'))
8 | ```
9 |
10 | The imported file should export component as default.
11 |
--------------------------------------------------------------------------------
/docs/api/create-two-way-binding.md:
--------------------------------------------------------------------------------
1 | # createTwoWayBinding
2 |
3 | ```typescript
4 | declare function createTwoWayBinding(
5 | // object to convert to be two-way-binding
6 | data: object,
7 | // function to invoke when two-way-binding changed
8 | updator: (value: any, keyPath: string[], data: object) => void,
9 | // whether to generate formalized two-way-binding like [value, update]
10 | formalized?: boolean,
11 | ): Proxy
12 | ```
13 |
14 |
15 | ```js
16 | import { createTwoWayBinding, Component } from 'nautil'
17 |
18 | const $state = createTwoWayBinding(
19 | // data
20 | { show: false },
21 | // updator
22 | (value, keyPath, data) => {
23 | assign(data, keyPath, value)
24 | },
25 | // formalized
26 | true,
27 | )
28 |
29 | // $state.show -> [false, (value, key) => data[key] = value]
30 |
31 |
32 | ```
33 |
--------------------------------------------------------------------------------
/docs/api/storage.md:
--------------------------------------------------------------------------------
1 | # Storage
2 |
3 | ```js
4 | import { Storage } from 'nautil'
5 |
6 | await Storage.getItem(key)
7 | await Storage.setItem(key, value)
8 | await Storage.delItem(key)
9 | await Storage.clear()
10 | ```
11 |
--------------------------------------------------------------------------------
/docs/assets/nautil-lifecircle.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangshuang/nautil/1cdd52330d481aef417b7560796c9e68d3a42167/docs/assets/nautil-lifecircle.jpg
--------------------------------------------------------------------------------
/docs/assets/nautil-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangshuang/nautil/1cdd52330d481aef417b7560796c9e68d3a42167/docs/assets/nautil-logo.png
--------------------------------------------------------------------------------
/docs/beginning/install.md:
--------------------------------------------------------------------------------
1 | # Install
2 |
3 | ```
4 | npm i nautil
5 | ```
6 |
--------------------------------------------------------------------------------
/docs/beginning/package-usage.md:
--------------------------------------------------------------------------------
1 | # Package Usage
2 |
3 | ```js
4 | import { Component } from 'nautil'
5 | ```
6 |
7 | We recommand use `nautil-cli` to build even though it is not nessesary.
8 | Your building tool should must support rewrite `process.env.NODE_ENV` to real env vars.
9 |
10 | Nautil is using ES6 `Proxy` to implement reactive system, so your building targets should must support `Proxy`.
11 | This means nautil does not support low version browsers such as IE, earlier chrome or firefox.
12 | Here is a browserslist:
13 |
14 | ```browserslist
15 | chrome 49
16 | firefox 18
17 | edge 12
18 | safari 10
19 | opera 36
20 | ```
21 |
22 | To render Nautil components, you should import renderer from different platform exports:
23 |
24 | ```js
25 | import { mount, update, unmount } from 'nautil/dom'
26 | ```
27 |
28 | Nautil is based on react@^16.8 and use react@17.x inside, if your project are using higher version (i.e. react@18.x), you should use inner React to render nautil components:
29 |
30 | ```js
31 | import { React } from 'nautil'
32 | ```
33 |
34 | And `tyshemo` is a inner driver, if you need full APIs of tyshemo, you can import like:
35 |
36 | ```js
37 | import { TySheMo } from 'nautil'
38 |
39 | const { shouldmatch, String8 } = TySheMo
40 | ```
41 |
--------------------------------------------------------------------------------
/docs/cli/command.md:
--------------------------------------------------------------------------------
1 | # Nautil-CLI Command
2 |
3 | Nautil CLI is a high-level scaffolding tool. If you use nautil-cli, your project will follow the pattern of it. To clearify, you should think about whether to use it (without nautil-cli is ok to use nautil).
4 |
5 | You can call `nautil` as alias of `nautil-cli`, so I will use only `nautil` instead of `nautil-cli` in the following.
6 |
7 | ## init
8 |
9 | ```
10 | nautil init [--verbose]
11 | ```
12 |
13 | Initialize a new project or override an exisiting project.
14 |
15 | You should run `nautil init` in an empty directory or an exisiting project directory.
16 |
17 | ## build
18 |
19 | ```
20 | nautil build
21 | ```
22 |
23 | Build a app, `app` stands for a dir name in `src/apps` dir. The dest dir is `dist/`.
24 |
25 | Notice, `NODE_ENV` makes sense. For example:
26 |
27 | ```
28 | NODE_ENV=development nautil build dom
29 | ```
30 |
31 | This will make your code without minified.
32 |
33 | ## dev
34 |
35 | ```
36 | nautil dev
37 | ```
38 |
39 | Setup a devserver / watching task to help you develop and preview.
40 |
41 | ## install
42 |
43 | ```
44 | nautil install [pkg@version] [pkg@version] ...
45 | nautil i -g [pkg@version] ...
46 | nautil i -f
47 | ```
48 |
49 | Install some dependencies:
50 |
51 | - pkg: the package you want to install
52 | - -g, --global: install dependencies into nautil-cli inside, for globally installed nautil-cli
53 | - -f, --force: pkg and --global will be ignore, all required dependencies will be reinstalled
54 |
55 | ## run
56 |
57 | ```
58 | nautil run
59 | ```
60 |
61 | `io` can be `android` `ios` `electron`.
62 |
63 | ```
64 | nautil run my-app ios
65 | ```
66 |
67 | This will serve up a metro building to preview an ios app.
68 |
69 | ## pack
70 |
71 | ```
72 | nautil pack
73 | ```
74 |
--------------------------------------------------------------------------------
/docs/cli/env-vars.md:
--------------------------------------------------------------------------------
1 | # Env Vars
2 |
3 | You can use `process.env.NODE_ENV` in your code and it will be replaced with given env vars.
4 |
5 | There are 3 way to give env vars:
6 |
7 | **.env**
8 |
9 | Create a `.env` file into your project root dir.
10 |
11 | It should follow [dotenv](https://www.npmjs.com/package/dotenv) rules.
12 |
13 | You will find a `.env_sample` file in your project dir after init.
14 |
15 | **export**
16 |
17 | Give export in CLI directly:
18 |
19 | ```
20 | NODE_ENV=development nautil build dom
21 | ```
22 |
23 | **define**
24 |
25 | Provide `define` filed in [cli-config.json](./config.md)
26 |
--------------------------------------------------------------------------------
/docs/cli/install.md:
--------------------------------------------------------------------------------
1 | # Nautil-CLI Install
2 |
3 | To install globally, so that you can use it again quickly:
4 |
5 | ```
6 | npm i -g nautil-cli
7 | ```
8 |
9 | To generate project only once:
10 |
11 | ```
12 | npx nautil-cli init
13 | ```
14 |
15 | This will install nautil-cli in your project as a devDependency.
16 |
17 | To use locally in your project:
18 |
19 | ```
20 | npm i -D nautil-cli
21 | ```
22 |
23 | With this, you should must use nautil-cli in npm scripts.
24 |
--------------------------------------------------------------------------------
/docs/cli/ts.md:
--------------------------------------------------------------------------------
1 | # TypeScript
2 |
3 | It is easy to support typescript with Nautil-CLI.
4 |
5 | First of all, you should enable typescript supporting in `.nautil/cli-config.json`.
6 |
7 | After you enable this config, and run `nautil dev `, it will install typescript dependencies for you automaticly.
8 |
9 | Then modify `tsconfig.json` in the project root directory to match your project requirement.
10 |
11 | Next create .ts files to export modules in src dir.
12 | Notice that, index.js should Must be .js file, it is entry file, you should write less code (no logic code) in it, and import .ts files in it.
13 | Nauti-CLI support .ts, .tsx files.
14 |
15 | Finally run `nautil build ` or `nautil dev ` to check whether it works.
16 | Build task will emit error and break out when checking fail.
17 | Dev task will only emit error in CLI and not emit error in browser, so you should must read error infomation in your commander when you developing.
18 |
--------------------------------------------------------------------------------
/docs/component/attrs.md:
--------------------------------------------------------------------------------
1 | # Attrs
2 |
3 | There is a `attrs` property on the component instance.
4 |
5 | ```js
6 | class SomeComponent extends Component {
7 | render() {
8 | const { some } = this.attrs
9 | }
10 | }
11 | ```
12 |
13 | It is a sub-set of `props`. It is from `props` but not the same. It contains:
14 |
15 | ```
16 |
17 |
18 | +---------------+--------------+
19 | | props | attrs |
20 | +---------------+--------------+
21 | | | |
22 | | one ----> one |
23 | | | |
24 | | $two ----> two |
25 | | | |
26 | | onSee | x |
27 | | | |
28 | +---------------+--------------+
29 | ```
30 |
31 | In the `Some` component, we can read `this.attrs.one` and `this.attrs.two`, `onSee` and `$two` are invisible. `this.attrs.two` is the real value of `this.props.$two[0]`.
32 |
33 | And a `this.$attrs` is availiable, it can be changed to trigger two way binding updator.
34 |
35 | ```js
36 | class SomeComponent extends Component {
37 | render() {
38 | return this.$attrs.two ++}>change
39 | }
40 | }
41 | ```
42 |
43 | ```js
44 | const $two = useState(0)
45 |
46 | ```
47 |
48 | `$attrs` is a Proxy, you can read value from it, however objects are not equal to origin data.
49 |
--------------------------------------------------------------------------------
/docs/component/class-or-function.md:
--------------------------------------------------------------------------------
1 | # Class or Function?
2 |
3 | > Should we use class components? Can we use functional components?
4 |
5 | Although we are try to support react completely, we had some difficulty to face. Remember the following rules:
6 |
7 | **1. Class components is much more strong than functional components!**
8 |
9 | **2. Two way binding only works for class components.**
10 |
11 | You should must create a class component to receive two way binding props, function components have no ability of nautil.
12 |
13 | ```js
14 | // bad
15 | function MyComponent(props) {
16 | return props.age ++ /* not support */}>grow
17 | }
18 |
19 | // good
20 | class MyComponent extends Component {
21 | static props = {
22 | $age: Number,
23 | }
24 |
25 | render() {
26 | const { age } = this.attrs
27 | return this.attrs.age ++}>grow
28 | }
29 | }
30 | ```
31 |
32 | **3. `stylesheet`, `props`, `defaultStylesheet` and event-stream only works for class component.**
33 |
34 | ```js
35 | // bad
36 | function A() {
37 | return xxx
38 | }
39 | A.props = {}
40 | A.defaultStylesheet = []
41 |
42 | // ok
43 | function A() {
44 | return
45 | }
46 | // bad
47 | A.props = {}
48 | A.defaultStylesheet = []
49 |
50 | // good
51 | class A extends Component {
52 | static props = {}
53 | static defaultStylesheet = []
54 |
55 | render() {
56 | return
57 | }
58 | }
59 | ```
60 |
61 | ## Use hooks in Class component.
62 |
63 | To use hooks functions in class component, you should must set `Render` method then `render`.
64 |
65 | ```js
66 | class Some extends Component {
67 | Render(props) {
68 | // use hooks directly here
69 | const [value, setState] = useState('')
70 | return
71 | }
72 | }
73 | ```
74 |
--------------------------------------------------------------------------------
/docs/component/lifecircle.md:
--------------------------------------------------------------------------------
1 | # Lifecircle
2 |
3 | A little different from react, Nautil has its own lifecircle:
4 |
5 | - ===== mount: =======
6 | - init
7 | - onDigested
8 | - onInit
9 | - render
10 | - onRender
11 | - shouldAffect
12 | - onAffect
13 | - onMounted
14 | - onAffected
15 | - ====== update: =======
16 | - shouldUpdate
17 | - onNotUpdate
18 | - onUpdate
19 | - onDigested
20 | - render
21 | - onRender
22 | - shouldAffect
23 | - onAffect
24 | - onUpdated
25 | - onAffected
26 | - ====== unmount: =======
27 | - onUnmount
28 | - ====== error: =======
29 | - onCatch
30 |
31 | Details:
32 |
33 | - init: when `constructor` run, use `init` so that you do not need to call `super` in `constructor`
34 | - onDigested: after this.attrs, this.className, this.style, this.context... generated, during onDigested, `onParseProps` is triggered, you can use it to transform props to generate different this.attrs, this.className and this.style
35 | - onInit: after onDigested immediately when mount
36 | - shouldUpdate: determine whether to rerender, should return boolean or an array, if return an array means affect when the array is shallow equal to previous, if not equal, to rerender
37 | - onRender: you have the chance to change the VDOM here, should return new VDOM
38 | - shouldAffect: detect whether to affect, should must return an array or `true`, if `true` means going to affect, if return an array means affect when the array is shallow equal to previous
39 | - onAffect: will be invoked before onMounted/onUpdated
40 | - onAffected: be invoked after onAffect and onMounted/onUpdated
41 |
42 | 
43 |
44 | Nautil lifecircle functions should not use together with react component life circle functions (except getDerivedStateFromProps and getSnapshotBeforeUpdate).
45 |
46 | For Nautil components, they will run a `digest` task to generate derived properties such as `attrs` `style` `className` and so on. After this task, before `render`, a `onDigested` lifecircle function will be called.
47 |
--------------------------------------------------------------------------------
/docs/component/props-type.md:
--------------------------------------------------------------------------------
1 | # Props-Type
2 |
3 | This paper will tell you how to set props type. As default, you can use react prop-types as what you did in your react application.
4 | However, we developed this, based on [tyshemo](http://github.com/tangshuang/tyshemo), we can check the data structure of props.
5 | ## Declare props type
6 |
7 | As you learned, we have 3 types of props in nautil: normal, two-way-binding and event-stream. How to declare each type?
8 |
9 | ```js
10 | import { Component } from 'nautil'
11 |
12 | export default class SomeComponent extends Component {
13 | static props = {
14 | // normal
15 | name: String,
16 | // tow-way-binding
17 | $show: Boolean,
18 | // event stream handler, only pass `true` to make it work
19 | onHit: true,
20 | }
21 | }
22 | ```
23 |
24 | ## Define props type
25 |
26 | Props-type system support deep nested object checking.
27 |
28 | ```js
29 | import { Component } from 'nautil'
30 |
31 | export default class SomeComponent extends Component {
32 | static props = {
33 | some: {
34 | name: String,
35 | age: Number,
36 | },
37 | }
38 | }
39 | ```
40 |
41 | Some logic can be inject:
42 |
43 | ```js
44 | import { Component } from 'nautil'
45 | import { ifexist } from 'tyshemo'
46 |
47 | export default class SomeComponent extends Component {
48 | static props = {
49 | some: {
50 | name: ifexist(String),
51 | age: Number,
52 | },
53 | }
54 | }
55 | ```
56 |
57 | Read [more](https://tyshemo.js.org) for type checking.
58 |
59 | ## Optimization
60 |
61 | `props` static property only works in development mode, when you run CLI in production mode, props checking will be dropped, and the `props` static property will be removed from source code by Nautil-CLI.
62 |
63 | ## Async Checking
64 |
65 | If you give `props` as a function, props type checking will run asyncly.
66 |
67 | ```js
68 | class SomeComponent extends Component {
69 | static props = () => ({
70 | name: String,
71 | age: Number,
72 | })
73 | }
74 | ```
75 |
--------------------------------------------------------------------------------
/docs/component/props.md:
--------------------------------------------------------------------------------
1 | # Props
2 |
3 | You can define a static property in Nautil Component class called `props` to declare the received props and their types.
4 |
5 | ```js
6 | import { Component } from 'nautil'
7 |
8 | export class MyComponent extends Component {
9 | static props = {
10 | // normal prop
11 | some: String,
12 |
13 | // object prop
14 | any: {
15 | name: String,
16 | age: Number,
17 | },
18 |
19 | // two-way-binding prop
20 | $show: Boolean,
21 |
22 | // event stream prop, there is no need to declare the real type, only pass `true`
23 | onClick: true,
24 | }
25 | }
26 | ```
27 |
28 | Type checking is following [tyshemo](https://github.com/tangshuang/tyshemo) which is a data schema system in runtime. We will learn more about props type [here](props-type.md).
29 |
30 | You can declare 3 kinds of prop:
31 |
32 | - normal prop
33 | - `$` beginning which is two-way-binding prop
34 | - `on` beginning which is event stream prop
35 |
36 | After you declare the props, you can use `defaultProps` to give the default values.
37 |
--------------------------------------------------------------------------------
/docs/component/stylesheet.md:
--------------------------------------------------------------------------------
1 | # Stylesheet
2 |
3 | In nautil.js, we praise css module, which is easy to implement cross-platform.
4 |
5 | ```js
6 | import { Section, Text, Component } from 'nautil'
7 | import * as Styles from './my-component.css'
8 |
9 | export default class MyComponent extends Component {
10 | render() {
11 | return (
12 |
16 | )
17 | }
18 | static defaultStylesheet = [
19 | 'some-classname',
20 | { color: 'red' },
21 | ]
22 | }
23 | ```
24 |
25 | Read the previous code, you can understand it very easily. You will notice points:
26 |
27 | - import .css as CSS Module
28 | - use a `stylesheet` prop
29 | - mixing style object and className in an array
30 | - this.className
31 | - this.style
32 | - this.css
33 | - static defaultStylesheet
34 | - static css
35 |
36 | **CSS Modules**
37 |
38 | In nautil, we praise CSS Module, and recommend to use CSS Module at the first.
39 | It is not recommended to use style object in react. So use styles in css file.
40 |
41 | **stylesheet**
42 |
43 | A Nautil Class Component can receive a stylesheet prop. The value will be parse automaticly, it can receive all kinds of style expression in web.
44 |
45 | - string: as className
46 | - object:
47 | - boolean: as className
48 | - normal: as style rules
49 | - array: mixing
50 |
51 | When you pass an object, it dependents on the value of each property. When the value is a boolean, it means you want to toggle this className.
52 |
53 | ```js
54 |
55 | ```
56 |
57 | **this.className**
58 |
59 | Get current component's `styesheet` parsed classNames to be a string.
60 |
61 | **this.style**
62 |
63 | Get current component's `stylesheet` parsed style object.
64 |
65 | **static defaultStylesheet**
66 |
67 | Prefix stylesheet for current component before render.
68 |
69 | **static css & this.css**
70 |
71 | A component has this.css inside which is generated based on `static css`.
72 |
73 | ```js
74 | class MyComponent extends Component {
75 | static css = {
76 | a: '__a',
77 | b: '__b',
78 | c: { width: 100, height: 90 },
79 | }
80 |
81 | render() {
82 | return (
83 |
86 | )
87 | }
88 | }
89 | ```
90 |
91 | To generate this.css dynamicly, you can pass `css` as a function:
92 |
93 | ```js
94 | class MyComponent extends Component {
95 | static css = ({ attrs, style, className }) => ({
96 | a: attrs.a ? '__a' : undefined,
97 | b: '__b',
98 | c: { width: 100, height: 90 },
99 | })
100 | }
101 | ```
102 |
103 | `css` is always used with CSS Modules together:
104 |
105 | ```js
106 | import * as SomeCss from 'some.css'
107 |
108 | class MyComponent extends Component {
109 | static css = SomeCss
110 | }
111 | ```
112 |
113 | This help us to generate cross-platform styles by only one css file.
114 |
--------------------------------------------------------------------------------
/docs/components/async.md:
--------------------------------------------------------------------------------
1 | # Async
2 |
3 | ```js
4 | new Promise(...)}
6 | then={data => {data.text} }
7 | catch={e => {e.message} }
8 | pending={ }
9 | />
10 | ```
11 |
--------------------------------------------------------------------------------
/docs/components/each.md:
--------------------------------------------------------------------------------
1 | # Each
2 |
3 | ```js
4 |
5 | {key}: {value}
6 | } />
7 | ```
8 |
--------------------------------------------------------------------------------
/docs/components/for.md:
--------------------------------------------------------------------------------
1 | # For
2 |
3 | ```js
4 |
5 | {i}
6 | } />
7 | ```
8 |
9 | Notice that, the range contains `end`.
10 |
--------------------------------------------------------------------------------
/docs/components/if-else.md:
--------------------------------------------------------------------------------
1 | # If/ElseIf/Else
2 |
3 | Render by given condition.
4 |
5 | ## If
6 |
7 | Render when `is` prop is true.
8 |
9 | ```js
10 |
11 | ```
12 |
13 | or
14 |
15 | ```js
16 |
17 | {Function}
18 |
19 | ```
20 |
21 | The `render` function, should return a Nautil Element. It is like `render` method of Nautil component.
22 |
23 | ## ElseIf
24 |
25 | It is a sub component of `If` component. You should use it inside `If`.
26 |
27 | ```js
28 |
29 |
30 |
31 | ```
32 |
33 | ## Else
34 |
35 | The same as `ElseIf` only without `is` prop.
36 |
37 | ```js
38 |
39 |
40 |
41 |
42 | ```
43 |
--------------------------------------------------------------------------------
/docs/components/observer.md:
--------------------------------------------------------------------------------
1 | # Observer
2 |
3 | This component is very important in Nautil.
4 | It is the power to make observer pattern work in your application.
5 |
6 | **props**
7 |
8 | - subscribe
9 | - unsubscribe
10 | - dispatch
11 | - render|children function
12 |
13 | ```js
14 | store.subscribe(dispatch)}
16 | unsubscribe={dispatch => store.unsubscribe(dispatch)}
17 | dispatch={this.update}
18 | >
19 | {() => {store.state.some} }
20 |
21 | ```
22 |
23 | Notice the `dispatch` function which passed into `subscribe`/`unsubscribe` prop. In fact, it is really the function you passed into `dispatch` prop.
24 |
--------------------------------------------------------------------------------
/docs/components/prepare.md:
--------------------------------------------------------------------------------
1 | # Prepare
2 |
3 | ```js
4 | loading} render={() =>
5 | loaded
6 | } />
7 | ```
8 |
--------------------------------------------------------------------------------
/docs/components/static.md:
--------------------------------------------------------------------------------
1 | # Static
2 |
3 | ```js
4 |
5 | {Date.now()}
6 | } />
7 | ```
8 |
9 | When `shouldUpdate` is `false`, the render result will not change. If change to `true`, the render will change.
10 |
11 | `shouldUpdate` can be:
12 |
13 | - boolean: true to update, false to keep static
14 | - array: if the passed array items are equal, to update, or to keep static, like `detectEffect`
15 | - function: function to return true or false, true to update
16 |
--------------------------------------------------------------------------------
/docs/components/switch-case.md:
--------------------------------------------------------------------------------
1 | # Switch/Case
2 |
3 | Render by given condition.
4 |
5 | ## Switch
6 |
7 | ```js
8 |
9 |
10 |
11 | ```
12 |
13 | When the value of `Case.is` equals `Switch.of`, use the `render` function to render.
14 |
15 | ## Case
16 |
17 | ```js
18 |
19 | ```
20 |
21 | - is: when equals `Switch.of`, render it
22 | - default: if all previous not match, use this render
23 | - break: if match, render this, and stop going down
24 |
25 | ```js
26 |
27 |
28 |
29 |
30 | ```
31 |
32 | Let's look this to learn about `break`. When `index%3 === 1`, it will **only** render first branch, or it will render the **both** branch
33 |
--------------------------------------------------------------------------------
/docs/concepts/css-module.md:
--------------------------------------------------------------------------------
1 | # CSS Module
2 |
3 | You'd better use [CSS Module](https://css-tricks.com/css-modules-part-1-need/) in Nautil. According to the [repo](https://github.com/css-modules/css-modules), CSS modules are:
4 |
5 | > CSS files in which all class names and animation names are scoped locally by default.
6 |
7 | Why should you use CSS Module in Nautil?
8 | It is much easier to find out from which css file the rules come. Another reason is becuase of cross-platform. It is very easy to load css rules as objects by using building tools, if you do not use CSS Module, there is no way to cross platform with one code.
9 |
10 | If you use nautil-cli, it is automaticly to import a css file as CSS Module:
11 |
12 | ```js
13 | import * as Css from './some.css' // -> CSS Module
14 | import './any.css' // -> global css import
15 | ```
16 |
--------------------------------------------------------------------------------
/docs/concepts/data-type-system.md:
--------------------------------------------------------------------------------
1 | # Data Type System
2 |
3 | To check type and structure of a data, Nautil uses [tyshemo](https://github.com/tangshuang/tyshemo). You can give a `props` static property to a class component so that props will be checked.
4 |
5 | ```js
6 | import BookType from '../types/book.type.js'
7 |
8 | class MyComponent extends Component {
9 | static props = {
10 | data: {
11 | name: String,
12 | age: Number,
13 | books: [BookType],
14 | },
15 | }
16 | }
17 | ```
18 |
19 | As you seen, it is very similar to real data structure, other developers can understand your props structure easily.
20 |
--------------------------------------------------------------------------------
/docs/concepts/observer-pattern.md:
--------------------------------------------------------------------------------
1 | # Observer Pattern
2 |
3 | [Observer Pattern](https://en.wikipedia.org/wiki/Observer_pattern) is the most important concepts you should keep in mind when you use Nautil. It is so important that already all abilities are built on it.
4 |
5 | > The observer pattern is a software design pattern in which an object, called the **subject**, maintains a list of its dependents, called **observers**, and notifies them automatically of any state changes, usually by calling one of their methods.
6 |
7 | Normally, we create a subject and pass a function into it, this function is a method of an observer. When the subject changes, the method will be called, so that the observer will change too.
8 |
9 | Let's look a simple example:
10 |
11 | ```js
12 | store.on('*', dispatch)}
15 | unsubscribe={dispatch => store.off('*', dispatch)}
16 | >
17 | {store.get('some')}
18 |
19 | ```
20 |
21 | In Nautil, we use `Observer` component as an *observer*, here `store` is a subject. `Observer` component receive a `dispatch` prop to define its *method*. The `subscribe` prop is the action to give the *method* to the *subject*. After `Observer` component mounted, the subscribe function will be called, so that the *subject* `store` will put the *method* `dispatch` in the dependents' notifies list. When `store` changes, `dispatch` which equals `this.update` will be called. Then the UI will be rerendered.
22 |
--------------------------------------------------------------------------------
/docs/concepts/stream.md:
--------------------------------------------------------------------------------
1 | # Stream
2 |
3 | [Rxjs](https://github.com/ReactiveX/RxJS) is used in Nautil to handle event streams. But what is a stream? A stream is data points separated by time. Read [this article](https://javascript.tutorialhorizon.com/2017/04/28/rxjs-tutorial-getting-started-with-rxjs-and-streams/) to know what and [this page](https://rxjs.dev/guide/operators) to know how.
4 |
5 | When you handle an event, you can pass a callback function, or, the deep usage, a stream pipe-chain and execution.
6 |
7 | ```
8 | [operator1, operator2, ...operators, execution]
9 | ```
10 |
11 | ```js
12 | import { map } from 'rxjs'
13 |
14 | e.target.value),
19 | map(value => value ++),
20 |
21 | // subscriber
22 | value => console.log(value)
23 | ]}
24 | />
25 | ```
26 |
27 | If you pass an array, the last item should must be a function which pass into `stream$.subscribe`.
28 |
29 | ```js
30 | import { Stream } from 'nautil'
31 |
32 | const change$ = new Stream()
33 | change$.pipe(
34 | map(e => e.target.value),
35 | map(value => value ++),
36 | ).subscribe(
37 | value => console.log(value)
38 | )
39 |
40 |
44 | ```
45 |
46 | By supporting this pattern, you will be able to seperate your event stream from UX handlers.
47 |
--------------------------------------------------------------------------------
/docs/concepts/two-way-binding.md:
--------------------------------------------------------------------------------
1 | # Two-Way-Binding
2 |
3 | There is no strict defination of **Two Way Binding**. In short, it is about a reactive proposal between view and model, which describes [when changing model changes the view and changing the view changes the model](https://medium.com/front-end-weekly/what-is-2-way-data-binding-44dd8082e48e).
4 |
5 | In react, data only goes one way, from parent component to child component by passing props. And the main voice in community is immutable data. However, it is not comfortable when we are going to build a intertwined application. In fact, redux is not good enough to solve the problem, it is to complex to write many non-business codes. We want an easy way.
6 |
7 | In Nautil, we can use Two Way Binding. Let's have a look:
8 |
9 | ```js
10 | const $some = useState(some)
11 |
12 | ```
13 |
14 | The previous code is very simple, however it is very powerful. You do not need to care about what it will do inside `Input`. It will give you right UI response when value of input changed. In the document of Two Way Binding, I will introduce the whole face of it.
15 |
16 | Relate APIs:
17 |
18 | - createTwoWayBinding(data, updator, formalized)
19 | - useTwoWayBinding(data, updator, formalized)
20 | - useTwoWayBindingAttrs(props)
21 | - useTwoWayBindingState(initState)
22 |
--------------------------------------------------------------------------------
/docs/controller/controller.md:
--------------------------------------------------------------------------------
1 | # Controller
2 |
3 | What is Controller in Nautil? It is in fact a type of special model, which controls view's data and reactive (event handlers). In Nautil, a controller orient a business scenes.
4 |
5 | In many business scenes, we have a same controller, but should act different views. The Controller is the way to keep the business logic same in one system, and be used in different views. For example, you have a payment module in your system, and have PC and App clients, however, business logic should must be same on both client sides. Here, you should create a controller which contains the same business logic, and use it on both client side views.
6 |
7 | ## Usage
8 |
9 | ```js
10 | import { Controller, Model, Service } from 'nautil'
11 |
12 | class SomeModel extends Model {}
13 |
14 | class SomeService extends Service {}
15 |
16 | class SomeController extends Controller {
17 | static someModel = SomeModel
18 | static someService = SomeService
19 |
20 | static increase$(stream) {
21 | stream.subscribe(() => {
22 | this.someModel.some = 'xxx'
23 | })
24 | }
25 |
26 | handleRemove() {
27 | ...
28 | }
29 | }
30 | ```
31 |
32 | `Controller` is a helpfull tool in nautil, it is designed to control a business area in one place.
33 | In a controller, you can define Model, Service, Events, Components and scoped handlers.
34 | The exported components from a controller can be used in other components in Nautil, the exported components are treated as business components but with small code size.
35 |
36 | When define a Controller, you should `extends` from `Controller` class, and given static properties. For example:
37 |
38 | ```js
39 | class SomeController extends Controller {
40 | static someModel = SomeModel // Model -> this.someModel
41 | static someDataService = SomeDataService // DataService -> this.someDataService
42 | static someService = SomeService // Service -> this.someService (single instance)
43 | static count$(stream$) { // stream$() -> this.count$
44 | // here you can use this point to Controller instance
45 | stream$.pipe(...).subscribe(...)
46 | }
47 | static store = Store // Store -> this.store, used as state helper
48 |
49 | // after Controller initialized
50 | init() {}
51 |
52 | // provide an API method
53 | decreaseCount() {
54 | this.count$.next(...)
55 | }
56 | }
57 | ```
58 |
59 | **You should never operate UI in controller.** A controller is an API set to provide to views, so you should keep in mind that it is an independent system from UI operation. And this is the way to seperate business logic from React components, make business logic management as an independent job.
60 |
61 | ## API
62 |
63 | ### observe(observer: Model | Store | Function) -> { start, stop }
64 |
65 | Observe other observable objects which are not put inside controller, for example:
66 |
67 | ```js
68 | controller.observe((dispatch) => {
69 | const timer = setInterval(dispatch, 5000)
70 | return () => clearInterval(timer)
71 | })
72 | ```
73 |
74 | After this, controller will react each 5s.
75 |
--------------------------------------------------------------------------------
/docs/decorators/decorate.md:
--------------------------------------------------------------------------------
1 | # decorate
2 |
3 | ```js
4 | function H(props) {
5 | const {
6 | render,
7 | } = props
8 | const state = 1
9 | const data = 2
10 | return render(state, data)
11 | }
12 |
13 | function C(props) {
14 | const { state, data } = props
15 | return null
16 | }
17 |
18 | export default decorate(H, ['state', 'data'], 'render')(C) // ->
19 | /**
20 | * function Wrapped(props) {
21 | * return
22 | *
23 | * } />
24 | * }
25 | */
26 | ```
27 |
28 | sign:
29 |
30 | ```
31 | function decorate(HOC: ComponentType, params: string[], renderProp: string): ComponentType
32 | ```
33 |
34 | - HOC: the higher order component to wrapper inner component
35 | - params: names which read from render function's params and patch to inner component's props, the values is provided by HOC
36 | - renderProp: the prop name of HOC render function, if renderProp is not passed, `children` should be a function to receive
37 |
38 |
39 | For example, `Consumer`'s render function is`(value) => ...`, you should pass `['value']` to params, and the inner component will receive `const { value } = props`.
40 |
--------------------------------------------------------------------------------
/docs/decorators/evolve.md:
--------------------------------------------------------------------------------
1 | # evolve
2 |
3 |
4 | ```js
5 | import { evolve } from 'nautil'
6 |
7 | const EvolutionComponent = evolve((props) => {
8 | const { a, b } = props
9 | return { a, b }
10 | })(OriginalComponent)
11 | ```
12 |
13 | In the previous code block, EvolutionComponent will only rerender when the { a, b } is different from previous { a, b }.
14 |
--------------------------------------------------------------------------------
/docs/decorators/initialize.md:
--------------------------------------------------------------------------------
1 | # initialize
2 |
3 | ```js
4 | import { initialize } from 'nautil'
5 |
6 | const WrappedComponent = initialize('some', Some)(OriginalComponent)
7 | ```
--------------------------------------------------------------------------------
/docs/decorators/inject.md:
--------------------------------------------------------------------------------
1 | # inject
2 |
3 | ```js
4 | import { inject } from 'nautil'
5 |
6 | const WrappedComponent = inject('now', () => Date.now())(OriginalComponent)
7 | ```
--------------------------------------------------------------------------------
/docs/decorators/nest.md:
--------------------------------------------------------------------------------
1 | # nest
2 |
3 | ```js
4 | import { nest } from 'nautil'
5 |
6 | const WrappedComponent = nest([
7 | [Provider, { store }],
8 | [Language, { i18n }],
9 | ])(OriginalComponent)
10 | ```
11 |
12 | => output:
13 |
14 | ```js
15 |
16 |
17 |
18 |
19 |
20 | ```
21 |
22 | - options:
23 | - Component
24 | - props: object or function to return object
25 |
26 | ```js
27 | import { nest } from 'nautil'
28 |
29 | const WrappedComponent = nest([
30 | [Provider, (props) => ({ store })],
31 | [Language, (props) => ({ i18n })],
32 | ])(OriginalComponent)
33 | ```
34 |
--------------------------------------------------------------------------------
/docs/decorators/observe.md:
--------------------------------------------------------------------------------
1 | # observe
2 |
3 | ```js
4 | import { observe } from 'nautil'
5 |
6 | const WrappedComponent = observe(
7 | dispatch => store.subscribe(dispatch),
8 | dispatch => store.unsubscribe(dispatch),
9 | )(OriginalComponent)
10 | ```
--------------------------------------------------------------------------------
/docs/decorators/pipe.md:
--------------------------------------------------------------------------------
1 | # pipe
2 |
3 | ```js
4 | import { pipe, observe, initialize } from 'nautil'
5 |
6 | const operate = pipe([
7 | observe(subscribe, unsubscribe),
8 | initialize('i18n', I18nController, { i18n }),
9 | ])
10 |
11 | const WrappedComponent = operate(OriginalComponent)
12 | ```
13 |
--------------------------------------------------------------------------------
/docs/elements/audio.md:
--------------------------------------------------------------------------------
1 | # Audio
2 |
3 | ```js
4 | import { Audio } from 'nautil'
5 |
6 |
7 | ```
8 |
9 | ## props
10 |
11 | - source: enumerate([String, Object]),
12 | - width: Unit,
13 | - height: Unit,
14 | - onPlay
15 | - onPause
16 | - onStop
17 | - onDrag
18 | - onResume
19 | - onReload
20 | - onLoad
21 | - onTick
22 | - onVolume
--------------------------------------------------------------------------------
/docs/elements/button.md:
--------------------------------------------------------------------------------
1 | # Button
2 |
3 | ```js
4 | import { Button } from 'nautil'
5 |
6 | click
7 | ```
8 |
9 | ## props
10 |
11 | - onHit
12 | - onHitStart
13 | - onHitEnd
--------------------------------------------------------------------------------
/docs/elements/checkbox.md:
--------------------------------------------------------------------------------
1 | # Checkbox
2 |
3 | ```js
4 | import { Checkbox } from 'nautil'
5 |
6 |
7 | ```
8 |
9 | ## props
10 |
11 | - checked: Boolean
12 | - onCheck
13 | - onUncheck
14 | - onChange
15 |
16 | `Checkbox` supports two-way-binding:
17 |
18 | ```js
19 |
20 | ```
--------------------------------------------------------------------------------
/docs/elements/form.md:
--------------------------------------------------------------------------------
1 | # Form
2 |
3 | ```js
4 | import { Form } from 'nautil'
5 |
6 |
9 | ```
10 |
11 | ## props
12 |
13 | - onChange
14 | - onReset
15 | - onSubmit
16 |
--------------------------------------------------------------------------------
/docs/elements/image.md:
--------------------------------------------------------------------------------
1 | # Image
2 |
3 | ```js
4 | import { Image } from 'nautil'
5 |
6 | ```
7 |
8 | ## props
9 |
10 | - source: enumerate([String, dict({ uri: String })]),
11 | - width: Unit,
12 | - height: Unit,
13 | - maxWidth: ifexist(Unit),
14 | - maxHeight: ifexist(Unit),
15 | - onLoad
16 |
--------------------------------------------------------------------------------
/docs/elements/input.md:
--------------------------------------------------------------------------------
1 | # Input
2 |
3 | ```js
4 | import { Input } from 'nautil'
5 |
6 |
7 | ```
8 |
9 | ## props
10 |
11 | - type: enumerate(['text', 'number', 'email', 'tel', 'url']) // dont support `date` or `range` right now
12 | - placeholder: ifexist(String)
13 | - value: enumerate([String, Number])
14 | - onChange
15 | - onFocus
16 | - onBlur
17 | - onSelect
18 |
19 | `Input` support two-way-binding:
20 |
21 | ```js
22 | function Some() {
23 | const value = useState('')
24 |
25 | return
26 | }
27 | ```
--------------------------------------------------------------------------------
/docs/elements/line.md:
--------------------------------------------------------------------------------
1 | # Line
2 |
3 | ```js
4 | import { Line } from 'nautil'
5 |
6 |
7 | ```
8 |
9 | ## props
10 |
11 | - length: Number
12 | - thick: Number
13 | - color: String
--------------------------------------------------------------------------------
/docs/elements/list-section.md:
--------------------------------------------------------------------------------
1 | # ListSection
2 |
3 | A composition component which used to render a list.
4 |
5 | ```js
6 | import { ListSection } from 'nautil'
7 |
8 | ...}
11 | itemKey="id"
12 | itemStyle={{ ... }}
13 | />
14 | ```
15 |
16 | It optimize the implement of list render inside, so you can render a large list once.
--------------------------------------------------------------------------------
/docs/elements/radio.md:
--------------------------------------------------------------------------------
1 | # Radio
2 |
3 | ```js
4 | import { Radio } from 'nautil'
5 |
6 |
7 | ```
8 |
9 | ## props
10 |
11 | - checked: Boolean
12 | - onCheck
13 | - onUncheck
14 | - onChange
15 |
16 | `Radio` supports two-way-binding:
17 |
18 | ```js
19 |
20 | ```
--------------------------------------------------------------------------------
/docs/elements/scroll-section.md:
--------------------------------------------------------------------------------
1 | # ScrollSection
2 |
3 | A composition component which can be scrolled.
4 |
5 | ```js
6 | import { ScrollSection } from 'nautil'
7 |
8 | ,
18 | finish: 'finish',
19 | }}
20 | onTopRelease={fetchData}
21 | >
22 | ...
23 |
24 | ```
25 |
26 | ## props
27 |
28 | - direction: enumerate([UP, DOWN, BOTH, NONE]),
29 | - distance: Number,
30 | - damping: range({ min: 0, max: 1 }),
31 | - topLoading: Boolean,
32 | - topIndicator:
33 | - [ACTIVATE]: Any,
34 | - [DEACTIVATE]: Any,
35 | - [RELEASE]: Any,
36 | - [FINISH]: Any,
37 | - topIndicatorStyle: enumerate([Object, String]),
38 | - onTopRelease: Function,
39 | - bottomLoading: Boolean,
40 | - bottomIndicator: {
41 | - [ACTIVATE]: Any,
42 | - [DEACTIVATE]: Any,
43 | - [RELEASE]: Any,
44 | - [FINISH]: Any,
45 | - bottomIndicatorStyle: enumerate([Object, String]),
46 | - onBottomRelease: Function,
47 | - onScroll: Function,
48 | - containerStyle: enumerate([Object, String]),
49 | - contentStyle: enumerate([Object, String]),
50 |
51 | ```js
52 | const { UP, DOWN, BOTH, NONE, ACTIVATE, DEACTIVATE, RELEASE, FINISH } = ScrollSection
53 | ```
54 |
55 | Indicators are content to show in top or bottom area.
56 |
57 | - DEACTIVATE: when users pull down/up but not reach the `distance`
58 | - ACTIVATE: when users pull down/up and reach the `distance`
59 | - RELEASE: when users pull down/up and reach the `distance` and release their fingers, at the same time, onTopRelase/onBottomRelease will be fired, here, you should must set topLoading/bottomLoading from `false` to `true`
60 | - FINISH: after topLoading/bottomLoading change from `true` to `false`
--------------------------------------------------------------------------------
/docs/elements/section.md:
--------------------------------------------------------------------------------
1 | # Section
2 |
3 | ```js
4 | import { Section } from 'nautil'
5 |
6 |
9 | ```
10 |
11 | ## Props
12 |
13 | - onHit
14 | - onHitStart
15 | - onHitMove
16 | - onHitEnd
17 | - onHitCancel
18 | - onHitOutside
--------------------------------------------------------------------------------
/docs/elements/select.md:
--------------------------------------------------------------------------------
1 | # Select
2 |
3 | ```js
4 | import { Select } from 'nautil'
5 |
6 |
7 | ```
8 |
9 | ## props
10 |
11 | - value: Any
12 | - options: List
13 | - text: String
14 | - value: Any
15 | - disabled: ifexist(Boolean)
16 | - placeholder: ifexist(String)
17 | - onChange
18 |
19 | `Select` supports two-way-binding:
20 |
21 | ```js
22 |
23 | ```
--------------------------------------------------------------------------------
/docs/elements/swipe-section.md:
--------------------------------------------------------------------------------
1 | # SwipeSection
2 |
3 | ```js
4 | import { SwipeSection } from 'nautil'
5 |
6 |
7 | ...
8 |
9 | ```
10 |
11 | ## props
12 |
13 | - sensitivity: Number, distance from screen border
14 | - distance: Number, moved distance before onStart
15 | - disabled: Boolean,
16 | - direction: enumerate(['left', 'right', 'both']),
17 | - throttle: Number,
18 | - onStart
19 | - onMove
20 | - onEnd
21 | - onCancel
22 | - onReach
23 | - onUnreach
24 |
--------------------------------------------------------------------------------
/docs/elements/text.md:
--------------------------------------------------------------------------------
1 | # Text
2 |
3 | ```js
4 | import { Text } from 'nautil'
5 |
6 | text
7 | ```
8 |
--------------------------------------------------------------------------------
/docs/elements/textarea.md:
--------------------------------------------------------------------------------
1 | # Textarea
2 |
3 | ```js
4 | import { Textarea } from 'nautil'
5 |
6 |
7 | ```
8 |
9 | ## props
10 |
11 | - value: String
12 | - line: Number, textarea row
13 | - placeholder: ifexist(String)
14 | - onChange
15 | - onFocus
16 | - onBlur
17 | - onSelect
18 |
19 | `Textarea` support two-way-binding:
20 |
21 | ```js
22 |
23 | ```
--------------------------------------------------------------------------------
/docs/elements/video.md:
--------------------------------------------------------------------------------
1 | # Video
2 |
3 | ```js
4 | import { Video } from 'nautil'
5 |
6 |
7 | ```
8 |
9 | ## props
10 |
11 | - source: enumerate([String, dict({ url: String })]),
12 | - width: Unit,
13 | - height: Unit,
14 | - onPlay
15 | - onPause
16 | - onStop
17 | - onDrag
18 | - onResume
19 | - onReload
20 | - onLoad
21 | - onTick
22 | - onVolume
--------------------------------------------------------------------------------
/docs/elements/webview.md:
--------------------------------------------------------------------------------
1 | # Webview
2 |
3 | ```js
4 | import { Webview } from 'nautil'
5 |
6 |
7 | ```
8 |
9 | ## props
10 |
11 | - source: enumerate([String, dict({ url: String })]),
12 | - width: Unit,
13 | - height: Unit,
14 | - onLoad
15 | - onReload
16 | - onResize
17 | - onScroll
18 | - onMessage
--------------------------------------------------------------------------------
/docs/hooks/use-controller.md:
--------------------------------------------------------------------------------
1 | # useController
2 |
3 | ```
4 | const controller = useController(Controller)
5 | ```
6 |
7 | ## applyController
8 |
9 | Create `useController` which can be shared amount components.
10 |
11 | ```
12 | export const { useController } = applyController(Controller)
13 | ```
14 |
--------------------------------------------------------------------------------
/docs/hooks/use-data-source.md:
--------------------------------------------------------------------------------
1 | # useDataSource
2 |
3 | ```js
4 | import { DataService, useDataSource } from 'nautil'
5 |
6 | const BookSource = DataService.source((bookId) => { ... }, {})
7 |
8 | function MyComponent({ bookId }) {
9 | const [book, renewBook] = useDataSource(BookSource, bookId)
10 | ...
11 | }
12 | ```
13 |
--------------------------------------------------------------------------------
/docs/hooks/use-force-update.md:
--------------------------------------------------------------------------------
1 | # useForceUpdate
2 |
3 | ```js
4 | function MyComponent() {
5 | const forceUpdate = useForceUpdate()
6 | return x
7 | }
8 | ```
9 |
--------------------------------------------------------------------------------
/docs/hooks/use-model.md:
--------------------------------------------------------------------------------
1 | # useModel(Model)
2 |
3 | ```
4 | const model = useModel(Model)
5 | ```
6 |
--------------------------------------------------------------------------------
/docs/hooks/use-service.md:
--------------------------------------------------------------------------------
1 | # useService
2 |
3 | ```
4 | const controller = useService(Service)
5 | ```
6 |
--------------------------------------------------------------------------------
/docs/hooks/use-shallow-latest.md:
--------------------------------------------------------------------------------
1 | # useShallowLatest
2 |
3 | ```
4 | const latest = useShallowLatest(obj)
5 | ```
6 |
7 | Get the latest shallow equal object. i.e.
8 |
9 | ```js
10 | const a = { test: 1 }
11 | const latest = useShallowLatest(a) // -> latest === a
12 |
13 | const b = { test: 1 }
14 | const latest2 = useShallowLatest(b) // -> latest2 === a !== b
15 | ```
--------------------------------------------------------------------------------
/docs/hooks/use-two-way-binding.md:
--------------------------------------------------------------------------------
1 | # useTwoWayBinding
2 |
3 | ```typescript
4 | declare function useTwoWayBinding(
5 | // object to convert to be two-way-binding
6 | data: object,
7 | // function to invoke when two-way-binding changed
8 | updator: (value: any, keyPath: string[], data: object) => void,
9 | // whether to generate formalized two-way-binding like [value, update]
10 | formalized?: boolean,
11 | ): Proxy
12 | ```
13 |
14 | ```js
15 | import { useTwoWayBinding, useForceUpdate } from 'nautil'
16 |
17 | function Some() {
18 | const forceUpdate = useForceUpdate()
19 | const $state = useTwoWayBinding({ value: '' }, (state, key, value) => {
20 | state[key] = value
21 | forceUpdate()
22 | })
23 | return
24 | }
25 | ```
26 |
27 | ## useTwoWayBindingAttrs(props)
28 |
29 | ```typescript
30 | declare function useTwoWayBindingAttrs(props: object, formalized: boolean): [object, Proxy]
31 | ```
32 |
33 | ```js
34 | function Some(props) {
35 | const [attrs, $attrs] = useTwoWayBindingAttrs(props)
36 |
37 | return $attrs.show = show]} />
38 | }
39 | ```
40 |
41 | ## useTwoWayBindingState(initState)
42 |
43 | ```typescript
44 | declare function useTwoWayBindingState(initState: object, formalized: boolean): [object, Proxy]
45 | ```
46 |
47 | ```js
48 | function Some(props) {
49 | const [state, $state] = useTwoWayBindingState({ show: false })
50 |
51 | return $state.show = show]} />
52 | }
53 | ```
54 |
--------------------------------------------------------------------------------
/docs/hooks/use-unique-keys.md:
--------------------------------------------------------------------------------
1 | # useUniqueKeys
2 |
3 | ```
4 | const keys: string[] = useUniqueKeys(items: any[], shouldDeepEqual: boolean)
5 | ```
6 |
7 | To get unique keys for all items in a list, so that you do not need to worry about react loop warning.
8 |
9 | ```js
10 | function MyComponent(props) {
11 | const { items } = props
12 |
13 | const keys = useUniqueKeys(items)
14 |
15 | return items.map((item, i) => )
16 | }
17 | ```
18 |
19 | Even though a item is moved (still in the list), the key for it will not change, i.e.
20 |
21 | ```js
22 | const a = { a: 1 }
23 | const b = { b: 1 }
24 | const c = { c: 1 }
25 |
26 | const items = useUniqueKeys([a, b, c])
27 | // => ['axxx', 'bxxx', 'cxxx']
28 |
29 | const items2 = useUniqueKeys([b, c, a])
30 | // => ['bxxx', 'cxxx', 'axxx'] -> a was moved to the end
31 | ```
32 |
33 | When you set `shouldDeepEqual` to be true, it will diff the deep nodes of item objects, not use `===` to compare.
34 |
--------------------------------------------------------------------------------
/docs/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Nautil.js - Powerful Cross Platform Business System Frontend Framework
6 |
7 |
8 |
9 |
15 |
16 |
17 |
18 |
19 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/docs/module/create-bootstrap.md:
--------------------------------------------------------------------------------
1 | # createBootstrap
2 |
3 | ```js
4 | import { createBootstrap } from 'nautil'
5 |
6 | const bootstrap = createBootstrap({
7 | router: {
8 | mode: '/',
9 | },
10 | i18n: {
11 | // default language name
12 | language: 'zh-CN',
13 | },
14 | // shared this context inside all modules
15 | context: {},
16 | })
17 |
18 | function App() {
19 | // ...
20 | }
21 |
22 | export default bootstrap(App) // -> it returns a ReactComponent
23 | ```
24 |
25 | Notice that, you should always use `createBootstrap` to bootstrap your application
26 |
27 | **options**
28 |
29 | - router
30 | - mode
31 | - 'memory' or '': default, use memory, when you refresh the browser, you lose the url state
32 | - 'storage': use Storage, will keep the previous visited url forever when you refresh the browser
33 | - '/': history mode in browser i.e. /app/page1
34 | - '#': hash mode in browser, i.e. /uri#/app/page1
35 | - '?url': search query mode in browser, i.e. /uri?url=/app/page1
36 | - '#?url': hash search query in browser, i.e. /uri#/some/path?url=/app/page1
37 | - i18n
38 | - language: initliaze global language
39 | - context: object, which can be get into module components
40 |
--------------------------------------------------------------------------------
/docs/module/import-module.md:
--------------------------------------------------------------------------------
1 | # importModule
2 |
3 | ```js
4 | import { importModule } from 'nautil'
5 |
6 | const Home = importModule({
7 | source: () => import('./home.jsx'),
8 | pending: (props) => null,
9 | prefetch: (props) => [
10 | `/api/detail/${props.id}`,
11 | ],
12 | })
13 | ```
14 |
15 | - source: async function which returns a Promise that resolve a ReactComponent
16 | - pending: display before source Promise resolved
17 | - prefetch: prefetch data from server side during source Promise pending/loading component file
18 | - navigator: boolean, whether enable navigator inside
19 | - context: object, share context inside
20 | - ready: boolean, whether enable ready action
21 |
22 | A module is a specific file which exports certain APIs.
23 |
24 | ```js
25 | // `export navigator` should be a function which is like hook function to return current module's navigator info
26 | // return data should contain `title` and `path`
27 | // - title: current navigator's title
28 | // - path: optional, current navigator's location path, we can navigate to this path by using router, if not give, we will use current location href as path by using useLocation
29 | // enabled by `navigator`
30 | export function navigator(props) {
31 | const [title, setTitle] = useState('')
32 | useEffect(() => {
33 | fetch('xxx').then(res => res.json()).then((data) => {
34 | setTitle(data.title)
35 | })
36 | }, [])
37 |
38 | return {
39 | title,
40 | }
41 | }
42 |
43 | // will be merged with shared context
44 | export function context(props) {
45 | // should must return an object
46 | // use hooks here
47 | // provide context for useModuleContext
48 | return {}
49 | }
50 |
51 | // run before this component initialize
52 | // enabled by `ready`
53 | export function ready(props) {
54 | // do something when the module ready before the module entry component initialize
55 | ThisModuleDataService.setBaseUrl('/xxx')
56 |
57 | // should must return true or false
58 | // return true to tell the module engine to render
59 | // return false to tell the modole waiting
60 | return true
61 | }
62 |
63 | // `export default` should be a component to be used as view
64 | export default function Home(props) {
65 | const navigator = useModuleNavigator()
66 | const context = useModuleContext()
67 |
68 | return (
69 | ..
70 |
71 | ..
72 | )
73 | }
74 |
75 | /**
76 | * work with useModuleI18n
77 | */
78 | export function i18n(props) {
79 | const i18n = useMemo(() => new I18n({ ... }), [])
80 | return i18n
81 | }
82 | ```
83 |
84 | It will use `Home` as source.
85 |
--------------------------------------------------------------------------------
/docs/renderers/dom.md:
--------------------------------------------------------------------------------
1 | # DOM Render
2 |
3 | ```js
4 | import { mount, update, unmount } from 'nautil/dom'
5 |
6 | mount('#app', App, props)
7 | update('#app', App, props_2)
8 | unmount('#app')
9 | ```
10 |
--------------------------------------------------------------------------------
/docs/renderers/native.md:
--------------------------------------------------------------------------------
1 | # Native Render
2 |
3 | ```js
4 | import { register } from 'nautil/native'
5 |
6 | register('MyApp', App)
7 | ```
8 |
--------------------------------------------------------------------------------
/docs/renderers/web-component.md:
--------------------------------------------------------------------------------
1 | # Web-Component Render
2 |
3 | ```js
4 | import { define } from 'nautil/web-component'
5 |
6 | define('my-app', App, cssText)
7 | ```
8 |
--------------------------------------------------------------------------------
/docs/services/event-service.md:
--------------------------------------------------------------------------------
1 | # EventService
2 |
3 | A global event service to listen and trigger event amoung modules.
4 |
5 | ```js
6 | import { EventService } from 'nautil'
7 |
8 | const eventService = new EventService()
9 |
10 | eventService.on('someEvent', () => ...)
11 |
12 | eventService.emit('someEvent')
13 |
14 | eventService.off('someEvent', fn)
15 |
16 | eventService.once('someEvent', fn)
17 |
18 | if (eventService.hasEvent('someEvent')) {
19 | ...
20 | }
21 | ```
22 |
23 | ```js
24 | import { EventService, Controller } from 'nautil'
25 |
26 | class MyEventService extends EventService {}
27 |
28 | class MyController extends Controller {
29 | static eventService = MyEventService
30 |
31 | init() {
32 | this.eventService.on('xxx', this.handler)
33 | }
34 |
35 | destroy() {
36 | this.eventService.off('xxx', this.handler)
37 | }
38 | }
39 | ```
40 |
--------------------------------------------------------------------------------
/docs/services/service.md:
--------------------------------------------------------------------------------
1 | # Service
2 |
3 | ```js
4 | import { Service } from 'nautil'
5 |
6 | class SomeService extends Service {
7 | static otherService = OtherService
8 |
9 | doSome() {
10 | return this.otherService.request()
11 | }
12 | }
13 |
14 | const service = SomeService.instance()
15 | ```
16 |
17 | Use `instance` static method to get a shared instance of service in your application.
18 |
--------------------------------------------------------------------------------
/docs/store/consumer.md:
--------------------------------------------------------------------------------
1 | # Consumer
2 |
3 | `Consumer` should must be used inside `Provider`.
4 |
5 | ```js
6 | import { Consumer } from 'nautil'
7 |
8 | function Some() {
9 | return (
10 |
11 | {
12 | ...
13 | }}>
14 |
15 | )
16 | }
17 | ```
18 |
19 | ## connect
20 |
21 | `connect` should must be used inside `Provider`.
22 |
23 | ```js
24 | const ConnectedComponent = connect(mapStoreToProps)(MyComponent)
25 |
26 |
29 | ```
30 |
31 | ## useStore
32 |
33 | `useStore` will watch the change of store and trigger rerendering:
34 |
35 | ```js
36 | function MyComponent() {
37 | useStore(store)
38 | ...
39 | }
40 | ```
--------------------------------------------------------------------------------
/docs/store/provider.md:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangshuang/nautil/1cdd52330d481aef417b7560796c9e68d3a42167/docs/store/provider.md
--------------------------------------------------------------------------------
/dom.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 |
3 | import { Component, DOMElement, ReactElement } from "react"
4 |
5 | export declare function mount(el: string | DOMElement, C: Component, props?: any): any
6 |
7 | export declare function unmount(el: string | DOMElement): any
8 |
9 | export declare function update(el: string | DOMElement, C: Component, props?: any): any
10 |
11 | export declare function render(el: string | DOMElement, vdom: ReactElement): any
12 |
--------------------------------------------------------------------------------
/native.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 |
3 | import { Component } from "react"
4 |
5 | export declare function register(name: String, C: Component): void
6 |
7 | export declare function registerConfig(config: any): void
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nautil",
3 | "version": "0.51.19",
4 | "description": "Enterprise Level Business System Frontend Framework",
5 | "main": "index.js",
6 | "miniprogram": "miniprogram_dist",
7 | "scripts": {
8 | "clean": "rimraf dist miniprogram_dist dom lib native ssr web-component wechat index.js",
9 | "build": "npm run clean && npm run build:cjs && npm run build:wechat",
10 | "build:cjs": "cross-env NODE_ENV=production babel src --out-dir . --config-file ./babel.config.js --keep-file-extension --copy-files",
11 | "build:wechat": "copyfiles -f src/wechat/components/dynamic/* miniprogram_dist/wechat/components/dynamic && cross-env NODE_ENV=production webpack --config .build/webpack.wechat.config.js",
12 | "dev": "webpack-dev-server --config .examples/dev/webpack.config.js",
13 | "postinstall": "node .scripts/files-for-wechat.js",
14 | "eslint": "eslint src --ext js,jsx"
15 | },
16 | "repository": {
17 | "type": "git",
18 | "url": "git+https://github.com/tangshuang/nautil.git"
19 | },
20 | "keywords": [
21 | "javascript",
22 | "framework",
23 | "react"
24 | ],
25 | "author": "tangshuang",
26 | "license": "MIT",
27 | "bugs": {
28 | "url": "https://github.com/tangshuang/nautil/issues"
29 | },
30 | "homepage": "https://github.com/tangshuang/nautil#readme",
31 | "dependencies": {
32 | "@babel/runtime": "^7.16.7",
33 | "@types/react": "^17.0.11",
34 | "algeb": "^3.0.3",
35 | "immer": "^9.0.18",
36 | "react": "^17.0.2",
37 | "rxjs": "^7.8.0",
38 | "ts-fns": "^11.1.0",
39 | "tyshemo": "^15.0.0"
40 | },
41 | "optionalDependencies": {
42 | "@react-native-async-storage/async-storage": "^1.15.14",
43 | "@react-navigation/native": "^6.0.10",
44 | "@react-navigation/stack": "^6.2.1",
45 | "react-dom": "^17.0.2",
46 | "react-native": "^0.70.1",
47 | "react-native-gesture-handler": "^1.10.3",
48 | "react-native-safe-area-context": "^3.4.1",
49 | "react-native-screens": "^3.13.1"
50 | },
51 | "devDependencies": {
52 | "@babel/cli": "^7.17.10",
53 | "@babel/core": "^7.14.5",
54 | "@babel/eslint-parser": "^7.17.0",
55 | "@babel/plugin-proposal-class-properties": "^7.14.5",
56 | "@babel/plugin-transform-modules-commonjs": "^7.18.2",
57 | "@babel/plugin-transform-runtime": "^7.12.10",
58 | "@babel/preset-env": "^7.14.5",
59 | "@babel/preset-react": "^7.14.5",
60 | "ansi-html": "^0.0.9",
61 | "ansi-regex": "^6.0.1",
62 | "babel-loader": "^8.1.0",
63 | "copyfiles": "^2.4.1",
64 | "cross-env": "^7.0.3",
65 | "css-loader": "^4.3.0",
66 | "eslint": "^8.15.0",
67 | "eslint-plugin-prettier": "^4.0.0",
68 | "eslint-plugin-react": "^7.29.4",
69 | "glob-parent": "^6.0.2",
70 | "less": "^3.12.2",
71 | "less-loader": "^7.0.1",
72 | "prettier-eslint": "^14.0.2",
73 | "react-reconciler": "^0.26.2",
74 | "rimraf": "^3.0.2",
75 | "scheduler": "^0.20.2",
76 | "style-loader": "^1.2.1",
77 | "webpack": "^5.65.0",
78 | "webpack-cli": "^4.9.1",
79 | "webpack-dev-server": "^4.7.2"
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/dom/elements/audio.jsx:
--------------------------------------------------------------------------------
1 | import { isString } from 'ts-fns'
2 | import { Audio } from '../../lib/elements/audio.jsx'
3 |
4 | Audio.implement(class {
5 | render() {
6 | const { source, width, height, ...rest } = this.attrs
7 | const style = { width, height, ...this.style }
8 | const src = isString(source) ? source : source.uri
9 | return (
10 |
11 | {this.children}
12 |
13 | )
14 | }
15 | })
16 |
17 | export { Audio }
18 | export default Audio
19 |
--------------------------------------------------------------------------------
/src/dom/elements/button.jsx:
--------------------------------------------------------------------------------
1 | import { Button } from '../../lib/elements/button.jsx'
2 |
3 | const isTouchable = (typeof document !== 'undefined' && 'ontouchmove' in document)
4 |
5 | Button.implement(class {
6 | render() {
7 | return this.dispatch('Hit', e)}
11 |
12 | onMouseDown={e => !isTouchable && this.dispatch('HitStart', e)}
13 | onMouseUp={e => !isTouchable && this.dispatch('HitEnd', e)}
14 |
15 | onTouchStart={e => isTouchable && this.dispatch('HitStart', e)}
16 | onTouchEnd={e => isTouchable && this.dispatch('HitEnd', e)}
17 |
18 | className={this.className}
19 | style={this.style}
20 | >{this.children}
21 | }
22 | })
23 |
24 | export { Button }
25 | export default Button
26 |
--------------------------------------------------------------------------------
/src/dom/elements/checkbox.jsx:
--------------------------------------------------------------------------------
1 | import { Checkbox } from '../../lib/elements/checkbox.jsx'
2 |
3 | Checkbox.implement(class {
4 | render() {
5 | const { checked, ...rest } = this.attrs
6 |
7 | const onChange = (e) => {
8 | this.$attrs.checked = !checked
9 |
10 | if (checked) {
11 | this.dispatch('Uncheck', e)
12 | }
13 | else {
14 | this.dispatch('Check', e)
15 | }
16 |
17 | this.dispatch('Change', e)
18 | }
19 |
20 | return
29 | }
30 | })
31 |
32 | export { Checkbox }
33 | export default Checkbox
34 |
--------------------------------------------------------------------------------
/src/dom/elements/form.jsx:
--------------------------------------------------------------------------------
1 | import { Form } from '../../lib/elements/form.jsx'
2 |
3 | Form.implement(class {
4 | render() {
5 | return
15 | }
16 | })
17 |
18 | export { Form }
19 | export default Form
20 |
--------------------------------------------------------------------------------
/src/dom/elements/image.jsx:
--------------------------------------------------------------------------------
1 | import { isString } from 'ts-fns'
2 | import { Image } from '../../lib/elements/image.jsx'
3 |
4 | Image.implement(class {
5 | render() {
6 | const { source, width, height, maxWidth, maxHeight, ...rest } = this.attrs
7 | const style = { width, height, maxWidth, maxHeight, ...this.style }
8 | const className = this.className
9 | const children = this.children
10 | const src = isString(source) ? source : source.uri
11 |
12 | // use image as background
13 | if (children) {
14 | return (
15 | {children}
26 | )
27 | }
28 | else {
29 | return
30 | }
31 | }
32 | })
33 |
34 | export { Image }
35 | export default Image
36 |
--------------------------------------------------------------------------------
/src/dom/elements/input.jsx:
--------------------------------------------------------------------------------
1 | import { Input } from '../../lib/elements/input.jsx'
2 |
3 | Input.implement(class {
4 | render() {
5 | const { type, ...rest } = this.attrs
6 |
7 | const onChange = (e) => {
8 | const value = e.target.value
9 | this.$attrs.value = type === 'number' || type === 'range' ? +value : value
10 | this.dispatch('Change', e)
11 | }
12 |
13 | return this.dispatch('Focus', e)}
20 | onBlur={e => this.dispatch('Blur', e)}
21 | onSelect={e => this.dispatch('Select', e)}
22 |
23 | className={this.className}
24 | style={this.style}
25 | />
26 | }
27 | })
28 |
29 | export { Input }
30 | export default Input
31 |
--------------------------------------------------------------------------------
/src/dom/elements/line.jsx:
--------------------------------------------------------------------------------
1 | import { Line } from '../../lib/elements/line.jsx'
2 |
3 | Line.implement(class {
4 | render() {
5 | const { width, thick, color, ...rest } = this.attrs
6 | const styles = { display: 'block', borderBottom: `${thick}px solid ${color}`, width, height: 0, ...this.style }
7 | return
8 | }
9 | })
10 |
11 | export { Line }
12 | export default Line
13 |
--------------------------------------------------------------------------------
/src/dom/elements/radio.jsx:
--------------------------------------------------------------------------------
1 | import { Radio } from '../../lib/elements/radio.jsx'
2 |
3 | Radio.implement(class {
4 | render() {
5 | const { checked, ...rest } = this.attrs
6 |
7 | const onChange = (e) => {
8 | this.$attrs.checked = !checked
9 |
10 | if (checked) {
11 | this.dispatch('Uncheck', e)
12 | }
13 | else {
14 | this.dispatch('Check', e)
15 | }
16 |
17 | this.dispatch('Change', e)
18 | }
19 |
20 | return
29 | }
30 | })
31 |
32 | export { Radio }
33 | export default Radio
34 |
--------------------------------------------------------------------------------
/src/dom/elements/section.jsx:
--------------------------------------------------------------------------------
1 | import { createRef } from 'react'
2 | import { Section } from '../../lib/elements/section.jsx'
3 |
4 | const isTouchable = (typeof document !== 'undefined' && 'ontouchmove' in document)
5 |
6 | Section.implement(class {
7 | init() {
8 | this._ref = createRef()
9 | this.handleClickOutside = this.handleClickOutside.bind(this)
10 | }
11 | handleClickOutside(event) {
12 | if (!this._ref) {
13 | return
14 | }
15 | if (this._ref.current === event.target) {
16 | return
17 | }
18 | if (this._ref.current && this._ref.current.contains && this._ref.current.contains(event.target)) {
19 | return
20 | }
21 | if (!this._isMounted) {
22 | return
23 | }
24 | this.dispatch('HitOutside', event)
25 | }
26 | onMounted() {
27 | document.addEventListener('click', this.handleClickOutside, true)
28 | }
29 | onUnmount() {
30 | document.removeEventListener('click', this.handleClickOutside)
31 | }
32 | render() {
33 | return this.dispatch('Hit', e)}
37 |
38 | onMouseDown={e => !isTouchable && this.dispatch('HitStart', e)}
39 | onMouseMove={e => !isTouchable && this.dispatch('HitMove', e)}
40 | onMouseUp={e => !isTouchable && this.dispatch('HitEnd', e)}
41 |
42 | onTouchStart={e => isTouchable && this.dispatch('HitStart', e)}
43 | onTouchMove={e => isTouchable && this.dispatch('HitMove', e)}
44 | onTouchEnd={e => isTouchable && this.dispatch('HitEnd', e)}
45 | onTouchCancel={e => isTouchable && this.dispatch('HitCancel', e)}
46 |
47 | className={this.className}
48 | style={this.style}
49 |
50 | ref={this._ref}
51 | >{this.children}
52 | }
53 | })
54 |
55 | export { Section }
56 | export default Section
57 |
--------------------------------------------------------------------------------
/src/dom/elements/select.jsx:
--------------------------------------------------------------------------------
1 | import { Select } from '../../lib/elements/select.jsx'
2 | import { isRef } from '../../lib/utils.js'
3 |
4 | Select.implement(class {
5 | render() {
6 | const { inputRef, options, optionValueKey, optionTextKey, ...attrs } = this.attrs
7 |
8 | const onChange = (e) => {
9 | const value = e.target.value
10 | const item = options.find(item => item[optionValueKey || 'value'] + '' === value)
11 | this.$attrs.value = item[optionValueKey || 'value']
12 | this.dispatch('Change', e)
13 | }
14 |
15 | const { placeholder } = attrs
16 | const hasPlaceholder = typeof placeholder !== 'undefined'
17 | let isPlaceholderSelected = false
18 |
19 | if (hasPlaceholder) {
20 | if ('value' in attrs) {
21 | const { value } = attrs
22 | const selected = options.some(item => item.value === value)
23 | if (!selected) {
24 | attrs.value = ''
25 | isPlaceholderSelected = true
26 | }
27 | }
28 | else if ('defaultValue' in attrs) {
29 | const { defaultValue } = attrs
30 | const selected = options.some(item => item.value === defaultValue)
31 | if (!selected) {
32 | attrs.defaultValue = ''
33 | isPlaceholderSelected = true
34 | }
35 | }
36 | else {
37 | attrs.defaultValue = ''
38 | isPlaceholderSelected = true
39 | }
40 | delete attrs.placeholder
41 | }
42 |
43 | if (isPlaceholderSelected) {
44 | attrs['data-non-selected'] = true
45 | }
46 |
47 | return (
48 | isRef(inputRef) && (inputRef.current = el)}
54 | >
55 | {hasPlaceholder ? {placeholder || ''} : null}
56 | {options ? options.map(option => {optionTextKey ? option[optionTextKey] : option.text} ) : this.children}
57 |
58 | )
59 | }
60 | })
61 |
62 | export { Select }
63 | export default Select
64 |
--------------------------------------------------------------------------------
/src/dom/elements/text.jsx:
--------------------------------------------------------------------------------
1 | import { Text } from '../../lib/elements/text.jsx'
2 |
3 | Text.implement(class {
4 | render() {
5 | return {this.children}
6 | }
7 | })
8 |
9 | export { Text }
10 | export default Text
11 |
--------------------------------------------------------------------------------
/src/dom/elements/textarea.jsx:
--------------------------------------------------------------------------------
1 | import { Textarea } from '../../lib/elements/textarea.jsx'
2 |
3 | Textarea.implement(class {
4 | render() {
5 | const { line, placeholder, value, ...rest } = this.attrs
6 |
7 | const onChange = (e) => {
8 | const value = e.target.value
9 | this.$attrs.value = value
10 | this.dispatch('Change', e)
11 | }
12 |
13 | return
28 | }
29 | })
30 |
31 | export { Textarea }
32 | export default Textarea
33 |
--------------------------------------------------------------------------------
/src/dom/elements/video.jsx:
--------------------------------------------------------------------------------
1 | import { isString } from 'ts-fns'
2 | import { Video } from '../../lib/elements/video.jsx'
3 |
4 | Video.implement(class {
5 | render() {
6 | const { source, width, height, ...rest } = this.attrs
7 | const style = { width, height, ...this.style }
8 | const src = isString(source) ? source : source.uri
9 | return (
10 |
11 | {this.children}
12 |
13 | )
14 | }
15 | })
16 |
17 | export { Video }
18 | export default Video
19 |
--------------------------------------------------------------------------------
/src/dom/elements/webview.jsx:
--------------------------------------------------------------------------------
1 | import { isString } from 'ts-fns'
2 | import { Webview } from '../../lib/elements/webview.jsx'
3 |
4 | Webview.implement(class {
5 | render() {
6 | const { source, width, height, ...rest } = this.attrs
7 | const style = { width, height, ...this.style }
8 | const src = isString(source) ? source : source.uri
9 | return
10 | }
11 | })
12 |
13 | export { Webview }
14 | export default Webview
15 |
--------------------------------------------------------------------------------
/src/dom/i18n/language-detector.js:
--------------------------------------------------------------------------------
1 | import { LanguageDetector } from '../../lib/i18n/language-detector.js'
2 |
3 | LanguageDetector.getLang = () => {
4 | return window.navigator.language
5 | }
6 |
7 | export { LanguageDetector }
8 | export default LanguageDetector
9 |
--------------------------------------------------------------------------------
/src/dom/index.js:
--------------------------------------------------------------------------------
1 | import './style/transform.js'
2 |
3 | export { Section } from './elements/section.jsx'
4 | export { Text } from './elements/text.jsx'
5 | export { Button } from './elements/button.jsx'
6 | export { Line } from './elements/line.jsx'
7 |
8 | export { Form } from './elements/form.jsx'
9 | export { Select } from './elements/select.jsx'
10 | export { Checkbox } from './elements/checkbox.jsx'
11 | export { Input } from './elements/input.jsx'
12 | export { Radio } from './elements/radio.jsx'
13 | export { Textarea } from './elements/textarea.jsx'
14 |
15 | export { ListSection } from './elements/list-section.jsx'
16 | export { ScrollSection } from './elements/scroll-section.jsx'
17 | export { SwipeSection } from './elements/swipe-section.jsx'
18 |
19 | export { Image } from './elements/image.jsx'
20 | export { Audio } from './elements/audio.jsx'
21 | export { Video } from './elements/video.jsx'
22 | export { Webview } from './elements/webview.jsx'
23 |
24 | export { Storage } from './storage/storage.js'
25 | export { Router } from './router/router.jsx'
26 |
27 | export { mount, update, unmount, render } from './render.js'
28 |
--------------------------------------------------------------------------------
/src/dom/render.js:
--------------------------------------------------------------------------------
1 | import { render as reactRender, unmountComponentAtNode } from 'react-dom'
2 | import { createElement } from 'react'
3 |
4 | export function mount(el, Component, props = {}) {
5 | return render(el, createElement(Component, props))
6 | }
7 |
8 | export function unmount(el) {
9 | if (typeof el === 'string') {
10 | el = document.querySelector(el)
11 | }
12 |
13 | return unmountComponentAtNode(el)
14 | }
15 |
16 | export function update(...args) {
17 | return mount(...args)
18 | }
19 |
20 | export function render(el, vdom) {
21 | if (typeof el === 'string') {
22 | el = document.querySelector(el)
23 | }
24 |
25 | return reactRender(vdom, el)
26 | }
27 |
--------------------------------------------------------------------------------
/src/dom/storage/storage.js:
--------------------------------------------------------------------------------
1 | import { mixin } from 'ts-fns'
2 | import { Storage } from '../../lib/storage/storage.js'
3 |
4 | mixin(Storage, class {
5 | async getItem(key) {
6 | const value = localStorage.getItem(key)
7 | if (typeof value === 'string') {
8 | return JSON.parse(value)
9 | }
10 | else {
11 | return value
12 | }
13 | }
14 | async setItem(key, value) {
15 | const data = JSON.stringify(value)
16 | return localStorage.setItem(key, data)
17 | }
18 | async delItem(key) {
19 | return localStorage.delItem(key)
20 | }
21 | async clear() {
22 | return localStorage.clear()
23 | }
24 | })
25 |
26 | export { Storage }
27 | export default Storage
28 |
--------------------------------------------------------------------------------
/src/dom/style/transform.js:
--------------------------------------------------------------------------------
1 | import { isNumber, isArray, each, mixin } from 'ts-fns'
2 | import { Transform } from '../../lib/style/transform.js'
3 |
4 | mixin(Transform, class {
5 | get() {
6 | const rules = this.rules
7 | const convert = v => isNumber(v) ? parseInt(v, 10) + 'px' : v
8 |
9 | let text = ''
10 | each(rules, (value, key) => {
11 | const v = isArray(value) ? value.map(convert).join(', ') : convert(value)
12 | text += `${key}(${v}) `
13 | })
14 |
15 | return text
16 | }
17 | })
18 |
19 | export { Transform }
20 | export default Transform
21 |
--------------------------------------------------------------------------------
/src/lib/animate/easings.js:
--------------------------------------------------------------------------------
1 | // https://gist.github.com/gre/1650294
2 | // https://easings.net/
3 | export const easings = {
4 | // no easing, no acceleration
5 | linear: t => t,
6 | // accelerating from zero velocity
7 | easeInQuad: t => t*t,
8 | // decelerating to zero velocity
9 | easeOutQuad: t => t*(2-t),
10 | // acceleration until halfway, then deceleration
11 | easeInOutQuad: t => t<.5 ? 2*t*t : -1+(4-2*t)*t,
12 | // accelerating from zero velocity
13 | easeInCubic: t => t*t*t,
14 | // decelerating to zero velocity
15 | easeOutCubic: t => (--t)*t*t+1,
16 | // acceleration until halfway, then deceleration
17 | easeInOutCubic: t => t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1,
18 | // accelerating from zero velocity
19 | easeInQuart: t => t*t*t*t,
20 | // decelerating to zero velocity
21 | easeOutQuart: t => 1-(--t)*t*t*t,
22 | // acceleration until halfway, then deceleration
23 | easeInOutQuart: t => t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t,
24 | // accelerating from zero velocity
25 | easeInQuint: t => t*t*t*t*t,
26 | // decelerating to zero velocity
27 | easeOutQuint: t => 1+(--t)*t*t*t*t,
28 | // acceleration until halfway, then deceleration
29 | easeInOutQuint: t => t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t,
30 | // elastic bounce effect at the beginning
31 | easeInElastic: t => (.04 - .04 / t) * Math.sin(25 * t) + 1,
32 | // elastic bounce effect at the end
33 | easeOutElastic: t => .04 * t / (--t) * Math.sin(25 * t),
34 | // elastic bounce effect at the beginning and end
35 | easeInOutElastic: t => (t -= .5) < 0 ? (.02 + .01 / t) * Math.sin(50 * t) : (.02 - .01 / t) * Math.sin(50 * t) + 1,
36 | easeInSin: t => 1 + Math.sin(Math.PI / 2 * t - Math.PI / 2),
37 | easeOutSin: t => Math.sin(Math.PI / 2 * t),
38 | easeInOutSin: t => (1 + Math.sin(Math.PI * t - Math.PI / 2)) / 2,
39 | }
40 | export default easings
41 |
--------------------------------------------------------------------------------
/src/lib/animate/transition.js:
--------------------------------------------------------------------------------
1 | import { tween } from './tween.js'
2 | import easings from './easings.js'
3 | import { noop } from '../utils.js'
4 |
5 | export class Transition {
6 | constructor(options = {}) {
7 | const { ease = 'linear', start = 0, end = 1, duration = 0, loop = false } = options
8 |
9 | this._ease = ease
10 | this._start = start
11 | this._end = end
12 | this._duration = duration
13 | this._loop = loop
14 | this.current = start
15 |
16 | this.status = -1
17 | this._time = 0
18 |
19 | this._listeners = []
20 | }
21 |
22 | on(event, callback) {
23 | this._listeners.push([event, callback])
24 | }
25 |
26 | emit(event, data) {
27 | this._listeners.forEach(([e, fn]) => {
28 | if (event === e) {
29 | fn(data)
30 | }
31 | })
32 | }
33 |
34 | animate() {
35 | if (this.status < 1) {
36 | return
37 | }
38 |
39 | const currentTime = Date.now()
40 | const t = (currentTime - this._time) / this._duration
41 | const tw = t > 1 ? 1 : t < 0 ? 0 : t
42 | const easing = easings[this._ease]
43 | const factor = easing(tw) || 0
44 | const end = this._end
45 | const start = this._start
46 | const value = tween(start, end, factor)
47 |
48 | this.current = value
49 | this.emit('update', value)
50 |
51 | if (tw === 1 && this._loop) {
52 | this._time = currentTime
53 | }
54 | else if (tw === 1) {
55 | this.stop()
56 | return
57 | }
58 |
59 | requestAnimationFrame(() => {
60 | this.animate()
61 | })
62 | }
63 | start() {
64 | // finish the loop immeditately
65 | if (!easings[this._ease] || this._duration <= 0) {
66 | this.status = 1
67 | this.emit('start')
68 |
69 | const value = this._end
70 | this.current = value
71 | this.emit('update', value)
72 |
73 | this.stop()
74 | return
75 | }
76 |
77 | if (this.status > 0) {
78 | return
79 | }
80 | if (this.status < 0) {
81 | this._time = Date.now()
82 | }
83 |
84 | this.status = 1
85 | this.emit('start')
86 | this.animate()
87 | }
88 | pause() {
89 | if (this.status <= 0) {
90 | return
91 | }
92 |
93 | this.status = 0
94 | this.emit('pause')
95 | }
96 | stop() {
97 | if (this.status < 0) {
98 | return
99 | }
100 |
101 | this.status = -1
102 | this.emit('stop')
103 | }
104 | }
105 |
106 | Transition.animate = animate
107 |
108 | function animate({ ease = 'linear', start = 0, end = 1, duration = 0, onStart = noop, onUpdated = noop, onStop = noop }) {
109 | const tx = new Transition({ ease, start, end, duration })
110 | tx.on('start', onStart)
111 | tx.on('update', onUpdated)
112 | tx.on('stop', onStop)
113 | tx.start()
114 | return tx
115 | }
116 |
117 | export default Transition
118 |
--------------------------------------------------------------------------------
/src/lib/animate/tween.js:
--------------------------------------------------------------------------------
1 | import { groupArray } from 'ts-fns'
2 |
3 | export function tween(start, end, factor) {
4 | const value = (end - start) * factor + start
5 | return value
6 | }
7 |
8 | export default tween
9 |
10 | export function tweenColor(start, end, factor) {
11 | const [sr, sg, sb, sa] = start.indexOf('#') === 0 ? parseHex(start) : parseRgba(start)
12 | const [er, eg, eb, ea] = end.indexOf('#') === 0 ? parseHex(end) : parseRgba(end)
13 |
14 | const cr = tween(sr, er, factor)
15 | const cg = tween(sg, eg, factor)
16 | const cb = tween(sb, eb, factor)
17 | const ca = sa === undefined && ea === undefined ? undefined : tween(sa === undefined ? 1 : sa, ea === undefined ? 1 : ea, factor)
18 |
19 | const color = end.indexOf('#') === 0 ? createHex(cr, cg, cb, ca) : createRgba(cr, cg, cb, ca)
20 | return color
21 | }
22 |
23 | function parseRgba(rgba) {
24 | const values = rgba.split('(')[1].split(')')[0].split(',').map((item, i) => {
25 | item = item.trim()
26 | if (item.indexOf('%') > -1 && i < 3) {
27 | const value = item.substr(0, item.length - 1) / 100 * 255
28 | return value
29 | }
30 | else if (item.indexOf('%') > -1 && i === 3) {
31 | const value = item.substr(0, item.length - 1) / 100
32 | return value
33 | }
34 | else {
35 | return +item
36 | }
37 | })
38 | return values
39 | }
40 |
41 | function parseHex(hex) {
42 | const values = hex.substr(1).split('')
43 | const isSingle = hex.length === 4 || hex.length === 5
44 | const hexes = isSingle ? values.map(item => item + item) : groupArray(values, 2).map(item => item.join(''))
45 |
46 | const red = +('0x' + hexes[0])
47 | const green = +('0x' + hexes[1])
48 | const blue = +('0x' + hexes[2])
49 | const alpha = hexes[3] ? +((+('0x' + hexes[3]) / 255).toFixed(4)) : undefined
50 |
51 | const results = alpha ? [red, green, blue, alpha] : [red, green, blue]
52 | return results
53 | }
54 |
55 | function createHex(r, g, b, a) {
56 | const make = (num) => {
57 | const str = num.toString(16).substr(0, 2)
58 | const value = str.length === 1 ? '0' + str : str
59 | return value
60 | }
61 | const red = make(r)
62 | const green = make(g)
63 | const blue = make(b)
64 | const alpha = a !== undefined ? make(Math.round(a * 255)) : undefined
65 |
66 | const color = '#' + red + green + blue + (alpha ? alpha : '')
67 | return color
68 | }
69 |
70 | function createRgba(r, g, b, a) {
71 | const color = (a !== undefined ? 'rgba(' : 'rgb(') + r + ', ' + g + ', ' + b + (a !== undefined ? ', ' + a : '') + ')'
72 | return color
73 | }
74 |
--------------------------------------------------------------------------------
/src/lib/components/async.jsx:
--------------------------------------------------------------------------------
1 | import { ifexist, Any, Enum } from 'tyshemo'
2 | import { isFunction } from 'ts-fns'
3 |
4 | import Component from '../core/component.js'
5 | import { createPlaceholderElement, noop } from '../utils.js'
6 |
7 | export class Async extends Component {
8 | static props = {
9 | await: new Enum([Function, Promise]),
10 | then: ifexist(Function),
11 | catch: Function,
12 | pending: ifexist(Any),
13 | }
14 | static defaultProps = {
15 | catch: noop,
16 | }
17 | state = {
18 | status: 'pending',
19 | data: null,
20 | error: null,
21 | }
22 | onMounted() {
23 | const { await: fn } = this.attrs
24 | const deferer = isFunction(fn) ? fn() : fn
25 | deferer.then((data) => {
26 | if (this._isUnmounted) {
27 | return
28 | }
29 | this.setState({ status: 'resolved', data })
30 | }).catch((error) => {
31 | if (this._isUnmounted) {
32 | return
33 | }
34 | this.setState({ status: 'rejected', error })
35 | })
36 | }
37 | onUnmount() {
38 | this._isUnmounted = true
39 | }
40 | render() {
41 | const { pending, then, catch: catchFn } = this.attrs
42 | const { status, data, error } = this.state
43 | const inside = (data) => isFunction(this.children) ? this.children(data) : this.children
44 |
45 | if (status === 'pending') {
46 | return pending ? createPlaceholderElement(pending) : null
47 | }
48 | else if (status === 'resolved') {
49 | return then ? then(data) : inside(data)
50 | }
51 | else if (status === 'rejected') {
52 | return catchFn ? catchFn(error) : null
53 | }
54 | else {
55 | return null
56 | }
57 | }
58 | }
59 | export default Async
60 |
--------------------------------------------------------------------------------
/src/lib/components/for-each.jsx:
--------------------------------------------------------------------------------
1 | import { enumerate, ifexist } from 'tyshemo'
2 | import { each, isFunction, isArray, decideby } from 'ts-fns'
3 | import { cloneElement, Children, Fragment } from 'react'
4 |
5 | import { Component } from '../core/component.js'
6 | import { useUniqueKeys } from '../hooks/unique-keys.js'
7 |
8 | export class For extends Component {
9 | static props = {
10 | start: Number,
11 | end: Number,
12 | step: Number,
13 | unique: ifexist(enumerate([String, Function])),
14 | map: ifexist(Function),
15 | render: ifexist(Function),
16 | }
17 | static defaultProps = {
18 | step: 1,
19 | }
20 |
21 | render() {
22 | const { start, end, step, map, render, unique } = this.attrs
23 | const children = this.children
24 | const blocks = []
25 |
26 | for (let i = start; i <= end; i += step) {
27 | const data = map ? map(i) : i
28 | const uniqueKey = unique ? (isFunction(unique) ? unique(data, i) : (data && typeof data === 'object' ? data[unique] : i)) : i
29 | const block = decideby(() => {
30 | if (isFunction(render)) {
31 | return render(data, i, uniqueKey)
32 | }
33 | if (isFunction(children)) {
34 | return children(data, i, uniqueKey)
35 | }
36 | return {Children.map(children, (child) => cloneElement(child))}
37 | })
38 | if (!block) {
39 | return
40 | }
41 | if (block.key) {
42 | blocks.push(block)
43 | } else {
44 | blocks.push({block} )
45 | }
46 | }
47 | return blocks
48 | }
49 | }
50 |
51 | export class Each extends Component {
52 | static props = {
53 | of: enumerate([Array, Object]),
54 | unique: ifexist(enumerate([String, Function])),
55 | map: ifexist(Function),
56 | render: ifexist(Function),
57 | }
58 |
59 | Render() {
60 | const obj = this.attrs.of
61 | const children = this.children
62 | const blocks = []
63 | const { map, render, unique } = this.attrs
64 | const data = map ? map(obj) : obj
65 |
66 | const keys = useUniqueKeys(isArray(data) ? data : [])
67 |
68 | each(data, (value, key) => {
69 | const defaultKey = isArray(data) ? keys[key] : key
70 | const uniqueKey = unique
71 | ? (
72 | isFunction(unique) ? unique(value, key)
73 | : (value && typeof value === 'object' ? value[unique] : defaultKey)
74 | )
75 | : defaultKey
76 | const block = decideby(() => {
77 | if (isFunction(render)) {
78 | return render(value, key, uniqueKey)
79 | }
80 | if (isFunction(children)) {
81 | return children(value, key, uniqueKey)
82 | }
83 | return {Children.map(children, (child) => cloneElement(child))}
84 | })
85 | if (!block) {
86 | return
87 | }
88 | if (block.key) {
89 | blocks.push(block)
90 | } else {
91 | blocks.push({block} )
92 | }
93 | })
94 |
95 | return blocks
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/src/lib/components/if-else.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * If, ElseIf, Else
3 | *
4 | *
5 | *
6 | *
7 | *
8 | *
9 | *
10 | *
11 | *
12 | *
13 | *
14 | *
15 | *
16 | */
17 |
18 | import { ifexist } from 'tyshemo'
19 | import { isFunction } from 'ts-fns'
20 | import { Children, createElement, Fragment, Suspense, useRef } from 'react'
21 |
22 | import { Component } from '../core/component.js'
23 |
24 | export class Else extends Component {
25 | static props = {
26 | render: ifexist(Function),
27 | }
28 |
29 | render() {
30 | return null
31 | }
32 | }
33 |
34 | export class ElseIf extends Component {
35 | static props = {
36 | is: Boolean,
37 | render: ifexist(Function),
38 | }
39 |
40 | render() {
41 | return null
42 | }
43 | }
44 |
45 | export class If extends Component {
46 | static props = {
47 | is: Boolean,
48 | render: ifexist(Function),
49 | }
50 |
51 | render() {
52 | const children = this.children
53 | const { is, render } = this.attrs
54 |
55 | if (is && isFunction(render)) {
56 | return render()
57 | }
58 |
59 | if (isFunction(children)) {
60 | return is ? children() : null
61 | }
62 |
63 | let block = {
64 | is,
65 | render,
66 | elements: [],
67 | }
68 |
69 | const create = () => {
70 | if (isFunction(block.render)) {
71 | return createElement(Fragment, {}, ...[].concat(block.render()))
72 | }
73 | else if (block.elements.length) {
74 | return createElement(Fragment, {}, ...block.elements)
75 | }
76 | else {
77 | return null
78 | }
79 | }
80 |
81 | const items = Children.toArray(children)
82 | for (let i = 0, len = items.length; i < len; i ++) {
83 | const item = items[i]
84 | const { type } = item
85 |
86 | if (type === Else || type === ElseIf) {
87 | if (block.is) {
88 | return create()
89 | }
90 |
91 | const { props } = item
92 | const { is, render } = props
93 | block = {
94 | is: type === Else ? true : is,
95 | render,
96 | elements: [],
97 | }
98 | }
99 | else {
100 | block.elements.push(item)
101 | }
102 | }
103 |
104 | if (block.is) {
105 | return (
106 |
107 |
108 |
109 | )
110 | }
111 |
112 | return null
113 | }
114 | }
115 |
116 | function TroubleMaker(props) {
117 | const { is, render } = props
118 | const deferer = useRef()
119 |
120 | if (!is) {
121 | let resolve = null
122 | const promise = new Promise((r) => {
123 | resolve = r
124 | })
125 | deferer.current = resolve
126 | throw promise
127 | }
128 | else if (deferer.current) {
129 | deferer.current()
130 | deferer.current = null
131 | }
132 |
133 | return render()
134 | }
135 |
136 | export default If
137 |
--------------------------------------------------------------------------------
/src/lib/components/observer.jsx:
--------------------------------------------------------------------------------
1 | import { ifexist, Ty } from 'tyshemo'
2 |
3 | import Component from '../core/component.js'
4 | import { noop } from '../utils.js'
5 | import { isFunction } from 'ts-fns'
6 |
7 | export class Observer extends Component {
8 | static props = {
9 | subscribe: Function,
10 | unsubscribe: ifexist(Function),
11 | dispatch: ifexist(Function),
12 | render: ifexist(Function),
13 | }
14 | static defaultProps = {
15 | unsubscribe: noop,
16 | }
17 |
18 | onMounted() {
19 | const { subscribe, dispatch = this.weakUpdate } = this.attrs
20 | this._unsubscribe = subscribe(dispatch)
21 | }
22 |
23 | onUnmount() {
24 | const { unsubscribe = this._unsubscribe, dispatch = this.weakUpdate } = this.attrs
25 | if (process.env.NODE_ENV !== 'production') {
26 | Ty.expect(unsubscribe).to.be(Function)
27 | }
28 | unsubscribe(dispatch)
29 | }
30 |
31 | render() {
32 | const { render } = this.attrs
33 | if (isFunction(render)) {
34 | return render()
35 | }
36 | else if (isFunction(this.children)) {
37 | return this.children()
38 | }
39 | else {
40 | return this.children
41 | }
42 | }
43 | }
44 | export default Observer
45 |
--------------------------------------------------------------------------------
/src/lib/components/prepare.jsx:
--------------------------------------------------------------------------------
1 | import { Any, ifexist } from 'tyshemo'
2 | import { isFunction } from 'ts-fns'
3 |
4 | import Component from '../core/component.js'
5 | import { createPlaceholderElement } from '../utils.js'
6 |
7 | export class Prepare extends Component {
8 | static props = {
9 | ready: Boolean,
10 | pending: ifexist(Any),
11 | render: ifexist(Function),
12 | }
13 | render() {
14 | const { ready, pending, render } = this.attrs
15 | return ready
16 | ? (
17 | isFunction(render) ? render()
18 | : isFunction(this.children) ? this.children()
19 | : this.children
20 | )
21 | : createPlaceholderElement(pending)
22 | }
23 | }
24 | export default Prepare
25 |
--------------------------------------------------------------------------------
/src/lib/components/static.jsx:
--------------------------------------------------------------------------------
1 | import { isFunction } from 'ts-fns'
2 | import { enumerate, ifexist } from 'tyshemo'
3 |
4 | import Component from '../core/component.js'
5 |
6 | export class Static extends Component {
7 | static props = {
8 | shouldUpdate: enumerate([Function, Boolean, Array]),
9 | render: ifexist(Function),
10 | }
11 |
12 | shouldUpdate(nextProps) {
13 | const { shouldUpdate } = nextProps
14 | return isFunction(shouldUpdate) ? shouldUpdate() : shouldUpdate
15 | }
16 |
17 | render() {
18 | const { render } = this.attrs
19 | return isFunction(render) ? render()
20 | : isFunction(this.children) ? this.children()
21 | : this.children
22 | }
23 | }
24 | export default Static
25 |
--------------------------------------------------------------------------------
/src/lib/components/switch-case.jsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Switch, Case
3 | *
4 | *
5 | * 0
6 | * 1
7 | * x
8 | *
9 | */
10 |
11 | import { Any, ifexist } from 'tyshemo'
12 | import { isFunction } from 'ts-fns'
13 | import { Children, isValidElement } from 'react'
14 |
15 | import Component from '../core/component.js'
16 |
17 | export class Case extends Component {
18 | static props = {
19 | is: Any,
20 | default: ifexist(Boolean),
21 | break: ifexist(Boolean),
22 | render: ifexist(Function),
23 | }
24 |
25 | render() {
26 | return null
27 | }
28 | }
29 |
30 | export class Switch extends Component {
31 | static props = {
32 | of: Any,
33 | }
34 |
35 | render() {
36 | const children = this.children
37 | const target = this.attrs.of
38 | const blocks = []
39 |
40 | let isMeet = false
41 |
42 | const items = Children.toArray(children)
43 | for (let i = 0, len = items.length; i < len; i ++) {
44 | const item = items[i]
45 | if (!isValidElement(item)) {
46 | continue
47 | }
48 |
49 | const { type, props } = item
50 | if (type !== Case) {
51 | continue
52 | }
53 |
54 | const { is, default: isDefault, break: isBreak, render, children } = props
55 | const h = () => isFunction(render) ? render() : isFunction(children) ? children() : children
56 | if (is === target) {
57 | const block = h()
58 | blocks.push(block)
59 | isMeet = true
60 | if (isBreak) {
61 | break
62 | }
63 | }
64 | if (isDefault && !isMeet) {
65 | const block = h()
66 | blocks.push(block)
67 | break
68 | }
69 | }
70 |
71 | return blocks
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/lib/core/service.js:
--------------------------------------------------------------------------------
1 | import { each, getConstructorOf, isInheritedOf, isFunction } from 'ts-fns'
2 | import { Stream } from './stream.js'
3 | import { Model } from 'tyshemo'
4 | import { SingleInstance } from '../utils.js'
5 |
6 | export class Service extends SingleInstance {
7 | __init() {
8 | const Constructor = getConstructorOf(this)
9 | const streams = []
10 | each(Constructor, (_, key) => {
11 | const Item = Constructor[key]
12 | if (Item && isInheritedOf(Item, Service)) {
13 | this[key] = Item.instance()
14 | }
15 | else if (Item && isInheritedOf(Item, Model)) {
16 | this[key] = new Item()
17 | }
18 | else if (isFunction(Item) && key[key.length - 1] === '$') {
19 | const stream$ = new Stream()
20 | this[key] = stream$
21 | streams.push([stream$, Item])
22 | }
23 | }, true)
24 | // register all streams at last, so that you can call this.stream$ directly in each function.
25 | streams.forEach(([stream$, fn]) => fn.call(this, stream$))
26 | }
27 | }
28 | export default Service
29 |
--------------------------------------------------------------------------------
/src/lib/core/stream.js:
--------------------------------------------------------------------------------
1 | import { Subject as Stream } from 'rxjs'
2 |
3 | export { Stream }
4 |
5 | export function createStream(fn) {
6 | const stream$ = new Stream()
7 | if (fn) {
8 | fn(stream$)
9 | }
10 | return stream$
11 | }
12 |
--------------------------------------------------------------------------------
/src/lib/decorators/combiners.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Conbime operating, with order
3 | * @param {*} wrappers
4 | */
5 | export function pipe(wrappers) {
6 | const items = [...wrappers]
7 | items.reverse()
8 | return function(C) {
9 | return items.reduce((C, wrap) => wrap(C), C)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/lib/elements/audio.jsx:
--------------------------------------------------------------------------------
1 | import { enumerate } from 'tyshemo'
2 |
3 | import Component from '../core/component.js'
4 | import { Unit } from '../utils.js'
5 |
6 | export class Audio extends Component {
7 | static props = {
8 | source: enumerate([String, Object]),
9 | width: Unit,
10 | height: Unit,
11 | onPlay: false,
12 | onPause: false,
13 | onStop: false,
14 | onDrag: false,
15 | onResume: false,
16 | onReload: false,
17 | onLoad: false,
18 | onTick: false,
19 | onVolume: false,
20 | }
21 | static defaultProps = {
22 | width: '100%',
23 | height: 90,
24 | }
25 | }
26 | export default Audio
27 |
--------------------------------------------------------------------------------
/src/lib/elements/button.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from '../core/component.js'
2 |
3 | export class Button extends Component {
4 | static props = {
5 | onHit: false,
6 | onHitStart: false,
7 | onHitEnd: false,
8 | }
9 | static defaultProps = {
10 | type: 'button',
11 | }
12 | }
13 | export default Button
14 |
--------------------------------------------------------------------------------
/src/lib/elements/checkbox.jsx:
--------------------------------------------------------------------------------
1 | import Component from '../core/component.js'
2 |
3 | export class Checkbox extends Component {
4 | static props = {
5 | checked: Boolean,
6 | onChange: false,
7 | onCheck: false,
8 | onUncheck: false,
9 | }
10 | static defaultProps = {
11 | checked: false,
12 | }
13 | }
14 | export default Checkbox
15 |
--------------------------------------------------------------------------------
/src/lib/elements/form.jsx:
--------------------------------------------------------------------------------
1 | import Component from '../core/component.js'
2 |
3 | export class Form extends Component {
4 | static props = {
5 | onChange: false,
6 | onReset: false,
7 | onSubmit: false,
8 | }
9 | }
10 | export default Form
11 |
--------------------------------------------------------------------------------
/src/lib/elements/image.jsx:
--------------------------------------------------------------------------------
1 | import { enumerate, ifexist, dict } from 'tyshemo'
2 |
3 | import Component from '../core/component.js'
4 | import { Unit } from '../utils.js'
5 |
6 | export class Image extends Component {
7 | static props = {
8 | source: enumerate([String, dict({
9 | uri: String,
10 | })]),
11 | width: Unit,
12 | height: Unit,
13 | maxWidth: ifexist(Unit),
14 | maxHeight: ifexist(Unit),
15 | onLoad: false,
16 | }
17 | static defaultProps = {
18 | width: '100%',
19 | height: 'auto',
20 | }
21 | }
22 | export default Image
23 |
24 | // TODO: use image as background
25 |
--------------------------------------------------------------------------------
/src/lib/elements/input.jsx:
--------------------------------------------------------------------------------
1 | import { enumerate, ifexist } from 'tyshemo'
2 |
3 | import Component from '../core/component.js'
4 |
5 | export class Input extends Component {
6 | static props = {
7 | type: enumerate(['text', 'number', 'email', 'tel', 'url']),
8 | placeholder: ifexist(String),
9 | value: enumerate([String, Number]),
10 | onChange: false,
11 | onFocus: false,
12 | onBlur: false,
13 | onSelect: false,
14 | }
15 | static defaultProps = {
16 | type: 'text',
17 | }
18 | }
19 | export default Input
20 |
--------------------------------------------------------------------------------
/src/lib/elements/line.jsx:
--------------------------------------------------------------------------------
1 | import Component from '../core/component.js'
2 |
3 | export class Line extends Component {
4 | static props = {
5 | width: Number,
6 | thick: Number,
7 | color: String,
8 | }
9 | static defaultProps = {
10 | width: '100%',
11 | thick: 1,
12 | color: '#888888',
13 | }
14 | }
15 | export default Line
16 |
--------------------------------------------------------------------------------
/src/lib/elements/list-section.jsx:
--------------------------------------------------------------------------------
1 | import { list } from 'tyshemo'
2 |
3 | import Component from '../core/component.js'
4 |
5 | export class ListSection extends Component {
6 | static props = {
7 | data: list([Object]),
8 | itemRender: Function,
9 | itemKey: String,
10 | itemStyle: Object,
11 | }
12 | static defaultProps = {
13 | itemStyle: {},
14 | }
15 | }
16 |
17 | export default ListSection
18 |
--------------------------------------------------------------------------------
/src/lib/elements/radio.jsx:
--------------------------------------------------------------------------------
1 | import Component from '../core/component.js'
2 |
3 | export class Radio extends Component {
4 | static props = {
5 | checked: Boolean,
6 | onCheck: false,
7 | onUncheck: false,
8 | onChange: false,
9 | }
10 | static defaultProps = {
11 | checked: false,
12 | }
13 | }
14 | export default Radio
15 |
--------------------------------------------------------------------------------
/src/lib/elements/scroll-section.jsx:
--------------------------------------------------------------------------------
1 | import { range, Any, enumerate } from 'tyshemo'
2 |
3 | import Component from '../core/component.js'
4 |
5 | const DOWN = 'down'
6 | const UP = 'up'
7 | const BOTH = 'both'
8 | const NONE = 'none'
9 | const ACTIVATE = 'activate'
10 | const DEACTIVATE = 'deactivate'
11 | const RELEASE = 'release'
12 | const FINISH = 'finish'
13 |
14 | export class ScrollSection extends Component {
15 | static props = {
16 | direction: enumerate([UP, DOWN, BOTH, NONE]),
17 | distance: Number,
18 | damping: range({ min: 0, max: 1 }),
19 |
20 | topLoading: Boolean,
21 | topIndicator: {
22 | [ACTIVATE]: Any,
23 | [DEACTIVATE]: Any,
24 | [RELEASE]: Any,
25 | [FINISH]: Any,
26 | },
27 | topIndicatorStyle: enumerate([Object, String]),
28 | onTopRelease: false,
29 |
30 | bottomLoading: Boolean,
31 | bottomIndicator: {
32 | [ACTIVATE]: Any,
33 | [DEACTIVATE]: Any,
34 | [RELEASE]: Any,
35 | [FINISH]: Any,
36 | },
37 | bottomIndicatorStyle: enumerate([Object, String]),
38 | onBottomRelease: false,
39 |
40 | onScroll: false,
41 |
42 | containerStyle: enumerate([Object, String]),
43 | contentStyle: enumerate([Object, String]),
44 | }
45 |
46 | static defaultProps = {
47 | direction: NONE,
48 | distance: 40,
49 | damping: 0.4,
50 |
51 | topLoading: false,
52 | topIndicator: {
53 | [ACTIVATE]: 'release',
54 | [DEACTIVATE]: 'pull',
55 | [RELEASE]: 'refreshing',
56 | [FINISH]: 'finish',
57 | },
58 | topIndicatorStyle: {},
59 |
60 | bottomLoading: false,
61 | bottomIndicator: {
62 | [ACTIVATE]: 'release',
63 | [DEACTIVATE]: 'pull',
64 | [RELEASE]: 'loading',
65 | [FINISH]: 'finish',
66 | },
67 | bottomIndicatorStyle: {},
68 |
69 | containerStyle: {},
70 | contentStyle: {},
71 | }
72 | }
73 |
74 | ScrollSection.UP = UP
75 | ScrollSection.DOWN = DOWN
76 | ScrollSection.BOTH = BOTH
77 | ScrollSection.NONE = NONE
78 | ScrollSection.ACTIVATE = ACTIVATE
79 | ScrollSection.DEACTIVATE = DEACTIVATE
80 | ScrollSection.RELEASE = RELEASE
81 | ScrollSection.FINISH = FINISH
82 |
83 | export default ScrollSection
84 |
--------------------------------------------------------------------------------
/src/lib/elements/section.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from '../core/component.js'
2 |
3 | export class Section extends Component {
4 | static props = {
5 | onHit: false,
6 | onHitStart: false,
7 | onHitMove: false,
8 | onHitEnd: false,
9 | onHitCancel: false,
10 | onHitOutside: false,
11 | }
12 | }
13 | export default Section
14 |
--------------------------------------------------------------------------------
/src/lib/elements/select.jsx:
--------------------------------------------------------------------------------
1 | import { Any, list, ifexist } from 'tyshemo'
2 |
3 | import Component from '../core/component.js'
4 |
5 | export class Select extends Component {
6 | static props = {
7 | options: ifexist(list([{
8 | text: String,
9 | value: Any,
10 | disabled: ifexist(Boolean),
11 | }])),
12 | placeholder: ifexist(String),
13 | value: ifexist(Any),
14 | onChange: false,
15 | }
16 | }
17 | export default Select
18 |
--------------------------------------------------------------------------------
/src/lib/elements/swipe-section.jsx:
--------------------------------------------------------------------------------
1 | import { enumerate } from 'tyshemo'
2 |
3 | import Component from '../core/component.js'
4 |
5 | export class SwipeSection extends Component {
6 | static props = {
7 | sensitivity: Number,
8 | distance: Number,
9 | disabled: Boolean,
10 | direction: enumerate(['left', 'right', 'both']),
11 | throttle: Number,
12 |
13 | onStart: false,
14 | onMove: false,
15 | onEnd: false,
16 | onCancel: false,
17 | onReach: false,
18 | onUnreach: false,
19 | }
20 |
21 | static defaultProps = {
22 | sensitivity: 5,
23 | distance: 100,
24 | disabled: false,
25 | direction: 'both',
26 | throttle: 0,
27 | }
28 | }
29 | export default SwipeSection
30 |
--------------------------------------------------------------------------------
/src/lib/elements/text.jsx:
--------------------------------------------------------------------------------
1 | import Component from '../core/component.js'
2 |
3 | export class Text extends Component {}
4 | export default Text
5 |
--------------------------------------------------------------------------------
/src/lib/elements/textarea.jsx:
--------------------------------------------------------------------------------
1 | import { ifexist } from 'tyshemo'
2 |
3 | import Component from '../core/component.js'
4 |
5 | export class Textarea extends Component {
6 | static props = {
7 | value: String,
8 | line: Number,
9 | placeholder: ifexist(String),
10 |
11 | onChange: false,
12 | onFocus: false,
13 | onBlur: false,
14 | onSelect: false,
15 | }
16 | static defaultProps = {
17 | line: 3,
18 | }
19 | }
20 | export default Textarea
21 |
--------------------------------------------------------------------------------
/src/lib/elements/video.jsx:
--------------------------------------------------------------------------------
1 | import { enumerate, dict } from 'tyshemo'
2 |
3 | import Component from '../core/component.js'
4 | import { Unit } from '../utils.js'
5 |
6 | export class Video extends Component {
7 | static props = {
8 | source: enumerate([String, dict({
9 | url: String,
10 | })]),
11 | width: Unit,
12 | height: Unit,
13 |
14 | onPlay: false,
15 | onPause: false,
16 | onStop: false,
17 | onDrag: false,
18 | onResume: false,
19 | onReload: false,
20 | onLoad: false,
21 | onTick: false,
22 | onVolume: false,
23 | }
24 | static defaultProps = {
25 | width: '100%',
26 | height: 90,
27 | }
28 | }
29 | export default Video
30 |
--------------------------------------------------------------------------------
/src/lib/elements/webview.jsx:
--------------------------------------------------------------------------------
1 | import { enumerate, dict } from 'tyshemo'
2 |
3 | import Component from '../core/component.js'
4 | import { Unit } from '../utils.js'
5 |
6 | export class Webview extends Component {
7 | static props = {
8 | source: enumerate([String, dict({
9 | url: String,
10 | })]),
11 | width: Unit,
12 | height: Unit,
13 |
14 | onLoad: false,
15 | onReload: false,
16 | onResize: false,
17 | onScroll: false,
18 | onMessage: false,
19 | }
20 | static defaultProps = {
21 | width: '100%',
22 | height: '100%',
23 | }
24 | }
25 | export default Webview
26 |
--------------------------------------------------------------------------------
/src/lib/hooks/controller.js:
--------------------------------------------------------------------------------
1 | import { useForceUpdate } from './force-update.js'
2 | import { useMemo, useEffect } from 'react'
3 |
4 | export function useController(Controller) {
5 | const forceUpdate = useForceUpdate()
6 | const controller = useMemo(() => {
7 | return new Controller()
8 | }, [Controller])
9 | useEffect(() => {
10 | controller.subscribe(forceUpdate)
11 | return () => {
12 | controller.unsubscribe(forceUpdate)
13 | controller.destructor()
14 | }
15 | }, [controller])
16 | return controller
17 | }
18 |
19 | export function applyController(Controller) {
20 | let controller = null
21 | let count = 0
22 |
23 | const useController = () => {
24 | const forceUpdate = useForceUpdate()
25 | useMemo(() => {
26 | if (!controller) {
27 | controller = new Controller()
28 | }
29 | }, [])
30 | useEffect(() => {
31 | count ++
32 | controller.subscribe(forceUpdate)
33 | return () => {
34 | count --
35 | controller.unsubscribe(forceUpdate)
36 | setTimeout(() => {
37 | if (!count) {
38 | controller.destructor()
39 | controller = null
40 | }
41 | }, 64)
42 | }
43 | }, [])
44 | return controller
45 | }
46 |
47 | return { useController }
48 | }
49 |
--------------------------------------------------------------------------------
/src/lib/hooks/force-update.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | export function useForceUpdate() {
4 | const [, setState] = useState({})
5 | return () => setState({})
6 | }
7 | export default useForceUpdate
8 |
--------------------------------------------------------------------------------
/src/lib/hooks/model.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useMemo } from 'react'
2 | import { useForceUpdate } from './force-update.js'
3 |
4 | export function useModel(Model) {
5 | const forceUpdate = useForceUpdate()
6 | const model = useMemo(() => new Model(), [Model])
7 | useEffect(() => {
8 | model.watch('*', forceUpdate, true)
9 | model.watch('!', forceUpdate)
10 | model.on('recover', forceUpdate)
11 | return () => {
12 | model.unwatch('*', forceUpdate)
13 | model.unwatch('!', forceUpdate)
14 | model.off('recover', forceUpdate)
15 | }
16 | }, [model])
17 | return model
18 | }
19 |
--------------------------------------------------------------------------------
/src/lib/hooks/service.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 | import { isInstanceOf } from 'ts-fns'
3 | import { Service } from '../core/service.js'
4 |
5 | /**
6 | * 使用一个controller
7 | * @example
8 | * const ctrl = useController(() => MyController.instance()) // 全局单例
9 | * const ctrl = useController(() => new MyController()) // 局部实例
10 | */
11 | export function useService(serv) {
12 | const service = serv.instance()
13 |
14 | if (!isInstanceOf(service, Service)) {
15 | throw new Error(`useService 必须返回一个 Service 实例`)
16 | }
17 |
18 | useEffect(
19 | () => () => {
20 | service.destructor()
21 | },
22 | [],
23 | )
24 |
25 | return service
26 | }
27 |
--------------------------------------------------------------------------------
/src/lib/hooks/shallow-latest.js:
--------------------------------------------------------------------------------
1 | import { useRef } from 'react'
2 | import { isShallowEqual } from '../utils.js'
3 | import { isArray, isObject } from 'ts-fns'
4 |
5 | /**
6 | * @param {*} obj
7 | * @returns the latest shallow equal object
8 | */
9 | export function useShallowLatest(obj) {
10 | const used = useRef(false)
11 | const latest = useRef(obj)
12 |
13 | if (used.current && !isShallowEqual(latest.current, obj, isShallowEqual)) {
14 | latest.current = isArray(obj) ? [...obj] : isObject(obj) ? { ...obj } : obj
15 | }
16 |
17 | if (!used.current) {
18 | used.current = true
19 | }
20 |
21 | return latest.current
22 | }
23 |
--------------------------------------------------------------------------------
/src/lib/hooks/unique-keys.js:
--------------------------------------------------------------------------------
1 | import { useMemo, useRef } from 'react'
2 | import { createArray, createRandomString, isEqual } from 'ts-fns'
3 |
4 | export function useUniqueKeys(items, shouldDeepEqual) {
5 | const lastest = useRef()
6 | const keys = useMemo(() => {
7 | // the first call
8 | if (!lastest.current) {
9 | const arr = createArray('', items.length)
10 | const keys = arr.map(() => createRandomString(8))
11 | lastest.current = { items, keys }
12 | return keys
13 | }
14 | // call again
15 | else {
16 | const { items: prevItems, keys: prevKeys } = lastest.current
17 | const nextKeys = []
18 |
19 | items.forEach((item, index) => {
20 | for (let i = 0, len = prevItems.length; i < len; i ++) {
21 | const one = prevItems[i]
22 | const isEqualed = shouldDeepEqual ? isEqual(item, one) : item === one
23 | if (!isEqualed) {
24 | continue
25 | }
26 |
27 | const prevKey = prevKeys[i]
28 | // this is the key line
29 | // there may be two same value in the list, for example: [1, 0, 1, 1] -> [1, 1, 0, 1]
30 | // TODO: O(n^3) -> O(n^2)
31 | if (nextKeys.includes(prevKey)) {
32 | continue
33 | }
34 |
35 | nextKeys[index] = prevKey
36 | break
37 | }
38 |
39 | if (!nextKeys[index]) {
40 | nextKeys[index] = createRandomString(8)
41 | }
42 | })
43 |
44 | lastest.current = { items, keys: nextKeys }
45 | return nextKeys
46 | }
47 | }, [items, shouldDeepEqual, items.length]) // items may keep the same array but invoke push/pop, so we use length to determine
48 |
49 | return keys
50 | }
51 |
--------------------------------------------------------------------------------
/src/lib/i18n/i18n.jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useMemo, useState, useCallback } from 'react'
2 | import { useForceUpdate } from '../hooks/force-update.js'
3 | import { LanguageDetector } from './language-detector.js'
4 | import { noop } from '../utils.js'
5 |
6 | const i18nRootContext = createContext()
7 | export function I18nRootProvider(props) {
8 | const { lanuage, children } = props
9 | const [lng, setLng] = useState(typeof lanuage === 'string' ? lanuage : '')
10 |
11 | const updateLng = useCallback((lanuage) => {
12 | if (lanuage === LanguageDetector) {
13 | const input = lanuage.getLang()
14 | updateLng(input)
15 | }
16 | else if (lanuage && lanuage instanceof Promise) {
17 | lanuage.then(updateLng).catch(noop)
18 | }
19 | else if (typeof lanuage === 'string') {
20 | setLng(lanuage)
21 | }
22 | }, [])
23 |
24 | useMemo(() => {
25 | if (lanuage !== lng) {
26 | updateLng(lanuage)
27 | }
28 | }, [lanuage])
29 |
30 | const ctx = useMemo(() => {
31 | return {
32 | lng,
33 | setLng: updateLng,
34 | }
35 | }, [lng])
36 |
37 | const { Provider } = i18nRootContext
38 | return (
39 |
40 | {children}
41 |
42 | )
43 | }
44 |
45 | export function useLanguage() {
46 | const { lng, setLng } = useContext(i18nRootContext)
47 | return [lng, setLng]
48 | }
49 |
50 | export function useI18n(i18n, lang) {
51 | useMemo(() => {
52 | if (lang) {
53 | i18n.setLng(lang)
54 | }
55 | }, [i18n])
56 | const forceUpdate = useForceUpdate()
57 | useEffect(() => {
58 | i18n.on('changeLanguage', forceUpdate)
59 | i18n.on('changeResources', forceUpdate)
60 | return () => {
61 | i18n.off('changeLanguage', forceUpdate)
62 | i18n.off('changeResources', forceUpdate)
63 | }
64 | }, [i18n])
65 | useEffect(() => {
66 | if (i18n.lng !== lang) {
67 | i18n.setLng(lang)
68 | }
69 | }, [lang])
70 | return i18n
71 | }
72 |
73 | export function useTranslate(i18n, lang) {
74 | const [language] = useLanguage()
75 | const forceUpdate = useForceUpdate()
76 | useEffect(() => {
77 | i18n.on('changeResources', forceUpdate)
78 | return () => {
79 | i18n.off('changeResources', forceUpdate)
80 | }
81 | }, [i18n])
82 | const lng = lang || language
83 | const t = (key, params) => i18n.parse(lng, key, params)
84 | return t
85 | }
86 |
--------------------------------------------------------------------------------
/src/lib/i18n/language-detector.js:
--------------------------------------------------------------------------------
1 | export class LanguageDetector {
2 | static getLang() {}
3 | }
4 | export default LanguageDetector
5 |
--------------------------------------------------------------------------------
/src/lib/services/event-service.js:
--------------------------------------------------------------------------------
1 | import { Service } from '../core/service.js'
2 |
3 | export class EventService extends Service {
4 | constructor() {
5 | super()
6 |
7 | this.events = []
8 | }
9 |
10 | on(event, fn) {
11 | this.events.push([event, fn])
12 | return this
13 | }
14 |
15 | once(event, fn) {
16 | this.events.push([event, fn, true])
17 | return this
18 | }
19 |
20 | off(event, fn) {
21 | this.events = this.events.filter(item => item[0] === event && (!fn || fn === item[1]))
22 | return this
23 | }
24 |
25 | /**
26 | * @param {*} event
27 | * @notice we do not provide broadcast data, because we do not want you to use it as EventBus,
28 | * EventService is a message center, not a data post channel
29 | */
30 | emit(event) {
31 | this.events.forEach((item) => {
32 | if (event !== item[0]) {
33 | return
34 | }
35 |
36 | const [, fn, once] = item
37 | fn()
38 | if (once) {
39 | this.off(event, fn)
40 | }
41 | })
42 | }
43 |
44 | hasEvent(event) {
45 | return this.events.some((item) => item[0] === event)
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/lib/storage/storage.js:
--------------------------------------------------------------------------------
1 | let store = {}
2 | export class Storage {
3 | static async getItem(key) {
4 | return store[key]
5 | }
6 | static async setItem(key, value) {
7 | store[key] = value
8 | }
9 | static async delItem(key) {
10 | delete store[key]
11 | }
12 | static async clear() {
13 | store = {}
14 | }
15 | }
16 | export default Storage
17 |
--------------------------------------------------------------------------------
/src/lib/store/shared.js:
--------------------------------------------------------------------------------
1 | import { Component } from '../core/component.js'
2 | import { useLocalStore, Consumer } from './context.jsx'
3 | import { isInstanceOf } from 'ts-fns'
4 | import { Store } from './store.js'
5 |
6 | export function applyStore(store) {
7 | const useStore = (watch) => useLocalStore(store, watch)
8 | const connect = (mapStoreToProps, watch) => C => {
9 | return class ConnectedComponent extends Component {
10 | render() {
11 | return (
12 | {
13 | const mapped = data && typeof data === 'object' ? (isInstanceOf(data, Store) ? data.getState() : data) : {}
14 | const props = { ...this.props, ...mapped }
15 | return
16 | }} />
17 | )
18 | }
19 | }
20 | }
21 |
22 | return { useStore, connect }
23 | }
24 |
--------------------------------------------------------------------------------
/src/lib/store/store.js:
--------------------------------------------------------------------------------
1 | import produce from 'immer'
2 | import { isObject, assign } from 'ts-fns'
3 | import { createTwoWayBinding } from '../utils.js'
4 |
5 | export class Store {
6 | constructor(initState) {
7 | const origin = initState ? initState : this.initState()
8 |
9 | this.state = origin
10 | this._subscribers = []
11 | this._origin = origin
12 |
13 | if (origin && typeof origin === 'object') {
14 | this.$state = createTwoWayBinding(this.state, (value, keyPath) => {
15 | this.update((state) => {
16 | assign(state, keyPath, value)
17 | })
18 | })
19 | }
20 |
21 | // bind to store, so that we can destruct from store
22 | this.update = this.update.bind(this)
23 | this.setState = this.setState.bind(this)
24 | this.getState = this.getState.bind(this)
25 | }
26 |
27 | subscribe(fn) {
28 | this._subscribers.push(fn)
29 | }
30 |
31 | unsubscribe(fn) {
32 | this._subscribers.forEach((item, i) => {
33 | if (item === fn) {
34 | this._subscribers.splice(i, 1)
35 | }
36 | })
37 | }
38 |
39 | dispatch(...args) {
40 | this._subscribers.forEach((fn) => {
41 | fn(...args)
42 | })
43 | }
44 |
45 | initState() {
46 | return {}
47 | }
48 |
49 | getState() {
50 | return this.state
51 | }
52 |
53 | resetState() {
54 | this.update(this._origin)
55 | }
56 |
57 | setState(state) {
58 | this.update(draft => {
59 | if (!isObject(draft)) {
60 | return state
61 | }
62 | Object.assign(draft, state)
63 | })
64 | }
65 |
66 | update(updator) {
67 | const prev = this.state
68 | const next = typeof updator === 'function' ? produce(prev, updator) : updator
69 | this.state = next
70 |
71 | if (next && typeof next === 'object') {
72 | this.$state = createTwoWayBinding(this.state, (value, keyPath) => {
73 | this.update((state) => {
74 | assign(state, keyPath, value)
75 | })
76 | })
77 | }
78 | else {
79 | delete this.$state
80 | }
81 |
82 | this.dispatch(prev, next)
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/lib/style/classname.js:
--------------------------------------------------------------------------------
1 | import {
2 | each,
3 | isString,
4 | isObject,
5 | isBoolean,
6 | uniqueArray,
7 | } from 'ts-fns'
8 |
9 | export class ClassName {
10 | static make(stylesheet) {
11 | const classNames = []
12 | const patchStylesheetObject = (style = {}) => {
13 | each(style, (value, key) => {
14 | if (isBoolean(value)) {
15 | if (value) {
16 | classNames.push(key)
17 | }
18 | }
19 | })
20 | }
21 |
22 | stylesheet.forEach((item) => {
23 | if (isString(item)) {
24 | classNames.push(item)
25 | }
26 | else if (isObject(item)) {
27 | patchStylesheetObject(item)
28 | }
29 | })
30 |
31 | return classNames
32 | }
33 |
34 | static ensure(classNames) {
35 | const className = uniqueArray(classNames.filter(item => !!item)).join(' ') || undefined
36 | return className
37 | }
38 |
39 | static create(stylesheet) {
40 | const stylequeue = [].concat(stylesheet)
41 | const classNames = ClassName.make(stylequeue)
42 | const className = ClassName.ensure(classNames)
43 | return className
44 | }
45 | }
46 | export default ClassName
47 |
--------------------------------------------------------------------------------
/src/lib/style/style.js:
--------------------------------------------------------------------------------
1 | import Transfrom from './transform.js'
2 | import {
3 | each,
4 | isObject,
5 | isBoolean,
6 | isFunction,
7 | isNumeric,
8 | isNumber,
9 | } from 'ts-fns'
10 |
11 | export class Style {
12 | /**
13 | * @param {array} stylesheet
14 | */
15 | static make(stylesheet) {
16 | const style = {}
17 |
18 | stylesheet.forEach((item) => {
19 | if (isObject(item)) {
20 | each(item, (value, key) => {
21 | if (!isBoolean(value)) {
22 | style[key] = value
23 | }
24 | })
25 | }
26 | })
27 |
28 | return style
29 | }
30 |
31 | /**
32 | * @param {object} style
33 | * @param {function} iterate
34 | */
35 | static ensure(style, iterate) {
36 | // will be override in react-native
37 | const rules = {}
38 | each(style, (value, key) => {
39 | if (!Style.filter(key, value)) {
40 | return
41 | }
42 | if (isFunction(iterate)) {
43 | rules[key] = iterate(value, key)
44 | }
45 | else if (key === 'transform' && !isBoolean(value)) {
46 | const rule = Transfrom.convert(value)
47 | rules[key] = rule
48 | }
49 | else {
50 | rules[key] = Style.convert(value)
51 | }
52 | })
53 | return rules
54 | }
55 |
56 | static filter(_key, _value) {
57 | return true
58 | }
59 |
60 | static convert(value, _key) {
61 | return value
62 | }
63 |
64 | /**
65 | * @param {*} stylesheet
66 | */
67 | static create(stylesheet) {
68 | const stylequeue = [].concat(stylesheet)
69 | const style = Style.make(stylequeue)
70 | const rules = Style.ensure(style)
71 | return rules
72 | }
73 |
74 | static stringify(rules) {
75 | const keys = Object.keys(rules)
76 | let str = ''
77 |
78 | keys.forEach((key) => {
79 | const rule = rules[key]
80 | const name = key.replace(/[A-Z]/, (matched) => {
81 | return '-' + matched.toLocaleLowerCase()
82 | })
83 | const value = isNumber(rule) || isNumeric(rule) ? rule + 'px' : rule
84 | str += `${name}: ${value}`
85 | })
86 |
87 | return str
88 | }
89 | }
90 | export default Style
91 |
--------------------------------------------------------------------------------
/src/lib/style/transform.js:
--------------------------------------------------------------------------------
1 | import { dict, ifexist, enumerate, tuple } from 'tyshemo'
2 | import { each, isString, isArray, isObject } from 'ts-fns'
3 |
4 | const TranslateType = enumerate([String, Number])
5 | const ParamsType = dict({
6 | rotate: ifexist(String),
7 | rotateX: ifexist(String),
8 | rotateY: ifexist(String),
9 | rotateZ: ifexist(String),
10 | scale: ifexist(String),
11 | scaleX: ifexist(String),
12 | scaleY: ifexist(String),
13 | translate: ifexist(tuple([TranslateType, TranslateType])),
14 | translateX: ifexist(TranslateType),
15 | translateY: ifexist(TranslateType),
16 | skew: ifexist(tuple([String, String])),
17 | skewX: ifexist(String),
18 | skewY: ifexist(String),
19 | })
20 |
21 | export class Transform {
22 | constructor(rules = {}) {
23 | this.rules = { ...rules }
24 | }
25 |
26 | set(rules) {
27 | if (process.env.NODE_ENV !== 'production') {
28 | ParamsType.assert(rules)
29 | }
30 | Object.assign(this.rules, rules)
31 | return this
32 | }
33 | del(rules) {
34 | each(rules, (value, key) => {
35 | if (!value) {
36 | return
37 | }
38 | delete this.rules[key]
39 | })
40 | return this
41 | }
42 | get() {
43 | throw new Error(`Transform.prototype.get should be overrided.`)
44 | }
45 |
46 | static parse(value) {
47 | const rules = {}
48 | // array in native
49 | if (isArray(value)) {
50 | value.forEach((item) => {
51 | Object.assign(rules, item)
52 | })
53 | return rules
54 | }
55 | // is a object
56 | else if (isObject(value)) {
57 | return value
58 | }
59 | // string in web
60 | else if (isString(value)) {
61 | const blocks = value.split(' ').filter(item => !!item)
62 | blocks.forEach((item) => {
63 | const [name, x, y] = item.split(/[(,)]/).map(item => item.trim())
64 | rules[name] = y ? [x, y] : x
65 | })
66 | }
67 | return rules
68 | }
69 |
70 | static generate(rules) {
71 | const trans = new Transform(rules)
72 | const res = trans.get()
73 | return res
74 | }
75 |
76 | static convert(value) {
77 | const obj = Transform.parse(value)
78 | const rule = Transform.generate(obj)
79 | return rule
80 | }
81 | }
82 | export default Transform
83 |
--------------------------------------------------------------------------------
/src/native/elements/audio.jsx:
--------------------------------------------------------------------------------
1 | import { Audio } from '../../lib/elements/audio.jsx'
2 |
3 | Audio.implement(class {
4 | render() {
5 | // TODO
6 | }
7 | })
8 |
9 | export { Audio }
10 | export default Audio
11 |
--------------------------------------------------------------------------------
/src/native/elements/button.jsx:
--------------------------------------------------------------------------------
1 | import { Children } from 'react'
2 | import { TouchableOpacity } from 'react-native'
3 | import { Button } from '../../lib/elements/button.jsx'
4 | import { Text } from '../../lib/elements/text.jsx'
5 |
6 | Button.implement(class {
7 | render() {
8 | const children = this.children
9 | const isPuerText = !Children.toArray(children).some(node => node.type)
10 | const content = isPuerText ? {children} : children
11 |
12 | return (
13 | this.dispatch('Hit', e)}
15 | onPressIn={e => this.dispatch('HitStart', e)}
16 | onPressOut={e => this.dispatch('HitEnd', e)}
17 | style={this.style}
18 | {...this.attrs}
19 | >{content}
20 | )
21 | }
22 | })
23 |
24 | export { Button }
25 | export default Button
26 |
--------------------------------------------------------------------------------
/src/native/elements/checkbox.jsx:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native'
2 | import { Checkbox } from '../../lib/elements/checkbox.jsx'
3 |
4 | Checkbox.implement(class {
5 | render() {
6 | const { checked, ...rest } = this.attrs
7 | const { color = '#888888' } = this.style
8 |
9 | const onChange = (e) => {
10 | this.$attrs.checked = !checked
11 |
12 | if (checked) {
13 | this.dispatch('Uncheck', e)
14 | }
15 | else {
16 | this.dispatch('Check', e)
17 | }
18 | }
19 |
20 | return (
21 |
34 | {
35 | checked ? : null
40 | }
41 |
42 | )
43 | }
44 | })
45 |
46 | export { Checkbox }
47 | export default Checkbox
48 |
--------------------------------------------------------------------------------
/src/native/elements/form.jsx:
--------------------------------------------------------------------------------
1 | import { Form } from '../../lib/elements/form.jsx'
2 |
3 | Form.implement(class {
4 | render() {
5 | // TODO
6 | }
7 | })
8 |
9 | export { Form }
10 | export default Form
11 |
--------------------------------------------------------------------------------
/src/native/elements/image.jsx:
--------------------------------------------------------------------------------
1 | import { Children } from 'react'
2 | import { isString } from 'ts-fns'
3 | import { Image as NativeImage, ImageBackground } from 'react-native'
4 | import { Image } from '../../lib/elements/image.jsx'
5 |
6 | Image.implement(class {
7 | render() {
8 | const { source, width, height, maxWidth, maxHeight, ...rest } = this.attrs
9 | const styles = { ...this.style, width, height, maxWidth, maxHeight }
10 | const children = this.children
11 | const src = isString(source) ? { uri: source } : source
12 |
13 | if (Children.count(children)) {
14 | return {children}
15 | }
16 | else {
17 | return
18 | }
19 | }
20 | })
21 |
22 | export { Image }
23 | export default Image
24 |
--------------------------------------------------------------------------------
/src/native/elements/input.jsx:
--------------------------------------------------------------------------------
1 | import { TextInput } from 'react-native'
2 | import Input from '../../lib/elements/input.jsx'
3 |
4 | Input.implement(class {
5 | render() {
6 | const { type, placeholder, value, readOnly, disabled, ...rest } = this.attrs
7 | const editable = !readOnly && !disabled
8 |
9 | const onChange = (e) => {
10 | const value = e.target.value
11 | this.$attrs.value = value
12 | this.dispatch('Change', e)
13 | }
14 |
15 | const contentType = type === 'password' ? 'password' : 'none'
16 | const keyboardType = type === 'number' ? 'decimal-pad' : type === 'email' ? 'email-address' : type === 'tel' ? 'phone-pad' : 'default'
17 |
18 | return (
19 | this.onFocus$.next(e)}
31 | onBlur={e => this.onBlur$.next(e)}
32 | onSelectionChange={e => this.onSelect$.next(e)}
33 |
34 | className={this.className}
35 | style={this.style}
36 | >
37 | )
38 | }
39 | })
40 |
41 | export { Input }
42 | export default Input
43 |
--------------------------------------------------------------------------------
/src/native/elements/line.jsx:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native'
2 | import Line from '../../lib/elements/line.jsx'
3 |
4 | Line.implement(class {
5 | render() {
6 | const { width, thick, color = '#888888', ...rest } = this.attrs
7 | const styles = { width, height: 0, borderBottomColor: color, borderBottomWidth: thick, ...this.style }
8 | return
9 | }
10 | })
11 |
12 | export { Line }
13 | export default Line
14 |
--------------------------------------------------------------------------------
/src/native/elements/list-section.jsx:
--------------------------------------------------------------------------------
1 | import { ListSection } from '../../lib/elements/list-section.jsx'
2 | import { Section } from '../../lib/elements/section.jsx'
3 | import { FlatList } from 'react-native'
4 |
5 | ListSection.implement(class {
6 | render() {
7 | const { itemRender, data, itemKey, itemStyle = {} } = this.attrs
8 | return (
9 |
10 | {itemRender(item, index)} }
13 | keyExtractor={item => item[itemKey]}
14 | />
15 |
16 | )
17 | }
18 | })
19 |
20 | export { ListSection }
21 | export default ListSection
22 |
--------------------------------------------------------------------------------
/src/native/elements/radio.jsx:
--------------------------------------------------------------------------------
1 | import { Radio } from '../../lib/elements/radio.jsx'
2 | import { View } from 'react-native'
3 |
4 | Radio.implement(class {
5 | render() {
6 | const { checked, color, ...rest } = this.attrs
7 |
8 | const onChange = (e) => {
9 | this.$attrs.checked = !checked
10 |
11 | if (checked) {
12 | this.dispatch('Uncheck', e)
13 | }
14 | else {
15 | this.dispatch('Check', e)
16 | }
17 | }
18 |
19 | return (
20 |
34 | {
35 | checked ? : null
41 | }
42 |
43 | )
44 | }
45 | })
46 |
47 | export { Radio }
48 | export default Radio
49 |
--------------------------------------------------------------------------------
/src/native/elements/scroll-section.jsx:
--------------------------------------------------------------------------------
1 | import { SectionList, Dimensions } from 'react-native'
2 |
3 | import { ScrollSection } from '../../lib/elements/scroll-section.jsx'
4 |
5 | const {
6 | DOWN,
7 | UP,
8 | BOTH,
9 | NONE,
10 | ACTIVATE,
11 | DEACTIVATE,
12 | RELEASE,
13 | FINISH,
14 | } = ScrollSection
15 |
16 | ScrollSection.implement(class {
17 | init() {
18 | this.state = {
19 | status: DEACTIVATE,
20 | }
21 |
22 | this.reset()
23 | }
24 |
25 | reset() {
26 | this._startY = 0
27 | this._latestY = 0
28 | }
29 |
30 | onUpdated(prevProps) {
31 | const { refreshing, loading } = this.attrs
32 | if (prevProps.refreshing && !refreshing) {
33 | this.setState({ status: FINISH })
34 | this.reset()
35 | }
36 | if (prevProps.loading && !loading) {
37 | this.setState({ status: FINISH })
38 | this.reset()
39 | }
40 | }
41 |
42 | render() {
43 | const { refreshing, loading, distance, direction, refreshIndicator, loadMoreIndicator } = this.attrs
44 | const { status } = this.state
45 | const { height } = Dimensions.get('window')
46 |
47 | const { _startY, _latestY } = this
48 | const directTo = _startY < _latestY ? DOWN : _startY > _latestY ? UP : NONE
49 | const threshold = direction === NONE ? 0 : distance/height
50 | const doing = directTo === DOWN ? refreshing : directTo === UP ? loading : false
51 |
52 | return (
53 | children}
55 | sections={[this.children]}
56 | onScroll={(e) => {
57 | if (direction === NONE) {
58 | return
59 | }
60 | const { nativeEvent } = e
61 | const { contentOffset } = nativeEvent
62 | const { y } = contentOffset
63 | this._startY = this._startY || y
64 | this._latestY = y
65 | }}
66 | onScrollBeginDrag={() => this.setState({ status: DEACTIVATE })}
67 | onScrollEndDrag={() => this.setState({ status: DEACTIVATE })}
68 | onEndReachedThreshold={threshold}
69 | onEndReached={() => {
70 | if (direction === NONE) {
71 | return
72 | }
73 | this.setState({ status: ACTIVATE })
74 | }}
75 | onRefresh={() => {
76 | if (direction === NONE) {
77 | return
78 | }
79 | this.setState({ status: RELEASE })
80 | if ([DOWN, BOTH].includes(direction) && directTo === DOWN) {
81 | this.dispatch('Refresh')
82 | }
83 | else if ([UP, BOTH].includes(direction) && directTo === UP) {
84 | this.dispatch('LoadMore')
85 | }
86 | }}
87 | refreshing={doing}
88 | ListFooterComponent={loadMoreIndicator[status]}
89 | ListHeaderComponent={refreshIndicator[status]}
90 | />
91 | )
92 | }
93 | })
94 |
95 | export { ScrollSection }
96 | export default ScrollSection
97 |
--------------------------------------------------------------------------------
/src/native/elements/section.jsx:
--------------------------------------------------------------------------------
1 | import { Children } from 'react'
2 | import { View } from 'react-native'
3 | import { Section } from '../../lib/elements/section.jsx'
4 | import { Text } from '../../lib/elements/text.jsx'
5 |
6 | let activePath = []
7 | let activeNode = null
8 | let capturePath = []
9 |
10 | Section.implement(class {
11 | onMouted() {
12 | this.__mounted = true
13 | }
14 | onUnmount() {
15 | this.__mounted = false
16 | }
17 | isPathOutsidePath(capturePath, activePath) {
18 | for (let i = 0, len = activePath.length; i < len; i ++) {
19 | const active = activePath[i]
20 | const capture = capturePath[i]
21 | if (active !== capture) {
22 | return false
23 | }
24 | }
25 | return true
26 | }
27 | render() {
28 | const { pointerEvents } = this.style
29 |
30 | const children = this.children
31 | const isPuerText = !Children.toArray(children).some(node => node.type)
32 | const content = isPuerText ? {children} : children
33 |
34 | /**
35 | * hitOutside https://www.jianshu.com/p/98e0b21473be
36 | */
37 |
38 | return (
39 | {
43 | const nodeId = e.target
44 | capturePath.push(nodeId)
45 | return false
46 | }}
47 | onStartShouldSetResponder={(e) => {
48 | if (this.isPathOutsidePath(capturePath, activePath) && activeNode.__mounted) {
49 | activeNode.dispatch('HitOutside', e)
50 | }
51 |
52 | activeNode = this
53 | activePath = capturePath
54 | capturePath = []
55 | return true
56 | }}
57 |
58 | onResponderStart={e => this.dispatch('HitStart', e)}
59 | onResponderMove={e => this.dispatch('HitMove', e)}
60 | onResponderRelease={e => this.dispatch('HitEnd', e)}
61 | onResponderEnd={e => this.dispatch('Hit', e)}
62 | onResponderTerminate={e => this.dispatch('HitCancel', e)}
63 |
64 | style={this.style}
65 | pointerEvents={pointerEvents}
66 | >{content}
67 | )
68 | }
69 | })
70 |
71 | export { Section }
72 | export default Section
73 |
--------------------------------------------------------------------------------
/src/native/elements/select.jsx:
--------------------------------------------------------------------------------
1 | import { Picker } from 'react-native'
2 | import { Select } from '../../lib/elements/select.jsx'
3 |
4 | Select.implement(class {
5 | render() {
6 | const { value, placeholder, options, readOnly, disabled, ...rest } = this.attrs
7 | const enabled = !readOnly && !disabled
8 |
9 | const onChange = (e) => {
10 | const value = e.target.value
11 | this.$attrs.value = value
12 | this.dispatch('Change', e)
13 | }
14 |
15 | return (
16 |
27 | {options ? options.map(item => item.disabled ? null : ) : null}
28 |
29 | )
30 | }
31 | })
32 |
33 | export { Select }
34 | export default Select
35 |
--------------------------------------------------------------------------------
/src/native/elements/text.jsx:
--------------------------------------------------------------------------------
1 | import { Text as NativeText } from 'react-native'
2 | import { Text } from '../../lib/elements/text.jsx'
3 |
4 | Text.implement(class {
5 | render() {
6 | return (
7 | {this.children}
12 | )
13 | }
14 | })
15 |
16 | export { Text }
17 | export default Text
18 |
--------------------------------------------------------------------------------
/src/native/elements/textarea.jsx:
--------------------------------------------------------------------------------
1 | import { TextInput } from 'react-native'
2 | import { Textarea } from '../../lib/elements/textarea.jsx'
3 |
4 | Textarea.implement(class {
5 | render() {
6 | const { line, placeholder, value, readOnly, disabled, ...rest } = this.attrs
7 | const editable = !readOnly && !disabled
8 |
9 | const onChange = (e) => {
10 | const value = e.target.value
11 | this.$attrs.value = value
12 | this.dispatch('Change', e)
13 | }
14 |
15 | return (
16 | this.dispatch('Focus', e)}
28 | onBlur={e => this.dispatch('Blur', e)}
29 | onSelectionChange={e => this.dispatch('Select', e)}
30 |
31 | className={this.className}
32 | style={this.style}
33 | >
34 | )
35 | }
36 | })
37 |
38 | export { Textarea }
39 | export default Textarea
40 |
--------------------------------------------------------------------------------
/src/native/elements/video.jsx:
--------------------------------------------------------------------------------
1 | import { Video } from '../../lib/elements/video.jsx'
2 |
3 | Video.implement(class {
4 | render() {
5 | // TODO
6 | }
7 | })
8 |
9 | export { Video }
10 | export default Video
11 |
--------------------------------------------------------------------------------
/src/native/elements/webview.jsx:
--------------------------------------------------------------------------------
1 | import { WebView as NativeWebview } from 'react-native'
2 | import Webview from '../../lib/elements/webview.jsx'
3 |
4 | Webview.implement(class {
5 | render() {
6 | const { source, width, height, ...rest } = this.attrs
7 | const style = { ...this.style, width, height }
8 | return {this.children}
9 | }
10 | })
11 |
12 | export { Webview }
13 | export default Webview
14 |
--------------------------------------------------------------------------------
/src/native/i18n/language-detector.js:
--------------------------------------------------------------------------------
1 | import { LanguageDetector } from '../../lib/i18n/language-detector.js'
2 | import { NativeModules, Platform } from 'react-native'
3 |
4 | LanguageDetector.getLang = () => {
5 | const deviceLanguage =
6 | Platform.OS === 'ios'
7 | ? NativeModules.SettingsManager.settings.AppleLocale ||
8 | NativeModules.SettingsManager.settings.AppleLanguages[0] // iOS 13
9 | : NativeModules.I18nManager.localeIdentifier
10 | return deviceLanguage
11 | }
12 |
13 | export { LanguageDetector }
14 | export default LanguageDetector
15 |
--------------------------------------------------------------------------------
/src/native/index.js:
--------------------------------------------------------------------------------
1 | import './style/transform.js'
2 | import './style/style.js'
3 |
4 | export { Section } from './elements/section.jsx'
5 | export { Text } from './elements/text.jsx'
6 | export { Button } from './elements/button.jsx'
7 | export { Line } from './elements/line.jsx'
8 |
9 | export { Form } from './elements/form.jsx'
10 | export { Select } from './elements/select.jsx'
11 | export { Checkbox } from './elements/checkbox.jsx'
12 | export { Input } from './elements/input.jsx'
13 | export { Radio } from './elements/radio.jsx'
14 | export { Textarea } from './elements/textarea.jsx'
15 |
16 | export { ListSection } from './elements/list-section.jsx'
17 | export { ScrollSection } from './elements/scroll-section.jsx'
18 | export { SwipeSection } from './elements/swipe-section.jsx'
19 |
20 | export { Image } from './elements/image.jsx'
21 | export { Audio } from './elements/audio.jsx'
22 | export { Video } from './elements/video.jsx'
23 | export { Webview } from './elements/webview.jsx'
24 |
25 | export { Storage } from './storage/storage.js'
26 | export { Router } from './router/router.jsx'
27 |
28 | export { register, registerConfig } from './register.js'
29 |
--------------------------------------------------------------------------------
/src/native/register.js:
--------------------------------------------------------------------------------
1 | import { AppRegistry, LogBox } from 'react-native'
2 |
3 | // remove no use warning
4 | LogBox.ignoreLogs([
5 | 'Warning: isMounted(...) is deprecated',
6 | 'Module RCTImageLoader',
7 | ])
8 |
9 | export function registerConfig(config) {
10 | AppRegistry.registerConfig(config)
11 | }
12 |
13 | export function register(name, Component) {
14 | AppRegistry.registerComponent(name, () => Component)
15 | }
16 |
--------------------------------------------------------------------------------
/src/native/storage/storage.js:
--------------------------------------------------------------------------------
1 | import { mixin } from 'ts-fns'
2 | import AsyncStorage from '@react-native-async-storage/async-storage'
3 | import { Storage } from '../../lib/storage/storage.js'
4 |
5 | mixin(Storage, class {
6 | static async getItem(key) {
7 | return await AsyncStorage.getItem(key)
8 | }
9 | static async setItem(key, value) {
10 | await AsyncStorage.setItem(key, value)
11 | }
12 | static async delItem(key) {
13 | await AsyncStorage.delItem(key)
14 | }
15 | static async clear() {
16 | await AsyncStorage.clear()
17 | }
18 | })
19 |
20 | export { Storage }
21 | export default Storage
22 |
--------------------------------------------------------------------------------
/src/native/style/style.js:
--------------------------------------------------------------------------------
1 | import { isString, mixin, isNumeric } from 'ts-fns'
2 | import { Style } from '../../lib/style/style.js'
3 | import { StyleSheet, PixelRatio, Dimensions } from 'react-native'
4 |
5 | const { create } = Style
6 |
7 | mixin(Style, class {
8 | static filter(key) {
9 | if (['transition', 'animation'].includes(key)) {
10 | return false
11 | }
12 | return true
13 | }
14 | static convert(value) {
15 | if (isString(value) && value.indexOf('rem') > 0) {
16 | const size = parseInt(value, 10)
17 | return PixelRatio.get() <= 2 ? 1.4 * size : 1.8 * size
18 | }
19 | if (isString(value) && value.indexOf('em') > 0) {
20 | const size = parseInt(value, 10)
21 | return PixelRatio.get() <= 2 ? 14 * size : 18 * size
22 | }
23 | if (isString(value) && value.indexOf('px') > 0) {
24 | return parseInt(value, 10)
25 | }
26 | if (isString(value) && value.indexOf('vw') > 0) {
27 | return vw(parseInt(value, 10))
28 | }
29 | if (isString(value) && value.indexOf('vh') > 0) {
30 | return vh(parseInt(value, 10))
31 | }
32 | if (isNumeric(value)) {
33 | return +value
34 | }
35 | return value
36 | }
37 | static create(stylesheet) {
38 | const rules = create(stylesheet)
39 | const styles = StyleSheet.create({ rules })
40 | return styles.rules
41 | }
42 | })
43 |
44 | // fork https://github.com/graftonstudio/react-native-css-vh-vw/blob/master/src/index.js
45 | function vh(percentage) {
46 | const viewportHeight = Dimensions.get('window').height
47 | const decimal = percentage * .01
48 | percentage = parseInt(percentage, 10)
49 |
50 | // Hard limits
51 | if (percentage < 0) {
52 | percentage = 100
53 | }
54 | if (percentage > 1000) {
55 | percentage = 1000
56 | }
57 |
58 | return Math.round(viewportHeight * decimal)
59 | }
60 | function vw(percentage) {
61 | const viewportWidth = Dimensions.get('window').width
62 | const decimal = percentage * .01
63 | percentage = parseInt(percentage, 10)
64 |
65 | // Hard limits
66 | if (percentage < 0) {
67 | percentage = 100
68 | }
69 | if (percentage > 1000) {
70 | percentage = 1000
71 | }
72 |
73 | return Math.round(viewportWidth * decimal)
74 | }
75 |
76 | export { Style }
77 | export default Style
78 |
--------------------------------------------------------------------------------
/src/native/style/transform.js:
--------------------------------------------------------------------------------
1 | import { each, mixin } from 'ts-fns'
2 | import { Transform } from '../../lib/style/transform.js'
3 |
4 | mixin(Transform, class {
5 | get() {
6 | const rules = this.rules
7 | let arr = []
8 | each(rules, (value, key) => {
9 | if (key === 'translate' || key === 'skew') {
10 | const [x, y] = value
11 | const xitem = { [key + 'X']: x }
12 | const yitem = { [key + 'Y']: y }
13 | arr.push(xitem, yitem)
14 | }
15 | else {
16 | const item = { [key]: value }
17 | arr.push(item)
18 | }
19 | })
20 | return arr
21 | }
22 | })
23 |
24 | export { Transform }
25 | export default Transform
26 |
--------------------------------------------------------------------------------
/src/ssr/client/render.js:
--------------------------------------------------------------------------------
1 | import ReactDOM from 'react-dom'
2 | import { isFunction } from 'ts-fns'
3 |
4 | export { unmount, update, mount } from '../dom/index.js'
5 |
6 | export async function hydrate(el, Component, props = {}, options = {}) {
7 | const {
8 | navigations = [],
9 | i18ns = [],
10 | onHydrate,
11 | } = options
12 |
13 | // set url into navigation
14 | const url = window.__hydrate_data.url
15 | navigations.forEach((navigation) => {
16 | if (navigation.options.mode === 'history') {
17 | navigation.setUrl(url)
18 | }
19 | })
20 |
21 | // set language
22 | const lang = window.__hydrate_data.language
23 | if (lang) {
24 | i18ns.forEach((i18n) => {
25 | i18n.setLang(lang)
26 | i18n.on('languageChanged', (lng) => window.fetch(url + (url.indexOf('?') > 0 ? '&' : '?') + 'lng=' + lng))
27 | })
28 | }
29 |
30 | // call before render
31 | if (isFunction(onHydrate)) {
32 | await onHydrate.call(window.__hydrate_data)
33 | }
34 |
35 | // query selector
36 | if (typeof el === 'string') {
37 | el = document.querySelector(el)
38 | }
39 |
40 | // use hydrate
41 | return ReactDOM.hydrate( , el)
42 | }
43 |
--------------------------------------------------------------------------------
/src/ssr/navigation/navigation.js:
--------------------------------------------------------------------------------
1 | import { mixin } from 'ts-fns'
2 | import Navigation from '../../lib/navigation/navigation.js'
3 |
4 | mixin(Navigation, class {
5 | init() {}
6 | setUrl(url) {
7 | const { base = '/' } = this.options
8 | if (base !== '/') {
9 | url = url.replace(base, '')
10 | }
11 |
12 | const state = this.parseUrlToState(url)
13 | // reset history, because on server side, there is no need to keep navigation state
14 | this._history.length = 0
15 | this.push(state, false)
16 | }
17 | })
18 |
19 | export { Navigation }
20 | export default Navigation
21 |
--------------------------------------------------------------------------------
/src/ssr/server/core/component.js:
--------------------------------------------------------------------------------
1 | import { mixin } from 'ts-fns'
2 | import { PrimitiveComponent, Component } from '../../../lib/core/component.js'
3 |
4 | mixin(PrimitiveComponent, class {
5 | })
6 |
7 | export { Component }
8 | export default Component
9 |
--------------------------------------------------------------------------------
/src/ssr/server/index.js:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tangshuang/nautil/1cdd52330d481aef417b7560796c9e68d3a42167/src/ssr/server/index.js
--------------------------------------------------------------------------------
/src/web-component/define.js:
--------------------------------------------------------------------------------
1 | import { mount, unmount } from '../dom/render.js'
2 | import retargetEvents from './retarget-events.js'
3 |
4 | export function define(name, Component, cssText) {
5 | // https://hackernoon.com/how-to-turn-react-component-into-native-web-component-84834315cb24
6 | class NautilCustomElement extends HTMLElement {
7 | constructor() {
8 | super()
9 |
10 | this.attachShadow({ mode: 'open' })
11 | this.observer = new MutationObserver(() => this.update())
12 | this.observer.observe(this, { attributes: true })
13 | }
14 | connectedCallback() {
15 | this._initStyleSheets()
16 | this._initContainer()
17 | this.mount()
18 | this._unBindEvents = retargetEvents(this.shadowRoot)
19 | }
20 | disconnectedCallback(){
21 | this.unmount()
22 | this._unBindEvents()
23 | this.observer.disconnect()
24 | }
25 |
26 | update() {
27 | this.unmount()
28 | this.mount()
29 | }
30 | mount() {
31 | const { props : PropsTypes = {}, propTypes = {} } = Component
32 | const types = { ...propTypes, ...PropsTypes }
33 | const props = {
34 | ...this._getProps(this.attributes, types),
35 | ...this._getEvents(types),
36 | }
37 | mount(this.container, Component, props)
38 | }
39 | unmount() {
40 | unmount(this.container)
41 | }
42 |
43 | _initContainer() {
44 | const container = document.createElement('div')
45 | this.container = container
46 | this.shadowRoot.appendChild(container)
47 | }
48 | _initStyleSheets() {
49 | if (!cssText) {
50 | return
51 | }
52 |
53 | const style = document.createElement('style')
54 | style.type = 'text/css'
55 | if (style.styleSheet) {
56 | style.styleSheet.cssText = cssText
57 | }
58 | else {
59 | style.appendChild(document.createTextNode(cssText))
60 | }
61 |
62 | this.shadowRoot.appendChild(style)
63 | }
64 |
65 | _getEvents(propTypes) {
66 | return Object.keys(propTypes).filter(key => /on([A-Z].*)/.exec(key))
67 | .reduce((events, ev) => ({
68 | ...events,
69 | // ev.substr(2).replace(/^[A-Z]/, letter => letter.toLowerCase())
70 | [ev]: args => this.dispatchEvent(new CustomEvent(ev, args)),
71 | }), {})
72 | }
73 | _getProps(attributes, propTypes = {}) {
74 | return [...attributes].filter(attr => attr.name !== 'style')
75 | .map(attr => this._convertProp(attr.name, attr.value, propTypes))
76 | .reduce((props, prop) => ({ ...props, [prop.name]: prop.value }), {})
77 | }
78 | _convertProp(attrName, attrValue, propTypes) {
79 | const propName = Object.keys(propTypes).find(key => key.toLowerCase() == attrName)
80 | let value = attrValue
81 | if (attrValue === 'true' || attrValue === 'false') {
82 | value = attrValue === 'true'
83 | }
84 | else if (!isNaN(attrValue) && attrValue !== '') {
85 | value = +attrValue
86 | }
87 | else if (/^{.*}/.exec(attrValue)) {
88 | value = JSON.parse(attrValue)
89 | }
90 | return {
91 | name: propName ? propName : attrName,
92 | value,
93 | }
94 | }
95 | }
96 | window.customElements.define(name, NautilCustomElement)
97 | }
98 |
--------------------------------------------------------------------------------
/src/web-component/index.js:
--------------------------------------------------------------------------------
1 | import '../dom/style/transform.js'
2 |
3 | export { Section } from '../dom/elements/section.jsx'
4 | export { Text } from '../dom/elements/text.jsx'
5 | export { Button } from '../dom/elements/button.jsx'
6 | export { Line } from '../dom/elements/line.jsx'
7 |
8 | export { Form } from '../dom/elements/form.jsx'
9 | export { Select } from '../dom/elements/select.jsx'
10 | export { Checkbox } from '../dom/elements/checkbox.jsx'
11 | export { Input } from '../dom/elements/input.jsx'
12 | export { Radio } from '../dom/elements/radio.jsx'
13 | export { Textarea } from '../dom/elements/textarea.jsx'
14 |
15 | export { ListSection } from '../dom/elements/list-section.jsx'
16 | export { ScrollSection } from '../dom/elements/scroll-section.jsx'
17 | export { SwipeSection } from '../dom/elements/swipe-section.jsx'
18 |
19 | export { Image } from '../dom/elements/image.jsx'
20 | export { Audio } from '../dom/elements/audio.jsx'
21 | export { Video } from '../dom/elements/video.jsx'
22 | export { Webview } from '../dom/elements/webview.jsx'
23 |
24 | export { Storage } from '../dom/storage/storage.js'
25 | export { Router } from '../dom/router/router.jsx'
26 |
27 | export { define } from './define.js'
28 |
--------------------------------------------------------------------------------
/src/wechat/components/dynamic/dynamic.js:
--------------------------------------------------------------------------------
1 | import { isShallowEqual } from 'ts-fns'
2 |
3 | const componentConfig = {
4 | properties: {
5 | data: {
6 | type: Object,
7 | },
8 | },
9 | data: {
10 | type: '',
11 | props: {},
12 | children: [],
13 | content: '',
14 | pageId: '',
15 | nodeId: '',
16 | },
17 | observers: {
18 | data(data) {
19 | if (!data) {
20 | return
21 | }
22 |
23 | const { type, props, children = [] } = this.data
24 | if (type !== data.type) {
25 | const { type, props = {}, children = [], content = '' } = data
26 | this.setData({ type, props, children, content })
27 | }
28 | else {
29 | const next = {}
30 | let flag = false
31 |
32 | if (!isShallowEqual(props, data.props)) {
33 | next.props = data.props || {}
34 | flag = true
35 | }
36 |
37 | if (type === '#text') {
38 | if (this.data.content !== data.content) {
39 | next.content = data.content
40 | flag = true
41 | }
42 | }
43 | else if (!isShallowEqual(children.map(item => item.type), data.children.map(item => item.type))) {
44 | next.children = data.children
45 | flag = true
46 | }
47 |
48 | if (flag) {
49 | this.setData(next)
50 | }
51 | }
52 | },
53 | },
54 | lifetimes: {
55 | attached() {
56 | if (!this.properties.data) {
57 | return
58 | }
59 | const { id: nodeId, type, props = {}, children = [], content = '' } = this.properties.data
60 | const pageId = this.getPageId()
61 | this.setData({ type, props, children, pageId, nodeId, content })
62 | },
63 | },
64 | methods: {},
65 | }
66 |
67 | // createHandlers
68 | function createHandle(name) {
69 | return function(e) {
70 | const { props } = this.data
71 | if (typeof props[name] === 'function') {
72 | props[name](e)
73 | }
74 | }
75 | }
76 | const handlers = [
77 | 'bindtap',
78 | 'bindtapstart',
79 | 'bindtapmove',
80 | 'bindtapend',
81 | 'bindtapcancel',
82 | 'bindlongpress',
83 | 'bindgetuserinfo',
84 | 'bindcontact',
85 | 'bindgetphonenumber',
86 | 'binderror',
87 | 'bindopensetting',
88 | 'bindlaunchapp',
89 | 'bindplay',
90 | 'bindpause',
91 | 'bindtimeupdate',
92 | 'bindended',
93 | 'bindchange',
94 | 'bindsubmit',
95 | 'bindreset',
96 | 'bindload',
97 | 'bindinput',
98 | 'bindfocus',
99 | 'bindblur',
100 | 'bindconfirm',
101 | 'bindkeyboardheightchange',
102 | 'bindselect',
103 | 'bindcancel',
104 | 'bindfullscreenchange',
105 | 'bindwaiting',
106 | 'bindprogress',
107 | 'bindloadedmetadata',
108 | 'bindcontrolstoggle',
109 | 'bindenterpictureinpicture',
110 | 'bindleavepictureinpicture',
111 | 'bindseekcomplete',
112 | 'bindmessage',
113 | ]
114 | handlers.forEach((name) => {
115 | componentConfig.methods[name] = createHandle(name)
116 | })
117 |
118 | // eslint-disable-next-line no-undef
119 | Component(componentConfig)
120 |
--------------------------------------------------------------------------------
/src/wechat/components/dynamic/dynamic.json:
--------------------------------------------------------------------------------
1 | {
2 | "_require": {
3 | "setting": {
4 | "es6": true
5 | },
6 | "libVersion": "2.7.1"
7 | },
8 | "component": true,
9 | "usingComponents": {
10 | "dynamic": "./dynamic"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/wechat/components/dynamic/fns.wxs:
--------------------------------------------------------------------------------
1 | function isType(v, t) {
2 | return v.constructor === t
3 | }
4 |
5 | module.exports = {
6 | isType: isType,
7 | }
--------------------------------------------------------------------------------
/src/wechat/elements/audio.jsx:
--------------------------------------------------------------------------------
1 | import { isString, mixin } from 'ts-fns'
2 | import { Audio } from '../../lib/elements/audio.jsx'
3 | import { Style } from '../../lib/style/style.js'
4 |
5 | mixin(Audio, class {
6 | render() {
7 | const { source, width, height, ...rest } = this.attrs
8 | const style = { width, height, ...this.style }
9 | const src = isString(source) ? source : source.uri
10 | return (
11 |
19 | {this.children}
20 |
21 | )
22 | }
23 | })
24 |
25 | export { Audio }
26 | export default Audio
27 |
--------------------------------------------------------------------------------
/src/wechat/elements/button.jsx:
--------------------------------------------------------------------------------
1 | import { mixin } from 'ts-fns'
2 | import { Button } from '../../lib/elements/button.jsx'
3 | import { Style } from '../../lib/style/style.js'
4 |
5 | mixin(Button, class {
6 | render() {
7 | const { type: _type, ...attrs } = this.attrs
8 | return this.dispatch('Hit', e)}
12 | bindtapstart={e => this.dispatch('HitStart', e)}
13 | bindtapend={e => this.dispatch('HitEnd', e)}
14 |
15 | class={this.className}
16 | style={Style.stringify(this.style)}
17 | >{this.children}
18 | }
19 | })
20 |
21 | export { Button }
22 | export default Button
23 |
--------------------------------------------------------------------------------
/src/wechat/elements/checkbox.jsx:
--------------------------------------------------------------------------------
1 | import { mixin } from 'ts-fns'
2 | import { Checkbox } from '../../lib/elements/checkbox.jsx'
3 | import { Style } from '../../lib/style/style.js'
4 |
5 | mixin(Checkbox, class {
6 | render() {
7 | const { checked, ...rest } = this.attrs
8 |
9 | const onChange = (e) => {
10 | this.$attrs.checked = !checked
11 |
12 | if (checked) {
13 | this.dispatch('Uncheck', e)
14 | }
15 | else {
16 | this.dispatch('Check', e)
17 | }
18 |
19 | this.dispatch('Change', e)
20 | }
21 |
22 | return
31 | }
32 | })
33 |
34 | export { Checkbox }
35 | export default Checkbox
36 |
--------------------------------------------------------------------------------
/src/wechat/elements/form.jsx:
--------------------------------------------------------------------------------
1 | import { mixin } from 'ts-fns'
2 | import { Form } from '../../lib/elements/form.jsx'
3 | import { Style } from '../../lib/style/style.js'
4 |
5 | mixin(Form, class {
6 | render() {
7 | return
17 | }
18 | })
19 |
20 | export { Form }
21 | export default Form
22 |
--------------------------------------------------------------------------------
/src/wechat/elements/image.jsx:
--------------------------------------------------------------------------------
1 | import { mixin, isString } from 'ts-fns'
2 | import { Image } from '../../lib/elements/image.jsx'
3 | import { Style } from '../../lib/style/style.js'
4 |
5 | mixin(Image, class {
6 | render() {
7 | const { source, width, height, maxWidth, maxHeight, ...rest } = this.attrs
8 | const style = { width, height, maxWidth, maxHeight, ...this.style }
9 | const children = this.children
10 | const src = isString(source) ? source : source.uri
11 |
12 | // use image as background
13 | if (children) {
14 | return (
15 | {children}
27 | )
28 | }
29 | else {
30 | return (
31 |
39 | )
40 | }
41 | }
42 | })
43 |
44 | export { Image }
45 | export default Image
46 |
--------------------------------------------------------------------------------
/src/wechat/elements/input.jsx:
--------------------------------------------------------------------------------
1 | import { mixin } from 'ts-fns'
2 | import { Input } from '../../lib/elements/input.jsx'
3 | import { Style } from '../../lib/style/style.js'
4 |
5 | mixin(Input, class {
6 | render() {
7 | const { type, ...rest } = this.attrs
8 |
9 | const onChange = (e) => {
10 | const value = e.target.value
11 | this.$attrs.value = type === 'number' || type === 'range' ? +value : value
12 | this.dispatch('Change', e)
13 | }
14 |
15 | return this.dispatch('Focus', e)}
22 | bindblur={e => this.dispatch('Blur', e)}
23 | bindselect={e => this.dispatch('Select', e)}
24 |
25 | class={this.className}
26 | style={Style.stringify(this.style)}
27 | />
28 | }
29 | })
30 |
31 | export { Input }
32 | export default Input
33 |
--------------------------------------------------------------------------------
/src/wechat/elements/line.jsx:
--------------------------------------------------------------------------------
1 | import { mixin } from 'ts-fns'
2 | import { Line } from '../../lib/elements/line.jsx'
3 | import { Style } from '../../lib/style/style.js'
4 |
5 | mixin(Line, class {
6 | render() {
7 | const { width, thick, color, ...rest } = this.attrs
8 | const styles = { display: 'block', borderBottom: `${thick}px solid ${color}`, width, height: 0, ...this.style }
9 | return
10 | }
11 | })
12 |
13 | export { Line }
14 | export default Line
15 |
--------------------------------------------------------------------------------
/src/wechat/elements/list-section.jsx:
--------------------------------------------------------------------------------
1 | import { mixin } from 'ts-fns'
2 | import { ListSection } from '../../lib/elements/list-section.jsx'
3 |
4 | mixin(ListSection, class {
5 | render() {
6 | // TODO
7 | }
8 | })
9 |
10 | export { ListSection }
11 | export default ListSection
12 |
--------------------------------------------------------------------------------
/src/wechat/elements/radio.jsx:
--------------------------------------------------------------------------------
1 | import { mixin } from 'ts-fns'
2 | import { Radio } from '../../lib/elements/radio.jsx'
3 | import { Style } from '../../lib/style/style.js'
4 |
5 | mixin(Radio, class {
6 | render() {
7 | const { checked, ...rest } = this.attrs
8 |
9 | const onChange = (e) => {
10 | this.$attrs.checked = !checked
11 |
12 | if (checked) {
13 | this.dispatch('Uncheck', e)
14 | }
15 | else {
16 | this.dispatch('Check', e)
17 | }
18 |
19 | this.dispatch('Change', e)
20 | }
21 |
22 | return
31 | }
32 | })
33 |
34 | export { Radio }
35 | export default Radio
36 |
--------------------------------------------------------------------------------
/src/wechat/elements/scroll-section.jsx:
--------------------------------------------------------------------------------
1 | import { mixin } from 'ts-fns'
2 | import { ScrollSection } from '../../lib/elements/scroll-section.jsx'
3 |
4 | // const { DOWN, UP, BOTH, NONE, ACTIVATE, DEACTIVATE, RELEASE, FINISH } = ScrollSection
5 |
6 | mixin(ScrollSection, class {
7 | render() {
8 | // TODO
9 | }
10 | })
11 |
12 | export { ScrollSection }
13 | export default ScrollSection
14 |
--------------------------------------------------------------------------------
/src/wechat/elements/section.jsx:
--------------------------------------------------------------------------------
1 | import { mixin } from 'ts-fns'
2 | import { Section } from '../../lib/elements/section.jsx'
3 | import { Style } from '../../lib/style/style.js'
4 |
5 | mixin(Section, class {
6 | render() {
7 | return this.dispatch('Hit', e)}
11 | bindtapstart={e => this.dispatch('HitStart', e)}
12 | bindtapmove={e => this.dispatch('HitMove', e)}
13 | bindtapend={e => this.dispatch('HitEnd', e)}
14 | bindtapcancel={e => this.dispatch('HitCancel', e)}
15 |
16 | class={this.className}
17 | style={Style.stringify(this.style)}
18 | >{this.children}
19 | }
20 | })
21 |
22 | export { Section }
23 | export default Section
24 |
--------------------------------------------------------------------------------
/src/wechat/elements/select.jsx:
--------------------------------------------------------------------------------
1 | import { mixin, decideby } from 'ts-fns'
2 | import { Select } from '../../lib/elements/select.jsx'
3 | import { isRef } from '../../lib/utils.js'
4 | import { Style } from '../../lib/style/style.js'
5 |
6 | mixin(Select, class {
7 | render() {
8 | const { inputRef, options, optionValueKey, optionTextKey, placeholder, defaultValue, ...attrs } = this.attrs
9 | const value = 'value' in attrs ? attrs.value : 'defaultValue' in attrs ? defaultValue : ''
10 | const text = decideby(() => {
11 | if (value) {
12 | const item = options.find(item => item[optionValueKey || 'value'] === value)
13 | if (item) {
14 | return optionTextKey ? item[optionTextKey] : item.text
15 | }
16 | }
17 | return ''
18 | })
19 | const onChange = (e) => {
20 | const value = e.target.value
21 | const item = options.find(item => item[optionValueKey || 'value'] + '' === value)
22 | this.$attrs.value = item[optionValueKey || 'value']
23 | this.dispatch('Change', e)
24 | }
25 |
26 | return (
27 | isRef(inputRef) && (inputRef.current = el)}
37 | >
38 | {!value && placeholder ? {placeholder} : null}
39 | {value ? {text} : null}
40 |
41 | )
42 | }
43 | })
44 |
45 | export { Select }
46 | export default Select
47 |
--------------------------------------------------------------------------------
/src/wechat/elements/swipe-section.jsx:
--------------------------------------------------------------------------------
1 | import { mixin } from 'ts-fns'
2 | import { SwipeSection } from '../../lib/elements/swipe-section.jsx'
3 |
4 | mixin(SwipeSection, class {
5 | render() {
6 | // TODO
7 | }
8 | })
9 |
10 | export { SwipeSection }
11 | export default SwipeSection
12 |
--------------------------------------------------------------------------------
/src/wechat/elements/text.jsx:
--------------------------------------------------------------------------------
1 | import { mixin } from 'ts-fns'
2 | import { Text } from '../../lib/elements/text.jsx'
3 | import { Style } from '../../lib/style/style.js'
4 |
5 | mixin(Text, class {
6 | render() {
7 | return {this.children}
8 | }
9 | })
10 |
11 | export { Text }
12 | export default Text
13 |
--------------------------------------------------------------------------------
/src/wechat/elements/textarea.jsx:
--------------------------------------------------------------------------------
1 | import { mixin } from 'ts-fns'
2 | import { Textarea } from '../../lib/elements/textarea.jsx'
3 | import { Style } from '../../lib/style/style.js'
4 |
5 | mixin(Textarea, class {
6 | render() {
7 | const { line, placeholder, value, ...rest } = this.attrs
8 |
9 | const onChange = (e) => {
10 | const value = e.target.value
11 | this.$attrs.value = value
12 | this.dispatch('Change', e)
13 | }
14 |
15 | return
30 | }
31 | })
32 |
33 | export { Textarea }
34 | export default Textarea
35 |
--------------------------------------------------------------------------------
/src/wechat/elements/video.jsx:
--------------------------------------------------------------------------------
1 | import { mixin, isString } from 'ts-fns'
2 | import { Video } from '../../lib/elements/video.jsx'
3 | import { Style } from '../../lib/style/style.js'
4 |
5 | mixin(Video, class {
6 | render() {
7 | const { source, width, height, ...rest } = this.attrs
8 | const style = { width, height, ...this.style }
9 | const src = isString(source) ? source : source.uri
10 | return (
11 |
19 | {this.children}
20 |
21 | )
22 | }
23 | })
24 |
25 | export { Video }
26 | export default Video
27 |
--------------------------------------------------------------------------------
/src/wechat/elements/webview.jsx:
--------------------------------------------------------------------------------
1 | import { mixin, isString } from 'ts-fns'
2 | import { Webview } from '../../lib/elements/webview.jsx'
3 | import { Style } from '../../lib/style/style.js'
4 |
5 | mixin(Webview, class {
6 | render() {
7 | const { source, width, height, ...rest } = this.attrs
8 | const style = { width, height, ...this.style }
9 | const src = isString(source) ? source : source.uri
10 | return (
11 |
17 | )
18 | }
19 | })
20 |
21 | export { Webview }
22 | export default Webview
23 |
--------------------------------------------------------------------------------
/src/wechat/i18n/language-detector.js:
--------------------------------------------------------------------------------
1 | import { LanguageDetector } from '../../lib/i18n/language-detector.js'
2 |
3 | LanguageDetector.getLang = () => {
4 | return new Promise((resolve, reject) => {
5 | // eslint-disable-next-line no-undef
6 | wx.getSystemInfo({
7 | success: (res) => {
8 | resolve(res.language)
9 | },
10 | error: reject,
11 | })
12 | })
13 | }
14 |
15 | export { LanguageDetector }
16 | export default LanguageDetector
17 |
--------------------------------------------------------------------------------
/src/wechat/index.js:
--------------------------------------------------------------------------------
1 | import './style/transform.js'
2 |
3 | export { Section } from './elements/section.jsx'
4 | export { Text } from './elements/text.jsx'
5 | export { Button } from './elements/button.jsx'
6 | export { Line } from './elements/line.jsx'
7 |
8 | export { Form } from './elements/form.jsx'
9 | export { Select } from './elements/select.jsx'
10 | export { Checkbox } from './elements/checkbox.jsx'
11 | export { Input } from './elements/input.jsx'
12 | export { Radio } from './elements/radio.jsx'
13 | export { Textarea } from './elements/textarea.jsx'
14 |
15 | export { ListSection } from './elements/list-section.jsx'
16 | export { ScrollSection } from './elements/scroll-section.jsx'
17 | export { SwipeSection } from './elements/swipe-section.jsx'
18 |
19 | export { Image } from './elements/image.jsx'
20 | export { Audio } from './elements/audio.jsx'
21 | export { Video } from './elements/video.jsx'
22 | export { Webview } from './elements/webview.jsx'
23 |
24 | export { Router } from './router/router.jsx'
25 | export { Storage } from './storage/storage.js'
26 | export { LanguageDetector } from './i18n/language-detector.js'
27 |
28 | export { registerApp, registerPage, createBehavior, runApp } from './render.js'
29 |
--------------------------------------------------------------------------------
/src/wechat/router/router.jsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | import { mixin } from 'ts-fns'
3 | import { Router } from '../../lib/router/router.jsx'
4 | import { History } from '../../lib/router/history.js'
5 | import { revokeUrl } from '../../lib/utils.js'
6 |
7 | class WechatHistory extends History {
8 | getUrl(abs, mode) {
9 | const { query, base } = mode
10 | const root = base && base !== '/' ? base + abs : abs
11 |
12 | const pages = getCurrentPages()
13 | const currentPage = pages[pages.length - 1]
14 | const { options = {} } = currentPage
15 | const search = options[query] || ''
16 |
17 | const url = decodeURIComponent(search)
18 | return revokeUrl(root, url)
19 | }
20 | setUrl(to, abs, mode, params, replace) {
21 | const url = this.makeUrl(to, abs, mode, params)
22 | if (replace) {
23 | wx.redirectTo({
24 | path: url,
25 | })
26 | }
27 | else {
28 | wx.navigateTo({
29 | path: url,
30 | })
31 | }
32 | }
33 | makeUrl(to, abs, mode, params) {
34 | const { query } = mode
35 |
36 | const url = this.$discernUrl(to, abs, mode, params)
37 | const encoded = encodeURIComponent(url)
38 |
39 | const pages = getCurrentPages()
40 | const currentPage = pages[pages.length - 1]
41 | const { route } = currentPage
42 | const page = route.split('/').pop()
43 | return page + '?' + query + '=' + encoded
44 | }
45 | }
46 |
47 | History.implement('search', WechatHistory)
48 |
49 | mixin(Router, class {
50 | static $createLink(data) {
51 | const { children, href, open, navigate, ...attrs } = data
52 | const handleClick = () => {
53 | if (open) {
54 | wx.navigateToMiniProgram({
55 | appId: process.env.WECHAT_MINIPROGRAM_APP_ID,
56 | path: href,
57 | })
58 | }
59 | else {
60 | navigate()
61 | }
62 | }
63 | return (
64 | {children}
65 | )
66 | }
67 | })
68 |
69 | export { Router }
70 | export default Router
71 |
--------------------------------------------------------------------------------
/src/wechat/storage/storage.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | import { mixin } from 'ts-fns'
3 | import { Storage } from '../../lib/storage/storage.js'
4 |
5 | mixin(Storage, class {
6 | static async getItem(key) {
7 | return new Promise((resolve,) => {
8 | wx.getStorage({
9 | key,
10 | success(res) {
11 | resolve(res.data)
12 | },
13 | fail() {
14 | resolve()
15 | },
16 | })
17 | })
18 | }
19 | static async setItem(key, value) {
20 | return new Promise((resolve, reject) => {
21 | wx.setStorage({
22 | key,
23 | data: value,
24 | success() {
25 | resolve()
26 | },
27 | fail(error) {
28 | reject(error)
29 | },
30 | })
31 | })
32 | }
33 | static async delItem(key) {
34 | return new Promise((resolve, reject) => {
35 | wx.removeStorage({
36 | key,
37 | success() {
38 | resolve()
39 | },
40 | fail(error) {
41 | reject(error)
42 | },
43 | })
44 | })
45 | }
46 | static async clear() {
47 | return new Promise((resolve, reject) => {
48 | wx.clearStorage({
49 | success() {
50 | resolve()
51 | },
52 | fail(error) {
53 | reject(error)
54 | },
55 | })
56 | })
57 | }
58 | })
59 |
60 | export { Storage }
61 | export default Storage
62 |
--------------------------------------------------------------------------------
/src/wechat/style/transform.js:
--------------------------------------------------------------------------------
1 | import { isNumber, isArray, each, mixin } from 'ts-fns'
2 | import { Transform } from '../../lib/style/transform.js'
3 |
4 | mixin(Transform, class {
5 | get() {
6 | const rules = this.rules
7 | const convert = v => isNumber(v) ? parseInt(v, 10) + 'px' : v
8 |
9 | let text = ''
10 | each(rules, (value, key) => {
11 | const v = isArray(value) ? value.map(convert).join(', ') : convert(value)
12 | text += `${key}(${v}) `
13 | })
14 |
15 | return text
16 | }
17 | })
18 |
19 | export { Transform }
20 | export default Transform
21 |
--------------------------------------------------------------------------------
/web-component.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 |
3 | import { Component } from "react"
4 |
5 | export declare function define(name: string, C: Component, cssText: string): void
6 |
--------------------------------------------------------------------------------
/wechat.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 |
3 | import { Component } from "react"
4 |
5 | export declare function registerApp(register: (context: any) => any): void
6 | export declare function registerPage(register: (context: any) => any, dataKey: string, C: Component, props?: any): void
7 | export declare function createBehavior(dataKey: string, C: Component, props?: any): any
8 | export declare function runApp(App: Component, regApp: (context: any) => any, regPage: (context: any) => any): (isApp: boolean) => void
9 |
--------------------------------------------------------------------------------