Задать вопрос
@Niksak

По какой причине cloneNode undefined?

Сразу скину ссылку на рабочий код: https://github.com/yankovalenko94/React_admin_pane...

Ниже будет мой код, который я переписываю уже на функциональных компонентах а не на классовых, как по ссылке:
Проблема в функции save, где не хочет работать cloneNode. То есть даже просто банально не хочет автозаполняться, его в списке методов для virtualDom нет при автозаполнении. Но в других функциях есть такое, допустим, в функции open оно работает, т.е. я не получал ошибок когда прописывал virtualDom.cloneNode(vitrualDom) там.
import "../helpers/iframeLoader.js"; // волшебная пилюля
import axios from "axios";
import React, { useState, useEffect } from "react";

const App = () => {
  const [pageList, setPageList] = useState([]); //Это наш список страниц
  const [newPageName, setNewPageName] = useState(""); //А это имя страницы
  const [currentPage, setCurrentPage] = useState("index.html"); //Текущая страница
  //const [virtualDom, setVirtualDom] = useState(null);
  var iframe; //наш фрейм
  var virtualDom;

  //Загружаем список страниц
  const loadPageList = () => {
    axios.get("./api").then(response => setPageList(response.data)); //data - то что возвращает axios, в нем нужная информация
  }
  //Вызываем функцию init только когда впервые отрендерится компонент
  useEffect(() => {
    
    init(currentPage);
  }, []);

  //Принимает название страницы, открывает ее
  const init = (page) => {
    iframe = document.querySelector('iframe'); //наш фрейм: определяем его селектором 
    open(page); //открывает страницу
    //loadPageList();
  }

  //Здесь мы устанавливаем путь динамический к нужной странице
  const open = (page) => {
    setCurrentPage(`../${page}?rnd=${Math.random()}`); //Устанавливаем путь к странице + сброс кэширования
    
    //Посылаем запрос на сервер и получаем страницу
    axios.get(`../${page}`).then(res => parseStringDOM(res.data)) //Получаем чистый исходный код страницы в виде строки и превращаем в DOM структуру
                            .then(wrapTextNodes) //Метод оборачивает все текстовые ноды и оборачиваем в свой спец. тег чтобы редачить его
                            .then(dom => { //Сохраняем "чистую" копию в virtual dom. На этом моменте чистая копия === загрязненной
                              virtualDom = dom;
                              
                              return dom;
                            })
                            .then(domToString) //Превращаем DOM дерево в строку
                            .then(html => axios.post("./api/saveTemplate.php", {html})) //Отправка строки dom на сервер. На сервере создается новая html страница с нужной структурой
                            .then(() => iframe.load("../temp.html")) //Загружаем temp.html в iframe (страницу, созданную выше)
                            .then(() => enabelEditing()) //iframe готов - значит врубаем функцию для доступа к редактированию элементов
  }
  
  //Сох
  const save = () => {
    const newDom = virtualDom.cloneNoder(virtualDom); //Копируем данные, лежащие в виртуальном dom
    unwrapTextNodes(newDom); 
    const html = domToString(newDom)
    console.log(html)
  };

  const unwrapTextNodes = (dom) => {
    dom.body.querySelectorAll("text-editor").forEach(element => { //Перебираем все наши теги кастомные
      //console.log(element)
      element.parentNode.replaceChild(element.firstChild, element); //Обращаемся к род. ноде элемента и заменяем ее дочернюю ноду
    })
  }
  
  //Добавляем возможность редактирования
  const enabelEditing = () => {
    iframe.contentDocument.body.querySelectorAll("text-editor").forEach(element => {
      element.contentEditable = "true";
      element.addEventListener("input", () => { //Навешиваем обработчик событий чтобы синхронизировать копии
        onTextEdit(element)
      })
    })
  };
  //Запустится когда текст отредактируется
  const onTextEdit = (element) => {
    const id = element.getAttribute("nodeid");
    //Синхронизируем то что вводит пользователь с virtual dom ( чистой версией ), помещая введенную юзером информацию в чистую копию
    virtualDom.body.querySelector(`[nodeid="${id}"]`).innerHTML = element.innerHTML; //То что ввел юзер (после равно) записывается в чистую копию
  }
   

  //Разбираем код страницы в DOM дерево
  const parseStringDOM = (str) => {
    const parser = new DOMParser(); //DOMParser парсит html/xml, содержащиеся в строке DOM
    return parser.parseFromString(str, "text/html");
  }

  //Найти все текстовые ноды и обернуть их в редактируемые теги
  const wrapTextNodes = (dom) => {
    const body = dom.body; //получаем body тег нашего iframe
    let textNodes = [];//Сюда складываем каждую отдельную ноду с текстом

    function recursy (element) {
      element.childNodes.forEach(node => { //Получаем всех детей element и выводим в консоль
          
      //Здесь мы проверяем, нашли ли мы текстовую ноду и пустая ли это нода. Заменяем пробелы на пустой текст если встречаются и проверяем больше нуля ли текст
        if(node.nodeName === "#text" && node.nodeValue.replace(/\s+/g, "").length > 0){ // если мы зашли в дочерний элемент и наткнулись на текст, то это то что надо
             textNodes.push(node); //Пушим ноды в массив
        }else{ //если наткнулись не на текст - снова перебираем всю страницу
            recursy(node);
        }
      })
    };

    recursy(body); //запускаем функцию на body страницы, этим самым переходя по каждому дочернему элементу
      
    //Даем каждой текстовой ноде возможность редактирования
    textNodes.forEach((node, i) => {
      const wrapper = dom.createElement("text-editor"); //Создаем свой тег враппеп для текста
      node.parentNode.replaceChild(wrapper, node); //заменяем тег в который обернут текст своим тегом
      wrapper.appendChild(node);  
      wrapper.setAttribute("nodeid", i);
    });

    return dom;
  }

  //Превращение DOM структуры в строку для отправки на сервер
  const domToString = (dom) => {
    //XMLSerializer может быть использован для конвертации веток DOM-дерева или дерева целиком в текст
    const serializer = new XMLSerializer();
    return serializer.serializeToString(dom);
  }

  //Создание новой страницы
  const createPage = () => {
    axios.post("./api/createPage.php", { //Делаем post запрос, указываем что мы отправляем
      "name": newPageName
    })
    .then(loadPageList()) //В then действия с результатом, т.е. мы загружаем снова список чтобы он обновился
    .catch(() => alert("Страница уже есть!")); //Обработка ошибок
  };

  //Обработка ввода инпута
  const inputChangeHandler = (evt) => {  
    setNewPageName(evt.target.value);
  }
  
  //Удаление страницы
  const deletePage = (page) => {
    axios.post("./api/deletePage.php", { //Делаем post запрос, указываем что мы отправляем
      "name": page //Посылаем page 
    }).then(loadPageList())
      .catch(() => alert("Страница и так не существует"));
  };

  return(
  <>
    <button onClick={() => save()}>Клик!</button>
    <iframe src={currentPage} frameBorder="0"></iframe>
  </>
  )
}

