понедельник, 19 декабря 2022 г.

Текстовый редактор JavaScript Editor

Contenteditable – текстовый редактор

https://snipp.ru/php/contenteditable

Если добавить атрибут contenteditable к элементу, его содержимое становится доступно для редактирования пользователю, а с использованием JS и JQuery можно сделать простой WYSIWYG редактор.

<div class="editor" contenteditable="true"></div>
HTML

JS-метод document.execCommand(сommand, showDefaultUI, аrgument) применяет команды к элементу с атрибутом contenteditable, который находится в фокусе. Т.к. нет единого стандарта, логика команд может отличатся в разных браузерах так и их поддержка, описание команд на https://developer.mozilla.org.

Проверить доступность команды можно методом queryCommandSupported(сommand).

var enable = document.queryCommandSupported('paste');
if (enable) {
...
}
JS

Поддержка команд в браузере:

backColorheadingjustifyLeft
boldhiliteColorjustifyRight
contentReadOnlyincreaseFontSizeoutdent
copyindentpaste
createLinkinsertBrOnReturnredo
cutinsertHorizontalRuleremoveFormat
decreaseFontSizeinsertHTMLselectAll
deleteinsertImagestrikeThrough
enableInlineTableEditinginsertOrderedListsubscript
enableObjectResizinginsertUnorderedListsuperscript
fontNameinsertParagraphunderline
fontSizeinsertTextundo
foreColoritalicunlink
formatBlockjustifyCenteruseCSS
forwardDeletejustifyFullstyleWithCSS

Жирный

Включает и отключает у выделенного текста жирное начертание тегом <b>, IE использует тег <strong>.

document.execCommand('bold', false, null);
JS

Курсив

Тег <i>, IE использует <em>.

document.execCommand('italic', false, null);
JS

Подчеркнутый

Тег <u>.

document.execCommand('underline', false, null);
JS

Перечёркнутый

Тег <strike>.

document.execCommand('strikethrough', false, null);
JS

Верхний индекс

Тег <sup>.

document.execCommand('superscript', false, null);
JS

Нижний индекс

Тег <sub>.

document.execCommand('subscript', false, null);
JS

Маркированный список

Тег <ul>.

document.execCommand('insertUnorderedList', false, null);
JS

Нумерованный список

Тег <ol>.

document.execCommand('insertOrderedList', false, null);
JS

Заголовки, параграф, цитата и т.д.

formatBlock добавляет блочный тег вокруг выделенного текста.

document.execCommand('formatBlock', false, 'h1');
document.execCommand('formatBlock', false, 'h2');
document.execCommand('formatBlock', false, 'h3');
document.execCommand('formatBlock', false, 'p');
document.execCommand('formatBlock', false, 'blockquote');
document.execCommand('formatBlock', false, 'div');
JS

Горизонтальная линия

Тег <hr>.

document.execCommand('insertHorizontalRule', false, null);
JS

Изображение

var url = prompt('Введите адрес изображения', '');
document.execCommand('insertImage', false, url);
JS

Ссылка

var url = prompt('Введите URL', '');
document.execCommand('CreateLink', false, url);
JS

Удаление ссылки:

document.execCommand('unlink', false, null);
JS

Вставка текста

var text = prompt('Введите текст', '');
document.execCommand('insertText', false, text);
JS

Вставка HTML

var html = prompt('Введите HTML код', '');
document.execCommand('insertHTML', false, html);
JS

С помощью insertHTML можно обернуть выделенный текст тегом, например <pre>:

document.execCommand('insertHTML', false, '<pre>' + document.getSelection().toString() + '</pre>');
JS

Выравнивание текста

Добавляет родительскому блоку CSS-свойство text-align.

По левому краю:

document.execCommand('justifyLeft', false, null);
JS

По центру:

document.execCommand('justifyCenter', false, null);
JS

По правому краю:

document.execCommand('justifyRight', false, null);
JS

По ширине:

document.execCommand('justifyFull', false, null);
JS

Шрифты

По умолчанию команды установки шрифта добавляются тегом <font> c атрибутами facesizecolor, что не очень хорошо.

Команда styleWithCSS переключает режим, в место семантических тегов добавляются <span style="...">.

После установки шрифта нужно переключить styleWithCSS обратно т.к. он распространяется на другие теги (<b><i><s> и т.д.).

Название шрифта

document.execCommand('styleWithCSS', false, true);
document.execCommand('fontName', false, 'monospace');
document.execCommand('styleWithCSS', false, false);
JS

Размер:

Размер шрифта задаётся в условных единицах (1-7), в режиме styleWithCSS некоторые браузеры используют font-size: xx-smallx-smallsmallmediumlargex-largexx-large.

document.execCommand('styleWithCSS', false, true);
document.execCommand('fontSize', false, '5');
document.execCommand('styleWithCSS', false, false);
JS

Цвет:

document.execCommand('styleWithCSS', false, true);
document.execCommand('foreColor', false, '#eeeeee');
document.execCommand('styleWithCSS', false, false);
JS

Фон:

document.execCommand('styleWithCSS', false, true);
document.execCommand('hiliteColor', false, 'orange');
document.execCommand('styleWithCSS', false, false);
JS

Undo и Redo

Отмена последнего действия, Ctrl + z.

document.execCommand('undo', false, null);
JS

Повтор последнего действия, Ctrl + y.

document.execCommand('redo', false, null);
JS

Выделить всё

document.execCommand('selectAll', false, null);
JS

Удаление выделенного

document.execCommand('delete', false, null);
JS

Очистить стили

Удаляет инлайновые теги и атрибуты style, все блочные теги остаются.

document.execCommand('removeFormat', false, null);
JS

Вырезать

document.execCommand('cut', false, null);
JS

Копировать

