Git Product home page Git Product logo

templize's Introduction

templize

HTML reactive template parts for any DOM elements with expressions and directives.

Based on Template Instantiation and DOM-parts specs.

Features

Extends template parts with the following:

  • Works with any elements, not just <template>;
  • Supports reactive fields;
  • Provides expression processor;
  • Enables loops, conditions;
  • Exposes directives API;
  • Plugs as vanilla ESM, doesn't require tooling.

Usage

It can be used as module via npm i templize or in HTML directly:

<script type="importmap">{ "imports": { "templize": "path/to/templize.js" }}</script>

<div id="foo" class="foo {{y}}">{{x}} world</div>

<script type="module">
  import templize from 'templize'

  templize(document.getElementById('foo'), { x: 'Hello', y: 'bar'})
  // <div id="foo" class="foo bar">Hello world</div>
</script>

API

const [params, update] = templize(element, init?);

params is proxy reflecting template fields values. Changing any of its props updates / rerenders fields.
update can be used for bulk-updating multiple props.
init is the initial state to render the template. It can include reactive values, see reactivity.

Reactivity

Template fields support the following async/reactive values:

  • Promise/Thenable
  • AsyncIterable
  • Observable/Subject

Update happens when any param changes:

<div id="done">{{ loading ? '...' : result }}</div>

<script type="module">
  import templize from 'templize'
  import { signal } from '@preact/signals'

  const loading = signal(false), result = signal(false)
  templize(document.querySelector('#done'), { loading, result })
  
  setTimeout(() => (loading.value = true, result.value = 'done'), 1000)

  // <div id="done">...</div>
  // ... 1s after
  // <div id="done">done</div>
</script>

This way, for example, @preact/signals or rxjs can be streamed directly to element attribute or content.

Note: observers don't require disposal, since they're connected in weak fashion. Once element is disposed, observables are disconnected.

Expressions

Templize enables expressions via default expression processor:

<header id="title">
  <h1>{{ user.name }}</h1>
  Email: <a href="mailto:{{ user.email }}" onclick="{{ e => { event.preventDefault(); await sendEmail(user.email); } }}">{{ user.email }}</a>
</header>

<script>
  import templize from 'templize';

  templize(
    document.querySelector('#title'),
    { user: { name: 'Hare Krishna', email: '[email protected]' }}
  )
</script>

It supports the following field expressions with common syntax:

Part Expression
Value {{ foo }}
Property {{ foo.bar?.baz }}, {{ foo[bar] }}
Call {{ foo.bar(baz, qux) }}
Boolean {{ !foo && bar || baz }}
Ternary {{ foo ? bar : baz }}
Primitives {{ "foo" }}, {{ true }}, {{ 0.1 }}
Comparison {{ foo == 1 }}, {{ bar >= 2 }}
Math {{ a * 2 + b / 3 }}
Pipe {{ bar | foo }}{{ foo(bar) }}

Attributes

Processor makes assumptions regarding how attribute parts set values.

  • hidden="{{ boolean }}" boolean values set or remove attribute.
  • onClick="{{ function }}" assigns onclick handler function (no need to call it, unlike in html).
  • class="{{ classes }}" can take either an array or a string.
  • style="{{ styles }}" can take either an object or a string.

Other attribute values cast to strings.

Directives

Templize recognizes shortcut directives via :<attr> (similar to vue).

Loops

Iterating over set of items can be done with each directive:

<ul>
  <li :each="{{ item, index in items }}" id="item-{{item.id}}" data-value="{{item.value}}">{{item.label}}</li>
</ul>

Conditions

To optionally display an element, there are if, else-if, else directives.

<span :if="{{ status == 0 }}">Inactive</span>
<span :else-if="{{ status == 1 }}">Active</span>
<span :else>Finished</span>

Note: text conditions can be organized via ternary operator:

<span>Status: {{ status === 0 ? 'Active' : 'Inactive' }}</span>

Adding directives

To register a directive, directive(name, onCreate) function can be used:

import templize, { directive } from 'templize'

directive('inline', (instance, innerTplPart, state) =>
  innerTplPart.replaceWith(innerTplPart.template.createInstance(state))
)

Interop

Templize supports any standard template parts processor:

const params = templize(element, initState, {
  createCallback(element, parts, state) {
    // ... init parts / parse expressions
  },
  processCallback(element, parts, state) {
    // ... update parts / evaluate expressions
  }
})

Any external processor can be used with templize, eg. @github/template-parts:

import templize from 'templize'
import { propertyIdentityOrBooleanAttribute } from '@github/template-parts'

const params = templize(
  document.getElementById('foo'),
  { x: 'Hello', hidden: false },
  propertyIdentityOrBooleanAttribute
)
params.hidden = true

Templize expression processor can also be used with other template instancing libraries as:

import { TemplateInstance } from '@github/template-parts'
import { processor } from 'templize'

const instance = new TemplateInstance(document.querySelector('my-template'), {}, processor)

Or it can be used with proposal polyfill:

import 'templize-instantiation-polyfill'
import { processor } from 'templize'

document.defineTemplateType('my-template-type', processor)

Dependencies

  • template-parts − compact template parts ponyfill.
  • subscript − fast and tiny expressions parser.
  • sube − subscribe to any reactive source.
  • element-props − normalized element properties setter.

Buddies

  • spect − selector observer, perfect match for organizing flexible native DOM templates.
  • value-ref − reactive value container with reactivity, useful for state management.
  • subscribable-things − reactive wrappers for various APIs.

Neighbors

  • stampino − small HTML template system based on lit-html.

🕉

templize's People

Contributors

dy avatar luwes avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

templize's Issues

Prop-ize? attrz? propx? attrx?

Simplified take from templize and alpine/lit/etc.
:if, :else-if, :else, :each are special directives with special formatting, excluding moustache wrappers.
We can mix it with standard lit/alpine/vue approach for dynamic properties, since it allows to safely dynamize html elements without templating-related errors. It's just sort of data- attributes, but evaluated to templates.

So by default :prop="x=1" evaluates to [predefined] element property.
Any property outside of props is handled as directive: :if, :each.

Events with modifiers @click.prevent... are pretty cool too, but create confusing convention. I guess they must point to specific code that gets executed, @click.prevent.throttle="someAPICall(event.xyz)".
Same time props can safely take :onclick="(e)=>{...}".
To make events more useful, they can have a meaning addEventListener, so that multiple attributes attach listeners, not assign events. Nope: <x @click @click></x> is considered one attribute.

Comparison to Alpine / drawbacks

Alpine

+Alps are in Switzerland, as well as design
-too many magic variables
+some vue-similar concepts
+no conflicts with DOM syntactically
-some unclarity about handler callback
-some vue-like syntaxing legacy :class, @click
-unelegant for,ifs
-redundant attrs like x-show etc
-x-transition.duration.500ms
+async handlers by default
-not really elegant integration of data in JS and markup. Some messy Alpine.bind or Alpine.data are cumbersome
+has extensive docs and reliable-ish support/use-cases
-reactivity is very secondary, but similar to preact-signals
+got some inspiration from the designs
+bundles a bunch of handy standardized event modifiers
-connection to external data/API is really messy - via alpine:init Apline.data
-main problem of Alpine is that's component-first focused: data resides in components, not connected from outside. Instead of data-first approach.
-incompatible with :if from the templize.

Templize

-11ty templates need to be wrapped into {% raw %}
-some direct field attributes are not loadable, like svg's width/height or img's src
-no clarity with binding events: introducing arrow functions is not nice, evaluating handlers code requires some magic functions
+direct any-reactivity integration

Consensus

Let's try Alpine with elevenlabs. To see if there are handy parts and as good ground for exploring/improving templize. It seems template parts are a bit more verbose than alpine for now. But alpine doesn't need preact signals.

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.