DEV Community

Cover image for Drawing with FabricJS and TypeScript Part 5: Deleting Objects
Matthew Jones
Matthew Jones

Posted on • Originally published at exceptionnotfound.net

Drawing with FabricJS and TypeScript Part 5: Deleting Objects

We've now completed our implementation for drawing simple objects in FabricJS, using our TypeScript models. Let's now work on how to remove objects from the canvas.

Lighter not included. Photo by Devin Avery / Unsplash

The Sample Project

As always, there's a sample project over on GitHub that contains the completed code for this series. Check it out!

Implementing the DeleteComponent

We're going to implement this delete functionality a bit backwards from the way we've implement other functionality: we're going to write the component first, then integrate it with the drawing editor.

Our component will exist in the toolbar, but needs to only be active when an object is selected; otherwise it should be disabled. Further, this isn't going to be the only component that changes the canvas: later in this series we're going to implement undo/redo functionality. So, we need a "base" class for all components that will do non-drawing functionality.

Chris termed these components ControlComponents, and implemented the following abstract class:

abstract class ControlComponent {
    target: string; //Selector for the component's render location
    cssClass: string; //CSS classes for icons
    hoverText: string; //Tooltip text
    canvassDrawer: DrawingEditor;
    handlers: { [key: string]: () => void };

    constructor(selector: string, classNames: string, altText: string, parent: DrawingEditor, handlers: { [key: string]: () => void }) {
        this.target = selector;
        this.cssClass = classNames;
        this.hoverText = altText;
        this.canvassDrawer = parent;
        this.render();
        this.handlers = handlers;
        this.attachEvents();
    }

    abstract render();

    attachEvents() {
        if (this.handlers['click'] != null) {
            $(this.target).click(this, () => {
                this.handlers['click']();
            });
        }

        if (this.handlers['change'] != null) {
            $(this.target).change(this, () => {
                this.handlers['change']();
            });
        }
    }
}

All of our non-drawing toolbar items will inherit from this abstract class.

Speaking of which, we now need such a class for the delete component. Here's the annotated code:

class DeleteComponent extends ControlComponent {
    constructor(target: string, parent: DrawingEditor) {
        super(
            target,
            "fa fa-trash-o", //CSS class for icons
            "Delete Selected Item", //Tooltip text
            parent,
            {
                //The component invokes a method 
                //on DrawingEditor to delete selected objects.
                'click': () => { parent.deleteSelected(); }
            });
    }

    //Render a disabled button with a trash can icon
    render() {
        const html = `<button id="${this.target.replace('#', '')}" title="${this.hoverText}" disabled class="btn btn-danger">
                        <i class="${this.cssClass}"></i>
                     </button>`;

        $(this.target).replaceWith(html);
    }

    //Enable the button
    //Will be called when a canvas object is selected
    enable() {
        var ele = document.getElementById(this.target.replace('#', ''));

        Object.assign(ele, {
            disabled: false
        });
    }

    //Disable the button
    //Will be called when no canvas objects are selected
    disable() {
        var ele = document.getElementById(this.target.replace('#', ''));

        Object.assign(ele, {
            disabled: true
        });
    }
}

With our component written, it's time to implement a few changes on the main DrawingEditor class.

Modifying the DrawingEditor

Our main DrawingEditor class now needs to react when canvas elements are selected, so it can enable or disable the delete button.

Here's the scenarios we want to react to:

  1. First, if an item is selected in the canvas, enable the Delete button.
  2. If no items are selected in the canvas, disable the Delete button.
  3. If the user clicks the Delete key, delete the selected objects.

We can implement the first two scenarios by modifying the DrawingEditor class to enable/disable the delete button and implement the deleteSelected() method needed by our DeleteComponent.

class DrawingEditor {
    //...Properties and Constructor

    private initializeCanvasEvents() {
        //...Other events

        this.canvas.on('object:selected', (o) => {
            this.cursorMode = CursorMode.Select;
            //sets currently selected object
            this.object = o.target;

            //If the delete component exists, enable it
            if (this.components['delete'] !== undefined) {
                (this.components['delete'][0] as DeleteComponent).enable();
            }
        });

        this.canvas.on('selection:cleared', (o) => {
            //If the delete component exists, disable it
            if (this.components['delete'] !== undefined) {
                (this.components['delete'][0] as DeleteComponent).disable();
            }

            this.cursorMode = CursorMode.Draw;
        });
    }

