Сегодня мы продолжим переписывать $mol Этот демо. Если вы не читали первую часть, я рекомендую вам сначала прочитать BALLSORT на $mol. Часть 1
Contents
Напомню задание
Экраны
-
Старт — стартовый экран с заголовком, кнопкой запуска игры и нижним колонтитулом со ссылками.
-
Игра – при нажатии на кнопку запуска открывается экран с игрой, где нужно рассортировать шарики. В шапке расположены кнопки возврата на стартовый экран и повторного запуска игры, а также счетчик количества пройденных шагов. В центре трубочка с шариками. В футере те же ссылки, что и на первом скрине.
-
Готово – когда шары будут отсортированы, над вторым экраном появится третий экран. Он содержит заголовок «Вы выиграли!», количество пройденных шагов и кнопку «Новая игра», которая открывает стартовый экран.
игровая механика
-
Нарисовано 6 трубочек, четыре из них заполнены шариками, а две пусты.
-
Заполненные тубы содержат 4 шарика 4 разных цветов.
-
Когда вы нажимаете на непустую трубку, она переходит в активное состояние.
-
Новый щелчок по активной трубке деактивирует ее, мяч переносится обратно в нее
-
После активации пробирки нажатие на другую пробирку переносит шарик с крышки на другую пробирку, при условии, что другая пробирка пуста или верхний шарик другой пробирки того же цвета, что и шарик на крышке активной пробирки.
-
Когда все 4 шарика одного цвета находятся в трубке, трубка переходит в состояние готовности, после чего шарики нельзя задвигать/вынимать из нее.
-
Игра закончится, когда будут готовы 4 трубки.
Отображать
Создадим отдельные модули для отображения:
-
Соединения
-
Кнопки
-
Мера
-
трубка
А потом мы объединим все это в модуль app
.
связь
Создать каталог ballsort/link
и классифицировать его link.view.tree
.
$hype_ballsort_link $mol_view
dom_name \a
attr *
href <= href \
target <= target \_self
sub / <= title \
view.tree — это DSL, прежде чем продолжить, я рекомендую вам прочитать эти книги: Состав компонентов, Декларативный состав компонентов
Прочитав документы по ссылкам выше, вы понимаете, что мы описали класс $hype_ballsort_link
который унаследован от базового класса компонента представления $mol_view
. Название тега изменено на a
узел dom имеет два определенных атрибута href
И target
на который привязаны одноименные свойства, и как дочерний элемент узла dom мы производим строку из свойства title
.
Нарисуем этот компонент. Открыть ссылку в браузере это модуль приложения, который содержит файл
index.html
. На экране отображается только домашняя линия.
Редактировать файл app/app.view.tree
$hype_ballsort_app $mol_view
sub /
<= Components $mol_list
rows /
<= Link $hype_ballsort_link
title \Ссылка
href \example.com
target \_blank
$mol_list — это компонент представления торгового центра, для отображения вертикального списка мы будем использовать его временно.
Смотрим в браузере:

Добавляйте стили, создавайте в link
подать link.view.css.ts
namespace $.$$ {
$mol_style_define( $hype_ballsort_link, {
color: 'lightgray',
padding: ['0.25rem', '1rem'],
} )
}
В отношении
css.ts
можно прочитать здесь: Каскадные стили компонентов, Расширенный CSS в TS, $mol_style readme.md
Готово, компонент ссылки теперь имеет необходимую функциональность и выглядит так же, как в исходном приложении.
кнопка
Мы делаем то же самое для компонента кнопки.
В файл ballsort/button/button.view.tree
:
$hype_ballsort_button $mol_view
dom_name \button
sub / <= title \
event *
click? <=> click? null
Поднесите кнопку к app
:
$hype_ballsort_app $mol_view
sub /
<= Components $mol_list
rows /
<= Link $hypr_ballsort_link
title \Ссылка
href \example.com
target \_blank
<= Button $hype_ballsort_button
title \Кнопка
Убеждаемся, что кнопка подтянута:

Добавление стилей в файл button.view.css.ts
:
namespace $.$$ {
$mol_style_define( $hype_ballsort_button, {
width: 'fit-content',
backgroundColor: 'white',
color: 'black',
padding: ['0.6rem', '1rem'],
fontSize: '1.3rem',
margin: [0, '0.2rem'],
border: {
width: '2px',
style: 'solid',
color: 'lightgray',
},
cursor: 'pointer',
position: 'relative',
':hover': {
backgroundColor: '#f1f1f1',
},
':focus': {
outline: 'none',
boxShadow: '0 0 0 4px lightblue',
borderColor: 'lightblue',
},
} )
}

мяч
Теперь давайте создадим компонент для мяча. Имя $hype_ballsort_ball
уже занят в классе модели, поместим рамку вида шара в $hype_ballsort_ball_view
.
Создать файл ballsort/ball/view/view.view.tree
Комментарии в view.tree начинаются со знака минус
$hype_ballsort_ball_view $mol_view
- Компонент шара будет принимать модель шара, из которой он достает цвет
ball $hype_ballsort_ball
- Для раскраски шара будет использоваться радиальный градиент из двух цветов
style *
--main-color <= color_main \
--light-color <= color_light \
- Цвета заранее заготовлены в массиве, такие же как в оригинальном приложении
- Всего предусмотрено 12 цветов, индексы от 0 до 11
- цвет по индексу 0 - основной цвет - color_main
- цвет по индексу 0 + 1 - второй цвет - color_light
colors /
\#8F7E22
\#FFE600
\#247516
\#70FF00
\#466799
\#00B2FF
\#29777C
\#00FFF0
\#17206F
\#4A72FF
\#BABABA
\#FFFFFF
\#4C3283
\#9D50FF
\#8B11C5
\#FF00F5
\#9D0D41
\#FF60B5
\#4B0000
\#FF0000
\#79480F
\#FF7A00
\#343434
\#B1B1B1
Рисуем шарик в app
$hype_ballsort_app $mol_view
sub /
<= Components $mol_list
rows /
<= Link $hype_ballsort_link
title \Ссылка
href \example.com
target \_blank
<= Button $hype_ballsort_button
title \Кнопка
<= Ball $hype_ballsort_ball_view
И мы его не видим, но он есть

Добавьте к нему стили, создайте файл ball/view/view.view.css.ts
namespace $.$$ {
$mol_style_define( $hype_ballsort_ball_view, {
width: '2rem',
height: '2rem',
boxSizing: 'content-box',
border: {
radius: '50%',
width: '2px',
style: 'solid',
color: 'black',
},
margin: '1px',
position: 'relative',
backgroundImage: 'radial-gradient(circle at 65% 15%, white 1px, var(--light-color) 3%, var(--main-color) 60%, var(--light-color) 100%)',
} )
}

Теперь нам нужно научить мяч принимать правильные цвета, давайте добавим немного логики. Создать файл view.view.ts
.
namespace $.$$ {
export class $hype_ballsort_ball_view extends $.$hype_ballsort_ball_view {
// В свойстве ball хранится инстанс модели шара
// из модели достаем цвет `color()` и умножаем на 2
// чтобы получить правильный индекс в массиве цветов
color_index() {
return this.ball().color() * 2
}
// Достаем из массива основной цвет по посчитанному индексу
// На случай если нам пришел индекс выходящий за массив с цветами
// выводим красный цвет
color_main() {
return this.colors()[ this.color_index() ] ?? 'red'
}
// Достаем второй цвет по индексу + 1
// и устанавливаем значение по умолчанию
color_light() {
return this.colors()[ this.color_index() + 1 ] ?? 'white'
}
}
}
А так как в модели по умолчанию цвет равен 0, мы видим первый цвет массива

