Desmond

Desmond

An introvert who loves web programming, graphic design and guitar
github
bilibili
twitter

Commonly Used Design Patterns in Frontend

Abstract Behavior Pattern#

Image Preview#

Our task is to add a "preview" feature to image elements in a fixed list. Online Demo
The corresponding HTML page is as follows:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Image Preview</title>
  <style>
    #list {
      list-style-type: none;
      justify-content: flex-start;
      display: flex;
      flex-wrap: wrap;
    }

    #list li {
      padding: 10px;
      margin: 0;
    }
    #list img {
      height: 200px;
      cursor: pointer;
    }
  </style>
</head>
<body>
  <ul id="list">
    <li>
      <img src="https://p4.ssl.qhimg.com/t01713d89cfdb45cdf5.jpg">
    </li>
    <li>
      <img src="https://p4.ssl.qhimg.com/t01e456146c8f8a639a.jpg">
    </li>
    <li>
      <img src="https://p1.ssl.qhimg.com/t015f613e2205b573d8.jpg">
    </li>
    <li>
      <img src="https://p0.ssl.qhimg.com/t01290338a28018d404.jpg">
    </li>
    <li>
      <img src="https://p3.ssl.qhimg.com/t01d9aa5ae469c8862e.jpg">
    </li>
    <li>
      <img src="https://p3.ssl.qhimg.com/t01cb20d35fc4aa3c0d.jpg">
    </li>
    <li>
      <img src="https://p5.ssl.qhimg.com/t0110b30256941b9611.jpg">
    </li>
  </ul>
</body>
</html>
const list = document.getElementById('list');
list.addEventListener('click', (evt) => {
  const target = evt.target;
  if(target.tagName === 'IMG') {
    preview(list, target);
  }
});
list.addEventListener('preview', ({detail}) => {
  detail.showMask();
});
function useBehavior(context) {
  const {type, getDetail} = context;
  return function (subject, target) {
    const event = new CustomEvent(type, {bubbles: true, detail: getDetail.call(context, subject, target)});
    target.dispatchEvent(event);
  };
}
const preview = useBehavior({
  type: 'preview',

  /*
    @subject: <ul> element
    @target: selected image element
  */
  getDetail(subject, target) {
    const imgs = Array.from(subject.querySelectorAll('img'));
    const selected = imgs.indexOf(target); // Get the index of the selected image in the image collection
    let mask = document.getElementById('mask');

    // If mask does not exist, create a mask element
    if(!mask) {
      mask = document.createElement('div');
      mask.id = 'mask';
      mask.innerHTML = `
        <a class="previous" href="###">&lt;</a>
        <img src="${imgs[selected].src}">
        <a class="next" href="###">&gt;</a>    
      `;
      // Set styles for the #mask element:
      Object.assign(mask.style, {
        position: 'absolute',
        left: 0,
        top: 0,
        width: '100%',
        height: '100%',
        backgroundColor: 'rgba(0,0,0,0.8)',
        display: 'none',
        alignItems: 'center',
        justifyContent: 'space-between',
      });

      // Set styles for the <a> elements on both sides of the #mask element:
      mask.querySelectorAll('a').forEach((a) => {
        Object.assign(a.style, {
          width: '30px',
          textAlign: 'center',
          fontSize: '2rem',
          color: '#fff',
          textDecoration: 'none',
        });
      });
      document.body.appendChild(mask);

      // Add click event handler for the #mask element:
      let idx = selected;
      mask.addEventListener('click', (evt) => {
        const target = evt.target;
        if(target === mask) { // If the clicked object is the mask element, hide the mask element
          mask.style.display = 'none';
        } else if(target.className === 'previous') { // Show the previous image
          update(--idx);
        } else if(target.className === 'next') { // Show the next image
          update(++idx);
        }
      });
    }

    // Set the src attribute of the img element to point to the specified image
    function update(idx) {
      const [previous, next] = [...mask.querySelectorAll('a')];
      previous.style.visibility = idx ? 'visible' : 'hidden';
      next.style.visibility = idx < imgs.length - 1 ? 'visible' : 'hidden';
      const img = mask.querySelector('img');
      img.src = imgs[idx].src;
    }

    return {
      showMask() { // Show the preview of the selected image
        mask.style.display = 'flex';
        update(selected);
      },
    };
  },
});

Image Selector#

Online Demo

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Abstract Behavior</title>
  <style>
    #list {
      list-style-type: none;
      justify-content: flex-start;
      display: flex;
      flex-wrap: wrap;
    }

    #list li {
      padding: 10px;
      margin: 0;
    }
    #list img {
      height: 200px;
      cursor: pointer;
      box-sizing: border-box;
      padding: 5px;
    }

    #list img.selected {
      border: solid 5px #37c;
      padding: 0;
    }
  </style>
</head>
<body>
  <ul id="list">
    <li>
      <img src="https://p4.ssl.qhimg.com/t01713d89cfdb45cdf5.jpg">
    </li>
    <li>
      <img src="https://p4.ssl.qhimg.com/t01e456146c8f8a639a.jpg">
    </li>
    <li>
      <img src="https://p1.ssl.qhimg.com/t015f613e2205b573d8.jpg">
    </li>
    <li>
      <img src="https://p0.ssl.qhimg.com/t01290338a28018d404.jpg">
    </li>
    <li>
      <img src="https://p3.ssl.qhimg.com/t01d9aa5ae469c8862e.jpg">
    </li>
    <li>
      <img src="https://p3.ssl.qhimg.com/t01cb20d35fc4aa3c0d.jpg">
    </li>
    <li>
      <img src="https://p5.ssl.qhimg.com/t0110b30256941b9611.jpg">
    </li>
  </ul>
