Core
In practice, Duck is based on two things,
  • Stores
  • Components
In a nutshell, stores are the logical/stateful part of your app and should be developed first. Components are the visual representation of your app state and should be developed after. When used together, stores and components provide a simple yet scalable means of developing reactive applications 👍.
You can organize your stores and components however you'd like - it's up to you and your unique application. Simple applications may have a single store and just a few components.
Below is an architectural diagram of what a Duck app with a main store, a secondary store, a main component, and several additional components may be designed like,
To provide an example, the above architecture could be a chat app with a login page followed by another page with the chat itself. You could use one store for user data (perhaps a list of friends) and another store for the actual chat data (a list of messages).
Before diving deeper into stores and components in Duck, let's quickly go over their basis, the Duck (conveniently named).
Ducks
In Duck, a Duck is a "factory function" that takes in a single parameter, a data object, and returns an object which exposes things to the outside world. That may sound fancy for beginners, but don't panic, it's extremely simple.
Here it is,
function Duck(data) {

  // variables and functions
  // ...

  return {
    // expose variables and functions
    // ...
  };
}
JavaScript
To create an instance of a Duck,
let duck = new Duck({ /*...*/ });
JavaScript
Here's a quick example,
function Duck(data) {
  let sound = data.sound;
  let voice = data.voice;

  function move() {
    console.log(`${sound} ${sound} ${sound}...`);
  }

  function talk() {
    console.log(`${voice}!`);
  }

  return {
    move,
    talk,
  };
}
JavaScript
let walkingDuck = new Duck({ sound: 'plop', voice: 'quack' });
let flyingDuck = new Duck({ sound: 'swoosh', voice: 'woohoo' });
walkingDuck.move();
walkingDuck.talk();
flyingDuck.move();
flyingDuck.talk();
JavaScript
RESULT
plop plop plop...
quack!
swoosh swoosh swoosh...
woohoo!
Plain text
Components
In Duck, a Component is a manager for data and a DOM element.
More technically, a Component is a Duck with the following structure,
function Component(data) {
  let elem;

  // create elem and add event listeners
  function construct() {}

  // a function to call to update elem
  function update() {}

  // a function to call to do something after elem is placed into DOM
  function enter() {}

  // a function to call to do something before elem is removed from DOM
  function exit() {}

  // a function to call to remove event listeners
  function destroy() {}

  construct();
  return { elem, data, update, enter, exit, destroy };
}
JavaScript
elem
The variable elem is the DOM element that the component manages. Duck leaves it up to the outside world to add/remove elem to/from the DOM. In other words, don't call document.append(elem) in your components.
construct()
The construct() function is a private function for creating elem.
update()
The update() function is called to make the contents of elem reflect the contents of data. In update(), do whatever you want to do to make elem reflect data.
enter() & exit()
The enter() and exit() functions are essentially "lifecycle methods". They must be defined, but can be left empty. Duck leaves it up to the outside world to call enter() and exit(), but they don't necessarily have to called.
destroy()
The destroy() function should be called when elem is being removed from the DOM in order to prevent memory leaks. Just like enter() and exit(), destroy() must be called externally.
data
The last, but not least, thing to mention is data. You can think of data as a sandbox of values/references. The data variable is the interface that the component has with the outside world. It's conventional to change something in component.data and then call component.update() to see the changes be reflected.
IMPORTANT CAVEAT: You must always pass a fresh/new/unique object as data when creating a component in Duck. This is because component instances use that same passed-in data object directly as their own component.data object. The reason why this is done is to keep Duck as stripped down as possible - following the Duck philosophy of being as basic as possible.
Here's a quick example of implementing a toggle button in Duck,
function ToggleButton(data) {
  let elem;

  function construct() {
    elem = document.createElement('button');
    elem.className = 'toggle-button';
    elem.style.width = '70px';
    elem.addEventListener('click', onClick);
    update();
  }

  function update() {
    if(data.active) {
      elem.innerHTML = 'ON';
      elem.style.backgroundColor = `hsla(${data.hue || 240}, 80%, 70%, 1)`;
    }else{
      elem.innerHTML = 'OFF';
      elem.style.backgroundColor = `hsla(0, 0%, 60%, 1)`;
    }
  }

  function enter() {}
  function exit() {}

  function destroy() {
    elem.removeEventListener('click', onClick);
  }

  function onClick() {
    data.active = !data.active;
    update();
  }

  construct();
  return { elem, data, update, enter, exit, destroy };
}
JavaScript
const container = document.getElementById('toggle-button-app');

// toggle button
const toggleButton = new ToggleButton({ active: true, hue: 130 });
container.appendChild(toggleButton.elem);

// change hue function
function randomizeActiveHue() {
  toggleButton.data.hue = Math.round(360*Math.random());
  toggleButton.update();
}
JavaScript
RESULT