трубка
Нам еще нужно создать компонент для трубы. По аналогии с шариком создаем файл tube/view/view.view.tree
Сделаем это на основе $mol_list
потому что он состоит из двух вертикальных частей
$hype_ballsort_tube_view $mol_list
tube $hype_ballsort_tube
active false
event *
click? <=> click? null
rows /
<= Roof $mol_view sub / <= roof null
<= Balls $mol_list
style * min-height \10rem
attr *
data-complete <= complete false
rows <= balls /
<= Ball*0 $hype_ballsort_ball_view
ball <= ball* $hype_ballsort_ball
-
tube $hype_ballsort_tube
– так же, как компонент шара, он будет хранить модель трубы -
active false
– свойство типаboolean
нужно показать активацию -
event * click? <=> click? null
– биндим свойствоclick
по событию клика -
rows /
– отображать детей на$mol_list
предоставленное имуществоrows
Но нетsub
как$mol_view
-
<= Roof $mol_view sub / <= roof null
– в собственностиRoof
подкомпонент будет найден$mol_view
который отображает содержимое свойстваroof
– это по умолчаниюnull
. Но когда трубка активированаroof
вернет взгляд на мяч -
<= Balls $mol_list
– в собственностиBalls
на основе подкомпонентов$mol_list
покажет шарики в трубе -
style * min-height \10rem
– минимальная высота указанаstyle
-
attr * data-complete <= complete false
– для отображения состояния готовности мы будем использовать атрибут данных -
rows <= balls /
– в подкомпонентеBalls
свойствоrows
заменить на нашу собственностьballs
который вернет массив шаров просмотра
О последней части я расскажу отдельно.
<= Ball*0 $hype_ballsort_ball_view
ball <= ball* $hype_ballsort_ball
Свойство Ball
– это фабрика, которая в сгенерированном классе будет помечена декоратором $mol_mem_key
. Те. он будет создавать и возвращать экземпляры шаров просмотра так же, как мы делали это вручную в $hype_ballsort_game
. Кроме того, свойство созданного мгновения будет изменено ball
к нашему.
Пример из модели:
@$mol_mem_key
Tube( index: number ) {
const obj = new $hype_ballsort_tube
obj.size = () => this.tube_size()
return obj
}
И это будет сгенерировано из view.tree
описания:
@ $mol_mem_key
Ball(id: any) {
const obj = new this.$.$hype_ballsort_ball_view()
obj.ball = () => this.ball(id)
return obj
}
Поднимите трубку для app
:
$hype_ballsort_app $mol_view
sub /
<= Components $mol_list
rows /
<= Link $hype_ballsort_link
title \Ссылка
href \example.com
target \_blank
<= Button $hype_ballsort_button
title \Кнопка
<= Ball $hype_ballsort_ball_view
<= Tube $hype_ballsort_tube_view
balls /
<= Ball1 $hype_ballsort_ball_view
color_index 2
<= Ball2 $hype_ballsort_ball_view
color_index 4
<= Ball3 $hype_ballsort_ball_view
color_index 6
И переопределить его собственность balls
видеть пули. А чтобы шарики были разного цвета, для каждого шарика переопределяем свойство color_index
.

Создать файл tube/view/view.view.css.ts
namespace $.$$ {
$mol_style_define( $hype_ballsort_tube_view, {
// В оригинальном приложении box-sizing = content-box
// а у $mol_view по дефолту стоит border-box
// поэтому меняем
boxSizing: 'content-box',
width: 'fit-content',
Roof: {
boxSizing: 'content-box',
height: '3rem',
alignItems: 'center',
justifyContent: 'center',
border: {
bottom: {
style: 'solid',
color: 'lightgray',
},
},
},
Balls: {
boxSizing: 'content-box',
width: '3rem',
flex: {
direction: 'column-reverse',
},
justifyContent: 'flex-start',
alignItems: 'center',
border: {
width: '2px',
style: 'solid',
color: 'lightgray',
},
padding: {
bottom: '0.4rem',
top: '0.4rem',
},
borderRadius: '0 0 2.4rem 2.4rem',
'@': {
'data-complete': {
true: {
// Когда data-complete=true
backgroundColor: 'lightgray',
},
},
},
},
} )
}

В $hype_ballsort_app добавьте мяч в верхнюю часть трубки:
- ...
<= Tube $hype_ballsort_tube_view
balls /
<= Ball1 $hype_ballsort_ball_view
color_index 2
<= Ball2 $hype_ballsort_ball_view
color_index 4
<= Ball3 $hype_ballsort_ball_view
color_index 6
roof <= Ball4 $hype_ballsort_ball_view
color_index 8

Осталось только добавить поведение, создать файл tube/view/view.view.ts
namespace $.$$ {
export class $hype_ballsort_tube_view extends $.$hype_ballsort_tube_view {
// Шар на крышке
@ $mol_mem
roof() {
// Получаем индекс последнего шара, напомню что this.tube() возвращает модель трубки
// Через фабрику получаем инстанс компонента шара который возвращаем
// Или возвращаем null
const index = this.tube().balls().length - 1
return this.active() ? this.Ball( index ) : null
}
// Массив компонентов шаров, которые будут отображаться в трубке
@ $mol_mem
balls() {
// В зависимости от активности трубки получаем список моделей шаров
const last_ball = this.tube().balls().at(-1)
const list = this.active() ? [last_ball] : this.tube().balls()
// Превращаем его в список компонентов шаров
return list.map((_, index) => this.Ball(index))
}
// Получаем модель шара по индексу
ball(index: number) {
return this.tube().balls()[index]
}
// Вытаскиваем из трубки состояние статуса готово
complete() {
return this.tube().complete()
}
}
}
заголовок
Давайте создадим подкомпонент для отображения заголовка.

Мы не будем выносить его в отдельный модуль. Давайте добавим его как подкомпонент к app.view.tree
$hype_ballsort_app $mol_view
sub /
<= Components $mol_list
rows /
- ...
<= Title $mol_view
dom_name \h2
sub /
<= Title_begin $mol_view sub / \BALL
<= Title_end $mol_view sub / \SORT

Добавьте к нему стили, создайте файл app.view.css.ts
namespace $.$$ {
$mol_style_define( $hype_ballsort_app, {
Title: {
font: {
size: '3rem',
weight: 300,
},
},
Title_begin: {
textDecoration: 'underline',
},
} )
}

приложение
Теперь можем собрать экраны, убрать лишнее app.view.tree
и создайте основную структуру:
$hype_ballsort_app $mol_view
game $hype_ballsort_game
title \BALL SORT
Title $mol_view
dom_name \h2
sub /
<= Title_begin $mol_view sub / \BALL
<= Title_end $mol_view sub / \SORT
sub /
<= Start_page $mol_list
<= Game_page $mol_list
<= Finish_page $mol_list
-
game $hype_ballsort_game
– в собственностиgame
мы будем хранить текущий экземпляр игры -
title \BALL SORT
– что будет отображаться в шапке вкладки -
Start_page
,Game_page
,Finish_page
заготовки для страниц
Домашняя страница
И давайте сразу украсим заставку:
- ...
sub /
<= Start_page $mol_list
rows /
<= Title
<= Start $hype_ballsort_button
title \Start game
click? <=> start? null
<= Links $mol_view
sub /
<= Sources $hype_ballsort_link
title \Source Code
href \
target \_blank
<= Game_page $mol_list
<= Finish_page $mol_list
-
Сначала мы отображаем
Title
-
Затем кнопка «Пуск»
Start
щелчок по нему привязывается к свойствуstart
к которому мы добавим поведение позже -
И отображается блок со ссылками в свойстве
Links
Давайте посмотрим, как это выглядит

Добавим недостающие стили в app.view.css.ts
Я просто перетаскиваю их из оригинального приложения.
namespace $.$$ {
$mol_style_define( $hype_ballsort_app, {
fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Noto Sans, sans-serif, BlinkMacSystemFont, Helvetica, Arial, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol',
color: '#e1e1e1',
lineHeight: 'normal',
padding: {
top: '1rem',
},
justifyContent: 'center',
background: {
color: '#101526',
},
// Title, Title_begin ...
Links: {
padding: {
top: '1rem',
},
justifyContent: 'center',
flex: {
wrap: 'wrap',
},
},
Start_page: {
alignItems: 'center',
},
} )
}

страница_игры
Перейдем на страницу игры, она состоит из трех вертикальных блоков.
-
Кнопки управления + отображение количества шагов
-
Шариковые трубы
-
Те же ссылки, что и на главной странице
Должно быть что-то вроде этого:
Game_page
Control
Home - кнопка возврата на стартовый экран
Restart - кнопка перезапуска игры
Move - число с количеством шагов
Tubes - трубки с шариками
Links - ссылка
Добавьте это в app.view.tree
$hype_ballsort_app $mol_view
- ...
sub /
<= Start_page $mol_list
- ...
<= Game_page $mol_list
rows /
<= Control $mol_view
sub /
<= Home $hype_ballsort_button
title \←
click? <=> home? null
<= Restart $hype_ballsort_button
title \Restart
click? <=> start?
<= Tubes $mol_view
<= Links
<= Finish_page $mol_list

