DEV Community

Juz_Bhargil
Juz_Bhargil

Posted on

2 1

How to integrate polotno in Angular framework?

What is Polotno?
Polotno is a fantastic design editing application that allows users to engage with and build any template. (for custom as well as social media post)

Why Polotno?
Polotno is an opinionated JavaScript library and React components to build canvas editors for several business use cases. It is a canvas editor tool using konvajs.org/

Problem Statement:
However, Polotno is only available in React components; if you want to integrate this library in the Angular framework, there are many obstacles in embedding React and extending all functionalities in Angular, but here is a step-by-step guide to help you understand.

Quick Start

  • Create a new Angular Project

ng new polotno-js-sample

  • Install these packages

npm install polotno --save

npm install react@17.0.2 --save

npm install react-dom@17.0.2 --save

  • Add this code in your app.component.html
    <div id="root"></div>

  • Create a new folder polotno-js inside app folder and create a new file app.tsx

  • Copy this code in app.tsx file

import { PolotnoContainer, SidePanelWrap, WorkspaceWrap } from "polotno";
import { Toolbar } from "polotno/toolbar/toolbar";
import { ZoomButtons } from "polotno/toolbar/zoom-buttons";
import { SidePanel } from "polotno/side-panel";
import { Workspace } from "polotno/canvas/workspace";
import * as React from "react";
import createStore from "polotno/model/store";
import styled from "polotno/utils/styled";
import {
  Button,
  Navbar,
  Alignment,
  Divider,
  Position,
  Menu,
  HTMLSelect,
  Slider,
} from "@blueprintjs/core";
import { observer } from "mobx-react-lite";
import { Popover2 } from "@blueprintjs/popover2";
import * as unit from "polotno/utils/unit";
import { t } from "polotno/utils/l10n";
import { downloadFile } from "polotno/utils/download";

const store = createStore({ key: "pVgGIyr19cAD0U8Z0OD1", showCredit: false });
const page = store.addPage();
page.addElement({
  x: 50,
  y: 50,
});
const NavbarContainer = styled("div")`
  @media screen and (max-width: 500px) {
    overflow-x: auto;
    overflow-y: hidden;
    max-width: 100vw;
  }
`;

const NavInner = styled("div")`
  @media screen and (max-width: 500px) {
    display: flex;
  }
`;
function getName() {
  var text = "";
  var possible =
    "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  for (var i = 0; i < 10; i++) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }
  return text;
}
const DownloadButton = observer(() => {
  const [saving, setSaving] = React.useState(false);
  const [quality, setQuality] = React.useState(1);
  const [type, setType] = React.useState("jpeg");

  return (
    <Popover2
      content={
        <Menu>
          <li className="bp4-menu-header">
            <h6 className="bp4-heading">File type</h6>
          </li>
          <HTMLSelect
            fill
            onChange={(e) => {
              setType(e.target.value);
              setQuality(1);
            }}
            value={type}
          >
            <option value="jpeg">JPEG</option>
            <option value="png">PNG</option>
            <option value="pdf">PDF</option>
          </HTMLSelect>
          <li className="bp4-menu-header">
            <h6 className="bp4-heading">Size</h6>
          </li>
          <div style={{ padding: "10px" }}>
            <Slider
              value={quality}
              labelRenderer={false}
              // labelStepSize={0.4}
              onChange={(quality) => {
                setQuality(quality);
              }}
              stepSize={0.2}
              min={0.2}
              max={3}
              showTrackFill={false}
            />
            {type === "pdf" && (
              <div>
                {unit.pxToUnitRounded({
                  px: store.width,
                  dpi: store.dpi / quality,
                  precious: 0,
                  unit: "mm",
                })}{" "}
                x{" "}
                {unit.pxToUnitRounded({
                  px: store.height,
                  dpi: store.dpi / quality,
                  precious: 0,
                  unit: "mm",
                })}{" "}
                mm
              </div>
            )}
            {type !== "pdf" && (
              <div>
                {Math.round(store.width * quality)} x{" "}
                {Math.round(store.height * quality)} px
              </div>
            )}
          </div>
          <Button
            fill
            intent="primary"
            loading={saving}
            onClick={async () => {
              if (type === "pdf") {
                setSaving(true);
                await store.saveAsPDF({
                  fileName: getName() + ".pdf",
                  dpi: store.dpi / quality,
                  pixelRatio: 2 * quality,
                });
                setSaving(false);
              } else {
                store.pages.forEach((page, index) => {
                  // do not add index if we have just one page
                  const indexString =
                    store.pages.length > 1 ? "-" + (index + 1) : "";
                  store.saveAsImage({
                    pageId: page.id,
                    pixelRatio: quality,
                    mimeType: "image/" + type,
                    fileName: getName() + indexString + "." + type,
                  });
                });
              }
            }}
          >
            Download {type.toUpperCase()}
          </Button>
        </Menu>
      }
      position={Position.BOTTOM_RIGHT}
    >
      <Button
        icon="import"
        text={t("toolbar.download")}
        intent="primary"
        loading={saving}
        onClick={() => {
          setQuality(1);
        }}
      />
    </Popover2>
  );
});
export const App = () => {
  const [show, setShow] = React.useState(false);
  const [saving, setSaving] = React.useState(false);

  return (
    <div className="bp4-light">
      <NavbarContainer className="bp4-navbar">
        <NavInner>
          <Navbar.Group align={Alignment.LEFT}>
            <Button
              icon="new-object"
              minimal
              onClick={() => {
                const ids = store.pages
                  .map((page) => page.children.map((child) => child.id))
                  .flat();
                const hasObjects = ids?.length;
                if (hasObjects) {
                  if (!window.confirm("Remove all content for a new design?")) {
                    return;
                  }
                  const pagesIds = store.pages.map((p) => p.id);
                  store.deletePages(pagesIds);
                  store.addPage();
                }
              }}
            >
              New
            </Button>
            <Button
              icon="floppy-disk"
              minimal
              loading={saving}
              onClick={async () => {
                const json = store.toJSON();
                setSaving(true);
                const url =
                  "data:text/json;base64," +
                  window.btoa(
                    unescape(encodeURIComponent(JSON.stringify(json)))
                  );

                downloadFile(url, "polotno.json");
              }}
            >
              Save My Design
            </Button>
          </Navbar.Group>
          <Navbar.Group align={Alignment.RIGHT}>
            <Divider />
            <DownloadButton />
            <Divider />
          </Navbar.Group>
        </NavInner>
      </NavbarContainer>
      <PolotnoContainer
        style={{ height: "90vh" }}
        className="polotno-app-container"
      >
        {" "}
        <SidePanelWrap>
          <SidePanel store={store} />
        </SidePanelWrap>
        <WorkspaceWrap>
          <Toolbar store={store} />
          <Workspace store={store} />
          <ZoomButtons store={store} />
        </WorkspaceWrap>
      </PolotnoContainer>
    </div>
  );
};

