How to transform your LMS with a React WYSIWYG HTML editor, part 1

We’ve all been there. Huge project, impending deadlines, hopeful clients or investors, and plenty of tasking tasks left. Uploading content like rich text with images, videos, and specific formatting, for example, is not as simple as it sounds. This is especially true for LMS (learning management systems), which require course creators to upload different content in one go. To accommodate this need, we must prepare to allow as many file types as possible while also considering rich text processing and storage. Thankfully, we have WYSIWYG editors for that. In this tutorial, we’ll tackle these LMS content issues using a React WYSIWYG HTML editor.

Key takeaways

  • A WYSIWYG HTML editor significantly simplifies content processing and uploading tasks
  • Video and image uploads, processing, and transformations make LMS more engaging and powerful.
  • Froala Editor now comes with built-in Filestack integration, making image transformations and more possible within the editor.
  • You can use React, PHP (or the back-end tool of your choice), and Froala to easily get started with a modern LMS project.
  • As your mastery with WYSIWYG editors and file upload tools grows, so do the capabilities of your LMS.

Defining the scope and features of our LMS project

First, let me clarify that this isn’t a full tutorial for creating a full-fledged LMS. Instead, this is a guide for those who want to have robust content editing and uploading features within their projects. We will be building the front end of the application using React and Froala. For file uploading, we’ll leverage the newly integrated Filestack within the editor. Finally, we will use PHP for the back-end parts and SQL Server for storing and retrieving the data.

Our mini LMS will have the following features:

  • View and add courses – each course will have a title, description, date published, and a unique ID.
  • View and add chapters – each course will consist of chapters, which contain a title, description, content, date published, and a unique ID.

We’ll first set up the React application and Froala Editor. And then, we will create the front end of the application, followed by the back end and database tables. In the end, we’ll run a demo of the application.

Setting up the React project and the WYSIWYG HTML editor

Let’s start our project by creating our React app and installing the packages that we need:  

npx create-react-app demo-lms
cd demo-lms
npm install react-froala-wysiwyg --save

npm install react-router-dom

 

Now, you should have a React application with the Froala WYSIWYG HTML editor and page routing dependencies. The next thing we need to do is create a new folder under “src” called “components.” Afterwards, create a new file named “FroalaComponent.jsx” and insert the following:

 

import React, { useEffect } from 'react';
import 'froala-editor/css/froala_style.min.css';
import 'froala-editor/css/froala_editor.pkgd.min.css';
import FroalaEditorComponent from 'react-froala-wysiwyg';
import 'froala-editor/js/plugins.pkgd.min.js';

function FroalaComponent({ setChapterContent, setChapterImage }) {
  const config = {
    filestackOptions: {
      uploadToFilestackOnly: true,
      filestackAPI: 'InsertYourFilestackAPIHere',
    },
    events: {
      'contentChanged': function () {
        setChapterContent(this.html.get());
      },
      'filestack.uploadedToFilestack': function (response) {
        if(response && response.filesUploaded[0].url){
          setChapterImage(response.filesUploaded[0].url);
        }

        else{
          console.error("Image upload failed, no URL found in response", response);
        }
      }
},
  };

  useEffect(() => {
    const filestackScript1 = document.createElement('script');
        filestackScript1.src = 'https://static.filestackapi.com/filestack-js/3.32.0/filestack.min.js';
        filestackScript1.async = true;
        document.body.appendChild(filestackScript1);

        const filestackScript2 = document.createElement('script');
        filestackScript2.src = 'https://static.filestackapi.com/filestack-drag-and-drop-js/1.1.1/filestack-drag-and-drop.min.js';
        filestackScript2.async = true;
        document.body.appendChild(filestackScript2);

        const filestackScript3 = document.createElement('script');
        filestackScript3.src = 'https://static.filestackapi.com/transforms-ui/2.x.x/transforms.umd.min.js';
        filestackScript3.async = true;
        document.body.appendChild(filestackScript3);

        const filestackStylesheet = document.createElement('link');
        filestackStylesheet.rel = 'stylesheet';
        filestackStylesheet.href = 'https://static.filestackapi.com/transforms-ui/2.x.x/transforms.css';
        document.head.appendChild(filestackStylesheet);

    return () => {
      document.body.removeChild(filestackScript1);
      document.body.removeChild(filestackScript2);
      document.body.removeChild(filestackScript3);
      document.head.removeChild(filestackStylesheet);
    };
  }, []);

  return (
    <div className="editor">
      <FroalaEditorComponent tag='textarea' config={config} />
    </div>
  );
}

export default FroalaComponent;

 

The first thing we need to do is declare our component and its properties. Note that for the Froala config, we have to declare the Filestack options, including our Filestack API. You can get one by creating a free Filestack account or starting a free trial of a paid plan. Also note that we’re handling Froala’s content-change events and Filestack’s image upload event. This is for storing a course chapter’s content and image URL later.

Additionally, we’re loading Filestack-related scripts and styles dynamically using useEffect. That way, we can easily use Filestack’s drag-and-drop functionality, image uploads, and other features. Lastly, we load our FroalaEditorComponent and its configuration. If you need additional guidance on setting up Froala for React, follow this helpful article.

Creating the LMS

Now that we have the application and our Froala WYSIWYG HTML editor component ready, let’s build our LMS! Start by creating 4 files under the components folder:

  • Courses.jsx: For viewing courses and navigating to a course’s chapters

 

import React, { useState, useEffect } from 'react';
import CourseModal from './CourseModal';
import { Link } from 'react-router-dom';

function Courses() {
  const [courses, setCourses] = useState([]);
  const [isModalOpen, setIsModalOpen] = useState(false);

  const fetchCourses = async () => {
    // Fetch courses from the back end
    const response = await fetch('path-to-backend/demo-lms-backend/fetchCourses.php');
    const data = await response.json();
    setCourses(data);
  };

  useEffect(() => {
    fetchCourses();
  }, []);

  const handleSaveCourse = () => {
    fetchCourses(); // Fetch courses again after saving a new one
    setIsModalOpen(false);
  };

  return (
    <div className="course-list">
      <h1>My Simple yet Powerful LMS</h1>
      <h2>Courses</h2>

      <button onClick={() => setIsModalOpen(!isModalOpen)}>
        {isModalOpen ? 'Close' : 'Add a Course'}
      </button>

      {isModalOpen && <CourseModal onSaveCourse={handleSaveCourse} onClose={() => setIsModalOpen(false)} />}
      {courses.map((course) => (
        <div key={course.course_id} className="course-card">
          <h3>{course.course_title}</h3>
          <p>{course.course_description}</p>
          <p>Date Published: {course.date_published ? new Date(course.date_published.date).toLocaleDateString() : 'N/A'}</p>
          <Link to={`/chapters/${course.course_id}`}>
            <button>View Course</button>
          </Link>
        </div>
      ))}
    </div>
  );
}

export default Courses;

 

  • CourseModal.jsx: For saving courses

 

import React, { useState } from 'react';

function CourseModal({ onSaveCourse, onClose }) {
  const [courseTitle, setCourseTitle] = useState('');
  const [courseDescription, setCourseDescription] = useState('');

  const handleSubmitCourse = async (e) => {
    e.preventDefault();

    const newCourse = {
      title: courseTitle,
      description: courseDescription,
      date_published: new Date().toISOString().split('T')[0],
    };

    const response = await fetch('path-to-backend/demo-lms-backend/saveCourse.php', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(newCourse),
    });

    const result = await response.json();
    if (response.ok) {

      // This will call fetchCourses from Courses.jsx
      onSaveCourse();
    } else {
      console.error('Failed to save course:', result);
    }
  };

  return (
    <div className="modal">
      <form onSubmit={handleSubmitCourse}>
        <h3>New Course</h3>
        <label>Title</label>
        <input type="text" value={courseTitle} onChange={(e) => setCourseTitle(e.target.value)} required />
     
        <label>Description</label>
        <textarea value={courseDescription} onChange={(e) => setCourseDescription(e.target.value)} required />

        <button type="submit">Save</button>
        <button type="button" onClick={onClose}>Cancel</button>
      </form>
    </div>
  );
}

export default CourseModal;

 

  • Chapters.jsx: For viewing the chapters of a course and navigating back to the courses

 

import React, { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import ChapterModal from './ChapterModal';
import { Link } from 'react-router-dom';

function Chapters() {
  const { courseId } = useParams();
  const [chapters, setChapters] = useState([]);
  const [course, setCourse] = useState(null); // To store course details
  const [isModalOpen, setIsModalOpen] = useState(false);

  // Fetch chapters for the course
  const fetchChapters = async () => {
    const response = await fetch(`path-to-backend/demo-lms-backend/fetchChapters.php?course_id=${courseId}`);
    const data = await response.json();
    setChapters(data);
  };

  // Fetch course details
  const fetchCourseDetails = async () => {
    const response = await fetch(`path-to-backend/demo-lms-backend/getCourseById.php?courseId=${courseId}`);
    const data = await response.json();
    setCourse(data);
  };

  useEffect(() => {
    fetchChapters();
    fetchCourseDetails(); // Fetch course details on load
  }, [courseId]);

  const handleSaveChapter = (newChapter) => {
    setChapters([...chapters, newChapter]);
    setIsModalOpen(false);
  };

  if (!course) {
    return <p>Loading course details...</p>;
  }

  return (
    <div className="course-details">
      <h1>{course.course_title}</h1>
      <p>Description: {course.course_description}</p>
      <p>Date Published: {course.date_published ? new Date(course.date_published.date).toLocaleDateString() : 'N/A'}</p>
      <Link to={`/`}>
        <button>Back to Courses</button>
      </Link>

      <button onClick={() => setIsModalOpen(!isModalOpen)}>
        {isModalOpen ? 'Close' : 'Add a Chapter'}
      </button>
   
      <div className="chapter-list">
        {chapters.map((chapter) => (
          <div key={chapter.chapter_id} className="chapter-card">
            <h3>{chapter.chapter_title}</h3>
            <p>{chapter.chapter_description}</p>
            <p>Date Published: {chapter.date_published}</p>
          </div>
        ))}
      </div>

      {isModalOpen && <ChapterModal courseId={courseId} onSaveChapter={handleSaveChapter} onClose={() => setIsModalOpen(false)} />}
    </div>
  );
}

export default Chapters;

 

  • ChapterModal.jsx: For saving chapters within a course

 

import React, { useState } from 'react';
import FroalaComponent from './FroalaComponent';

function ChapterModal({ courseId, onSaveChapter, onClose }) {
  const [chapterTitle, setChapterTitle] = useState('');
  const [chapterDescription, setChapterDescription] = useState('');
  const [chapterContent, setChapterContent] = useState('');
  const [chapterImage, setChapterImage] = useState('');

  const handleSubmitChapter = async (e) => {
    e.preventDefault();

    console.log('Chapter Image URL:', chapterImage);

    const newChapter = {
      course_id: courseId,
      title: chapterTitle,
      description: chapterDescription,
      content: chapterContent,
      chapter_img: chapterImage, // for Filestack URL
      date_published: new Date().toISOString().split('T')[0],
    };

    const response = await fetch('path-to-backend/demo-lms-backend/saveChapter.php', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify(newChapter),
    });

    const result = await response.json();
    if (response.ok) {
      onSaveChapter(result);
    } else {
      console.error('Failed to save chapter:', result);
    }
  };

  return (
    <div className="modal">
      <form onSubmit={handleSubmitChapter}>
        <h3>Add New Chapter</h3>
        <label>Title</label>
        <input type="text" value={chapterTitle} onChange={(e) => setChapterTitle(e.target.value)} required />
     
        <label>Description</label>
        <textarea value={chapterDescription} onChange={(e) => setChapterDescription(e.target.value)} required />
     
        <label>Content</label>
        <FroalaComponent setChapterContent={setChapterContent} setChapterImage={setChapterImage} />

        <div><button type="submit">Save</button> <button type="button" onClick={onClose}>Cancel</button></div>
      </form>
    </div>
  );
}

export default ChapterModal;

 

Lastly, replace the default code in your App.js with:

 

import React, { useState } from 'react';
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import './App.css';
import Courses from './components/Courses';
import Chapters from './components/Chapters';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Courses />} />
        <Route path="/chapters/:courseId" element={<Chapters />} />
      </Routes>
    </Router>
  );
}

export default App;

 

Here’s a quick summary of how our components work:

  • App.js displays the Courses page first. Using the fetchCourses function, the application will send a fetch request to fetchCourses.php on the server. This loads all courses from the database table.
  • When the user clicks the “Add a Course” button, we’ll show the CourseModal component. The user can then save a course through a fetch request to saveCourse.php. After saving the course, the application refreshes the courses.
  • The Chapters and ChapterModal components work similarly, except that loading and saving chapter data requires a course ID. Furthermore, we add the Froala component in our ChapterModal.

After creating the front end of our LMS, we’ll handle the back end of the application next.

Setting up the DB tables and back-end codes

For our database tables, we’ll create two simple tables in SQL Server:

  • course
    • course_id char(10)
    • course_title varchar(100)
    • course_description varchar(255)
    • date_published date
  • chapter
    • course_id char(10)
    • chapter_id char(10)
    • chapter_title varchar(100)
    • chapter_description varchar(255)
    • chapter_content varchar(max)
    • chapter_img_url varchar(255)
    • date_published date

These are just some basic tables, so you will want to improve this or plan ahead when implementing your LMS. Note that we plan to store the contents of the editor in chapter_content. On the other hand, we’ll store the Filestack URL in the chapter_img_url column. For now, we choose 255 as the max length, but you might want to adjust this in your implementation.

Alright, we’re almost done! The last thing we need to do is create our PHP files for fetching and saving data. We also assume that we have a “connection.php” that connects to a database instance. Here are our PHP files and their respective purposes:

  • fetchCourses.php: Retrieve all course data from the course table

 

<?php
    include "connection.php";

    $query = "SELECT course_id, course_title, course_description, date_published FROM course";
    $result = sqlsrv_query($conn, $query);

    $courses = array();
    while ($row = sqlsrv_fetch_array($result, SQLSRV_FETCH_ASSOC)) {
        $courses[] = $row;
    }

    sqlsrv_close($conn);
    echo json_encode($courses);
?>

 

  • saveCourse.php: Generate a unique course ID and save the course data

 

<?php
    include "connection.php";

    $input = json_decode(file_get_contents("php://input"), true);

    $course_title = $input["title"];
    $course_description = $input["description"];

    $query = "
        DECLARE @course_id CHAR(10)

        WHILE 1 = 1
        BEGIN
            SET @course_id = LEFT(CONVERT(NVARCHAR(36), NEWID()), 10)

            IF NOT EXISTS (SELECT 1 FROM course WHERE course_id = @course_id)
            BEGIN
                BREAK
            END
        END

        INSERT INTO course
        (course_id, course_title, course_description, date_published)
        VALUES
        (@course_id, ?, ?, GETDATE())
    ";
    $params = array($course_title, $course_description);
    $result = sqlsrv_query($conn, $query, $params);

    if($result===false) {
        echo json_encode(["error" => "Failed to save course"]);
    }
    else{
        echo 1;
    }
    sqlsrv_close($conn);
?>

 

  • getCourseById.php: Select all information of a specific course (for generating course data in our Chapters component)

 

<?php
    include "connection.php";

    if (isset($_GET["courseId"])) {
        $courseId = $_GET["courseId"];

        $query = "SELECT course_id, course_title, course_description, date_published FROM course WHERE course_id = ?";
        $params = array($courseId);
        $stmt = sqlsrv_query($conn, $query, $params);

        if($stmt === false){
            http_response_code(500);
            echo json_encode(["error" => "Failed to query the database."]);
            exit;
        }

        $course = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC);

        if($course){
            echo json_encode($course);
        }
        else{
            http_response_code(404);
            echo json_encode(["message" => "Course not found"]);
        }

        sqlsrv_free_stmt($stmt);
        sqlsrv_close($conn);
    }
    else{
        echo json_encode(["message" => "Course ID not provided"]);
    }
?>

 

  • fetchChapters.php: Get all chapters in a course

 

<?php
    include "connection.php";

    $course_id = $_GET["course_id"];

    $query = "SELECT chapter_title, chapter_description, date_published FROM chapter WHERE course_id = ?";
    $params = array($course_id);
    $result = sqlsrv_query($conn, $query, $params);

    if ($result === false) {
        die("Error in query preparation/execution: " . print_r(sqlsrv_errors(), true));
    }
    $chapters = array();
    while($row = sqlsrv_fetch_array($result, SQLSRV_FETCH_ASSOC)){
        $chapters[] = $row;
    }

    sqlsrv_close($conn);
    echo json_encode($chapters);
?>

 

  • saveChapter.php: Add a chapter to a course

 

<?php
    include "connection.php";

    $input = json_decode(file_get_contents("php://input"), true);

    $course_id = $input["course_id"];
    $chapter_title = $input["title"];
    $chapter_description = $input["description"];
    $chapter_content = $input["content"];
    $chapter_img_url = $input["chapter_img"];

    $query = "
        DECLARE @chapter_id CHAR(10)

        WHILE 1 = 1
        BEGIN
            SET @chapter_id = LEFT(CONVERT(NVARCHAR(36), NEWID()), 10)

            IF NOT EXISTS (SELECT 1 FROM chapter WHERE chapter_id = @chapter_id)
            BEGIN
                BREAK
            END
        END

        INSERT INTO chapter
        (course_id, chapter_id, chapter_title, chapter_description, chapter_content, chapter_img_url, date_published)
        VALUES
        (?, @chapter_id, ?, ?, ?, ?, GETDATE())
    ";

    $params = array($course_id, $chapter_title, $chapter_description, $chapter_content, $chapter_img_url);
    $result = sqlsrv_query($conn, $query, $params);

    if($result===false){
        echo json_encode(["error" => "Failed to save chapter"]);
    }
    else{
        echo 1;
    }
    sqlsrv_close($conn);
?>

 

And that’s it! Now, we have all we need for our React LMS with image transformations and rich-text editing. 

Seeing the React WYSIWYG HTML editor LMS in action

Let’s end this tutorial by checking how our React LMS works. After running npm start, we’ll see the homepage, which is empty at first. So, let’s add a course:

A GIF showing the sample LMS' feature that allows users to create courses.

We now have a course. Note that for demo purposes, we didn’t apply that much CSS for now, which is why the input fields and other elements look out of place. Next, let’s view the course and try to add a chapter. After clicking “Add a Chapter,” we will see the WYSIWYG HTML editor along with two input fields.

A GIF showing how you can create an LMS chapter for a course using Froala, Filestack, and React.

Once we’ve selected, cropped, and uploaded the image through the integrated Filestack file picker, we’ll now apply some transformations to the image.

A GIF showing how you can transform images using Froala's built-in Filestack file picker.

Lack of CSS aside, this LMS already looks and feels like a top-notch editing and content processing tool because of the WYSIWYG editor. Lastly, we save the enhanced image, insert a document (a sample chapter outline), add the chapter title and description, and save the chapter.

A GIF that shows how you can upload documents using Froala and Filestack.

Now, let’s check the database to see if we saved the information correctly. Note that I added the chapter to the course about 4 days after creating the course.

Database results that show correctness in terms of data uploaded

Let’s also check our Filestack dashboard:

We can see that we have both the edited image and our demo PDF in our Filestack dashboard. If we now download the image file, we’ll see the image that we transformed earlier in our LMS!

An image that was uploaded using Froala Editor and cropped and edited using Filestack

And there you have it: a React WYSIWYG HTML editor-powered LMS. Undoubtedly, this is far from a finished or even decent LMS product. However, it’s a good start, especially if you plan on implementing comprehensive image handling and content editing on your application. Moreover, we can still do a lot more that I haven’t shown you yet. Next time, we’ll continue this mini project and load the image from Filestack back to our application. Happy coding!

Get your Filestack API here for free.

Posted on October 29, 2024

Aaron Dumon

Aaron Dumon is an expert technical writer focusing on JavaScript WYSIWYG HTML Editors.

No comment yet, add your voice below!


Add a Comment

Your email address will not be published. Required fields are marked *

    Hide Show