├── .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 |
24 | 25 |
26 | {item.text} 27 |
28 | } /> 29 |
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 |
17 | 沸点 18 |
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 | 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 |
53 |
Title:</Section> 54 | <Section><Text>Price:</Text><Price /></Section> 55 | <Section><IncreasePriceButton /></Section> 56 | </Section> 57 | ) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.examples/dev/app.jsx: -------------------------------------------------------------------------------- 1 | import { Router, createBootstrap, Link } from 'nautil' 2 | 3 | function Home() { 4 | return <Link to="detail">go to detail</Link> 5 | } 6 | 7 | function Detail() { 8 | return ( 9 | <> 10 | <Link to="">go to home</Link> 11 | <Link to="./content">go to content</Link> 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 <Outlet /> 45 | } 46 | 47 | export default bootstrap(App) 48 | -------------------------------------------------------------------------------- /.examples/dev/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <meta charset="utf-8"> 3 | <meta name="viewport" content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=no"> 4 | <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> 5 | 6 | <style> 7 | html, body { 8 | margin: 0; 9 | padding: 0; 10 | font-family: -apple-system,system-ui,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,Helvetica Neue,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Arial; 11 | background-color: #f9f9f9; 12 | } 13 | </style> 14 | 15 | <div id="root"></div> 16 | <script src="index.js"></script> 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 <Some 17 | value={this.state.value} 18 | // here, I pass a stream directly into `onChange` 19 | onChange={this.change$} 20 | /> 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 <Input 12 | value={value} 13 | // use `emit` to notify event, which will emit stream this.Change$ 14 | onChange={(e) => 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 <Button onHit={() => setState(state + 1)}>{state}</Button> 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 | <div className="tabs"> 41 | <div> 42 | <Link to="home"><T>Home</T></Link> 43 | <Link to="list"><T>List</T></Link> 44 | <Link to={`detail/${id}`}><T>Detail</T></Link> 45 | </div> 46 | <Outlet /> 47 | </div> 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 | <div> 22 | <h2>Detail</h2> 23 | <div>{this.state.content}</div> 24 | <div className='tabs'> 25 | <Link to="basic"><T>Basic</T></Link> 26 | <Link to="extra"><T>Extra</T></Link> 27 | </div> 28 | <Outlet /> 29 | </div> 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 <Toggler $show={$show} /> 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 ? <Section>{this.children}</Section> : null} 11 | <Button onHit={() => this.attrs.show = !this.attrs.show}>toggle</Button> 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 | <div align="center"><img src="./assets/nautil-logo.png" width="200" height="200"></div> 4 | 5 | <div align="center">Enterprise Level Business System Frontend Framework</div> 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 | <Animation show={this.state.show} enter="fade:in move:left,top/right,bottom 100 easeInCubic" leave="fade:out move:right/left 100"> 7 | ... 8 | </Animation> 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 | <Some $show={$state.show} /> 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 <app> 21 | ``` 22 | 23 | Build a app, `app` stands for a dir name in `src/apps` dir. The dest dir is `dist/<app>`. 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 <app> 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 <app> <io> 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 <app> <io> 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 <app>`, 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 <app>` or `nautil dev <app>` 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 | <Some one={one} $two={two} onSee={onSee} /> 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 <button onClick={() => this.$attrs.two ++}>change</button> 39 | } 40 | } 41 | ``` 42 | 43 | ```js 44 | const $two = useState(0) 45 | <SomeComponent $two={$two} /> 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 <button onClick={() => props.age ++ /* not support */}>grow</button> 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 <button onClick={() => this.attrs.age ++}>grow</button> 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 <div stylesheet={[...]}>xxx</div> 38 | } 39 | A.props = {} 40 | A.defaultStylesheet = [] 41 | 42 | // ok 43 | function A() { 44 | return <Section stylesheet={[...]}>xxx</Section> 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 <Section stylesheet={[...]}>xxx</Section> 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 <Input $value={[value, setState]}> 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 | ![](../assets/nautil-lifecircle.jpg) 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 | <Section stylesheet={Styles.container}> 13 | <Text stylesheet={[Styles.text, 'mui-text', this.className]}>xxx</Text> 14 | <Text stylesheet={[Styles.text, { textAlign: 'right' }, this.style]}>xxx</Text> 15 | </Section> 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 | <A stylesheet={{ 'some-classname': !!show, color: '#999000' }} /> 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 | <Section stylesheet={this.css('a b c')}> 84 | ... 85 | </Section> 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 | <Async 5 | await={() => new Promise(...)} 6 | then={data => <Text>{data.text}</Text>} 7 | catch={e => <Text>{e.message}</Text>} 8 | pending={<Loading />} 9 | /> 10 | ``` 11 | -------------------------------------------------------------------------------- /docs/components/each.md: -------------------------------------------------------------------------------- 1 | # Each 2 | 3 | ```js 4 | <Each of={Array|Object} render={(value, key) => 5 | <Text>{key}: {value}</Text> 6 | } /> 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/components/for.md: -------------------------------------------------------------------------------- 1 | # For 2 | 3 | ```js 4 | <For start={1} end={10} step={1} render={i => 5 | <Text>{i}</Text> 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 | <If is={Boolean} render={Function} /> 11 | ``` 12 | 13 | or 14 | 15 | ```js 16 | <If is={Boolean}> 17 | {Function} 18 | </If> 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 | <If is={condition1} render={render}> 29 | <ElseIf is={condition2} render={render} /> 30 | </If> 31 | ``` 32 | 33 | ## Else 34 | 35 | The same as `ElseIf` only without `is` prop. 36 | 37 | ```js 38 | <If is={condition1} render={render}> 39 | <ElseIf is={condition2} render={render} /> 40 | <Else render={render} /> 41 | </If> 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 | <Observer 15 | subscribe={dispatch => store.subscribe(dispatch)} 16 | unsubscribe={dispatch => store.unsubscribe(dispatch)} 17 | dispatch={this.update} 18 | > 19 | {() => <Text>{store.state.some}</Text>} 20 | </Observer> 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 | <Prepare ready={Boolean} placeholder={<Text>loading</Text>} render={() => 5 | <Text>loaded</Text> 6 | } /> 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/components/static.md: -------------------------------------------------------------------------------- 1 | # Static 2 | 3 | ```js 4 | <Static shouldUpdate={Boolean} render={() => 5 | <Text>{Date.now()}</Text> 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 | <Switch of={Any}> 9 | <Case is={Any} render={Function} /> 10 | </Switch> 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 | <Case is={Any} default break render={Function} /> 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 | <Switch of={index%2}> 27 | <Case is={1} break={index%3 === 1} render={Function} /> 28 | <Case is={1} render={Function} /> 29 | </Switch> 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 | <Observer 13 | dispatch={this.update} 14 | subscribe={dispatch => store.on('*', dispatch)} 15 | unsubscribe={dispatch => store.off('*', dispatch)} 16 | > 17 | {store.get('some')} 18 | </Observer> 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 | <Input 15 | value={state.value} 16 | onChange={[ 17 | // pipe chain 18 | map(e => 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 | <Input 41 | value={state.value} 42 | onChange={change$} 43 | /> 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 | <Input $value={$some} /> 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 <H render={(state, data) => 22 | * <C {...props} state={state} data={data} /> 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 | <Provider store={store}> 16 | <Language i18n={i18n}> 17 | <OriginalComponent {...}> 18 | </Language> 19 | </Provider> 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 | <Audio source={...} /> 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 | <Button>click</Button> 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 | <Checkbox > 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 | <Checkbox $checked={checked}> 20 | ``` -------------------------------------------------------------------------------- /docs/elements/form.md: -------------------------------------------------------------------------------- 1 | # Form 2 | 3 | ```js 4 | import { Form } from 'nautil' 5 | 6 | <Form> 7 | ... 8 | </Form> 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 | <Image source={...} /> 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 | <Input value={value} onChange={onChange} /> 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 <Input $value={value} /> 26 | } 27 | ``` -------------------------------------------------------------------------------- /docs/elements/line.md: -------------------------------------------------------------------------------- 1 | # Line 2 | 3 | ```js 4 | import { Line } from 'nautil' 5 | 6 | <Line length={1} thick={1} color="#ccc" /> 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 | <ListSection 9 | data={[...]} 10 | itemRender={(item) => ...} 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 | <Radio > 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 | <Radio $checked={checked}> 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 | <ScrollSection 9 | direction="up" 10 | distance={10} 11 | damping={.5} 12 | 13 | topLoading={this.state.loading} 14 | topIndicator={{ 15 | activate: 'release', 16 | deactivate: 'pull', 17 | release: <Loading />, 18 | finish: 'finish', 19 | }} 20 | onTopRelease={fetchData} 21 | > 22 | ... 23 | </ScrollSection> 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 | <Section stylesheet={...}> 7 | ... 8 | </Section> 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 | <Select options={[...]} value={value} onChange={onChange} /> 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 | <Select options={[...]} $value={value} /> 23 | ``` -------------------------------------------------------------------------------- /docs/elements/swipe-section.md: -------------------------------------------------------------------------------- 1 | # SwipeSection 2 | 3 | ```js 4 | import { SwipeSection } from 'nautil' 5 | 6 | <SwipeSection> 7 | ... 8 | </SwipeSection> 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>text</Text> 7 | ``` 8 | -------------------------------------------------------------------------------- /docs/elements/textarea.md: -------------------------------------------------------------------------------- 1 | # Textarea 2 | 3 | ```js 4 | import { Textarea } from 'nautil' 5 | 6 | <Textarea value={value} onChange={onChange} /> 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 | <Textarea $value={value} /> 23 | ``` -------------------------------------------------------------------------------- /docs/elements/video.md: -------------------------------------------------------------------------------- 1 | # Video 2 | 3 | ```js 4 | import { Video } from 'nautil' 5 | 6 | <Video source={...}> 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 | <Webview source={...}> 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 <button onClick={forceUpdate}>x</button> 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 <Input $value={$state.value} /> 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 <SomeModal $show={[attrs.show, show => $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 <SomeModal $show={[state.show, show => $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) => <Item key={keys[i]} data={item} />) 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 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <title>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 |
27 | 28 |
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 | 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 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
this.dispatch('Change', e)} 9 | onReset={e => this.dispatch('Reset', e)} 10 | onSubmit={e => this.dispatch('Submit', e)} 11 | 12 | style={this.style} 13 | className={this.className} 14 | >{this.children}
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 | 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 | 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 | 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 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
this.dispatch('Change', e)} 11 | bindreset={e => this.dispatch('Reset', e)} 12 | bindsubmit={e => this.dispatch('Submit', e)} 13 | 14 | class={this.className} 15 | style={Style.stringify(this.style)} 16 | >{this.children}
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 | 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 | --------------------------------------------------------------------------------