Code Monkey home page Code Monkey logo

js-toolkit's People

Contributors

antoine4livre avatar dependabot[bot] avatar depfu[bot] avatar jeremschelb avatar jverneaut avatar notjb avatar perruche avatar renovate-bot avatar titouanmathis avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

js-toolkit's Issues

Re-organize the project folder structure

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.

Fix the matching of event methods

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.

See https://github.com/studiometa/js-toolkit/blob/develop/packages/js-toolkit/abstracts/Base/events.js#L52.

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.).

Trigger the updated() hook on child componants when using $update()

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);
});

Add common Vue utilities

When working with Vue, we often need to re-use some utilities from this toolkit.

  • Provide a $mq plugin to be able to use the breakpoint and breakpoints values from the resize service with Vue reactivity

Add a decorator to mount Vue application inside a component

This 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:

  • mount the vue app on the given root element when the component is mounted
  • detroy the vue app when the component is destroyed
  • update the vue app when the component is updated

Add support for native events on child components

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.

Improve the config property and events binding

Config property

Transform the config getter into a static property

It 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',
  };
}

Introduce the usage of props

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 value
  • Boolean with false as a default value
  • Array with [] as a default value
  • Object with {} as a default value

The Array and Object types will be read with JSON.parse(value) and written with JSON.stringify(value).

Introduce the definition of refs

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.

Advanced event management

Allow the configuration of event handling methods with new suffixes:

  • Passive to add the { passive: true } configuration to the addEventListener method
  • Once to add the { once: true } configuration to the addEventListener method
  • Capture to add the { capture: true } configuration to the addEventListener method

Example

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() {}
}

Add import helpers and decorators for specific media queries

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
  • etc.

All available media features can be found on MDN.

Any idea on the topic @studiometa/js?

Passing variables/instances between methods

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.

"data-ref" unassigned

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?

Add a cache class utility

/**
 * 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 = {};
  }
}

Event methods of extended components are called twice

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

Filter out undefined ref and add a warning in the console

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.

Accordion component should fully reset when destroyed

Description

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.

How to reproduce

Init a responsive Accordion component using withBreakpointDecorator

How to fix

Remove the styles when the component is destroyed

Intersection decorator used within the lazy demo component breaks on safari

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

Add a clamp function to the math utils

/**
 * 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;
}

Add an `intersect` service

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[] };
}

Improve types

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;
  }
}

Access the parent component of the DOM of a child component

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?).

refs are being created without specified in static.config

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. 😊

Define a way to communicate efficiently between components

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() {}
}

⚠️ We do not want to re-invent the wheel, a complex scenario where data needs to be shared between components with reactivity should be handled with Vue.js.


Ideas

  • A $children.$dispatch('childName', 'methodName', (child, index) => boolean) method which would try to trigger the given method on all children or one given child
class 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...

BreakpointManager breaks on safari with custom webpack build

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.

v. 1.1.0 disables debug mode

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!

Missing "icon" ref in AccordionItem component

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?

Add a utility function to get a random number

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;
  }

[withVue2] Allow configuration via a getter

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 } }),
    };
  }
}

Add support for defining an application's breakpoints without relying on reading them from the DOM

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:

  • Add support for passing screens definition to the createApp function
  • Add an activeBreakpoints: Record<keyof breakpoints, boolean> object to the resize service props

Example

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,
}

Improve `scrollTo` utility

  • Allow giving a custom element to use as the scroll container, with default to document.scrollingElement
  • Use element.scrollTop and element.scrollLeft to set scroll position

Defined refs are unresolved in phpstorm

Hi 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.
CleanShot 2022-08-15 at 04 06 30@2x

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

Plans for v3

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

  • ⚠️ Remove the $parent property: child to parent communication should be done by emitting events, accessing a known ancestor component can be achieved with getClosestParent(child, ParentClass)
  • ⚠️ Remove the withVue2 decorator
  • ⚠️ Remove the mounted, resized, scrolled, ticked and destroyed method from the withScrolledInView decorator
  • ⚠️ Remove ability to listen to undeclared events on a component

Things to change

  • ⚠️ Change ref or child index from event hooks methods to direct reference to the ref or the child and normalize the order of params
// 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;
}
  • ⚠️ Refactor resize props breakpoint management (see #264)

Add support for defining responsive options when using the `withResponsiveOptions` decorator

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,
    },
  };
}

[RFC] Simplifying event listeners binding

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.

Using a strict naming convention for methods

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) {}
}

Potential limitations

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 });
  }
}

Add math utilities

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);
  });
});

Add a helper function for lazy properties

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');
  }
}

Improve the documentation to be elligible for DocSearch

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:

  • Complete the guide
  • Add recipes to the guide
  • Add documentation for all the utilities

Cannot pass $options as props withVue2 decorator

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

Typescript support

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'.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    πŸ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. πŸ“ŠπŸ“ˆπŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❀️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.