= [
+ {formName: "Horizontal Form", formType: Horizontal},
+ {formName: "Inline Form", formType: Inline},
+ {formName: "Stacked Form", formType: Stacked},
+ ]
+
+
+
+ {forms
+ ->Array.map(form =>
+ SetFormType(form.formType)->dispatch}>
+ {form.formName->React.string}
+
+ )
+ ->React.array}
+
+
+ {switch state.form {
+ | Horizontal =>
+
+ | Stacked =>
+
+ | Inline =>
+
+ }}
+ {switch state.savingStatus {
+ | Error =>
+ | Idle
+ | Saving => React.null
+ }}
+
+}
diff --git a/client/app/bundles/comments/rescript/CommentForm/forms/HorizontalForm.res b/client/app/bundles/comments/rescript/CommentForm/forms/HorizontalForm.res
new file mode 100644
index 00000000..8c4bbab9
--- /dev/null
+++ b/client/app/bundles/comments/rescript/CommentForm/forms/HorizontalForm.res
@@ -0,0 +1,37 @@
+@react.component
+let make = (~author, ~handleAuthorChange, ~text, ~handleTextChange, ~handleSubmit, ~disabled) => {
+
+}
diff --git a/client/app/bundles/comments/rescript/CommentForm/forms/InlineForm.res b/client/app/bundles/comments/rescript/CommentForm/forms/InlineForm.res
new file mode 100644
index 00000000..b882cea7
--- /dev/null
+++ b/client/app/bundles/comments/rescript/CommentForm/forms/InlineForm.res
@@ -0,0 +1,40 @@
+@react.component
+let make = (~author, ~handleAuthorChange, ~text, ~handleTextChange, ~handleSubmit, ~disabled) => {
+
+}
diff --git a/client/app/bundles/comments/rescript/CommentForm/forms/StackedFrom.res b/client/app/bundles/comments/rescript/CommentForm/forms/StackedFrom.res
new file mode 100644
index 00000000..1daedcfa
--- /dev/null
+++ b/client/app/bundles/comments/rescript/CommentForm/forms/StackedFrom.res
@@ -0,0 +1,37 @@
+@react.component
+let make = (~author, ~handleAuthorChange, ~text, ~handleTextChange, ~handleSubmit, ~disabled) => {
+
+}
diff --git a/client/app/bundles/comments/rescript/CommentList/AlertError/AlertError.res b/client/app/bundles/comments/rescript/CommentList/AlertError/AlertError.res
new file mode 100644
index 00000000..122726c2
--- /dev/null
+++ b/client/app/bundles/comments/rescript/CommentList/AlertError/AlertError.res
@@ -0,0 +1,26 @@
+@module("../../ReScriptShow.module.scss") external css: {..} = "default"
+
+@react.component
+let make = (~errorMsg: string) => {
+ let nodeRef = React.useRef(Js.Nullable.null)
+
+ let cssTransitionGroupClassNames: ReactTransitionGroup.CSSTransition.t = {
+ enter: css["elementEnter"],
+ enterActive: css["elementEnterActive"],
+ exit: css["elementLeave"],
+ exitActive: css["elementLeaveActive"],
+ }
+
+ // The 500 must correspond to the 0.5s in:
+ // ../../RescriptShow.module.scss:9
+
+
+ {errorMsg->React.string}
+
+
+}
diff --git a/client/app/bundles/comments/rescript/CommentList/Comment/Comment.res b/client/app/bundles/comments/rescript/CommentList/Comment/Comment.res
new file mode 100644
index 00000000..817fe6b1
--- /dev/null
+++ b/client/app/bundles/comments/rescript/CommentList/Comment/Comment.res
@@ -0,0 +1,19 @@
+@react.component
+let make = (~comment: Actions.Fetch.t, ~cssTransitionGroupClassNames) => {
+ let rawMarkup = Marked.marked(comment.text, {gfm: true})
+ let innerHTML = {"__html": rawMarkup}
+ let nodeRef = React.useRef(Js.Nullable.null)
+
+ // The 500 must correspond to the 0.5s in:
+ // ../../RescriptShow.module.scss:9
+
+
+
{comment.author->React.string}
+
+
+
+}
diff --git a/client/app/bundles/comments/rescript/CommentList/CommentList.res b/client/app/bundles/comments/rescript/CommentList/CommentList.res
new file mode 100644
index 00000000..1a433257
--- /dev/null
+++ b/client/app/bundles/comments/rescript/CommentList/CommentList.res
@@ -0,0 +1,23 @@
+@module("../ReScriptShow.module.scss") external css: {..} = "default"
+
+@react.component
+let make = (~comments: Actions.Fetch.comments) => {
+ let cssTransitionGroupClassNames: ReactTransitionGroup.CSSTransition.t = {
+ enter: css["elementEnter"],
+ enterActive: css["elementEnterActive"],
+ exit: css["elementLeave"],
+ exitActive: css["elementLeaveActive"],
+ }
+
+
+
+ {comments
+ ->Array.map(comment =>
+ Int.toString}
+ />
+ )
+ ->React.array}
+
+
+}
diff --git a/client/app/bundles/comments/rescript/Header/Header.res b/client/app/bundles/comments/rescript/Header/Header.res
new file mode 100644
index 00000000..8c794489
--- /dev/null
+++ b/client/app/bundles/comments/rescript/Header/Header.res
@@ -0,0 +1,55 @@
+@react.component
+let make = () => {
+
+}
diff --git a/client/app/bundles/comments/rescript/ReScriptShow.module.scss b/client/app/bundles/comments/rescript/ReScriptShow.module.scss
new file mode 100644
index 00000000..01a74159
--- /dev/null
+++ b/client/app/bundles/comments/rescript/ReScriptShow.module.scss
@@ -0,0 +1,20 @@
+// The 0.5s must correspond to the 500s in:
+// ./CommentList/AlertError/AlertError.res:10
+// ./CommentList/Comment/Comment.res:18
+.elementEnter {
+ opacity: 0.01;
+
+ &.elementEnterActive {
+ opacity: 1;
+ transition: opacity 0.5s ease-in;
+ }
+}
+
+.elementLeave {
+ opacity: 1;
+
+ &.elementLeaveActive {
+ opacity: 0.01;
+ transition: opacity 0.5s ease-in;
+ }
+}
diff --git a/client/app/bundles/comments/rescript/ReScriptShow.res b/client/app/bundles/comments/rescript/ReScriptShow.res
new file mode 100644
index 00000000..4c64f8b2
--- /dev/null
+++ b/client/app/bundles/comments/rescript/ReScriptShow.res
@@ -0,0 +1,66 @@
+type commentsFetchStatus =
+ | FetchError
+ | CommentsFetched(Actions.Fetch.comments)
+
+type state = {commentsFetchStatus: commentsFetchStatus}
+
+type action =
+ | SetComments(Actions.Fetch.comments)
+ | SetFetchError
+
+let reducer = (_, action: action): state => {
+ switch action {
+ | SetComments(comments) => {commentsFetchStatus: CommentsFetched(comments)}
+ | SetFetchError => {commentsFetchStatus: FetchError}
+ }
+}
+
+@react.component
+let default = () => {
+ let (state, dispatch) = React.useReducer(
+ reducer,
+ {
+ commentsFetchStatus: CommentsFetched(([]: Actions.Fetch.comments)),
+ },
+ )
+
+ let fetchData = async () => {
+ let comments = await Actions.Fetch.fetchComments()
+ switch comments {
+ | Ok(comments) => SetComments(comments)->dispatch
+ | Error(_) => SetFetchError->dispatch
+ }
+ }
+
+ React.useEffect1(_ => {
+ fetchData()->ignore
+ None
+ }, [])
+
+
+
+
+
+
{"Comments"->React.string}
+
+ {"Text supports Github Flavored Markdown."->React.string}
+ {"Comments older than 24 hours are deleted."->React.string}
+ {"Name is preserved. Text is reset, between submits"->React.string}
+
+ {"To see Action Cable instantly update two browsers, open two browsers and submit a comment!"->React.string}
+
+
+
+ {switch state.commentsFetchStatus {
+ | FetchError =>
+ | CommentsFetched(comments) =>
+ }}
+
+
+}
diff --git a/client/app/bundles/comments/rescript/bindings/Axios.res b/client/app/bundles/comments/rescript/bindings/Axios.res
new file mode 100644
index 00000000..932abc48
--- /dev/null
+++ b/client/app/bundles/comments/rescript/bindings/Axios.res
@@ -0,0 +1 @@
+@module("axios") external post: (string, {..}, {..}) => promise = "post"
diff --git a/client/app/bundles/comments/rescript/bindings/Marked.res b/client/app/bundles/comments/rescript/bindings/Marked.res
new file mode 100644
index 00000000..782b00be
--- /dev/null
+++ b/client/app/bundles/comments/rescript/bindings/Marked.res
@@ -0,0 +1,2 @@
+type markedOptions = {gfm: bool}
+@module("marked") external marked: (string, markedOptions) => string = "marked"
diff --git a/client/app/bundles/comments/rescript/bindings/ReactTransitionGroup.res b/client/app/bundles/comments/rescript/bindings/ReactTransitionGroup.res
new file mode 100644
index 00000000..5acdd747
--- /dev/null
+++ b/client/app/bundles/comments/rescript/bindings/ReactTransitionGroup.res
@@ -0,0 +1,26 @@
+module TransitionGroup = {
+ @react.component @module("react-transition-group")
+ external make: (
+ ~children: React.element,
+ ~className: string,
+ ~component: string,
+ ) => React.element = "TransitionGroup"
+}
+
+module CSSTransition = {
+ type t = {
+ enter: string,
+ enterActive: string,
+ exit: string,
+ exitActive: string,
+ }
+
+ @react.component @module("react-transition-group")
+ external make: (
+ ~children: React.element,
+ ~key: string,
+ ~timeout: int,
+ ~nodeRef: React.ref>,
+ ~classNames: t,
+ ) => React.element = "CSSTransition"
+}
diff --git a/client/app/packs/client-bundle.js b/client/app/packs/client-bundle.js
index 5365a0cc..2675009a 100644
--- a/client/app/packs/client-bundle.js
+++ b/client/app/packs/client-bundle.js
@@ -10,6 +10,7 @@ import routerCommentsStore from '../bundles/comments/store/routerCommentsStore';
import commentsStore from '../bundles/comments/store/commentsStore';
import NavigationBarApp from '../bundles/comments/startup/NavigationBarApp';
import Footer from '../bundles/comments/components/Footer/Footer';
+import RescriptShow from '../bundles/comments/rescript/ReScriptShow.bs.js';
import '../assets/styles/application';
@@ -25,6 +26,7 @@ ReactOnRails.register({
NavigationBarApp,
SimpleCommentScreen,
Footer,
+ RescriptShow,
});
ReactOnRails.registerStore({
diff --git a/client/app/packs/server-bundle.js b/client/app/packs/server-bundle.js
index 4cec29f5..96b56491 100644
--- a/client/app/packs/server-bundle.js
+++ b/client/app/packs/server-bundle.js
@@ -8,6 +8,7 @@ import NavigationBarApp from '../bundles/comments/startup/NavigationBarApp';
import routerCommentsStore from '../bundles/comments/store/routerCommentsStore';
import commentsStore from '../bundles/comments/store/commentsStore';
import Footer from '../bundles/comments/components/Footer/Footer';
+import RescriptShow from '../bundles/comments/rescript/ReScriptShow.bs.js';
ReactOnRails.register({
App,
@@ -15,6 +16,7 @@ ReactOnRails.register({
NavigationBarApp,
SimpleCommentScreen,
Footer,
+ RescriptShow,
});
ReactOnRails.registerStore({
diff --git a/config/routes.rb b/config/routes.rb
index ab6d4132..1d8c7b7a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -9,6 +9,7 @@
root "pages#index"
get "simple", to: "pages#simple"
+ get "rescript", to: "pages#rescript"
get "no-router", to: "pages#no_router"
# React Router needs a wildcard
diff --git a/lib/tasks/ci.rake b/lib/tasks/ci.rake
index dcea8e00..29a4dc04 100644
--- a/lib/tasks/ci.rake
+++ b/lib/tasks/ci.rake
@@ -13,10 +13,15 @@ if Rails.env.development? || Rails.env.test?
sh "rspec"
end
+ task build_rescript: :environment do
+ puts Rainbow("Building ReScript files").green
+ sh "yarn res:build"
+ end
+
namespace :ci do
desc "Run all audits and tests"
# rspec_tests must be before lint and js_tests to build the locale files
- task all: %i[environment rspec_tests lint js_tests] do
+ task all: %i[environment build_rescript rspec_tests lint js_tests] do
puts "All CI tasks"
puts Rainbow("PASSED").green
puts ""
@@ -28,7 +33,7 @@ if Rails.env.development? || Rails.env.test?
end
desc "Run CI rspec tests"
- task rspec: %i[environment rspec_tests] do
+ task rspec: %i[environment build_rescript rspec_tests] do
puts "CI rspec tests"
puts Rainbow("PASSED").green
puts ""
@@ -40,7 +45,7 @@ if Rails.env.development? || Rails.env.test?
end
desc "Run CI js_tests"
- task js: %i[environment js_tests] do
+ task js: %i[environment build_rescript js_tests] do
puts "CI js_tests"
puts Rainbow("PASSED").green
puts ""
diff --git a/lib/tasks/linters.rake b/lib/tasks/linters.rake
index de6637dc..e8035ce6 100644
--- a/lib/tasks/linters.rake
+++ b/lib/tasks/linters.rake
@@ -50,7 +50,7 @@ if %w[development test].include? Rails.env
desc "See docs for task 'scss_lint'"
task scss: :scss_lint
- task lint: %i[rubocop js scss] do
+ task lint: %i[build_rescript rubocop js scss] do
puts "Completed all linting"
end
end
diff --git a/package.json b/package.json
index b1f5a166..f8fbf322 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,10 @@
},
"homepage": "https://github.com/shakacode/react-webpack-rails-tutorial",
"scripts": {
+ "res:clean": "rescript clean",
+ "res:format": "rescript format -all",
+ "res:dev": "yarn res:clean && rescript build -w",
+ "res:build": "yarn res:clean && rescript build",
"lint:eslint": "yarn eslint client --ext \".js,.jsx,.ts\"",
"lint:prettier": "yarn prettier \"**/*.@(js|jsx)\" --list-different",
"lint": " yarn lint:eslint --fix && yarn lint:prettier --w",
@@ -33,10 +37,14 @@
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/runtime": "^7.17.9",
+ "@glennsl/rescript-fetch": "^0.2.0",
+ "@glennsl/rescript-json-combinators": "^1.2.1",
"@hotwired/stimulus": "^3.2.1",
"@hotwired/stimulus-webpack-helpers": "^1.0.1",
"@hotwired/turbo-rails": "^7.3.0",
"@rails/actioncable": "7.0.5",
+ "@rescript/core": "^0.5.0",
+ "@rescript/react": "^0.11.0",
"autoprefixer": "^10.4.14",
"axios": "^0.21.1",
"babel-loader": "^9.1.2",
@@ -79,6 +87,8 @@
"react-transition-group": "4.4.5",
"redux": "^4.2.1",
"redux-thunk": "^2.2.0",
+ "rescript": "^10.1.4",
+ "rescript-react-on-rails": "^1.0.1",
"resolve-url-loader": "^2.2.0",
"sanitize-html": "^2.11.0",
"sass": "^1.58.3",
diff --git a/spec/rescript/rescript_spec.rb b/spec/rescript/rescript_spec.rb
new file mode 100644
index 00000000..aedc5aa9
--- /dev/null
+++ b/spec/rescript/rescript_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require "rails_helper"
+
+describe "with Rescript" do
+ describe "tabs change on click" do
+ before do
+ visit "/rescript"
+ end
+
+ it "shows horizontal tab on visit" do
+ page.has_css?("form-horizontal")
+ end
+
+ it "stops showing horizontal tab when other tab is clicked" do
+ find("button", text: "Inline Form")
+ page.has_no_css?("form-horizontal")
+ end
+
+ it "shows inline form when Inline Form link is clicked" do
+ find("button", text: "Inline Form", wait: 10)
+ page.has_css?("form-inline")
+ end
+
+ it "shows stacked form when Stacked Form link is clicked" do
+ find("button", text: "Stacked Form")
+ page.has_no_css?("form-inline") and page.has_no_css?("form-horizontal")
+ end
+ end
+
+ describe "form submission functions" do
+ let(:comment) { Comment.new(author: "Author", text: "This is a comment") }
+ let(:author_field) { "comment_author" }
+ let(:text_field) { "comment_text" }
+
+ before do
+ visit "/rescript"
+ end
+
+ it "adds a new comment to the page" do
+ fill_in author_field, with: comment.author
+ fill_in text_field, with: comment.text
+ click_button("Post")
+ expect(page).to have_selector "h2", text: comment.author
+ end
+
+ it "comment count increases with successful form submission" do
+ initital_comment_count = Comment.all.count
+ new_comment_count = initital_comment_count + 1
+ fill_in author_field, with: comment.author
+ fill_in text_field, with: comment.text
+ click_button("Post")
+ expect(Comment.all.count).to equal(new_comment_count)
+ end
+
+ it "comment count remains the same when author field is empty" do
+ initial_comment_count = Comment.all.count
+ fill_in text_field, with: comment.text
+ click_button("Post")
+ expect(Comment.all.count).to equal(initial_comment_count)
+ end
+
+ it "comment count remains the same when text field is empty" do
+ initial_comment_count = Comment.all.count
+ fill_in author_field, with: comment.author
+ click_button("Post")
+ expect(Comment.all.count).to equal(initial_comment_count)
+ end
+
+ it "comment count remains the same when both form fields are empty" do
+ initial_comment_count = Comment.all.count
+ click_button("Post")
+ expect(Comment.all.count).to equal(initial_comment_count)
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index f669c60c..efbbf832 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1407,6 +1407,16 @@
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
+"@glennsl/rescript-fetch@^0.2.0":
+ version "0.2.0"
+ resolved "https://registry.yarnpkg.com/@glennsl/rescript-fetch/-/rescript-fetch-0.2.0.tgz#b72085d8bb19d8b9266aaf18d59c440ddceaf8dd"
+ integrity sha512-0tsEqJ/6/WBm02prM4RYG+qpnNTaB8QKKIeQHXdDaE4C5YfA/nzjxMNW3CjsGIaEgyrAmmIXFS0kx24UjvOI6A==
+
+"@glennsl/rescript-json-combinators@^1.2.1":
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/@glennsl/rescript-json-combinators/-/rescript-json-combinators-1.2.1.tgz#3f641e4548ac1a664ce9acb140d5bba9ee1d5c9e"
+ integrity sha512-2Tw7NtrPerxN+9Y0u9bl9rUgqUqFcZkkRYELjHu9mIT/PDG3n8lO/osrwMIfoo6ze2FAur0Fw8hfDRJV8n3W3A==
+
"@hotwired/stimulus-webpack-helpers@^1.0.0", "@hotwired/stimulus-webpack-helpers@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@hotwired/stimulus-webpack-helpers/-/stimulus-webpack-helpers-1.0.1.tgz#4cd74487adeca576c9865ac2b9fe5cb20cef16dd"
@@ -1853,6 +1863,16 @@
resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.7.2.tgz#cba1cf0a04bc04cb66027c51fa600e9cbc388bc8"
integrity sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==
+"@rescript/core@^0.5.0":
+ version "0.5.0"
+ resolved "https://registry.yarnpkg.com/@rescript/core/-/core-0.5.0.tgz#3e83e75aaa60f42f4279e0d184358f2db5d40955"
+ integrity sha512-Keqnpi+8VqyhCk/3aMwar8hJbNy2IsINAAfIFeQC65IIegCR0QXFDBpQxfVcmbbtoHq6HnW4B3RLm/9GCUJQhQ==
+
+"@rescript/react@^0.11.0":
+ version "0.11.0"
+ resolved "https://registry.yarnpkg.com/@rescript/react/-/react-0.11.0.tgz#d2545546d823bdb8e6b59daa1790098d1666f79e"
+ integrity sha512-RzoAO+3cJwXE2D7yodMo4tBO2EkeDYCN/I/Sj/yRweI3S1CY1ZBOF/GMcVtjeIurJJt7KMveqQXTaRrqoGZBBg==
+
"@sinclair/typebox@^0.27.8":
version "0.27.8"
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e"
@@ -8218,6 +8238,16 @@ requires-port@^1.0.0:
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==
+rescript-react-on-rails@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/rescript-react-on-rails/-/rescript-react-on-rails-1.0.1.tgz#541dffdae64ec5053a50a3792b9db8783c959d1b"
+ integrity sha512-sbkDNCoiEWM9rqIiu+4joAj6W92yhM64KtLZQYfvYYm578jMcG02d98xpDeBT7MxZoPZZggFIed0m6Dj8bbDYA==
+
+rescript@^10.1.4:
+ version "10.1.4"
+ resolved "https://registry.yarnpkg.com/rescript/-/rescript-10.1.4.tgz#0f37710d371f32a704f17b4e804f66ce3c79a305"
+ integrity sha512-FFKlS9AG/XrLepWsyw7B+A9DtQBPWEPDPDKghV831Y2KGbie+eeFBOS0xtRHp0xbt7S0N2Dm6hhX+kTZQ/3Ybg==
+
resolve-cwd@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"