От JavaScript к функциональной веб-разработке (часть 2)

Если вы еще не читали, то можете прочитать первую часть здесь!

Когда мне было 12 или около того, мне очень нравилась браузерная MMO-игра. Я пытался воссоздать свою версию этой игры для себя и друзей. В процессе я действительно хотел показать статистику каждого, поэтому я написал таблицу рекордов. Сегодня мы воссоздадим эту таблицу здесь.

Раньше он работал с базой данных SQL, но теперь мы собираемся сделать так, чтобы он позволял нам добавлять новые записи на тот же веб-сайт.

Сначала мы просто создадим наше одностраничное приложение WebSharper, я назову свой проект «HighScoreTable». Мы будем работать только в Client.fs и index.html . Начнем с index.html, чтобы у нас была база для шаблона на F#. Из исходного проекта нам не нужно ничего менять из тега <head>. Наш код входит в <body> .

Для этого нам нужна новая информация о WebSharper. Мы уже видели, как работать со статическими элементами, сгенерированными F#, но на этот раз мы узнаем, как использовать шаблоны. С помощью шаблонов мы можем один раз написать наш код F#, а затем нам нужно только изменить html-файл, и не будет необходимости в пересборке F#.

Что нам нужно? Нам нужно table для нашей таблицы рекордов, несколько полей input для нашей новой записи и button для добавления нашей новой записи. Нам также понадобятся некоторые новые теги, которые мы еще не использовали:
-ws-var : привязывает реактивную переменную к значениям наших полей ввода'
-ws-children-template : создает для нас шаблон, но отбрасывает тегированные Элемент DOM
-ws-template : Создает шаблон, сохраняющий помеченный элемент DOM
-ws-replace : Заменяет элемент DOM заданным элементом(ами)
-ws-hole : Заполняет элемент DOM заданным элементом (s)
-ws-onclick : Назначает функцию действию элемента «onclick».

Но что такое шаблон? С помощью шаблонов мы можем определять html-блоки и использовать (и повторно использовать) их в нашем результирующем html-файле. Например:

<div ws-template="LoginTemplate">
    <input ws-var="name" value="Username" />
    <input type="password" ws-var="password" />
    <button ws-onclick="login">Login</button>
</div>

Мы могли бы сослаться на этот шаблон входа и разместить его в любом месте нашего кода из F# (поскольку мы пометили его тегом ws-template, <div> является частью шаблона). У него будет реактивная переменная, привязанная к «Имени» и «Паролю», и функция, привязанная к кнопке «Войти». Из нашего кода F# мы можем получить к нему доступ, и мы можем создать документ из этого шаблона, который позже можно будет использовать, например, для заполнения ws-replace. Сначала это может показаться странным, но к концу этого поста вы все поймете.

Индекс.html

Поскольку мы хотим добиться результатов модификации html без пересборки F#, нам нужно написать немного больше в нашем index.html . Давайте начнем с нашего основного шаблона, я назову его «Основной»:

<div ws-children-template="Main">
    ...
</div>

Таблица, которую мы собираемся использовать, проста, содержит статическую thead и динамическую tbody. Для создания нашего динамического тела мы будем использовать другой шаблон. Этот шаблон будет служить строкой таблицы для каждой отдельной записи. В этом примере мы назовем этот шаблон «Строка»:

<tr ws-template="Row">
    <td ws-hole="Name"></td>
    <td ws-hole="Level"></td>
    <td ws-hole="Gold"></td>
    <td ws-hole="Score"></td>
</tr>

У нас должен быть этот шаблон между нашими тегами <tbody></tbody, так как это шаблон <tr></tr>. Если у нас есть это вне стола, оно будет выброшено.

От нашего index.html осталось только одно — новая форма заявки. Поскольку мы используем четыре атрибута для каждой строки, у нас должно быть четыре поля ввода и кнопка:

<div>
    <input ws-var="Name" />
    <input type="number" ws-var="Level" />
    <input type="number" ws-var="Gold" />
    <input type="number" ws-var="Score" />
    <button ws-onclick="Add">Add</button>
</div>

С этим добавлением мы почти закончили с файлом html. Все, что нам нужно, это заполнитель для нашего шаблона и красивое название. Например, <div> с id="main" хорошо послужит заполнителем:

<h1>My high score table</h1>
<div id="main"></div>

Это наш index.html с добавленной статической частью таблицы:

Клиент.fs

Прежде всего, нам нужно получить доступ к нашему html-шаблону из файла fs. Мы можем сделать это, определив для него новый тип:

type IndexTemplate = Template<"index.html", ClientLoad.FromDocument>

Используя это, мы можем создать наш шаблон, используя IndexTemplate.Main() и IndexTemplate.Row(), поскольку мы назвали наши шаблоны «Основной» и «Ряд».

Так же, как мы сделали для html-файла, давайте посмотрим, что нам нужно в файле F#:
— Тип записи для наших записей
— Список с нашими записями
— Четыре реактивные переменные для новая запись
- Функция добавления нашей новой записи
- Заполнение шаблонов

Я назову нашу входную запись «Entry» и задам определенные выше свойства:

type Entry =
    {Name: string;
    Level: int;
    Gold: int;
    Score: int}

Чтобы сделать код более читаемым, а тип простым в использовании, мы можем определить функцию Create:

static member Create name level gold score =
        {
            Name = name
            Level = level
            Gold = gold
            Score = score
        }

Для списка мы должны использовать ListModel WebSharper, так как мы можем динамически использовать данные из этого списка, и это именно то, что нам нужно здесь. Мы создадим этот ListModel из seq с помощью функции ListModel.FromSeq и добавим в список элемент по умолчанию:

let Players =
    ListModel.FromSeq [
        Entry.Create "Name" 1 25 200
    ]

С этим у нас есть все, что нам нужно, чтобы заполнить наш шаблон строки таблицы. Мы собираемся начать с простой функции для преобразования нашей записи в строку таблицы:

let row entry =
    IndexTemplate.Row()
        .Name(entry.Name)
        .Level(string entry.Level)
        .Gold(string entry.Gold)
        .Score(string entry.Score)
        .Doc()

Теперь нам нужно просмотреть наш список и сопоставить эту функцию строки с каждым элементом, а затем создать из нее документ, чтобы использовать его в наших <tbody> ws-hole .
Для этого требуется более одной функции, но я все объясню:

let data =
    Players.View.Doc(fun lm ->
        lm
        |> Seq.sortByDescending (fun t ->
            t.Score
        )
        |> Seq.map row
        |> Doc.Concat
    )

Сначала мы создаем View из нашего списка. Это означает, что мы всегда будем получать текущие данные из списка. Затем, как того требует tbody, мы преобразуем наш список в файл Doc. Для этого мы даем функцию, которая получает ListModel в качестве параметра и возвращает Doc. Мы называем это ListModel «lm», затем применяем к нему сортировку (поскольку это таблица рекордов, нам нужна какая-то сортировка). Эта сортировка происходит с помощью функции Seq.sortByDescending. Этой функции нужна другая функция, чтобы знать, по какому полю сортировать. В этом примере мы установили сортировку по счету игрока. Теперь у нас есть отсортированный seq с данными игроков. Следующим шагом является преобразование его в таблицу Doc. Нам повезло, и у нас уже есть функция, которая делает то же самое: row. Все, что нам нужно сделать, это отобразить эту функцию в нашем списке. Мы почти закончили, но теперь у нас есть список в списке, но нам нужен только список на выходе tbody . Чтобы решить эту проблему, мы просто вызываем Doc.Concat, чтобы сделать один список из списка списков. На этом мы закончили с нашей таблицей.

Нам все еще нужны четыре реактивные переменные для хранения наших новых входных данных. Давайте создадим их с помощью Var.Create :

let newEntryName = Var.Create "Name"
let newEntryLevel = Var.Create 0
let newEntryGold = Var.Create 0
let newEntryScore = Var.Create 0

На этом мы почти закончили. Последнее, что нам нужно, это новая запись в нашей таблице и построение шаблона. Наша функция для добавления новой записи в список достаточно мала, чтобы оставить ее в объявлении шаблона как лямбда-функцию.

Как я упоминал ранее, мы можем использовать наш шаблон с IndexTemplate.Main(), и мы можем получить доступ к каждому полю, которое мы пометили ws-*="Something" как .Something(). Что нам здесь нужно сделать?
- Заполнить ws-hole в нашем tbody
- Связать каждое поле ввода с его реактивной переменной
- Написать нашу функцию добавления на кнопку с ws-onlick="Add" (мы можем добавить новая запись в наши Игроки ListModel с функцией Add)

IndexTemplate.Main()
    .Data(data)
    .Name(newEntryName)
    .Level(newEntryLevel)
    .Gold(newEntryGold)
    .Score(newEntryScore)
    .Add(fun _ ->
        Entry.Create (newEntryName.Value) (newEntryLevel.Value) (newEntryGold.Value) (newEntryScore.Value)
        |> Players.Add
        |> ignore
)

Мы почти закончили! Единственное, что нам нужно сделать, это доработать наш шаблон, создать из него документ с функцией .Doc() и, как мы делали это в нашем первом проекте, поместить его в наш <div> с помощью id="main". Мы делаем это, вызывая функцию Doc.RunById "main" в нашем шаблоне.

Давайте посмотрим на все Client.fs в его окончательном виде:

Конечно, это не выглядит причудливо или профессионально, но с нашим готовым кодом F# нам нужно только изменить index.html с помощью шаблона и стилей, и он будет работать без необходимости повторной сборки всего проекта. Просто измените index.html и нажмите «Обновить» в браузере.

Если вы хотите проверить этот веб-сайт или хотите поиграть с кодом, вы можете сделать это в Try WebSharper. Перейдя по этой ссылке, вы можете увидеть результат написанного нами кода и модифицировать его, чтобы увидеть изменения в вашем браузере!



Обязательно оставьте 💚, это поможет мне узнать, какие истории вы хотите прочитать, и оставьте комментарий, если у вас есть какие-либо вопросы! Надеюсь, вам было интересно читать этот пост, и вы полюбили F# и WebSharper.