├── settings.gradle ├── .gitignore ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── test │ └── kotlin │ │ ├── utils │ │ └── Testing.kt │ │ ├── react_test_renderer │ │ └── ReactTestRenderer.kt │ │ └── io │ │ └── akryl │ │ ├── ComponentTest.kt │ │ └── HooksTest.kt └── main │ └── kotlin │ ├── io │ └── akryl │ │ ├── Context.kt │ │ ├── Component.kt │ │ └── Hooks.kt │ └── react │ └── React.kt ├── webpack.config.d └── babel.js ├── .github └── workflows │ └── gradle.yml ├── gradlew.bat ├── gradlew └── README.md /settings.gradle: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /.gradle 3 | /build 4 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.js.experimental.generateKotlinExternals=false 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akryl-kt/akryl-core/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/test/kotlin/utils/Testing.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import kotlin.test.assertEquals 4 | 5 | fun assertJsonEquals(expected: dynamic, actual: dynamic) { 6 | assertEquals(JSON.stringify(expected), JSON.stringify(actual)) 7 | } 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.2.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /webpack.config.d/babel.js: -------------------------------------------------------------------------------- 1 | config.module.rules.push({ 2 | test: /\.m?js$/, 3 | exclude: /(node_modules|bower_components|packages_imported)/, 4 | use: { 5 | loader: 'babel-loader', 6 | options: { 7 | presets: ['@babel/preset-env'], 8 | plugins: [ 9 | ['babel-plugin-akryl', {}] 10 | ], 11 | } 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Test & Upload 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Fetch all 15 | run: git fetch --prune --unshallow --tags 16 | - name: Show current version 17 | run: git describe --tags 18 | - name: Set up JDK 1.8 19 | uses: actions/setup-java@v1 20 | with: 21 | java-version: 1.8 22 | - name: Test with Gradle 23 | run: ./gradlew test 24 | - name: Deploy with Gradle 25 | env: 26 | BINTRAY_USER: ${{ secrets.BINTRAY_USER }} 27 | BINTRAY_KEY: ${{ secrets.BINTRAY_KEY }} 28 | run: ./gradlew bintrayUpload 29 | -------------------------------------------------------------------------------- /src/main/kotlin/io/akryl/Context.kt: -------------------------------------------------------------------------------- 1 | package io.akryl 2 | 3 | import react.Context 4 | import react.ProviderProps 5 | import react.React 6 | import react.ReactElement 7 | 8 | /** 9 | * Accepts a [value] prop to be passed to consuming components that are descendants of this [provider]. 10 | * One [provider] can be connected to many consumers. 11 | * Providers can be nested to override values deeper within the tree. 12 | * @see [useContext] 13 | */ 14 | fun Context.provider(value: T, children: List>): ReactElement> { 15 | return React.createElement( 16 | Provider, 17 | ProviderProps( 18 | value = value, 19 | children = undefined 20 | ), 21 | *children.toTypedArray() 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/test/kotlin/react_test_renderer/ReactTestRenderer.kt: -------------------------------------------------------------------------------- 1 | package react_test_renderer 2 | 3 | import react.ReactElement 4 | import kotlin.js.Json 5 | 6 | @JsModule("react-test-renderer") 7 | @JsNonModule 8 | external class ReactTestRenderer { 9 | companion object Factory { 10 | fun create(element: ReactElement<*>): ReactTestRenderer 11 | fun act(block: () -> dynamic) 12 | } 13 | 14 | fun update(element: ReactElement<*>) 15 | fun toJSON(): Json 16 | } 17 | 18 | @Suppress("UNCHECKED_CAST") 19 | fun ReactTestRenderer.Factory.akt(block: () -> T): T { 20 | var result: T? = null 21 | 22 | act { 23 | result = block() 24 | undefined 25 | } 26 | 27 | return result as T 28 | } 29 | 30 | fun ReactTestRenderer.Factory.aktCreate(block: () -> ReactElement<*>): ReactTestRenderer { 31 | return akt { 32 | create(block()) 33 | } 34 | } 35 | 36 | fun ReactTestRenderer.aktUpdate(block: () -> ReactElement<*>) { 37 | ReactTestRenderer.akt { 38 | update(block()) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/kotlin/react/React.kt: -------------------------------------------------------------------------------- 1 | package react 2 | 3 | @JsModule("react") 4 | @JsNonModule 5 | external object React { 6 | fun createElement(type: String, props: dynamic = definedExternally, vararg children: dynamic): ReactElement 7 | fun

createElement(type: Component

, props: P, vararg children: ReactElement<*>): ReactElement

8 | fun

cloneElement(element: ReactElement

, props: P, vararg children: ReactElement<*>): ReactElement

9 | fun memo(inner: dynamic): dynamic 10 | fun useState(initialState: dynamic): Array 11 | fun useEffect(effect: () -> EffectDisposer?, dependencies: Array? = definedExternally) 12 | fun useCallback(callback: () -> R, dependencies: Array? = definedExternally): () -> R 13 | fun useContext(context: Context): T 14 | fun useRef(initialValue: R): MutableRefObject 15 | fun useDebugValue(value: Any?) 16 | fun useMemo(fn: () -> R, dependencies: Array? = definedExternally): R 17 | fun isValidElement(obj: dynamic): Boolean 18 | fun createContext(defaultValue: T): Context 19 | } 20 | 21 | external interface Component

{ 22 | operator fun invoke(props: P): ReactElement

? 23 | } 24 | 25 | typealias ReactNode = Array?> 26 | 27 | class ProviderProps( 28 | val value: T, 29 | val children: ReactNode? 30 | ) 31 | 32 | typealias Provider = Component> 33 | 34 | class ConsumerProps( 35 | val children: (value: T) -> ReactElement<*> 36 | ) 37 | 38 | typealias Consumer = Component> 39 | 40 | @Suppress("PropertyName") 41 | external interface Context { 42 | val Provider: Provider 43 | val Consumer: Consumer 44 | val displayName: String? 45 | } 46 | 47 | external interface ReactElement

{ 48 | val type: dynamic 49 | val props: P 50 | val key: Any? 51 | } 52 | 53 | typealias FunctionalComponent

= (props: P) -> ReactElement

? 54 | 55 | typealias EffectDisposer = () -> Unit 56 | 57 | external interface MutableRefObject { 58 | var current: R 59 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/akryl/ComponentTest.kt: -------------------------------------------------------------------------------- 1 | package io.akryl 2 | 3 | import react.React 4 | import utils.assertJsonEquals 5 | import kotlin.js.json 6 | import kotlin.test.Test 7 | import kotlin.test.assertEquals 8 | import kotlin.test.assertNotNull 9 | import kotlin.test.assertTrue 10 | 11 | private fun emptyComponent() = component { 12 | React.createElement("div") 13 | } 14 | 15 | private fun emptyMemo() = memo { 16 | React.createElement("div") 17 | } 18 | 19 | private fun componentWithProps(a: Int, b: String) = component { 20 | React.createElement("div", json("a" to a, "b" to b)) 21 | } 22 | 23 | private fun memoWithProps(a: Int, b: String) = memo { 24 | React.createElement("div", json("a" to a, "b" to b)) 25 | } 26 | 27 | class ComponentTest { 28 | @Test 29 | fun testEmptyComponent() { 30 | val node = emptyComponent() 31 | assertTrue(React.isValidElement(node)) 32 | assertEquals("emptyComponent", node.type.name) 33 | } 34 | 35 | @Test 36 | fun testEmptyMemo() { 37 | val node = emptyMemo() 38 | assertTrue(React.isValidElement(node)) 39 | assertNotNull(node.type.`$$typeof`) 40 | assertEquals("emptyMemo", node.type.type.name) 41 | } 42 | 43 | @Test 44 | fun testComponentWithProps() { 45 | val node = componentWithProps(10, "str") 46 | assertTrue(React.isValidElement(node)) 47 | assertEquals("componentWithProps", node.type.name) 48 | assertJsonEquals(json("a" to 10, "b" to "str"), node.props) 49 | } 50 | 51 | @Test 52 | fun testMemoWithProps() { 53 | val node = memoWithProps(10, "str") 54 | assertTrue(React.isValidElement(node)) 55 | assertEquals("memoWithProps", node.type.type.name) 56 | assertJsonEquals(json("a" to 10, "b" to "str"), node.props) 57 | } 58 | 59 | @Test 60 | fun testKey() { 61 | val node = React.createElement("div", null, listOf("child")).withKey("foobar") 62 | assertTrue(React.isValidElement(node)) 63 | assertJsonEquals(json("children" to arrayOf("child")), node.props) 64 | assertEquals("foobar", node.key) 65 | } 66 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/kotlin/io/akryl/Component.kt: -------------------------------------------------------------------------------- 1 | package io.akryl 2 | 3 | import react.FunctionalComponent 4 | import react.React 5 | import react.ReactElement 6 | 7 | typealias ComponentWrapper

= (FunctionalComponent

) -> FunctionalComponent

8 | 9 | typealias RenderFunction = ComponentScope.() -> ReactElement<*>? 10 | 11 | interface ComponentScope 12 | 13 | @Suppress("FunctionName", "UNUSED_PARAMETER") 14 | fun __akryl_react_component_marker__( 15 | react: React, 16 | wrapper: ComponentWrapper<*>, 17 | render: RenderFunction 18 | ): ReactElement<*> = throw NotImplementedError("Implemented by babel-plugin-akryl") 19 | 20 | /** 21 | * Converts a simple Kotlin function [render] into a React component. 22 | * Parameters of an enclosing function will be converted to the component props. 23 | * 24 | * For example, this Kotlin code: 25 | * 26 | * ``` 27 | * fun greeting(name: String) = component { 28 | * Div(text = "Hello, $name!") 29 | * } 30 | * 31 | * val element = greeting(name = "World") 32 | * ``` 33 | * 34 | * will be converted to this JS code: 35 | * 36 | * ``` 37 | * function greeting$lambda({name}) { 38 | * return React.createElement("div", {}, `Hello, {name}!`); 39 | * } 40 | * 41 | * function greeting(name) { 42 | * return React.createElement(greeting$lambda, {name}); 43 | * } 44 | * 45 | * const element = greeting("World"); 46 | * ``` 47 | * 48 | * You can provide additional [wrapper] function what will be called with the component as a first argument. 49 | * The [wrapper] must be a lambda function without closure. It can use top-level static declarations only. 50 | */ 51 | @Suppress("NOTHING_TO_INLINE") 52 | inline fun component(noinline wrapper: ComponentWrapper<*> = { it }, noinline render: RenderFunction): ReactElement<*> { 53 | return __akryl_react_component_marker__(React, wrapper, render) 54 | } 55 | 56 | /** 57 | * Wraps the [component] function and adds memoization to a component. 58 | * An underlying [render] function will be called only when one of the props values are changed. 59 | * Useful when the component contains some expansive computations. 60 | * Works similar to [React.memo] but with additional syntax sugar. 61 | * 62 | * Example: 63 | * ``` 64 | * fun markdown(md: String) = memo { 65 | * val html = renderMarkdown(md) 66 | * Div(innerHtml = html) 67 | * } 68 | * ``` 69 | */ 70 | @Suppress("NOTHING_TO_INLINE", "UnsafeCastFromDynamic") 71 | inline fun memo(noinline render: RenderFunction): ReactElement<*> { 72 | return component({ React.memo(it) }, render) 73 | } 74 | 75 | /** 76 | * Adds a [key] to [this] react element. 77 | * The key is useful inside lists of elements. It adds identity to a list item 78 | * and prevents unnecessary re-rendering. 79 | * 80 | * Example: 81 | * ``` 82 | * Ul(children = items.map { item -> 83 | * Li(text = item.value).withKey(item.id) 84 | * }) 85 | * ``` 86 | */ 87 | @Suppress("UNUSED_PARAMETER") 88 | fun

ReactElement

.withKey(key: Any): ReactElement

{ 89 | val element = this 90 | return React.cloneElement(element, js("Object.assign({}, element.props, {key: key})") as P) 91 | } 92 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS='"-Xmx64m"' 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/kotlin/io/akryl/Hooks.kt: -------------------------------------------------------------------------------- 1 | package io.akryl 2 | 3 | import react.Context 4 | import react.EffectDisposer 5 | import react.MutableRefObject 6 | import react.React 7 | 8 | typealias SetStateAction = (newState: S) -> Unit 9 | 10 | /** 11 | * Returns a pair of a stateful value, and a function to update it. 12 | * 13 | * Example: 14 | * ``` 15 | * val (state, setState) = useState(0) 16 | * ``` 17 | * 18 | * During the initial render, the returned `state` is the same as the [initialState]. 19 | * The `setState` function is used to update the state. It accepts a new state value 20 | * and enqueues a re-render of the component. 21 | */ 22 | fun ComponentScope.useState(initialState: S): Pair> { 23 | hookMarker() 24 | val (state, setState) = React.useState(initialState) 25 | return Pair( 26 | state.unsafeCast(), 27 | setState.unsafeCast>() 28 | ) 29 | } 30 | 31 | /** 32 | * Overload of the [useState] that accepts an [initializer] lambda that returns an initial state. 33 | * It is useful when computation of the initial state takes a lot of time. 34 | * 35 | * Example: 36 | * ``` 37 | * val (state, setState) = useState { /* compute initial state */ } 38 | * ``` 39 | */ 40 | fun ComponentScope.useState(initializer: () -> S): Pair> { 41 | hookMarker() 42 | val (state, setState) = React.useState(initializer) 43 | return Pair( 44 | state.unsafeCast(), 45 | setState.unsafeCast>() 46 | ) 47 | } 48 | 49 | class DisposeScope { 50 | private val items = ArrayList() 51 | 52 | fun dispose(block: EffectDisposer) { 53 | items.add(block) 54 | } 55 | 56 | fun build(): EffectDisposer { 57 | val items = ArrayList(this.items) 58 | return { 59 | items.forEach { it() } 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * Accepts a function [effect] that contains imperative, possibly effectful code. 66 | * Use [useEffect] for mutations, subscriptions, timers, logging, and other side effects 67 | * that are not allowed inside the main body of a functional component. 68 | * The [effect] function will run after the render is committed to the screen. 69 | * 70 | * Example: 71 | * ``` 72 | * useEffect { 73 | * document.title = title 74 | * } 75 | * ``` 76 | * 77 | * By default, effects run after every completed render, but you can choose to fire them 78 | * only when certain values have changed. To implement this, pass a list of [dependencies] to the [useEffect]. 79 | * 80 | * Example: 81 | * ``` 82 | * useEffect(listOf(title)) { 83 | * document.title = title 84 | * } 85 | * ``` 86 | * 87 | * Often, effects create resources that need to be cleaned up before the component leaves the screen, 88 | * such as a subscription or timer ID. To do this, the function passed to [useEffect] can call [DisposeScope.dispose]. 89 | * 90 | * Example: 91 | * ``` 92 | * useEffect { 93 | * val timerId = window.setTimeout({ /* some code */ }, 1000) 94 | * dispose { window.clearTimeout(timerId) } 95 | * } 96 | * ``` 97 | * 98 | * The [DisposeScope.dispose] can be called multiple times. 99 | * All passed lambdas will be executed in the same order as [DisposeScope.dispose] were called. 100 | */ 101 | fun ComponentScope.useEffect(dependencies: List? = undefined, effect: DisposeScope.() -> Unit) { 102 | hookMarker() 103 | React.useEffect({ 104 | DisposeScope().apply(effect).build() 105 | }, dependencies?.toTypedArray()) 106 | } 107 | 108 | /** 109 | * Accepts a [context] object (the value returned from [React.createContext]) 110 | * and returns the current context value for that context. 111 | * When the nearest [Context.provider] above the component updates, 112 | * this hook will trigger a re-render. 113 | * 114 | * Example: 115 | * ``` 116 | * val MyContext = React.createContext(0) 117 | * 118 | * fun app() = component { 119 | * MyContext.provider(value = 10, children = listOf( 120 | * button() 121 | * )) 122 | * } 123 | * 124 | * fun button() = component { 125 | * val value = useContext(MyContext) 126 | * Div(text = "value = $value") 127 | * } 128 | * ``` 129 | */ 130 | fun ComponentScope.useContext(context: Context): T { 131 | hookMarker() 132 | return React.useContext(context) 133 | } 134 | 135 | /** 136 | * Returns a memoized [callback]. 137 | * Pass a [callback] and a list of [dependencies]. [useCallback] will return a memoized version of the [callback] 138 | * that only changes if one of the [dependencies] has changed. 139 | * 140 | * Example: 141 | * ``` 142 | * val cb = useCallback(listOf(value)) { 143 | * console.log(value) 144 | * } 145 | * ``` 146 | */ 147 | fun ComponentScope.useCallback(dependencies: List? = undefined, callback: () -> R): () -> R { 148 | hookMarker() 149 | return React.useCallback(callback, dependencies?.toTypedArray()) 150 | } 151 | 152 | /** 153 | * Returns a mutable ref object whose [MutableRefObject.current] property is initialized 154 | * to the passed [initialValue]. 155 | * The returned object will persist for the full lifetime of the component. 156 | * 157 | * Example: 158 | * ``` 159 | * fun example() = component { 160 | * val ref = useRef(null) 161 | * useEffect(emptyList()) { 162 | * ref.current?.focus() 163 | * } 164 | * Input(ref = ref) 165 | * } 166 | * ``` 167 | */ 168 | fun ComponentScope.useRef(initialValue: R): MutableRefObject { 169 | hookMarker() 170 | return React.useRef(initialValue) 171 | } 172 | 173 | /** 174 | * Can be used to display a label [value] for custom hooks in React DevTools. 175 | * 176 | * Example: 177 | * ``` 178 | * fun ComponentScope.useFriendStatus(status: Boolean) { 179 | * useDebugValue(if (status) "Online" else "Offline") 180 | * } 181 | * ``` 182 | */ 183 | fun ComponentScope.useDebugValue(value: Any?) { 184 | hookMarker() 185 | React.useDebugValue(value) 186 | } 187 | 188 | /** 189 | * Returns a memoized result of [fn] call. `useMemo` will only recompute the memoized value 190 | * when one of the [dependencies] has changed. 191 | * This optimization helps to avoid expensive calculations on every render. 192 | * 193 | * Example: 194 | * ``` 195 | * fun markdown(md: String) = component { 196 | * val html = useMemo(listOf(md)) { renderMarkdown(md) } 197 | * Div(innerHtml = html) 198 | * } 199 | * ``` 200 | */ 201 | fun ComponentScope.useMemo(dependencies: List? = undefined, fn: () -> R): R { 202 | hookMarker() 203 | return React.useMemo(fn, dependencies?.toTypedArray()) 204 | } 205 | 206 | @Suppress("NOTHING_TO_INLINE", "unused") 207 | private inline fun ComponentScope.hookMarker() { 208 | // this function is only to suppress unused `ComponentScope` receiver 209 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Akryl 2 | 3 | Kotlin wrapper around ReactJS. 4 | 5 | With akryl, you can write simple and idiomatic Kotlin code that will be converted into React components. 6 | For example, you can rewrite this JavaScript code: 7 | 8 | ```jsx 9 | const App = ({name}) => { 10 | return

Hello, {name}!
; 11 | } 12 | ``` 13 | 14 | in Kotlin using akryl: 15 | 16 | ```kotlin 17 | fun App(name: String) = component { 18 | div(text = "Hello, $name!") 19 | } 20 | ``` 21 | 22 | # Install 23 | 24 | There are two options to install Akryl: 25 | 26 | - Starter project - a convenient way for beginners. 27 | - Manual install - if you want full control over the project. 28 | 29 | ## Starter Project 30 | 31 | If you don't want to set up the project yourself, you can clone [akryl-frontend-starter](https://github.com/akryl-kt/akryl-frontend-starter) repository: 32 | 33 | ```bash 34 | git clone https://github.com/akryl-kt/akryl-frontend-starter 35 | cd akryl-frontend-starter 36 | ./run.sh 37 | # open http://localhost:8080 when build completes 38 | ``` 39 | 40 | ## Manual Install 41 | 42 | To add Akryl to an existing project, follow the steps below. This instruction assumes that you already have a [Kotlin/JS project configured with Gradle](https://kotlinlang.org/docs/reference/js-project-setup.html). 43 | 44 | Akryl library consists of several pieces: 45 | 46 | - [akryl-core](https://github.com/akryl-kt/akryl-core) - basic integration with React, hooks support. 47 | - [babel-plugin-arkyl](https://github.com/akryl-kt/babel-plugin-akryl) - babel plugin for components support. 48 | - [akryl-dom](https://github.com/akryl-kt/akryl-dom) - functions to work with HTML/CSS. 49 | - [akryl-redux](https://github.com/akryl-kt/akryl-redux) - wrapper around `react-redux` library. 50 | 51 | In most cases, you need to install all of them. 52 | 53 | 1. Add jcenter repository: 54 | 55 | ```gradle 56 | repositories { 57 | jcenter() 58 | ... 59 | } 60 | ``` 61 | 62 | 2. Add these dependencies into your `build.gradle` file: 63 | 64 | ```gradle 65 | kotlin { 66 | sourceSets { 67 | main { 68 | dependencies { 69 | ... 70 | // kotlin dependencies 71 | implementation "io.akryl:akryl-core:0.+" 72 | implementation "io.akryl:akryl-dom:0.+" 73 | implementation "io.akryl:akryl-redux:0.+" 74 | 75 | // babel-plugin-akryl dependencies 76 | implementation npm("babel-loader", "8.0.6") 77 | implementation npm("@babel/core", "7.7.7") 78 | implementation npm("@babel/preset-env", "7.7.7") 79 | implementation npm("babel-plugin-akryl", "0.1.1") 80 | // react dependencies 81 | implementation npm("react", "16.12.0") 82 | implementation npm("react-dom", "16.12.0") 83 | implementation npm("redux", "4.0.5") 84 | implementation npm("react-redux", "7.1.3") 85 | } 86 | } 87 | } 88 | } 89 | ``` 90 | 91 | 3. Add `babel.js` file into `webpack.config.d` directory to load `babel-plugin-akryl`: 92 | 93 | ```js 94 | config.module.rules.push({ 95 | test: /\.m?js$/, 96 | exclude: /(node_modules|bower_components|packages_imported)/, 97 | use: { 98 | loader: 'babel-loader', 99 | options: { 100 | presets: ['@babel/preset-env'], 101 | plugins: [ 102 | ['babel-plugin-akryl', {production: !config.devServer}], 103 | ], 104 | } 105 | } 106 | }); 107 | ``` 108 | 109 | 4. Add `src/main/resources/index.html` file: 110 | 111 | ```html 112 | 113 | 114 | 115 | 116 | 117 | Akryl Frontend Starter 118 | 119 | 120 | 121 |
122 | 123 | 124 | 125 | ``` 126 | 127 | 5. Add `src/main/kotlin/App.kt` file: 128 | 129 | ```kotlin 130 | import io.akryl.component 131 | import io.akryl.dom.html.Div 132 | import react_dom.ReactDom 133 | import kotlin.browser.document 134 | 135 | fun app() = component { 136 | div(text = "Hello, World!") 137 | } 138 | 139 | fun main() { 140 | ReactDom.render(app(), document.getElementById("app")) 141 | } 142 | ``` 143 | 144 | 6. Run the project: 145 | 146 | ```bash 147 | ./gradlew run --continuous 148 | ``` 149 | 150 | # Documentation 151 | 152 | `akryl-core` provides basic React features: 153 | 154 | - Components 155 | - Context 156 | - Hooks 157 | 158 | To use JSX-like syntax you need to install [akryl-dom](https://github.com/akryl-kt/akryl-dom) library. 159 | 160 | ## Components 161 | 162 | With Akryl you can use only hooks API and functional components. To create a component you must define a function, that takes props as arguments and returns a virtual DOM element. It is important to wrap the function body into a `component` function call. It tells the `babel-plugin-akryl` to convert a simple Kotlin function into a React component. 163 | 164 | Example: 165 | 166 | ```kotlin 167 | fun Calc(a: Int, b: Int) = component { 168 | div(text = "Sum = ${a + b}") 169 | } 170 | ``` 171 | 172 | The `Calc` is a React component, that accepts `a` and `b` in props and returns div element. Here is the equivalent component in JSX: 173 | 174 | ```jsx 175 | export const Calc = ({a, b}) => { 176 | return
Sum = {a + b}
; 177 | }; 178 | ``` 179 | 180 | and in TSX: 181 | 182 | ```typescript 183 | export interface CalcProps { 184 | a: number; 185 | b: number; 186 | } 187 | 188 | export const Calc = ({a, b} : CalcProps) => { 189 | return
Sum = {a + b}
; 190 | }; 191 | ``` 192 | 193 | In Akryl you don't need to wrap props into an interface or in a class - just pass them as function arguments. 194 | 195 | You can call a component function from any place in code. There is no restriction to use a component only inside another component. 196 | 197 | Example: 198 | 199 | ```kotlin 200 | // Akryl 201 | val element = Calc(a = 10, b = 20) 202 | ``` 203 | 204 | ```jsx 205 | // JSX 206 | const element = ; 207 | ``` 208 | 209 | If your component is pure, you can use `memo` instead of `component` function. It has the same effect as in React: it will prevent a component from re-render if its props are not changed. 210 | 211 | ## Hooks 212 | 213 | Akryl has common hooks from React: 214 | 215 | - [useState](src/main/kotlin/io/akryl/Hooks.kt#L22) 216 | - [useEffect](src/main/kotlin/io/akryl/Hooks.kt#L101) 217 | - [useContext](src/main/kotlin/io/akryl/Hooks.kt#L130) 218 | - [useCallback](src/main/kotlin/io/akryl/Hooks.kt#L147) 219 | - [useRef](src/main/kotlin/io/akryl/Hooks.kt#L168) 220 | - [useDebugValue](src/main/kotlin/io/akryl/Hooks.kt#L183) 221 | - [useMemo](src/main/kotlin/io/akryl/Hooks.kt#L201) 222 | 223 | All hooks have receiver argument of type `ComponentScope`, that prevents usage outside of a component at compile time. 224 | 225 | ```kotlin 226 | // will compile 227 | fun counter() = component { 228 | val (state, setState) = useState(0) 229 | } 230 | 231 | // will not compile: ComponentScope receiver is not provided 232 | val (state, setState) = useState(0) 233 | ``` 234 | 235 | To create a custom hook, write a function that has the `ComponentScope` receiver parameter: 236 | 237 | ```kotlin 238 | fun ComponentScope.useRenderCount(): Int { 239 | val ref = useRef(0) 240 | ref.current += 1 241 | return ref.current 242 | } 243 | ``` 244 | 245 | ## JavaScript Interop 246 | 247 | Akryl components can accept JS components as children and vice versa. There can be a component tree that contains an arbitrary mix of Akryl and JS components. 248 | 249 | To use an existing React library, declare the library using `@JsModule` and write helper functions to use components in a convenient way. For example, let's create a wrapper for [blueprintjs](https://github.com/palantir/blueprint) button: 250 | 251 | ```kotlin 252 | // library declaration 253 | 254 | @JsModule("@blueprintjs/core") 255 | @JsNonModule 256 | external object Blueprint { 257 | val Button: Component 258 | } 259 | 260 | // helper function for button 261 | 262 | fun bpButton( 263 | onClick: (() -> Unit)? = undefined, 264 | children: List> = emptyList() 265 | ) = React.createElement( 266 | type = Blueprint.Button, 267 | props = json( 268 | "onClick" to onClick 269 | ), 270 | children = *children.toTypedArray() 271 | ) 272 | 273 | // usage in Akryl 274 | 275 | fun app() = component { 276 | bpButton( 277 | onClick = { window.alert("Hello, World!") }, 278 | children = listOf(text("Click me!")) 279 | ) 280 | } 281 | ``` 282 | 283 | # Comparison with [kotlin-react](https://github.com/JetBrains/kotlin-wrappers/tree/master/kotlin-react) 284 | 285 | There are two options to create a component in kotlin-react. 286 | 287 | 1. Class API 288 | 289 | ```kotlin 290 | interface GreetingProps : RProps { 291 | var name: String 292 | } 293 | 294 | class Greeting : RComponent() { 295 | override fun RBuilder.render() { 296 | div { 297 | +"Hello, ${props.name}!" 298 | } 299 | } 300 | } 301 | 302 | fun RBuilder.greeting(name: String = "John") = child(Greeting::class) { 303 | attrs.name = name 304 | } 305 | ``` 306 | 307 | 2. Functional API 308 | 309 | ```kotlin 310 | interface GreetingProps : RProps { 311 |    var name: String 312 | } 313 | 314 | val greeting = functionalComponent { props -> 315 | div { 316 | +"Hello, ${props.name}!" 317 | } 318 | } 319 | 320 | fun RBuilder.greeting(name: String = "John") = child(greeting) { 321 | attrs.name = name 322 | } 323 | ``` 324 | 325 | Akryl has only a functional API. 326 | 327 | ```kotlin 328 | fun greeting(name: String = "John") = component { 329 | div(text = "Hello, $name!") 330 | } 331 | ``` 332 | 333 | Differences between kotlin-react and akryl: 334 | 335 | - In both options of kotlin-react, you need to create a props class, a component body, and a helper function. 336 | In akryl, you only need to create a single function. 337 | - The functional component of kotlin-react will be anonymous. 338 | It will not have a name in React DevTools, so it will be harder to debug. 339 | - `attrs` are not enforcing required props. 340 | -------------------------------------------------------------------------------- /src/test/kotlin/io/akryl/HooksTest.kt: -------------------------------------------------------------------------------- 1 | package io.akryl 2 | 3 | import react.React 4 | import react_test_renderer.ReactTestRenderer 5 | import react_test_renderer.akt 6 | import react_test_renderer.aktCreate 7 | import react_test_renderer.aktUpdate 8 | import utils.assertJsonEquals 9 | import kotlin.js.json 10 | import kotlin.test.Test 11 | import kotlin.test.assertEquals 12 | 13 | private class EventEmitter { 14 | private lateinit var event: (T) -> Unit 15 | 16 | operator fun invoke(event: (T) -> Unit) { 17 | this.event = event 18 | } 19 | 20 | fun emit(value: T) = event(value) 21 | } 22 | 23 | private class Value(var value: T) 24 | 25 | private fun counterComponent(emitter: EventEmitter) = component { 26 | val (state, setState) = useState(0) 27 | emitter { setState(state + 1) } 28 | React.createElement("div", null, state.toString()) 29 | } 30 | 31 | private fun counterInitializerComponent(emitter: EventEmitter) = component { 32 | val (state, setState) = useState { 0 } 33 | emitter { setState(state + 1) } 34 | React.createElement("div", null, state.toString()) 35 | } 36 | 37 | private fun simpleSideEffectComponent(sideEffect: Value, value: String) = component { 38 | useEffect { 39 | sideEffect.value = value 40 | } 41 | null 42 | } 43 | 44 | private fun dependenciesSideEffectComponent(sideEffect: Value, value: String) = component { 45 | useEffect(listOf(value)) { 46 | sideEffect.value += 1 47 | } 48 | null 49 | } 50 | 51 | private fun disposeSideEffectComponent(sideEffect: Value, disposeResult: Value, value: String) = component { 52 | useEffect(listOf(value)) { 53 | sideEffect.value += 1 54 | dispose { disposeResult.value += 1 } 55 | } 56 | null 57 | } 58 | 59 | data class TestContext( 60 | val value: String 61 | ) 62 | 63 | private val testContext = React.createContext(null) 64 | 65 | private fun contextComponent() = component { 66 | val data = useContext(testContext) 67 | React.createElement("div", null, data?.value.toString()) 68 | } 69 | 70 | private fun dependenciesCallbackComponent(dependency: String, callback: () -> Int) = component { 71 | val cb = useCallback(listOf(dependency), callback) 72 | React.createElement("div", null, cb().toString()) 73 | } 74 | 75 | private fun simpleCallbackComponent(callback: () -> Int) = component { 76 | val cb = useCallback { callback() } 77 | React.createElement("div", null, cb().toString()) 78 | } 79 | 80 | private fun permanentCallbackComponent(callback: () -> Int) = component { 81 | val cb = useCallback(emptyList()) { callback() } 82 | React.createElement("div", null, cb().toString()) 83 | } 84 | 85 | private fun refComponent(eventEmitter: EventEmitter) = component { 86 | val ref = useRef(0) 87 | eventEmitter { ref.current = it } 88 | React.createElement("div", null, ref.current.toString()) 89 | } 90 | 91 | private fun debugValueComponent() = component { 92 | useDebugValue("test") 93 | null 94 | } 95 | 96 | private fun memoComponent(renderCount: Value, memoCount: Value, dependency: String) = component { 97 | renderCount.value += 1 98 | val result = useMemo(listOf(dependency)) { 99 | memoCount.value += 1 100 | dependency 101 | } 102 | React.createElement("div", null, result) 103 | } 104 | 105 | private fun ComponentScope.useCounter(emitter: EventEmitter): Int { 106 | val (count, setCount) = useState(0) 107 | val cb = useCallback(listOf(count)) { 108 | setCount(count + 1) 109 | } 110 | emitter { cb() } 111 | return count 112 | } 113 | 114 | private fun customHookComponent(emitter: EventEmitter) = component { 115 | val count = useCounter(emitter) 116 | React.createElement("div", null, count.toString()) 117 | } 118 | 119 | class HooksTest { 120 | @Test 121 | fun testState() { 122 | val emitter = EventEmitter() 123 | 124 | val root = ReactTestRenderer.aktCreate { 125 | counterComponent(emitter) 126 | } 127 | 128 | for (i in 0..2) { 129 | assertContent(i.toString(), root) 130 | 131 | ReactTestRenderer.akt { 132 | emitter.emit(Unit) 133 | } 134 | } 135 | } 136 | 137 | @Test 138 | fun testStateWithInitializer() { 139 | val emitter = EventEmitter() 140 | 141 | val root = ReactTestRenderer.aktCreate { 142 | counterInitializerComponent(emitter) 143 | } 144 | 145 | for (i in 0..2) { 146 | assertContent(i.toString(), root) 147 | 148 | ReactTestRenderer.akt { 149 | emitter.emit(Unit) 150 | } 151 | } 152 | } 153 | 154 | @Test 155 | fun testSimpleEffect() { 156 | val sideEffect = Value("") 157 | 158 | val root = ReactTestRenderer.aktCreate { 159 | simpleSideEffectComponent(sideEffect, "foo") 160 | } 161 | assertEquals("foo", sideEffect.value) 162 | 163 | root.aktUpdate { 164 | simpleSideEffectComponent(sideEffect, "bar") 165 | } 166 | assertEquals("bar", sideEffect.value) 167 | } 168 | 169 | @Test 170 | fun testDependenciesEffect() { 171 | val sideEffect = Value(0) 172 | 173 | val root = ReactTestRenderer.aktCreate { 174 | dependenciesSideEffectComponent(sideEffect, "foo") 175 | } 176 | assertEquals(1, sideEffect.value) 177 | 178 | root.aktUpdate { 179 | dependenciesSideEffectComponent(sideEffect, "foo") 180 | } 181 | assertEquals(1, sideEffect.value) 182 | 183 | root.aktUpdate { 184 | dependenciesSideEffectComponent(sideEffect, "bar") 185 | } 186 | assertEquals(2, sideEffect.value) 187 | } 188 | 189 | @Test 190 | fun testDisposeEffect() { 191 | val sideEffect = Value(0) 192 | val disposeResult = Value(0) 193 | 194 | val root = ReactTestRenderer.aktCreate { 195 | disposeSideEffectComponent(sideEffect, disposeResult, "foo") 196 | } 197 | assertEquals(1, sideEffect.value) 198 | assertEquals(0, disposeResult.value) 199 | 200 | root.aktUpdate { 201 | disposeSideEffectComponent(sideEffect, disposeResult, "bar") 202 | } 203 | assertEquals(2, sideEffect.value) 204 | assertEquals(1, disposeResult.value) 205 | 206 | root.aktUpdate { 207 | React.createElement("div") 208 | } 209 | assertEquals(2, sideEffect.value) 210 | assertEquals(2, disposeResult.value) 211 | } 212 | 213 | @Test 214 | fun testContextAbsent() { 215 | val root = ReactTestRenderer.aktCreate { 216 | contextComponent() 217 | } 218 | assertContent("null", root) 219 | } 220 | 221 | @Test 222 | fun testContextPresented() { 223 | val root = ReactTestRenderer.aktCreate { 224 | testContext.provider( 225 | value = TestContext(value = "foobar"), 226 | children = listOf( 227 | contextComponent() 228 | ) 229 | ) 230 | } 231 | assertContent("foobar", root) 232 | } 233 | 234 | @Test 235 | fun testSimpleCallback() { 236 | val root = ReactTestRenderer.aktCreate { 237 | simpleCallbackComponent { 10 } 238 | } 239 | assertContent("10", root) 240 | 241 | root.aktUpdate { 242 | simpleCallbackComponent { 20 } 243 | } 244 | assertContent("20", root) 245 | } 246 | 247 | @Test 248 | fun testPermanentCallback() { 249 | val root = ReactTestRenderer.aktCreate { 250 | permanentCallbackComponent { 10 } 251 | } 252 | assertContent("10", root) 253 | 254 | root.aktUpdate { 255 | permanentCallbackComponent { 20 } 256 | } 257 | assertContent("10", root) 258 | } 259 | 260 | @Test 261 | fun testDependenciesCallback() { 262 | val root = ReactTestRenderer.aktCreate { 263 | dependenciesCallbackComponent("first") { 10 } 264 | } 265 | assertContent("10", root) 266 | 267 | root.aktUpdate { 268 | dependenciesCallbackComponent("first") { 20 } 269 | } 270 | assertContent("10", root) 271 | 272 | root.aktUpdate { 273 | dependenciesCallbackComponent("second") { 20 } 274 | } 275 | assertContent("20", root) 276 | } 277 | 278 | @Test 279 | fun testRef() { 280 | val emitter = EventEmitter() 281 | 282 | val root = ReactTestRenderer.aktCreate { 283 | refComponent(emitter) 284 | } 285 | assertContent("0", root) 286 | 287 | ReactTestRenderer.akt { 288 | emitter.emit(1) 289 | } 290 | assertContent("0", root) 291 | 292 | root.aktUpdate { 293 | refComponent(emitter) 294 | } 295 | assertContent("1", root) 296 | } 297 | 298 | @Test 299 | fun testDebugValueRuns() { 300 | ReactTestRenderer.aktCreate { 301 | debugValueComponent() 302 | } 303 | } 304 | 305 | @Test 306 | fun testMemo() { 307 | val renderCount = Value(0) 308 | val memoCount = Value(0) 309 | 310 | val root = ReactTestRenderer.aktCreate { 311 | memoComponent(renderCount, memoCount, "a") 312 | } 313 | assertEquals(1, renderCount.value) 314 | assertEquals(1, memoCount.value) 315 | assertContent("a", root) 316 | 317 | root.aktUpdate { 318 | memoComponent(renderCount, memoCount, "a") 319 | } 320 | assertEquals(2, renderCount.value) 321 | assertEquals(1, memoCount.value) 322 | assertContent("a", root) 323 | 324 | root.aktUpdate { 325 | memoComponent(renderCount, memoCount, "b") 326 | } 327 | assertEquals(3, renderCount.value) 328 | assertEquals(2, memoCount.value) 329 | assertContent("b", root) 330 | } 331 | 332 | @Test 333 | fun testCustomHook() { 334 | val emitter = EventEmitter() 335 | 336 | val root = ReactTestRenderer.aktCreate { 337 | customHookComponent(emitter) 338 | } 339 | assertContent("0", root) 340 | 341 | ReactTestRenderer.akt { 342 | emitter.emit(Unit) 343 | } 344 | assertContent("1", root) 345 | } 346 | } 347 | 348 | private fun assertContent(expected: String, actual: ReactTestRenderer) { 349 | assertJsonEquals( 350 | json( 351 | "type" to "div", 352 | "props" to json(), 353 | "children" to arrayOf(expected) 354 | ), 355 | actual.toJSON() 356 | ) 357 | } 358 | --------------------------------------------------------------------------------