document.execCommand('copy', false, null);
JS
<link href="/font-awesome.min.css" rel="stylesheet">
<script src="/jquery.min.js"></script>
<div class="toolbar">
<a href="#" class="toolbar-b fas fa-bold" title="Жирный"></a>
<a href="#" class="toolbar-i fas fa-italic" title="Курсив"></a>
<a href="#" class="toolbar-u fas fa-underline" title="Подчёркнутый"></a>
<a href="#" class="toolbar-s fas fa-strikethrough" title="Зачёркнутый"></a>
<a href="#" class="toolbar-sup fas fa-superscript" title="Верхний индекс"></a>
<a href="#" class="toolbar-sub fas fa-subscript" title="Нижний индекс"></a>
<a href="#" class="toolbar-ul fas fa-list-ul" title="Маркированный список"></a>
<a href="#" class="toolbar-ol fas fa-list-ol" title="Нумерованный список"></a>
<a href="#" class="toolbar-p" title="Параграф">p</a>
<a href="#" class="toolbar-h1" title="Заголовок">H1</a>
<a href="#" class="toolbar-hr" title="Горизонтальная линия">hr</a>
<a href="#" class="toolbar-blockquote fas fa-quote-right" title="Цитата"></a>
<a href="#" class="toolbar-img far fa-image" title="Изображение"></a>
<a href="#" class="toolbar-a fas fa-link" title="Ссылка"></a>
<a href="#" class="toolbar-unlink fas fa-unlink" title="Удаление ссылки"></a>
<a href="#" class="toolbar-html" title="Вставить html">HTML</a>
<a href="#" class="toolbar-text" title="Вставить текст">Text</a>
<br>
<a href="#" class="toolbar-left fas fa-align-left" title="по левому краю"></a>
<a href="#" class="toolbar-center fas fa-align-center" title="по центру"></a>
<a href="#" class="toolbar-right fas fa-align-right" title="по правому краю"></a>
<a href="#" class="toolbar-justify fas fa-align-justify" title="по ширине"></a>
<select class="toolbar-font">
<option selected="selected" disabled="disabled">Шрифт</option>
<option value="arial">Arial</option>
<option value="Courier New">Courier New</option>
<option value="georgia">Georgia</option>
<option value="impact">Impact</option>
<option value="roboto">Tahoma</option>
<option value="Times New Roman">Times New Roman</option>
<option value="verdana">Verdana</option>
</select>
<select class="toolbar-size">
<option selected="selected" disabled="disabled">Размер</option>
<option value="1">10px</option>
<option value="2">12px</option>
<option value="3">14px</option>
<option value="4">16px</option>
<option value="5">18px</option>
<option value="6">21px</option>
<option value="7">26px</option>
</select>
<span>Цвет</span> <input class="toolbar-color" type="color" value="#ff0000">
<span>Фон</span> <input class="toolbar-bg" type="color" value="#ffff00">
<br>
<a href="#" class="toolbar-undo fas fa-undo" title="Отмена"></a>
<a href="#" class="toolbar-redo fas fa-redo" title="Повтор"></a>
<a href="#" class="toolbar-delete far fa-trash-alt" title="Удалить"></a>
<a href="#" class="toolbar-selectAll">Выделить всё</a>
<a href="#" class="toolbar-removeFormat">Очистить стили</a>
<a href="#" class="toolbar-cut fas fa-cut" title="Вырезать"></a>
<a href="#" class="toolbar-copy fas fa-copy" title="Копировать"></a>
</div>
<div class="editor" contenteditable="true">...</div>
HTML
CSS
.toolbar a {
display: inline-block;
border: 1px solid #888;
padding: 5px 8px;
margin: 0 5px 10px 0;
color: #000;
border-radius: 3px;
font-size: 12px;
box-shadow: 1px 1px 2px #ddd;
background: #fff;
vertical-align: top;
text-decoration: none;
}
.toolbar select {
display: inline-block;
height: 28px;
line-height: 28px;
background: #fff;
padding: 0;
margin: 0 5px 10px 0;
color: #000;
box-shadow: 1px 1px 2px #ddd;
border-radius: 3px;
vertical-align: top;
  font-size: 12px;
}
.toolbar input {
display: inline-block;
height: 28px;
line-height: 28px;
background: #fff;
padding: 0;
margin: 0 5px 10px 0;
color: #000;
box-shadow: 1px 1px 2px #ddd;
border-radius: 3px;
vertical-align: top;
font-size: 12px;
}
.toolbar span {
display: inline-block;
height: 30px;
line-height: 30px;
padding: 0;
margin: 0 0 10px 0;
color: #000;
vertical-align: top;
font-size: 12px;
}
.editor {
min-height: 150px;
border: 1px solid #ddd;
padding: 10px;
border-radius: 2px;
box-shadow: 1px 1px 2px #ddd;
background: #fff;
}

JavaScript
// Жирный (b)
$('body').on('click', '.toolbar-b', function(){
document.execCommand('bold', false, null);
return false;
});
 
// Курсив (i)
$('body').on('click', '.toolbar-i', function(){
document.execCommand('italic', false, null);
return false;
});
 
// Подчёркнутый текст (u)
$('body').on('click', '.toolbar-u', function(){
document.execCommand('underline', false, null);
return false;
});
 
// Зачёркнутый текст (strike)
$('body').on('click', '.toolbar-s', function(){
document.execCommand('strikethrough', false, null);
return false;
});
 
// Верхний индекс (sup)
$('body').on('click', '.toolbar-sup', function(){
document.execCommand('superscript', false, null);
return false;
});
 
// Нижний индекс (sub)
$('body').on('click', '.toolbar-sub', function(){
document.execCommand('subscript', false, null);
return false;
});
 
// Маркированный список (ul)
$('body').on('click', '.toolbar-ul', function(){
document.execCommand('insertUnorderedList', false, null);
return false;
});
 
// Нумерованный список (ol)
$('body').on('click', '.toolbar-ol', function(){
document.execCommand('insertOrderedList', false, null);
return false;
});
 
// Параграф (p)
$('body').on('click', '.toolbar-p', function(){
document.execCommand('formatBlock', false, 'p');
return false;
});
 
// Заголовок (h1)
$('body').on('click', '.toolbar-h1', function(){
document.execCommand('formatBlock', false, 'h1');
return false;
});
 
// Горизонтальная линия (hr) 
$('body').on('click', '.toolbar-hr', function(){
document.execCommand('insertHorizontalRule', false, null);
return false;
});
 
// Цитата (blockquote)
$('body').on('click', '.toolbar-blockquote', function(){
document.execCommand('formatBlock', false, 'blockquote');
return false;
});
 
