Repository: cyclejs/cycle-react-native Branch: master Commit: 3562213aae51 Files: 12 Total size: 19.6 KB Directory structure: gitextract_7tgwe5p_/ ├── .gitignore ├── LICENSE ├── package.json ├── readme.md ├── src/ │ ├── components.ts │ ├── driver.ts │ ├── helper.ts │ └── index.ts ├── test/ │ ├── helpers.ts │ ├── index.ts │ └── tsconfig.json └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ /.vscode /lib /coverage node_modules package-lock.json shrinkwrap.yaml yarn.lock pnpm-lock.yaml ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2018 Andre 'Staltz' Medeiros Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: package.json ================================================ { "name": "@cycle/react-native", "version": "3.0.0", "description": "Cycle.js driver that uses React Native to render", "author": "Andre Staltz ", "license": "MIT", "bugs": "https://github.com/cyclejs/react-native/issues", "repository": "https://github.com/cyclejs/react-native/tree/master", "keywords": [ "react", "cyclejs", "xstream", "mvi", "react-native", "driver" ], "files": [ "lib" ], "main": "lib/cjs/index.js", "typings": "lib/cjs/index.d.ts", "dependencies": { "@cycle/react": "2.x", "xstream": "11.x.x" }, "peerDependencies": { "react": ">=16.8.0", "react-native": ">=0.64.0" }, "devDependencies": { "@cycle/run": "^5.4.0", "@huston007/react-native-mock": "^0.3.3", "@types/mocha": "^9.0.0", "@types/node": "^14.6.0", "@types/react": "16.9.46", "@types/react-native": "0.63.8", "@types/react-test-renderer": "^16.9.3", "babel-cli": "^6.26.0", "babel-preset-react-native": "4.0.1", "expo": "38.x.x", "mocha": "^9.1.3", "prettier": "^2.5.1", "react": ">=17.0.2", "react-native": ">=0.64.0", "react-test-renderer": "17.0.2", "ts-node": "^10.4.0", "typescript": "4.5.2", "xstream": "^11.14.0" }, "publishConfig": { "access": "public" }, "scripts": { "prepublishOnly": "npm run compile", "compile": "rm -rf lib && npm run compile-cjs && npm run compile-es6", "compile-cjs": "tsc --module commonjs --outDir ./lib/cjs", "compile-es6": "tsc --module es6 --outDir ./lib/es6", "test": "ts-node -P test/tsconfig.json --require @huston007/react-native-mock/mock.js node_modules/mocha/bin/mocha test/*.ts" }, "prettier": { "singleQuote": true, "trailingComma": "es5", "bracketSpacing": false } } ================================================ FILE: readme.md ================================================ # Cycle React Native > Cycle.js driver that uses React Native to render - Provides a driver factory `makeReactNativeDriver` - Contains hyperscript helper functions, such as `View()`, `Text()`, etc ``` npm install @cycle/react-native ``` ## Example ```js import xs from 'xstream'; import {run} from '@cycle/run'; import {makeReactNativeDriver, TouchableOpacity, View, Text} from '@cycle/react-native'; function main(sources) { const inc = Symbol(); const inc$ = sources.react.select(inc).events('press'); const count$ = inc$.fold(count => count + 1, 0); const elem$ = count$.map(i => TouchableOpacity(inc, [ View([ Text(`Counter: ${i}`), ]) ]), ); return { react: elem$, }; } // Necessary shim in React Native's JS engine global.queueMicrotask = fn => setTimeout(fn, 1); run(main, { react: makeReactNativeDriver('MyApp'), }); ``` ## Installation Start by installing React Native prerequisites (XCode, react-native-cli, watchman). Then create a React Native project using the CLI. When the project is set up, npm install `@cycle/react-native`, `@cycle/run`, and a stream library like `xstream`, then replace the index.js with something that looks like the example code in this readme. ## API ### `makeReactNativeDriver(appKey)` Returns a driver that uses React Native to render your Cycle.js app in the native application known by the string `appKey`. ### Hyperscript helpers Import hyperscript helpers such as `View`, `Text`, `TouchableOpacity`, etc to create React elements to represent the respective built-in native components: ``, ``, ``, etc. The basic usage is `View(props, children)`, but some variations and shortcuts are allowed: - `View()` becomes `` - `View(props)` becomes `` - `Text('text content')` becomes `text content` - `View([child1, child2])` - `Text(props, 'text content')` - `View(props, [child1, child2])` - etc There are also shortcuts for (MVI) intent selectors: - `Touchable(someSymbol)` becomes `h(Touchable, {sel: someSymbol})` - `Touchable(sym, props)` becomes `h(Touchable, {sel: sym, ...props})` - `Text('myselector', 'text content')` - `Touchable('inc', [child])` - `Touchable('inc', propsObject, [child])` - etc For non-built-in components (e.g. third party) components, you can use `h(ThirdPartyComponent)` with `h` from `@cycle/react` or you can build a helper using `makeHelper(ThirdPartyComponent)` with `makeHelper` from `@cycle/react-native`. ## Other native drivers This library only covers React components in React Native and View-related rendering. For other native APIs in React Native, use drivers specifically built for those. See the list below: - [cycle-native-alert](https://gitlab.com/staltz/cycle-native-alert) - [cycle-native-asyncstorage](https://gitlab.com/staltz/cycle-native-asyncstorage) - [cycle-native-android-local-notification](https://gitlab.com/staltz/cycle-native-android-local-notification) - [cycle-native-clipboard](https://gitlab.com/staltz/cycle-native-clipboard) - [cycle-native-keyboard](https://gitlab.com/staltz/cycle-native-keyboard) - [cycle-native-linking](https://gitlab.com/staltz/cycle-native-linking) - [cycle-native-share](https://gitlab.com/staltz/cycle-native-share) - [cycle-native-toastandroid](https://gitlab.com/staltz/cycle-native-toastandroid) - (please build more of these and add them here!) ## License MIT, Andre 'Staltz' Medeiros 2018 ================================================ FILE: src/components.ts ================================================ import { // General ActivityIndicator as _ActivityIndicator, Button as _Button, FlatList as _FlatList, Image as _Image, KeyboardAvoidingView as _KeyboardAvoidingView, Modal as _Modal, Pressable as _Pressable, RefreshControl as _RefreshControl, ScrollView as _ScrollView, SectionList as _SectionList, StatusBar as _StatusBar, Switch as _Switch, Text as _Text, TextInput as _TextInput, TouchableHighlight as _TouchableHighlight, TouchableOpacity as _TouchableOpacity, TouchableWithoutFeedback as _TouchableWithoutFeedback, View as _View, // Android DrawerLayoutAndroid as _DrawerLayoutAndroid, TouchableNativeFeedback as _TouchableNativeFeedback, // iOS InputAccessoryView as _InputAccessoryView, SafeAreaView as _SafeAreaView, } from 'react-native'; import {makeHelper} from './helper'; export const ActivityIndicator = makeHelper(_ActivityIndicator); export const Button = makeHelper(_Button); export const DrawerLayoutAndroid = makeHelper(_DrawerLayoutAndroid); export const FlatList = makeHelper(_FlatList); export const Image = makeHelper(_Image); export const InputAccessoryView = makeHelper(_InputAccessoryView); export const KeyboardAvoidingView = makeHelper(_KeyboardAvoidingView); export const Modal = makeHelper(_Modal); export const RefreshControl = makeHelper(_RefreshControl); export const ScrollView = makeHelper(_ScrollView); export const SectionList = makeHelper(_SectionList); export const StatusBar = makeHelper(_StatusBar); export const Switch = makeHelper(_Switch); export const TextInput = makeHelper(_TextInput); export const Text = makeHelper(_Text); export const TouchableHighlight = makeHelper(_TouchableHighlight); export const TouchableNativeFeedback = makeHelper(_TouchableNativeFeedback); export const TouchableOpacity = makeHelper(_TouchableOpacity); export const TouchableWithoutFeedback = makeHelper(_TouchableWithoutFeedback); export const View = makeHelper(_View); ================================================ FILE: src/driver.ts ================================================ import {Stream} from 'xstream'; import {ReactSource, makeCycleReactComponent} from '@cycle/react'; import {ReactElement} from 'react'; import {AppRegistry} from 'react-native'; export function makeReactNativeDriver( appKey: string, { registerRootComponent = (Root) => AppRegistry.registerComponent(appKey, () => Root), }: {registerRootComponent?: (component: React.ComponentType) => any} = {} ) { return function reactNativeDriver(sink: Stream>) { const source = new ReactSource(); const Root = makeCycleReactComponent(() => ({source, sink})); registerRootComponent(Root); return source; }; } ================================================ FILE: src/helper.ts ================================================ import {ReactElement, ComponentType} from 'react'; import {h} from '@cycle/react'; function parseSelector(param: any) { if (typeof param === 'symbol') return param; if (typeof param === 'string' && param.length > 0) return param; return null; } export type Children = Array> | string; export type HelperSig

