Như đã nói trước đó thì chúng ta sẽ không thể trực tiếp định nghĩa một hàm thay đổi nội dung của phần tử <head>
bởi vì các trình đóng gói của Elm
đều không cung cấp giao diện lập trình hỗ trợ. Tuy nhiên, khi người dùng nhấn vào một liên kết bất kỳ trong trang web và nội dung đã được thay đổi để đáp ứng lại yêu cầu đó thì chúng ta cũng cần phải quan tâm tới một vài yếu tố mô tả quan trọng khác.
Ví dụ điển hình là tên của trang đơn được gắn ở thẻ <title>
, hoặc xa hơn thì sẽ là các thẻ <meta>
mô tả nội dung của trang đơn mà người dùng vừa chuyển tới để chương trình tự động duyệt web của các dịch vụ tìm kiếm Google, Bing, v.v... có thể dễ dàng xếp loại được nội dung của mỗi trang đơn có mặt trong tập dữ liệu.
Và giải pháp ở đây là chúng ta sẽ cần một phương thức để tương tác 2 chiều với code JavaScript
bên ngoài. Tức là ở thời điểm code Elm
cần thực hiện một tác vụ như trên thì chúng ta có thể gửi yêu cầu ủy thác cho code JavaScript
hoặc gọi một hàm được định nghĩa bằng code JavaScript
.
Elm Ports & WebSocket
Elm
có hỗ trợ một phương thức để gửi/nhận yêu cầu với code JavaScript
qua các cổng port
được khai báo như code ví dụ dưới đây. Chúng ta sẽ tạo ra một cổng thông tin gửi/nhận với code JavaScript
được tạo ra bởi từ khóa port
ở dòng đầu tiên khai báo module Main
và ở định nghĩa của các hàm sendMessage
và messageReceiver
ngay sau phần import
.
port module Main exposing (..) import Browser
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Json.Decode as D -- PORTS - - - - - - - - - port sendMessage : String -> Cmd msg
port messageReceiver : (String -> msg) -> Sub msg main : Program () Model Msg
main = Browser.element { init = init , view = view , update = update , subscriptions = subscriptions } -- INIT - - - - - - - - - type alias Model = { draft : String , messages : List String } init : () -> ( Model, Cmd Msg )
init flags = ( { draft = "", messages = [] } , Cmd.none ) -- VIEW - - - - - - - - - view : Model -> Html Msg
view model = div [] [ h1 [] [ text "Echo Chat" ] , ul [] (List.map (\msg -> li [] [ text msg ]) model.messages) , input [ type_ "text" , placeholder "Draft" , onInput DraftChanged , on "keydown" (ifIsEnter Send) , value model.draft ] [] , button [ onClick Send ] [ text "Send" ] ] ifIsEnter : msg -> D.Decoder msg
ifIsEnter msg = let getKey = D.field "key" D.string decodeKey = (\key -> if key == "Enter" then D.succeed msg else D.fail "some other key") in getKey |> D.andThen decodeKey type Msg = DraftChanged String | Send | Recv String -- UPDATE - - - - - - - - - update : Msg -> Model -> ( Model, Cmd Msg )
update msg model = case msg of DraftChanged draft -> ( { model | draft = draft } , Cmd.none ) Send -> ( { model | draft = "" } , sendMessage model.draft ) Recv message -> ( { model | messages = model.messages ++ [message] } , Cmd.none ) subscriptions : Model -> Sub Msg
subscriptions _ = messageReceiver Recv
Như vậy là code ở view
chúng ta có khi người dùng nhấn phím Enter
hoặc nhấn vào nút gửi <form>
thì sẽ có một thông báo Send
được gửi tới trình update
. Lúc này update
sẽ sử dụng hàm sendMessage
để gửi dữ liệu và yêu cầu xử lý tới code JavaScript
bên ngoài.
Sau đó chúng ta có các subscriptions
được tạo ra để theo dõi sự kiện phản hồi từ code JavaScript
, và khi nhận thấy sự kiện thì sẽ có một tin nhắn Recv
kèm theo kết quả xử lý từ JavaScript
được gửi tới trình update
. Lúc này có thể một bản ghi model
mới sẽ được tạo ra và giao diện của trang web sẽ được thay đổi để đáp ứng với thao tác gửi <form>
.
Ở phía của JavaScript
, chúng ta cần sử dụng WebSocket
để tạo một mini server
trong môi trường trình duyệt web và định nghĩa một kênh gửi/nhận tương tác. Công cụ này được các trình duyệt web hỗ trợ bắt đầu từ Internet Explorer 10
.
<!doctype HTML>
<html> <head> <meta charset="UTF-8" /> <title> Elm + WebSocket </title> <script type="text/javascript" src="elm.js"> </script>
</head> <body> <div id="elm-spa"> </div>
</body> <script type="text/javascript"> // -- Create your WebSocket var socket = new WebSocket('wss://echo.websocket.org'); // -- Init Elm SPA var app = Elm.Main.init({ node: document.getElementById('elm-spa')
}); // -- Create channel "message" on socket socket.addEventListener("message", function(event) { app.ports.messageReceiver.send(event.data);
}); // -- Send request message from Elm SPA app.ports.sendMessage.subscribe(function(message) { socket.send(message);
}); </script> </html>
Custom Elements
Một cách thức khác để thực hiện tương tác gửi/nhận 2 chiều với code JavaScript
đó là chúng ta có thể tự định nghĩa một class
mô tả phần tử HTML
với giao diện lập trình cần sử dụng, và sau đó tạo ra HTML node
bằng code Elm
để sử dụng giao diện lập trình vừa định nghĩa.
Ví dụ HTML5
cho phép chúng ta sử dụng các thẻ với tên tự đặt và có thể là <formatted-date>
. Tuy nhiên chúng ta cũng muốn rằng phần tử được tạo ra bởi thẻ này có một vài chức năng đặc biệt và quyết định tự định nghĩa một class
kế thừa của HTMLElement
. Và ở đây chúng ta có code ví dụ về một hàm định dạng thông tin localizeDate
được viết trong JavaScript
, sau đó được sử dụng trong giao diện lập trình của class
tự định nghĩa kế thừa HTMLElement
.
<script> // -- Extension for Elm function localizeDate (lang, year, month) { const dateTimeFormat = new Intl.DateTimeFormat (lang, { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); return dateTimeFormat.format (new Date (year, month));
} // -- Define custom HTMLElement class customElements.define ('formatted-date', class extends HTMLElement { // -- things required by Custom Elements constructor ( ) { super ( ); } connectedCallback ( ) { this.setTextContent ( ); } attributeChangedCallback ( ) { this.setTextContent ( ); } static get observedAttributes ( ) { return ['lang','year','month']; } setTextContent ( ) { const lang = this.getAttribute ('lang'); const year = this.getAttribute ('year'); const month = this.getAttribute ('month'); this.textContent = localizeDate (lang, year, month); } } // class
); // -- Init Elm SPA ... </script>
Và trong code của Elm
thì chúng ta có thể sử dụng hàm khởi tạo node
để tạo phần tử HTML
với tên tự định nghĩa ở trên và như vậy phần tử này sẽ có tính năng hoạt động đặc biệt như mong muốn.
module Custom exposing (..) import Html exposing (Html, node)
import Html.Attributes (attribute) viewDate : String -> Int -> Int -> Html msg
viewDate lang year month = node "intl-date" [ attribute "lang" lang , attribute "year" (String.fromInt year) , attribute "month" (String.fromInt month) ] []