diff --git a/README.md b/README.md index dad87f1..efe8859 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Kotlin React Redux PoC +# Kotlin/JS IR BE and React + Redux ToDo List Sample This project is a PoC of using Kotlin (JS) and React, React-Dom, React-Router, Redux and React-Redux. This project is an implementation/translation of the react-redux [Todo List example project](https://redux.js.org/basics/example) in Kotlin (with the addition of react-router). The project showcases the following features: diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 80d59da..0000000 --- a/build.gradle +++ /dev/null @@ -1,66 +0,0 @@ -plugins { - id "kotlin2js" version "1.3.21" - id "org.jetbrains.kotlin.frontend" version "0.0.45" - id "kotlin-dce-js" version "1.3.21" -} -group 'kotlin-poc-frontend-react-redux' -version '1.0-SNAPSHOT' - -ext { - web_dir = "web" -} - -repositories { - mavenCentral() - jcenter() - maven { url "https://kotlin.bintray.com/kotlin-js-wrappers" } - maven { url "https://kotlin.bintray.com/kotlinx" } -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-js" - implementation 'org.jetbrains:kotlin-react:16.6.0-pre.70-kotlin-1.3.21' - implementation 'org.jetbrains:kotlin-react-dom:16.6.0-pre.70-kotlin-1.3.21' - implementation 'org.jetbrains:kotlin-react-redux:5.0.7-pre.70-kotlin-1.3.21' - implementation 'org.jetbrains:kotlin-redux:4.0.0-pre.70-kotlin-1.3.21' - implementation 'org.jetbrains:kotlin-styled:1.0.0-pre.70-kotlin-1.3.21' - implementation 'org.jetbrains:kotlin-react-router-dom:4.3.1-pre.70-kotlin-1.3.21' -} - -clean.doFirst() { - delete("${web_dir}") -} - - -bundle.doLast() { - copy { - from "${buildDir}/resources/main/web" - from "${buildDir}/bundle" - into "${web_dir}" - } -} - -kotlinFrontend { - npm { - dependency "react" - dependency "react-dom" - dependency "react-router-dom" - dependency "react-redux" - dependency "redux" - dependency "core-js" - dependency "inline-style-prefixer" - dependency "styled-components" - } - - webpackBundle { - bundleName = "this-will-be-overwritten" // NOTE: for example purposes this is overwritten in `webpack.config.d/filename.js`. - contentPath = file('src/main/resources/web') - if (project.hasProperty('prod')) { - mode = "production" - } - } -} - -compileKotlin2Js { - kotlinOptions.moduleKind = 'commonjs' -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..0a6ad7d --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,39 @@ +import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig.* + +plugins { + kotlin("js") version "1.6.10" +} + +group = "react-redux-todo-list-sample" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + jcenter() + maven { url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-js-wrappers") } +} + +dependencies { + implementation("org.jetbrains.kotlin-wrappers:kotlin-react:17.0.2-pre.290-kotlin-1.6.10") + implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom:17.0.2-pre.290-kotlin-1.6.10") + implementation("org.jetbrains.kotlin-wrappers:kotlin-redux:4.1.2-pre.290-kotlin-1.6.10") + implementation("org.jetbrains.kotlin-wrappers:kotlin-react-redux:7.2.6-pre.290-kotlin-1.6.10") + implementation("org.jetbrains.kotlin-wrappers:kotlin-styled:5.3.3-pre.290-kotlin-1.6.10") + implementation("org.jetbrains.kotlin-wrappers:kotlin-react-router-dom:6.2.1-pre.290-kotlin-1.6.10") + implementation("org.jetbrains.kotlin-wrappers:kotlin-ring-ui:4.1.5-pre.290-kotlin-1.6.10") + + // for kotlin-ring-ui + implementation(npm("core-js", "^3.16.0")) +} + +kotlin { + js(IR) { + binaries.executable() + browser { + commonWebpackConfig { + mode = if(project.hasProperty("prod")) Mode.PRODUCTION else Mode.DEVELOPMENT + } + } + useCommonJs() + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 290541c..da9702f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.8-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index a8c634e..0000000 --- a/settings.gradle +++ /dev/null @@ -1,19 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - maven { url 'https://dl.bintray.com/kotlin/kotlin-eap' } - } - resolutionStrategy { - eachPlugin { - if (requested.id.id == "kotlin2js" || requested.id.id == "kotlin-dce-js") { - useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}") - } - if (requested.id.id == "org.jetbrains.kotlin.frontend") { - useModule("org.jetbrains.kotlin:kotlin-frontend-plugin:${requested.version}") - } - } - } -} - -rootProject.name = 'kotlin-poc-frontend-react-redux' - diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..063a9bc --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,2 @@ +rootProject.name = "react-redux-todo-list-sample" + diff --git a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/Index.kt b/src/main/kotlin/nl/lawik/poc/frontend/reactredux/Index.kt deleted file mode 100644 index 3032396..0000000 --- a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/Index.kt +++ /dev/null @@ -1,28 +0,0 @@ -package nl.lawik.poc.frontend.reactredux - -import nl.lawik.poc.frontend.reactredux.components.app -import nl.lawik.poc.frontend.reactredux.reducers.State -import nl.lawik.poc.frontend.reactredux.reducers.combinedReducers -import react.dom.render -import react.redux.provider -import redux.RAction -import redux.compose -import redux.createStore -import redux.rEnhancer -import kotlin.browser.document - -val store = createStore( - combinedReducers(), State(), compose( - rEnhancer(), - js("if(window.__REDUX_DEVTOOLS_EXTENSION__ )window.__REDUX_DEVTOOLS_EXTENSION__ ();else(function(f){return f;});") - ) -) - -fun main() { - val rootDiv = document.getElementById("root") - render(rootDiv) { - provider(store) { - app() - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/components/App.kt b/src/main/kotlin/nl/lawik/poc/frontend/reactredux/components/App.kt deleted file mode 100644 index 3579740..0000000 --- a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/components/App.kt +++ /dev/null @@ -1,43 +0,0 @@ -package nl.lawik.poc.frontend.reactredux.components - -import nl.lawik.poc.frontend.reactredux.containers.addTodo -import nl.lawik.poc.frontend.reactredux.containers.visibleTodoList -import react.RBuilder -import react.dom.br -import react.dom.div -import react.dom.h1 -import react.router.dom.browserRouter -import react.router.dom.navLink -import react.router.dom.route -import react.router.dom.switch - -private const val TODO_LIST_PATH = "/todolist" - -fun RBuilder.app() = - browserRouter { - switch { - route("/", exact = true) { - div { - h1 { - +"Kotlin React + React-Dom + Redux + React-Redux + React-Router Example" - } - navLink(TODO_LIST_PATH) { - +"Go to todo list" - } - } - } - route(TODO_LIST_PATH) { - div { - addTodo {} - visibleTodoList {} - footer() - br {} - navLink("/") { - +"Go back" - } - } - } - } - } - - diff --git a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/components/Link.kt b/src/main/kotlin/nl/lawik/poc/frontend/reactredux/components/Link.kt deleted file mode 100644 index 81edfac..0000000 --- a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/components/Link.kt +++ /dev/null @@ -1,28 +0,0 @@ -package nl.lawik.poc.frontend.reactredux.components - -import kotlinx.css.px -import kotlinx.html.js.onClickFunction -import react.RBuilder -import react.RComponent -import react.RProps -import react.RState -import styled.css -import styled.styledButton - -interface LinkProps : RProps { - var active: Boolean - var onClick: () -> Unit -} - -class Link(props: LinkProps) : RComponent(props) { - override fun RBuilder.render() { - styledButton { - attrs.onClickFunction = { props.onClick() } - attrs.disabled = props.active - css { - marginLeft = 4.px - } - children() - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/components/Todo.kt b/src/main/kotlin/nl/lawik/poc/frontend/reactredux/components/Todo.kt deleted file mode 100644 index 0287fcf..0000000 --- a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/components/Todo.kt +++ /dev/null @@ -1,18 +0,0 @@ -package nl.lawik.poc.frontend.reactredux.components - -import kotlinx.css.properties.TextDecorationLine -import kotlinx.css.properties.textDecoration -import kotlinx.html.js.onClickFunction -import nl.lawik.poc.frontend.reactredux.entities.Todo -import react.RBuilder -import styled.css -import styled.styledLi - -fun RBuilder.todo(todo: Todo, onClick: () -> Unit) = - styledLi { - attrs.onClickFunction = { onClick() } - css { - if (todo.completed) textDecoration(TextDecorationLine.lineThrough) - } - +todo.text - } \ No newline at end of file diff --git a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/components/TodoList.kt b/src/main/kotlin/nl/lawik/poc/frontend/reactredux/components/TodoList.kt deleted file mode 100644 index 96f8ad1..0000000 --- a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/components/TodoList.kt +++ /dev/null @@ -1,21 +0,0 @@ -package nl.lawik.poc.frontend.reactredux.components - -import nl.lawik.poc.frontend.reactredux.entities.Todo -import react.RBuilder -import react.RComponent -import react.RProps -import react.RState -import react.dom.ul - -interface TodoListProps : RProps { - var todos: Array - var toggleTodo: (Int) -> Unit -} - -class TodoList(props: TodoListProps) : RComponent(props) { - override fun RBuilder.render() { - ul { - props.todos.forEach { todo(it) { props.toggleTodo(it.id) } } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/containers/AddTodo.kt b/src/main/kotlin/nl/lawik/poc/frontend/reactredux/containers/AddTodo.kt deleted file mode 100644 index d7f3023..0000000 --- a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/containers/AddTodo.kt +++ /dev/null @@ -1,45 +0,0 @@ -package nl.lawik.poc.frontend.reactredux.containers - -import kotlinx.html.ButtonType -import kotlinx.html.InputType -import kotlinx.html.js.onSubmitFunction -import nl.lawik.poc.frontend.reactredux.actions.AddTodo -import nl.lawik.poc.frontend.reactredux.store -import org.w3c.dom.HTMLInputElement -import react.* -import react.dom.button -import react.dom.div -import react.dom.form -import react.dom.input -import react.redux.rConnect -import redux.WrapperAction - - -class AddTodo(props: RProps) : RComponent(props) { - private val inputRef = createRef() - override fun RBuilder.render() { - div { - form { - attrs.onSubmitFunction = { event -> - event.preventDefault() - inputRef.current!!.let { - if (it.value.trim().isNotEmpty()) { - store.dispatch(AddTodo(it.value)) - it.value = "" - } - } - } - input(type = InputType.text) { - ref = inputRef - } - button(type = ButtonType.submit) { - +"Add Todo" - } - } - } - } -} - - -val addTodo: RClass = - rConnect()(nl.lawik.poc.frontend.reactredux.containers.AddTodo::class.js.unsafeCast>()) \ No newline at end of file diff --git a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/containers/FilterLink.kt b/src/main/kotlin/nl/lawik/poc/frontend/reactredux/containers/FilterLink.kt deleted file mode 100644 index cab3167..0000000 --- a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/containers/FilterLink.kt +++ /dev/null @@ -1,34 +0,0 @@ -package nl.lawik.poc.frontend.reactredux.containers - -import nl.lawik.poc.frontend.reactredux.actions.SetVisibilityFilter -import nl.lawik.poc.frontend.reactredux.components.Link -import nl.lawik.poc.frontend.reactredux.components.LinkProps -import nl.lawik.poc.frontend.reactredux.enums.VisibilityFilter -import nl.lawik.poc.frontend.reactredux.reducers.State -import react.RClass -import react.RProps -import react.invoke -import react.redux.rConnect -import redux.WrapperAction - -interface FilterLinkProps : RProps { - var filter: VisibilityFilter -} - -private interface LinkStateProps : RProps { - var active: Boolean -} - -private interface LinkDispatchProps : RProps { - var onClick: () -> Unit -} - -val filterLink: RClass = - rConnect( - { state, ownProps -> - active = state.visibilityFilter == ownProps.filter - }, - { dispatch, ownProps -> - onClick = { dispatch(SetVisibilityFilter(ownProps.filter)) } - } - )(Link::class.js.unsafeCast>()) diff --git a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/containers/VisibleTodoList.kt b/src/main/kotlin/nl/lawik/poc/frontend/reactredux/containers/VisibleTodoList.kt deleted file mode 100644 index 7dd714e..0000000 --- a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/containers/VisibleTodoList.kt +++ /dev/null @@ -1,37 +0,0 @@ -package nl.lawik.poc.frontend.reactredux.containers - -import nl.lawik.poc.frontend.reactredux.actions.ToggleTodo -import nl.lawik.poc.frontend.reactredux.components.TodoList -import nl.lawik.poc.frontend.reactredux.components.TodoListProps -import nl.lawik.poc.frontend.reactredux.entities.Todo -import nl.lawik.poc.frontend.reactredux.enums.VisibilityFilter -import nl.lawik.poc.frontend.reactredux.reducers.State -import react.RClass -import react.RProps -import react.invoke -import react.redux.rConnect -import redux.WrapperAction - -private fun getVisibleTodos(todos: Array, filter: VisibilityFilter): Array = when (filter) { - VisibilityFilter.SHOW_ALL -> todos - VisibilityFilter.SHOW_ACTIVE -> todos.filter { !it.completed }.toTypedArray() - VisibilityFilter.SHOW_COMPLETED -> todos.filter { it.completed }.toTypedArray() -} - -private interface TodoListStateProps : RProps { - var todos: Array -} - -private interface TodoListDispatchProps : RProps { - var toggleTodo: (Int) -> Unit -} - -val visibleTodoList: RClass = - rConnect( - { state, _ -> - todos = getVisibleTodos(state.todos, state.visibilityFilter) - }, - { dispatch, _ -> - toggleTodo = { dispatch(ToggleTodo(it)) } - } - )(TodoList::class.js.unsafeCast>()) \ No newline at end of file diff --git a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/reducers/Index.kt b/src/main/kotlin/nl/lawik/poc/frontend/reactredux/reducers/Index.kt deleted file mode 100644 index ace899d..0000000 --- a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/reducers/Index.kt +++ /dev/null @@ -1,18 +0,0 @@ -package nl.lawik.poc.frontend.reactredux.reducers - -import nl.lawik.poc.frontend.reactredux.entities.Todo -import nl.lawik.poc.frontend.reactredux.enums.VisibilityFilter -import nl.lawik.poc.frontend.reactredux.util.combineReducers - - -data class State( - val todos: Array = emptyArray(), - val visibilityFilter: VisibilityFilter = VisibilityFilter.SHOW_ALL -) - -fun combinedReducers() = combineReducers( - mapOf( - State::todos to ::todos, - State::visibilityFilter to ::visibilityFilter - ) -) diff --git a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/reducers/Todos.kt b/src/main/kotlin/nl/lawik/poc/frontend/reactredux/reducers/Todos.kt deleted file mode 100644 index 1dd604c..0000000 --- a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/reducers/Todos.kt +++ /dev/null @@ -1,18 +0,0 @@ -package nl.lawik.poc.frontend.reactredux.reducers - -import nl.lawik.poc.frontend.reactredux.actions.AddTodo -import nl.lawik.poc.frontend.reactredux.actions.ToggleTodo -import nl.lawik.poc.frontend.reactredux.entities.Todo -import redux.RAction - -fun todos(state: Array = emptyArray(), action: RAction): Array = when (action) { - is AddTodo -> state + Todo(action.id, action.text, false) - is ToggleTodo -> state.map { - if (it.id == action.id) { - it.copy(completed = !it.completed) - } else { - it - } - }.toTypedArray() - else -> state -} \ No newline at end of file diff --git a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/util/Util.kt b/src/main/kotlin/nl/lawik/poc/frontend/reactredux/util/Util.kt deleted file mode 100644 index 8c02845..0000000 --- a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/util/Util.kt +++ /dev/null @@ -1,28 +0,0 @@ -package nl.lawik.poc.frontend.reactredux.util - -import redux.Reducer -import redux.combineReducers -import kotlin.reflect.KProperty1 - - -/** - * Helper function that combines reducers using [combineReducers] where the keys in the map are - * properties of the state object instead of strings with the name of the state's properties - * this helper function has 2 advantages over the original: - * - * 1. It is less error-prone, when you change the name of the property of the state you must change the - * corresponding key or you will get a compile error. - * 2. The compiler is now able to infer the [S] type parameter which means it is no longer needed to provide the 2 type parameters explicitly. - * - * @param S state - * @param A action - * @param R state property type - * - * @param reducers map where the key is the state property and the value is the reducer for said property. - * - * @return the combined reducer. - * - */ -fun combineReducers(reducers: Map, Reducer<*, A>>): Reducer { - return combineReducers(reducers.mapKeys { it.key.name }) -} \ No newline at end of file diff --git a/src/main/kotlin/reactredux/Index.kt b/src/main/kotlin/reactredux/Index.kt new file mode 100644 index 0000000..b9df2a0 --- /dev/null +++ b/src/main/kotlin/reactredux/Index.kt @@ -0,0 +1,21 @@ +package reactredux + +import reactredux.components.app +import reactredux.reducers.State +import react.dom.render +import react.redux.provider +import redux.createStore +import redux.rEnhancer +import kotlinx.browser.document +import reactredux.reducers.rootReducer + +val store = createStore(::rootReducer, State(), rEnhancer()) + +fun main() { + val rootDiv = document.getElementById("root")!! + render(rootDiv) { + provider(store) { + app() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/actions/Actions.kt b/src/main/kotlin/reactredux/actions/Actions.kt similarity index 61% rename from src/main/kotlin/nl/lawik/poc/frontend/reactredux/actions/Actions.kt rename to src/main/kotlin/reactredux/actions/Actions.kt index a4edde9..c7a1706 100644 --- a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/actions/Actions.kt +++ b/src/main/kotlin/reactredux/actions/Actions.kt @@ -1,6 +1,6 @@ -package nl.lawik.poc.frontend.reactredux.actions +package reactredux.actions -import nl.lawik.poc.frontend.reactredux.enums.VisibilityFilter +import reactredux.enums.VisibilityFilter import redux.RAction class SetVisibilityFilter(val filter: VisibilityFilter) : RAction @@ -13,4 +13,5 @@ class AddTodo(val text: String): RAction { } class ToggleTodo(val id: Int): RAction - +class DeleteTodo(val id: Int): RAction +class EditTodo(val id: Int, val newText: String): RAction \ No newline at end of file diff --git a/src/main/kotlin/reactredux/components/App.kt b/src/main/kotlin/reactredux/components/App.kt new file mode 100644 index 0000000..88e4d93 --- /dev/null +++ b/src/main/kotlin/reactredux/components/App.kt @@ -0,0 +1,40 @@ +package reactredux.components + +import reactredux.pages.toDoListPage +import react.RBuilder +import react.createElement +import react.dom.div +import react.dom.h1 +import react.router.Route +import react.router.Routes +import react.router.dom.* + +private const val TODO_LIST_PATH = "/todolist" + +fun RBuilder.app() = + BrowserRouter { + Routes { + Route { + attrs.path = "/" + attrs.element = createElement { + div { + h1 { + +"Kotlin React + React-Dom + Redux + React-Redux + React-Router Example" + } + NavLink { + attrs.to = TODO_LIST_PATH + +"Go to todo list" + } + } + } + } + Route { + attrs.path = TODO_LIST_PATH + attrs.element = createElement { + toDoListPage() + } + } + } + } + + diff --git a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/components/Footer.kt b/src/main/kotlin/reactredux/components/Filters.kt similarity index 55% rename from src/main/kotlin/nl/lawik/poc/frontend/reactredux/components/Footer.kt rename to src/main/kotlin/reactredux/components/Filters.kt index 54a9f31..5a64d51 100644 --- a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/components/Footer.kt +++ b/src/main/kotlin/reactredux/components/Filters.kt @@ -1,14 +1,12 @@ -package nl.lawik.poc.frontend.reactredux.components +package reactredux.components -import nl.lawik.poc.frontend.reactredux.containers.filterLink -import nl.lawik.poc.frontend.reactredux.enums.VisibilityFilter +import reactredux.containers.filterLink +import reactredux.enums.VisibilityFilter import react.RBuilder -import react.dom.div -import react.dom.span +import ringui.ButtonGroup -fun RBuilder.footer() = - div { - span { +"Show: " } +fun RBuilder.filters() = + ButtonGroup { filterLink { attrs.filter = VisibilityFilter.SHOW_ALL +"All" diff --git a/src/main/kotlin/reactredux/components/Link.kt b/src/main/kotlin/reactredux/components/Link.kt new file mode 100644 index 0000000..ed70f03 --- /dev/null +++ b/src/main/kotlin/reactredux/components/Link.kt @@ -0,0 +1,20 @@ +package reactredux.components + +import react.* +import ringui.Button + +external interface LinkProps : PropsWithChildren { + var active: Boolean + var onClick: () -> Unit +} + +@JsExport +class Link(props: LinkProps) : RComponent(props) { + override fun RBuilder.render() { + Button { + attrs.onMouseDown = { props.onClick() } + attrs.active = props.active + props.children() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/reactredux/components/Todo.kt b/src/main/kotlin/reactredux/components/Todo.kt new file mode 100644 index 0000000..17d64ec --- /dev/null +++ b/src/main/kotlin/reactredux/components/Todo.kt @@ -0,0 +1,93 @@ +package reactredux.components + +import kotlinx.css.properties.TextDecorationLine +import kotlinx.css.properties.textDecoration +import kotlinx.html.js.onClickFunction +import reactredux.entities.Todo +import org.w3c.dom.HTMLInputElement +import react.* +import ringui.* +import styled.css +import styled.styledP + +external interface TodoProps : Props { + var todo: Todo + var onClick: () -> Unit + var onDelete: () -> Unit + var onUpdate: (String) -> Unit +} + +private val TodoItem = fc { props -> + val (isEdit, setEdit) = useState(false) + val (editableValue, setEditableValue) = useState(props.todo.text) + + Row { + attrs.baseline = RowPosition.xs + attrs.between = RowPosition.xs + Col { + if (!isEdit) { + styledP { + css { + if (props.todo.completed) textDecoration(TextDecorationLine.lineThrough) + } + attrs.onClickFunction = { props.onClick() } + +props.todo.text + } + } else { + Input { + attrs { + value = editableValue + onChange = { event -> + val target = event.target as HTMLInputElement + setEditableValue(target.value) + } + } + } + } + } + Col { + ButtonGroup { + if (isEdit) { + Button { + attrs.onMouseDown = { + setEditableValue(props.todo.text) + setEdit(false) + } + +"Cancel" + } + Button { + attrs { + onMouseDown = { + props.onUpdate(editableValue) + setEdit(false) + } + disabled = editableValue.isBlank() + } + +"Save" + } + } + Button { + attrs.onMouseDown = { props.onDelete() } + +"Delete" + } + if (!isEdit) { + Button { + attrs.onMouseDown = { setEdit(true) } + +"Edit" + } + } + } + } + } +} + +fun RBuilder.todo(todo: Todo, onClick: () -> Unit, onDelete: () -> Unit, onUpdate: (String) -> Unit) { + TodoItem { + attrs { + this.todo = todo + this.onClick = onClick + this.onDelete = onDelete + this.onUpdate = onUpdate + } + } +} diff --git a/src/main/kotlin/reactredux/components/TodoList.kt b/src/main/kotlin/reactredux/components/TodoList.kt new file mode 100644 index 0000000..00da2f5 --- /dev/null +++ b/src/main/kotlin/reactredux/components/TodoList.kt @@ -0,0 +1,28 @@ +package reactredux.components + +import react.* +import reactredux.entities.Todo +import react.dom.ul + +external interface TodoListProps : Props { + var todos: Array + var toggleTodo: (Int) -> Unit + var deleteTodo: (Int) -> Unit + var updateTodo: (Int, String) -> Unit +} + +@JsExport +class TodoList(props: TodoListProps) : RComponent(props) { + override fun RBuilder.render() { + ul { + props.todos.forEach { + todo( + todo = it, + onClick = { props.toggleTodo(it.id) }, + onDelete = { props.deleteTodo(it.id) }, + onUpdate = { newText -> props.updateTodo(it.id, newText) } + ) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/reactredux/containers/AddTodo.kt b/src/main/kotlin/reactredux/containers/AddTodo.kt new file mode 100644 index 0000000..0fc9db1 --- /dev/null +++ b/src/main/kotlin/reactredux/containers/AddTodo.kt @@ -0,0 +1,54 @@ +package reactredux.containers + +import kotlinx.html.ButtonType +import kotlinx.html.js.onSubmitFunction +import reactredux.actions.AddTodo +import reactredux.store +import org.w3c.dom.HTMLInputElement +import react.* +import react.dom.div +import react.dom.form +import react.redux.rConnect +import redux.WrapperAction +import ringui.* + +@JsExport +class AddTodo(props: Props) : RComponent(props) { + private val inputRef = createRef() + override fun RBuilder.render() { + div { + form { + attrs.onSubmitFunction = { event -> + event.preventDefault() + inputRef.current!!.let { + if (it.value.trim().isNotEmpty()) { + store.dispatch(AddTodo(it.value)) + it.value = "" + } + } + } + Row { + attrs.baseline = RowPosition.xs + attrs.center = RowPosition.xs + Col { + Input { + attrs.size = "L" + attrs.inputRef = inputRef + attrs.label = "Task name" + } + } + Col { + Button { + attrs.asDynamic().type = ButtonType.submit + +"Add Todo" + } + } + } + } + } + } +} + + +val addTodo: ComponentClass = + rConnect()(reactredux.containers.AddTodo::class.js.unsafeCast>()) \ No newline at end of file diff --git a/src/main/kotlin/reactredux/containers/FilterLink.kt b/src/main/kotlin/reactredux/containers/FilterLink.kt new file mode 100644 index 0000000..9b9fee0 --- /dev/null +++ b/src/main/kotlin/reactredux/containers/FilterLink.kt @@ -0,0 +1,34 @@ +package reactredux.containers + +import reactredux.actions.SetVisibilityFilter +import reactredux.components.Link +import reactredux.components.LinkProps +import reactredux.enums.VisibilityFilter +import reactredux.reducers.State +import react.ComponentClass +import react.Props +import react.invoke +import react.redux.rConnect +import redux.WrapperAction + +external interface FilterLinkProps : Props { + var filter: VisibilityFilter +} + +private external interface LinkStateProps : Props { + var active: Boolean +} + +private external interface LinkDispatchProps : Props { + var onClick: () -> Unit +} + +val filterLink: ComponentClass = + rConnect( + { state, ownProps -> + active = state.visibilityFilter == ownProps.filter + }, + { dispatch, ownProps -> + onClick = { dispatch(SetVisibilityFilter(ownProps.filter)) } + } + )(Link::class.js.unsafeCast>()) diff --git a/src/main/kotlin/reactredux/containers/VisibleTodoList.kt b/src/main/kotlin/reactredux/containers/VisibleTodoList.kt new file mode 100644 index 0000000..aea8d88 --- /dev/null +++ b/src/main/kotlin/reactredux/containers/VisibleTodoList.kt @@ -0,0 +1,44 @@ +package reactredux.containers + +import reactredux.actions.DeleteTodo +import reactredux.actions.EditTodo +import reactredux.actions.ToggleTodo +import reactredux.components.TodoList +import reactredux.components.TodoListProps +import reactredux.entities.Todo +import reactredux.enums.VisibilityFilter +import reactredux.reducers.State +import react.ComponentClass +import react.Props +import react.invoke +import react.redux.rConnect +import redux.RAction +import redux.WrapperAction + +private fun getVisibleTodos(todos: Array, filter: VisibilityFilter): Array = when (filter) { + VisibilityFilter.SHOW_ALL -> todos + VisibilityFilter.SHOW_ACTIVE -> todos.filter { !it.completed }.toTypedArray() + VisibilityFilter.SHOW_COMPLETED -> todos.filter { it.completed }.toTypedArray() +} + +private external interface TodoListStateProps : Props { + var todos: Array +} + +private external interface TodoListDispatchProps : Props { + var toggleTodo: (Int) -> Unit + var deleteTodo: (Int) -> Unit + var updateTodo: (Int, String) -> Unit +} + +val visibleTodoList: ComponentClass = + rConnect( + { state, _ -> + todos = getVisibleTodos(state.todos, state.visibilityFilter) + }, + { dispatch, _ -> + toggleTodo = { dispatch(ToggleTodo(it)) } + deleteTodo = { dispatch(DeleteTodo(it)) } + updateTodo = { id, newText -> dispatch(EditTodo(id, newText)) } + } + )(TodoList::class.js.unsafeCast>()) \ No newline at end of file diff --git a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/entities/Todo.kt b/src/main/kotlin/reactredux/entities/Todo.kt similarity index 59% rename from src/main/kotlin/nl/lawik/poc/frontend/reactredux/entities/Todo.kt rename to src/main/kotlin/reactredux/entities/Todo.kt index 281a634..5cc23b0 100644 --- a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/entities/Todo.kt +++ b/src/main/kotlin/reactredux/entities/Todo.kt @@ -1,3 +1,3 @@ -package nl.lawik.poc.frontend.reactredux.entities +package reactredux.entities data class Todo(val id: Int, val text: String, var completed: Boolean) diff --git a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/enums/VisibilityFilter.kt b/src/main/kotlin/reactredux/enums/VisibilityFilter.kt similarity index 63% rename from src/main/kotlin/nl/lawik/poc/frontend/reactredux/enums/VisibilityFilter.kt rename to src/main/kotlin/reactredux/enums/VisibilityFilter.kt index 9565798..5170bb8 100644 --- a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/enums/VisibilityFilter.kt +++ b/src/main/kotlin/reactredux/enums/VisibilityFilter.kt @@ -1,4 +1,4 @@ -package nl.lawik.poc.frontend.reactredux.enums +package reactredux.enums enum class VisibilityFilter { SHOW_ALL, diff --git a/src/main/kotlin/reactredux/pages/ToDoListPage.kt b/src/main/kotlin/reactredux/pages/ToDoListPage.kt new file mode 100644 index 0000000..414638c --- /dev/null +++ b/src/main/kotlin/reactredux/pages/ToDoListPage.kt @@ -0,0 +1,45 @@ +package reactredux.pages + +import reactredux.components.filters +import reactredux.containers.addTodo +import reactredux.containers.visibleTodoList +import react.RBuilder +import react.dom.br +import react.router.dom.NavLink +import ringui.* + +fun RBuilder.toDoListPage() { + Grid { + Row { + attrs { center = RowPosition.xs } + + Col { + attrs { + xs = 10 + md = 8 + lg = 5 + } + + Island { + IslandHeader { + attrs.border = true + + Heading { + +"Todo app sample" + } + } + IslandContent { + addTodo {} + filters() + visibleTodoList {} + br {} + NavLink { + attrs.to = "/" + +"Go back" + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/reactredux/reducers/Index.kt b/src/main/kotlin/reactredux/reducers/Index.kt new file mode 100644 index 0000000..92d7ad7 --- /dev/null +++ b/src/main/kotlin/reactredux/reducers/Index.kt @@ -0,0 +1,18 @@ +package reactredux.reducers + +import reactredux.entities.Todo +import reactredux.enums.VisibilityFilter +import redux.RAction + +data class State( + val todos: Array = emptyArray(), + val visibilityFilter: VisibilityFilter = VisibilityFilter.SHOW_ALL +) + +fun rootReducer( + state: State, + action: Any +) = State( + todos(state.todos, action.unsafeCast()), + visibilityFilter(state.visibilityFilter, action.unsafeCast()), +) \ No newline at end of file diff --git a/src/main/kotlin/reactredux/reducers/Todos.kt b/src/main/kotlin/reactredux/reducers/Todos.kt new file mode 100644 index 0000000..4277132 --- /dev/null +++ b/src/main/kotlin/reactredux/reducers/Todos.kt @@ -0,0 +1,28 @@ +package reactredux.reducers + +import reactredux.actions.AddTodo +import reactredux.actions.DeleteTodo +import reactredux.actions.EditTodo +import reactredux.actions.ToggleTodo +import reactredux.entities.Todo +import redux.RAction + +fun todos(state: Array = emptyArray(), action: RAction): Array = when (action) { + is AddTodo -> state + Todo(action.id, action.text, false) + is ToggleTodo -> state.map { + if (it.id == action.id) { + it.copy(completed = !it.completed) + } else { + it + } + }.toTypedArray() + is DeleteTodo -> state.filterNot { it.id == action.id }.toTypedArray() + is EditTodo -> state.map { + if(it.id == action.id) { + it.copy(text = action.newText) + } else { + it + } + }.toTypedArray() + else -> state +} \ No newline at end of file diff --git a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/reducers/VisiblityFilter.kt b/src/main/kotlin/reactredux/reducers/VisiblityFilter.kt similarity index 55% rename from src/main/kotlin/nl/lawik/poc/frontend/reactredux/reducers/VisiblityFilter.kt rename to src/main/kotlin/reactredux/reducers/VisiblityFilter.kt index 030e8d5..27907b0 100644 --- a/src/main/kotlin/nl/lawik/poc/frontend/reactredux/reducers/VisiblityFilter.kt +++ b/src/main/kotlin/reactredux/reducers/VisiblityFilter.kt @@ -1,7 +1,7 @@ -package nl.lawik.poc.frontend.reactredux.reducers +package reactredux.reducers -import nl.lawik.poc.frontend.reactredux.actions.SetVisibilityFilter -import nl.lawik.poc.frontend.reactredux.enums.VisibilityFilter +import reactredux.actions.SetVisibilityFilter +import reactredux.enums.VisibilityFilter import redux.RAction fun visibilityFilter( diff --git a/src/main/resources/web/index.html b/src/main/resources/index.html similarity index 70% rename from src/main/resources/web/index.html rename to src/main/resources/index.html index 6795924..043f7b4 100644 --- a/src/main/resources/web/index.html +++ b/src/main/resources/index.html @@ -6,6 +6,6 @@
- + \ No newline at end of file diff --git a/webpack.config.d/filename.js b/webpack.config.d/filename.js index af1c9a6..26f1738 100644 --- a/webpack.config.d/filename.js +++ b/webpack.config.d/filename.js @@ -1 +1 @@ -config.output.filename = "kotlin-poc-frontend-react-redux.bundle.js" \ No newline at end of file +config.output.filename = "react-redux-todo-list-sample.bundle.js" \ No newline at end of file diff --git a/webpack.config.d/ring-ui.js b/webpack.config.d/ring-ui.js new file mode 100644 index 0000000..41da041 --- /dev/null +++ b/webpack.config.d/ring-ui.js @@ -0,0 +1,3 @@ +const ringConfig = require('@jetbrains/ring-ui/webpack.config').config; + +config.module.rules.push(...ringConfig.module.rules) \ No newline at end of file