card-dom
HTML
<div>
<h2>Card DOM</h2>
<p>This is a JS solution that creates simple DOM for a Card Editor/Player from this post
<a href="https://www.linkedin.com/pulse/lets-compare-programming-languages-more-fun-painful-way-kalmatskiy-oydie">Let's Compare Programming Languages in a More Fun (and Painful) Way</a>
<p>All requirements was met. But some of them required excessive coding, and some are not space and time efficient. Some checks are feasible only in runtime which leads to runtime exceptions.
<p>What JS gives us out of the box:
<ul>
<li>No memory leaks (eventually by GC)</li>
<li>Protection against deleted object while they are referenced from stack</li>
</ul>
What was implemented manually:
<ul>
<li>Connector from/to turn null when its target is removed, the same for Button's target link. To make this manually wee had to implement registration of links in a hash sets and backpropagarion of detach events. There are alternatives but they require indirections and they are harder to implement in this small example.</li>
<li>Topologycally correct copy operations. We implemented two step process - making copy and weka resolution and a Copy Context holding a hash map between original and copy objects.</li>
<li>Unshare on mutation strategy for styles and bitmaps guarantees. Again due JS limitations, it's a runtime only safety net thatt harms resilience, but it's normal for such an typeless interpreted language as JS.</li>
<li>Safety net against multiparenting and loops by GroupItem inside itself. It's runtime only for the same reason. It requires parent chain scan on each attach to parent. But it works.</li>
</div>
CSS
div{
font: 11pt sans-serif;
line-height: 14pt;
max-width: 500px;
margin: auto;
}
div * {
margin: 10px 0px;
}
JavaScript
class DeepCopyContext {
constructor() {
this.nodeMap = new Map(); // original → copy
}
register(original, copy) {
this.nodeMap.set(original, copy);
return copy;
}
resolve(original) {
return this.nodeMap.get(original) || original;
}
}
class Document {
constructor() {
this.cards = [];
}
addCard(card) {
card.setParent(this);
this.cards.push(card);
}
removeCard(card) {
this.cards = this.cards.filter(c => c !== card);
card.detach();
}
deepCopy(ctx) {
const newDoc = new Document();
for (const card of this.cards) {
newDoc.addCard(card.deepCopy(ctx));
}
return ctx.register(this, newDoc);
}
resolve(ctx) {
for (const card of this.cards) {
card.resolve(ctx);
}
}
}
class Card {
constructor() {
this.items = [];
this.parent = null;
this.inboundButtons = new Set(); // Track buttons pointing to this card
}
addItem(item) {
item.setParent(this);
this.items.push(item);
}
removeItem(item) {
this.items = this.items.filter(i => i !== item);
item.detach();
}
registerButton(button) {
this.inboundButtons.add(button);
}
unregisterButton(button) {
this.inboundButtons.delete(button);
}
setParent(parent) {
if (this.parent && this.parent !== parent) {
throw new Error("Card already has a parent");
}
this.parent = parent;
}
detach() {
for (const item of this.items) item.detach();
for (const button of this.inboundButtons) {
button.setTargetCard(null);
}
this.inboundButtons.clear();
}
deepCopy(ctx) {
const newCard = new Card();
for (const item of this.items) {
newCard.addItem(item.deepCopy(ctx));
}
return ctx.register(this, newCard);
}
resolve(ctx) {
for (const item of this.items) {
item.resolve(ctx);
}
}
}
class CardItem {
...