Skip to content
This repository has been archived by the owner on Dec 9, 2023. It is now read-only.

Core Developer Documentation

Benjamin Knorr edited this page Aug 12, 2019 · 3 revisions

Packages Overview

The repository is structured as a monorepo containing the core, shared ui elements, plugins and renderer. Theres also a storybook demo, which is live deployed from master to demo.edtr.io

This documentation focuses on the concepts of edtr.io and parts of the core package.

Concepts

Plugins

Plugins are the main element to the editor. All editing functionality lies in these plugins, defining a specific content element. E.g. there is an image plugin, which can be used to handle uploads of images, display them on the page and also edit meta informations like an alt-text. Plugins can be nested, so plugins can display other plugins inside (e.g. you could create a slideshow using a list of image plugins - where the slideshow doesnt need to reimplement the uploading process, but just displays them in a slider).

This allows us to create complex layout information as well. A grid plugin could have a two-dimensional list of arbitrary plugins, which it renders in a responsive grid layout. For the basic purpose of just multiple content rows we have the plugin-rows included, implementing some drag&drop functionality as well as a flow for adding plugins in a new row.

The editor state

The editor state is a composition of plugins. Since plugins can be nested we can simply use one plugin as root, so the (serialized) state just looks like this:

interface DocumentState {
  plugin: string
  state?: unknown
}

where the plugin will define what the state looks like. A concrete example (NOTE: these plugins don't exist in this project, only used for illustrating purposes)

{
  plugin: 'rows',
  state: [ // state is just a list of other plugins
    {
      plugin: 'simple-text',
      state: 'This is the first line' // state is just a string
    },
    {
      plugin: 'slideshow',
      state: { // state contains an interval for automatic cycling and a list of nested image plugins
        interval: 2000,
        images: [
          {
            plugin: 'image',
            state: { /*...*/ }
          }, 
          // more images
        ]
      }
    }
    //... more rows
  ]
}

Behind the scenes: Deserialization

During deserialization the tree like structure will be broken up into a Record of plugins identified by a string id, replacing the nested elements with the id, so the above example during editing is handled something like this:

{
  root: {
    plugin: 'rows',
    state: ['1', '2', /* more rows */] // plugins replaced with id
  },
  1: {
    plugin: 'simple-text',
    state: 'This is the first line'
  },
  2: {
    plugin: 'slideshow'
    state: { interval: 2000, images: ['3', /* more images */] }
  },
  3: {
    plugin: 'image',
    state: { /*...*/ }
  }
  // ... more documents
}

This way, changes in one plugin are well isolated and don't cause rerendering of other parts.

State Transformation with Redux

The editor uses redux for state transformations. To handle multiple editor instances in the same store, the store state of one instance is scoped and the whole store is a record of scoped states. Here is what the type definition looks like:

export type EditorState = Record<string, ScopeState>

export interface ScopeState {
  plugins: {  // a record of the plugins registered on the editor instance
    defaultPlugin: string
    plugins: Record<string, Plugin>
  }
  documents: Record<string, DocumentState> // the deserialized state as detailed above 
  focus: string | null // currently focused id
  root: string | null // id of the root plugin (atm. always 'root')
  clipboard: DocumentState[] // a clipboard for copy&paste of documents
  history: HistoryState // a history of all actions to reproduce the current state (sideffect free), see below
}

export interface HistoryState {
  initialState?: {
    documents: ScopeState['documents']
  }
  undoStack: PureAction[][] // all pure actions, multiple actions may be combined to an array for cleaner undo
  redoStack: PureAction[][] // actions pushed on undo, to allow redo.
  pendingChanges: number // quick way of checking if there are unsaved changes
}

All side effects are resolved using redux-saga and then dispatched as pure actions, which get stored in the store.state.history. These actions are sufficient to calculate the current state from the initial state.

Deep dive into the core code

The <Editor> and <SubDocument> Components

The Editor Component is the entry point for integration of Edtr.io into a webpage. It creates the redux store and handles the deserialization of the initial state by dispatching an initRoot event to the root saga.

After the deserialization the root <SubDocument> gets rendered. That Component does connect to the redux store to collect some props for the plugin, creates the StateType Helpers (you can read more on the StateTypes in the docs) and then gets the React Component of the plugin and renders that Component.
When it is a nested plugin, the plugin Component will then use the StateType.child helper for rendering, another SubDocument for every child, building up the rendered dom.

Store: Reducer, Redux and Redux Saga

When an action is dispatched it will be passed to the saga middleware, which will then just call all of the sagas of a part of the store state (e.g. documents, history, ...). All of these parts use the same pattern:

  • In actions.ts the module can define all actions affecting the substate. Actions that can be called publicly are exported as publicSomeActionName (e.g. document actions) and exposed. This way we can control which actions can be dispatched in the plugins.
  • If the module does handle impure actions (probably the public ones), it defines a saga in the saga.ts file. (E.g. the root saga handles the initialization of the root document and therefor deserialization of the initial state). The saga will then dispatch pure actions using put. (e.g. inserting the deserialized documents)
  • Then finally there is the reducer.ts for the substate, resolving the pure actions triggered by sagas. It also defines selectors used for connecting.