DEV Community

YQteam
YQteam

Posted on

yq-sanyi v0.4.0: define a component once, use it as a real HTML tag anywhere

yq-sanyi is a web component framework with no runtime dependencies. You define a component once, and after that it is a native HTML element you can use anywhere.

yq.define('yq-counter', {
  template: '',
  style: 'button { font-size: 18px; padding: 8px 18px; }',
  script: function () {
    return {
      state: { count: 0 },
      inc: function (state) {
        state.count = state.count + 1
      }
    }
  }
})
Enter fullscreen mode Exit fullscreen mode

Then, in plain HTML:


Enter fullscreen mode Exit fullscreen mode

The tag mounts itself, renders, updates and cleans up. The template is HTML, the style is CSS, the script is JS — nothing custom to learn. No JSX, no virtual DOM, no compiler, and whoever drops the component on a page doesn't need a build step either.

  • ~12.6 kB core bundle, gzipped

  • 0 runtime dependencies

  • 298 / 298 tests passing

  • Apache-2.0

Why another component framework?

Fair question, there are plenty.

But a lot of pages don't need a framework, they need one widget. If you pull React into a landing page for a single dropdown, you also bring its compiler, its runtime and a dependency tree. If you stay native with Web Components instead, you end up writing the templating, style isolation, reactivity and lifecycle wiring yourself, every time.

yq-sanyi is for that middle ground. It removes the boilerplate, and the result is still a plain custom element — so it works in any page, any template engine, any CMS, next to whatever else is already on the page.

Quick start

git clone https://github.com/YQteam-hq/yq-sanyi.git
cd yq-sanyi
npm install
npm run build
Enter fullscreen mode Exit fullscreen mode

Then write a plain HTML file:


 lang="en">

   charset="utf-8">
  </span>yq-sanyi quick start<span class="nt">


  

  
  


Enter fullscreen mode Exit fullscreen mode

Custom element rules still apply: the name starts lowercase and contains a hyphen, so works and doesn't. define throws on an invalid name instead of letting it fail quietly in the page.

Template, behavior and style, in one place

Most setups keep structure, logic and style in three separate places. yq-sanyi keeps them in one definition that shares a scope, a reactive state and a lifecycle. That's where the name "trinity" comes from.

One definition, three parts, one scope: template, behavior and scoped style share one scope, one reactive state and one lifecycle

The template layer is a set of declarative directives that covers the usual cases:


 yq-for="(task, index) in tasks" yq-key="id">
  {{ index }}. {{ task.title }}
   yq-on:click="toggle">toggle
yq-for="group in groups" yq-key="id"> {{ group.name }} yq-for="row in group.items" yq-key="tag">
Enter fullscreen mode Exit fullscreen mode

Conditionals are yq-if / yq-else-if / yq-else / yq-show; two-way binding is yq-model, with .trim / .number / .lazy; events are written yq-on:click.

Accessibility got its own pass in 0.4.0. When an aria-* binding resolves to an empty value, the attribute isn't rendered at all, so you don't get a dangling aria-label="" sitting in the DOM:

 id="email"
       yq-model.trim="email"
       yq-on:focus="markFocus"
       yq-on:blur="validate"
       aria-label="{{ labelFor(name) }}"
       aria-invalid="{{ error ? 'true' : '' }}">
Enter fullscreen mode Exit fullscreen mode

Style isolation uses scope rewriting by default and doesn't require Shadow DOM. That's on purpose — global theming still reaches the component, and outside CSS isn't fully sealed off. When you do want a hard boundary, pass useShadowDOM to createScopedElement. A component's style is injected once and shared by every instance.

State comes back from script. Handlers receive the reactive state, and writing to it re-renders. Several writes in the same synchronous task are batched into one flush.

What's new in v0.4.0

Five merged batches, four areas.

Structure

  • Nested yq-for at any depth, with yq-key and the row index still working

  • aria-* bindings, plus yq-on:keydown / focus / blur / paste / wheel

  • defineAlias(displayName, realName) for hyphenated display names

  • Reusable static fragments via