Enter fullscreen mode Exit fullscreen mode
  • Now just Add this code in your app.component.ts
import { AfterViewInit, Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import * as ReactDOM from 'react-dom';
import * as React from 'react';
import { App } from './polotno-js/app';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, AfterViewInit{
  title = 'polotno-js';
  ngOnInit(): void {}

  public ngOnChanges() {
    this.renderComponent();
  }

  public ngAfterViewInit() {
    this.renderComponent();
  }

  private renderComponent() {
       ReactDOM.render(
        React.createElement(App) 
      ,document.getElementById('root')
      );
  }
}

Enter fullscreen mode Exit fullscreen mode
  • Now change some setting in your tsconfig.ts file inside compilerOptions object
    "jsx": "react",
    "skipLibCheck": true,
    "noImplicitAny": true,
Enter fullscreen mode Exit fullscreen mode
  • Put some polotno default css in index.html
 <link href="https://unpkg.com/@blueprintjs/icons@4/lib/css/blueprint-icons.css" rel="stylesheet"/>
<link href="https://unpkg.com/@blueprintjs/core@4/lib/css/blueprint.css"
      rel="stylesheet"/>

<link href="https://unpkg.com/@blueprintjs/popover2@1/lib/css/blueprint-popover2.css" rel="stylesheet"/>

Enter fullscreen mode Exit fullscreen mode
  • Now run your project using npm start and Open your browser and run localhost:4200 and see the magic Image description

Test with codesendbox

  • Over To You!

Looking for a Sample Source Code? Here you go: GitHub.

Image of Timescale

Timescale – the developer's data platform for modern apps, built on PostgreSQL

Timescale Cloud is PostgreSQL optimized for speed, scale, and performance. Over 3 million IoT, AI, crypto, and dev tool apps are powered by Timescale. Try it free today! No credit card required.

Try free

Top comments (0)

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

👋 Kindness is contagious

Discover a treasure trove of wisdom within this insightful piece, highly respected in the nurturing DEV Community enviroment. Developers, whether novice or expert, are encouraged to participate and add to our shared knowledge basin.

A simple "thank you" can illuminate someone's day. Express your appreciation in the comments section!

On DEV, sharing ideas smoothens our journey and strengthens our community ties. Learn something useful? Offering a quick thanks to the author is deeply appreciated.

Okay