You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository has been archived by the owner on Sep 3, 2021. It is now read-only.
In 3.0, we will be rewriting the entire JS layer using ES6. This will include the removal of the current class system and the requirement of a new class system (based on ES6 classes). The following diagrams are our current thoughts on how this new plugin class will work. Ideas are taken from React and Flux.
At its core, we would have the parent Plugin class, which both Components and Behaviors will extend. The Plugin should provide shared functionality and a common instantiation process.
Components
Components are a type of plugin that add pre-built functionality to an element, known as the "primary" element. Components can be interacted with from the JS layer to trigger certain events or actions.
There are 3 types of components, based on specific use cases. They are:
Embedded - Primary elements already exist in the DOM
Rendered - Primary elements are rendered on demand using a template as a source and then mounted in the DOM
Composite - A container for multiple child components, either embedded or rendered
A few examples of embedded components are accordion, carousel, off canvas, and rendered components are type ahead, toast, and finally composite components are modal, tooltip, flyout.
class Component extends Plugin {}
class EmbeddedComponent extends Component {}
class RenderedComponent extends Component {}
class CompositeComponent extends Component {}
Behaviors
Behaviors are a type of plugin that add unique behavior to a collection of elements, but ultimately don't have a "primary" element. Once initialized, they usually never need to be interacted with.
A few examples of behaviors are: lazy load, stalker, and drag and drop.
class Behavior extends Plugin {}
Initialization
The plugin class constructor will look something like the following (using pseudo code). The constructor will need to initialize members in a specific order: options -> element -> props -> binds. We want the constructor to be generic enough that it can be re-used by all plugins (and sub-classes) without need for overriding or super calls -- but still could if need be.
The initialize() method contains a handful of other methods used in the initialization of the class. These methods (and the order of operation) are as follows:
initOptions() will take the custom options from the constructor argument, merge them with the static options for the plugin, and set them to this.options.
initElement() will take the selector (a string) from the constructor argument and either find or create an element based on the type of plugin. The element will be set to this.element.
Embedded components will find and use the element from the DOM.
Rendered components will render an element based on a template and mount it in the DOM.
Composite components work similar to rendered components but only create a container element.
Behaviors will find all elements that match the selector.
initProperties() will set class properties, for example, this.headers and this.sections from the accordion. This will need to be ran after options and the element are set, as certain properties will require those fields.
initBinds() will set a mapping of events and their callbacks to be bound to the element found in step 2. This will also require the options and element to be set.
emit() will dispatch an init event.
Once initialization is complete, the plugin will be enabled using enable(). This method will bind DOM events and set the enabled flag to true.
Once enabled, the startup() method is called, which is used by sub-classes to bootstrap the plugin by setting its initial state (more information below). For example, a carousel will need to start cycle timers, the matrix will need to position tiles, etc.
A very rudimentary example of this in action may look something like.
This approach makes it much easier to initialize a class as each class can define which init* methods they want to customize. This also gets around ES6's lack of class properties.
A plugin and its DOM must represent a single state at any given time. The current state is represented as an object on this.state and can be modified using setState(). The setState() method accepts either an object, or a function that returns an object, and will compare the new state to the old state and determine whether to call render() or not.
The render() method must be defined in the child class as this method is used for modifying the state, whether in the current DOM, or rendering a new state and inserting into the DOM.
Embedded components will modify the DOM (add/remove classes, show/hide elements, etc).
Rendered components will render a new element based on a template and replace the old element.
Composite components will find the child element that the state is interacting with and pass the state down.
Behaviors don't have a state.
To better understand how state works, let's use the carousel component again. What does a carousel's state look like? Well, we have the index for the item currently being displayed, a flag on whether it's stopped or not, the current width and height dimensions, and the size to cycle width. All of these can be modified when calling setState(), which in turn will call render() which modifies the DOM accordingly.
Another rudimentary example may be.
class Carousel extends EmbeddedComponent {
// ...
render() {
// Update the active state tabs using `this.state.index`
// Animate the items to the new `this.state.index`
// Toggle the display of next and previous arrows
// etc
}
prev() {
this.setState({ index: this.state.index - 1 });
}
next() {
this.setState({ index: this.state.index + 1 });
}
}
This state functionality is very similar to React's state.
Destroying a plugin will include the removal of DOM elements (if not embedded), unbinding of events, removing the class instance, and any other clean up tasks. This can be achieved using the destroy() method (same as 2.0 and below versions).
The destroy() method should be common amongst all plugins, similar to how the constructor is handled above. The pseudo code may look like the following.
+-----------------------+
| destroy() |
+-----------------------+
|
V
+-----------------------+
| emit('destroying') |
+-----------------------+
|
V
+-----------------------+
| shutdown() |
+-----------------------+
|
V
+-----------------------+ +-----------------------+
| disable() | ----> | unbindEvents() |
+-----------------------+ +-----------------------+
|
V
+-----------------------+
| unmount() |
+-----------------------+
|
V
+-----------------------+
| emit('destroyed') |
+-----------------------+
Rendering
Rendering, depending on the context, can mean one of two things. The first being the mutation of the DOM whenever the state of a plugin has changed, usually through Plugin.render(). The second being the conversion (parsing and rendering) of a logic based templating language, usually represented by template strings, into a set of unmounted DOM nodes, usually through Toolkit.renderTemplate().
Templates
The default templating language within Toolkit will be a logic based custom implementation, with syntax loosely based off of Mustache and Handlebars. Basing the syntax on those libraries will allow for easy drop-in support if Handlebars or Mustache is being used.
The language should support basic conditional blocks, loops, and variable interpolation. The syntax would look like the following.
If/else conditionals.
<divclass="post">
{{#if post.deleted}}
Post is deleted.
{{#elif post.hidden}}
Post is hidden.
{{/if}}
</div>
The renderer can be customized or overridden by changing the Toolkit.renderTemplate() method.
Toolkit.renderTemplate=function(){// Return rendered template as a string};
Events & Listeners
The old hook system from the previous Toolkit versions is now known as the listener system... since that's what they are. The old "event" system will now be referred to as event bindings (below). Listeners can be subscribed to an event by calling the on() method on the plugin, and the reverse for unsubscribing by calling off().
Events can be dispatched (emitted, triggered, whatever) using the emit() method (known as fireEvent() in previous versions). This method will loop through and notify all listeners, while passing an optional list of arguments.
DOM listeners attached to the primary element of a plugin will also be triggered. Since these events are bound outside the context of the plugin, they must use the event naming convention of <event>.toolkit.<component>.
The old event system from the previous Toolkit versions is now known as the binds system, or event bindings. The naming has changed because the old system was rather confusing as the term "event" was rather ambiguous. Was this binding events? Dispatching them? Subscribing listeners? In 3.0, this should be much more straight forward as event bindings will now refer to the mapping of events to bind to the primary element when a plugin is instantiated.
Bindings will be mapped during initialization when the initBinds() method is called, and subsequently the setBinds() method. The setBinds() method accepts an object of key-value pairs, with the key being an event to bind to, and the value being the function callback. The key should include the event name, the context, and an optional delegation context. An example implementation:
classCarouselextendsEmbeddedComponent{initBinds(){this.setBinds({'resize window': 'onResize',// Window events'ready document': 'onReady',// Document events'{mode} document': 'onMode',// Mode based events based on options.mode (either "click" or "hover")'click document {selector}': 'onClick',// Document events with delegation and selector'click element': debounce(this.func),// Property events'click element [data-foo]': 'onDel'// Delegated property events});}}
The previous example is a bit contrived but it simply showcases many of the different patterns for bindings. For a better understanding, here is the binding diagram pattern.
PATTERN
EVENT CONTEXT[ DELEGATE]: FUNCTION
EVENT
The name of the event. If "ready" is used, it will be set as "DOMContentLoaded". If "{mode}" is used, it will be replaced with "options.mode".
CONTEXT
The context (or element) in which to add listeners to. If "window" or "document" is used, the respective objects will be used, else it will attempt to use a property on the plugin that matches by name.
DELEGATE
An optional CSS selector to apply delegation to within the CONTEXT. All values after the CONTEXT will be used as a delegation selector. If "{selector}" is used, it will be replaced with the "selector" passed to the constructor.
FUNCTION
The function to bind as an event listener. If a string is passed, it will attempt to find a method on the plugin that matches by name. If a literal function is passed, it will be used directly.
The text was updated successfully, but these errors were encountered:
In 3.0, we will be rewriting the entire JS layer using ES6. This will include the removal of the current class system and the requirement of a new class system (based on ES6 classes). The following diagrams are our current thoughts on how this new plugin class will work. Ideas are taken from React and Flux.
At its core, we would have the parent
Plugin
class, which bothComponent
s andBehavior
s will extend. ThePlugin
should provide shared functionality and a common instantiation process.Components
Components are a type of plugin that add pre-built functionality to an element, known as the "primary" element. Components can be interacted with from the JS layer to trigger certain events or actions.
There are 3 types of components, based on specific use cases. They are:
A few examples of embedded components are accordion, carousel, off canvas, and rendered components are type ahead, toast, and finally composite components are modal, tooltip, flyout.
Behaviors
Behaviors are a type of plugin that add unique behavior to a collection of elements, but ultimately don't have a "primary" element. Once initialized, they usually never need to be interacted with.
A few examples of behaviors are: lazy load, stalker, and drag and drop.
Initialization
The plugin class constructor will look something like the following (using pseudo code). The constructor will need to initialize members in a specific order: options -> element -> props -> binds. We want the constructor to be generic enough that it can be re-used by all plugins (and sub-classes) without need for overriding or super calls -- but still could if need be.
The
initialize()
method contains a handful of other methods used in the initialization of the class. These methods (and the order of operation) are as follows:initOptions()
will take the custom options from the constructor argument, merge them with the static options for the plugin, and set them tothis.options
.initElement()
will take the selector (a string) from the constructor argument and either find or create an element based on the type of plugin. The element will be set tothis.element
.initProperties()
will set class properties, for example,this.headers
andthis.sections
from the accordion. This will need to be ran after options and the element are set, as certain properties will require those fields.initBinds()
will set a mapping of events and their callbacks to be bound to the element found in step 2. This will also require the options and element to be set.emit()
will dispatch aninit
event.Once initialization is complete, the plugin will be enabled using
enable()
. This method will bind DOM events and set the enabled flag totrue
.Once enabled, the
startup()
method is called, which is used by sub-classes to bootstrap the plugin by setting its initial state (more information below). For example, a carousel will need to start cycle timers, the matrix will need to position tiles, etc.A very rudimentary example of this in action may look something like.
This approach makes it much easier to initialize a class as each class can define which
init*
methods they want to customize. This also gets around ES6's lack of class properties.The entire flow will look like the following.
State
A plugin and its DOM must represent a single state at any given time. The current state is represented as an object on
this.state
and can be modified usingsetState()
. ThesetState()
method accepts either an object, or a function that returns an object, and will compare the new state to the old state and determine whether to callrender()
or not.The
render()
method must be defined in the child class as this method is used for modifying the state, whether in the current DOM, or rendering a new state and inserting into the DOM.To better understand how state works, let's use the carousel component again. What does a carousel's state look like? Well, we have the index for the item currently being displayed, a flag on whether it's stopped or not, the current width and height dimensions, and the size to cycle width. All of these can be modified when calling
setState()
, which in turn will callrender()
which modifies the DOM accordingly.Another rudimentary example may be.
This state functionality is very similar to React's state.
The flow.
Destroying
Destroying a plugin will include the removal of DOM elements (if not embedded), unbinding of events, removing the class instance, and any other clean up tasks. This can be achieved using the
destroy()
method (same as 2.0 and below versions).The
destroy()
method should be common amongst all plugins, similar to how the constructor is handled above. The pseudo code may look like the following.And information on the steps. These usually have to happen in order, hence the reason this method should be shared.
emit()
will dispatch adestroying
event.shutdown()
is a custom method used in the cleanup of the plugin (similar tostartup()
but does the opposite).disable()
will disable the plugin by unbinding events and setting the enabled flag tofalse
.unmount()
will remove the element from the DOM if need be.emit()
will dispatch adestroyed
event.Continuing on our carousel example, the
shutdown()
may look like the following. This pseudo code is taken from the current 2.1 implementation.The flow.
Rendering
Rendering, depending on the context, can mean one of two things. The first being the mutation of the DOM whenever the state of a plugin has changed, usually through
Plugin.render()
. The second being the conversion (parsing and rendering) of a logic based templating language, usually represented by template strings, into a set of unmounted DOM nodes, usually throughToolkit.renderTemplate()
.Templates
The default templating language within Toolkit will be a logic based custom implementation, with syntax loosely based off of Mustache and Handlebars. Basing the syntax on those libraries will allow for easy drop-in support if Handlebars or Mustache is being used.
The language should support basic conditional blocks, loops, and variable interpolation. The syntax would look like the following.
If/else conditionals.
Variable interpolations. Escaped and unescaped.
Object and array iteration.
The renderer can be customized or overridden by changing the
Toolkit.renderTemplate()
method.Events & Listeners
The old hook system from the previous Toolkit versions is now known as the listener system... since that's what they are. The old "event" system will now be referred to as event bindings (below). Listeners can be subscribed to an event by calling the
on()
method on the plugin, and the reverse for unsubscribing by callingoff()
.Events can be dispatched (emitted, triggered, whatever) using the
emit()
method (known asfireEvent()
in previous versions). This method will loop through and notify all listeners, while passing an optional list of arguments.DOM listeners attached to the primary element of a plugin will also be triggered. Since these events are bound outside the context of the plugin, they must use the event naming convention of
<event>.toolkit.<component>
.Event Bindings
The old event system from the previous Toolkit versions is now known as the binds system, or event bindings. The naming has changed because the old system was rather confusing as the term "event" was rather ambiguous. Was this binding events? Dispatching them? Subscribing listeners? In 3.0, this should be much more straight forward as event bindings will now refer to the mapping of events to bind to the primary element when a plugin is instantiated.
Bindings will be mapped during initialization when the
initBinds()
method is called, and subsequently thesetBinds()
method. ThesetBinds()
method accepts an object of key-value pairs, with the key being an event to bind to, and the value being the function callback. The key should include the event name, the context, and an optional delegation context. An example implementation:The previous example is a bit contrived but it simply showcases many of the different patterns for bindings. For a better understanding, here is the binding diagram pattern.
The text was updated successfully, but these errors were encountered: