diff --git a/BrowserTests/Package.swift b/BrowserTests/Package.swift index f3662da..6141175 100644 --- a/BrowserTests/Package.swift +++ b/BrowserTests/Package.swift @@ -5,14 +5,15 @@ import PackageDescription var products: [Product] = [] var targets: [Target] = [] -func addPage(name: String) { +func addPage(name: String, resources: [String]? = nil) { targets.append( .executableTarget( name: name, dependencies: [ .product(name: "React", package: "swift-react"), .target(name: "BRTSupport") - ] + ], + resources: resources?.map { .copy($0) } ) ) products.append( @@ -22,12 +23,13 @@ func addPage(name: String) { addPage(name: "QSComponents") addPage(name: "QSMarkup") -addPage(name: "QSDisplayingData") +addPage(name: "QSDisplayingData", resources: ["styles.css"]) addPage(name: "QSConditional") addPage(name: "QSLists") addPage(name: "QSEvents") -addPage(name: "QSUpdating") -addPage(name: "QSSharing") +addPage(name: "QSUpdating", resources: ["styles.css"]) +addPage(name: "QSSharing", resources: ["styles.css"]) +addPage(name: "TicTacToe", resources: ["styles.css"]) let package = Package( name: "BrowserTests", @@ -43,6 +45,9 @@ let package = Package( name: "BRTSupport", dependencies: [ .product(name: "React", package: "swift-react") + ], + resources: [ + .copy("common.css") ] ) ] + targets + [ diff --git a/BrowserTests/Sources/BRTSupport/CSS.swift b/BrowserTests/Sources/BRTSupport/CSS.swift index 18ffa4d..8a008e2 100644 --- a/BrowserTests/Sources/BRTSupport/CSS.swift +++ b/BrowserTests/Sources/BRTSupport/CSS.swift @@ -10,7 +10,7 @@ public func addCSS(path: String) throws { try tag.setAttributes([ "rel": "stylesheet", "type": "text/css", - "href": path + "href": "/.build/wasm32-unknown-wasi/debug/" + path ]) try head.appendChild(tag) } diff --git a/BrowserTests/Sources/GenPagesModule/GenPages.swift b/BrowserTests/Sources/GenPagesModule/GenPages.swift index 6e1e4ef..10d80b2 100644 --- a/BrowserTests/Sources/GenPagesModule/GenPages.swift +++ b/BrowserTests/Sources/GenPagesModule/GenPages.swift @@ -22,7 +22,7 @@ public struct GenPages { private static func pages(rootDir: URL) throws -> [String] { let packageCode = try String(contentsOf: rootDir.appending(components: "Package.swift")) - let pageRegex = /addPage\(name: "(\w*)"\)/ + let pageRegex = /addPage\(name: "(\w*)"/ return packageCode.matches(of: pageRegex).map { String($0.output.1) } } diff --git a/BrowserTests/Sources/QSDisplayingData/main.swift b/BrowserTests/Sources/QSDisplayingData/main.swift index 13d8987..724ce76 100644 --- a/BrowserTests/Sources/QSDisplayingData/main.swift +++ b/BrowserTests/Sources/QSDisplayingData/main.swift @@ -32,5 +32,5 @@ struct Profile: Component { } } -try addCSS(path: "/Sources/QSDisplayingData/styles.css") +try addCSS(path: "BrowserTests_QSDisplayingData.resources/styles.css") try renderRoot(component: Profile()) diff --git a/BrowserTests/Sources/QSSharing/main.swift b/BrowserTests/Sources/QSSharing/main.swift index 2dc1ac9..a128a55 100644 --- a/BrowserTests/Sources/QSSharing/main.swift +++ b/BrowserTests/Sources/QSSharing/main.swift @@ -36,7 +36,7 @@ struct MyApp: Component { } } -try addCSS(path: "/Sources/QSSharing/styles.css") +try addCSS(path: "BrowserTests_QSSharing.resources/styles.css") try renderRoot(component: MyApp()) diff --git a/BrowserTests/Sources/QSUpdating/main.swift b/BrowserTests/Sources/QSUpdating/main.swift index e57c063..30c0395 100644 --- a/BrowserTests/Sources/QSUpdating/main.swift +++ b/BrowserTests/Sources/QSUpdating/main.swift @@ -33,7 +33,7 @@ struct MyApp: Component { } } -try addCSS(path: "/Sources/QSUpdating/styles.css") +try addCSS(path: "BrowserTests_QSUpdating.resources/styles.css") try renderRoot(component: MyApp()) diff --git a/BrowserTests/Sources/TicTacToe/main.swift b/BrowserTests/Sources/TicTacToe/main.swift new file mode 100644 index 0000000..209b016 --- /dev/null +++ b/BrowserTests/Sources/TicTacToe/main.swift @@ -0,0 +1,143 @@ +import React +import JavaScriptKit +import SRTDOM +import BRTSupport + +// https://ja.react.dev/learn/tutorial-tic-tac-toe + +func calculateWinner(squares: [String?]) -> String? { + let lines: [[Int]] = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [0, 3, 6], + [1, 4, 7], + [2, 5, 8], + [0, 4, 8], + [2, 4, 6] + ] + for line in lines { + let (a, b, c) = (line[0], line[1], line[2]) + + if let x = squares[a], + squares[b] == x, + squares[c] == x { + return x + } + } + return nil +} + +struct Square: Component { + var value: String? + var onSquareClick: EventListener + + func render() -> Node { + return button( + attributes: ["class": "square"], + listeners: ["click": onSquareClick] + ) { + value + } + } +} + +struct Board: Component { + var xIsNext: Bool = true + var squares: [String?] = Array(repeating: nil, count: 9) + var onPlay: Function + + func handleClick(index: Int) { + if squares[index] != nil || + calculateWinner(squares: squares) != nil { return } + + var squares = self.squares + + if xIsNext { + squares[index] = "X" + } else { + squares[index] = "O" + } + + onPlay(squares) + } + + func render() -> Node { + let winner = calculateWinner(squares: squares) + let status: String + if let winner { + status = "Winner: " + winner; + } else { + status = "Next player: " + (xIsNext ? "X" : "O") + } + + return Fragment { + div(attributes: ["class": "status"]) { status } + div(attributes: ["class": "board-row"]) { + Square(value: squares[0], onSquareClick: EventListener { (_) in handleClick(index: 0) }) + Square(value: squares[1], onSquareClick: EventListener { (_) in handleClick(index: 1) }) + Square(value: squares[2], onSquareClick: EventListener { (_) in handleClick(index: 2) }) + } + div(attributes: ["class": "board-row"]) { + Square(value: squares[3], onSquareClick: EventListener { (_) in handleClick(index: 3) }) + Square(value: squares[4], onSquareClick: EventListener { (_) in handleClick(index: 4) }) + Square(value: squares[5], onSquareClick: EventListener { (_) in handleClick(index: 5) }) + } + div(attributes: ["class": "board-row"]) { + Square(value: squares[6], onSquareClick: EventListener { (_) in handleClick(index: 6) }) + Square(value: squares[7], onSquareClick: EventListener { (_) in handleClick(index: 7) }) + Square(value: squares[8], onSquareClick: EventListener { (_) in handleClick(index: 8) }) + } + } + } +} + +struct Game: Component { + @State var history: [[String?]] = [Array(repeating: nil, count: 9)] + @State var currentMove: Int = 0 + + func render() -> Node { + let xIsNext = currentMove % 2 == 0 + + let currentSquares = history[currentMove] + + let handlePlay = Function { (nextSquares) in + history = history[...currentMove] + [nextSquares] + currentMove = history.count - 1 + } + + func jumpTo(nextMove: Int) { + currentMove = nextMove + } + + let moves: [Node] = history.enumerated().map { (move, squares) -> Node in + let description: String + if move > 0 { + description = "Go to move #\(move)" + } else { + description = "Go to game start" + } + return li(key: move) { + button(listeners: ["click": EventListener { (e) in jumpTo(nextMove: move) }]) { + description + } + } + } + + return div(attributes: ["class": "game"]) { + div(attributes: ["class": "game-board"]) { + Board(xIsNext: xIsNext, squares: currentSquares, onPlay: handlePlay) + } + div(attributes: ["class": "game-info"]) { + ol { + moves + } + } + } + } +} + +try addCSS(path: "BrowserTests_TicTacToe.resources/styles.css") +try renderRoot(component: Game()) + + diff --git a/BrowserTests/Sources/TicTacToe/styles.css b/BrowserTests/Sources/TicTacToe/styles.css new file mode 100644 index 0000000..b0a89a1 --- /dev/null +++ b/BrowserTests/Sources/TicTacToe/styles.css @@ -0,0 +1,32 @@ +.square { + background: #fff; + border: 1px solid #999; + float: left; + font-size: 24px; + font-weight: bold; + line-height: 34px; + height: 34px; + margin-right: -1px; + margin-top: -1px; + padding: 0; + text-align: center; + width: 34px; +} + +.board-row:after { + clear: both; + content: ''; + display: table; +} + +.status { + margin-bottom: 10px; +} +.game { + display: flex; + flex-direction: row; +} + +.game-info { + margin-left: 20px; +} diff --git a/BrowserTests/index.html b/BrowserTests/index.html index b6bd7ee..22b86d2 100644 --- a/BrowserTests/index.html +++ b/BrowserTests/index.html @@ -30,6 +30,9 @@
  • QSSharing
  • +
  • + TicTacToe +
  • \ No newline at end of file diff --git a/BrowserTests/pages/TicTacToe.html b/BrowserTests/pages/TicTacToe.html new file mode 100644 index 0000000..60762e5 --- /dev/null +++ b/BrowserTests/pages/TicTacToe.html @@ -0,0 +1,14 @@ + + + + + TicTacToe + + + + + + \ No newline at end of file diff --git a/BrowserTests/vite.config.js b/BrowserTests/vite.config.js index cce5791..433ab3d 100644 --- a/BrowserTests/vite.config.js +++ b/BrowserTests/vite.config.js @@ -13,6 +13,7 @@ export default defineConfig({ QSEvents: resolve(__dirname, "pages/QSEvents.html"), QSUpdating: resolve(__dirname, "pages/QSUpdating.html"), QSSharing: resolve(__dirname, "pages/QSSharing.html"), + TicTacToe: resolve(__dirname, "pages/TicTacToe.html"), }, }, } diff --git a/Sources/SRTDOM/JSDocument.swift b/Sources/SRTDOM/JSDocument.swift index 5694ca0..55395e4 100644 --- a/Sources/SRTDOM/JSDocument.swift +++ b/Sources/SRTDOM/JSDocument.swift @@ -28,6 +28,10 @@ public struct JSDocument: ConvertibleToJSObject & ConstructibleFromJSValue { try .mustConstruct(from: try jsValue.throws.createTextNode(data)) } + public func getElementById(_ id: String) throws -> JSHTMLElement? { + try .mustConstruct(from: try jsValue.throws.getElementById(id)) + } + public func querySelector(_ selectors: String) throws -> JSHTMLElement? { try .mustConstruct(from: try jsValue.throws.querySelector(selectors)) } diff --git a/Sources/SRTDOM/JSWindow.swift b/Sources/SRTDOM/JSWindow.swift index b7fbd98..fcdfe0c 100644 --- a/Sources/SRTDOM/JSWindow.swift +++ b/Sources/SRTDOM/JSWindow.swift @@ -18,7 +18,7 @@ public struct JSWindow: ConvertibleToJSObject & ConstructibleFromJSValue { public var document: JSDocument { .unsafeConstruct(from: jsValue.document) } - public func alert(_ message: String) throws { try jsValue.throws.alert(message) } + public func alert(_ message: String) throws { _ = try jsValue.throws.alert(message) } public static var global: JSWindow { JSWindow(jsObject: JSObject.global)