// Изображение (img)
$('body').on('click', '.toolbar-img', function(){
var url = prompt('Введите адрес изображения', 'https://snipp.ru/demo/526/image.jpg');
document.execCommand('insertImage', false, url);
return false;
});
 
// Ссылка (a)
$('body').on('click', '.toolbar-a', function(){
var url = prompt('Введите URL', '');
document.execCommand('CreateLink', false, url);
return false;
});
 
// Удаление ссылки
$('body').on('click', '.toolbar-unlink', function(){
document.execCommand('unlink', false, null);
return false;
});
 
// Вставить html
$('body').on('click', '.toolbar-html', function(){
var html = prompt('Введите HTML код', '');
document.execCommand('insertHTML', false, html);
return false;
});
 
// Вставить текст
$('body').on('click', '.toolbar-text', function(){
var text = prompt('Введите текст', '');
document.execCommand('insertText', false, text);
return false;
});
 
// Выравнивание текста по левому краю
$('body').on('click', '.toolbar-left', function(){
document.execCommand('justifyLeft', false, null);
return false;
});
 
// Выравнивание текста по центру
$('body').on('click', '.toolbar-center', function(){
document.execCommand('justifyCenter', false, null);
return false;
});
 
// Выравнивание текста по правому краю
$('body').on('click', '.toolbar-right', function(){
document.execCommand('justifyRight', false, null);
return false;
});
 
// Выравнивание по ширине
$('body').on('click', '.toolbar-justify', function(){
document.execCommand('justifyFull', false, null);
return false;
});
 
// Шрифт
$('body').on('input', '.toolbar-font', function(){
var val = $(this).val();
document.execCommand('styleWithCSS', false, true);
document.execCommand('fontName', false, val);
document.execCommand('styleWithCSS', false, false);
});
 
// Размер шрифта
$('body').on('input', '.toolbar-size', function(){
var val = $(this).val();
document.execCommand('styleWithCSS', false, true);
document.execCommand('fontSize', false, val);
document.execCommand('styleWithCSS', false, false);
});
 
// Цвет шрифта
$('body').on('input', '.toolbar-color', function(){
var val = $(this).val();
document.execCommand('styleWithCSS', false, true);
document.execCommand('foreColor', false, val);
document.execCommand('styleWithCSS', false, false);
});
 
// Цвет фона
$('body').on('input', '.toolbar-bg', function(){
var val = $(this).val();
document.execCommand('styleWithCSS', false, true);
document.execCommand('hiliteColor', false, val);
document.execCommand('styleWithCSS', false, false);
});
 
// Отмена
$('body').on('click', '.toolbar-undo', function(){
document.execCommand('undo', false, null);
return false;
});
 
// Повтор
$('body').on('click', '.toolbar-redo', function(){
document.execCommand('redo', false, null);
return false;
});
 
// Удалить
$('body').on('click', '.toolbar-delete', function(){
document.execCommand('delete', false, null);
return false;
});
 
// Выделить всё
$('body').on('click', '.toolbar-selectAll', function(){
document.execCommand('selectAll', false, null);
return false;
});
 
// Очистить стили
$('body').on('click', '.toolbar-removeFormat', function(){
document.execCommand('removeFormat', false, null);
return false;
});
 
// Вырезать
$('body').on('click', '.toolbar-cut', function(){
document.execCommand('cut', false, null);
return false;
});
 
// Копировать
$('body').on('click', '.toolbar-copy', function(){
document.execCommand('copy', false, null);
return false;
});





В большинстве браузерах при нажатии на Enter, новая строка начнется с преведущего блочного элемента (p, li, blockquote).

Если редактор был пуст, то вставится <div>, в место него можно использовать <p> вызвав команду:

document.execCommand('defaultParagraphSeparator', false, 'p');
JS

Если редактору сделать display: inline-block, то будут вставляться только <br>.

В contenteditable клавиша Tab не добавляет отступ, а переключает фокус на следующий элемент. Включить табуляцию можно, добавив CSS-свойство white-space: pre-wrap чтобы начали выводится пробельные символы, но при этом переносы строк тоже заработают.

.editor {
white-space: pre-wrap;
tab-size: 3; /* Ширина табуляции */
}
CSS

И обработчик нажатия клавиши на JQuery:

$('body').on('keydown', '.editor', function(e){
if (e.keyCode === 9) {
e.preventDefault();
var editor = this;
var doc = editor.ownerDocument.defaultView;
var sel = doc.getSelection();
var range = sel.getRangeAt(0);
var tabNode = document.createTextNode("\t");
range.insertNode(tabNode);
range.setStartAfter(tabNode);
range.setEndAfter(tabNode);
sel.removeAllRanges();
sel.addRange(range);
}
});
JS

Результат:

for (let i = 0; i < 10; i++) { if (i % 2) { alert( i ); } }


При вставки текста из буфера переносятся все стили и теги скопированные из HTML страницы или Word файла, это можно предотвратить очисткой:

function escape_text(text) {
var map = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;'};
return text.replace(/[&<>"']/g, function(m) {
return map[m];
});
}
$('body').on('paste', '.editor', function(e){
e.preventDefault();
var text = (e.originalEvent || e).clipboardData.getData('text/plain');
document.execCommand('insertHtml', false, escape_text(text));
});
JS
6
<div class="editor" contenteditable="true" placeholder="Введите текст"></div>
HTML
.editor[placeholder]:empty:before {
content: attr(placeholder);
color: #555;
}
.editor[placeholder]:empty:focus:before {
content: '';
}
CSS
$('body').on('focusout', '.editor', function() {
var element = $(this);
if (!element.text().replace(' ', '').length) {
element.empty();
}
});
JS

Показать исходный код можно заменив <div> на <textarea>.

<input type="checkbox" id="toggle-editor"> Показать исходный код
<div contenteditable="true" class="editor">
...
</div>
HTML
textarea.editor {
display: block;
width: 100%;
box-sizing: border-box;
white-space: pre-wrap;
}
CSS
$('#toggle-editor').click(function(){
var target = $('.editor');
if ($(this).is(':checked')){
target.replaceWith('<textarea class="editor">' + target.html() + '</textarea>');
} else {
target.replaceWith('<div contenteditable="true" class="editor">' + target.val() + '</div>');
}
});
JS

