studiometa / js-toolkit Goto Github PK
View Code? Open in Web Editor NEWπ§ A data-attributes driven JavaScript micro-framework
Home Page: https://js-toolkit.studiometa.dev/
License: MIT License
π§ A data-attributes driven JavaScript micro-framework
Home Page: https://js-toolkit.studiometa.dev/
License: MIT License
The doc, demo and source files all share the same module as there is only one package.json file. The introduction of workspaces in the latest version of NPM could help create a better folder structure by separating all 3 parts of this project.
The new structure could be as follow:
package.json β private
packages/
demo/ β private
docs/ β private
js-toolkit/ β public
Note: we have to be careful to not break the import structure of the package.
There is currently a toLowerCase()
call on the event name when association a class method to a child component or ref event, this does not allow the usage of event names in camelCase or PascalCase.
We should either allow only alpha lowercase names for events (i.e. myevent
instead of myEvent
or my-event
) or implement a more robust matching between the class methods (in camelCase) and any potential event name (in kebab-case, snake_case, camelCase, PascalCase, etc.).
Currently if you run App.$update()
it will only trigger the updated()
hook in the App class. Not on the child components.
It would be usefull to also trigger the updated()
hook on child components.
This would help in case like reseting a state / rerunning an animation in a component when App is updated.
Potential solution in src/abstracs/Base/index.js
replacing mountComponents(this)
with mountOrUpdateComponents(this)
this.$on('updated', () => {
unbindMethods.forEach((method) => method());
unbindMethods = [...bindServices(this), ...bindEvents(this)];
mountComponents(this);
});
When working with Vue, we often need to re-use some utilities from this toolkit.
$mq
plugin to be able to use the breakpoint
and breakpoints
values from the resize service with Vue reactivityThis would help stay consistent when mounting a Vue application inside of a Base component.
import Base from '@studiometa/js-toolkit';
import withVue from '@studiometa/js-toolkit/decorators/withVue';
import VueComponent from './VueComponent.vue';
class ComponentWithVue extends withVue(Base) {
static vueConfig = {
components: {
VueComponent,
},
render: (h) => h('VueComponent'),
}
get vueRootElement() {
return this.$refs.vue;
}
mounted() {
// Vue instance is available with `this.$vue`
console.log(this.$vue);
}
}
Vue should be added as an optionalDependency to the package.
The decorator should:
For now, event bindings with onChild...
methods will only work for events that are defined in the child component configuration. It would be interesting to be able to directly use a onChildClick
method without having to define the click
event in the config.emits
array. We could then use child components more easily and without having to pass on native events.
config
getter into a static propertyIt makes more sense as the config
getter returns the same value for each instance. The get config()
pattern would be marked as deprecated but would still be available for some time.
// Declaring a config objet
class Component extends Base {
static config = {
name: 'Component'
};
}
// Inheriting a parent config object
class ComponentOverride extends Component {
static config = {
...Component.config,
name: 'ComponentOverride',
};
}
Today, the config
getter is merged with the data-options
attribute of the rool element for each instance. This might be complex to maintain for the long run. I suggest to separate the dynamic values of the config into props in order to keep the config
as readonly and to simplify the management of dynamic values.
A new props
property would be introduced to the config
object to define the dynamic values which would be defined with data-prop-[name]
attributes on the instance's root element. The props would be available via the this.$props
property.
<div data-component="Component" data-prop-openLabel="Open">
</div>
class Component extends Base {
static config = {
name: 'Component',
props: {
openLabel: String,
closeLabel: [String, 'Close'], // Default value for the prop if it is not set
isOpen: Boolean
},
};
mounted() {
console.log(this.$props.openLabel); // "Open"
console.log(this.$props.closeLabel); // "Close"
console.log(this.$props.isOpen); // false
}
}
The following types would be available:
String
with ""
as a default valueBoolean
with false
as a default valueArray
with []
as a default valueObject
with {}
as a default valueThe Array
and Object
types will be read with JSON.parse(value)
and written with JSON.stringify(value)
.
Refs can be registered today by using a data-ref="[name]"
attribute. The required refs for a component are not configurable and this can lead to errors at runtime. Defining the required refs for a component would allow for a better developer experience and for a more robust refs management. This would help with the refs resolutions as the component would know the names to look for.
A refs
property should be added to the static config
property:
class Component extends Base {
static config = {
name: 'Component',
refs: ['btn', 'input'],
};
}
Element with a data-ref
attribute with a value not listed in the configuration would still be attached to the instance, but a warning would suggest to either add the ref to the configuration or rename it to match the names in the configuration.
Allow the configuration of event handling methods with new suffixes:
Passive
to add the { passive: true }
configuration to the addEventListener
methodOnce
to add the { once: true }
configuration to the addEventListener
methodCapture
to add the { capture: true }
configuration to the addEventListener
methodExample
class Component extends Base {
static config = {
name: 'Component',
refs: ['btn'],
};
// Only trigger this method once
onBtnClickOnce() {}
// Combine the 3 suffixes in any order
onBtnClickOncePassiveCapture() {}
onBtnClickPassiveOnceCapture() {}
}
The bindChildrenEvents
function in packages/js-toolkit/abstracts/Base/events.js
does not handle children as promised and fail when trying to do $child.$on(...)
.
It could be useful to be able to import or mount a component based on a media query (prefers-reduced-motion
or prefers-reduced-data
for example).
Import helper example usage:
import { Base, importOnMediaQuery } from "@studiometa/js-toolkit";
class App extends Base {
static config = {
name: "App",
components: {
AnimationComponent: () =>
importOnMediaQuery(
() => import("./AnimationComponent.js"),
"not (prefers-reduced-motion)"
),
HeavyComponent: () =>
importOnMediaQuery(
() => import("./HeavyComponent.js"),
"not (prefers-reduced-data)"
),
},
};
}
Decorator example usage:
import { Base, withMountOnMediaQuery } from "@studiometa/js-toolkit";
export default class AnimationComponent extends withMountOnMediaQuery(
Base,
"not (prefers-reduced-motion)"
) {
static config = {
name: "AnimationComponent",
};
}
We could add some predefined import helpers and decorators for common media queries:
importWhenPrefersMotion
importWhenPrefersData
withMountWhenPrefersMotion
All available media features can be found on MDN.
Any idea on the topic @studiometa/js?
Or maybe a withKeepUnmounted()
decorator to be able to use components and mount them programatically when needed.
Hi there, lately I found your toolkit, which matches my current stack in many ways and therefore, would be a good fit. Playing around with it a little, I am wondering if I get things right. Currently I often define variables or props within my constructor. Your framework provides the config.options
object for this purpose, but does not accept instances from third party libraries, e.g. gsap. So given the example, that I'd need access to a greensock timeline within all functions of a component to revert or modify it, I thought I could do it this way:
import Base from '@studiometa/js-toolkit';
import { gsap } from 'gsap';
export default class Button extends Base {
static config = {
name: 'Button',
refs: ['inner1', 'inner2', 'inner3', 'inner4', 'inner5'],
log: true,
debug: true
};
constructor(element: HTMLElement) {
super(element);
this.$el = element;
this.$tl = gsap.timeline({paused: true})
}
mounted() {
console.log('Now I am able to use' + this.$tl + 'everywhere in this component');
}
}
Although this seems to work, it feels a little bit verboose, because I end up using config.options and the constructor, which literally are doing similar things. So I am wondering, if there is a better way to handle such use cases or if I missed something?
Thanks in advance and keep up the great work.
I have a problem of badly associated data-ref
.
<html data-component="App">
<head>
</head>
<body>
<div data-ref="firstSection">
β¦
</div>
</body>
</html>
// pages/home.js
import Base from '@studiometa/js-toolkit';
class PageHome extends Base {
static config = {
name: 'PageHome',
refs: ['firstSection'],
};
}
const pageHome = new PageHome(document.body);
pageHome.$mount();
export default pageHome;
In this example, once the page has finished initializing, the reference will be assigned to the App
component, and so it will create a warning in the console saying [App] The "firstSection" ref is not defined in the class configuration. Did you forgot to define it?
.
Here is what I have to do to fix the problem:
// pages/home.js
import Base from '@studiometa/js-toolkit';
class PageHome extends Base {
static config = {
name: 'PageHome',
refs: ['firstSection'],
};
mounted() {
this.$el.setAttribute('data-component', this.$options.name); // Add attribute to html element
}
}
const pageHome = new PageHome(document.body);
pageHome.$mount();
export default pageHome;
Would it be possible to do it natively?
/**
* Cache Class
*/
export default class Cache {
/**
* Class constructor.
* @param {String} options.name The name of the cache.
* @param {Number} options.ttl The time to live of any cache item.
* @return {Cache} A new Cache instance.
*/
constructor({ name, ttl = 0 }) {
this.name = name;
this.store = {};
this.expire = ttl === 0 ? false : ttl * 1000 * 60;
}
/**
* Get an item by key.
* @param {String} key The key of the item.
* @return {*} The saved item or undefined if its TTL has expired.
*/
get(key) {
const now = Date.now();
const value = this.store[key];
if (value === undefined || (value.expire !== false && value.expire < now)) {
return undefined;
}
return value.data;
}
/**
* Set an item.
* @param {String} key The key of the item.
* @param {*} value The value of the item.
*/
set(key, value) {
const now = Date.now();
this.store[key] = {
expire: this.expire === false ? false : now + this.expire,
data: value,
};
}
/**
* Remove an item by its key.
* @param {String} key The key of the item.
*/
remove(key) {
delete this.store[key];
}
/**
* Clear the cache.
*/
clear() {
this.store = {};
}
}
When extending an existing component and overriding one of its event methods (i.e. onBtnClick
), the overridden method will be attached to the event as many times as there are similar methods in the prototype chain.
Given the following classes:
class Component extends Base {
static config = {
name: 'Component',
refs: ['btn'],
}
onBtnClick() {
console.log('original');
}
}
class FirstOverride extends Component {
onBtnClick() {
console.log('first override');
}
}
class SecondOverride extends Component {
onBtnClick() {
console.log('second override');
}
}
We would have the following behavior:
const [override] = FirstOverride.$factory('FirstOverride');
override.$refs.btn.click();
// first override
// first override
const [secondOverride] = SecondOverride.$factory('SecondOverride');
secondOverride.$refs.btn.click();
// second override
// second override
// second override
See https://codepen.io/titouanmathis/pen/RwpZYyw?editors=1011
The expected behavior would be for the latest method in the prototype chain to be attached once to its corresponding event. Following the example above, we should have the following:
const [override] = FirstOverride.$factory('FirstOverride');
override.$refs.btn.click();
// first override
const [secondOverride] = SecondOverride.$factory('SecondOverride');
secondOverride.$refs.btn.click();
// second override
The elements
parameter can be [undefined]
, throwing an error when trying to remove an event listener.
When refs are defined in the static config
object but not found in the DOM, the RefsManager
should warn the user that some refs are missing.
It could also skip adding the missing refs to avoid error when binding/unbinding events.
We should be able to import components on demand to avoid loading all components on load. Potential use cases are:
This should help to reduce the initial bundle size and improve Web Core Vitals as less JavaScript would be parsed on load.
While looking up some things in the docs I recognized that the Services section with all sublinks leads to a 404 (e.g. https://js-toolkit.meta.fr/services/key/)
The Accordion component does not fully reset when destroyed. The styles added by the component are not removed.
There is a style="height: 0px;"
on the data-ref="content"
which can be breaking a layout.
Init a responsive Accordion component using withBreakpointDecorator
Remove the styles when the component is destroyed
Hey there, the intersection decorator used within the lazy demo component does not seem to work properly on safari. I get the following error with the example of your docs
TypeError: window.requestIdleCallback is not a function. (In 'window.requestIdleCallback(this.load)', 'window.requestIdleCallback' is undefined)
This also happens on your demo page with the lazyload image component here. The IDE complains about TS2339: Property 'requestIdleCallback' does not exist on type 'Window & typeof globalThis'.
Maybe this helps.
All the best
/**
* Clamp a value between two other values.
*
* @param int value The value.
* @param int min The min value.
* @param int max The max value.
*/
export default function clamp(value, min, max) {
return min < max
? value < min
? min
: value > max
? max
: value
: value < max
? max
: value > min
? min
: value;
}
Creating a lot of IntersectionObserver
instance can have a negative impact on performances (see https://www.bennadel.com/blog/3954-intersectionobserver-api-performance-many-vs-shared-in-angular-11-0-5.htm). We could create a new service called intersect
which would create a minimum amount of IntersectionObserver
instances.
import { useIntersect } from '@studiometa/js-toolkit';
const intersect = useIntersect();
intersect.add('a', (entries) => {}, {
target: document.body,
rootMargin: '50%',
})
// Will use the same `IntersectionObserver` instance as the previous one as they have the same `rootMargin` option
intersect.add('b', (entries) => {}, {
target: document.querySelectorAll('div'),
rootMargin: '50%',
});
Its type could be:
interface IntersectServiceOptions extends IntersectionObserverInit {
target: HTMLElement;
}
interface IntersectService<T extends HTMLElement> {
add: (key:string, callback: (IntersectionObserverEntry[]) => void, options: IntersectServiceOptions<T>) => void;
remove: (key:string) => void;
has: (key:string) => void;
props: () => { target: T, entries: IntersectionObserverEntry[] };
}
The types improvements in v2.4.2 are not stable yet and does not work well with extending components and decorators. The example below shows how types could be improved to fix this.
type BaseInterface = {
$el?: HTMLElement;
$options?: Record<string, any>;
$refs?: Record<string, HTMLElement|HTMLElement[]>;
$children?: Record<string, Base | Promise<Base>>;
}
class Base<Interface extends BaseInterface = {}> {
$el: Interface['$el'] & HTMLElement;
$options: Interface['$options'] & Record<string, any>;
$refs: Interface['$refs'] & Record<string, HTMLElement|HTMLElement[]>;
$children: { [key in keyof Interface['$children']]: Interface['$children'][key][] } & Record<string, Base[] | Promise<Base>[]>;
}
type FooInterface = {
$options: {
foo: boolean;
}
$refs: {
trigger: HTMLButtonElement;
}
$children: {
Foo: Foo
}
}
class Foo<Interface = {}> extends Base<FooInterface & Interface> {
constructor() {
super();
this.$el;
this.$options.foo;
this.$refs.trigger;
}
}
type BarInterface = {
$el: HTMLInputElement;
$options: {
bar: boolean;
};
$refs: {
btn: HTMLButtonElement;
items: HTMLElement[];
};
$children: {
Bar: Bar
}
}
class Bar<Interface = {}> extends Foo<BarInterface & Interface> {
constructor() {
super();
this.$el.valueAsNumber;
this.$options.foo;
this.$options.bar;
this.$refs.btn;
this.$refs.items;
this.$refs.trigger;
this.$children.Bar;
this.$children.Foo[0].$options.foo;
this.$children.Foo[0].$refs.trigger;
}
}
function withDecorator<DecoratorInterface = {}>(BaseClass:typeof Base) {
type WithDecoratorInterface = {
$options: {
withDecorator: true;
}
}
class WithDecorator<Interface = {}> extends BaseClass<WithDecoratorInterface & Interface & DecoratorInterface> {
constructor() {
super();
this.$options.withDecorator;
}
}
return WithDecorator;
}
type BazInterface = {
$refs: {
input: HTMLInputElement;
}
$options: {
color: 'red' | 'blue';
}
}
class Baz<Interface = {}> extends withDecorator<BazInterface>(Base) {
constructor() {
super();
this.$refs.input;
this.$options.color;
this.$options.withDecorator;
}
}
Hi!
I regularly use the JS toolkit and I noticed a few things.
When you put in js-toolkit
components, children have access to the "$parent" property. And this component is not its parent linked in the DOM, but the js-toolkit
parent to the one it is linked to.
Example:
Here I have 2 components "Hero" and "HeroSlider". They can work independently because in my forehead I can create simple Hero and also Hero sliders. If I want it to communicate together I have to go through the "refs".
index.html
<html>
<body>
<div data-component="HeroSlider">
<div data-component="Hero" data-ref="heros[]">
β¦
</div>
<div data-component="Hero" data-ref="heros[]">
β¦
</div>
</div>
</body>
</html>
index2.html
<html>
<body>
<div data-component="Hero">
β¦
</div>
</body>
</html>
app.js
import Base from '@studiometa/js-toolkit';
import Hero from './components/Hero';
import HeroSlider from './components/HeroSlider';
class App extends Base {
get config() {
return {
name: 'App',
components: {
Hero,
HeroSlider,
},
};
}
}
export default = new App(document.documentElement);
components/Hero.js
import Base from '@studiometa/js-toolkit';
export default class Hero extends Base {
get config() {
return {
name: 'Hero',
};
}
mounted() {
console.log(this.$parent); // is "App" component
console.log(this.myCustomParent); // is "HeroSlider" component dom parent, but asyncβ¦
}
}
components/HeroSlider.js
import Base from '@studiometa/js-toolkit';
export default class HeroSlider extends Base {
get config() {
return {
name: 'HeroSlider',
};
}
mounted() {
console.log(this.$parent); // is "App" component
this.$refs.forEach(($hero) => {
$hero.myCustomParent = this; // Adds a custom property to the children
});
}
}
In itself it makes sense, but I think it would be interesting if children could also access their parent from the DOM ($parentDom
?).
Hey there,
the docs says that we should define the refs of the components by specifying their name in the configuration. I realized that those refs seem to exist even without specifying them in the config, as long as they have been set in the html markup.
Example:
<div data-component="Example">
<div data-ref="foo">Foo</div>
</div>
import Base from "@studiometa/js-toolkit";
export default class Example extends Base {
static config = {
name: 'Example'
};
mounted() {
console.log(this.$refs.foo); // works -> <div data-ref="foo">Foo</div>
}
}
I am wondering if this is intended? I interpreted the docs to mean that one should explicitly define those refs, which should be created within the config object. If the current behavior is intended, could you please provide an example, for which use case it is then necessary to define the refs within the config object at all?
Please don't get me wrong, I do not want to show a critical attitude, just want to ask to understand. π
We currently have access to child components with the this.$children
property and to the parent component with the this.$parent
one, and parent component can listen to their children's events, but their is not a clearly defined way to pass down information from the parent to its children.
Vue uses props and events for this. React only uses props. How could we solve this problem?
The following can be done with the current version of the Base
class:
class Child extends Base {
childMethod() {
this.$parent.parentMethod();
}
}
class Parent extends Base {
get config() {
return { components: { Child } };
}
mounted() {
this.$children.Child[0].foo();
}
parentMethod() {}
}
$children.$dispatch('childName', 'methodName', (child, index) => boolean)
method which would try to trigger the given method on all children or one given childclass Parent extends Base {
mounted() {
// trigger the `childMethod` method on all `Child` instances
this.$children.$dispatch('Child', 'childMethod');
// trigger the `childMethod` method on the `Child` instances filtered by the callback function
this.$children.$dispatch('Child', 'childMethod', (child, index) => index % 2);
}
}
To be completed...
Hey there, I am using tailwind with jit and therefore I copied over your breakpoints plugin and required it in my tailwindconfig. The BreakpointManager works well on all browser except on Safari. When replicating your BreakpointManagerDemo, I get following error, even tough I defined my breakpoints and also added the data-breakpoint
attribute to the DOM.
Error: The `BreakpointManager` class requires breakpoints to be defined.
Strangely, when I am using a dynamic import for the component, the BreakpointManager also works on Safari:
import Base from '@studiometa/js-toolkit';
import { $body } from './utils';
class App extends Base {
static config = {
name: 'App',
components: {
// This works, as long as I am using a dynamic import
BreakpointManagerDemo: () => import( /* webpackChunkName: "BreakpointManagerDemo" */ './components/BreakpointManagerDemo')
},
};
}
const app = new App($body);
app.$mount();
export default app;
// Accept HMR as per: https://webpack.js.org/api/hot-module-replacement#accept
if (module.hot) {
module.hot.accept();
}
So it seems that something is messing up the initialization. I know that this might not be a real bug, but rather could rely on my webpack config. But I would be very thankful if someone have an idea or hint, how to solve the issue.
Hey there,
I just recognized that debug: true
stops working as expected after updating to version 1.1.0
, although process.NODE_ENV
is set to development
on my end. The console doesn't output anything. Reverting back to 1.0.4
fixes this. Anything else seems to work on version 1.1.0
. If I can assist anything further in debugging please let me know.
Cheers & have a nice weekend!
When I set up an accordion with a transition, I have a warning
in my console :
[AccordionItem] The "icon" ref is not defined in the class configuration. Did you forgot to define it?
When using the scrollTo
utility with scroll-behavior: smooth
applied on the scrolling element, the 2 animations are conflicting, resulting in laggy scroll in Chrome and Safari.
See https://codepen.io/titouanmathis/pen/ExEmZgb.
Maybe scrollTo
should try to animate the scroll when scroll-behavior
is set to smooth
?
Currently, AccordionItem component refers to btn
, container
and content
, but icon
ref is missing. A warning message is displayed in the console about this:
[AccordionItem] The "icon" ref is not defined in the class configuration. Did you forgot to define it?
Raf service performance might be improved with the help of framesync.
Needs investigation.
Add a utility function to get a random number between a given minimum and maximum value.
/**
* Get a random number between bounds
*
* @param {number} min minimum value
* @param {number} max maximum value
* @returns {number}
*/
getRandomNumber(min = 0, max = 3) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
Suggestion:
const normalizedClassNames = isArray(classNames) ? classNames : classNames.split(' ').filter((className)=> className && className !== '');
The static vueConfig
property prevent us from using the current class instance in the Vue app. Allowing to define the Vue configuration in a vueConfig
getter would fix that:
import { Base, withVue2 } from '@studiometa/js-toolkit';
import App from './App.vue';
export default class Foo extends withVue2(Base) {
get vueConfig() {
return {
render: (h) => h(App, { props: { instance: this } }),
};
}
}
ReferenceError: Can't find variable: ResizeObserver
https://caniuse.com/#feat=resizeobserver
This bug was introduce in alpha.11 version which is using new ResizeObserver()
to handle resize in src/services/resize.js by using.
Should add a polyfill to support at least the last 2 versions of each main browser ?
Currently, breakpoints values are read from the pseudo-elements ::before
and ::after
of a [data-breakpoint]
element in the DOM. This works well, but is a little bit too magical for an easy use.
In addition, the current breakpoint
and breakpoints
properties of the resize service props does not let us know if a given breakpoint is still active, forcing us to always test multiple breakpoints:
if (['l', 'xl', 'xxl'].includes(this.$services('resized').breakpoint)) {
// ...
}
We could change this without a breaking change with 2 small additions:
createApp
functionactiveBreakpoints: Record<keyof breakpoints, boolean>
object to the resize service propsExample
import { screens } from '@studiometa/tailwind-config';
import { Base, createApp } from '@studiometa/js-toolkit';
class App extends Base {
static config = {
name: 'App',
};
}
export default createApp(
App,
{ screens },
);
With the screens
export being:
export const screens = {
xxs: '0px',
xs: '480px',
s: '768px',
m: '1024px',
l: '1280px',
xl: '1440px',
xxl: '1920px',
};
The activeBreakpoints
property would be:
const { activeBreakpoints } = useResize().props();
console.log(activeBreakpoints);
{
xxs: true,
xs: true,
s: true,
m: false,
l: false,
xl: false,
xxl: false,
}
document.scrollingElement
element.scrollTop
and element.scrollLeft
to set scroll positionHi there,
I am testing your toolkit but having a hard time fighting suspicious unresolved ref variables. Maybe you could point me in the right direction?
My component test file looks like this:
import { Base } from '@studiometa/js-toolkit';
export default class Foo extends Base {
static config = {
name: 'Foo',
refs: ['bar']
};
mounted() {
this.$refs.bar;
console.log('Child mounted');
}
}
My main entry point looks like this:
import { Base, createApp } from '@studiometa/js-toolkit';
import Foo from './Foo.js';
class App extends Base {
static config = {
name: 'App',
components: {
Foo,
}
};
}
export default createApp(App, document.body);
The component does indeed mount correctly and the refs are also accessible, but phpstorm keeps complaining about the bar
ref, which is allegedly undefined.
I also tried to fight the issue by using jsdoc
statements and included @studiometa/eslint-config
& @studiometa/prettier-config
with no success. Do you have an idea what's wrong here on my end? Using @studiometa/js-toolkit@^2.4.1
This issue will help us track changes which would be breaking that we would like to have for a next major version.
Things to delete
$parent
property: child to parent communication should be done by emitting events, accessing a known ancestor component can be achieved with getClosestParent(child, ParentClass)
withVue2
decoratormounted
, resized
, scrolled
, ticked
and destroyed
method from the withScrolledInView
decoratorThings to change
// v2
onChildClick(arg1, arg2, index, event) {
const currentChild = this.$children.Child[index];
}
onRefClick(event, index) {
const currentRef = this.$refs.ref[index];
}
// v3
onChildClick(event, arg1, arg2) {
const currentChild = event.target;
}
onRefClick(event) {
const currentRef = event.currentTarget;
}
// v3 bis
onChildClick(event, index, arg1, arg2) {
const currentChild = this.$children.Child[index];
}
onRefClick(event, index) {
const currentRef = this.$refs.Ref[index];
}
// v3 ter (keeping index as it can be useful)
onChildClick(event, { child, index }, arg1, arg2) {
const currentChild = child;
}
onRefClick(event, { ref, index }) {
const currentRef = ref;
}
When a component is imported asynchronously, the parent component seems to not have access to all of the child component instance's properties and methods, and their $isMounted
property is not updated correctly.
For now, we need to specify which options are responsive manually, this can become complex when using the decorator on an existing component. Adding support for defining options which should support responsive configuration would help avoid complex extension.
Before
import { withResponsiveOptions } from '@studiometa/js-toolkit';
import { Menu as MenuCore } from '@studiometa/ui';
export default class Menu extends withResponsiveOptions(MenuCore) {
static config = {
...MenuCore.config,
components: {
...MenuCore.config.components,
Menu,
},
options: {
...MenuCore.config.options,
mode: {
...MenuCore.config.options.mode,
responsive: true,
},
},
};
}
After
import { withResponsiveOptions } from '@studiometa/js-toolkit';
import { Menu as MenuCore } from '@studiometa/ui';
export default class Menu extends withResponsiveOptions(MenuCore, { responsiveOptions: ['mode'] }) {
static config = {
...MenuCore.config,
components: {
...MenuCore.config.components,
Menu,
},
};
}
Usage shows us that we often bind multiple event listeners to multiple refs in the mounted
hook, and then unbind them in the destroyed
hook.
The following proposals aim at reducing this repetitive task to the minimum by providing a logical mapping between refs, events and methods. It would also prevent unwanted memomy leak by systematically removing listeners when a component is destroyed.
Naming methods based on the ref and event they should be bound:
clickHandler
β this.$el.addEventListener('click', this.clickHandler)
contentClickHandler
β this.$refs.content[<index>].addEventListener('click', this.contentClickHandler)
The following naming convention should be considered:
<refName><EventName>Handler(event[, index])
(the current untold convention)on<RefName><EventName>(event[, index])
(the shortest)handle<RefName><EventName>(event[, index])
(the closest to the event API)class Foo extends Base {
get config() {
return { name: 'Foo' };
}
/**
* Automatically bound/unbound to the `click` event on the `this.$el` element.
* @param {Event} event The event object.
*/
clickHandler(event) {}
// or
onClick(event) {}
// or
handleClick(event) {}
/**
* Automatically bound/unbound to the `click` event on every `this.$refs.content` element.
* @param {Event} event The event object.
* @param {Number} index The current ref index.
*/
contentClickHandler(event, index) {}
// or
onContentClick(event, index) {}
// or
handleContentClick(event, index) {}
}
Adding the same listener to multiple events could become verbose:
class Foo extends Base {
start() {
console.log('I am starting!');
}
onMousemove(event) {
this.start();
}
onTouchmove(event) {
this.start();
}
}
// Smaller version
class Foo extends Base {
onMousemove = this.start;
onTouchmove = this.start;
this.start(event) {
console.log('I am starting!');
}
}
To add multiple listeners to one ref we would have to dispatch them from the method following the naming convention:
class Foo extends Base {
clickHandler(event) {
if (this.isOpen) {
this.close()
}
if (this.isHovered) {
this.highlight()
}
// ...
}
}
Adding listeners to some ref after another action without the same simplicity could be deceiving:
class Foo extends Base {
start() {
console.log('Starting...');
}
stop() {
console.log('Stopping.');
}
mouseenterHandler() {
this.start();
this.$el.addEventListener('mousedown', this.stop, { once: true });
}
}
Clamp functions:
/**
* Clamp a value in a given range.
* @param {number} value
* @param {number} min
* @param {number} max
* @return {number}
*/
function clamp(value, min, max) {
/* eslint-disable no-nested-ternary */
return min < max
? value < min
? min
: value > max
? max
: value
: value < max
? max
: value > min
? min
: value;
/* eslint-enable no-nested-ternary */
}
/**
* Clamp a value in the 0β1 range.
* @param {number} value
* @return {number}
*/
function clamp01(value) {
return clamp(value, 0, 1);
}
Clamp functions tests:
import { clamp, clamp01 } from '~/utils/math';
describe('The math utils', () => {
it('should clamp a value between the given range', () => {
expect(clamp(0, 0, 10)).toBe(0);
expect(clamp(-5, 0, 10)).toBe(0);
expect(clamp(15, 0, 10)).toBe(10);
expect(clamp(5, 0, 10)).toBe(5);
expect(clamp(5, 10, 0)).toBe(5);
expect(clamp(-5, 10, 0)).toBe(0);
expect(clamp(15, 10, 0)).toBe(10);
});
it('should clamp a value between 0 and 1', () => {
expect(clamp01(0)).toBe(0);
expect(clamp01(0.5)).toBe(0.5);
expect(clamp01(1)).toBe(1);
expect(clamp01(-1)).toBe(0);
expect(clamp01(2)).toBe(1);
});
});
This would reduce the code needed to implement lazy access properties (more details here).
import { lazyGetter } from '@studiometa/js-toolkit/utils';
class Foo {
get foo() {
return lazyGetter(this, 'foo', 'value');
}
}
From the answer DocSearch sent us on our first application:
We had a look at the link you provided. Your website looks to be in a building stage. We spotted some βlorem ipsumβ content OR blank page OR WIP markup. This means that we won't be able to offer you DocSearch yet.
Some ideas to improve the doc:
Taking the following template and JS component. It is not possible to use this.$options
as a vue2 props
this.$options
being undefined
<div data-component="Component" data-option-property="{{ property }}">
<div data-ref="vue"></div>
</div>
import { withVue2, Base } from '@studiometa/js-toolkit';
import Vue from 'vue';
import App from './App.vue';
export default class Component extends withVue2(Base, Vue) {
static config = {
options: {
property: {
type: String,
default: '',
},
},
};
static vueConfig = {
render: (h) =>
h(App, {
props: {
property: this.$options.property,
},
}),
};
}
This limit the decorator value by a lot as this is a pretty common pattern
Hey there,
love to see the current work flowing into this. Is there any typescript support already built in? I recognized the jsdoc type declarations and the node_modules folder also shows .d.ts files after installing the package. However I got linting issues as soon as I try to work with the refs object, e.g. this.$refs.toggler.addEventListener('click', this.toggle);
leads to <html>TS2339: Property 'addEventListener' does not exist on type 'typeof Base | HTMLElement | (typeof Base | HTMLElement)[]'.<br/>Property 'addEventListener' does not exist on type 'typeof Base'.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
π Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. πππ
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google β€οΈ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.