npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

requery-js

v0.1.2

Published

A Web Component framework that works with raw HTML - no JSX, string literals, or special templating syntax is required. Built for seamless integration with website builders like Webflow.

Downloads

221

Readme

ReQuery

A Web Component framework that works with raw HTML - no JSX, string literals, or special templating syntax is required. Built for seamless integration with website builders like Webflow.

Why ReQuery?

ReQuery bridges the gap between traditional development and visual development. For traditional developers, it provides a type-safe, component-based framework with familiar Web Component patterns. For visual developers and the No-Code/Low-Code community, it enables seamless integration with visual website builders like Webflow, while maintaining the power and flexibility of a full development framework.

Key features:

  • Web Components: Built on native web standards using Custom Elements
  • Type-Safe: Full TypeScript support with generics for props, store, and actions
  • Visual Tool Friendly: No JSX or complex templating - works with any HTML
  • Reactive: Powered by vue-reactivity for automatic UI updates
  • Lightweight: No virtual DOM, minimal overhead
  • Framework Agnostic: Works with any frontend stack (theoretically)

Special Thanks

ReQuery wouldn't exist without the Vue ecosystem. The reactive core is powered by @vue/reactivity, and the API design was inspired by Petite Vue. Thank you to Evan You and the Vue team for these incredible tools.

Installation

NPM

npm install requery-js

CDN

<script type="module">
  import {
    defineComponent,
    queryComponent,
  } from 'https://esm.run/requery-js@0';

  // Your code here
</script>

Quick Start

ReQuery works by marking interactive elements with the rq attribute in your HTML. These elements are then "queried" in your component's setup function where you can bind reactive data and event handlers. This approach keeps your HTML clean and compatible with visual tools, while maintaining full programmatic control in your JavaScript/TypeScript code.

  1. Mark the interactive & reactive elements with the rq attribute:
<rq-counter prop:initial-count="0" prop:step="1">
  <button rq="button">
    Clicked: <span rq="count">0</span> times
  </button>
</rq-counter>
  1. Define the component:
defineComponent("rq-counter", {
  props: {
    initialCount: 0,
    step: 1
  },
  store: {
    count: 0
  },
  setup(component, props, store, actions) {
    // Initialize from props
    store.count = props.initialCount;

    // Bind UI elements
    component.query("count").text(() => store.count);
    component.query("button").on("click", (el, evt) => {
      store.count += props.step;
    });
  }
});

Components

Components in ReQuery are built on top of the Web Components standard. They provide a clean way to create reusable UI elements with their own properties, state, and behaviors.

Parameters

| Name | Type | Description | |-----------|------|-------------| | name | string | The name of the element. Must contain a hyphen and follow the custom element naming rules. | | props | object | Properties that can be set using prop: attributes in the HTML | | store | object | Internal reactive state for the component | | actions | object | Methods that can be called on the component | | setup | function | Setup function called when component is mounted |

Using TypeScript

ReQuery components are fully typed in TypeScript:

TypeScript

interface Item {
  id: number;
  name: string;
  image: string;
}

interface CollectionProps {
  selected?: number;
}

interface CollectionStore {
  items: Item[];
  loading: boolean;
  error: string | null;
}

interface CollectionActions
  extends ComponentActions<CollectionProps, CollectionStore> {
  load(
    ctx: ActionContext<CollectionProps, CollectionStore>,
    offset: number
  ): void;
}

interface PokemonResponse {
  count: number;
  next: string;
  previous: string;
  results: { name: string; url: string }[];
}

defineComponent<CollectionProps, CollectionStore, CollectionActions>(
  "rq-collection",
  {
    props: {
      selected: undefined,
    },
    store: {
      items: [],
      loading: false,
      error: null,
    },
    actions: {
      async load(ctx, offset = 0) {
        ctx.store.loading = true;
        ctx.store.error = null;
        const perPage = 4;

        try {
          const response = await fetch(
            `https://pokeapi.co/api/v2/pokemon?limit=${perPage}&offset=${offset}`
          );

          if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
          }

          const data = (await response.json()) as PokemonResponse;

          const newItems = data.results.map((item, index) => {
            // Calculate the Pokemons Id as it is not in the response
            const id = index + offset + 1;
            return {
              id,
              name: item.name,
              image: `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${id}.png`,
            } as Item;
          });

          ctx.store.items.push(...newItems);
        } catch (err) {
          if (err instanceof Error) {
            ctx.store.error = err.message;
          } else {
            ctx.store.error = "Failed to load Pokemon";
          }
          console.error("Failed to load Pokemon:", err);
        } finally {
          ctx.store.loading = false;
        }
      },
    },
    setup(component, props, store, actions) {
      // Initial load
      actions.load(0);

      // Render items using the for directive
      component.query("item").for(
        () => store.items, // items array
        (item) => item.id, // key function (optional)
        (el, item) => {
          // Setup item bindings
          el.bind("class.ring-2", () => props.selected === item.id)
            .bind("class.ring-blue-500", () => props.selected === item.id)
            .on("click", (el, evt) => {
              props.selected = item.id;
            });

          // Setup item elements
          el.query("title").text(() => item.name);
          el.query("image").bind("src", () => item.image);
        }
      );

      // Refresh button
      component
        .query("btn-load")
        .bind("disabled", () => store.loading)
        .on("click", (el, evt) => {
          actions.load(store.items.length);
        });

      // Load button loader
      component.query("btn-load-loader").show(() => store.loading);

      // Show/hide error message
      component
        .query("error")
        .show(() => store.error !== null)
        .text(() => store.error!);
    },
  }
);

HTML Markup

<rq-collection prop:per-page="4">
  <!-- Pokemon grid -->
  <div class="grid grid-cols-2 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
    <div
      rq="item"
      class="bg-white p-4 rounded-lg shadow-sm hover:shadow-md transition-shadow cursor-pointer"
    >
      <img rq="image" alt="Pokemon" class="w-32 h-32 mx-auto mb-4" />
      <h3
        rq="title"
        class="text-lg font-medium text-gray-800 capitalize text-center"
      ></h3>
    </div>
  </div>

  <!-- Load more button -->
  <div class="text-center">
    <button
      rq="btn-load"
      class="px-6 py-2 bg-white text-gray-600 rounded-lg shadow-sm hover:shadow-md transition-all disabled:opacity-50 disabled:cursor-not-allowed"
    >
      Load More
      <span rq="btn-load-loader" rq-cloak class="inline-block ml-2"
        >...</span
      >
    </button>
  </div>
</rq-collection>

Component Instance Keys

Components can have unique instance keys for identification:

<rq-collection prop:key="pokedex">
  <!-- This instance can be queried using the key -->
</rq-collection>

Querying Components

If you need to access a component instance outside of the setup function, you can use the queryComponent function.

Parameters

| Name | Type | Description | |------|------|-------------| | name | string | The name of the component | | key | string | The key of the component |

Example

// Query individual instance
const collection = queryComponent("rq-collection");

// Query specific instance by key
const postsCollection = queryComponent("rq-collection", "posts");

// Access component properties
console.log("Props", postsCollection.props);
console.log("Store", postsCollection.store);
console.log("Actions", postsCollection.actions);

// Call component actions
postsCollection.actions.refresh();

// Register event listeners
postsCollection.on("itemSelected", (el, evt) => {
  console.log("itemSelected", evt);
});

Parent-Child Communication

Components can communicate with their parents and children through props, events, and direct component queries.

Props

Props can be passed to components using:

  • the prop: prefix in HTML attributes
  • accessing the props property on the component instance
  • parent component setup function
HTML Attributes

Just like native Web Components, you can pass props to components via HTML attributes. The only difference is that you use the prop: prefix.

<rq-collection
  prop:key="pokedex"
  prop:selected="1"
>
  <!-- Child component with `rq` attribute -->
  <rq-item rq="pokemon"></rq-item>
</rq-collection>

Props are automatically converted to their appropriate types:

  • Boolean values: "true"true
  • Numbers: "123"123
  • Objects/Arrays: Parsed from JSON strings
  • Strings: Kept as-is
Component Instance

You can access the props of a component instance using the props property.