export default App;


641211289fac1929869272.png

Да, знаю, вопрос уже типовой, я знаю что значит эта ошибка но в этом случае я даже не представляю причины ее возникновения, в этот раз не могу избавиться:(
  • Вопрос задан
  • 61 просмотр
Подписаться 1 Простой 2 комментария
Пригласить эксперта
Ответы на вопрос 1
@HeyAleksey
Пойдем по-порядку:
1. Никогда не используйте ключевое слово var. Есть вполне себе let и const
2. Переменные iframe и virtualDom объявлены так, что их значение будет сбрасываться в undefined при каждом ререндере компонента. Отсюда, собственно и ошибка.
Чтобы это поправить можно объявить эти переменные с использованием useRef
const virtualDom = useRef();
Далее в асинхронном коде произвести установку в ref
virtualDom.current = dom;
Ну и наконец, обернуть функцию save в useCallback:

const save = useCallback(() => {
    const newDom = virtualDom.current?.cloneNoder(virtualDom); //Копируем данные, лежащие в виртуальном dom
    ...
  }, [virtualDom]);


Кстати, обернуть в useCallback необходимо все функции, использующие переменную virtualDom и всё тоже свмое необходимо проделать относительно переменной iframe.

P.S. Обязательно почитайте про хуки и жизненный цикл компонентов React.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы