Client-side integration

Client-side integration

Here are the sample source

In this section we add JavaScript to jump to the editor and to update the edited page using fetch API. We add the html and JavaScript code of the editor in the public/layouts/ directory. Therefore, we firstly create the directory:

mkdir public/layouts

Then the directory structure will become as follows:

public/
├── edit/
├── layouts/
├── pages/
└── test.html

File editor

Add the following html file public/layouts/edit.html

<!-- public/layouts/edit.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>Editing</title>
  </head>
  <body>
    <form action="/edit" method="POST">
      <input
        type="text"
        id="input_path"
        name="input_path"
        required
        minlength="20"
        maxlength="100"
        size="100"
        value="TITLE"
      />
      <br />
      <textarea name="input_content" id="input_content" rows="40" cols="50">
MARKDOWN</textarea
      >
      <br />
      <input type="submit" id="btn_submit" name="btn_submit" value="Update" />
    </form>
  </body>
  <script></script>
</html>

Here are the scripts added in the script tags <script></script>.

// scripts in the public/layouts/page.html

window.addEventListener("DOMContentLoaded", () => {
  // 送信ボタンのHTMLを取得
  const btn_submit = document.getElementById("btn_submit");

  btn_submit.addEventListener("click", async (e) => {
    e.preventDefault();

    // (3) フォーム入力欄のHTMLを取得
    const input_path = document.querySelector("input[name=input_path]");
    const path = input_path.value;

    // (3) フォーム入力欄のHTMLを取得
    const input_content = document.querySelector(
      "textarea[name=input_content]"
    );

    // (4) FormDataオブジェクトにデータをセット
    const body = input_content.value;

    // (5) フォームの入力値を送信
    const response = await fetch("/edit", {
      method: "POST", // *GET, POST, PUT, DELETE, etc.
      mode: "cors", // no-cors, *cors, same-origin
      cache: "no-cache", // *default, no-cache, reload, force-cache, only-if-cached
      credentials: "same-origin", // include, *same-origin, omit
      headers: {
        "Content-Type": "application/json",
        // 'Content-Type': 'application/x-www-form-urlencoded',
      },
      redirect: "follow", // manual, *follow, error
      referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
      body: JSON.stringify({ path: path, body: body }), // body data type must match "Content-Type" header
    });

    const location = await response.json();
    console.log("location: ", location);

    // redirect to the returned location
    window.location = location;
  });
});

Test the edit page

Open https://127.0.0.1:8443/files/layouts/edit.html on your browser, fill the contents and press the Update button. Then you will be redirected to the newly created page.

Improvement in the editor

Currently we are just displaying the raw file with the initial contents but we want a improvement in it such as presetting the previous markdown contents. We will replace the preset contents with the previous markdown at the Http GET request to the editor. The newly added API is follows:

  • GET /edit?path=<filename>
    • Get the editor with the previous markdown of the file <filename> as the preset contents
    • Load the markdown from the public/edit/<filename>

Notice that the path given here may be the path to a non-existing files since this can be used to newly create a file. Thus, we firstly implement a new auxiliary function that read the contents of the file with the default string which will be returned if the file does not exist.

// src/main.rs

/// Read a file
/// If the file doesn not exists, then return the default string
fn read_with_default(path: &str, default: &str) -> String {
    let contents = std::fs::read_to_string(&path);
    match contents {
        Ok(contents) => contents,
        Err(error) => match error.kind() {
            io::ErrorKind::NotFound => String::from(default),
            other_error => panic!("Problem opening the file: {:?}", other_error),
        },
    }
}

Then we are adding a new function handles the GET request to the editor

// src/main.rs

/// This handler uses json extractor with limit
/// GET the page for editing the page
async fn get_editor(item: web::Query<QueryPath>) -> Result<HttpResponse, Error> {
    println!("get_edit_page ? {:?}", item);
    let path: PathBuf = get_path("public/edit", &item.path);
    let contents = read_with_default(&path.to_string_lossy(), "");

    // decode the path to obtain the title
    let title = urlencoding::decode(&item.path).expect("cannot decode");

    // Open the file for editing
    let editor = std::fs::read_to_string("public/layouts/edit.html")?;
    // Replace the contents
    let editor = editor
        .replace("TITLE", &title.into_owned())
        .replace("MARKDOWN", &contents);

    Ok(HttpResponse::Ok().content_type("text/html").body(editor))
}

and add the routing to the editor

// src/main.rs

  .service(
      web::resource("/edit")
          .route(web::get().to(get_editor)) // **Newly added here** GET the editor
          .route(web::post().to(post)) // POST the new contents to update the file
          .route(web::delete().to(delete)), // Delete the file
  )

Test the editor page

Open https://127.0.0.1:8443/edit?path=filename on your browser, Check that the previous contents are there. fill the contents and press the Update button. Then you will be redirected to the newly created page.

Currently we are just displaying the raw html file generated by markdown but we want a improvement in it such as adding the link to the editor

Therefore, we use a template html file public/layouts/page.html and replace the contents with the generated html at the Http GET request to the pages.

<!-- public/layouts/page.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>TITLE</title>
  </head>
  <body>
    <div>
      <!-- A link to the editor -->
      <a href="/edit?path=PATH">Update</a>
    </div>
    <div>BODY</div>
  </body>
</html>

We update the function to update the html file to use the template layout after parsing the given markdown:

// src/main.rs
// in the `post` function

    // decode the path to obtain the title
    let title = urlencoding::decode(&item.path).expect("cannot decode");

    // Open the default file
    let default_page = std::fs::read_to_string("public/layouts/page.html")?;
    // Replace the title, path, contents
    let page = default_page
        .replace("TITLE", &title.into_owned())
        .replace("PATH", &item.path)
        .replace("BODY", &html_buf);

    // Update the file with the given contents
    let path: PathBuf = get_path("public/pages", &item.path);
    println!("writing to the file {:?}", path);
    let mut file = File::create(&path)?;
    file.write_all(page.as_bytes())?;

Test

Open https://127.0.0.1:8443/edit?path=filename on your browser, fill the contents and press the Update button. Then you will be redirected to the newly created page with the proper title and a link to the editor.

Improvement in page layout (Add a delete button)

We will be adding a delete button which deletes the page. The delete button can be implemented as same as the Update button we have already created. The new html is the following:

<!-- public/layouts/page.html -->

<!DOCTYPE html>
<html lang="en">
  <head>
    <title>TITLE</title>
  </head>
  <body>
    <div>
      <a href="/">HOME</a>
      <a href="/edit?path=PATH">Update</a>
      <form action="/edit" method="DELETE">
        <input type="submit" id="btn_submit" name="btn_submit" value="Delete" />
      </form>
    </div>
    <div>BODY</div>
  </body>

  <script>
    window.addEventListener("DOMContentLoaded", () => {
      // 送信ボタンのHTMLを取得
      const btn_submit = document.getElementById("btn_submit");

      btn_submit.addEventListener("click", async (e) => {
        e.preventDefault();

        // フォームの入力値を送信
        const response = await fetch(
          "/edit?" + new URLSearchParams({ path: "TITLE" }),
          {
            method: "DELETE", // *GET, POST, PUT, DELETE, etc.
          }
        );

        // redirect to the home page
        window.location = "/";
      });
    });
  </script>
</html>

Test it with clicking the delete button.