Building a Rich Text Editor: Day 2
Today, I’ll be segregating the features and design more models that I need. Notice that, I developed and solved my problems as I encounter them. In my last post, if you see the parameters passed on onKeyDown and onEnterKey. This was decided totally on the basis of the local problem-solving goal. As the project grows, the parameters tend to change and the iterative refactoring results in chaos.
Chaos is inevitable
Furthermore, this hinders development. Since changing existing code is difficult, I’ll tend to take the easier path and add another method to the component that does the same task with different parameters.
To solve this, I’ll be giving a structure to the development of any feature. Let's try designing the flow of a simple feature, insert character. Insertion would require the following scenarios handled:
- Insert a character at an index in the block. If the length of the current block has reached the horizontal end of the page, split the current block and append the later half to the beginning of the next block.
- This would go on recursively till horizontal overflow isn’t there anymore. But, wait! What if the current block is the last block of the page? Then we’ll have to append a new block at the end.
- This would also mean a time will come when the page starts overflowing with blocks. In this case, the last block will be moved to the next page.
Phew! No more overflows. Say I want to list down the methods I’ll need:
- onKeyDownHandler detects keypress directly and checks if for the type of input, like keyboard shortcut, character input, etc.
- Operation: this is the high-level logical portion where the current state of caret, the document will be analyzed and keyboard input will be used to manipulate the document through DOM Manipulators and Utilities. This would also mean the state of the document and other models will be updated as needed.
We can see that the second operation is getting complex. A lot of tasks would require document state to be updated. I’ll try to structure the Operation Methods internally.
Say for example:
No need to verify the algorithm. This is just to give an idea of how complex a simple enter key could be. What happens if I miss out on a case? It is difficult to think of all possible scenarios like this.
In the above example, if you look at the 3rd test condition: if the caret is at end of the page, I’ve missed the recursive nature of this problem: Creating a new block on the next page can cause overflow on the next page. And, handling overflow on the next page could trigger overflow on the following pages. What if there were other models involved? The if-else would ladder could simply be difficult to manage. And this is the case with just one simple keypress: enter key.
The problem is that it is difficult to predict how changing one model affects the state of other models and following these paths result in a tree. However, if we build this tree and consider the interaction between the root (first model that is manipulated) and its children, the models that are directly interacting with this root, it becomes extremely easy to predict the behavior.
So say model A can interact with model B through 3 events: x, y z. For instance, the block can interact with the page by saying, “I got deleted”, “I got inserted” and “I got highlighted” (text selection). Now, these pretty easy to handle by a page. In fact, the page does not really have to bother if a block got highlighted.
There must be some way to make these models behave like real-world organisms. So, it is made sure that they are updating their states independently. This means that the source point of the problem does not need to know if the components directly affected by it are having any problems and the way they are getting solved.
How is this possible? Well, why is this not possible? A possible answer would be that the component does not know what problems are it able to cause, say, blocks do not know that they overflow a page. All they know is that they are always surrounded by blocks. But, the end block does know that it is at the end of the page and has overflown the height of the page. Is there some way to pass information? For instance, if a new block is created in the middle of the page, can they pass information to the ends.
They are fairly easy to implement concepts. Basically, it’s a chat room. We talk to our friends daily on WhatsApp. If we have a specific project to complete, we form new group chats where the relevant people are added. Similarly, all models join a chat room. These chat rooms are called events. The name of a chat room is related to the type of data they are going to share with each other. If a model sends a message to the room, we say that the model has “published” some “data” to the “event”. And, if a model wants to receive the messages coming through an “event”, we say that a model has “subscribed” to the event.
Another method is to predefine any state through getter setters. However, the problem is that I’ll have to hard code each connection between models. Say I have defined 4 models and developed its connections through some getter setter tools. Each time I add a new model, I’ll have to make changes in each model to handle the interaction with this new model. In the case of publisher-subscribers, I will never have to update existing models to pass the state information to the new model. Although the processing is pretty much minimized and efficient, the complexity of the code increases tremendously.
Avoiding the Chaos
Considering the fact that DOM already has mouse and keyboard events and we subscribe to them easily, adding a few more events should not be a problem.
1. data updated
1. config updated
2. block inserted at a location
3. block deleted at a location
1. config updated
2. page inserted at a location
3. page deleted at a location
How are these events designed? These events are based on the properties of the Document Core designed earlier. Each model will be able to monitor directly if any changes were made to their properties. Easy to manage, easy to implement.
So, what’s the final decision?
It might seem I’m going with the former option to use Pub/Subs. It might even seem it would be easy to design the “events”. A Block can simply subscribe to a Page and a Page would subscribe to a Document. These are efficient designs for events.
However, how am I going to update the data in a block?
One way would be to tell Document to pass data to the page which would eventually pass data to the target block. Say there are 30 blocks per page and there are 40 pages. That would mean 120 blocks subscribe to 40 pages and 40 pages subscribe to 1 DOM just to update few characters in a single block. Scaling this system would cause trouble if more than 1 document has been opened in the application. This is going to slow down the application immensely. Why? This is because when the document broadcasts updates to all the pages, other pages, ie, blocks on other pages would also receive this update, and finding that it was not meant for them, it’ll be discarded. This is a wastage of processing power.
I’m thinking of a hybrid solution in mind. The simple software design that uses CRUD methods and interfaces to update data is efficient in terms of performance. Can we do use that mechanism to update data and use “events” to trigger changes in data?
That’s all for now.