diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dec708e1f5..9f4f0eecfe 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -205,6 +205,17 @@ example.exampleMessage = 'hello world';
**Note:** Be careful of choosing your attribute names, never overwrite existing standard attributes without good reason!
+#### `batchProps(props)`
+
+A fast and simpler way to update multiple props in one go.
+Especially useful for integrations and to prevent multiple or delayed re-renders.
+
+#### `shouldUpdateCallback(newValue, oldValue)`
+
+`shouldUpdateCallback()` is invoked upon `attributeChangedCallback()` or Property `setter()` invocation to determine if rendering is necessary when new props are being received - it returns `true` if re-rendering is desireable, else `false`.
+
+**Important:** This does only a shallow comparison, if you need to deal with more complex data, like objects or arrays either stick to immutable data structures or override this method to implement your own test.
+
#### `willRenderCallback(initial)`
Invoked before the custom element's [flattened DOM](#flattened-dom) will be rendered.
@@ -221,6 +232,44 @@ Invoked when the custom element is disconnected from the document's DOM.
The render loop makes sure that upon each [`attributeChangedCallback()`](#attributechangedcallbackname-oldvalue-newvalue) invocation or any observed [property `setter()`](#property-setter) invocation that the flattened DOM is recomputed and that [`willRenderCallback()`](#willrendercallbackinitial) and [`didRenderCallback()`](#didrendercallbackinitial) lifecycle hooks are called respectively.
+## Integration
+
+The goal is that custom elements can be shared across frameworks and libraries like Angular, React, Vue, you name it. To ease this process we provide generic wrapper functions.
+
+### `withReact()`
+
+To turn any custom element into a working React Component, you just need to follow these steps:
+
+1. `import` React
+2. `import` withReact
+3. `import` any web components you need
+4. wrap all your needed web components
+ - and may pass optional options for type of component or event init options
+5. use them like regular React components in your app
+
+ **Note:** events work similar to React's standard events, but each web components could trigger custom events like `axa-click` - camelcased and `on`-prefixed in React such as `onAxaClick={yourEventHandler}`. Make sure to check them out at the web-components documentation itself!
+
+```js
+// import your dependencies - 1, 2, and 3
+import React from 'react';
+import withReact from '@axa-ch/patterns-library/src/js/with-react';
+import AXAButton from '@axa-ch/patterns-library/dist/components/m-button';
+
+// 4. wrap your needed web components
+// and optionally pass options
+const AXAButtonReact = withReact(AXAButton, {
+ pure: true,
+ // event init options are also supported
+ passive: false,
+});
+
+// 5. use them in your app like regular React components
+// note the custom event axa-click - camelcased and on-prefixed in React
+const MyApp = ({ color, onClick }) => (
+ Hello World
+);
+```
+
# How do we release a new version
Please run `npm run release` and follow the steps in the wizard.
diff --git a/package.json b/package.json
index 98e391aabe..e635a3cd30 100644
--- a/package.json
+++ b/package.json
@@ -139,5 +139,8 @@
"nanohtml": "^1.2.2",
"react": "^16.2.0",
"react-dom": "^16.2.0"
+ },
+ "peerDependencies": {
+ "react": ">=0.14.0"
}
}
diff --git a/src/components/m-button/_template.js b/src/components/m-button/_template.js
index 27419811e8..5ffcc35b80 100644
--- a/src/components/m-button/_template.js
+++ b/src/components/m-button/_template.js
@@ -13,7 +13,7 @@ export default function ({
gpu,
arrow,
}, childrenFragment) {
- const buttonClasses = classnames('m-button', classes, {
+ const buttonClasses = classnames('m-button', 'js-button', classes, {
[`m-button--${color}`]: color,
[`m-button--${size}`]: size,
'm-button--ghost': ghost,
diff --git a/src/components/m-button/index.js b/src/components/m-button/index.js
index 789740858e..c1a0c3d646 100644
--- a/src/components/m-button/index.js
+++ b/src/components/m-button/index.js
@@ -2,6 +2,7 @@ import styles from './index.scss';
import template from './_template';
import BaseComponentGlobal from '../../js/abstract/base-component-global';
import wcdomready from '../../js/wcdomready';
+import Button from './js/button';
class AXAButton extends BaseComponentGlobal {
static get observedAttributes() { return ['arrow', 'classes', 'color', 'ghost', 'motion', 'size', 'tag', 'url']; }
@@ -9,6 +10,21 @@ class AXAButton extends BaseComponentGlobal {
constructor() {
super(styles, template);
}
+
+ didRenderCallback() {
+ if (this.button) {
+ this.button.destroy();
+ }
+
+ this.button = new Button(this);
+ }
+
+ disconnectedCallback() {
+ if (this.button) {
+ this.button.destroy();
+ delete this.button;
+ }
+ }
}
wcdomready(() => {
diff --git a/src/components/m-button/js/button.js b/src/components/m-button/js/button.js
new file mode 100644
index 0000000000..3979c80749
--- /dev/null
+++ b/src/components/m-button/js/button.js
@@ -0,0 +1,78 @@
+import on from '../../../js/on';
+import fire from '../../../js/fire';
+
+/**
+ * @fires Button#axa-click
+ */
+class Button {
+ static DEFAULTS = {
+ button: '.js-button',
+ };
+
+ constructor(wcNode, options = {}) {
+ this.wcNode = wcNode;
+
+ this.options = {
+ ...Button.DEFAULTS,
+ ...options,
+ };
+
+ this.handleClick = this.handleClick.bind(this);
+
+ this.init();
+ }
+
+ init() {
+ this.button = this.wcNode.querySelector(this.options.button);
+
+ this.on();
+ }
+
+ on() {
+ this.off();
+
+ this.unClick = on(this.button, 'click', this.handleClick, {
+ passive: false,
+ });
+ }
+
+ off() {
+ if (this.unClick) {
+ this.unClick();
+ }
+ }
+
+ handleClick(event) {
+ /**
+ * axa-click event.
+ *
+ * @event Button#axa-click
+ * @type {null}
+ */
+ const cancelled = fire(this.wcNode, 'axa-click', null, { bubbles: true, cancelable: true, composed: true });
+
+ if (!cancelled) {
+ event.preventDefault();
+ }
+ }
+
+ destroy() {
+ this.off();
+
+ if (this.button) {
+ delete this.button;
+ }
+
+ if (this.wcNode) {
+ delete this.wcNode;
+ }
+
+ if (this.options) {
+ delete this.options;
+ }
+
+ delete this.handleClick;
+ }
+}
+
+export default Button;
diff --git a/src/components/m-footer-links/_template.js b/src/components/m-footer-links/_template.js
index ad122c4c74..26a506b174 100644
--- a/src/components/m-footer-links/_template.js
+++ b/src/components/m-footer-links/_template.js
@@ -8,9 +8,9 @@ export default function ({ title, items }) {
diff --git a/src/components/m-footer-links/index.scss b/src/components/m-footer-links/index.scss
index 6ff2c76431..124c636a96 100644
--- a/src/components/m-footer-links/index.scss
+++ b/src/components/m-footer-links/index.scss
@@ -129,6 +129,7 @@
color: $footer-links__link-color;
+ .is-footer-links__list-item-active > &,
&:hover,
&:active,
&:focus {
diff --git a/src/components/m-footer-links/js/footer-links.js b/src/components/m-footer-links/js/footer-links.js
index 105de1c5bc..782e296f96 100644
--- a/src/components/m-footer-links/js/footer-links.js
+++ b/src/components/m-footer-links/js/footer-links.js
@@ -1,12 +1,28 @@
+import on from '../../../js/on';
+import fire from '../../../js/fire';
+import getAttribute from '../../../js/get-attribute';
import { subscribe } from '../../../js/pubsub';
import DropDown from '../../m-dropdown/js/drop-down';
const hasDropdownBreakpoints = 'xs';
// @TODO: dependency to a-device-state not explicit
+/**
+ * @fires FooterLinks#axa-click
+ */
export default class FooterLinks {
- constructor(wcNode) {
+ static DEFAULTS = {
+ link: 'js-footer-links__link',
+ };
+
+ constructor(wcNode, options) {
this.wcNode = wcNode;
+ this.options = {
+ ...FooterLinks.DEFAULTS,
+ ...options,
+ };
+
+ this.handleClick = this.handleClick.bind(this);
this.on();
}
@@ -25,12 +41,35 @@ export default class FooterLinks {
delete this.dropDown;
}
});
+
+ this.unClick = on(this.wcNode, 'click', this.options.link, this.handleClick, { passive: false });
}
off() {
if (this.unsubscribe) {
this.unsubscribe();
}
+
+ if (this.unClick) {
+ this.unClick();
+ }
+ }
+
+ handleClick(event, delegateTarget) {
+ // @todo: would be cool to be able to use props here, cause now it needs JSON.parse...
+ const index = getAttribute(delegateTarget, 'index');
+ const { wcNode: { items } } = this;
+ /**
+ * axa-click event.
+ *
+ * @event FooterLinks#axa-click
+ * @type {object}
+ */
+ const cancelled = fire(this.wcNode, 'axa-click', items[index], { bubbles: true, cancelable: true, composed: true });
+
+ if (!cancelled) {
+ event.preventDefault();
+ }
}
destroy() {
diff --git a/src/components/m-header-logo/_template.js b/src/components/m-header-logo/_template.js
index b18f8a8f3e..c76b47cc19 100644
--- a/src/components/m-header-logo/_template.js
+++ b/src/components/m-header-logo/_template.js
@@ -2,7 +2,7 @@ import html from 'nanohtml';
import raw from 'nanohtml/raw';
export default ({ src, alt = 'AXA Logo', href = '#' } = {}) => html`
-