    //...Other methods

    addComponent(target: string, component: string) {
        switch (component) {
            case 'line':
                this.components[component] 
                  = [new LineDisplayComponent(target, this)];
                break;
            case 'rect':
                this.components[component] 
                  = [new RectangleDisplayComponent(target, this)];
                break;
            case 'oval':
                this.components[component] 
                  = [new OvalDisplayComponent(target, this)];
                break;
            case 'tria':
                this.components[component] 
                  = [new TriangleDisplayComponent(target, this)];
                break;
            case 'text':
                this.components[component] 
                  = [new TextDisplayComponent(target, this)];
                break;
            case 'polyline':
                this.components[component] 
                  = [new PolylineDisplayComponent(target, this)];
                break;
            //New component
            case 'delete':
                this.components[component] 
                  = [new DeleteComponent(target, this)];
                break;
        }
    }

    //...Other methods

    deleteSelected(): void {
        //Get currently-selected object
        const obj = this.canvas.getActiveObject();

        //Delete currently-selected object
        this.canvas.remove(this.canvas.getActiveObject());
        this.canvas.renderAll(); //Re-render the drawing in Fabric
    }

Modifying the Markup and Script

There's something little bit strange about FabricJS's Canvas object: it doesn't provide events for keydown or keypress, so we have to wire those events up against the page, and then call the appropriate methods in the DrawingEditor class.

Here's the markup changes and script changes we need to make in our Razor Page. Note that the delete button will sit by itself on the leftmost side of the toolbar:

<div class="row bg-secondary text-light">
    <div class="btn-toolbar">
        <div class="btn-group mr-2" role="group">
            <!-- New Component -->
            <div id="deleteComponent"></div>
        </div>
    </div>
    <div class="col-sm-11">
        <div class="row col-12">
            <div class="row drawingToolbar">
                <label class="col-form-label controlLabel">Tools:</label>
                <div class="d-inline-block">
                    <div class="btn-group btn-group-toggle" role="group" data-toggle="buttons" aria-label="Drawing Components">
                        <div id="polylineDisplayComponent"></div>
                        <div id="lineDisplayComponent"></div>
                        <div id="rectangleDisplayComponent"></div>
                        <div id="ovalDisplayComponent"></div>
                        <div id="triangleDisplayComponent"></div>
                        <div id="textDisplayComponent"></div>
                    </div>
                </div>
            </div>
        </div>
    </div>
</div>

Note also that we need to implement a new listener event for "keydown" in the script below:

@section Scripts{
    //...Get scripts
    <script>
        var editor; //We now need to access the editor outside of the
                    //initialization method
        $(function () {
            //...Instantiate editor

            //Create a list of available display components
            const components = [
                { id: '#lineDisplayComponent', type: 'line' },
                { id: '#rectangleDisplayComponent', type: 'rect' },
                { id: '#ovalDisplayComponent', type: 'oval' },
                { id: '#triangleDisplayComponent', type: 'tria' },
                { id: '#textDisplayComponent', type: 'text' },
                { id: '#polylineDisplayComponent', type: 'polyline' },
                //New component
                { id: '#deleteComponent', type: 'delete'}
            ];

            //Add the components to the DrawingEditor, which will render them.
            editor.addComponents(components);
            $('#lineDisplayComponent').click();

            //New event, which listens for the delete key
            $("html").on('keydown', function (event) {
                const key = event.key;
                if (key == "Delete") {
                    editor.deleteSelected();
                }
            });
        });
    </script>
}

GIF Time!

Once the drawer, display component, markup, and script are updated, we can now delete objects on our canvas! Here's an example GIF:

Ta-da! We've completed the delete functionality for our drawing canvas!

Summary

To implement deletion of the selected object in our drawing canvas, we needed to:

  1. Create a new abstract base component for a toolbar control.
  2. Implement the abstract base component to create a Delete component.
  3. Wire up the delete component to the DrawingEditor class.
  4. Listen for the Delete key, and delete selected objects.

Don't forget to check out the sample project over on GitHub!

In the next part of this series, we will implement an undo/redo function, in which we keep track of the items being added to the canvas and can undo or redo them at any time!

Happy Drawing!

Top comments (0)