2

This question is language/framework agnostic. EDIT: The comments and answers have highlighted how different languages approach events differently, which made me realize this question isn't really neutral across languages or frameworks. To clarify, I'm using Vanilla JS, and my components are built with Web Components. While I do need a JS-focused solution, I'm also keen on understanding the bigger picture, as if JavaScript wasn't a limiting factor.

I'm working on a canvas component (EDIT: i.e. a web component encapsulating a canvas element) that allows users to draw a rectangle. For data flow, I use the props-down, events-up principle: parents pass data to children using the children's properties, and children pass data to parents by raising events.

An issue arises when the user intends to create a rectangle that touches the canvas' edges: they must left-click with pixel-perfect precision on the edge, which can be challenging. To enhance usability, I want to enable users to start drawing from outside the component.


EDIT: To clarify, as long as the mouse is down, the rectangle should be updated based on the mouse's position. The user should be allowed to move the mouse freely, without the rectangle "freezing" when the mouse leaves the child component. The component should behave as if it were a "window" through which the user can see the rectangle, which is drawn behind it.

Here is an updated illustration:

Illustration


Since the component is only aware of mouse events occurring within its boundaries, we cannot naively react to these events from within the component. The obvious solution would be to handle them from the parent:

// Pseudo-code
Parent {
    onMouseDown() {
        dragging = true
        startPosition = ...
    }
    onMouseMove() {
        if(dragging)
            child.rectangle = [startPosition, currentPosition]
    }
    onMouseUp() {
        dragging = false
    }
}

However, this approach somewhat violates the component encapsulation: as drawing the rectangle is a behavior inherently associated with the component, it should really be encapsulated within it. Furthermore, this approach hurts reusability, necessitating each parent to individually handle the mouse events when reusing the component in different contexts.

A different approach is to pass the child a reference to the parent, so it can handle the mouse events for itself:

Parent {
    initialize() {
        child.parent = this
    }
}
Child {
    parent.onMouseDown() {
        dragging = true
        startPosition = ...
    }
    parent.onMouseMove() {
        if(dragging)
            DrawRectangle(...)
    }
    onMouseUp() {
        dragging = false
    }
}

But should the child really have access, let alone control, over the parent state? I think not.

What is the proper way of handling this behavior?

2 Answers2

3

Given 100% flexibility in the language/framework used, "best practice" would suggest that you pass in a mouse interface to the constructor of the component. ie

RectComponent
{
   IMouse mouse
   RectComponent(IMouse mouse) {
     this.mouse = mouse;
     this.mouse.OnClick += dowhatever //remember to unsubscribe!
     ...
   }
}

Similar to your passing in the parent component.

However, There are some issues with the event subscription and it looks like you are working in a web browser so you have a bunch of constraints that might make this a bit ugly to implement.

Instead you could do the binding of event at the top level of your page.

Parent
{
   RectComponent child

onMouseDown => { child.MouseDown(mouse.x, mouse.y); } ... }

With the binding done at the top level, you can orchestrate the events easily over multiple child components, but also keep the logic of what those components actually do when the events happen in the components themselves.

Personally I would be tempted to go further and have:

Parent
{
   RectComponent child

onMouseDown => { child.StartRectangle(mouse.x, mouse.y); } ... }

This brings some of the "logic" out into your parent, but its fair enough, just say the component doesn't care how you draw, it could be a mouse or keyboard or programmed event and its no longer the responsibility of the component.

This makes for a neater more flexible components with a single place of event orchestration at the top level parent.

You could also go with a mediator pattern, but I think this is basically the same as passing in the parent, but with more code.

Ewan
  • 83,178
0

I think it depends on the level of abstraction you want to have. If you limit the implementation to a single element then yes, you inevitably leak implementation details in the first approach. However, the second approach is even worse in my opinion cause it negates reusability, since the user will have to wire it all up every single time.

How about instead of limiting yourself to a single element, you treat it as a black box which includes all the elements, mechanics and wiring to just work? Essentially, what everyone calls nowadays a component is the ideal abstraction level.

Nick Ribal
  • 109
  • 3