Для отправки текста вместе с формой нужно добавить скрытый <textarea>, при событии отправки формы скидывать в него содержимое редактора.

<form id="form" method="post" action="">
<div class="editor" contenteditable="true">...</div>
<textarea id="textarea" name="text" style="dispalay: none;"></textarea>
<input type="supmit" value="Отправить">
</form>
HTML

$('#form').on('submit', function(){
$('#textarea').val($('.editor').html());
return true;
});


Auto-Saving

To make the editor more user-friendly, we should add auto-saving. The first method automatically saves your work every five seconds.

1
setInterval(function() {
2
  for (var i = 0; i < editables.length; i++) {
3
    localStorage.setItem(editables[i].getAttribute('id'), editables[i].innerHTML);
4
  }
5
}, 5000);

You can also save the changes on every keydown event.

1
document.addEventListener('keydown', function(e) {
2
  for (var i = 0; i < editables.length; i++) {
3
    localStorage.setItem(editables[i].getAttribute('id'), editables[i].innerHTML);
4
  }
5
});

In this tutorial, I'm sticking with the latter method. You are free to trigger auto-save based on any event that seems appropriate in your projects.


Retrieving Saved Content

If you make changes to any of the elements in the previous demo and reload the page, you will notice that the changes you made are gone. This is because there is no code in place to retrieve the saved data. Once the content has been saved in localStorage, we need to retrieve it later when a user visits the webpage again.

1
if (typeof(Storage) !== "undefined") {
2
3
  if (localStorage.getItem('title') !== null) {
4
    editables[0].innerHTML = localStorage.getItem('title');
5
  }
6
  
7
  if (localStorage.getItem('author') !== null) {
8
    editables[1].innerHTML = localStorage.getItem('author');
9
  }
10
  
11
  if (localStorage.getItem('content') !== null) {
12
    editables[2].innerHTML = localStorage.getItem('content');
13
  } 
14
}

The code above checks if the title, author, or content already exist in localStorage. If they do, we set the innerHTML of the respective elements to the retrieved values.


Creating the Editor

To create the inline editor, you need to have the ability to change the value of the contentEditable attribute whenever a user decides to edit something.

While toggling the contentEditable attribute, it is necessary to know what value the attribute holds currently. To accomplish that, you can use the isContentEditable property. If isContentEditable returns true for an element, then the element is currently editable—otherwise it is not. We will use this property shortly to determine the state of various elements in our document.

First, we need to create a directory called contenteditable-editor. Inside that, create a new file called index.html. You can use this as a skeleton for your HTML file.

1
<!DOCTYPE html>
2
<html lang="en">
3
  <head>
4
    <meta charset="UTF-8" />
5
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
6
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
    <title>Text Editor</title>
8
  </head>
9
  <body></body>
10
</html>

The first step in building the editor is the creation of a button to toggle editing and some editable elements. Inside the <body> element, put this:

1
<button id="editBtn" type="button">Edit Document</button>
2
3
<div id="editor">
4
    <h1 id="title">A Nice Heading.</h1>
5
    <p>Last Edited By - <span id="author">Monty Shokeen</span></p>
6
    <p id="content">Some content that needs correction.</p>
7
</div>

Each element that we intend to keep editable needs to have its own unique Id. This will be helpful when we have to save the changes or retrieve them later to replace the text inside each element.

The following JavaScript code handles all the editing and saving.

1
const editBtn = document.getElementById('editBtn');
2
const editables = document.querySelectorAll('#title, #author, #content')
3
4
editBtn.addEventListener('click', function(e) {
5
  if (!editables[0].isContentEditable) {
6
    editables[0].contentEditable = 'true';
7
    editables[1].contentEditable = 'true';
8
    editables[2].contentEditable = 'true';
9
    editBtn.innerHTML = 'Save Changes';
10
    editBtn.style.backgroundColor = '#6F9';
11
  } else {
12
    // Disable Editing 
13
    editables[0].contentEditable = 'false';
14
    editables[1].contentEditable = 'false';
15
    editables[2].contentEditable = 'false';
16
    // Change Button Text and Color 
17
    editBtn.innerHTML = 'Enable Editing';
18
    editBtn.style.backgroundColor = '#F96';
19
    // Save the data in localStorage 
20
    for (var i = 0; i < editables.length; i++) {
21
      localStorage.setItem(editables[i].getAttribute('id'), editables[i].innerHTML);
22
    }
23
  }
24
});

You can put this code in a <script> tag at the bottom of the <body> tag. We use querySelectorAll() to store all the editable elements in a variable. This method returns a NodeList which contains all the elements in our document that are matched by specified selectors. This way, it's easier to keep track of editable elements with one variable. For instance, the title of our document can be accessed by using editables[0], which is what we will do next.

Next, we add an event listener to our button's click event. Every time a user clicks on the Edit Document button, we check if the title is editable. If it is not editable, we set the contentEditable property on each of the editable elements to true. Moreover, the text 'Edit Document' changes to 'Save Changes'. After users have made some edits, they can click on the 'Save Changes' button and the changes made can be saved permanently.

If the title is editable, we set the contentEditable property on each of the editable elements to false. At this point, we can also save the content of our document on the server to retrieve later or synchronize the changes to a copy that exists somewhere else. In this tutorial, I am going to save everything in localStorage instead. When saving the value in localStorage, I am using the Id of each element to make sure that I don't overwrite anything.

React Hooks

Как работают функциональные компоненты с хуками.


https://reactjs.org/docs/hooks-reference.html


---------------------------------------------------------------------

1) render

---------------------------------------------------------------------


Код тела функции компонента выполняется всегда полностью, включая объявление переменных и функций при:

- монтировании компонента,

- изменении на другие значения внешних props переданных в компонент, если значение не изменилось, то тело функции компонента не выполнится.

- при изменении значений внутреннего state компонента, но только если хотя бы одно из них изменилось на другое значение, если значение не изменилось,

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

   внутреннего state нужно выполнить такой хук:


function useForceUpdate () {

    const [value, setValue] = useState(0);

    return function () {

        setValue(function (previousValue) {

           return previousValue + 1;

        });

    };

}


