# Fudgel - complete documentation Generated from the pages at https://fudgel.js.org/. The short version is https://fudgel.js.org/llms.txt. --- Source page: https://fudgel.js.org/index.html
Fudgel
Write less. Do more.
### Easy and Lightweight Web Components Do you miss working with plain JavaScript and HTML? Are other frameworks causing pain or bloating your project? Tired of a slow transpile step? Fudgel allows you to write web components easily using familiar JavaScript classes and HTML, staying out of your way and making your life more effortless. Event bindings are added automatically, calling methods in your controller class. Property changes in your controller automatically update the HTML. Integrates seamlessly with Angular, React, and other frameworks. All of this is done while keeping the bundle size very small, so your users aren't waiting for a massive download just to see your content. Fudgel uses under 8k to provide: - Automatic DOM updates when [properties change](https://fudgel.js.org/bindings.html) - Add and remove elements, classes, loop over data, events, and more with [directives](https://fudgel.js.org/directives.html) - [Lifecycle](https://fudgel.js.org/lifecycle.html) stages exposed as events and controller methods - Creates a custom element using standard Web Component APIs - Two-way property and attribute bindings, plus events for passing data - Works with all major frameworks (eg. Angular, React, Vue, Svelte, etc.) - Supports both Shadow DOM and Light DOM rendering - Styles scoped to your component automatically, even when not using Shadow DOM - A simplistic `` element for [content projection](https://fudgel.js.org/content-projection.html) in the Light DOM - Building is optional; works with vanilla JavaScript - Can use the library directly in the browser - Fully tree-shakeable for smaller bundle sizes - Published as both UMD and a module - Taking care of developers and security-conscious teams - Works even with "Content-Security-Policy" directives (inline styles could break with a _really strict_ policy) - Full TypeScript support - Dependency injection for services, with overrides for testing Example (samples/welcome-to-fudgel.js): ```js import { component } from '/fudgel.min.js'; component('welcome-to-fudgel', { template: ` Hello {{name}}, welcome to Fudgel! ` }, class MyCustomElement { name = "Developer" onInit() { setTimeout(() => this.name = 'Super Developer', 5000); } }); ``` In the above example, a new custom element is defined. Whenever you use the new HTML element ``, it will automatically insert "Hello Developer, welcome to Fudgel!" as text. Because of the timeout set up in `onInit` (a lifecycle method), in five seconds the text automatically changes to "Hello Super Developer, welcome to Fudgel!" Web components make great additions to your browser-based UI because they work everywhere. Self-contained chunks of functionality that can eliminate lots of work on your side. Also, upgrading them or upgrading your existing framework is easier because their dependencies are built into the custom element. The downside is a bit of extra size from the wiring that has to take place, but Fudgel limits that to just the necessities.
As a comparison, just the HTML for this page is about the same size (excluding JavaScript, CSS, images, and fonts). A standard "Hello, world!" style project using Angular is reported at 35k and React is about 46k; however, both project sizes can vary immensely. ## Browser Support 99% of tracked browser traffic works with Fudgel. Here is a list of the minimum supported browser versions: | Browser | Version | Released | |:----------------:|:-------:|:--------:| | Chrome | 64 | Jan 2018 | | Edge | 79 | Jan 2020 | | Safari | 11.1 | Mar 2018 | | Firefox | 110 | Feb 2023 | | iOS Safari | 11.3 | Mar 2018 | | Samsung Internet | 9.0 | Jan 2019 | The build is compiled to ES2018 syntax and every release is checked against it, so a newer language feature cannot slip into the bundle. If you need to support slightly older versions, look at Fudgel 2.x or use Babel to transpile the library. (Firefox 110 is where [`CSSPageRule.selectorText` became available](https://developer.mozilla.org/en-US/docs/Web/API/CSSPageRule/selectorText), which is needed for CSS scoping.) See the details on the most restrictive browser features required to run Fudgel using [CanIUse.com feature list](https://caniuse.com/mdn-html_elements_slot,mdn-api_customelementregistry,mdn-api_shadowroot,mdn-api_csspagerule_selectortext,wf-spread). At the bottom, click "Show Summary", "Intersection" and change usage to "all tracked" to exclude bots, `curl`, and other unknown browsers. ## Goals and Prior Work This project has received the benefit of having others blaze trails in related areas. Thanks to the following projects for their inspiration: - [Slim.js](https://slimjs.com/) provided the starting point that a lightweight library can contain lots of functionality, such as the automatic bindings and text parser. - [Angular](https://angular.dev/) lifecycle hooks and structural directives were useful to mimic. - [Alpine.js](https://alpinejs.dev/) has event handlers using `@` prefixes and modifiers. - [Vue.js](https://vuejs.org/) is big into using slots for content projection, plus they also have event handlers with modifiers. - [Skruv](https://skruv.io/) is a small library that's similar to Slim.js and is the inspiration for the CSS scoping. - [jsep](https://github.com/EricSmekens/jsep) is how the basis for how expressions are parsed within templates without using "eval()" or "new Function()". - [a-wc-router](https://github.com/colscott/a-wc-router) inspired the `` element for [content projection](https://fudgel.js.org/content-projection.html) in the light DOM. --- Source page: https://fudgel.js.org/getting-started.html # Getting Started Step 1: Install Fudgel or include it into your project. This can take several forms, depending on your needs. Example (samples/umd-from-cdn.html): ```html ``` Example (samples/module-from-cdn.html): ```html ``` Installing locally as a package is simple. * `npm install fudgel` * `yarn add fudgel` While developing, also import `fudgel/dev` once, before your components are defined. It does not change how Fudgel works and is left out of a production build; it warns in the console about the mistakes Fudgel otherwise cannot report, such as a property name that HTML lowercased, an expression that resolves to nothing, a `` that cannot project, a route that can never match, and a lifecycle hook that is nearly spelled right. ```js import 'fudgel/dev'; ``` Step 2: At this point you have access to the `Fudgel` object or the module's exports. It's time to write your first controller. Select the chunk of code that best fits your needs. Example (samples/hello-world-module.js): ```js // Module import { component } from '/fudgel.min.js'; component( 'hello-world-module', { template: `Hello {{audience}}!`, }, class { audience = 'world'; } ); ``` Example (samples/hello-world-umd.html): ```html ``` Example (samples/hello-world-typescript.js): ```js // TypeScript with decorators import { Component } from '/fudgel.min.js'; @Component( 'hello-world-typescript', { template: `Hello {{audience}}!`, } ) class MyCustomComponent { audience = 'world'; } ``` Step 3: You've already made a custom element at this point. What's left is adding content to the template and handling actions by users. Investigate the following topics to learn more about Fudgel's features. Component creation and configuration: * [Best Practices](https://fudgel.js.org/best-practices.html) - How to get the most benefit and avoid problems. * [Component Config](https://fudgel.js.org/component-config.html) - Configure your component. * [Naming Conventions](https://fudgel.js.org/naming.html) - camelCase in JavaScript, dash-case in HTML, and why. * [Styling](https://fudgel.js.org/styling.html) - Style using either Light DOM or Shadow DOM. Directives and data bindings used within the templates: * [Bindings](https://fudgel.js.org/bindings.html) - Connect the template to a controller. * [Reactivity](https://fudgel.js.org/reactivity.html) - What makes a binding update, and what does not. * [Forms](https://fudgel.js.org/forms.html) - Inputs, checkboxes and selects. * [Directive Basics](https://fudgel.js.org/directives.html) - Overview of directives. * [Event `@` Directive](https://fudgel.js.org/directive-event.html) - Respond to user actions. * [Property `.` Directive](https://fudgel.js.org/directive-property.html) - Set properties on elements. * [`#class` Directive](https://fudgel.js.org/directive-class.html) - Conditionally set CSS classes. * [`#ref` Directive](https://fudgel.js.org/directive-ref.html) - Reference elements in your controller. * [`*for` Directive](https://fudgel.js.org/directive-for.html) - Repeat elements based on data. * [`*if` Directive](https://fudgel.js.org/directive-if.html) - Conditionally include elements. * [`*repeat` Directive](https://fudgel.js.org/directive-repeat.html) - Repeat elements a number of times. Data flow: * [Content Projection](https://fudgel.js.org/content-projection.html) - Insert user content into your component. * [Events](https://fudgel.js.org/events.html) - Communicate using custom events. * [Input](https://fudgel.js.org/input.html) - Receive information from outside. * [Output](https://fudgel.js.org/output.html) - Send information to outside. Everything Else: * [Expressions](https://fudgel.js.org/expressions.html) - Use expressions in bindings and directives. * [Gotchas and FAQ](https://fudgel.js.org/gotchas.html) - Symptoms, causes and fixes for the things that surprise people. * [Lifecycle](https://fudgel.js.org/lifecycle.html) - Respond to component lifecycle stages through events and methods. * [Routing](https://fudgel.js.org/routing.html) - Create single-page applications with routing. * [Upgrading](https://fudgel.js.org/upgrading.html) - Upgrade from older versions of Fudgel to the current version. * [Utilities](https://fudgel.js.org/utilities.html) - Helpful utility functions provided by Fudgel. ## TypeScript Fudgel ships its own types. Two things are worth knowing. The `@Component()` decorator works with both `experimentalDecorators` and the standard decorators in TypeScript 5 and later; it returns nothing, so the decorated name still refers to your controller class. `component()` returns the custom element instead. Class fields work with either setting of `useDefineForClassFields`. A controller can be any class. To have TypeScript check the signatures of the lifecycle hooks and complete their names, implement `StrictController`: ```ts import { StrictController, ControllerMetadata, metadata } from 'fudgel'; class MyController implements StrictController { [metadata]?: ControllerMetadata; count = 0; onChange(propName: string, oldValue: unknown, newValue: unknown) {} } ``` Declare the properties your templates use as class fields, whether or not they have an initial value, so that a misspelled name in a template is a property Fudgel can watch rather than a global lookup that finds nothing. --- Source page: https://fudgel.js.org/bindings.html # Bindings Fudgel uses a simple and powerful binding syntax to connect your component's template to its controller. Bindings are enclosed in double curly braces `{{ }}` and can be used to insert dynamic content into text and attribute values. Here's an example of a simple component that binds text and an attribute. Example (samples/binding-example.js): ```js import { component } from '/fudgel.min.js'; const links = [ { siteName: 'Fudgel', url: 'https://fudgel.js.org/' }, { siteName: 'Example (disabled)', url: 'https://example.com/', disabled: true }, { siteName: 'MDN Web Docs', url: 'https://developer.mozilla.org/' }, { siteName: 'W3C (disabled)', url: 'https://www.w3.org/', disabled: true }, ]; component( 'binding-example', { template: `

This rotates through sites every 4 seconds:

{{ siteName }}

`, }, class { onInit() { this.index = -1; const update = () => { this.index = (this.index + 1) % links.length; this.siteName = links[this.index].siteName; this.url = links[this.index].url; this.disabled = !!links[this.index].disabled; }; update(); this.interval = setInterval(update, 4000); } onDestroy() { clearInterval(this.interval); } } ); ``` The URL used in the link will be changed to match the `url` property from the controller, and the link text will reflect the `linkText` property. This will continue to update automatically as long as the component is in the DOM. The bindings don't have to be simple property names. JavaScript syntax can perform calculations or call methods on the controller, such as a method that formats a date. A binding runs again when a top-level identifier in its expression is assigned, so pass a method the values it depends on: `{{ format(date) }}` updates when `date` is assigned, while `{{ formatDate() }}` runs once and never again. You are not allowed to use operators that change state, such as assignment (`=`), increment (`++`), or decrement (`--`). Learn more about [supported expressions](https://fudgel.js.org/expressions.html) and [reactivity](https://fudgel.js.org/reactivity.html). Because not all JavaScript is supported in bindings and to encourage better separation of concerns, it's recommended to use methods to perform complex logic and update state. It also helps keep your templates clean and easy to read. The link in the above example will be disabled for one of the items in the list. When an attribute is bound to a boolean expression, null, or undefined, the attribute will be added or removed based on the truthiness of the value. This is useful for attributes like `disabled`, `checked`, or `hidden`. However, if there is even one character outside of the binding in the attribute value, it will always be treated as a string. Bindings are updated whenever a top-level property in the expression changes. For example, if you have a binding like `{{ user.name }}`, the binding will update whenever the `user` property is assigned a new object. However, changes to nested properties (like `user.name`) will not automatically trigger an update unless you reassign the `user` property itself or use the `update()` [utility function](https://fudgel.js.org/utilities.html). The [Reactivity](https://fudgel.js.org/reactivity.html) page covers exactly what is watched. Similarly, bindings are used for other directives, such as [Event Directives](https://fudgel.js.org/directive-event.html) and [If Directive](https://fudgel.js.org/directive-if.html). This allows you to create dynamic behavior based on the component's state. --- Source page: https://fudgel.js.org/best-practices.html # Best Practices Using Fudgel to create custom elements is straightforward. To help ensure maximum compatibility and performance, here are some best practices to follow when writing your components. ## Performance For performance, do not reassign to a bound property. Modify a local variable and only assign it back to the bound property when all changes are complete. This prevents multiple updates to the DOM. Example (samples/assign-only-once.js): ```js // Bad idea; it updates title multiple times this.title = this.title.toUpperCase(); this.title = this.title.trim(); if (this.title.length > 20) { this.title = this.title.substring(0, 20) + '...'; } // Much better; title is only updated once let updatedTitle = this.title; updatedTitle = updatedTitle.toUpperCase(); updatedTitle = updatedTitle.trim(); if (updatedTitle.length > 20) { updatedTitle = updatedTitle.substring(0, 20) + '...'; } this.title = updatedTitle; ``` ## Compatibility When building a library, do not automatically register your custom elements in your exported module. This won't allow developers to resolve name conflicts. Instead, export a function that allows a prefix or a custom name to be provided, similar to how `defineRouterElement()` (see [Routing](https://fudgel.js.org/routing.html)) and `defineSlotComponent()` (described in [Content Projection](https://fudgel.js.org/content-projection.html)) work for Fudgel's built-in elements. Example (samples/export-component-function.js): ```js import { component } from 'fudgel'; export function defineMyElement(prefix = 'my-') { component( `${prefix}export-component-function`, { template: 'Hello, world!', }, class { // Add your controller logic here } ); } ``` ## Data Flow In general, you want your element to accept data into the controller using attributes (strings) and properties (any data). Attributes have universal support, where as most support properties. Example (samples/data-flow-into-component.html): ```html ``` When you need to send data out of your controller, send an event. If you need data to be visible and retrievable from the outside, expose it through a property. Example (samples/emit-and-exposing-property.html): ```html ``` Alternately, you can use a service to share data between components. This example uses the built-in `Emitter` class, as found on the [utilities page](https://fudgel.js.org/./utilities.html). Example (samples/service-data-sharing.html): ```html ``` --- Source page: https://fudgel.js.org/directive-class.html # `#class` Directive There's a [directive](https://fudgel.js.org/directives.html) that makes it much easier to add and remove classes dynamically. The `#class` directive allows you to define class names to add or remove if the associated value is truthy or falsy. It accepts an object where the property names are the class names and the property values are the conditions. Example (samples/directive-class.js): ```js import { component } from '/fudgel.min.js'; component( 'directive-class', { style: ` .even { background-color: lightblue; } .notMultipleOfThree { border: 2px solid black; } .moreThanFive { font-weight: bold; } `, template: `

N: {{n}}

  • Blue background if even
  • Black border if not a multiple of 3
  • Bold text if greater than 5
`, }, class { n = 1; switchClasses() { this.n = (this.n + 1) % 10; } } ); ``` --- Source page: https://fudgel.js.org/component-config.html # Component Config The `component()` function will create a new element and register it with `window.customElements` as long as the element was not yet defined. A name that is already defined is skipped, so the same library can be loaded twice. A name the browser rejects, such as one without a hyphen, throws the browser's error rather than leaving an element that never upgrades. The newly defined custom element will be assigned a constructor of `FudgelElement`, not your class. This is done to allow you to utilize any property in your class without fear of overwriting something important to `HTMLElement`. The call to `component()` accepts three parameters: 1. The name of the custom element, which must contain a hyphen and be a valid custom element name. 2. An object containing configuration to define for the new element. 3. An optional class to instantiate as the controller. Example (samples/hello-world-module.js): ```js // Module import { component } from '/fudgel.min.js'; component( 'hello-world-module', { template: `Hello {{audience}}!`, }, class { audience = 'world'; } ); ``` If you use TypeScript, you can also use the `@Component()` decorator, which takes only the first two parameters; the element name and the object of static values. Example (samples/hello-world-typescript.js): ```js // TypeScript with decorators import { Component } from '/fudgel.min.js'; @Component( 'hello-world-typescript', { template: `Hello {{audience}}!`, } ) class MyCustomComponent { audience = 'world'; } ``` The decorator defines the custom element and returns nothing, so the decorated name still refers to your controller class. `component()` returns the custom element it defined, which is a different class; a decorator returning that would rebind the name to the wrong type, and TypeScript does not allow it. Reach for `component()` when you want that constructor, and the decorator when you want a class that stays itself. When using TypeScript, you may also want to use the `metadata` property to find information about the controller, the host element, or scope. This example shows how you can set up the types so that the controller will be able to access metadata. Example (samples/show-tag-name-typescript.js): ```js // TypeScript with decorators. Shows how to use `metadata` // and have everything typed correctly. import { Component, ControllerMetadata, metadata } from '/fudgel.min.js'; @Component( 'show-tag-name-typescript', { template: `My tag name is {{tagName}}`, } ) class MyCustomComponent { audience = 'world'; [metadata]!: ControllerMetadata; tagName = 'Unknown'; onInit() { this.tagName = this[metadata].tagName; } } ``` ## Configuration The configuration supplied as the second parameter will define the template, styles, and if the Shadow DOM will be used. This is done by setting the following properties in the object. ### `attr` (optional, array of strings) Monitor these attributes on the element for changes. When a listed attribute is changed, the controller's matching property name will be updated to match the new value, the `change` event will be fired, `onChange()` will be called, and any bindings using that property will be updated. Attributes are always in camelCase in this list, even though they are in kebab-case in HTML. For example, to monitor the `data-value` attribute, you would add `"dataValue"` to this list and the controller's `.dataValue` property will be updated. A kebab-case name in this list is treated as the camelCase one. See [Naming Conventions](https://fudgel.js.org/naming.html). When the controller's property is changed, the attribute will be updated accordingly in the DOM: a string sets it, `true` sets it to an empty string, and `false`, `null` or `undefined` removes it. Other types are not reflected, as attributes can only hold string values. An absent attribute leaves the property's default in place; removing the attribute later sets it to `null`. When a name is listed in both `attr` and `prop`, the most recently changed source (attribute or property) will update the other. Learn more about [component inputs](https://fudgel.js.org/input.html) and how attributes and properties work. ### `prop` (optional, array of strings) Performs two-way mirroring of these properties, linking the element's property to the controller's property. When either one is changed, the other will be updated to match, the `change` event will be fired, `onChange()` will be called, and any bindings using that property will be updated. Properties can hold any type of value, including objects and dates. When a name is listed in both `attr` and `prop`, the most recently changed source (attribute or property) will update the other. Learn more about [component inputs](https://fudgel.js.org/input.html) and how attributes and properties work. ### `style` (optional, string) CSS styles for your custom element. These will be added to the document when your element is used. The technique changes if you are using the Shadow DOM or not. Shadow DOM: A ` `, }, class { async onViewInit() { // Insert an image to the Fudgel logo const img = document.createElement('img'); img.src = '/logo.png'; img.alt = 'Fudgel Logo'; this[metadata].root.appendChild(img); } } ); ``` ## Light DOM Example (samples/styling-example-light-dom.html): ```html

``` If you go into the playground and try it out, you'll see a regular button and a larger, styled button. The larger button is styled through `:host` in the component's `style` configuration. Normally that only works for Shadow DOM, but Fudgel rewrites it so there's one syntax for both Shadow DOM and Light DOM. You might now wonder about the styling for `button` and why both buttons aren't extra large. Keen observation! Fudgel automatically scopes your styles so they don't leak out to the rest of the page. This is done by rewriting your CSS to target only your component. Your component's HTML template is also rewritten to include the necessary classes for scoping. This is effectively what Fudgel would generate for the above component. Example (samples/styling-example-light-dom-generated.html): ```html ``` ## Shadow DOM Things are slightly different for Shadow DOM. This next example simply changes `useShadow: true` to leverage Shadow DOM. Example (samples/styling-example-shadow-dom.html): ```html

``` Because of the encapsulation provided by Shadow DOM, Fudgel doesn't need to do as much to rewrite your CSS for scoping. Here would be what Fudgel generates for the above example component. Example (samples/styling-example-shadow-dom-generated.html): ```html ``` --- Source page: https://fudgel.js.org/upgrading.html # Upgrading When a new version of Fudgel is released, you may need to make some changes to your existing components to ensure they continue to work correctly. ## From 3.4.x to 4.0.x Three behaviors changed on purpose. * An attribute that is absent no longer sets the controller property to `null`; the class field's default stays. Removing an attribute later still sets the property to `null`, and `onChange()` is no longer called for an absent attribute at startup. Code that relied on a declared attribute being `null` when absent should give the field no initial value, or treat `null` and `undefined` alike. * The index a `*repeat` provides starts at 0, like every other index in JavaScript and like the keys of `*for` over an array. Add 1 where a template shows the number to people. * In the light DOM, a descendant named after `:host` in a style rule is scoped to the component, so `:host p` no longer styles a `

` inside a nested component, which it never did in the shadow DOM. To style content that another component provides, style it from that component. Everything else needs no code changes. Several behaviors were bugs and now match what the documentation said; check anything that depended on the old behavior. * Bindings and event listeners are cleaned up when a directive removes their element. Listeners on the window or document from `@event.window`, `.document` and `.outside` used to stay attached forever. * A getter on the controller's prototype is read each time a binding runs instead of being copied once when the template linked. An accessor on the instance receives the instance as `this` when assigned. * A global such as `Math` or `Date` in a binding resolves to the global. It used to be shadowed by an undefined controller property. * Expressions support the conditional operator, `a ? b : c`, and parentheses, `(a + b) * c`. * `component()` throws when the browser rejects the element name, such as a name without a hyphen. It used to fail silently. A name that is already defined is still skipped. * A kebab-case name in `attr` or `prop` is accepted as the camelCase one. * Assigning `true`, `false`, `null` or `undefined` to a property listed in `attr` reflects to the attribute, as documented. * `@keydown.arrow-left` and other dashed key modifiers work; they never matched before. `@keydown.space` matches the space bar. `@some-event` also listens for the dashed name `some-event`, so events from other libraries bind. * `*for` accepts an expression with spaces, `*for="x of a ?? b"`, and a `track` clause that keys rows by identity. A row that still exists is moved rather than rebuilt when the list is reordered. * A growing `*repeat` appends new items after the existing ones instead of inserting them before. * Light DOM style scoping handles `:host(...)`, `:host-context(...)`, pseudo-elements, and commas inside `:is()` and attribute selectors. A `p::before` rule no longer leaks to the whole page. A selector Fudgel cannot rewrite is reported in the console. * A structural directive at the end of a template no longer re-links the content it rendered, which could re-read braces in your data as expressions. * The router routes on the resolved location, so `history.replaceState(state)` without a URL keeps the current route and a relative URL is resolved. Clicks with a modifier key, another button, a `target`, `download`, `rel="external"`, or a fragment on the current page are left to the browser. History is patched once for the page rather than once per router. `RouterComponent` exposes its route elements as `routes`. * `di()` recovers after a service constructor throws; it used to report a circular dependency forever. * A property, object key, or route parameter named `entries` works. * `fudgel` can be imported outside a browser, so services that use `di()` can be unit tested under Node. `require('fudgel')` works, and `package.json` has an `exports` map with `fudgel` and `fudgel/dev`. * The build is compiled to ES2018 and checked against it, so Safari 11.1 can parse it again. * New: `import 'fudgel/dev'` while developing prints warnings for common mistakes. See [Getting Started](https://fudgel.js.org/getting-started.html). * New types: `StrictController` and `ControllerHooks`. `Controller` no longer declares the `wasAsync` parameter removed in 3.2. `camelToDash` and `dashToCamel` are exported. ## From 3.3.x to 3.4.x * The router now matches on the path alone. A URL carrying a query string or a fragment, such as `/orders?status=open`, previously failed to match `/orders` and fell through to the catch-all route. Both are still left on the URL; read them with `location.search` and `location.hash`. See [Routing](https://fudgel.js.org/routing.html). * Routes may now name query parameters to receive as attributes, using a `query` attribute on the route. See [Routing](https://fudgel.js.org/routing.html). * The `routeChange` event detail is now the matched path. It previously carried whatever URL was passed to the History API, query string and all. * The `@Component()` decorator no longer returns a value. It defined the custom element and then returned it, which TypeScript rejects because a class decorator may only return the class it decorated. The decorated name has always referred to the controller at runtime; now the types agree. No code changes are required. * Only `dist` is published. If you were reaching into `fudgel/src` or `fudgel/docs` from an installed copy, import from the package instead. ## From 3.1.x to 3.2.x * `onViewInit()` and `onParse()` are now always asynchronous and their lifecycle stages no longer pass the `wasAsync` argument. * Exposed [`lifecycle` function](https://fudgel.js.org/utilities.html) to allow for custom [lifecycle stages](https://fudgel.js.org/lifecycle.html). ## From 3.0.x to 3.1.x * `*for` was made faster. No code changes are required. ## From 2.x.x to 3.x.x * Parsing expressions changed. * `parse()` is now `parse.js()` (one of several parsing functions available). * `parsed[0]` now accepts a list of objects to search as opposed to an array. * `nextTick()` was removed. * `controllerToElement()`, `elementToController()`, and `rootElement()` has been removed. Use `metadata`, as seen on the [Utilities](https://fudgel.js.org/utilities.html) page instead. * All hooks have been removed and switched to [events](https://fudgel.js.org/events.html). * `hookOnGlobal()` is removed and mostly replaced with `events.on()`. * `component` hook changed to an event and has an additional argument. * `set:PROP_NAME` and `set:` hooks removed. `change` and `update` fire instead, respectively. * The `update()` [utility function](https://fudgel.js.org/utilities.html) no longer allows updating specific properties. * [Lifecycle](https://fudgel.js.org/lifecycle.html) stages now fire events globally, fire events on the controller, and call methods on the controller. * Created `update` and `unlink` events. * `parse` and `viewInit` have been updated to potentially be synchronous, with a new argument indicating if this was called synchronously. * Documentation reviewed and significant improvements made. ## From 1.x.x to 2.x.x * No changes. The internals changed significantly and additional information was exposed, but the API remained the same. --- Source page: https://fudgel.js.org/utilities.html # Utility Functions Fudgel comes with a few helpful functions that you should leverage to squeeze as much as you can out of this tiny library. They will help you communicate with other web components and perform common activities. ## `emit(source, name, detail, options)` - Sending Data to Parents * `source` (Element | Controller) - The element or controller that is sending the event. If passed a controller, the event will be sent from the controller's host element. * `name` (string) - The name of the event to send. * `detail` (any, optional) - The data to send with the event. This will be available on the event's `detail` property. * `options` (object, optional) - Additional options for the event. Dispatching events from your controller can be tricky with the shadow DOM. Also, for minification, you will probably want to leverage shared code to send events. Example (samples/utility-emit.js): ```js import { component, emit } from '/fudgel.min.js'; component( 'utility-emit', { template: ` to send a custom event `, }, class { clicked() { emit(this, 'button-sample-event', { extraData: 'can go in here', youCanUse: 'whatever you like', when: new Date(), }); } } ); ``` ## `metadata` - Access Components and Metadata The `metadata` symbol allows you to do the following activities. **On A Custom Element:** Using `element[metadata]` will give you access to the controller instance for that element. **On A Controller:** From a controller, using `this[metadata]` will let you access the metadata object for that component, which contains the following properties: * `attr` - A `Set()` of attribute names, based on the [component config](https://fudgel.js.org/component-config.html) `attr` property. * `cssClassName` - The CSS class name used for [scoping styles](https://fudgel.js.org/styling.html). * `events` - An `Emitter` instance for this component's events. * `host` - The `CustomElement` in the DOM that is being controlled by the current controller. * `prop` - A `Set()` of property names, based on the [component config](https://fudgel.js.org/component-config.html) `prop` property. * `root` - The root element for the component. If using Shadow DOM, this is the shadow root. Otherwise it is the custom element itself. * `style` - The styles to be added for the component, already [scoped](https://fudgel.js.org/styling.html). * `tagName` - The tag name of the custom element. * `template` - The HTML template string for the component, which is already [scoped](https://fudgel.js.org/styling.html). * `useShadow` - A boolean indicating whether the component is using Shadow DOM, which is based on the [component config](https://fudgel.js.org/component-config.html) `useShadow` property. **On A Scope:** If the scope has the `metadata` property, it is the root scope, upon which all other scopes are based. The root scope is attached to `document.body`. ### Using `element[metadata]` If you need the controller of another element, you can use `element[metadata]` to get the controller instance. Example (samples/utility-metadata-element.js): ```js import { component, metadata } from '/fudgel.min.js'; component('utility-metadata-element', { template: '', useShadow: true }, class { // Called from child element logMessage(message) { console.log('Parent:', message); } }); component('child-element', { template: ` `, useShadow: true }, class { clicked() { // Get the custom element in the DOM (the host, not the root) const childElement = this[metadata].host; if (!childElement) { console.error('Child element not in DOM'); return; } const parentElement = this.closest('parent-element', childElement); if (!parentElement) { console.error('Could not find parent element in DOM'); return; } const parentController = parentElement[metadata]; if (!parentController) { console.error('Could not find parent controller'); return; } parentController.logMessage('Hello from child'); } // Pierce the shadow DOM layers to get the parent element. closest(selector, el) { return ( (el !== document && el !== window && el.closest(selector)) || this.closest(selector, el.getRootNode().host) ); } }); ``` ### Using `controller[metadata].host` You might need to toggle attributes or perform other actions on the DOM from the controller. To get the actual custom element in the DOM, use `this[metadata].host`. Example (samples/utility-metadata-host.js): ```js import { component, metadata } from '/fudgel.min.js'; component( 'utility-metadata-host', { template: 'Inspect the DOM and notice the attribute value changing.', useShadow: true, }, class { onViewInit() { let count = 0; this.interval = setInterval(() => { this[metadata].host.setAttribute('data-attribute', count++); }, 1000); } onDestroy() { clearInterval(this.interval); } } ); ``` ### Using `controller[metadata].root` Your component creates all of its template elements in one parent element. When using a shadow DOM, this is the shadow DOM element. Otherwise, this is the custom element. To easily access this element from your controller for content manipulation, use `this[metadata].root`. Example (samples/utility-metadata-root.js): ```js import { component, metadata } from '/fudgel.min.js'; component( 'utility-metadata-root', { template: 'Wait for it ...', }, class { onViewInit() { setTimeout(() => { // The root is where the template was placed: the shadow root // when using one, otherwise the custom element itself. this[metadata].root.innerHTML = 'Hello, World!'; }, 1000); } } ); ``` ## `update(component)` - Redrawing the UI * `component` (Controller, optional) - The controller to update. If not provided, all components will be updated. When you update internal data within an object, the change detection will not pick it up. You can flag specific properties as needing to be redrawn. `update()` - When called with no arguments, it will update all bindings for all Fudgel components everywhere. This is the slowest way to trigger updates, but it is also the easiest. `update(this)` - This is a better, more focused way to update all of the bindings for just the one controller. Example (samples/utility-update.js): ```js import { component, update } from '/fudgel.min.js'; component( 'utility-update', { template: `

Name: {{user.name}}

`, }, class { onInit() { // Set up a deeply nested object this.user = { name: 'Test User', }; } clicked() { // Update the deeply nested object this.user.name = 'Updated'; // Force an update update(this); } } ); ``` ## `html(string)` and `css(string)` - Dummy Tagged Template Functions * `string` (string) - The HTML or CSS string. There is no processing done on this string. * return (string) - The same string that was passed in. There are some build tools that will allow minification of HTML and CSS when they are used within [tagged template literals](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals#tagged_templates). Fudgel exports `html()` and `css()` as empty tag functions so your toolset will detect the appropriate type of template and allow minification. You can see them in use with the [Be Prepared](https://github.com/Be-Prepared/Be-Prepared.github.io/) PWA, which also uses Vite and a plugin to minify the HTML and CSS used the template literals. Example (samples/utility-html-css.js): ```js import { component, css, html } from '/fudgel.min.js'; component('utility-html-css', { style: css` div { font-size: 3em; font-weight: bold; } `, template: html`
BIG WORDS
`, }); ``` ## `di(ServiceConstructor)` - Dependency Injection * `ServiceConstructor` (Function) - The constructor function or class of the service to inject. * return (Object) - A singleton instance of the requested service. Also included is a minimal dependency injection system, allowing you to inject singleton services into your components. Why use dependency injection? Well, it makes testing much easier, plus you only need to indicate what you want as opposed to how you obtain it or its dependencies. A requirement for this system is that you must not have side-effects nor arguments in the constructors of the services being injected. If you can follow that rule, then this system will work for live sites, test systems, and even work after your code is minified. Example (samples/utility-di.js): ```js import { component, di } from '/fudgel.min.js'; class LogService { writeToConsole(message) { console.log(message); } } component( 'utility-di', { template: ` `, }, class { logger = di(LogService); sendLog() { this.logger.writeToConsole('Logging a message'); } } ); ``` ## `diOverride(ServiceConstructor, instance)` - Override a Dependency Injection Service * `ServiceConstructor` (Function) - The constructor function or class of the service to override. * `instance` (Object) - The instance to use when the service is requested via `di()`. If you need arguments, you could instead create an instance of the service and push it into the dependency injection system using `diOverride()`. This same mechanism is used for testing, allowing you to push in mock versions of services. Example (samples/utility-di-override.js): ```js import { component, di, diOverride } from '/fudgel.min.js'; class LogService { constructor(prefix) { this.prefix = prefix; } writeToConsole(message) { console.log(`${this.prefix}: ${message}`); } } diOverride(LogService, new LogService('MESSAGE:')); component( 'utility-di-override', { template: ` `, }, class { logger = di(LogService); sendLog() { this.logger.writeToConsole('Logging a message'); } } ); ``` ## `new Emitter()` - Send data to other things outside of the DOM Node.js has an EventEmitter and it's useful for communication between services. The browser has an Event object that can be emitted up the DOM tree, but nothing really geared for services. Fudgel comes with a tiny event emitter you can use in your code. Create an emitter using `const emitter = new Emitter();` and then use the following methods to communicate: * `emitter.on(name, callback)` - Register a callback for an event name. Returns a function to remove the callback. * `emitter.off(name, callback)` - Remove a callback for an event name. * `emitter.emit(name, ...data)` - Emit an event with the given name and data. Example (samples/utility-emitter.js): ```js import { component, Emitter } from '/fudgel.min.js'; class MyService { emitter = new Emitter(); constructor() { setInterval(() => this.emitter.emit('tick'), 1000); } onTick(callback) { return this.emitter.on('tick', callback); } offTick(callback) { return this.emitter.off('tick', callback); } } const myService = new MyService(); component( 'utility-emitter', { template: `
Tick count: {{tickCount}}
`, }, class { tracking = false; tickCount = 0; tickTracker = () => (this.tickCount += 1); onDestroy() { myService.offTick(this.tickTracker); } toggleTracking() { this.tracking = !this.tracking; if (this.tracking) { myService.onTick(this.tickTracker); } else { myService.offTick(this.tickTracker); } } } ); ``` ## `parse` - A collection of methods for working with expressions There are different ways one can parse expressions in Fudgel, and each has its own method and use case. All of these methods will return an array. * `[0]` - A function that can be called with one or more context objects to evaluate the expression against. Typically, one calls this with the current scope and the controller instance. * `[1]` - An array of property names that are being bound to in the expression. Example (samples/parse-js.js): ```js import { addDirective, component, metadata, getScope, parse, } from './fudgel.js'; addDirective('#tick', (controller, node, attrValue) => { const parsed = parse.js(attrValue); // parsed[0] is a function that takes context objects, // then evaluates the expression against those contexts. // parsed[1] is a list of bound variables, which is not // needed for this directive. const scope = Object.create(getScope(node)); const updateFn = tickValue => { scope.$event = tickValue; parsed[0](scope, controller); }; const interval = setInterval(() => updateFn(Date.now()), 1000); controller[metadata].events.on('destroy', () => clearInterval(interval)); }); component( 'parse-js', { template: `
Last tick: {{tickValue}}
`, }, class { tickValue = -1; // Called from the #tick directive updateTick(tickValue) { this.tickValue = tickValue; } } ); ``` ### `parse.attr(expressionString)` * `expressionString` (string) - The expression string to parse. * return (Array) - An array where `[0]` is a function to evaluate the expression, and `[1]` is an array of property names used in the expression. This is nearly identical to `parse.text()`. When called, the resulting `[0]` function will return a string that is suitable for use as an attribute value. However, if the expression would result in `null`, `undefined`, or `false`, then the result of the function would be `false`, indicating that the attribute should be removed. This parse function is used internally by Fudgel when processing attribute [bindings](https://fudgel.js.org/bindings.html). ### `parse.js(expressionString)` * `expressionString` (string) - The expression string to parse. * return (Array) - An array where `[0]` is a function to evaluate the expression, and `[1]` is an array of property names used in the expression. This method is used to parse expressions that look like JavaScript. It supports most JavaScript syntax, except for multiple statements, function creation, assignments, and a few other items. Please see the [Expressions](https://fudgel.js.org/expressions.html) page for more information on what is and isn't supported. This will likely be the most commonly used parse function. The following example adds a new directive that will send an update every second. ### `parse.text(expressionString)` * `expressionString` (string) - The expression string to parse. * return (Array) - An array where `[0]` is a function to evaluate the expression, and `[1]` is an array of property names used in the expression. The result of running the function provided at `[0]` will always be a string. This is used for text [bindings](https://fudgel.js.org/bindings.html). ## `getScope(node)` - Get a scope object associated to a node or a node's ancestor * `node` (Node) - The starting point for searching for a scope. * return (Scope) - The scope associated with the node or the nearest ancestor node. Fudgel's [expressions](https://fudgel.js.org/expressions.html) and bindings work within scopes. A controller creates a scope when it is bound to an element. Similarly, directives are able to create child scopes that have changes but don't overwrite a parent scope's values. This is how the [`*for` directive](https://fudgel.js.org/directive-for.html) works when it iterates. You may have need to create your own scope, especially if you are creating a new directive. This works in conjunction with `parse`. Please see the example there for how to create a child scope. ## `lifecycle(controller, stage, ...args)` * `controller` (Controller) - The controller instance. * `stage` (string) - The lifecycle stage to invoke. Built-in ones include `change`, `destroy`, `init`, `parse`, `unlink`, `update`, `viewInit`. * `args` (any, optional) - Additional arguments to pass to the lifecycle method events or methods. * return (void) - No return value. This will emit the lifecycle stage event as a global Fudgel event, on the controller's event emitter, and will also call the lifecycle method on the controller if it exists. The lifecycle events will use the stage name as supplied, however the lifecycle method will have the first letter capitalized and prefixed with `on`. For example, the `init` stage will call the `onInit()` method on the controller if it exists. Learn more about [lifecycle stages](https://fudgel.js.org/lifecycle.html). ## `camelToDash(name)` and `dashToCamel(name)` - Name Conversion * `name` (string) - The name to convert. * return (string) - The converted name. The conversions Fudgel uses between JavaScript and HTML names, as described in [Naming Conventions](https://fudgel.js.org/naming.html). `camelToDash('isSaveable')` is `is-saveable` and `dashToCamel('is-saveable')` is `isSaveable`. ## `fudgel/dev` - Development Warnings ```js import 'fudgel/dev'; ``` Importing this module once, before components are defined, enables console warnings for mistakes that the production build cannot afford to detect. It never changes what Fudgel does, and each distinct warning is printed once. * A method on a controller whose name is nearly a lifecycle hook, such as `onViewInitt()`. * A `` in a light DOM template when `defineSlotComponent()` was not called. * A `.property` or attribute on a Fudgel component whose lowercased name matches a declared property, such as `.isSaveable` for `isSaveable`; the warning gives the dash-case spelling to use. * An identifier in an expression that is not on the controller, not in scope, and not a global. * A `*for` written with `in` instead of `of`. * A route that can never match because an earlier, shorter route matches the same paths. Leave the import out of a production build.