</body>
</html>
function useBehavior(context) {
  const {type, getDetail} = context;
  return function(subject, target) {
    const event = new CustomEvent(type, {bubbles: true, detail: getDetail.call(context, subject, target)});
    target.dispatchEvent(event);
  }
}

const select = useBehavior({
  type: 'select',
  data: {
    picked: new Set(), // Set of selected images
  },
  getDetail(subject, target) {
    const picked = this.data.picked;

    if(picked.has(target)) {
      target.className = '';
      picked.delete(target);
    } else {
      target.className = 'selected';
      picked.add(target);
    }

    return {
      changed: target,
      picked,
    };
  },
});
const list = document.getElementById('list');
list.addEventListener('click', (evt) => {
  const target = evt.target;
  if(target.tagName === 'IMG') {
    select(list, target);
  }
});

list.addEventListener('select', ({detail}) => {
  // do nothing
  console.log(detail.changed, detail.picked);
});

Through the first story and the second story, we can combine the "preview" and "select" behaviors. For example, when the mouse is clicked while holding down the alt key, it indicates selecting the image; otherwise, it is to preview the image:

/* ...other code omitted... */

const list = document.getElementById('list');
list.addEventListener('click', (evt) => {
  const target = evt.target;
  if(target.tagName === 'IMG') {
    if(evt.altKey) {
      select(list, target);
    } else {
      preview(list, target);
    }
  }
});

list.addEventListener('preview', ({detail}) => {
  const {showMask} = detail;
  showMask();
});

As shown in the code above, the abstract behavior pattern allows a component to flexibly combine or unload multiple behaviors without conflict.

Mediator Pattern#

Our next new task is to implement a synchronized scrolling editor and preview area, which is a common interaction form in some online editing web applications. Online Demo

<body>
  <textarea id="editor" oninput="this.editor.update()"
            rows="6" cols="60">
Between 2001 and 2003, Judith Miller published a series of articles in The New York Times claiming that Iraq had the capability and ambition to produce weapons of mass destruction. This is fake news.

Looking back, we cannot determine the role these stories by Miller played in the decision made in the U.S. in 2013 to launch the Iraq War; the information from the same sources as Miller was closely linked to the foreign policy team of the Bush administration. However, The New York Times still played a role in endorsing this policy, especially to Democrats, who should have been more firmly opposed to Bush's policies. After all, The New York Times is not some obscure local tabloid; it is the most influential newspaper in the entire United States, generally considered to have a left-leaning bias. Miller's stories somewhat aligned with the newspaper's political leanings.

We can relate Miller's mistakes to the recent fake news issues surrounding Facebook; Facebook tells us through its own stories that "fake news is bad." However, I hold a different view: **Whether news is fake or not is not as important as who decides what is news.**

<!--more-->

#### Facebook's Media Commercialization

In [Aggregation Theory](https://stratechery.com/2015/aggregation-theory/), I described how the demise of economic rights based on distribution led to the rise of powerful intermediaries that control customer experience and commodify their suppliers. [In the case of Facebook](https://stratechery.com/2016/the-fang-playbook/), social networks emerged because offline social networks were transitioning to online networks. Given that human nature is social, users began spending time on Facebook reading, expressing opinions, and obtaining news.

...(content omitted)
                     
  </textarea>
  <div id="preview"> </div>
  <div id="hintbar"> 0% </div>
</body>
body{
  display: flex;
}

#editor {
  width: 45%;
  height: 350px;
  margin-right: 10px;
}

#preview {
  width: 45%;
  height: 350px;
  overflow: scroll;
}

#hintbar {
  position: absolute;
  right: 10px;
}
function Editor(input, preview) {
  this.update = function () {
    preview.innerHTML = markdown.toHTML(input.value);
  };
  input.editor = this;
  this.update();
}
new Editor(editor, preview);

class PubSub {
  constructor() {
    this.subscribers = {};
  }
  pub(type, sender, data){
    var subscribers = this.subscribers[type];
    subscribers.forEach(function(subscriber){
      subscriber({type, sender, data});
    });
  }
  sub(type, receiver, fn){
    this.subscribers[type] = this.subscribers[type] || [];
    this.subscribers[type].push(fn.bind(receiver));
  }
}

function scrollTo({data:p}){
  this.scrollTop = p * (this.scrollHeight - this.clientHeight);
}

var mediator = new PubSub();
mediator.sub('scroll', preview, scrollTo);
mediator.sub('scroll', editor, scrollTo);
mediator.sub('scroll', hintbar, function({data:p}){
  this.innerHTML = Math.round(p * 100) + '%';
});

function debounce(fn, ms = 100) {
  let debounceTimer = null;
  return function(...args) {
    if(debounceTimer) clearTimeout(debounceTimer);

    debounceTimer = setTimeout(() => {
      fn.apply(this, args);
    }, ms);
  }
}

let scrollingTarget = null;
editor.addEventListener('scroll', debounce(function(evt){
  scrollingTarget = null;
}));

function updateScroll(evt) {
  var target = evt.target;
  if(!scrollingTarget) scrollingTarget = target;
  if(scrollingTarget === target) {
    var scrollRange = target.scrollHeight - target.clientHeight,
      p = target.scrollTop / scrollRange;
    mediator.pub('scroll', target, p);
  }
}
editor.addEventListener('scroll', updateScroll);
preview.addEventListener('scroll', updateScroll);
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.