Socket
Socket
Sign inDemoInstall

@lion/field

Package Overview
Dependencies
Maintainers
1
Versions
103
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@lion/field - npm Package Compare versions

Comparing version 0.11.3 to 0.11.4

test-helpers.js

11

CHANGELOG.md

@@ -6,2 +6,13 @@ # Change Log

## [0.11.4](https://github.com/ing-bank/lion/compare/@lion/field@0.11.3...@lion/field@0.11.4) (2020-03-19)
### Bug Fixes
* normalization model-value-changed events ([1b6c3a4](https://github.com/ing-bank/lion/commit/1b6c3a44c820b9d61c26849b91bbb1bc8d6c772b))
## [0.11.3](https://github.com/ing-bank/lion/compare/@lion/field@0.11.2...@lion/field@0.11.3) (2020-03-05)

@@ -8,0 +19,0 @@

5

package.json
{
"name": "@lion/field",
"version": "0.11.3",
"version": "0.11.4",
"description": "Fields are the most fundamental building block of the Form System",

@@ -34,2 +34,3 @@ "author": "ing-bank",

"test-suites",
"test-helpers",
"translations",

@@ -48,3 +49,3 @@ "*.js"

},
"gitHead": "54c4432b818cf1f6023557d0df5c461ae50412da"
"gitHead": "f286e3ac28fe0b34284383344fda4dc8e9ea593d"
}

@@ -292,3 +292,6 @@ /* eslint-disable class-methods-use-this */

this.dispatchEvent(
new CustomEvent('model-value-changed', { bubbles: true, composed: true }),
new CustomEvent('model-value-changed', {
bubbles: true,
detail: { formPath: [this] },
}),
);

@@ -295,0 +298,0 @@ }

@@ -58,2 +58,18 @@ import { html, css, nothing, dedupeMixin, SlotMixin } from '@lion/core';

_ariaDescribedNodes: Array,
/**
* Based on the role, details of handling model-value-changed repropagation differ.
* @type {'child'|'fieldset'|'choice-group'}
*/
_repropagationRole: String,
/**
* By default, a field with _repropagationRole 'choice-group' will act as an
* 'endpoint'. This means it will be considered as an individual field: for
* a select, individual options will not be part of the formPath. They
* will.
* Similarly, components that (a11y wise) need to be fieldsets, but 'interaction wise'
* (from Application Developer perspective) need to be more like fields
* (think of an amount-input with a currency select box next to it), can set this
* to true to hide private internals in the formPath.
*/
_isRepropagationEndpoint: Boolean,
};

@@ -155,2 +171,4 @@ }

this._ariaDescribedNodes = [];
this._repropagationRole = 'child';
this.addEventListener('model-value-changed', this.__repropagateChildrenValues);
}

@@ -558,3 +576,93 @@

}
firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.__dispatchInitialModelValueChangedEvent();
}
async __dispatchInitialModelValueChangedEvent() {
// When we are not a fieldset / choice-group, we don't need to wait for our children
// to send a unified event
if (this._repropagationRole === 'child') {
return;
}
await this.registrationComplete;
// Initially we don't repropagate model-value-changed events coming
// from children. On firstUpdated we re-dispatch this event to maintain
// 'count consistency' (to not confuse the application developer with a
// large number of initial events). Initially the source field will not
// be part of the formPath but afterwards it will.
this.__repropagateChildrenInitialized = true;
this.dispatchEvent(
new CustomEvent('model-value-changed', {
bubbles: true,
detail: { formPath: [this], initialize: true },
}),
);
}
// eslint-disable-next-line class-methods-use-this, no-unused-vars
_onBeforeRepropagateChildrenValues(ev) {}
__repropagateChildrenValues(ev) {
// Allows sub classes to internally listen to the children change events
// (before stopImmediatePropagation is called below).
this._onBeforeRepropagateChildrenValues(ev);
// Normalize target, we also might get it from 'portals' (rich select)
const target = (ev.detail && ev.detail.element) || ev.target;
const isEndpoint =
this._isRepropagationEndpoint || this._repropagationRole === 'choice-group';
// Prevent eternal loops after we sent the event below.
if (target === this) {
return;
}
// A. Stop sibling handlers
//
// Make sure our sibling event listeners (added by Application developers) will not get
// the child model-value-changed event, but the repropagated one at the bottom of this
// method
ev.stopImmediatePropagation();
// B1. Are we still initializing? If so, halt...
//
// Stop repropagating children events before firstUpdated and make sure we de not
// repropagate init events of our children (we already sent our own
// initial model-value-change event in firstUpdated)
const isGroup = this._repropagationRole !== 'child'; // => fieldset or choice-group
const isSelfInitializing = isGroup && !this.__repropagateChildrenInitialized;
const isChildGroupInitializing = ev.detail && ev.detail.initialize;
if (isSelfInitializing || isChildGroupInitializing) {
return;
}
// B2. Are we a single choice choice-group? If so, halt when unchecked
//
// We only send the checked changed up (not the unchecked). In this way a choice group
// (radio-group, checkbox-group, select/listbox) acts as an 'endpoint' (a single Field)
// just like the native <select>
if (this._repropagationRole === 'choice-group' && !this.multipleChoice && !target.checked) {
return;
}
// C1. We are ready to dispatch. Create a formPath
//
// Compute the formPath. Choice groups are regarded 'end points'
let parentFormPath = [];
if (!isEndpoint) {
parentFormPath = (ev.detail && ev.detail.formPath) || [target];
}
const formPath = [...parentFormPath, this];
// C2. Finally, redispatch a fresh model-value-changed event from our host, consumable
// for an Application Developer
//
// Since for a11y everything needs to be in lightdom, we don't add 'composed:true'
this.dispatchEvent(
new CustomEvent('model-value-changed', { bubbles: true, detail: { formPath } }),
);
}
},
);

@@ -49,2 +49,5 @@ // eslint-disable-next-line max-classes-per-file

});
this.registrationComplete = new Promise(resolve => {
this.__resolveRegistrationComplete = resolve;
});

@@ -80,2 +83,12 @@ this._onRequestToAddFormElement = this._onRequestToAddFormElement.bind(this);

this.__readyForRegistration = true;
// After we allow our children to register, we need to wait one tick before they
// all sent their 'form-element-register' event.
// TODO: allow developer to delay this moment, similar to LitElement.performUpdate can be
// delayed.
setTimeout(() => {
this.registrationHasCompleted = true;
this.__resolveRegistrationComplete();
});
formRegistrarManager.becomesReady();

@@ -82,0 +95,0 @@ this.__hasBeenRendered = true;

115

test/FormControlMixin.test.js

@@ -1,5 +0,7 @@

import { expect, fixture, html, defineCE, unsafeStatic } from '@open-wc/testing';
import { expect, html, defineCE, unsafeStatic } from '@open-wc/testing';
import { LitElement, SlotMixin } from '@lion/core';
import sinon from 'sinon';
import { formFixture as fixture } from '../test-helpers/formFixture.js';
import { FormControlMixin } from '../src/FormControlMixin.js';
import { FormRegistrarMixin } from '../src/registration/FormRegistrarMixin.js';

@@ -182,2 +184,111 @@ describe('FormControlMixin', () => {

});
describe('Model-value-changed event propagation', () => {
const FormControlWithRegistrarMixinClass = class extends FormControlMixin(
FormRegistrarMixin(SlotMixin(LitElement)),
) {
static get properties() {
return {
modelValue: {
type: String,
},
};
}
};
const groupElem = defineCE(FormControlWithRegistrarMixinClass);
const groupTag = unsafeStatic(groupElem);
describe('On initialization', () => {
it('redispatches one event from host', async () => {
const formSpy = sinon.spy();
const fieldsetSpy = sinon.spy();
const formEl = await fixture(html`
<${groupTag} name="form" ._repropagationRole=${'form-group'} @model-value-changed=${formSpy}>
<${groupTag} name="fieldset" ._repropagationRole=${'form-group'} @model-value-changed=${fieldsetSpy}>
<${tag} name="field"></${tag}>
</${groupTag}>
</${groupTag}>
`);
const fieldsetEl = formEl.querySelector('[name=fieldset]');
expect(fieldsetSpy.callCount).to.equal(1);
const fieldsetEv = fieldsetSpy.firstCall.args[0];
expect(fieldsetEv.target).to.equal(fieldsetEl);
expect(fieldsetEv.detail.formPath).to.eql([fieldsetEl]);
expect(formSpy.callCount).to.equal(1);
const formEv = formSpy.firstCall.args[0];
expect(formEv.target).to.equal(formEl);
expect(formEv.detail.formPath).to.eql([formEl]);
});
});
describe('After initialization', () => {
it('redispatches one event from host and keeps formPath history', async () => {
const formSpy = sinon.spy();
const fieldsetSpy = sinon.spy();
const fieldSpy = sinon.spy();
const formEl = await fixture(html`
<${groupTag} name="form">
<${groupTag} name="fieldset">
<${tag} name="field"></${tag}>
</${groupTag}>
</${groupTag}>
`);
const fieldEl = formEl.querySelector('[name=field]');
const fieldsetEl = formEl.querySelector('[name=fieldset]');
formEl.addEventListener('model-value-changed', formSpy);
fieldsetEl.addEventListener('model-value-changed', fieldsetSpy);
fieldEl.addEventListener('model-value-changed', fieldSpy);
fieldEl.dispatchEvent(new Event('model-value-changed', { bubbles: true }));
expect(fieldsetSpy.callCount).to.equal(1);
const fieldsetEv = fieldsetSpy.firstCall.args[0];
expect(fieldsetEv.target).to.equal(fieldsetEl);
expect(fieldsetEv.detail.formPath).to.eql([fieldEl, fieldsetEl]);
expect(formSpy.callCount).to.equal(1);
const formEv = formSpy.firstCall.args[0];
expect(formEv.target).to.equal(formEl);
expect(formEv.detail.formPath).to.eql([fieldEl, fieldsetEl, formEl]);
});
it('sends one event for single select choice-groups', async () => {
const formSpy = sinon.spy();
const choiceGroupSpy = sinon.spy();
const formEl = await fixture(html`
<${groupTag} name="form">
<${groupTag} name="choice-group" ._repropagationRole=${'choice-group'}>
<${tag} name="choice-group" id="option1" .checked=${true}></${tag}>
<${tag} name="choice-group" id="option2"></${tag}>
</${groupTag}>
</${groupTag}>
`);
const choiceGroupEl = formEl.querySelector('[name=choice-group]');
const option1El = formEl.querySelector('#option1');
const option2El = formEl.querySelector('#option2');
formEl.addEventListener('model-value-changed', formSpy);
choiceGroupEl.addEventListener('model-value-changed', choiceGroupSpy);
// Simulate check
option2El.checked = true;
option2El.dispatchEvent(new Event('model-value-changed', { bubbles: true }));
option1El.checked = false;
option1El.dispatchEvent(new Event('model-value-changed', { bubbles: true }));
expect(choiceGroupSpy.callCount).to.equal(1);
const choiceGroupEv = choiceGroupSpy.firstCall.args[0];
expect(choiceGroupEv.target).to.equal(choiceGroupEl);
expect(choiceGroupEv.detail.formPath).to.eql([choiceGroupEl]);
expect(formSpy.callCount).to.equal(1);
const formEv = formSpy.firstCall.args[0];
expect(formEv.target).to.equal(formEl);
expect(formEv.detail.formPath).to.eql([choiceGroupEl, formEl]);
});
});
});
});
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc