Меню в стиле Media Object Bootstrap
![]() |
Вот так выглядит меню на сайте библиотеки ABCLib. |
Сегодня попробуем реализовать с помощью ReactJS компонент, выглядящий так же, или, хотя бы похоже.
Предварительные условия
Вначале определимся с условиями.
-
Поскольку мы используем меню в стиле Bootstrap, компоненты Bootstrap, т.е. стили css, а также файлы js должны быть подключены в нашем основном
index.html.
-
Содержимое меню, т.е. заголовки, подзаголовки, названия изображений сгруппируем, в конечном итоге, во внешнем файле формата
json
и будем подгружать в процессе создания меню. И в самом деле, не стоит встраивать эти данные в тело компонента. -
Необходимо, чтобы все компоненты, из которых состоит наше меню, т.е. файлы js, файлы css и файлы изображений были сгруппированы в одном месте.
Подготовка
За основу возьмем проект, описанный в предыдущей заметке Создание базового приложения react, или создадим новый:
create-react-app reactMedia
В папке public
должна быть подпапка vendor
, в которой размещается содержимое внешних подключаемых библиотек. Внутри vendor присутствуют две папки: js
и css
, в которых находятся предварительно скачанные компоненты Bootstrap. Обращаем внимание на наличие popper.min.js и jquery.
Этот файлы тоже необходимы. Подробно об этом написано здесь Создание базового приложения react.
Я не буду подробно расписывать процесс написания компонента, просто покажу исходный код и коротко опишу какие части кода за что отвечают. В первоначальном варианте пункты меню мы разместим в теле компонента, в окончательном - вынесем в отдельный файл. Все файлы компонента мы разместим в папке src/media
. У нас уже есть папка src и в ней находится пока единственный файл - index.js
.
Создаём подпапку в папке src
:
mkdir src/media
mkdir src/media/images
Разместим в папке src/media/images
несколько предварительно подготовленных изображений в формате png.
В моём случае это файлы, содержащие изображения размером 64x64 пикселя или 128x128 пикселей.
В папке src/media создаём файл MediaList.js
со следующим содержимым:
MediaList.js
import React, { Component } from 'react';
import MediaItem from "./MediaItem";
import './medialist.css';
class MediaList extends Component {
constructor(props) {
super(props);
this.state = {
lists: [
{
caption: 'Populars',
description: 'Go to Populars page',
png: 'populars',
href: '/populars'
},
{
caption: 'Authors',
description: 'Go to Authors page',
png: 'authors',
href: '/authors'
},
{
caption: 'Genres',
description: 'Go to Genres page',
png: 'genres',
href: '/genres'
},
{
caption: 'Series',
description: 'Go to Series page',
png: 'series',
href: '/series'
},
{
caption: 'Favorites',
description: 'Go to Favorites page',
png: 'favorites',
href: '/favorites'
},
{
caption: 'Basket',
description: 'Go to Basket page',
png: 'basket',
href: '/basket'
}
]
}
}
render() {
const classes = ["navigation", "text-center"].join(' ');
return (
<div>
<h4 className={classes}>Navigation</h4>
{this.state.lists.map((list) => {
return <MediaItem
key={list.key}
caption={list.caption}
description={list.description}
png={list.png}
href={list.href}
/>
})}
</div>
)
}
}
export default MediaList;
В верхней части файла размещаются необходимые импорты.
import React, { Component } from 'react';
import MediaItem from "./MediaItem";
import './medialist.css';
import MediaItem from "./MediaItem" и import './medialist.css'; рассмотрим несколько позже, а пока сосредоточимся на содержимом конструктора, в частности на элементе lists
:
constructor(props) {
super(props);
this.state = {
lists: [
{
caption: 'Populars',
description: 'Go to Populars page',
png: 'populars',
href: '/populars'
},
. . .
{
caption: 'Basket',
description: 'Go to Basket page',
png: 'basket',
href: '/basket'
}
]
}
}
Собственно, lists
и есть то самое меню, из которого состоит наш компонент.
В данном случае:
caption
- заголовок пункта меню,description
- подзаголовок,png
- css-класс изображения,href
- http-ссылка, вызываемая при нажатии на данный пункт меню.
Пунктов меню может быть сколько угодно.
render() {
const classes = ["navigation", "text-center"].join(' ');
return (
<div>
<h4 className={classes}>Navigation</h4>
{this.state.lists.map((list) => {
return <MediaItem
key={list.key}
caption={list.caption}
description={list.description}
png={list.png}
href={list.href}
/>
})}
</div>
)
}
Строка
const classes = ["navigation", "text-center"].join(' ');
решение проблемы множественных классов для следующего фрагмента:
<h4 className={classes}>Navigation</h4>
Самая интересная часть компонента здесь:
{this.state.lists.map((list) => {
return <MediaItem
key={list.key}
caption={list.caption}
description={list.description}
png={list.png}
href={list.href}
/>
})}
{this.state.lists.map((list) => { } )}
- по сути просто цикл для итерации списка lists
. Каждый элемент списка - list
участвует в создании пунктов меню <
MediaItem ... />
- следующей части нашего составного компонента, в которую в качестве свойств (props
) передаются такие данные как caption
, description
, png
(по сути просто название изображения) и http
-ссылка href.
Вот эта строка - import MediaItem from "./MediaItem"
- импортирует наш дочерний компонент <
.MediaItem />
MediaItem.js
Вот так выглядит код одиночного пункта Media object с сайта Bootstrap:
<div class="media">
<img class="mr-3" src="..." alt="Generic placeholder image">
<div class="media-body">
<h5 class="mt-0">Media heading</h5>
Cras sit amet nibh libero . . .
</div>
</div>
При встраивании в компонент мы этот код слегка модифицировали:
import React, { Component } from 'react';
import './mediaitem.css';
class MediaItem extends Component {
render() {
const classes = ["png", this.props.png, "_32x"].join(' ');
return (
<a className="media-it" href={this.props.href}>
<div className="media">
<i className={classes}/>
<div className='media-body'>
<h5 className="mt-0"> {this.props.caption}</h5>
<hr className="slim"/>
<span className="text-muted">{this.props.description}</span>
</div>
</div>
</a>
);
}
}
export default MediaItem;
Во-первых, обернули его в тег <a></a>
, чтобы кликабельными были все части нашего компонента. В качестве свойства href
для тега <a> выступает {this.props.href}, переданный из родительского компонента. Этой же цели служат и другие свойства: {this.props.caption}, {this.props.description} и this.props.png, указывающий название изображения, как подкласс множественного класса const classes
.
mediaitem.css
.png {
-moz-font-feature-settings: normal;
-moz-font-language-override: normal;
display: inline-block;
vertical-align: middle;
margin-right: 5px;
}
.populars {
background: url("./images/populars.png") no-repeat scroll center center transparent;
}
.authors {
background: url("./images/authors.png") no-repeat scroll center center transparent;
}
.genres {
background: url("./images/genres.png") no-repeat scroll center center transparent;
}
.series {
background: url("./images/series.png") no-repeat scroll center center transparent;
}
.favorites {
background: url("./images/favorites.png") no-repeat scroll center center transparent;
}
.basket {
background: url("./images/basket.png") no-repeat scroll center center transparent;
}
._32x {
width: 32px;
height: 32px;
background-size: 32px;
}
/* media objects */
a.media-it{
text-decoration: none;
color: #1b1e21;
}
.media {
padding: 5px;
cursor: pointer;
opacity: .7;
}
.media:hover {
opacity: 1;
background-color: #f0ede6;
}
.media > .png {
margin-top: 5px;
margin-right: 10px;
}
.media-body > h5 {
margin-top: 10px;
margin-bottom: 0;
font-size: 100%;
}
.media-body > span {
font-size: 85%;
}
hr.slim {
margin: 3px 0 0;
border-color: #d7d7d7;
width: 95%;
}
В самом начале располагается css
-класс .png
, тот самый первый элемент составного класса <i className={classes}/> в компоненте MediaItem
.
Ниже располагаются классы .populars
, .authors
и т.д., указывающие на конкретные изображения пунктов меню - второй элемент составного класса.
И еще ниже ._32x
- определяющий размер изображения - третий элемент составного класса.
Еще ниже расположены модификаторы, призванные улучшить вид меню и его поведение. В частности hr.slim
меняет "умолчательные" отступы, переопределяя свойство margin
, и т.д.
medialist.css
.navigation {
border-bottom: 1px solid #d7d7d7;
margin-bottom: 0;
padding: 10px 0 3px;
}
Правильнее было бы объединить содержимое этого файла с mediaitem.css
. Но так будет нагляднее.
src/index.js
На этом собственно все. Осталось проверить работу нашего компонента. Но, предварительно заглянем в файл src/index.js
. Его первоначальный вид таков:
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(
<h1>Hello, world!</h1>,
document.getElementById('root')
);
Нам необходимо импортировать в этот файл наш компонент MediaList
. Ниже импортов добавляем строку
import App from './media/MediaList';
а вместо
<h1>Hello, world!</h1>,
вписываем следующее:
<App />,
Вот так должен выглядеть файл index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './media/MediaList';
ReactDOM.render(
<App />,
document.getElementById('root')
);
Что мы сделали?
По сути, создали еще один компонент <App
/>
, который включает в себя компонент <
MediaList />, и который в свою очередь состоит из списка компонентов <
MediaItem />
.
Осталось проверить работу:
Выглядит очень похоже!
Одним из наших условий было обещание вынести список с пунктами меню в отдельный файл. Именно этим сейчас и займёмся.
MediaListJSON.js
Создадим в src/media файл с названием MediaListJSON.js
. Скопируем в него содержимое MediaList.js
.
В папке public создадим подпапку jsondata
, а в ней файл medialist.json
. В этот файл вставим следующие данные, вырезанные (cut) из MediaListJSON.js
:
[
{
caption: 'Populars',
description: 'Go to Populars page',
png: 'populars',
href: '/populars'
},
{
caption: 'Authors',
description: 'Go to Authors page',
png: 'authors',
href: '/authors'
},
{
caption: 'Genres',
description: 'Go to Genres page',
png: 'genres',
href: '/genres'
},
{
caption: 'Series',
description: 'Go to Series page',
png: 'series',
href: '/series'
},
{
caption: 'Favorites',
description: 'Go to Favorites page',
png: 'favorites',
href: '/favorites'
},
{
caption: 'Basket',
description: 'Go to Basket page',
png: 'basket',
href: '/basket'
}
]
Это наш список пунктов меню.
Отредактируем MediaListJSON.js
, приведя его к следующему виду:
import React, { Component } from 'react';
import MediaItem from "./MediaItem";
const URL_DATA = "/jsondata/medialist.json";
class MediaListJSON extends Component {
constructor(props) {
super(props);
this.state = { data: [] };
}
componentDidMount() {
fetch(URL_DATA)
.then(request => request.json())
.then(data => { this.setState({data: data });
});
}
render() {
return (
<div>
{this.state.data.map((data) => {
return <MediaItem
key={data.key}
caption={data.caption}
description={data.description}
png={data.png}
href={data.href}
/>
})}
</div>
)
}
}
export default MediaListJSON;
Что мы сделали?
Во-первых, переместили список с пунктами меню во внешний файл public/jsondata/medialist.json
.
Во-вторых, поменяли имя компонента MediaList
на MediaListJSON
в файле MediaListJSON.js
.
Дополнительно, добавили const URL_DATA
, которой присвоили адрес с местонахождением medialist.json
.
И, наконец, в тело компонента добавили следующий кусок кода:
componentDidMount() {
fetch(URL_DATA)
.then(request => request.json())
.then(data => { this.setState({data: data });
});
}
componentDidMount():
вызывается после рендеринга компонента. Здесь можно выполнять запросы к удаленным ресурсам, что мы собственно и сделали, загрузив после отрисовки компонента содержимое medialist.json
в переменную со списком data
.
src/index.js
Поскольку мы изменили наименование компонента необходимо выполнить соответствующее изменение в src/index.js
. Заменим строку:
import App from './media/MediaList';
на следующее содержимое:
import App from './media/MediaListJSON';
Осталось убедиться что все работает.
На этом все!