const forceUpdate = useForceUpdate();


handleClick () {

    setStateValue(0);

    forceUpdate();

}


---------------------------------------------------------------------

2) hooks

---------------------------------------------------------------------


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

поскольку React нужно, чтобы они всегда вызывались и удалялись при размонтировании в определенном порядке.



---------------------------------------------------------------------

3) useContext

---------------------------------------------------------------------


Позволяет получить значение context всем компонентам потомкам сразу без необходимости передачи значения по цепочке потомков в каждый props.


Компонент, вызывающий useContext, всегда будет перерендериваться при изменении значения контекста.


Когда ближайший <MyContext.Provider> над компонентом обновляется, этот хук вызовет повторный рендер с последним значением контекста,

переданным этому провайдеру MyContext.


Контекст использует сравнение по ссылкам, чтобы определить, когда запускать последующий рендер. Поэтому повторный рендер дочернего компонента будет производиться

при каждом повторном рендере родительского Provider-компонента, если передаваемый им объект будет создаваться каждый раз. Один из вариантов решения этой проблемы - это передача вариантов

контекст только данных примитивов из родительского компонента.


useContext(MyContext) позволяет только читать контекст и подписываться на его изменения.

Обязательно необходимо прописывать<MyContext.Provider> выше в дереве компонентов, чтобы предоставить значение для этого контекста.


import React, {useContext} from 'react';


import ReactDOM from 'react-dom';


const themes = {

    light: {

        background: "#ffffff"

    },

    dark: {

        background: "#000000"

    }

};


const ThemeContext1 = React.createContext(themes.light); // значение по умолчанию

const ThemeContext2 = React.createContext(themes.dark); // значение по умолчанию


function Child (): JSX.Element {


    const theme1 = useContext(ThemeContext1);

    const theme2 = useContext(ThemeContext2);


    return (

        <>

            <div>{theme1.background}</div>

            <br />

            <div>{theme2.background}</div>

        </>

    );


}


function Parent (): JSX.Element {

    return (

        <ThemeContext1.Provider value={themes.light}>

            <ThemeContext2.Provider value={themes.dark}>

                <Child />

            </ThemeContext2.Provider>

        </ThemeContext1.Provider>

    );

}


---------------------------------------------------------------------

4) useState

---------------------------------------------------------------------


Хранит в себе внутреннее состояние компонента.


Можно 1 раз задавать первоначальное значение при монтировании компонента.


Если первоначальное значение не задано, то оно устанваливается в undefined. При этом программа выполняется далее.


В качестве первоначального значения при монтировании компонента можно устанавливать значение, взятое из внешнего props.

Но в этом случае при последующем изменении внешннего значения из props внутреннее значение в state само обновляться не будет.


Для обновления внутреннего значения его нужно будет обновиться с помощью вызова функции setState внутри useEffect, который будет реагировать на изменение

значения из внешнего props.


Для ручного обновления значения state вызывается функция setState.


В setState можно передавать новое значение для state или функцию для сложного расчета обновления внутреннего значения.

Переданная функция для обновления также имеет доступ к предыдущему состоянию state обновляемого значения.


Функция setState не имеет функции callback, которая могла бы быть выполнена после обновления занчения и ререндеринга компонента.

Функией callback в таком случае является выполнение useEffect, который будет реагировать на изменения значения state.

На этой основе можно создать функцию, которая будет имитировать setState with callback.


Функция useState будет всегда будет возвращать последнее установленное значение.


const [value, setValue] = useState(0);


const [value, setValue] = useState(props.parentValue);


const [value, setValue] = useState(function () {

    return props.parentValue + 1;

});


function handleClick () {

    setValue(1);

}


function handleClick () {

    setValue(function (previousValue) {

        return previousValue + 1;

    });

}


useEffect(function () {

    setValue(props.parentValue);

}, [props.parentValue]);


function useStateWithCallback (initialState, alwayExecuteCallback) {


    const [state, setState] = useState(initialState);


    const callbackRef = useRef(undefined);


    const setStateAndCallback = useCallback(function setStateAndCallback (newState, callback) {

        callbackRef.current = callback;

        setState(newState);

    }, []);


    useEffect(function (): void {

        if (callbackRef.current) {

            callbackRef.current(state);

            callbackRef.current = undefined;

        }

    }, alwayExecuteCallback ? undefined : [state]);


    return [state, setStateAndCallback];


}


const [value, setValue] = useStateWithCallback(0, false);


function handleClick () {

    setValue(1, function (newState) {

        console.log(newState); // 1

    });

}


---------------------------------------------------------------------

5) useReducer

---------------------------------------------------------------------


Альтернатива для useState.

Принимает функцию редюсер типа (state, action) => newState и возвращает текущее состояние в паре с методом dispatch.


Хук useReducer обычно предпочтительнее useState, когда у вас сложная логика состояния, которая включает в себя несколько значений,

или когда следующее состояние зависит от предыдущего.


import React, {useReducer} from 'react';


const initialState = {count: 0};


function reducer (state, action) {

    switch (action.type) {

        case 'increment': return {count: state.count + 1};

        case 'decrement': return {count: state.count - 1};

        default: throw new Error();

    }

}


function Child (): JSX.Element {


    const [state, dispatch] = useReducer(reducer, initialState);


    function handleMinusClick () {

        dispatch({type: 'decrement'});

    }


    function handlePlusClick () {

        dispatch({type: 'increment'});

    }


    return (

        <>

            <div>Count: {state.count}</div>

            <button onClick={handleMinusClick}>-</button>

            <button onClick={handlePlusClick}>+</button>

        </>

    );


}


---------------------------------------------------------------------

6) useEffect

---------------------------------------------------------------------


Вызывается после завершения рендера.


Функция вызывает переданную в неё функцию каждый раз при монтировании компонента.


Затем, если в неё передан массив с зависимыми значениями, то функция вызывается каждый раз при последующих рендерингах при передаче props или изменении

внутреннего state.


Если передан массив с зависимыми значениями, то функция вызывается каждый раз при последующих рендерингах и изменении любого значения из массива.


Значения в массиве сравниваются поверхностно через строго равенство ===. Поэтому переданные в массив объекты всегда будут не равны своим предыдущим версиям.