Как мы будем определять, запущена игра или нет?
В view.tree у нас объявлено свойство игры, в котором хранится экземпляр игрового класса. В view.ts мы переопределим его, сделаем изменяемым свойством и по умолчанию оно будет возвращать null
. Логика такова:
-
game
обратная связьnull
– показать заставку -
game
вернуть экземпляр игры – показать экран игры -
Нажатие кнопок запуска и перезагрузки поместит свойство
game
новая копия игры -
Нажатие кнопки «Назад» поместит
null
к собственностиgame
-
Чтобы понять, что игра окончена, в классе игры есть свойство
finish
мы будем использовать это
Как мы будем менять экран?
Теперь у нас есть все три экрана, отображаемые в свойстве sub
. В view.ts нам нужно переопределить свойство sub
чтобы он возвращал только один экран за раз.
Создать файл app.view.ts
помните фрагменты кода в VSCode, здесь вам нужен фрагмент logic
.
namespace $.$$ {
export class $hype_ballsort_app extends $.$hype_ballsort_app {
// Переопределяем свойство game
// Теперь оно изменяемое и nullable
@ $mol_mem
game(next?: $hype_ballsort_game | null) {
return next ?? null!
}
// Кнопки start и restart забиндены на свойство start
// Тут мы просто помещаем новый инстанс игры в свойство game
@ $mol_action
start() {
this.game( new $hype_ballsort_game )
}
// Кнопка возврата забиндена на свойство `home`
// Тут мы помещаем null в свойство game
@ $mol_action
home() {
this.game(null)
}
// Дети компонента $mol_view берутся из свойства sub
// Тут мы возвращаем нужный экран в зависимости состояния игры
@ $mol_mem
sub() {
if (!this.game()) return [ this.Start_page() ]
return [ this.game().finished() === false ? this.Game_page() : this.Finish_page() ]
}
}
}

Трубки и шарики
Теперь настала очередь рисовать трубочки с шариками. Нам нужно взять список труб из игры и отобразить его, завернув каждую модель трубы в компонент представления.
давай меняться app.view.tree
$hype_ballsort_app $mol_view
- ...
sub /
<= Start_page $mol_list
- ...
<= Game_page $mol_list
rows /
<= Control $mol_view
- ...
<= Tubes $mol_view
sub <= tubes /
<= Tube*0 $hype_ballsort_tube_view
tube <= tube* $hype_ballsort_tube
click? <=> tube_click*? null
active <= tube_active* false
<= Links
<= Finish_page $mol_list
Что здесь происходит:
-
<= Tubes $mol_view
– создаем подкомпонентTubes
на основе базового компонента$mol_view
и поместите его вrows /
к объекту в собственностиGame_page
-
sub <= tubes /
свойствоsub
ВTubes
заменяемый имуществомtubes
и установите его значение по умолчанию -
И как ценность мы заменяем фабричную собственность
Tube
в зависимости от компонента вида трубы, и сразу настроить его, изменив свойстваtube
,click
,active
Приведенный выше код преобразуется в этот код TS:
@ $mol_mem
Tubes() {
const obj = new this.$.$mol_view()
obj.sub = () => this.tubes()
return obj
}
tubes() {
return [
this.Tube("0")
] as readonly any[]
}
Мы должны переопределить tubes
так что он берет список каналов из игровой модели и оборачивает его в компонент представления канала. давай меняться app.view.ts
namespace $.$$ {
export class $hype_ballsort_app extends $.$hype_ballsort_app {
// ...
@ $mol_mem
tubes() {
return this.game().tubes().map( ( _, index ) => this.Tube( index ) )
}
}
}