const pokedex = queryComponent("rq-collection", "pokedex");

// Access props
console.log(pokedex.props);
Setup Function

You can also bind child component props in the parent component setup function.

// Child component
defineComponent("rq-item", {
  props: {
    key: null,
    name: null,
    image: null,
  },
  store: {},
  setup(component, props, store, actions) {
    component.query("title").text(() => props.name);
    component.query("image").bind("src", () => props.image);
  },
});

// Parent component
defineComponent<CollectionProps, CollectionStore, CollectionActions>(
  "rq-collection",
  {
    props: {
      selected: undefined,
    },
    store: {
      items: [],
      loading: false,
      error: null,
    },
    actions: {
      async load(ctx, offset = 0) {
        ...
      },
    },
    setup(component, props, store, actions) {
      actions.load(0);

      component.query("item").for(
        () => store.items,
        (item) => item.id,
        (el, item) => {
          el.bind("prop:key", () => item.id);
          el.bind("prop:name", () => item.name);
          el.bind("prop:image", () => item.image);
          el.bind("class.ring-2", () => props.selected === item.id)
            .bind("class.ring-blue-500", () => props.selected === item.id)
            .on("click", (el, evt) => {
              props.selected = item.id;
            });
        }
      );

      ...
    },
  }
);

Events

ReQuery components allow you to easily emit events to parent components.

// Child component
defineComponent("rq-item", {
  props: {
    key: null,
    name: null,
    image: null,
  },
  store: {},
  setup(component, props, store, actions) {
    component.query("title").text(() => props.name);
    component.query("image").bind("src", () => props.image);

    component.query("btn-favorite").on("click", (el, evt) => {
      // Prevent default behavior
      evt.preventDefault();
      evt.stopPropagation();
      
      // Emit favoriteItem event to the parent component
      component.emit("favoriteItem", {
        id: props.key,
        name: props.name,
        image: props.image,
      });
    });
  },
});

// Parent component
defineComponent(
  "rq-collection",
  {
    props: {},
    store: {},
    actions: {},
    setup(component, props, store, actions) {
      ...
      component.on("favoriteItem", (value) => {
        // Format the value for display
        const formattedValue = JSON.stringify(value, null, 2);

        // Display the formatted value in an alert
        alert(
          `Favorite item event received in the rq-collection component:\n\nPokemon:\n${formattedValue}`
        );
      });
      ...
    },
  }
);

Component Lifecycle

Components follow the Web Components lifecycle with some enhancements:

defineComponent("rq-example", {
  setup(component, props, store, actions) {
    // Setup runs when component is mounted
    
    // Cleanup when component is disposed
    return () => {
      // Cleanup code
    };
  }
});

Elements

Elements are reactive references to DOM nodes that can be queried using the query method available on a component instance. They provide a way to interact with the DOM and apply directives.

🔎 query()

The query method is used to find elements marked with an rq attribute:

Parameters

| Name | Type | Description | |------|------|-------------| | name | string | The value of the rq attribute to search for |

Example

HTML
<div rq="warning-message">
  Warning: Count is <span rq="count">0</span>!
</div>
JavaScript
// Query element from a component
const warningMessage = component.query("warning-message");

When using the if and for directives, you should query children using the element not the component to ensure they are cleaned up correctly.

warningMessage.if(
  () => store.showWarning,
  (el) => { 
    // Query child element
    el.query("count").text(() => store.count);
  }
);

Query Tips

  • Names must be unique within their parent scope
  • Elements can query for their own child elements
  • Queries are cached and reused for performance
  • Returns an RqElement with reactive capabilities

⚡️ on()

Registers event listeners on the element.

Parameters

| Name | Type | Description | |------|------|-------------| | event | string | The event to listen for | | handler | (el: RqElement, evt: Event) => void | The handler function to call when the event is triggered |

Example

component.query("btn-increment").on("click", (el, evt) => {
  store.count++;
});

Tips

  • Event handlers are automatically cleaned up when the element is disposed
  • Returns element for chaining

🪢 bind()

Binds a property to a reactive value.

Parameters