В этом случае переданная в useEffect функция будет вызываться всегда.


Если функция useEffect возращает через return какую-то функцию, то она будет вызываться всегда при размонтировании компонента и какждый раз перед выполнением

основной переданной функции, когда прийдет момент вызова той.


В React 18 useEffect будет вызываться 2 раза, имитируя монтирование, размонтирование и повторное монтирование компонента.


Поэтому useEffect не рекомендуется использовать во всех возможных ситуациях.


// componentDidMount 1 раз

useEffect(function () {

    function log () {console.log(1);}

    log();

}, []);


// componentDidMount 1 раз и всегда componentDidUpdate

useEffect(function () {

    function log () {console.log(1);}

    log();

});


// componentDidMount 1 раз и componentDidUpdate, когда изменяется любое значение в массиве зависимостей

useEffect(function () {

    function log () {console.log(props.one + props.two);}

    log();

}, [props.one, props.two]);


// componentDidMount 1 раз и componentDidUpdate, когда изменяется любое значение в массиве зависимостей, или componentWillUnmount

useEffect(function () {


    function log () {console.log(props.one + props.two);}

    const intervalId = setInterval(function () {

        log();

    }, 1000);


    return function () { // componentWillUnmount

        clearInverval(intervalId);

    };


}, [props.one, props.two]);


---------------------------------------------------------------------

7) useLayoutEffect

---------------------------------------------------------------------


Идентичен useEffect, но в отличии от useEffect этот хук запускается синхронно после всех изменений DOM.

Используйте его для чтения макета из DOM и синхронного повторного рендеринга.

Обновления, запланированные внутри useLayoutEffect, будут полностью применены синхронно перед тем, как браузер получит шанс осуществить отрисовку.


Предпочитайте стандартный useEffect, когда это возможно, чтобы избежать блокировки визуальных обновлений.


Если вы используете серверный рендеринг, имейте в виду, что ни useLayoutEffect, ни useEffect не могут работать до загрузки JavaScript.

Вот почему React предупреждает, когда серверный компонент содержит useLayoutEffect.

Чтобы справиться с данной проблемой, либо переместите эту логику в useEffect (если она не нужна для первого рендера),

либо задержите отображение этого компонента до тех пор, пока не выполнится рендеринг на стороне клиента (если HTML некорректный до запуска useLayoutEffect).


// componentDidMount 1 раз

useLayoutEffect(function () {

    function log () {console.log(1);}

    log();

}, []);


// componentDidMount 1 раз и всегда componentDidUpdate

useLayoutEffect(function () {

    function log () {console.log(1);}

    log();

});


// componentDidMount 1 раз и componentDidUpdate, когда изменяется любое значение в массиве зависимостей

useLayoutEffect(function () {

    function log () {console.log(props.one + props.two);}

    log();

}, [props.one, props.two]);


// componentDidMount 1 раз и componentDidUpdate, когда изменяется любое значение в массиве зависимостей, или componentWillUnmount

useLayoutEffect(function () {


    function log () {console.log(props.one + props.two);}

    const intervalId = setInterval(function () {

        log();

    }, 1000);


    return function () { // componentWillUnmount

        clearInverval(intervalId);

    };


}, [props.one, props.two]);


---------------------------------------------------------------------

8) useInsertionEffect

---------------------------------------------------------------------


Аналогичен useEffect, но в отличии от useEffect, который запускается после изменения DOM, useInsertionEffect выполняется синхронно до изменения DOM.


Используется для вставки CSS стилей в DOM до того, как проичтается layout страницы.


useInsertionEffect не имеет доступ к ref и не может выполняться по расписанию.


useInsertionEffect должен использоваться только для создания библотек для css-in-js.


Вместо useInsertionEffect предпочтительней использовать useEffect или useLayoutEffect.


---------------------------------------------------------------------

9) useRef

---------------------------------------------------------------------


Используется дла хранения ссылки на тэг, как id и getElementById(), или для сохранения внутри значения переменной.


В случае передачи в тэг значению ref присваивается ссылка на этот тэг, прямой доступ к которой осуществляется через ref.current.

В ref тэга также можно передать функцию, которая будет устанавливать на него одновременно внутреннюю ссылку innerRef и внешнюю ссылку outerRef,

переданную из родительского компонента с помощью функции forwardRef.


Функция установки ref в тэг будет запускаться каждый раз после завершения рендеренга. Поэтому в ней можно прописать установку фокуса на какой-нибудь другой тэг

ref.current.parentElement.focus() или выполнить прокрутку scroll блока div.


В случае хранения в ref просто значения переменной прямой доступ к этому значения осуществляется через ref.current.


В useRef можно устанавливать 1 раз первоначально заданное значение. Оно само не будет обновляться при последующих рендренгах.


Для изменения значения ref нужно присвоить ей другое значение в ref.current.

Ручное изенение значения ref.current, в отличии от изменения внутреннего state или внешних props, не будет вызывать ререндеренг компонента.


Ref будет существовать тех пор, пока компонент не будет размонтирован.


// tag ref

const inputRef = useRef(null);


function handleClick () {

    inputRef.current.focus();

}


return <input type="text" ref={inputRef} />



// variable ref

const variableRef = useRef(100);


function handleClick () {

    variableRef.current = variableRef.current + 10; // 110

}


// append remove tag ref

const divRef = useRef(document.createElement('div'));


useEffect(function () {

    document.body.appendChild(divRef);


    return function () {

        document.body.removeChild(divRef);

    };

}, []);



// callback ref

function useStateWithCallback (initialState, alwayExecuteCallback) {


    const [state, setState] = useState(initialState);


    const callbackRef = useRef(undefined);


    const setStateAndCallback = useCallback(function setStateAndCallback (newState, callback) {

        callbackRef.current = callback;

        setState(newState);

    }, []);


    useEffect(function (): void {

        if (callbackRef.current) {

            callbackRef.current(state);

            callbackRef.current = undefined;

        }

    }, alwayExecuteCallback ? undefined : [state]);


    return [state, setStateAndCallback];


}


const [value, setValue] = useStateWithCallback(0, false);


function handleClick () {

    setValue(1, function (newState) {

        console.log(newState); // 1

    });

}


// inner, outer refs

const innerInputRef = useRef(null);


function setRef (element) {

    innerInputRef.current = element;

    if (outerInputRef) {

        if (typeof outerInputRef === 'function') {

            outerInputRef(element);

        } else {

            outerInputRef.current = element;

        }

    }

}


<input type="text" ref={setRef} />


---------------------------------------------------------------------

10) React.forwardRef

---------------------------------------------------------------------


Позволяет передать внешний Ref из родительского компонента в дочерний.


export const DivComponent: React.FC<Props> = forwardRef<HTMLDivElement, Props>(

    function DivComponent (props: Props, outerRef: ForwardedRef<HTMLDivElement>): JSX.Element {


        const innerRef: React.MutableRefObject<HTMLDivElement> = useRef<HTMLDivElement>(null);


        function setRef (element: HTMLDivElement): void {

            innerRef.current = element;

            if (outerRef) {

                if (typeof outerRef === 'function') {

                    outerRef(element);

                } else {

                    outerRef.current = element;

                }

            }

        }


        return (

            <div ref={setRef}>block</div>

        );


    }

);



---------------------------------------------------------------------

11) useCallback

---------------------------------------------------------------------


Во время выполнения тела функции компонента все его элементы внутри также пересоздаются.


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

значений в массиве зависимостей.


Функция всегда создается 1 раз при первом рендеренге компонента. И в дальнейшем пересоздается только, если изменились значения в массиве зависимостей.


const sendMessage = useCallback(function () {

    console.log(props.message + ' to ' + props.address);

}, [props.message, props.address]);


function handleClick () {

    sendMessage();

}


---------------------------------------------------------------------

12) useMemo

---------------------------------------------------------------------


Во время выполнения тела функции компонента все его элементы внутри также пересоздаются.


При этом вычисления значений переменных могут быть весьма затратны и длительны.


Чтобы оптимизировать процесс повторного вычисления значений переменных используется useMemo, которая заново вычисляет значение переменной только после изменения хотя бы одного из

значений в массиве зависимостей.


Переменная всегда создается 1 раз при первом рендеренге компонента. И в дальнейшем пересоздается только, если изменились значения в массиве зависимостей.


const bigArray = useMemo(function () {

    return new Array(1000).map(function (id) {

        return id + ') ' + props.message + ' to ' + props.address;

    });

}, [props.message, props.address]);


function handleClick () {

    console.log(bigArray);

}


---------------------------------------------------------------------

13) React.createPortal

---------------------------------------------------------------------


Помещает JSX-тэги внутрь обычного тэга.


interface Props {

    children: React.ReactNode;

}


function Portal (props: Props): JSX.Element {


    const container: React.MutableRefObject<HTMLDivElement> = useRef<HTMLDivElement>(document.createElement('div'));


    useEffect(function (): () => void {

        document.body.appendChild(container.current);

        const elementForRemoval: HTMLDivElement = container.current;


        return function (): void {

            document.body.removeChild(elementForRemoval);

        };

    }, [container]);


    return ReactDOM.createPortal(props.children, container.current);


}


---------------------------------------------------------------------

14) React.Children, React.cloneElement

---------------------------------------------------------------------


Позволяет обработать входящих props.children и изменить из props.


</div>

    {

        React.Children.map(props.children, function (child: React.ReactElement<TabProps>): React.ReactElement<TabProps> {

            return React.cloneElement(

                child,

                {

                    ...child.props,

                    selectedTabId: props.selectedTabId,

                    onChange: props.onChange

                }

            );

        })

    }

</div>


---------------------------------------------------------------------

15) useDebugValue

---------------------------------------------------------------------


Можно использовать в собственных хуках при отладке через React Dev Tools для вывода туда переданного в хук useDebugValue значения.


function useNetworkStatus () {


    const [isOnline, setIsOnline] = useState(null);


    useDebugValue(isOnline ? 'В сети' : 'Не в сети'); // В React Dev Tools выведется: NetworkStatus: "Не в сети"

    // useDebugValue(date, function (date) {return date.toDateString();}); // Можно передавать функцию для форматирования значения перед его выводом в React Dev Tools.


    return isOnline;


}


---------------------------------------------------------------------

16) useDeferredValue

---------------------------------------------------------------------


Делает отсрочку в применении обновления переданного значения например при быстром вводе символов в <input>


function Typeahead () {


    const query = useSearchQuery('');

    const deferredQuery = useDeferredValue(query);


    // useMemo позволяет обновить значение только когда изменится отложенное по времени deferredQuery, а не просто каждый query.

    const suggestions = useMemo(function () {

        <SearchSuggestions query={deferredQuery} />

    }, [deferredQuery]);


    return (

        <>

            <SearchInput query={query} />

            <Suspense fallback="Loading results...">{suggestions}</Suspense>

        </>

    );


}


---------------------------------------------------------------------

17) useTransition

---------------------------------------------------------------------


Возвращает функцию для запуска процесса перехода от одного состояния к другому и переменную, хранящую в себе статус состояния перехода.


function App () {


    const [count, setCount] = useState(0);


    const [isPending, startTransition] = useTransition();


    function handleClick () {

        startTransition(function () {

            setCount(function (previosCount) {

                return previosCount + 1;

            });

        });

    }


    return (

        <>

            <div>{isPending && <Spinner />}</div>

            <div>{count}</div>

            <button onClick={handleClick}>[+]</button>

        </>

    );

}


---------------------------------------------------------------------

18) useId

---------------------------------------------------------------------


Просто генерирует уникальные ID, которые стабильны на сервере и на клиенте для избегания несовпадения при гидрации.

useId нельзя использовать для генерирования ключей массивов.


function Fields () {

    const id = useId();

    return (

        <div>

            <label htmlFor={id + '-firstName'}>First Name</label>

            <input id={id + '-firstName'} type="text" />

            <br />

            <label htmlFor={id + '-lastName'}>Last Name</label>

            <input id={id + '-lastName'} type="text" />

        </div>

    );

}


---------------------------------------------------------------------

19) useSyncExternalStore

---------------------------------------------------------------------


useSyncExternalStore рекомендован для использования в качестве чтения и подписки на внешние источники данных.