Добавить реализацию для свойств tube
, tube_click
, tube_active
что мы описали в view.tree
tube <= tube* $hype_ballsort_tube
click? <=> tube_click*? null
active <= tube_active* false
давай меняться app.view.ts
Снова:
namespace $.$$ {
export class $hype_ballsort_app extends $.$hype_ballsort_app {
// ...
@ $mol_mem
tubes() {
return this.game().tubes().map( ( _, index ) => this.Tube( index ) )
}
// По индексу достаем инстанс модели трубки из игры
// декротар тут можно опустить
tube( index: number ) {
return this.game().Tube(index)
}
// По клику вызываем tube_click в игре
// Передавая туда трубку по которой кликнули
@ $mol_action
tube_click( index: number ) {
this.game().tube_click( this.tube(index) )
}
// Проверяем активна ли текущая трубка
@ $mol_mem_key
tube_active( index: number ) {
return this.game().tube_active() === this.tube(index)
}
}
}

Напечатаем количество шагов. давай меняться app.view.tree
$hype_ballsort_app $mol_view
- ...
sub /
<= Start_page $mol_list
- ...
<= Game_page $mol_list
rows /
<= Control $mol_view
sub /
<= Home $hype_ballsort_button
title \←
click? <=> home? null
<= Restart $hype_ballsort_button
title \Restart
click? <=> start?
- Тут добавим Moves
<= Moves $mol_view
sub / <= moves \Moves: {count}
- ...
<= Finish_page $mol_list
А в view.ts переопределим свойство moves
– moves \Moves: {count}
заменять {count}
по количеству шагов
namespace $.$$ {
export class $hype_ballsort_app extends $.$hype_ballsort_app {
// ...
@ $mol_mem
moves() {
return super.moves().replace( '{count}', `${ this.game().moves() }` )
}
}
}

И добавьте стили в app.view.css.ts
namespace $.$$ {
$mol_style_define( $hype_ballsort_app, {
// ...
Moves: {
padding: ['0.6rem', '0.4rem'],
fontSize: '1.3rem',
},
Tubes: {
justifyContent: 'center',
},
Control: {
justifyContent: 'center',
},
Tube: {
margin: '1rem',
},
} )
}

end_page
Осталось только добавить финишный экран. давай меняться app.view.tree
:
$hype_ballsort_app $mol_view
- ...
sub /
- ...
<= Finish_page $mol_list
rows /
<= Control
<= Tubes
<= Links
<= Finish $mol_list
rows /
<= Finish_title $mol_view
dom_name \h1
sub / \You won!
<= Finish_moves $mol_view
dom_name \h2
sub / \In 16 moves
<= Finish_home $hype_ballsort_button
title \New game
click? <=> home?
Экран прибытия, отображается в верхней части игрового экрана. Control
, Tubes
, Links
и после завершения регистраций и кнопки.
Сразу добавить стили для него в app.view.css.ts
namespace $.$$ {
$mol_style_define( $hype_ballsort_app, {
Finish: {
position: 'fixed',
bottom: 0,
top: 0,
left: 0,
right: 0,
background: {
color: $mol_style_func.rgba(255, 255, 255, 0.6),
},
backdropFilter: $mol_style_func.blur('6px'),
alignItems: 'center',
paddingTop: '5rem',
},
Finish_title: {
color: 'black',
textShadow: '0 0 2px white',
},
Finish_moves: {
color: 'black',
textShadow: '0 0 2px white',
margin: {
top: '1rem',
},
},
Finish_home: {
margin: {
top: '1rem',
},
},
} )
}

Протестируйте приложение
Давайте напишем тест, чтобы убедиться, что наши экраны меняются правильно. Создать файл app.view.test.ts
namespace $.$$ {
$mol_test({
"Screan changing"() {
const app = new $hype_ballsort_app
// По умолчанию должен показываться стартовый экран
$mol_assert_like(app.sub(), [app.Start_page()])
// Кликаем по кнопке старта и проверяем что теперь отображается экран игры
app.start()
$mol_assert_like(app.sub(), [app.Game_page()])
// Выиграем игру, просто установим всем шарам один цвет и проверим экран
app.game().balls().forEach(obj => obj.color(0))
$mol_assert_like(app.sub(), [app.Finish_page()])
},
})
}

Убедитесь, что тест работает, сломав его, заменив Finish_page
на Game_page
в последнем утверждении.

По любым вопросам вы можете обратиться здесь.