= { (sel: symbol): ReactElement

; (child: string): ReactElement

; (props: P): ReactElement

; (children: Children): ReactElement

; (sel: string | symbol, props: P): ReactElement

; (props: P, children: Children): ReactElement

; (sel: string | symbol, children: Children): ReactElement

; (sel: string | symbol, props: P, children: Children): ReactElement

; }; export function makeHelper

(type: ComponentType

): HelperSig

{ return function helper(a?: any, b?: any, c?: any): ReactElement

{ const hasA = typeof a !== 'undefined'; const hasB = typeof b !== 'undefined'; const hasBChildren = Array.isArray(b) || typeof b === 'string'; const hasC = typeof c !== 'undefined'; const sel = parseSelector(a); if (sel) { if (hasB && hasC) { return h(type, {...b, sel}, c); } else if (hasB && hasBChildren) { return h(type, {sel} as any, b as Array>); } else if (hasB) { return h(type, {...b, sel}); } else if (typeof sel === 'symbol') { return h(type, {sel} as any); } else { return h(type, sel as any /* child, not a sel */); } } else if (hasC) { return h(type, b, c); } else if (hasB) { return h(type, a, b); } else if (hasA) { return h(type, a); } else { return h(type); } }; } ================================================ FILE: src/index.ts ================================================ export * from './helper'; export * from './components'; export * from './driver'; ================================================ FILE: test/helpers.ts ================================================ import 'mocha'; import * as renderer from 'react-test-renderer'; import * as React from 'react'; import xs, {Stream} from 'xstream'; import {h, ReactSource, makeCycleReactComponent} from '@cycle/react'; import {run} from '@cycle/run'; import {makeHelper, View, Text} from '../src/index'; import {View as _View} from 'react-native'; const assert = require('assert'); class _Touchable extends React.PureComponent { public press() { if (this.props.onPress) { this.props.onPress(null); } } public render() { return this.props.children; } } describe('helpers', function () { it('Touchable w/ selector, Text w/ text child, View w/ children', (done) => { const Touchable = makeHelper(_Touchable); function main(sources: {react: ReactSource}) { const inc$ = sources.react.select('button').events('press'); const count$ = inc$.fold((acc: number, x: any) => acc + 1, 0); const vdom$ = count$.map((i: number) => Touchable('button', [View([Text('' + i)])]) ); return {react: vdom$}; } function testDriver(sink: Stream>) { let turn = 0; const source = new ReactSource(); const Root = makeCycleReactComponent(() => ({source, sink})); const r = renderer.create(React.createElement(Root as any)); const root = r.root; const check = () => { const to = root.findByType(_Touchable); const view = to.props.children; const text = view.props.children; assert.strictEqual(text.props.children, `${turn}`); to.instance.press(); turn++; if (turn === 3) { done(); } }; setTimeout(check, 50); setTimeout(check, 100); setTimeout(check, 150); return source; } run(main, {react: testDriver}); }); it('View w/ symbol selector', (done) => { function main(sources: {react: ReactSource}) { const foo = Symbol(); const vdom$ = xs.of(View(foo)); return {react: vdom$}; } function testDriver(sink: Stream>) { const source = new ReactSource(); const Root = makeCycleReactComponent(() => ({source, sink})); const r = renderer.create(React.createElement(Root as any)); const root = r.root; setTimeout(() => { const view = root.findByType(_View); assert.strictEqual(!!view, true); done(); }, 50); return source; } run(main, {react: testDriver}); }); it('View w/ selector and props', (done) => { function main(sources: {react: ReactSource}) { const vdom$ = xs.of(View('foo', {accessible: true})); return {react: vdom$}; } function testDriver(sink: Stream>) { const source = new ReactSource(); const Root = makeCycleReactComponent(() => ({source, sink})); const r = renderer.create(React.createElement(Root as any)); const root = r.root; setTimeout(() => { const view = root.findByType(_View); assert.strictEqual(view.props.accessible, true); done(); }, 50); return source; } run(main, {react: testDriver}); }); it('View w/ selector and children', (done) => { function main(sources: {react: ReactSource}) { const vdom$ = xs.of(View('foo', [Text('hello world')])); return {react: vdom$}; } function testDriver(sink: Stream>) { const source = new ReactSource(); const Root = makeCycleReactComponent(() => ({source, sink})); const r = renderer.create(React.createElement(Root as any)); const root = r.root; setTimeout(() => { const view = root.findByType(_View); const text = view.props.children; assert.strictEqual(text.props.children, 'hello world'); done(); }, 50); return source; } run(main, {react: testDriver}); }); it('View w/ selector and props and children', (done) => { function main(sources: {react: ReactSource}) { const vdom$ = xs.of( View('foo', {accessible: true}, [Text('hello world')]) ); return {react: vdom$}; } function testDriver(sink: Stream>) { const source = new ReactSource(); const Root = makeCycleReactComponent(() => ({source, sink})); const r = renderer.create(React.createElement(Root as any)); const root = r.root; setTimeout(() => { const view = root.findByType(_View); const text = view.props.children; assert.strictEqual(view.props.accessible, true); assert.strictEqual(text.props.children, 'hello world'); done(); }, 50); return source; } run(main, {react: testDriver}); }); it('View w/ props and children', (done) => { function main(sources: {react: ReactSource}) { const vdom$ = xs.of(View({accessible: true}, [Text('hello world')])); return {react: vdom$}; } function testDriver(sink: Stream>) { const source = new ReactSource(); const Root = makeCycleReactComponent(() => ({source, sink})); const r = renderer.create(React.createElement(Root as any)); const root = r.root; setTimeout(() => { const view = root.findByType(_View); const text = view.props.children; assert.strictEqual(view.props.accessible, true); assert.strictEqual(text.props.children, 'hello world'); done(); }, 50); return source; } run(main, {react: testDriver}); }); it('Text w/ selector and text child', (done) => { function main(sources: {react: ReactSource}) { const vdom$ = xs.of(View([Text('foo', 'hello world')])); return {react: vdom$}; } function testDriver(sink: Stream>) { const source = new ReactSource(); const Root = makeCycleReactComponent(() => ({source, sink})); const r = renderer.create(React.createElement(Root as any)); const root = r.root; setTimeout(() => { const view = root.findByType(_View); const text = view.props.children; assert.strictEqual(text.props.children, 'hello world'); done(); }, 50); return source; } run(main, {react: testDriver}); }); }); ================================================ FILE: test/index.ts ================================================ import 'mocha'; import * as renderer from 'react-test-renderer'; import * as React from 'react'; import xs, {Stream} from 'xstream'; import * as ReactNative from 'react-native'; import {h, ReactSource, makeCycleReactComponent} from '@cycle/react'; import {run} from '@cycle/run'; const assert = require('assert'); const {View, Text} = ReactNative; class Touchable extends React.PureComponent { public press() { if (this.props.onPress) { this.props.onPress(null); } } public render() { return this.props.children; } } describe('React Native driver', function () { it('converts an MVI Cycle app into a React component', function (done) { function main(sources: {react: ReactSource}) { const inc = Symbol(); const inc$ = sources.react.select(inc).events('press'); const count$ = inc$.fold((acc: number, x: any) => acc + 1, 0); const vdom$ = count$.map((i: number) => h(Touchable, {sel: inc}, [h(View, [h(Text, {}, '' + i)])]) ); return {react: vdom$}; } function testDriver(sink: Stream>) { let turn = 0; const source = new ReactSource(); const Root = makeCycleReactComponent(() => ({source, sink})); const r = renderer.create(React.createElement(Root as any)); const root = r.root; const check = () => { const to = root.findByType(Touchable); const view = to.props.children; const text = view.props.children; assert.strictEqual(text.props.children, `${turn}`); to.instance.press(); turn++; if (turn === 3) { done(); } }; setTimeout(check, 50); setTimeout(check, 100); setTimeout(check, 150); return source; } run(main, {react: testDriver}); }); }); ================================================ FILE: test/tsconfig.json ================================================ { "compilerOptions": { "removeComments": false, "preserveConstEnums": true, "strictNullChecks": true, "suppressImplicitAnyIndexErrors": true, "module": "commonjs", "target": "ES5", "rootDir": "../", "outDir": "lib/", "lib": [ "dom", "es5", "scripthost", "es2015.collection", "es2015.promise", "es2015.iterable" ] }, "formatCodeOptions": { "indentSize": 2, "tabSize": 2 }, "files": ["index.ts", "helpers.ts"] } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "removeComments": false, "preserveConstEnums": true, "strictNullChecks": true, "declaration": true, "sourceMap": true, "suppressImplicitAnyIndexErrors": true, "module": "commonjs", "target": "ES5", "rootDir": "src/", "outDir": "lib/cjs/", "skipLibCheck": true, "noImplicitAny": true, "newLine": "LF", "moduleResolution": "node", "lib": ["dom", "es5", "scripthost", "es2015"] }, "include": ["src/**/*"] }