Хук возвращает значение и принимает 3 аргумента:

- subscribe - функци для регистрации callback, который будет вызван при изменении данных в источнике данных store.

- getSnapshot - функция, которая вернет конкретное значение из источника данных store.

- getServerSnapshot - фукникция, которая вернет снимок snapshot в течении серверного рендеринга.


Когда производится серверный рендерингг, то вам надо сериализовать значение на сервере с помощью useSyncExternalStore.

React в этом случае используется snapshot в процессе hydration для предотвращения ошибок.


const selectedField = useSyncExternalStore(

    store.subscribe,

    function () {return store.getSnapshot().selectedField;},

    function () {return INITIAL_SERVER_SNAPSHOT.selectedField;}

);


---------------------------------------------------------------------

20) useOutsideClickEventHandler

---------------------------------------------------------------------


import React, {useEffect} from 'react';


export function useOutsideClickEventHandler <T extends HTMLElement = HTMLElement> (ref: React.RefObject<T>, handler: (event: Event) => void): void {

    useEffect(function (): () => void {


        function handleClickOutside (event: Event): void {

            if (ref.current && !ref.current.contains(event.target as Node)) {

                handler(event);

            }

        }


        document.addEventListener('mousedown', handleClickOutside);


        return function (): void {

            document.removeEventListener('mousedown', handleClickOutside);

        };


    }, [ref, handler]);

}


---------------------------------------------------------------------

21) useReduxState

---------------------------------------------------------------------


import {useSelector} from 'react-redux';


import get from 'lodash-es/get';


import {RootStateInterface} from './interfaces';


type RootStateKey = keyof RootStateInterface;

type StateKeyWithDots = `.${string}`;

type RootOrDotsStateKey = RootStateKey | StateKeyWithDots;


interface SelectedStateInterface extends Partial<RootStateInterface> {

    [key: StateKeyWithDots]: any;

}


export function useReduxState (firstStateKey: RootOrDotsStateKey, ...stateKeys: RootOrDotsStateKey[]): SelectedStateInterface {

    stateKeys.push(firstStateKey);

    const selectedState: SelectedStateInterface = {};

    stateKeys.forEach(function (stateKey: RootOrDotsStateKey): void {

        selectedState[stateKey] = useSelector(function (state: RootStateInterface): any {

            if (stateKey.indexOf('.') === 0) {

                return get(state, stateKey.slice(1));

            }

            return state[stateKey];

        });

    });

    return selectedState;

}


const {

    input: {

        value: inputValue,

        date: calendarDate

    }

} = useReduxState('calendar');

среда, 26 октября 2022 г.

React Extend Children Props | React cloneElement

 import React from 'react';


function RootComponent () {

    return (

        <ParentComponent>

            <ChildComponent a="1" b="2" />

            <ChildComponent a="3" b="4" />

        </ParentComponent>

    );

}


function ChildComponent (props: ChildComponentProps) {

    return (

        <div>{JSON.stringify(props)}</div>

    );

}


function ParentComponent (props: ParentComponentProps) {


    /* 

    // Variant 1.

    return (

        <div>{

            React.Children.map(props.children, function (child: React.ReactElement<ChildComponentProps>): React.ReactElement<ChildComponentProps> {

                return React.cloneElement(

                    child, 

                    {

                        a: child.props.a + 1,

                        b: child.props.b + 2,

                        key: child.props.a + 1

                    },

                    null

                );

            })

        }</div>

    );

    */


    /*

    // Variant 2.

    return (

        <div>{

            React.Children.map(props.children, function (child: ChildComponent): ChildComponent {

                return (

                    <ChildComponent

                        {...child.props} 

                        {...{

                            a: child.props.a + 1,

                            b: child.props.b + 2,

                            key: child.props.a + 1

                        }} 

                     />

                );

            })

        }</div>

    );

    */


    // Variant 3.

    return (

        <div>{

            React.Children.map(props.children, function (child: React.ReactElement<ChildComponentProps>): React.ReactElement<ChildComponentProps> {

                return (

                    <child.type

                        {...child.props} 

                        {...{

                            a: child.props.a + 1,

                            b: child.props.b + 2,

                            key: child.props.a + 1

                        }} 

                     />

                );

            })

        }</div>

    );


}

React Modal Portal

Вариант с классом.


import React from 'react';

import ReactDOM from 'react-dom';


class Modal extends React.Component {


    modalDiv = undefined;


    constructor (props) {

        super(props);

        this.modalDiv = document.createElement('div');

    }


    render () {

        return ReactDOM.createPortal(

            this.props.children,

            this.modalDiv

        );

    }


    componentDidMount () {

        document.body.appendChild(this.modalDiv);

    }


    componentWillUnmount () {

        document.body.removeChild(this.modalDiv);

    }


}


class Parent extends React.Component {


    constructor (props) {

        super(props);

    }


    render () {

        return (

            <Modal>

                <Child />

            </Modal>

        );

    }


}


class Child extends React.Component {


    constructor (props) {

        super(props);

    }


    render () {

        return (

            <div className="modal-message">

                <h1>Hello</h1>

            </div>

        );

    }


}


Результат


<html>

    <body>

        <div id="app"></div>

        <div>

            <div class="modal-message">

                <h1>Hello</h1>

            </div>

        </div>

    </body>

</html>


Вариант с функцией.


import React, {useEffect, useState} from 'react';

import ReactDOM from 'react-dom';


interface PortalProps {

    children: React.ReactNode;

}


function Portal (props: PortalProps): JSX.Element {


    const [div] = useState(function (): HTMLDivElement {

        return document.createElement('div');

    });

    

    useEffect(function (): () => void {

    

        document.body.appendChild(div);

        

        return function (): void {

            document.body.removeChild(div);

        };

        

    }, [div]);

    

    return ReactDOM.createPortal(props.children, div);

    

}


interface ModalProps {}


function Modal (props: ModalProps): JSX.Element {

    return (

        <Portal>

            <div className="modal-message">

                <h1>Hello</h1>

            </div>

        </Portal>

    );

}


<html>

    <body>

        <div id="app"></div>

        <div>

            <div class="modal-message">

                <h1>Hello</h1>

            </div>

        </div>

    </body>

</html>