Javascript development can be painful especially when you have to deal with responsive websites where you have to adjust the behavior of the code based on the available screen real estate. Therefore we came up with a component called StateManager, which provides you with the ability to define states and triggers a callback function, if a state was entered or left.
On the other hand, we have our lovely jQuery plugins which are not always a pleasure to build. To simplify the process, we've implemented a plugin base class which features the best practices of jQuery plugin development that flawlessly integrates with the StateManager.
In the following document we want to give you a general overview of the provided functionality, which can come in handy for your next theme.
As mentioned, the jQuery plugin base class was built up with the best practices of jQuery plugin development. Here's feature set at a glance:
data
attributes to configure the plugindata
-methodAs you can see, we've put a lot of effort in the provided feature set to provide you with an easy to use class for your next jQuery plugin.
Now it's time to take a look at the actual implementation process of a jQuery plugin using the plugin base class. Here's a commented example of a generic plugin:
/**
* Example jQuery plugin using the base class
*
* The $.plugin method bound to the globally available jQuery
* object. The method needs two parameters, the first one is
* simply the name of the plugin which will be used to bind
* the plugin to jQuery's $.fn namespace. The second parameter
* is an object which provides the default configuration and
* the actual implementation of the plugin.
*/
$.plugin('example', {
/**
* The default configuration object of the plugin. The
* user can provide custom settings which will be automatically
* merged into a new object which can be accessed using "this.opts"
* in any plugin method which scope is in the plugin.
*/
defaults: {
activeCls: 'js--is-active'
},
/**
* The "init" method acts like a constructor for the plugin.
* Usually you'll cache necessary elements and registers the
* event listeners for your plugin. Additionally you can switch
* up the behavior of the plugin based on the provided configuration.
*/
init: function() {
var me = this;
/**
* Calling the "applyDataAttributes" method the base class
* automatically reads out the all "data" attributes from
* the element and overrides the configuration. It's especially
* useful if you want to configure your plugin using HTML
* markup instead of providing a configuration object.
*
* For example, we call this plugin on the following element:
* <div data-activeCls="some-other-class">...</div>
*
* ... the "data" attribute will override the "activeCls"
* property with the value "some-other-class".
*/
me.applyDataAttributes();
/**
* Now we're setting up a new event listener for the plugin
* using the built-in "_on" method which is actually a proxy
* method for jQuery's "on" method with some additional benefits.
* The event listener and the event will be registered in a
* plugin specific event collection. The collection will be
* automatically iterated and removes the registered event listeners
* from the element.
* Additionally, the event name will be namespaced on the fly which
* provides us with a safe way to remove a specific event listener from
* an element and doesn't affect other plugins which are listening on
* the same event.
*/
me._on(me.$el, 'click', function(event) {
event.preventDefault();
/**
* In the condition we're using the custom expression of the plugin
* to terminate if the element uses our plugin.
* Additionally you see that we're using the variable "this.$el" which
* is the element that has instantiated the plugin.
*/
if(me.$el.is('plugin-example')) {
/**
* Now we're accessing the merged configuration of the plugin using
* the variable "this.opts".
*/
me.$el.toggleClass(me.opts.activeCls);
}
});
},
/**
* The destroy method can either be called programmically from outside the plugin
* or automatically using the "StateManager" when the defined states are left.
* Usually, you remove classes which were added by your plugin to the element and
* removes the event listeners from the element.
*/
destroy: function() {
var me = this;
me.$el.removeClass(me.opts.activeCls);
/**
* Calling the "_destroy" method will remove all event listeners which were
* registered using the "_on" method of the plugin base.
* You can access the collection of the events in the plugin using the variable
* "this._events" if you want to iterate over the event listeners yourself.
*/
me._destroy();
}
});
Fully commented jQuery plugin using the base class.
_name : String
$el : jQuery
opts : Object
this.applyDataAttributes()
method overrides the property values in the object._events : Array
_on
method.init()
destroy()
update()
_destroy()
_events
property of the plugin. Additionally, the method removes the in-memory binding of the plugin to the element using jQuery's removeData()
method and fires an event on the globally available observer._on()
element : jQuery | HTMLElement
- The event target for the specified event listener.event : String
- A string representing the event type to listen for.fn : Function
- The object that receives a notification when an event of the specified type occurs.on()
method which binds an event listener to the provided element and registers the listener in the _events
event collection._off()
element : jQuery | HTMLElement
- The event target which has an event listenerevent : String
- One or more space-separated event types and optional namespaces, or just namespaces, such as "click" or "keydown.myPlugin"getName()
getEventName()
event : String | Array
- One or more space-separated event typesgetElement()
getOptions()
getOption()
key : String
- Key of the configuration propertysetOption()
key : String
- Key of the configuration propertyvalue : Mixed
- Value for the provided keyapplyDataAttributes()
[shouldDeserialize : Boolean = undefined]
- Tries to parse the
given string values and returns the right value if its successful.
Supports boolean, null, number, json, string. This feature is enabled by default.
Pass false
to deactivate parsing.[ignoreList : Array = []]
- A list of options which will be excluded
when applying the data attributes to the corresponding options. Introduced with Shopware 5.3.7data
attributes. Hint: You don't need to convert
(camel|pascal)-case
java script variable names to (dash|hyphend|kebab)-case
html attribute names.We've added a global event observer into Shopware 5 as well. It provides us with the ability to define events globally in the jQuery object and therefore every plugin can listen to these events:
// Register a new event
$.publish('plugin/some-plugin/onInit', me);
// Listen for an event
$.subscribe('plugin/some-plugin/onInit', function() {
console.log('onInit');
})
// Remove an event listener
$.unsubscribe('plugin/some-plugin/onInit');
Please keep in mind to register your event listeners with a namespace, otherwise you'll remove all subscribed event listeners for the certain event type.
$.subscribe('plugin/some-plugin/onInit.my-plugin', function() {});
// Remove an event listener
$.unsubscribe('plugin/some-plugin/onInit.my-plugin');
The StateManager helps you master different behaviors for different screen sizes. It provides you with the ability to register different states that are handled by breakpoints.
Those breakpoints are defined by entering and exiting points (in pixels)
based on the viewport width.
By entering the breakpoint range, the enter()
functions of the registered
listeners are called.
When the defined points are reached, the registered exit()
listener
functions will be called.
This way you can register callbacks that will be called on entering / exiting the defined state.
The StateManager provides you with multiple helper methods and polyfills which help you master responsive design.
The StateManager is self-containing and globally available in the global javascript scope in the storefront.
It's initialized with the following breakpoints:
0
and 479
pixels480
and 767
pixels768
and 1023
pixels1024
and 1259
pixels1260
and 5160
pixelsRegistering or removing an event listener which uses the StateManager, is as easy as doing it in pure javascript.
The following example shows how to register an event listener:
StateManager.registerListener([{
state: 'xs',
enter: function() { console.log('onEnter'); },
exit: function() { console.log('onExit'); }
}]);
The registration of event listeners also supports wildcards, so the enter()
and exit()
methods are called by every change of the breakpoint:
StateManager.registerListener([{
state: '*',
enter: function() { console.log('onEnter'); },
exit: function() { console.log('onExit'); }
}]);
The default breakpoints can be extended using the registerBreakpoint()
method of the StateManager.
Note: Breakpoint ranges are not allowed to overlap with other existing ranges.
StateManager.registerBreakpoint({
state: 'xxl',
enter: 78.75, // = 1260px
exit: 90 // = 1440px
});
EventEmitter
* Class constructor for an EventEmitter
init()
breakpoints : Array | Object
- The states, which should be available on start uphtml
element and sets a device specific cookie.registerBreakpoint()
breakpoints : Array | Object
- The states, which should be available on start upremoveBreakpoint()
state : String
- State which should be removed, e.g. "xs" or "l"registerListener()
listener : Array | Object
- Either a single listener object or an array with multiple listener objectsaddPlugin()
selector : String | HTMLElement | jQuery
- Element selectorpluginName : String
- Name of the plugin which should be added to the selector.config : Object (optional)
- Custom configuration for the plugin. Can be omitted.viewport: Array | String
- The states where the plugin should be active.removePlugin()
selector : String | HTMLElement | jQuery
- Element selectorpluginName : String
- Name of the plugin which should be removed from the selector.viewport: Array | String
- A state where the plugin should be removed.updatePlugin()
selector : String | HTMLElement | jQuery
- Element selectorpluginName : String
- Name of the plugin which should be updated.update()
method of the plugin themself. The method calls the update()
method of the plugin.destroyPlugin()
selector : String | HTMLElement | jQuery
- Element selectorpluginName : String
- Name of the plugin which should be destroyed.removePlugin()
method, the method calls the destroy()
method of the provided plugin.getViewportWidth()
getViewportHeight()
getPreviousState()
String
or null
when no previous state was active.isPreviousState()
state : String
- State which should be checked, e.g. "xs" or "l"getCurrentState()
isCurrentState()
state : String
- State which should be checked, e.g. "xs" or "l"isPortraitMode()
isLandscapeMode()
getDevicePixelRatio()
isBrowser()
browser : String
- Browser name to test, e.g. "firefox" or "safari"getScrollBarHeight()
matchMedia()
matchMedia
polyfill, which provides the ability to test CSS media queries in javascript.requestAnimationFrame()
requestAnimationFrame
polyfill for cross-browser supportcancelAnimationFrame()
cancelAnimationFrame
polyfill for cross-browser supportgetVendorProperty()
property : String
- The property which needs the vendor prefixsoftError : Boolean
- Truthy to return the provided property when no vendor was found, otherwise the method returns null
The EventEmitter is a utility class, offering a simple base-class for event driven architecture. Currently the EventEmitter is used as the basis for the StateManager, but you can use it to let your own objects emit events.
An object which inherits from the EventEmitter exposes the functionality of subscribing and listening to events. Much like the global event observer.
on()
eventName : String
- The name of the event which to listen tocallback : Function
- The function which should be called, when the event is triggeredcontext : Any
- An optional context, which to bind the callback to. e.g. this
once()
eventName : String
- The name of the event which to listen tocallback : Function
- The function which should be called, when the event is triggeredcontext : Any
- An optional context, which to bind the callback to. e.g. this
off()
eventName : String
- Optional, the name used for subscribingcallback : Function
- Optional, the callback used for subscribingcontext : Any
- Optional, the context used for subscribing
* Removes an event listener. It tries to find the listener you added by the parameters you've given. E.g. if there's a listener matching the eventName
and the callback
you've given, but not the context
(if you've given one) the listener won't be removed. If you only supply the eventName, all listeners of that event will be removed. If you supply no parameters all event listeners will be removed.
* Chainabletrigger()
eventName : String
- The name of the event to trigger
* Triggers an event.
* Chainabledestroy()
* Can be called to clean up.
* ChainableThe combination of the StateManager paired with the jQuery plugin base class provides an easy-to-use way to register jQuery plugins for certain state. That provides us with the ability to provide different behavior for components based on the current active state. For example the Offcanvas menu plugin is only active on mobile devices (states "xs" and "s") and is disabled on tablets and desktop pc's.
The StateManager is available in the global javascript scope of the storefront. To register your plugin, simply call the addPlugin() method of the StateManager.
In the following example we register our own jQuery plugin for the XS and S states. The name of the plugin is "myPlugin" and we will bind it to the HTML DOM nodes which have the class .my-selector:
StateManager.addPlugin('.my-selector', 'myPlugin', [ 'xs', 's' ]);
It's also possible to pass user configuration options to the plugin, which will be merged with the plugin's default configuration. The merged configuration is accessible using the this.opts object in your plugin.
// your plugin
$.plugin('myPlugin', {
defaults: {
'speed': 300
}
});
// Registration of the plugin
StateManager.addPlugin('.my-selector', 'myPlugin', {
'speed': 2000
}, [ 'xs', 's' ]);
Or pass the configuration via html data
attributes and call the applyDataAttributes()
method in our init()
method.
<div class="my-selector" data-myStringVar="myOverwrittenValue" myBooleanVar=true></div>
$.plugin('myPlugin', {
defaults: {
myStringVar: 'myStandardValue',
myBooleanVar: false
},
init: function() {
var me = this;
me.applyDataAttributes();
}
});
If you need to pass modified configuration to your plugin for a specific viewport, you can use the following pattern:
StateManager.addPlugin('.my-selector', 'myPlugin', {
'speed': 300
}).addPlugin('.my-selector', 'myPlugin', {
'speed': 2000
}, 's');
Working with compressors isn't always as easy as adding the files to your HTML structure using script
tags. The built-in javascript compressor is just as easy as this and perfectly suited to your workflow as a web developer.
Simply place your javascript files in the frontend/_public
directory and add their paths to the $javascript
array in your Theme.php
and you're good to go.
/** @var array Defines the files which should be compiled by the javascript compressor */
protected $javascript = array(
'src/js/jquery.my-plugin.js'
);