Сегодня мы продолжим переписывание на $mol этой демки. Кто не читал первую часть, рекомендую сначала ознакомиться с ней BALLSORT на $mol. Часть 1

Напомню задачу

gif
gif

Экраны

  • Start - стартовый экран на котором отображается заголовок, кнопка для запуска игры, и подвал с cсылками

  • Game - при клике на кнопку запуска, открывается экран с игрой, на котором необходимо сортировать шарики. В хедере находятся кнопки возврата на стартовый экран и рестарта игры, а также счетчик числа сделаyных шагов. В центре трубки с шарами. В подвале те же ссылки что и на первом экране.

  • Finish - когда шарики отсортированы, поверх второго экрана отображается третий экран. На нем находится заголовок "You won!", количество сделанных шагов, и кнопка "New game" которая открывает стартовый экран.

Механика игры

  1. Рисуются 6 трубок, четыре и них заполнены шарами и две пустые

  2. В заполненных трубках находятся по 4 шара, четырех разных цветов

  3. При клике на непустую трубку, она переходит в активное состояние

    • В активном состоянии верхний шар в трубке переносится на ее крышку

  4. Повторный клик по активной трубке дезактивирует ее, шар переносится обратно в нее

  5. После активации трубки, клик по другой трубке переносит шар с крышки в другую трубку при условии, что другая трубка пуста или верхний шар другой трубки такого же цвета как шар на крышке активной трубке

  6. Когда в одной и трубок все 4 шара одного цвета она переходит в статус готово, после этого шары в нее/из нее перемещать нельзя.

  7. Игра закончится, когда 4 трубки перейдут в статус готово.

Отображение

Мы создадим отдельные модули для отображения:

  • Ссылки

  • Кнопки

  • Шара

  • Трубки

А затем соберем все в модуле app.

link

Создайте директорию 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, который наследуется от базового класса view-компонент $mol_view. Имя тега изменено на a, у dom-ноды установлены два аттрибута href и target на которые забиндены одноименные свойства, а в качестве ребенка dom-ноды выводим строку из свойства title.

Отрисуем этот компонент. Откройте в браузере ссылку http://127.0.0.1:9080/hype/ballsort/app/-/test.html - это модуль приложения, в котором находится файл 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 - это view-компонент из мола, для отображения вертикального списка, временно воспользуемся им.

Заглянем в браузер:

ссылка
ссылка

Добавим стилей, создайте в link файл link.view.css.ts

namespace $.$$ {
	
	$mol_style_define( $hype_ballsort_link, {
		
		color: 'lightgray',
		padding: ['0.25rem', '1rem'],
		
	} )
	
}

Про css.ts можно почитать тут: Каскадные стили компонент, Продвинутый CSS-in-TS, $mol_style readme.md

Готово, компонент ссылки теперь имеет необходимый функционал и выглядит также как в оригинальном приложении.

button

Проделываем все тоже самое для компонента кнопки.

Файл 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',
		},

	} )

}
Кнопка со стилями
Кнопка со стилями

ball

Теперь создадим компонент для шара. Имя $hype_ballsort_ball уже занято в классе модели, view-шку шара поместим в $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

Нам осталось создать компонент для трубки. По аналогии с шаром, создайте файл tube/view/view.view.tree

Сделаем его на основе $mol_list, т.к. он состоит из двух вертикальных частей

  • крышка

  • сама трубка с шариками, которая тоже на $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 будет возвращать view-ку шара

  • <= Balls $mol_list - в свойстве Balls подкомпонент на основе $mol_list будет отображать шары в трубке

  • style * min-height \10rem - минимальную высоту указываем через style

  • attr * data-complete <= complete false - чтобы отобразить состояние готово будем использовать data-аттрибут

  • rows <= balls / - у подкомпонента Balls свойство rows заменяем на наше свойство balls которое будет возвращать массив view-шек шаров

Про последнюю часть скажу отдельно.

<= Ball*0 $hype_ballsort_ball_view
	ball <= ball* $hype_ballsort_ball

Свойство Ball - это фабрика, которая в сгенерированном классе пометиться декоратором $mol_mem_key. Т.е. она будет создавать и возвращать инстансы view-шек шаров точно также как мы делали это руками в $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()
		}

	}

}

title

Создадим подкомпонент для отображения заголовка.

Сам заголовок
Сам заголовок

Его не будем выносить в отдельный модуль. Добавим его как подкомпонент в 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

Теперь мы можем собрать экраны, удалим лишнее из 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 заготовки для страниц

Start_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 \https://github.com/PavelZubkov/ballsort
							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

Перейдем к странице игры. Она состоит из трех вертикальных блоков

  • Кнопки управления + вывод количества шагов

  • Трубки с шариками

  • Те же ссылки что и на главной странице

Должно быть что-то такое:

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 у нас объявлено свойство game, которое хранит экземпляр класса игры. Во 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() ]
		}

	}

}
Переходы между экранами
Переходы между экранами

Трубки и шары

Теперь пришла очередь отрисовать трубки с шарами. Нам надо взять список трубок из игры и вывести его обернув каждую модель трубки во view-компонент.

Изменим 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 на основе view-компонента трубки, и тут же настраиваем его подменяя свойства 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, чтобы оно брало список трубок из модели игры и оборачивало во view-компонент трубки. Изменим 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',
		},

	} )

}
Экран игры
Экран игры

Finish_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 в последнем ассерте.

Тест работает
Тест работает
  • Полные исходники можно найти тут

  • Приложение здесь

По всем вопросам можно идти сюда.

Актуальный оригинал на $hyoo_page

Комментарии (3)


  1. nin-jin
    12.06.2023 17:00
    +3

    Стоит отметить, что в $mol нет необходимости велосипедить свои кнопки, ссылки и тд - можно взять стандартные $mol_link и $mol_button и настроить их вид по своему вкусу.

    Комментарии лучше писать внутри узла данных, чтобы они не парсились в AST:

    - \Любой, даже не валидный, текст

    min-height лучше не задавать руками, чтобы случайно не сломать работу виртуализации, так как он вычисляется автоматически, на основе контента.

    Вместо camelCase имён CSS-свойств лучше использовать строго-типизированные вложенные структуры:

    // justify-content: stretch
    justify: {
      content: 'stretch',
    },


  1. FranCOder
    12.06.2023 17:00

    Хватит отмечать хабы с другими фреймворками\библиотеками, Вам сделали хаб mol его только и выбирайте. Я не хочу видеть в ленте у себя этого франкенштейна


    1. PavelZubkov Автор
      12.06.2023 17:00

      Ок