Меню в стиле Media Object Bootstrap

Заметки для себя
 menu-as-media-object.png
Вот так выглядит меню на сайте библиотеки 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 />.

Осталось проверить работу:

end-variant.png

 Выглядит очень похоже!

 

Одним из наших условий было обещание вынести список с пунктами меню в отдельный файл. Именно этим сейчас и займёмся.

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';

Осталось убедиться что все работает.

На этом все!