diff --git a/config/toc-docs.yaml b/config/toc-docs.yaml index 2430f488..842efdbc 100644 --- a/config/toc-docs.yaml +++ b/config/toc-docs.yaml @@ -66,6 +66,18 @@ Plugins: ajax/javascript-api: "JavaScript API" ajax/extras: "Extra Features" +Snowboard: + icon: "icon-bolt" + pages: + snowboard/introduction: "Introduction" + snowboard/migration-guide: "Migration Guide" + snowboard/handlers: "Serverside Event Handlers" + snowboard/request: "AJAX Requests (JavaScript API)" + snowboard/data-attributes: "AJAX Requests (Data Attributes API)" + snowboard/extras: "Extra Features" + snowboard/utilities: "Utilities" + snowboard/plugin-development: "Plugin Development" + Database: icon: "icon-hdd" pages: @@ -113,6 +125,7 @@ Console: console/setup-maintenance: "Setup & Maintenance" console/plugin-management: "Plugin Management" console/theme-management: "Theme Management" + console/asset-compilation: "Asset Compilation (Mix)" console/scaffolding: "Scaffolding" console/utilities: "Utilities" diff --git a/console-asset-compilation.md b/console-asset-compilation.md new file mode 100644 index 00000000..471b5345 --- /dev/null +++ b/console-asset-compilation.md @@ -0,0 +1,203 @@ +# Asset Compilation (Mix) + +- [Introduction](#introduction) +- [Requirements](#requirements) +- [Registering a package](#registering-packages) + - [Automatic registration](#automatic-registration) + - [Registering plugin packages](#registering-plugins) + - [Registering theme packages](#registering-themes) +- [Mix configuration](#mix-configuration) +- [Examples](#examples) +- [Commands](#commands) + - [Install Node dependencies](#mix-install) + - [List registered Mix packages](#mix-list) + - [Compile a Mix package](#mix-compile) + - [Watch a Mix package](#mix-watch) + + +## Introduction + +Winter brings first-class support for handling Node-based compilation for frontend assets through the Mix commands. The comamnds use the [Laravel Mix](https://laravel-mix.com/) wrapper, a user-friendly and simple interface for setting up compilation of multiple types of frontend assets through Webpack and various libraries. + + +### Requirements + +To take advantage of Mix asset compilation, you must have Node and the Node package manager (NPM) installed in your development environment. This will be dependent on your operating system - please review the [Download NodeJS](https://nodejs.org/en/download/) page for more information on installing Node. + +[Laravel Mix](https://laravel-mix.com/) should also be present in the `package.json` file for any packages that will be using it (either in `dependencies` or a `devDependencies`) but if it is not specified in the project's `package.json` file then it can be optionally automatically added when running the [`mix:install`](#mix-install) command. + + +## Registering a package + +Registering for asset compilation through Mix is very easy. Automatic registration should meet your needs most of the time, and if not there are several methods available to manually register Mix packages. + + +### Automatic registration + +By default, Winter will scan all available and enabled modules, plugins and themes for the presence of a `winter.mix.js` file under each extension's root folder (i.e. `modules/system/winter.mix.js`, `plugins/myauthor/myplugin/winter.mix.js`, or `themes/mytheme/winter.mix.js`). + +If the `winter.mix.js` file is found, it will be automatically registered as a package with an automatically generated package name, and will show up when running the Mix commands. Most of the time, this should be all you need to do in order to get started with Laravel Mix asset compilation in Winter CMS. + + +### Registering plugins + +To register frontend assets to be compiled through Mix in your plugin, simply return an array with the package names as the keys and the package paths relative to the plugin's directory as the values to register from your [`Plugin.php`](../plugin/registration) registration file's `registerMixPackages()` method. See below example. + +```php +public function registerMixPackages() +{ + return [ + 'custom-package-name' => 'assets/js/build.mix.js', + ]; +} +``` + + +### Registering themes + +Registration of asset compilation of themes is even easier, and can be done by adding a `mix` definition to your [theme information file](../themes/development#theme-information) (`theme.yaml`). + +```yaml +name: "Winter CMS Demo" +description: "Demonstrates the basic concepts of the frontend theming." +author: "Winter CMS" +homepage: "https://wintercms.com" +code: "demo" + +mix: + : winter.mix.js +``` + +The `mix` definition takes any number of registered packages as a YAML object, with the key being the name of the package as a kebab-case string and the location of your `winter.mix.js` file relative to the theme's root directory. + +For example, if you want to register two packages called `demo-theme-style` and `demo-theme-shop` located in the assets folder, you would use the following definition: + +```yaml +name: "Winter CMS Demo" +description: "Demonstrates the basic concepts of the frontend theming." +author: "Winter CMS" +homepage: "https://wintercms.com" +code: "demo" + +mix: + demo-theme-style: assets/style/winter.mix.js + demo-theme-shop: assets/shop/winter.mix.js +``` + + +## Mix configuration + +The Mix configuration file (`winter.mix.js`) is a configuration file that manages the configuration of Laravel Mix itself. In conjunction with the `package.json` file that defines your dependencies, this file defines how Laravel Mix will compile your assets. + +You can [review examples](https://laravel-mix.com/docs/6.0/examples) or the [full Mix API](https://laravel-mix.com/docs/6.0/api) at the [Laravel Mix website](https://laravel-mix.com). + +Your `winter.mix.js` file must include Mix as a requirement, and must also define the public path to the current directory, as follows: + +```js +const mix = require('laravel-mix'); + +// For assets in the current directory +// mix.setPublicPath(__dirname); + +// For assets in a /assets subdirectory +mix.setPublicPath(__dirname + '/assets'); + +// Your mix configuration below +``` + +### Paths + +When the `winter.mix.js` file is evaluated, regardless of where you ran `mix:compile` from, the working directory is set to the parent directory of the `winter.mix.js` file. That means that any relative paths used in the configuration will be relative to the current directory of the `winter.mix.js` file. + +>**NOTE:** Winter's [path symbols](../services/helpers#path-symbols) are also supported in the `winter.mix.js` file. + + +## Examples + +Here are some examples of installing common frontend libraries for use with the asset compilation. + +### Tailwind CSS + +For themes that wish to use Tailwind CSS, include the `tailwindcss`, `postcss` and `autoprefixer` dependencies in your `package.json` file. + +```bash +# Inside the project root folder +npm install --save-dev tailwindcss postcss autoprefixer + +# Run the Tailwind initialisation +npx taildwindcss init +``` + +This will create a Tailwind configuration file (`tailwind.config.js`) inside your theme that you may [configure](https://tailwindcss.com/docs/installation) to your specific theme's needs. + +Then, add a `winter.mix.js` configuration file that will compile Tailwind as needed: + +```js +const mix = require('laravel-mix'); +mix.setPublicPath(__dirname); + +// Render Tailwind style +mix.postCss('assets/css/base.css', 'assets/css/theme.css', [ + require('postcss-import'), + require('tailwindcss'), +]); +``` + +In the example above, we have a base CSS file that contains the Tailwind styling - `assets/css/base.css` - that will compile to a final compiled CSS file in `assets/css/theme.css`. + +Your theme will now be ready for Tailwind CSS development. + + +## Commands + +### Install Node dependencies + +```bash +php artisan mix:install [-p ] [--npm ] +``` + +The `mix:install` command will install Node dependencies for all registered Mix packages. + +This command will add each registered package to the `workspaces.packages` property of your root `package.json` file and then run and display the results of `npm install` from your project root to install all of the dependencies for all of the registered packages at once. + +You can optionally provide a `-p` or `--package` flag to install dependencies for one or more packages. To define multiple packages, simply add more `-p` flags to the end of the command. + +If the command is run with a `-p` or `--package` flag and the provided package name is not already registered and the name matches a valid module, plugin, or theme package name (modules are prefixed with `module-$moduleDirectory`, themes are prefixed with `theme-$themeDirectory`, and plugins are simply `Author.Plugin`) then a `winter.mix.js` file will be automatically generated for that package and will be included in future runs of any mix commands through the [automatic registration](#automatic-registration) feature. + +The `--npm` flag can also be provided if you have a custom path to the `npm` program. If this is not provided, the system will try to guess where `npm` is located. + +### List registered Mix packages + +```bash +php artisan mix:list +``` + +The `mix:list` command will list all registered Mix packages found in the Winter installation. This is useful for determining if your plugin or theme is correctly registered. + +The command will list all packages, as well as the directory for the asset and the configuration file that has been defined during registration. + +### Compile a Mix packages + +```bash +php artisan mix:compile [-p ] [-f|--production] [-- ] +``` + +The `mix:compile` command compiles all registered Mix packages, running each package through Laravel Mix for compilation. + +By specifying the `-p` flag, you can compile one or more selected packages. To define multiple packages, simply add more `-p` flags to the end of the command. + +By default, all packages are built in "development" mode. If you wish to compile in "production" mode, which may include more optimisations for production sites, add the `-f` or `--production` flag to the command. + +The command will generate a report of all compiled files and their final size once complete. + +If you wish to pass extra options to the Webpack CLI, for special cases of compilation, you can add `--` to the end of the command, followed by [any parameters](https://webpack.js.org/api/cli/) as per the Webpack CLI options. + +### Watch a Mix package + +```bash +php artisan mix:watch [-f|--production] [-- ] +``` + +The `mix:watch` command is similar to the the `mix:compile` command, except that it remains active and watches for any changes made to files that would be affected by your compilation. When any changes are made, a compile is automatically executed. This is useful for development in allowing you to quickly make changes and review them in your browser. + +With this command, only one package can be provided and watched at any one time. \ No newline at end of file diff --git a/console-introduction.md b/console-introduction.md index 5a670e8d..73a6cdca 100644 --- a/console-introduction.md +++ b/console-introduction.md @@ -63,6 +63,11 @@ Command | Description [`theme:use`](../console/theme-management#theme-use) | Switches Winter to the given theme. [`theme:remove`](../console/theme-management#theme-install) | Removes a theme. [`theme:sync`](../console/theme-management#theme-sync) | Synchronises a theme between the filesystem and the database, if you use the [Database Templates](../cms/themes#database-driven-themes) feature. +**Asset compilation (Mix)** | +[`mix:install`](../console/asset-compilation#mix-install) | Install Node dependencies for registered Mix packages. +[`mix:list`](../console/asset-compilation#mix-list) | Lists all registered Mix packages. +[`mix:compile`](../console/asset-compilation#mix-compile) | Compiles one or more Mix packages. +[`mix:watch`](../console/asset-compilation#mix-watch) | Watches changes within a Mix package and automatically compiles the package on any change. **Scaffolding** | [`create:theme`](../console/scaffolding#create-theme) | Create a theme. [`create:plugin`](../console/scaffolding#create-plugin) | Create a plugin. diff --git a/snowboard-data-attributes.md b/snowboard-data-attributes.md new file mode 100644 index 00000000..af623de3 --- /dev/null +++ b/snowboard-data-attributes.md @@ -0,0 +1,100 @@ +# AJAX Requests (Data Attributes API) + +- [Introduction](#introduction) +- [Available Data Attributes](#available-attributes) +- [Usage Examples](#usage-examples) + + +## Introduction + +The Data Attributes API is the simpler way of embedding AJAX functionality in your themes and plugins, and removes the need to be experienced with JavaScript. While the [JavaScript API](../snowboard/request) has had numerous changes from the original [AJAX framework](../ajax/introduction), the Data Attributes API has remain largely unchanged, despite being powered by the new Snowboard framework under the hood. + +It can be loaded by adding the following tag into your CMS Theme's page or layout: + +```twig +{% snowboard request attr %} +``` + +> **NOTE:** As per the [Migration Guide](../snowboard/migration-guide), arbitrary JavaScript is no longer allowed through the Data Attributes API. Thus, the `data-request-before-update`, `data-request-success`, `data-request-error` and `data-request-complete` attributes are no longer supported. Please use the [JavaScript API](../snowboard/request) if you require this functionality. + + +## Available Data Attributes + +Triggering an AJAX request from a valid element is as simple as adding the `data-request` attribute to that element. This generally should be done on a button, link, or form. You can also customize the AJAX request using the following attributes: + + +
+ +Attribute | Description +--------- | ----------- +`data-request` | Specifies the AJAX handler name to target for the request. +`data-request-confirm` | Specifies the confirmation message to present to the user before proceeding with the request. If the user cancels, the request is not sent. +`data-request-redirect` | Specifies a URL to redirect the browser to, if a successful AJAX response is received. +`data-request-url` | Specifies the URL to send the AJAX request to. By default, this will be the current URL. +`data-request-update` | Specifies a list of partials and page elements (CSS selectors) to update on a successful AJAX response. The format is as follows: `partial: selector, partial: selector`. Usage of quotes is required in most cases: `'partial': 'selector'`. If the selector is prepended with an `@` symbol, the content received from the server will be appended to the element. If the selector is prepended with a `^` symbol, the content will be prepended. Otherwise, received content will replace the original content in the element. +`data-request-data` | Specifies additional data to send with the request to the server. The format is as follows: `'var': 'value', 'var2': 'new value'`. You may also specify this same attribute on any parent elements of the triggering element, and this data will be merged with the parent data (with the triggering data taking preference). It will also be merged with any form data, if this request triggers within a form. +`data-request-form` | Specifies the form that the AJAX request will include its data from. If this is unspecified, the closest form will be used, or if the element itself is a form, then this will be used. +`data-request-flash` | Specifies if flash messages will be accepted from the response. +`data-request-files` | Specifies if file data will be included in the request. This will allow any file inputs in the form to work. +`data-browser-validate` | Specifies if the in-built browser validation will be triggered. If present, the request will be cancelled if the browser validation fails. +`data-track-input` | Specifies if an input will trigger an AJAX request anytime the input changes. An optional number can be specified in this attribute, which represents the amount of milliseconds between any change and the AJAX request triggering. + + +When the `data-request` attribute is specified for an element, the element triggers an AJAX request when a user interacts with it. Depending on the type of element, the request is triggered on the following events: + +Element | Event +------------- | ------------- +**Forms** | when the form is submitted. +**Links, buttons** | when the element is clicked. +**Text, number, and password fields** | when the text is changed and only if the `data-track-input` attribute is presented. +**Dropdowns, checkboxes, radios** | when the element is selected. + + +## Usage examples + +Trigger the `onCalculate` handler when the form is submitted. Update the element with the identifier "result" with the **calcresult** partial: + +```html +
+``` + +Request a confirmation when the Delete button is clicked before the request is sent: + +```html + + ... + +``` + +Redirect to another page after the successful request: + +```html + +``` + +Send a POST parameter `mode` with a value `update`: + +```html + +``` + +Send a POST parameter `id` with value `7` across multiple elements: + +```html +
+ + +
+``` + +Including [file uploads](../services/request-input#files) with a request: + +```html + + + +
+``` diff --git a/snowboard-extras.md b/snowboard-extras.md new file mode 100644 index 00000000..e120bf21 --- /dev/null +++ b/snowboard-extras.md @@ -0,0 +1,203 @@ +# Extra UI Features + +- [Introduction](#introduction) +- [Loading indicator](#loader-stripe) +- [Loading button](#loader-button) +- [Flash messages](#ajax-flash) +- [Form validation](#ajax-validation) + - [Throwing a validation error](#throw-validation-exception) + - [Displaying error messages](#error-messages) + - [Displaying errors with fields](#field-errors) + - [Usage examples](#usage-examples) + + +## Introduction + +When using the Snowboard framework, you have the option to specify the `extras` flag which includes additional UI features. These features are often useful when working with AJAX requests in frontend CMS pages. + +```twig +{% snowboard extras %} +``` + + +## Loading indicator + +The loading indicator is a loading bar that is displayed on the top of the page when an AJAX request runs. The indicator hooks in to [global events](../snowboard/request#global-events) used by the Snowboard framework. + +When an AJAX request starts, the `ajaxPromise` event is fired. This displays the loading indicator at the top of the page. When this promise is resolved, the loading bar is removed. + + +## Loading button + +When any element contains the `data-attach-loading` attribute, the CSS class `wn-loading` will be added to it during the AJAX request. This class will spawn a *loading spinner* on button and anchor elements using the `:after` CSS selector. + +```html +
+ +
+ + + Do something + +``` + + +## Flash messages + +Specify the `data-request-flash` attribute on a form to enable the use of flash messages on successful AJAX requests. + +```html +
+ +
+``` + +Combined with use of the `Flash` facade in the event handler, a flash message will appear after the request finishes. + +```php +function onSuccess() +{ + Flash::success('You did it!'); +} +``` + +When using AJAX Flash messages you should also ensure that your theme supports [standard flash messages](../markup/tag-flash) by placing the following code in your page or layout in order to render Flash messages that haven't been displayed yet when the page loads. + +```twig +{% flash %} +

