@ayatkevich/query
TypeScript icon, indicating that this package has built-in type declarations

0.2.0 • Public • Published

@ayatkevich/query

A lightweight, chainable, lazy DOM manipulation library inspired by jQuery, written in TypeScript. It provides a simple and expressive API for selecting and manipulating DOM elements, with the added benefit of lazy evaluation.

Table of Contents

Installation

npm install @ayatkevich/query

Usage

Selecting Elements

Import the $ class and create a new instance by passing a CSS selector:

import { $ } from '@ayatkevich/query';

new $('div');

Specifying a Root Element

You can optionally specify a root element for your query. By default, the root is document, but you can pass any Element as the second argument to scope your selector:

const rootElement = document.getElementById('root');
new $('span', rootElement);

This will select all <span> elements within rootElement, ignoring any <span> elements outside of it.

Example:

import { $ } from '@ayatkevich/query';

// HTML structure:
// <div id="root">
//   <span class="inside">Inside Root</span>
// </div>
// <span class="outside">Outside Root</span>

const rootElement = document.getElementById('root');
const elements = new $('span', rootElement).unwrap();

console.log(elements.length); // Output: 1
console.log(elements[0].textContent); // Output: "Inside Root"

Chaining Methods

The library supports method chaining for a fluent API:

new $('div').addClass('active').setAttribute('role', 'button');

Lazy Evaluation

@ayatkevich/query is designed with laziness in mind. Mutations are not immediately applied to the selected elements. Instead, they are deferred and only executed when necessary. This approach can lead to performance improvements by reducing unnecessary DOM manipulations.

Example of Laziness

Consider the following test cases:

import { describe, expect, it } from '@jest/globals';
import { parseHTML } from 'linkedom';
import { $ } from '@ayatkevich/query';

describe('query', () => {
  it('should allow chaining and demonstrate laziness', () => {
    globalThis.document = parseHTML(/* HTML */ `
      <div>Hello</div>
      <div>World</div>
    `).document;

    const chain = new $('div').addClass('foo').addClass('bar');

    // At this point, mutations have not been applied yet due to laziness.

    const [div] = chain; // Accessing the first element applies mutations to it.
    expect(div.classList.contains('foo')).toBe(true);
    expect(div.classList.contains('bar')).toBe(true);

    // The second div has not had mutations applied yet.
    const divs = Array.from(new $('div'));
    expect(divs[1].classList.contains('foo')).toBe(false);
    expect(divs[1].classList.contains('bar')).toBe(false);
  });

  it('should apply mutations to all elements when unwrap() is called', () => {
    globalThis.document = parseHTML(/* HTML */ `
      <div>Hello</div>
      <div>World</div>
    `).document;

    new $('div').addClass('foo').unwrap();

    expect(
      Array.from(document.querySelectorAll('div')).map((element) =>
        Array.from(element.classList)
      )
    ).toEqual([['foo'], ['foo']]);
  });
});

In the first test case, mutations are applied lazily. The classes 'foo' and 'bar' are only added to the first <div> when it's accessed. The second <div> remains unchanged until it's accessed or until unwrap() is called.

In the second test case, unwrap() forces all pending mutations to be applied to every selected element. This is useful when you want to ensure that all mutations take effect immediately.

Async Unwrapping with await

@ayatkevich/query supports async unwrapping using the await syntax. This allows you to apply all pending mutations to all selected elements asynchronously.

Example of Async Unwrapping

import { describe, expect, it } from '@jest/globals';
import { parseHTML } from 'linkedom';
import { $ } from '@ayatkevich/query';

describe('query', () => {
  it('should allow async unwrapping with await syntax', async () => {
    globalThis.document = parseHTML(/* HTML */ `
      <div>Hello</div>
      <div>World</div>
    `).document;

    const query = new $('div').addClass('async-test');

    // At this point, mutations have not been applied due to laziness
    const divs = Array.from(document.querySelectorAll('div'));
    expect(divs[0].classList.contains('async-test')).toBe(false);
    expect(divs[1].classList.contains('async-test')).toBe(false);

    // Use await to unwrap the query
    await query;

    // After awaiting, mutations should be applied to all elements
    const updatedDivs = Array.from(document.querySelectorAll('div'));
    expect(updatedDivs[0].classList.contains('async-test')).toBe(true);
    expect(updatedDivs[1].classList.contains('async-test')).toBe(true);
  });
});

By awaiting the query, you force all pending mutations to be applied to all selected elements. This can be especially useful in asynchronous contexts or when you want to ensure that all mutations are applied before proceeding.

Class Manipulation

  • addClass(className: string): Lazily adds a class to the selected elements.

    new $('div').addClass('highlight');
  • removeClass(className: string): Lazily removes a class from the selected elements.

    new $('div').removeClass('highlight');
  • toggleClass(className: string): Lazily toggles a class on the selected elements.

    new $('div').toggleClass('active');
  • replaceClass(oldClassName: string, newClassName: string): Lazily replaces an existing class with a new one.

    new $('div').replaceClass('old-class', 'new-class');

Attribute Manipulation

  • setAttribute(name: string, value: string): Lazily sets an attribute on the selected elements.

    new $('input').setAttribute('placeholder', 'Enter your name');
  • removeAttribute(name: string): Lazily removes an attribute from the selected elements.

    new $('input').removeAttribute('disabled');
  • toggleAttribute(name: string): Lazily toggles an attribute on the selected elements.

    new $('input').toggleAttribute('readonly');

Data Attributes

  • setData(data: Record<string, string>): Lazily sets data attributes on the selected elements.

    new $('div').setData({ userId: '123', role: 'admin' });
  • removeData(key: string): Lazily removes a data attribute from the selected elements.

    new $('div').removeData('userId');

DOM Tree Manipulation

  • append(node: Node): Lazily appends a node to the selected elements.

    import { html } from '@ayatkevich/query';
    
    new $('div').append(html`<span>Hello World</span>`);
  • prepend(node: Node): Lazily prepends a node to the selected elements.

    new $('div').prepend(html`<span>Start</span>`);
  • remove(): Lazily removes the selected elements from the DOM.

    new $('.obsolete').remove();
  • before(node: Element): Lazily inserts a node before the selected elements.

    new $('div').before(html`<hr />`);
  • after(node: Element): Lazily inserts a node after the selected elements.

    new $('div').after(html`<hr />`);

Event Handling

  • on(event: string, handler: (event: Event) => void): Lazily attaches an event handler to the selected elements.

    new $('button').on('click', (event) => {
      console.log('Button clicked!');
    });
  • once(event: string, handler: (event: Event) => void): Lazily attaches a one-time event handler to the selected elements.

    new $('button').once('click', (event) => {
      console.log('Button clicked once!');
    });

Testing

The library includes a comprehensive test suite using Jest. To run the tests:

npm test

Example Test Case Using Root Element:

import { describe, expect, it } from '@jest/globals';
import { parseHTML } from 'linkedom';
import { $ } from '@ayatkevich/query';

describe('query', () => {
  it('should allow querying within a specific root element', () => {
    globalThis.document = parseHTML(/* HTML */ `
      <div id="root">
        <span class="inside">Inside Root</span>
      </div>
      <span class="outside">Outside Root</span>
    `).document;

    const rootElement = document.getElementById('root')!;
    const query = new $('span', rootElement);

    const elements = query.unwrap();

    expect(elements.length).toBe(1);
    expect(elements[0].textContent).toBe('Inside Root');
  });
});

This test case demonstrates how to use the root argument to scope your queries to a specific element.

License

This project is licensed under the MIT License.

Readme

Keywords

none

Package Sidebar

Install

npm i @ayatkevich/query

Weekly Downloads

0

Version

0.2.0

License

MIT

Unpacked Size

22.6 kB

Total Files

6

Last publish

Collaborators

  • ayatkevich