Stores
In Duck, a store is a container for mediating state that exposes getters, setters, and events.
More technically, a Store is a Duck with the following structure,
NOTE: Please ignore the "events" section for now...
function Store(data) {
  const events = {};

  // internal variables
  // ...

  // functions
  // ...

  return {
    // expose functions
    // ...
    on, off
  };

  ////////////////////////////////
  // events
  function on(name, handler) {
    if(!events[name]) { events[name] = []; }
    events[name].push(handler);
  }
  function off(name, handler) {
    if(events[name]) {
      const index = events[name].indexOf(handler);
      if(index != -1) { events[name].splice(index, 1); }
    }
  }
  function emit(name, value) {
    if(events[name]) {
      for(let i = 0; i < events[name].length; i++) {
        events[name][i](value);
      }
    }
  }
  ////////////////////////////////
}
JavaScript
Unlike components where data is entirely public and update() is the single go-to function to call, stores are seemingly completely opposite.
A store has all variables entirely private and can have as many publicly exposed functions as you want in order to get/change those private variables.
The events variable at the top along with the on(), off(), and emit() functions at the bottom are a basic pubsub implementation. These functions allow the outside world to subscribe to the happenings of the store. In practice, you should just ignore the events variable and the pubsub functions entirely. In fact, you can ignore them right now! (For whatever reason: not interested, it looks intimidating, etc)
There are two main types of functions that you can create in a store: getter functions and setter functions. Getter functions should return the values of internal variables. Setter functions should change the values of internal variables and then emit that a change has occured to the outside world.
NOTE: It's worth mentioning that you don't necessarily NEED to change a value or even emit in a setter function. Technically, a Duck setter function is any function that isn't a getter function.
For example, a "setter function" could be an async function that fetches data from a REST API.
A more appropriate name for "setter functions" is probably something like "action functions", but I find that most of my "actions" are just plain old "set" actions, so I just call them setter functions... But, feel free to call them "action functions" if that makes more sense to you!
Here's a quick example,
function Store(data) {
  const events = {};

  let message = data.message;

  function getMessage() {
    return message;
  }
  function setMessage(value) {
    message = value;
    emit('message');
  }

  return {
    getMessage,
    setMessage,
    on, off
  };

  ////////////////////////////////
  // events
  // ...
  ////////////////////////////////
}
JavaScript
// store
const store = new Store({ message: 'Hello' });
store.on('message', update);

// DOM element
const messageElem = document.getElementById('message-elem');

// initial update
update();

////////////////////////////////////////////////////////////////

function update() {
  messageElem.innerHTML = store.getMessage();
}
JavaScript
RESULT

Hello
How to Duck
Now that you understand all of the two building blocks of Duck, we're now ready to "Duck"! Or, to "build apps with Duck".
When developing in Duck, always try to develop in a store-first fashion. Iteratively build small features of your app as logic (ie, with no user interface elements) in a store, then incrementally build very basic components to visualize the state of that store. And, that's it! Beyond that, you can incrementally improve the components themselves with styling/animations/etc.
To give a component access a store, simply pass a reference to a store in through a component's data!
Remember the construct() and destroy() functions in a component? Simply do,
store.on('eventName', update) in construct()
&
store.off('eventName', update) in destroy()
... for whichever events are relevant to that component.
Then, in update(), simply call getter functions from the store and do whatever you feel to make elem (and DOM elements within elem) display whatever state you wish to display.
Oh, and when the user interacts with your component, such as a click, instead of directly making UI changes in the onclick handler function, call a store setter function instead. If you want to change your component as a result of an interaction, you can always just listen for the corresponding event that is emitted, and your component will update like clockwork!
Here's a visual,
Follow this coding style for everything that you build in your app, and your entire app will also work like clockwork!
Here's a quick example,
function Store(data) {
  const events = {};

  let value = data.value || '';

  function getValue() { return value; }
  function setValue(val) { value = val; emit('value'); }

  return {
    getValue,
    setValue,
    on, off
  };

  ////////////////////////////////
  // events
  // ...
  ////////////////////////////////
}
JavaScript
function Input(data) {
  let elem;
  const store = data.store;

  function construct() {
    elem = document.createElement('input');
    elem.addEventListener('input', onInput);
    store.on('value', update);
    update();
  }

  function update() {
    elem.value = store.getValue();
  }

  function enter() {}
  function exit() {}

  function destroy() {
    elem.removeEventListener('input', onInput);
    store.off('value', update);
  }

  function onInput() {
    store.setValue(elem.value);
  }

  construct();
  return { elem, data, update, enter, exit, destroy };
}
JavaScript
const appElem = document.getElementById('inputs-app');
const store = new Store({
  value: 'Type here...'
});

// start with two inputs
appendInput();
appendInput();

////////////////////////////////////////////////////////////////

function appendInput() {
  let input = new Input({ store });
  appElem.appendChild(input.elem);
}
JavaScript
RESULT

Next, Rules