+ {{ message }} +

+{% endflash %} +``` + + +## Form validation + +You may specify the `data-request-validate` attribute on a form to enable server-side validation features with fields and forms. + +```html +
+ +
+``` + + +### Throwing a validation error + +In the server side AJAX handler, you may throw a [validation exception](../services/error-log#validation-exception) using the `ValidationException` class to make a field invalid. The exception should be provided an array, which states the field names for the keys, and the error messages for the values. + +```php +function onSubmit() +{ + throw new ValidationException(['name' => 'You must give a name!']); +} +``` + +> **NOTE**: You can also pass a [Validator](../services/validation) instance as the first argument of the exception instead, to use the in-built validation service. + + +### Displaying error messages + +Inside the form, you may display the first error message by using the `data-validate-error` attribute on a container element. The content inside the container will be set to the error message and the element will be made visible. + +```html +
+``` + +To display multiple error messages, include an element with the `data-message` attribute. In this example the paragraph tag will be duplicated and set with content for each message that exists. + +```html +
+

+
+``` + +The `handleValidationErrors` callback, and the `ajaxValidationErrors` global event, that are available with the [Request API](../snowboard/request#global-events) allow you to fully customise the client-side validation handling. The `handleValidationErrors` callback can be used to control validation per request, while the `ajaxValidationErrors` global event can be used by [Snowboard plugins](../snowboard/plugin-development) to augment the client-side validation in a global fashion. + + +### Displaying errors with fields + +Alternatively, you can show validation messages for individual fields by defining an element that uses the `data-validate-for` attribute, passing the field name as the value. + +```html + + + + +
+``` + +If the element is left empty, it will be populated with the validation text from the server. Otherwise you can specify any text you like and it will be displayed instead. + +```html +
+ Oops.. phone number is invalid! +
+``` + + +### Usage examples + +Below is a complete example of form validation. It calls the `onDoSomething` event handler that triggers a loading submit button, performs validation on the form fields, then displays a successful flash message. + +```html +
+ +
+ + +
+ +
+ + +
+ + + +
+