| Name | Type | Description | |------|------|-------------| | property | string | The property to bind | | value | () => any | Function that returns the value to bind |

Example

component.query("avatar").bind("src", () => store.user.avatar);
component.query("status").bind("textContent", () => store.status);
component.query("btn-search").bind("disabled", () => store.isLoading);
component.query("input-search").bind("value", () => store.searchTerm);

Tips

  • Bind any property that can be set with a string
  • The class property can be bound to a string or an object
  • The style property can be bound to a string or an object
  • Individual classes can be bound with the class.<class-name> syntax
  • Returns element for chaining

🔄 for()

Renders lists of elements with optional keyed tracking.

Parameters

| Name | Type | Description | | --------- | ------------------------------------- | ------------------------------------------ | | items | () => T[] | Function that returns the array to iterate | | keyFn | (item: T) => any | *Optional function to generate unique keys | | setup | (el: RqElement, item: T) => void | Setup function for each item |

Example

// Non-keyed list
list.for(
  () => store.items,  // items array
  (el, item) => {
    // Chain directives in the function
    el.text(() => item.title);
  }
);

// Keyed list
list.for(
  () => store.items,  // items array
  (item) => item.id,  // key function (optional)
  (el, item) => {
    // Chain directives in the function
    el.text(() => item.title);
  }
);

Tips

  • Keyed lists will reorder DOM nodes when the list updates
  • Non-keyed lists will only update the data bound to the DOM items
  • Apply additional directives in the setup function
  • Does not return the element for chaining

🔀 if()

Conditionally renders elements based on a reactive condition.

Parameters

| Name | Type | Description | |------|------|-------------| | condition | () => boolean | Function that returns whether the element should be shown | | setup | (el: RqElement) => void | Optional setup function called when element is rendered |

Example

<div rq="user-info" rq-cloak>
  <img rq="avatar" />
  <span rq="name"></span>
</div>
component.query("user-info")
  .if(() => store.user, (el) => {
    el.query("name").text(() => store.user.name);
    el.query("avatar").bind("src", () => store.user.avatar);
  });

Tips

  • Use rq-cloak attribute to hide elements until conditions are evaluated
  • Child elements should be queried and bound within the setup function
  • Setup function runs each time an element is rendered
  • Cleanup is automatic when condition becomes false
  • Does not return the element for chaining

👻 show()

Shows or hides elements based on a reactive condition without removing them from the DOM.

Parameters

| Name | Type | Description | |------|------|-------------| | condition | () => boolean | Function that returns whether the element should be visible |

Example

<div rq="loading-spinner">Loading...</div>
component.query("loading-spinner").show(() => store.isLoading);

Tips

  • Unlike if, elements remain in the DOM but are hidden with display: none
  • Useful for frequently toggled elements
  • Returns element for chaining

🔤 text()

Sets the text content of an element reactively.

Parameters

| Name | Type | Description | |------|------|-------------| | content | () => string | Function that returns the text content |

Example

<span rq="counter">0</span>
<div rq="greeting">Welcome!</div>
// Simple text binding
component.query("counter")
  .text(() => store.count);

// Computed text content
component.query("greeting")
  .text(() => `Welcome ${store.user.name}!`);

Tips

  • Safely escapes HTML content
  • Returns element for chaining

👾 html()

Binds the element's inner HTML with reactive content.

Parameters

| Name | Type | Description | |------|------|-------------| | content | () => string | HTML content or function returning HTML |

Example

component.query("content-area").html(() => 
  `<strong>Current count: ${store.count}</strong>`
);

Tips

  • Use with caution - ensure HTML content is sanitized
  • Returns element for chaining

🏍️ onMounted()

Elements support mounted callbacks for integration with external libraries

component.query("bar-chart")
  .onMounted(() => {
    // Run after element is mounted
    initializeChart();
    
    // Optional cleanup
    return () => {
      cleanupChart();
    };
  });

Chaining Directives

Most directives can be chained for complex behaviors:

component
  .query("btn-increment")
  .text(() => `Clicked ${store.count} times`)
  .bind("disabled", () => store.count >= 5)
  .on("click", (el, evt) => {
    store.count++;
  });

License

MIT