+
+ +
+``` + +The AJAX event handler looks at the POST data sent by the client and applies some rules to the validator. If the validation fails, a `ValidationException` is thrown, otherwise a `Flash::success` message is returned. + +```php +function onDoSomething() +{ + $data = post(); + + $rules = [ + 'name' => 'required', + 'email' => 'required|email', + ]; + + $validation = Validator::make($data, $rules); + + if ($validation->fails()) { + throw new ValidationException($validation); + } + + Flash::success('Jobs done!'); +} +``` diff --git a/snowboard-handlers.md b/snowboard-handlers.md new file mode 100644 index 00000000..3c67f8ad --- /dev/null +++ b/snowboard-handlers.md @@ -0,0 +1,156 @@ +# Server-side Event Handlers + +- [Introduction](#introduction) + - [Calling a handler](#calling-handlers) + - [Generic handler](#generic-handler) +- [Redirects in AJAX handlers](#redirects-in-handlers) +- [Returning data from AJAX handlers](#returning-data-from-handlers) +- [Throwing an AJAX exception](#throw-ajax-exception) +- [Running code before handlers](#before-handler) + + +## Introduction + +AJAX event handlers are PHP functions that can be defined in the page or layout [PHP section](../cms/themes#php-section) or inside [components](../cms/components) and are used to execute the server-side functionality of an AJAX request made by the [Request API](../snowboard/request) or [Data Attributes API](../snowboard/data-attributes). + +Handler method names should be specified with the `on` prefix, followed by the event name in PascalCase - for example, `onMyHandler` or `onCreatePost`. + +All handlers support the use of [updating partials](#updating-partials) as part of the AJAX response. This behavior can also be controlled via the `update` option in the [Request API](../snowboard/request) or the `data-request-update` attribute in the [Data Attributes API](../snowboard/data-attributes). + +```php +function onSubmitContactForm() +{ + // AJAX handler functionality goes here +} +``` + +If two handlers with the same name are defined in a page and layout together, the page handler will be executed. The handlers defined in [components](../cms/components) have the lowest priority. + + +### Calling a handler + +Every AJAX request should specify a handler name. When the request is made, the server will search all the registered handlers and run the handler with the highest priority. + +```html + + + + + +``` + +If two components register the same handler name, it is advised to prefix the handler with the [component short name or alias](../cms/components#aliases). If a component uses an alias of **mycomponent** the handler can be targeted with `mycomponent::onName`. + +```html + +``` + +You should use the [`__SELF__`](../plugin/components#referencing-self) variable instead of the hard coded alias in order to support multiple instances of your component existing on the same page. + +```twig +
+``` + +### Generic handler + +Sometimes you may need to make an AJAX request for the sole purpose of updating page contents by pulling partial updates without executing any code. You may use the `onAjax` handler for this purpose. This handler is available everywhere the AJAX framework can respond. + +#### `clock.htm` Partial +```twig +The time is {{ 'now' | date('H:i:s') }} +``` + +#### `index.htm` Page +```twig + + {% partial 'clock' %} + + +``` + + +## Redirects in AJAX handlers + +If you need to redirect the browser to another location, return the `Redirect` object from the AJAX handler. The framework will redirect the browser as soon as the response is returned from the server. + +```php +function onRedirectMe() +{ + return Redirect::to('http://google.com'); +} +``` + +You may also specify a `redirect` in the Request API options, or through the `data-request-redirect` data attribute. This setting will take precedence over any redirect returned in the AJAX response. + + +## Returning data from AJAX handlers + +You may want to return structured, arbitrary data from your AJAX handlers. If an AJAX handler returns an array, you can access its elements in the `success` callback handler. + +```php +function onFetchDataFromServer() +{ + /* Some server-side code */ + + return [ + 'totalUsers' => 1000, + 'totalProjects' => 937 + ]; +} +``` + +Then, in JavaScript: + +```js +Snowboard.request(this, 'onHandleForm', { + success: function(data) { + console.log(data); + } +}); +``` + +Data returned in this fashion **cannot** be accessed through the [Data Attributes API](../snowboard/data-attributes). + +You may also retrieve the data in [several events](../snowboard/request#global-events) that fire as part of the Request lifecycle. + + +## Throwing an AJAX exception + +You may throw an [AJAX exception](../services/error-log#ajax-exception) using the `AjaxException` class to treat the response as an error while retaining the ability to send response contents as normal. Simply pass the response contents as the first argument of the exception. + +```php +throw new AjaxException([ + 'error' => 'Not enough questions', + 'questionsNeeded' => 2 +]); +``` + +> **NOTE**: When throwing this exception type, [partials will be updated](../ajax/update-partials) as normal. + + +## Running code before handlers + +Sometimes you may want code to execute before a handler executes. Defining an `onInit` function as part of the [page execution life cycle](../cms/layouts#dynamic-pages) allows code to run before every AJAX handler. + +```php +function onInit() +{ + // From a page or layout PHP code section +} +``` + +You may define an `init` method inside a [component class](../plugin/components#page-cycle-init) or [backend widget class](../backend/widgets). + +```php +function init() +{ + // From a component or widget class +} +``` diff --git a/snowboard-introduction.md b/snowboard-introduction.md new file mode 100644 index 00000000..a0ef7089 --- /dev/null +++ b/snowboard-introduction.md @@ -0,0 +1,57 @@ +# Snowboard.js - Winter JavaScript Framework + +- [Introduction](#introduction) +- [Features](#features) +- [Including the framework](#framework-script) +- [Concepts](#concepts) + + +## Introduction + +Winter includes an optional JavaScript framework called **Snowboard**, which acts as an upgrade to the previous [AJAX Framework](../ajax/introduction) and provides many new useful features in an extensible fashion, whilst dropping previous hard dependencies to supercharge your projects even further. + +The framework takes advantage of the incredible enhancements made to the JavaScript ecosystem in recent years to provide a unique experience, available only on Winter. + + +## Features + +- Rewritten AJAX and JavaScript framework, built from the ground-up using the latest JavaScript syntax (ES2015+) and functionality. +- No dependency on jQuery, allowing the framework to be used across a wide variety of JavaScript projects. +- Easy, comprehensive extensibility and event handling. +- Small footprint and full control over which core functionalities to include ensures your website loads quick. + + +## Including the framework + +> Before proceeding, please read the [Migration Guide](../snowboard/migration-guide), especially if you intend to use this framework on an existing project. + +Snowboard can be included in your [CMS theme](../cms/themes) by placing the `{% snowboard %}` tag anywhere inside your [page](../cms/pages) or [layout](../cms/layouts) where you would like the JavaScript assets to be loaded - generally, this should be at the bottom of the page before the closing `` tag. You must use this tag *before* you load any assets that rely on the framework, such as plugins or event listeners, and it should also be located before the `{% scripts %}` tag to allow third party code (i.e. [Winter Plugins](../plugin/registration#Introduction)) to provide [Snowboard plugins](plugin-development) if they wish. + +By default, only the base Snowboard framework and [necessary utilties](../snowboard/utilities) are included by the `{% snowboard %}` token in order to allow for complete control over which additional features (such as the AJAX framework) are desired to be included in your themes. + +You can specify further attributes to the tag to include optional additional functionality for the framework: + +Attribute | Includes +--------- | -------- +`all` | Includes all available plugins +`request` | The base [JavaScript AJAX](../snowboard/request) request functionality +`attr` | The [HTML data attribute](../snowboard/data-attributes) request functionality +`extras` | [Several useful UI enhancements](../snowboard/extras), including flash messages, loading states and transitions. + +To add Snowboard to your theme with all of its features enabled, you would use the following: + +```twig +{% snowboard all %} +``` + +To include the framework with just the JavaScript AJAX request functionality: + +```twig +{% snowboard request %} +``` + +Or to include both the JavaScript AJAX and HTML data attribute request functionality: + +```twig +{% snowboard request attr %} +``` diff --git a/snowboard-migration-guide.md b/snowboard-migration-guide.md new file mode 100644 index 00000000..93bc161d --- /dev/null +++ b/snowboard-migration-guide.md @@ -0,0 +1,93 @@ +# Migration Guide + +- [Introduction](#introduction) +- [Breaking changes](#breaking-changes) + - [Browser support is more strict](#browser-support) + - [jQuery is no longer required](#no-jquery) + - [JavaScript in the HTML data attribute framework is deprecated](#html-callbacks) + - [AJAX events are triggered as DOM events](#ajax-dom-events) +- [Other changes](#other-changes) + - [JavaScript AJAX Requests](#js-requests) + + +## Introduction + +While care has been given to ensure that the Snowboard framework covers the entire scope of functionality that the original [AJAX framework](../ajax/introduction) provided, there are subtle differences between the two frameworks. Please take the time to read through this document to ensure that you are across the changes, especially if you intend to upgrade an existing project to use this new framework. + + +## Breaking changes + + +### Browser support is more strict + +Snowboard drops support for Internet Explorer, as well as some less-used, or discontinued, browsers such as the Samsung Internet Browser and Opera Mini. The framework targets, at the very least, support for the ECMAScript 2015 (ES2015) JavaScript language. + +Our build script is set up to consider the following browsers as compatible with the framework: + +- The browser must have at least a 0.5% market share. +- The browser must be within the last 4 released versions of that browser. +- The browser must NOT be Internet Explorer. +- The browser must NOT be discontinued by the developer. + +For people who wish to support older browsers such as Internet Explorer, you may continue to use the original [AJAX framework](../ajax/introduction), which is still supported by the Winter maintainer team, but will likely not be receiving any new features going forward. + + +### jQuery is no longer required + +We have removed the hard dependency with jQuery, which also means that no jQuery functionality exists in this new framework. If you relied on jQuery being available for your own JavaScript functionality, you must include jQuery yourself in your theme. + + +### JavaScript in the HTML data attribute framework is deprecated + +The original [AJAX framework](../ajax/attributes-api#data-attributes) allowed for arbitrary JavaScript code to be specified within the callback data attributes, for example, `data-request-success`, `data-request-error` and `data-request-complete`, as a way of allowing JavaScript to run additional tasks depending on the success or failure of an AJAX request made through the HTML data attributes. + +We have dropped support of this feature due to its use of the `eval()` method in JavaScript to execute this JavaScript, which has security implications (especially on front-facing code) and prevents people from using content security policies on their sites without the use of the `unsafe-eval` [CSP rule](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src). + +If you wish to use JavaScript with the AJAX functionality, you must either use the [JavaScript Request functionality](../snowboard/request), or use the original [AJAX framework](../ajax/introduction) which retains this feature. + + +### AJAX events are triggered as DOM events + +Previously, the original AJAX framework used jQuery's Event system to trigger events on elements that are affected by an AJAX request. As jQuery is no longer used, we now use DOM events in their place. + +This change requires us to provide event data as properties of the DOM event, not as handler parameters. + +For example, the `ajaxAlways` event which is triggered on an element when an AJAX request is triggered on an element could have a listener set up through jQuery as follows: + +```js +$('#element').on('ajaxAlways', function (event, context, data, status, xhr) { + console.log(context); // The Request's context + console.log(data); // Data returned from the AJAX response +}); +``` + +Now, you must look at the Event object properties for this information: + +```js +$('#element').on('ajaxAlways', function (event) { + console.log(event.request); // The Request object + console.log(event.responseData); // Data returned from the AJAX response +}); +``` + +Please review the [JavaScript Request](../snowboard/request) documentation for information on what properties are available for DOM events. + + +## Other changes + + +### JavaScript AJAX Requests + +#### Making a request + +The original framework used a jQuery extension to call AJAX requests via JavaScript: + +```js +$('#element').request('onAjaxHandler', { /* ... options .. */ }) +``` + +This is now changed to use the base Winter class to call the Request plugin: + +```js +Snowboard.request('#element', 'onAjaxHandler', { /* ... options .. */ }); +``` \ No newline at end of file diff --git a/snowboard-plugin-development.md b/snowboard-plugin-development.md new file mode 100644 index 00000000..ff8fcf4d --- /dev/null +++ b/snowboard-plugin-development.md @@ -0,0 +1,179 @@ +# Snowboard Plugin Development + +- [Introduction](#introduction) +- [Framework Concepts](#concepts) + - [The Snowboard class](#snowboard-class) + - [The PluginLoader class](#plugin-loader-class) + - [The PluginBase and Singleton abstracts](#plugin-base-singleton) + - [Global events](#global-events) + - [Mocking](#mocking) + + +## Introduction + +The Snowboard framework has been designed to be extensible and customisable for the needs of your project. To this end, the following documentation details the concepts of the framework, and how to develop your own functionality to extend or replace features within Snowboard. + + +## Framework Concepts + +Snowboard works on the concept of an encompassing application, which acts as the base of functionality and is then extended through plugins. The main method of communication and functionality is through the use of [JavaScript classes](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes) which offer instances and extendability, and global events - a feature built into Snowboard. + +The following classes and abstracts are included in the Snowboard framework. + + +### The Snowboard class + +The Snowboard class is the representation of the application. It is the main point of adding, managing and accessing functionality within the framework, synonymous to using `jQuery` or `Vue` in your scripts. It is injected into the global JavaScript scope, and can be accessed through the `Snowboard` variable anywhere within the application after the `{% snowboard %}` tag is used. + +In addition, Snowboard is injected into all plugin classes that are used as the entry point for a plugin, and can be accessed via `this.snowboard` inside an entry plugin class. + +```js +// All these should work to use Snowboard globally +Snowboard.getPlugins(); +snowboard.getPlugins(); +window.Snowboard.getPlugins(); + +// In your plugin, Snowboard can also be accessed as a property. +class MyPlugin extends PluginBase { + myMethod() { + this.snowboard.getPlugins(); + } +} +``` + +The Snowboard class provides the following public API for use in managing plugins and calling global events: + +Method | Parameters | Description +------ | ---------- | ----------- +`addPlugin` | name(`String`)
instance(`PluginBase`) | Adds a plugin to the Snowboard framework. The name should be a unique name, unless you intend to replace a pre-defined plugin. The instance should be either a [PluginBase or Singleton](#plugin-base-singleton) instance that represents the "entry" point to your plugin. +`removePlugin` | name(`String`) | Removes a plugin, if it exists. When a plugin is removed, all active instances of the plugin will be destroyed. +`hasPlugin` | name(`String`) | Returns `true` if a plugin with the given name has been added. +`getPlugin` | name(`String`) | Returns the [PluginLoader](#plugin-loader-class) instance for the given plugin, if it exists. If it does not exist, an error will be thrown. +`getPlugins` | | Returns an object of added plugins as [PluginLoader](#plugin-loader-class) instances, keyed by the name of the plugin. +`getPluginNames` | | Returns all added plugins by name as an array of strings. +`listensToEvent` | eventName(`String`) | Returns an array of plugin names as strings for all plugins that listen to the given event name. This works for both Promise and non-Promise [global events](#global-events). +`globalEvent` | eventName(`String`)
*...parameters* | Calls a non-Promise [global event](#global-events). This will trigger event callbacks for all plugins listening to the given event. This method can be provided additional parameters that will be forwarded through to the event callbacks. This method returns `false` if the event was cancelled by a plugin. +`globalPromiseEvent` | eventName(`String`)
*...parameters* | Calls a Promise [global event](#global-events). This will trigger event callbacks for all plugins listening to the given event. This method can be provided additional parameters that will be forwarded through to the event callbacks. This method returns a Promise that will either be resolved or rejected depending on the response from the event callbacks of the plugins. +`debug` | *...parameters* | When the application is in debug mode, this method logs debug messages to the console. Each log message can display one or more parameters at the same time, and includes a trace of the entire call stack up to when the debug call was made. + +#### Debugging in Snowboard + +The Snowboard class provides a `debug` method that allows developers to easily debug their Snowboard application and plugins. This method only works if the Winter application is in debug mode (`'debug' => true` in the `config/app.php` file). + +Debugging can be called anywhere that the Snowboard class is accessible. + +```js +// Globally +Snowboard.debug('This is a debug message'); + +// Within a plugin +class MyPlugin extends PluginBase { + myMethod() { + this.snowboard.debug('Debugging my plugin', this); + } +} +``` + +In general, you would use the first parameter of the `debug` method to state the debug message. From there, additional parameters can be added to provide additional context. The method will print a collapsed debug message to your developer console on your browser. You may extend the debug message in your console to view a stack trace, showing the entire call stack up to when the debug message was triggered. + + +### The PluginLoader class + +The PluginLoader class is the conduit between your application (ie. the [Snowboard class](#snowboard-class)) and the plugins. It acts similar to a "factory", providing and managing instances of the plugins and allowing the Snowboard application to communicate to those instances. It also provides a basic level of mocking, to allow for testing or overwriting individual methods of the plugin dynamically. + +Each PluginLoader instance will be representative of one plugin. + +In general, you will not need to interact with this class directly - most developer-facing functionality should be done on the Snowboard class or the plugins themselves. Thus, we will only document the methods that *may* be accessed by developers. + +Method | Parameters | Description +------ | ---------- | ----------- +`hasMethod` | methodName(`String`) | Returns `true` if the plugin defines a method by the given name. +`getInstance` | *...parameters* | Returns an instance of the plugin. Please see the **Plugin instantiation** section below for more information. +`getInstances` | | Returns all current instances of the plugin. +`getDependencies` | | Returns an array of the names of all plugins that the current plugin depends on, as strings. +`dependenciesFulfilled` | | Returns `true` if the current plugin's dependencies have been fulfilled. +`mock` | methodName(`String`)
callback(`Function`) | Defines a mock for the current plugin, replacing the given method with the provided callback. See the [Mocking](#mocking) section for more information. +`unmock` | methodName(`String`) | Restores the original functionality for a previously-mocked method. See the [Mocking](#mocking) section for more information. + + +### The `PluginBase` and `Singleton` abstracts + +These classes are the base of all plugins in Snowboard, and represent the base functionality that each plugin contains. When creating a plugin class, you will almost always extend one of these abstract classes. + +There are two key differences between these abstracts, based on the reusability of the plugin and how it is instantiated in the course of the JavaScript functionality of your project: + +Detail | `PluginBase` | `Singleton` +------ | ------------ | ----------- +Reusability | Each use of the plugin creates a new instance of the plugin | Each use of the plugin uses the same instance. +Instantiation | Must be instantiated manually when it is needed to be used | Instantiated automatically when the page is loaded + +The reason for the separation is to provide better definition on how your plugin is intended to be used. For `PluginBase`, you would use this when you want each instance to have its own scope and data. Contrarily, you would use `Singleton` if you want the same data and scope to be shared no matter how many times you use the plugin. + +Here are some examples of when you would use one or the other: + +- `PluginBase` + - AJAX requests + - Flash messages + - Reusable widgets with their own data +- `Singleton` + - Event listeners + - Global utilities + - Base user-interface handlers + + +### Global events + +Global events are an intrinsic feature of the Snowboard framework, allowing Snowboard plugins to respond to specific events in the course of certain functionality, similar in concept to DOM events or the Event functionality of Winter CMS. + +There are two entities that are involved in any global event: + +- The **triggering** class, which fires the global event with optional extra context, and, +- The **listening** class, which listens for when a global event is fired, and actions its own functionality. + +There are also two types of global events that can be triggered, they are: + +- A **standard** event, which fires and executes all listeners in listening classes, and, +- A **Promise** event, which fires and waits for the Promise to be resolved before triggering further functionality. + +In practice, you would generally use standard events for events in where you do not necessarily want to wait for a response. In all other cases, you would use a Promise event which allows all listening classes to respond to the event in due course. + +Firing either event is done by calling either the `globalEvent` or `globalPromiseEvent` method directly on the main Snowboard class. + +```js +// Standard event +snowboard.globalEvent('myEvent', context); + +// Promise event +snowboard.globalPromiseEvent('myPromiseEvent').then( + () => { + // functionality when the promise is resolved + }, + () => { + // functionality when the promise is rejected + } +); +``` + +For a plugin to register as a listening class, it must specify a `listens` method in the plugin class that returns an object. Each key should be the global event being listened for, and the value should be the name of a method inside the class that will handle the event when fired. This is the same whether the event is a standard event or a Promise event. + +```js +class MyPlugin extends PluginBase { + listens() { + return { + ready: 'ready', + eventName: 'myHandler', + }; + } + + ready() { + // This method is run when the `ready` global event is fired. + } + + myHandler(context) { + // This method is run when the `eventName` global event is fired. + } +} +``` + +Snowboard only has one in-built global event that is fired - the `ready` event, which is fired when the DOM is loaded and the page is ready. This event is synonymous with jQuery's `ready` event, and is mainly used to instantiate the Singletons that have been registered with Snowboard. + diff --git a/snowboard-request.md b/snowboard-request.md new file mode 100644 index 00000000..714fe878 --- /dev/null +++ b/snowboard-request.md @@ -0,0 +1,368 @@ +# AJAX Requests (JavaScript API) + +- [Introduction](#introduction) +- [Request workflow](#request-workflow) +- [Available options](#available-options) +- [Global events](#global-events) +- [Element events](#element-events) +- [Usage examples](#usage-examples) +- [Extending or replacing the Request class](#extending-replacing) + + +## Introduction + +Snowboard provides core AJAX functionality via the `Request` Snowboard plugin. The `Request` plugin provides powerful flexibility and reusability in making AJAX Requests with the Backend functionality in Winter. It can be loaded by adding the following tag into your CMS Theme's page or layout: + +```twig +{% snowboard request %} +``` + +And called using the following code in your JavaScript: + +```js +Snowboard.request('#element', 'onAjax', {}); +``` + +The base `Request` plugin uses the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) provided in most modern browsers to execute AJAX requests from the frontend to the backend in Winter. + +>**NOTE:** If you would like to replace any part of the functionality of the base `Request` plugin then you can write a custom [Snowboard Plugin](plugin-development) that extends and overrides the base `Request` plugin to customize it as desired. + +The `request` method takes three parameters: + + +
+ +Parameter | Required | Description +--------- | -------- | ----------- +`element` | No | The element that this AJAX request is targeting, either as a `HTMLElement` instance, or as a CSS-selector string. This can be any element, but usually will be used with a `form` element. +`handler` | **Yes** | The AJAX handler to call. This should be in the format `on`. +`options` | No | The [AJAX request options](#available-options), as an object. + + +## Request workflow + +AJAX requests made through the `Request` class go through the following process: + +- The request is initialized and validated with the given element and options. +- If `browserValidate` is enabled and the AJAX request is done with a form, browser-side validation occurs. If this fails, the request is cancelled. +- If `confirm` is specified, a confirmation is presented to the user. If they cancel, the request is cancelled. +- The data provided to the Request, along with any form data if applicable, will be compiled. +- The AJAX request is sent to the Backend context using the [given handler](../snowboard/handlers). +- A response is received from the Backend context and processed, from here, one of three things happen: + - If the response is successful, then any partials that are instructed to be updated will be updated at this point. + - If the response is unsuccessful due to a validation error, then a validation message will be shown and the failing fields will be highlighted. + - If the response is unsuccessful due to any other error, then an error message will be shown. +- The request is then complete. + + +## Available options + +All options below are optional. + + +
+ +Option | Type | Description +------ | ---- | ----------- +`confirm` | `string` | If provided, the user will be prompted with this confirmation message, and will be required to confirm if they wish to proceed with the request. +`data` | `Object` | Extra data that will be sent to the server along with any form data, if available. If `files` is `true`, you may also include files in the request by using [`Blob` objects](https://developer.mozilla.org/en-US/docs/Web/API/Blob). +`redirect` | `string` | If provided, the browser will be redirected to this URL after a request is successfully completed. +`form` | `HTMLElement` or `string` | Specifies the form that data will be extracted from and sent in the request. If this is not provided, the form will be automatically determined from the element provided with the request. If no element is given, then no form will be used. +`files` | `boolean` | If `true`, this request will accept file uploads in the data. +`browserValidate` | `boolean` | If `true`, the in-built client-side validation provided by most common browsers will be performed before sending the request. This is only applied if a form is used in the request. +`flash` | `boolean` | If `true`, the request will process and display any flash messages returned in the response. +`update` | `Object` | Specifies a list of partials and page elements that can be changed through the AJAX response. The key of the object represents the partial name and the value represents the page element (as a CSS selector) to target for the update. If the selector string is prepended with an `@` symbol, the content will be appended to the target. If the selector string is prepended with a `^` symbol, it will instead be prepended to the target. +`fetchOptions` | `Object` | If specified, this will override the options used with the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/fetch) to make the request. + +The following callbacks may also be specified in the `options` parameter. All callbacks expect a function to be provided. The `this` keyword inside all callbacks will be assigned the `Request` instance that represents the current AJAX request. + +Callback | Description +-------- | ----------- +`beforeUpdate` | Executes before page elements are updated with the response data. The function receives one parameter: the response data from the AJAX response as an object. +`success` | Execures when the AJAX request is successfully responded to. The function receives two parameters: the response data from the AJAX response as an object, and the `Request` instance. +`error` | Executes when the AJAX request fails due to a server-side error or validation error. The function receives two parameters: the response data from the AJAX response as an object, and the `Request` instance. +`complete` | Executes when the AJAX request is complete, regardless of success or failure. The function receives two parameters: the response data from the AJAX response as an object, and the `Request` instance. + +Finally, the following option parameters define override functionality for various actions that the `Request` instance may take during the processing of a response. As with the callback methods, these must be provided as a function. + + +
+ +Option | Parameters | Description +------ | ---------- | ----------- +`handleConfirmMessage` | `(string) confirmationMessage` | Handles any confirmations requested of the user. +`handleErrorMessage` | `(string) errorMessage` | Handles any errors occuring during the request +`handleValidationMessage` | `(string) message, (Object) fieldMessages` | Handles validation errors occurring during the request. `fieldMessages` has field names as the key and messages as the value. +`handleFlashMessage` | `(string) message, (string) type` | Handles flash messages. +`handleRedirectResponse` | `(string) redirectUrl` | Handles redirect responses. + + +## Global events + +The `Request` class fires several global events which can be used by plugins to augment or override the functionality of the `Request` class. [Snowboard plugins](../snowboard/plugin-development) can be configured to listen to, and act upon, these events by using the `listen()` method to direct the event to a method with the plugin class. + +```js +class HandleFlash extends Snowboard.Singleton +{ + /** + * Defines listeners for global events. + * + * @returns {Object} + */ + listens() { + return { + // when the "ajaxFlashMessages" event is called, run the "doFlashMessage" method in this class + ajaxFlashMessages: 'doFlashMessages', + }; + } + + doFlashMessages(messages) { + Object.entries(messages).forEach((entry) => { + const [cssClass, message] = entry; + + showFlash(message, cssClass); + }); + } +} +``` + +Some events are called as Promise events, which means that your listener must itself return a [`Promise`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) this is either resolved or rejected. + +```js +class ConfirmEverything extends Snowboard.Singleton +{ + /** + * Defines listeners for global events. + * + * @returns {Object} + */ + listens() { + return { + // when the "ajaxConfirmMessage" event is called, run the "confirm" method in this class + ajaxConfirmMessage: 'confirm', + }; + } + + // Confirm all confirmation messages + confirm() { + return Promise.resolve(true); + } +} +``` + +The following events are called during the Request process: + + +
+ +Event | Promise? | Parameters | Description +----- | -------- | ---------- | ----------- +`ajaxSetup` | No | `(Request) request` | Called after the Request is initialized and checked that it can be called. It is intended to be used for modifying the Request parameters before sending to the server. Returning `false` in any event listeners will cancel the request. +`ajaxConfirmMessage` | Yes | `(Request) request, (string) confirmationMessage` | Called if `confirm` is specified, and the Request is ready to be sent to the server. This allows developers to customise the confirmation process or display. If an event listener rejects the Promise, this will cancel the request. +`ajaxBeforeSend` | No | `(Request) request` | Called immediately before the Request is sent. It can be used for final changes to the Request, or cancelling it prematurely. Returning `false` in any event listeners will cancel the request. +`ajaxFetchOptions` | No | `(Object) fetchOptions, (Request) request` | Called immediately when the `Fetch API` is initialised to make the request. It can be used to modify the Fetch options via a plugin. This event cannot be cancelled. +`ajaxStart` | No | `(Promise) callback, (Request) request` | Called when the Request is sent. This event cannot be cancelled. +`ajaxBeforeUpdate` | Yes | `(mixed) response, (Request) request` | Called when a successful response is returned and partials are going to be updated. It can be used to determine which partials will be updated, or can also be used to cancel the partial updates. If an event listener rejects the Promise, no partials will be updated. +`ajaxUpdate` | No | `(HTMLElement) element, (string) content, (Request) request` | Called when an individual partial is updated. It can be used to make further updates to an element, or handle updates. Note that this event is fired *after* the element is updated. This event cannot be cancelled. +`ajaxUpdateComplete` | No | `(array of HTMLElement) elements, (Request) request)` | Called when the partials are updated. It can be used to determine which partials have been updated. This event cannot be cancelled. +`ajaxSuccess` | No | `(Object) responseData, (Request) request` | Called when a successful response is returned and all partial updating is completed. It can be used to cancel further response handling (ie. redirects, flash messages). Returning `false` in any event listeners will prevent any further response handling from taking place. +`ajaxError` | No | `(Object) responseData, (Request) request` | Called when an unsuccessful response is returned from the AJAX request. It can be used to cancel further error handling. Returning `false` in any event listeners will prevent any further response handling from taking place. +`ajaxRedirect` | No | `(string) redirectUrl, (Request) request` | Called when a redirect is to take place, either from the response or through the `redirect` option. Returning `false` in any event listeners will prevent the redirect from executing. +`ajaxErrorMessage` | No | `(string) message, (Request) request` | Called when an error message is to be shown to the user. Returning `false` in any event listeners will prevent the default error handling (showing an alert to the user) from executing. +`ajaxFlashMessages` | No | `(array of Object) flashMessages, (Request) request` | Called when one or more flash messages are to be shown to the user. There is no default functionality for flash messages, so if no event listeners trigger for this event, no activity will occur. +`ajaxValidationErrors` | No | `(HTMLElement) form, (array) fieldMessages, (Request) request` | Called when a validation error is returned in the response. There is no default functionality for validation errors, so if no event listeners trigger for this event, no activity will occur. + + +## Element events + +In addition to global events, local events are fired on elements that trigger an AJAX request. These events are treated as [DOM events](https://developer.mozilla.org/en-US/docs/Web/API/Event) and thus can be listened to by normal DOM event listeners or your framework of choice. The `Request` class will inject properties in the event depending on the type of event and the `event.request` property will always be the `Request` instance. + +```js +const element = document.getElementById('my-button'); +element.addEventListener('ajaxAlways', (event) => { + console.log(event.request); // The Request instance + console.log(event.responseData); // The raw response data as an object + console.log(event.responseError); // An error object if the response failed +}); +``` + +In most cases, you can cancel the event, and thus the Request, by adding `event.preventDefault()` to your callback. + +```js +const element = document.getElementById('my-button'); +element.addEventListener('ajaxSetup', (event) => { + // Never process a request for this element + event.preventDefault(); +}); +``` + +The following events are called during the Request process directly on the element that triggered the request: + + +
+ +Event | Description +----- | ----------- +`ajaxSetup` | Called after the Request is initialized and checked that it can be called. +`ajaxPromise` | Called when the AJAX request is sent to the server. A property called `promise` is provided, which is the Promise that is resolved or rejected when the response succeeds or fails. +`ajaxUpdate` | Called when an element is updated by a partial update from an AJAX response. **This event is fired on the element that is updated, not the triggering element.** and thus does not get given the `Request` instance. A property called `content` is provided with the content that was added to this element. +`ajaxDone` | Called when this element makes a successful AJAX request. A property called `responseData` is provided with the raw response data, as an object. +`ajaxFail` | Called when this element makes an unsuccessful AJAX request. A property called `responseError` is provided with the error object. +`ajaxAlways` | Called when an AJAX request is completed, regardless of success or failure. It is provided two properties: `responseData` and `responseError`, which represent the raw response data and error object, depending on whether the AJAX request succeeded or failed. + + +## Usage examples + +Make a simple request to an AJAX handler without specifying a triggering element. + +```js +Snowboard.request(null, 'onAjax'); +``` + +Request a confirmation before submitting a form, and redirect them to a success page. + +```js +Snowboard.request('form', 'onSubmit', { + confirm: 'Are you sure you wish to submit this data?', + redirect: '/form/success', +}); +``` + +Run a calculation handler and inject some data from the page, then update the total. + +```js +Snowboard.request('#calculate', 'onCalculate', { + data: { + firstValue: document.getElementById('first-value').value, + secondValue: document.getElementById('second-value').value, + }, + update: { + totalResult: '.total-result' + }, +}); +``` + +Run a calculation handler and show a success message when calculated. + +```js +Snowboard.request('#calculate', 'onCalculate', { + data: { + firstValue: document.getElementById('first-value').value, + secondValue: document.getElementById('second-value').value, + }, + success: (data) => { + const total = data.total; + alert('The answer is ' + total); + }, +}); +``` + +Prompt a user to confirm a redirect. + +```js +Snowboard.request('form', 'onSubmit', { + handleRedirectResponse: (url) => { + if (confirm('Are you sure you wish to go to this URL: ' + url)) { + window.location.assign(url); + } else { + alert('Redirect cancelled'); + } + }, +}); +``` + +Track when an element is updated from an AJAX request. + +```js +const element = document.getElementById('updated-element'); +element.addEventListener('ajaxUpdate', (event) => { + console.log('The "updated-element" event was updated with the following content:', event.content); +}); +``` + +Disable file uploads globally from AJAX requests by modifying the `Request` instance. + +```js +// In your own Snowboard plugin +class DisableFileUploads extends Snowboard.Singleton +{ + listens() { + return { + ajaxSetup: 'stopFileUploads', + }; + } + + stopFileUploads(request) { + request.options.files = false; + } +} +``` + + +## Extending or replacing the Request class + +As part of making Snowboard an extensible and flexible platform, developers have the option to extend, or replace entirely, the base Request class. This allows developers to use their own preferred platforms and frameworks for executing the AJAX functionality, partial updates, and much more. + +For example, if a developer wanted to use the [Axios library](https://github.com/axios/axios) to execute AJAX requests, as opposed to the in-built Fetch API, one could do this by creating their own Snowboard plugin and extending the `Request` class, replacing the `doAjax()` method in their own class: + +```js +const axios = require('axios'); + +class AxiosRequest extends Request +{ + doAjax() { + // Allow plugins to cancel the AJAX request before sending + if (this.snowboard.globalEvent('ajaxBeforeSend', this) === false) { + return Promise.resolve({ + cancelled: true, + }); + } + + const ajaxPromise = axios({ + method: 'post', + url: this.url, + headers: this.headers, + data: this.data, + }); + + this.snowboard.globalEvent('ajaxStart', ajaxPromise, this); + + if (this.element) { + const event = new Event('ajaxPromise'); + event.promise = ajaxPromise; + this.element.dispatchEvent(event); + } + + return ajaxPromise; + } +} +``` + +You could then either replace the `request` plugin with this class, or alias it as something else: + +```js +Snowboard.removePlugin('request'); +Snowboard.addPlugin('request', AxiosRequest); + +// Or run it as an alias +Snowboard.addPlugin('axios', AxiosRequest); +// And call it thusly +Snowboard.axios('#my-element', 'onSubmit'); +``` + +For more information on the best practices with setting up a Snowboard plugin, view the [Plugin Development](../snowboard/plugin-development) documentation. \ No newline at end of file diff --git a/snowboard-utilities.md b/snowboard-utilities.md new file mode 100644 index 00000000..ecacdbd5 --- /dev/null +++ b/snowboard-utilities.md @@ -0,0 +1,276 @@ +# Snowboard Utilities + +- [Introduction](#introduction) +- [Cookie](#cookie) + - [Basic Usage](#cookie-basic-usage) + - [Encoding](#cookie-encoding) + - [Cookie Attributes](#cookie-attributes) + - [expires](#cookie-attributes-expires) + - [path](#cookie-attributes-path) + - [domain](#cookie-attributes-domain) + - [secure](#cookie-attributes-secure) + - [sameSite](#cookie-attributes-sameSite) + - [Setting Defaults](#cookie-attributes-defaults) + - [Cookie Events](#cookie-events) + - [`cookie.get`](#cookie-event-get) + - [`cookie.set`](#cookie-event-set) +- [JSON Parser](#json-parser) +- [Sanitizer](#sanitizer) + + +## Introduction + +The Snowboard framework included several small utilities by default that help make development easier. + + +## Cookie + +The Cookie utility is a small wrapper around the [js-cookie](https://github.com/js-cookie/js-cookie/) package that provides a simple, lightweight JS API for interacting with browser cookies. + + +### Basic Usage + +Create a cookie, valid across the entire site: + +```js +Snowboard.cookie().set('name', 'value') +``` + +Create a cookie that expires 7 days from now, valid across the entire site: + +```js +Snowboard.cookie().set('name', 'value', { expires: 7 }) +``` + +Create an expiring cookie, valid to the path of the current page: + +```js +Snowboard.cookie().set('name', 'value', { expires: 7, path: '' }) +``` + +Read cookie: + +```js +Snowboard.cookie().get('name') // => 'value' +Snowboard.cookie().get('nothing') // => undefined +``` + +Read all visible cookies: + +```js +Snowboard.cookie().get() // => { name: 'value' } +``` + +>**NOTE:** Cookies can only be read if the place they are being read from has access to read the cookie according to the browser. + +Delete cookie: + +```js +Snowboard.cookie().remove('name') +``` + +Delete a cookie valid to the path of the current page: + +```js +Snowboard.cookie().set('name', 'value', { path: '' }) +Snowboard.cookie().remove('name') // fail! +Snowboard.cookie().remove('name', { path: '' }) // removed! +``` + +>**IMPORTANT!** When deleting a cookie and you're not relying on the [default attributes](#cookie-attributes-defaults), you must pass the exact same path and domain attributes that were used to set the cookie + +```js +Snowboard.cookie().remove('name', { path: '', domain: '.yourdomain.com' }) +``` + +>**NOTE:** Removing a nonexistent cookie neither raises any exception nor returns any value. + + +### Encoding + +The package is [RFC 6265](http://tools.ietf.org/html/rfc6265#section-4.1.1) compliant. All special characters that are not allowed in the cookie-name or cookie-value are encoded with each one's UTF-8 Hex equivalent using [percent-encoding](http://en.wikipedia.org/wiki/Percent-encoding). +The only character in cookie-name or cookie-value that is allowed and still encoded is the percent `%` character, it is escaped in order to interpret percent input as literal. +Please note that the default encoding/decoding strategy is meant to be interoperable [only between cookies that are read/written by js-cookie](https://github.com/js-cookie/js-cookie/pull/200#discussion_r63270778). To override the default encoding/decoding strategy you need to use a [converter](#cookie-converters). + +>**NOTE:** According to [RFC 6265](https://tools.ietf.org/html/rfc6265#section-6.1), your cookies may get deleted if they are too big or there are too many cookies in the same domain, [more details here](https://github.com/js-cookie/js-cookie/wiki/Frequently-Asked-Questions#why-are-my-cookies-being-deleted). + + +### Cookie Attributes + +Cookie attributes can be set globally by creating an instance of the API via `withAttributes()`, or individually for each call to `Snowboard.cookie().set(...)` by passing a plain object as the last argument. Per-call attributes override the default attributes. + +>**NOTE:** You should never allow untrusted input to set the cookie attributes or you might be exposed to a [XSS attack](https://github.com/js-cookie/js-cookie/issues/396). + + +#### expires + +Defines when the cookie will be removed. Value must be a [`Number`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number) which will be interpreted as days from time of creation or a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) instance. If omitted, the cookie becomes a session cookie. + +To create a cookie that expires in less than a day, you can check the [FAQ on the Wiki](https://github.com/js-cookie/js-cookie/wiki/Frequently-Asked-Questions#expire-cookies-in-less-than-a-day). + +**Default:** Cookie is removed when the user closes the browser. + +**Examples:** + +```js +Snowboard.cookie().set('name', 'value', { expires: 365 }) +Snowboard.cookie().get('name') // => 'value' +Snowboard.cookie().remove('name') +``` + + +#### path + +A [`String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) indicating the path where the cookie is visible. + +**Default:** `/` + +**Examples:** + +```js +Snowboard.cookie().set('name', 'value', { path: '' }) +Snowboard.cookie().get('name') // => 'value' +Snowboard.cookie().remove('name', { path: '' }) +``` + + +#### domain + +A [`String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) indicating a valid domain where the cookie should be visible. The cookie will also be visible to all subdomains. + +**Default:** Cookie is visible only to the domain or subdomain of the page where the cookie was created + +**Examples:** + +Assuming a cookie that is being created on `example.com`: + +```js +Snowboard.cookie().set('name', 'value', { domain: 'subdomain.example.com' }) +Snowboard.cookie().get('name') // => undefined (need to read at 'subdomain.example.com') +``` + + +#### secure + +Either `true` or `false`, indicating if the cookie transmission requires a secure protocol (https). + +**Default:** No secure protocol requirement. + +**Examples:** + +```js +Snowboard.cookie().set('name', 'value', { secure: true }) +Snowboard.cookie().get('name') // => 'value' +Snowboard.cookie().remove('name') +``` + + +#### sameSite + +A [`String`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String), allowing to control whether the browser is sending a cookie along with cross-site requests. + +Default: not set. + +>**NOTE:** More recent browsers are making "Lax" the default value even without specifiying anything here. + +**Examples:** + +```js +Snowboard.cookie().set('name', 'value', { sameSite: 'strict' }) +Snowboard.cookie().get('name') // => 'value' +Snowboard.cookie().remove('name') +``` + + +#### Setting up defaults + +In order to set global defaults that are used for every cookie that is created with the `Snowboard.cookie().set('name', 'value')` method you can call the `setDefaults(options)` method on the Cookie plugin and it will set the provided options as the global defaults. + +If you want to get the current defaults, call `Snowboard.cookie().getDefaults()`. + +```js +Snowboard.cookie().setDefaults({ path: '/', domain: '.example.com' }); +Snowboard.cookie().set('example', 'value'); +``` + + +### Events + +The Cookie plugin provides the ability to interact with cookies and modify their values during accessing or creating. + + +#### `cookie.get` + +This event runs during `Snowboard.cookie().get()` and provides the `(string) name` & `(string) value` parameters, along with a callback method that can be used by a plugin to override the cookie value programatically. This can be used to manipulate or decode cookie values. + +```js +class CookieDecryptor extends Singleton +{ + listens() { + return { + 'cookie.get': 'decryptCookie', + }; + } + + decryptCookie(name, value, setValue) { + if (name === 'secureCookie') { + setValue(decrypt(value)); + } + } +} +``` + + +#### `cookie.set` + +This event runs during `Snowboard.cookie().set()` and provides the `(string) name` & `(string) value` parameters, along with a callback method that can be used by a plugin to override the value saved to the cookie programatically. This will allow you to manipulate or encrypt cookie values before storing them with the browser. + +```js +class CookieEncryptor extends Singleton +{ + listens() { + return { + 'cookie.set': 'encryptCookie', + }; + } + + encryptCookie(name, value, setValue) { + if (name === 'secureCookie') { + setValue(encrypt(value)); + } + } +} +``` + + +## JSON Parser + +The JSON Parser utility is used to safely parse JSON-like (JS-object strings) data that does not strictly meet the JSON specifications. It is especially useful for parsing the values provided in the `data-request-data` attribute used by the [Data Attributes](data-attributes) functionality. + +This is somewhat similar to [JSON5](https://json5.org/) or [RJSON](http://www.relaxedjson.org/), but not exactly. The key aspect is that it allows for data represented as a JavaScript Object in string form as if it was actively running JS to be parsed without the use of `eval()` which could cause issues with Content Security Policies that block the use of `eval()`. + +>**NOTE:** Although this functionality is documented it is unlikely that regular developers will ever have need of interacting with this feature. + +### Usage: + +```js +let data = "key: value, otherKey: 'other value'; +let object = Snowboard.jsonParser().parse(`{${data}}`); +``` + + +## Sanitizer + +The Sanitizer utility is a client-side HTML sanitizer designed mostly to prevent self-XSS attacks. Such an attack could look like a user copying content from a website that uses clipboard injection to hijack the values actually stored in the clipboard and then having the user paste the content into an environment where the content would be treated as HTML, typically in richeditor / WYSIWYG fields. + +The sanitizer utility will strip all attributes that start with `on` (usually JS event handlers as attributes, i.e. `onload` or `onerror`) or that contain the `javascript:` pseudo protocol in their values. + +It is available both as a global function (`wnSanitize(html)`) and as a Snowboard plugin. + +The following example shows how the Froala WYSIWYG editor can be hooked into to protect against a clipboard injection / self-XSS attack. + +```js +$froalaEditor.on('froalaEditor.paste.beforeCleanup', function (ev, editor, clipboard_html) { + return Snowboard.sanitizer().sanitize(clipboard_html); +}); +``` \ No newline at end of file diff --git a/themes-development.md b/themes-development.md index 4ae6c63d..f1ea27ce 100644 --- a/themes-development.md +++ b/themes-development.md @@ -40,6 +40,7 @@ Field | Description `code` | the theme code, optional. The value is used on the Winter CMS marketplace for initializing the theme code value. If the theme code is not provided, the theme directory name will be used as a code. When a theme is installed from the Marketplace, the code is used as the new theme directory name. `form` | a configuration array or reference to a form field definition file, used for [theme customization](#customization), optional. `require` | an array of plugin names used for [theme dependencies](#dependencies), optional. +`mix` | an object that defines Mix packages contained in your theme for [asset compilation](../console/asset-compilation). Example of the theme information file: