From 09e53b7a7fe65194995d1a2387d3b03646db082c Mon Sep 17 00:00:00 2001
From: Rob Eisenberg <EisenbergEffect@users.noreply.github.com>
Date: Tue, 12 Apr 2022 21:45:39 -0400
Subject: [PATCH] feat: new HTMLDirective design (#5826)

* refactor: clean up array observer

* refactor: modernize the splice code

* chore: set up SSR package (#5589)

* project files and starting to set up test infrastructure

* incorporating ts project references and getting tests working

* adding .npmignore to ingore tests and server

* adding readmes

* Update packages/web-components/fast-ssr/package.json

Co-authored-by: Chris Holt <chhol@microsoft.com>

Co-authored-by: nicholasrice <nicholasrice@users.noreply.github.com>
Co-authored-by: Chris Holt <chhol@microsoft.com>

* feat: enable multiple instances of fast-element on a page at once (#5718)

* feat: enable multiple instances of fast-element on a page at once

* Change files

Co-authored-by: EisenbergEffect <roeisenb@microsoft.com>

* refactor: extract polyfill and polyfill-like code to an optional module (#5752)

* refactor: extract polyfill and polyfill-like code to an optional module

* Change files

* fix: correct build break in fast-foundation from removing $global

* fix: update templates to use classList and fix classList bug

* Change files

* feat: require trusted types for bindings to innerHtml & move core policy

* fix: bug that arises when there are real trusted types

* refactor: minor internal cleanup in the polyfills

* fix: remove polyfill external module dependency

Co-authored-by: EisenbergEffect <roeisenb@microsoft.com>

* feat: implement template renderer infrastructure  (#5698)

* implement factory fn to create TemplateRenderer and ElementRenderer

* rename index to exports

* adding default render info object

* adding initial directive rendering

* adds initial custom element rendering

* add directive renderers

* add renderInfo to DirectiveRenderer

* use default directive renderers

* adding attribute binding tests

* ensure attributes don't get emitted twice for custom elements

* Adding internal function to render op codes so that we can omit template elements when rendering custom element templates

* emit template open and close codes from template parser

* implement FAST parser changes and fix tests

* working AspectedHTMLDirective changes into branch

* naive implementation of custom element template rendering

* adds rendering of template op code

* progress on elmeent rendering

* fixing custom element attribute rendering bug

* Change files

* adding boolean and property binding test

* fix bug preventing template elements from being parsed

* add failing template nesting test

* update parsing of content nodes to correctly render interpolated templates

* fix API report and formatting

* adding repeater rendering

* adding directive implementations for Ref, Slotted, and Children

* clean up op types to reduce rendant information

* remove commented code that is no longer needed

* adding tests for ref, slotted, and children

* re-use noop function for noop directives

* cache length of arrays in for loops

* adding style tests

* implementing style renderer

* remove 'data' from style id name

* adding tests

* renaming files to align with current conventions

* update attachShadow signature

* adding classList support

* fixing tests and supporting string types

* removing comment that is a non-concern

* add playwright install step to ci

* add browser path to ci script

* change install script to install at the package level

* update playwright

* Change files

* fixing test

Co-authored-by: nicholasrice <nicholasrice@users.noreply.github.com>

* feat: new execution context design (#5800)

* refactor: new design for execution context

* feat: add two new event helpers to the execution context and tests

* fix: wip update types to match new context apis

* fix: update foundation and components template types

* Change files

* fix: update template type in fast-website

* fix: update site components for new template types

* fix: add missing api updates

Co-authored-by: EisenbergEffect <roeisenb@microsoft.com>

* feat: new HTMLDirective API

* feat: new directive registration/identification model

* refactor: refine design/implementation of new directive aproach

* refactor: clean up comments, interfaces, types for directives/registries

* fix: update foundation to new APIs

* fix: update router to new directive APIs.

* fix: update ssr to new directive APIs

* refactor: clean up ssr switch to new directive APIs

* fix: update React wrapper lib to use latest APIs

* Change files

* fix: post rebase issues

* fix: update reflectAttributes directive to new directive APIs

* test: add more tests to capture new html/directive aspect scenarios

* Change files

* chore: run prettier on foundation

* Update packages/web-components/fast-ssr/src/template-parser/template-parser.spec.ts

Co-authored-by: Nicholas Rice <3213292+nicholasrice@users.noreply.github.com>

* Update packages/web-components/fast-ssr/src/template-parser/template-parser.spec.ts

Co-authored-by: Nicholas Rice <3213292+nicholasrice@users.noreply.github.com>

* Update packages/web-components/fast-foundation/src/directives/reflect-attributes.ts

Co-authored-by: Nicholas Rice <3213292+nicholasrice@users.noreply.github.com>

* Update packages/web-components/fast-foundation/src/directives/reflect-attributes.ts

Co-authored-by: Nicholas Rice <3213292+nicholasrice@users.noreply.github.com>

* chore: cleanup tests after rebase

Co-authored-by: Nicholas Rice <3213292+nicholasrice@users.noreply.github.com>
Co-authored-by: nicholasrice <nicholasrice@users.noreply.github.com>
Co-authored-by: Chris Holt <chhol@microsoft.com>
Co-authored-by: EisenbergEffect <roeisenb@microsoft.com>
---
 ...-6effdb36-c7f1-45ae-9f15-7dac556840ab.json |   7 +
 ...-b9272117-48e8-48b0-8356-cc529d536cef.json |   7 +
 ...-3957121e-c6d9-47b4-8eeb-416d7b4807f1.json |   7 +
 ...-0c25947c-7fac-49d3-a9d0-06d39b0544e3.json |   7 +
 .../utilities/fast-react-wrapper/src/index.ts |   2 +-
 .../fast-element/docs/api-report.md           | 129 ++++--
 .../fast-element/src/components/controller.ts |   2 +-
 .../src/components/fast-definitions.spec.ts   |   2 +-
 .../src/components/fast-definitions.ts        |  40 +-
 .../src/components/fast-element.ts            |   5 +-
 .../fast-element/src/platform.ts              |  45 ++
 .../src/templating/binding.spec.ts            |   2 +-
 .../fast-element/src/templating/binding.ts    | 124 +++---
 .../src/templating/children.spec.ts           |  30 +-
 .../fast-element/src/templating/children.ts   |   8 +-
 .../src/templating/compiler.spec.ts           |  21 +-
 .../fast-element/src/templating/compiler.ts   |  56 ++-
 .../src/templating/html-directive.ts          | 209 ++++++---
 .../fast-element/src/templating/markup.ts     |  24 +-
 .../src/templating/node-observation.ts        |   4 +-
 .../fast-element/src/templating/ref.ts        |   5 +-
 .../src/templating/repeat.spec.ts             |  54 +--
 .../fast-element/src/templating/repeat.ts     |  29 +-
 .../src/templating/slotted.spec.ts            |  32 +-
 .../fast-element/src/templating/slotted.ts    |   3 +
 .../src/templating/template.spec.ts           | 413 ++++++++++++------
 .../fast-element/src/templating/template.ts   | 104 +++--
 .../fast-foundation/docs/api-report.md        |   8 +-
 .../src/calendar/calendar.template.ts         |   4 +-
 .../src/data-grid/data-grid-row.ts            |   2 +-
 .../src/data-grid/data-grid.template.ts       |   4 +-
 .../src/data-grid/data-grid.ts                |   2 +-
 .../src/design-system/design-system.ts        |   6 +-
 .../src/design-system/registration-context.ts |   6 +-
 .../src/directives/reflect-attributes.spec.ts | 120 ++---
 .../src/directives/reflect-attributes.ts      |  77 ++--
 .../foundation-element/foundation-element.ts  |   2 +-
 .../src/picker/picker.template.ts             |   8 +-
 .../fast-foundation/src/picker/picker.ts      |   4 +-
 .../src/test-utilities/fixture.ts             |   4 +-
 .../fast-router/docs/api-report.md            |   7 +-
 .../fast-router/src/commands.ts               |   8 +-
 .../fast-router/src/contributors.ts           |  22 +-
 .../src/element-renderer/element-renderer.ts  |  12 +-
 .../web-components/fast-ssr/src/exports.ts    |  15 +-
 .../fast-ssr/src/template-parser/op-codes.ts  |  19 +-
 .../template-parser/template-parser.spec.ts   |  28 +-
 .../src/template-parser/template-parser.ts    |  37 +-
 .../src/template-renderer/directives.ts       |  19 +-
 .../template-renderer/template-renderer.ts    |  47 +-
 packages/web-components/fast-ssr/src/view.ts  |  13 +-
 51 files changed, 1163 insertions(+), 682 deletions(-)
 create mode 100644 change/@microsoft-fast-element-6effdb36-c7f1-45ae-9f15-7dac556840ab.json
 create mode 100644 change/@microsoft-fast-foundation-b9272117-48e8-48b0-8356-cc529d536cef.json
 create mode 100644 change/@microsoft-fast-react-wrapper-3957121e-c6d9-47b4-8eeb-416d7b4807f1.json
 create mode 100644 change/@microsoft-fast-router-0c25947c-7fac-49d3-a9d0-06d39b0544e3.json

diff --git a/change/@microsoft-fast-element-6effdb36-c7f1-45ae-9f15-7dac556840ab.json b/change/@microsoft-fast-element-6effdb36-c7f1-45ae-9f15-7dac556840ab.json
new file mode 100644
index 00000000000..fb77ab8cf86
--- /dev/null
+++ b/change/@microsoft-fast-element-6effdb36-c7f1-45ae-9f15-7dac556840ab.json
@@ -0,0 +1,7 @@
+{
+  "type": "major",
+  "comment": "feat: new directive registration/identification model",
+  "packageName": "@microsoft/fast-element",
+  "email": "roeisenb@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@microsoft-fast-foundation-b9272117-48e8-48b0-8356-cc529d536cef.json b/change/@microsoft-fast-foundation-b9272117-48e8-48b0-8356-cc529d536cef.json
new file mode 100644
index 00000000000..60a5183d085
--- /dev/null
+++ b/change/@microsoft-fast-foundation-b9272117-48e8-48b0-8356-cc529d536cef.json
@@ -0,0 +1,7 @@
+{
+  "type": "major",
+  "comment": "fix: update foundation to new APIs",
+  "packageName": "@microsoft/fast-foundation",
+  "email": "roeisenb@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@microsoft-fast-react-wrapper-3957121e-c6d9-47b4-8eeb-416d7b4807f1.json b/change/@microsoft-fast-react-wrapper-3957121e-c6d9-47b4-8eeb-416d7b4807f1.json
new file mode 100644
index 00000000000..7e833996fc0
--- /dev/null
+++ b/change/@microsoft-fast-react-wrapper-3957121e-c6d9-47b4-8eeb-416d7b4807f1.json
@@ -0,0 +1,7 @@
+{
+  "type": "major",
+  "comment": "fix: update React wrapper lib to use latest APIs",
+  "packageName": "@microsoft/fast-react-wrapper",
+  "email": "roeisenb@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/change/@microsoft-fast-router-0c25947c-7fac-49d3-a9d0-06d39b0544e3.json b/change/@microsoft-fast-router-0c25947c-7fac-49d3-a9d0-06d39b0544e3.json
new file mode 100644
index 00000000000..199cd29c31f
--- /dev/null
+++ b/change/@microsoft-fast-router-0c25947c-7fac-49d3-a9d0-06d39b0544e3.json
@@ -0,0 +1,7 @@
+{
+  "type": "major",
+  "comment": "fix: update router to new directive APIs.",
+  "packageName": "@microsoft/fast-router",
+  "email": "roeisenb@microsoft.com",
+  "dependentChangeType": "patch"
+}
diff --git a/packages/utilities/fast-react-wrapper/src/index.ts b/packages/utilities/fast-react-wrapper/src/index.ts
index ebf41e0c3b0..19a698c3309 100644
--- a/packages/utilities/fast-react-wrapper/src/index.ts
+++ b/packages/utilities/fast-react-wrapper/src/index.ts
@@ -120,7 +120,7 @@ function getTagName<TElement, TEvents>(
 ) {
     if (!config.name) {
         /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
-        const definition = FASTElementDefinition.forType(type)!;
+        const definition = FASTElementDefinition.getByType(type)!;
 
         if (definition) {
             config.name = definition.name;
diff --git a/packages/web-components/fast-element/docs/api-report.md b/packages/web-components/fast-element/docs/api-report.md
index f4cb660fce3..d8ef1f1174a 100644
--- a/packages/web-components/fast-element/docs/api-report.md
+++ b/packages/web-components/fast-element/docs/api-report.md
@@ -11,6 +11,9 @@ export interface Accessor {
     setValue(source: any, value: any): void;
 }
 
+// @public
+export type AddViewBehaviorFactory = (factory: ViewBehaviorFactory) => string;
+
 // Warning: (ae-internal-missing-underscore) The name "AdoptedStyleSheetsStrategy" should be prefixed with an underscore because the declaration is marked as @internal
 //
 // @internal
@@ -25,23 +28,23 @@ export class AdoptedStyleSheetsStrategy implements StyleStrategy {
 }
 
 // @public
-export enum Aspect {
-    attribute = 0,
-    booleanAttribute = 1,
-    content = 3,
-    event = 5,
-    property = 2,
-    tokenList = 4
-}
+export const Aspect: Readonly<{
+    none: number;
+    attribute: number;
+    booleanAttribute: number;
+    property: number;
+    content: number;
+    tokenList: number;
+    event: number;
+    assign(directive: Aspected, value: string): void;
+}>;
 
 // @public
-export abstract class AspectedHTMLDirective extends HTMLDirective {
-    abstract readonly aspect: Aspect;
-    abstract readonly binding?: Binding;
-    abstract captureSource(source: string): void;
-    createPlaceholder: (index: number) => string;
-    abstract readonly source: string;
-    abstract readonly target: string;
+export interface Aspected {
+    aspectType: number;
+    binding?: Binding;
+    sourceAspect: string;
+    targetAspect: string;
 }
 
 // @public
@@ -106,7 +109,7 @@ export interface BindingConfig<T = any> {
 }
 
 // @alpha (undocumented)
-export type BindingMode = Record<Aspect, BindingType>;
+export type BindingMode = Record<number, BindingType>;
 
 // @public
 export interface BindingObserver<TSource = any, TReturn = any, TParent = any> extends Notifier {
@@ -172,14 +175,14 @@ export interface ChildViewTemplate<TSource = any, TParent = any> {
 // @public
 export type CompilationStrategy = (
 html: string | HTMLTemplateElement,
-directives: readonly HTMLDirective[]) => HTMLTemplateCompilationResult;
+factories: Record<string, ViewBehaviorFactory>) => HTMLTemplateCompilationResult;
 
 // @public
 export const Compiler: {
     setHTMLPolicy(policy: TrustedTypesPolicy): void;
-    compile<TSource = any, TParent = any, TContext extends ExecutionContext<TParent> = ExecutionContext<TParent>>(html: string | HTMLTemplateElement, directives: ReadonlyArray<HTMLDirective>): HTMLTemplateCompilationResult<TSource, TParent, TContext>;
+    compile<TSource = any, TParent = any, TContext extends ExecutionContext<TParent> = ExecutionContext<TParent>>(html: string | HTMLTemplateElement, directives: Record<string, ViewBehaviorFactory>): HTMLTemplateCompilationResult<TSource, TParent, TContext>;
     setDefaultStrategy(strategy: CompilationStrategy): void;
-    aggregate(parts: (string | HTMLDirective)[]): HTMLDirective;
+    aggregate(parts: (string | ViewBehaviorFactory)[]): ViewBehaviorFactory;
 };
 
 // @public
@@ -218,6 +221,11 @@ export class Controller<TElement extends HTMLElement = HTMLElement> extends Prop
     readonly view: ElementView<TElement> | null;
 }
 
+// Warning: (ae-internal-missing-underscore) The name "createTypeRegistry" should be prefixed with an underscore because the declaration is marked as @internal
+//
+// @internal
+export function createTypeRegistry<TDefinition extends TypeDefinition>(): TypeRegistry<TDefinition>;
+
 // @public
 export function css(strings: TemplateStringsArray, ...values: (ComposableStyles | CSSDirective)[]): ElementStyles;
 
@@ -231,7 +239,7 @@ export class CSSDirective {
 export function cssPartial(strings: TemplateStringsArray, ...values: (ComposableStyles | CSSDirective)[]): CSSDirective;
 
 // @public
-export function customElement(nameOrDef: string | PartialFASTElementDefinition): (type: Function) => void;
+export function customElement(nameOrDef: string | PartialFASTElementDefinition): (type: Constructable<HTMLElement>) => void;
 
 // @public
 export type DecoratorAttributeConfiguration = Omit<AttributeConfiguration, "property">;
@@ -329,17 +337,18 @@ export const FASTElement: (new () => HTMLElement & FASTElement) & {
         new (): HTMLElement;
         prototype: HTMLElement;
     }>(BaseType: TBase): new () => InstanceType<TBase> & FASTElement;
-    define<TType extends Function>(type: TType, nameOrDef?: string | PartialFASTElementDefinition | undefined): TType;
+    define<TType extends Constructable<HTMLElement>>(type: TType, nameOrDef?: string | PartialFASTElementDefinition | undefined): TType;
 };
 
 // @public
-export class FASTElementDefinition<TType extends Function = Function> {
+export class FASTElementDefinition<TType extends Constructable<HTMLElement> = Constructable<HTMLElement>> {
     constructor(type: TType, nameOrConfig?: PartialFASTElementDefinition | string);
     readonly attributeLookup: Record<string, AttributeDefinition>;
     readonly attributes: ReadonlyArray<AttributeDefinition>;
     define(registry?: CustomElementRegistry): this;
     readonly elementOptions?: ElementDefinitionOptions;
-    static readonly forType: <TType_1 extends Function>(key: TType_1) => FASTElementDefinition<Function> | undefined;
+    static readonly getByType: (key: Function) => FASTElementDefinition<Constructable<HTMLElement>> | undefined;
+    static readonly getForInstance: (object: any) => FASTElementDefinition<Constructable<HTMLElement>> | undefined;
     get isDefined(): boolean;
     readonly name: string;
     readonly propertyLookup: Record<string, AttributeDefinition>;
@@ -366,11 +375,23 @@ export interface FASTGlobal {
 export function html<TSource = any, TParent = any, TContext extends ExecutionContext<TParent> = ExecutionContext<TParent>>(strings: TemplateStringsArray, ...values: TemplateValue<TSource, TParent, TContext>[]): ViewTemplate<TSource, TParent>;
 
 // @public
-export abstract class HTMLDirective implements ViewBehaviorFactory {
-    abstract createBehavior(targets: ViewBehaviorTargets): Behavior | ViewBehavior;
-    abstract createPlaceholder(index: number): string;
-    targetId: string;
-    readonly uniqueId: string;
+export interface HTMLDirective {
+    createHTML(add: AddViewBehaviorFactory): string;
+}
+
+// @public
+export const HTMLDirective: Readonly<{
+    getForInstance: (object: any) => HTMLDirectiveDefinition<Constructable<HTMLDirective>> | undefined;
+    getByType: (key: Function) => HTMLDirectiveDefinition<Constructable<HTMLDirective>> | undefined;
+    define<TType extends Constructable<HTMLDirective>>(type: TType, options?: PartialHTMLDirectiveDefinition | undefined): TType;
+}>;
+
+// @public
+export function htmlDirective(options?: PartialHTMLDirectiveDefinition): (type: Constructable<HTMLDirective>) => void;
+
+// @public
+export interface HTMLDirectiveDefinition<TType extends Constructable<HTMLDirective> = Constructable<HTMLDirective>> extends Required<PartialHTMLDirectiveDefinition> {
+    readonly type: TType;
 }
 
 // @public
@@ -418,9 +439,9 @@ export interface ItemViewTemplate<TSource = any, TParent = any> {
 
 // @public
 export const Markup: Readonly<{
-    interpolation: (index: number) => string;
-    attribute: (index: number) => string;
-    comment: (index: number) => string;
+    interpolation: (id: string) => string;
+    attribute: (id: string) => string;
+    comment: (id: string) => string;
 }>;
 
 // Warning: (ae-internal-missing-underscore) The name "Mutable" should be prefixed with an underscore because the declaration is marked as @internal
@@ -492,7 +513,7 @@ export const oneTime: BindingConfig<DefaultBindingOptions> & BindingConfigResolv
 
 // @public
 export const Parser: Readonly<{
-    parse(value: string, directives: readonly HTMLDirective[]): (string | HTMLDirective)[] | null;
+    parse(value: string, factories: Record<string, ViewBehaviorFactory>): (string | ViewBehaviorFactory)[] | null;
 }>;
 
 // @public
@@ -505,6 +526,11 @@ export interface PartialFASTElementDefinition {
     readonly template?: ElementViewTemplate;
 }
 
+// @public
+export interface PartialHTMLDirectiveDefinition {
+    aspected?: boolean;
+}
+
 // @public
 export class PropertyChangeNotifier implements Notifier {
     constructor(subject: any);
@@ -572,12 +598,14 @@ export class RepeatBehavior<TSource = any> implements Behavior, Subscriber {
 }
 
 // @public
-export class RepeatDirective<TSource = any> extends HTMLDirective {
+export class RepeatDirective<TSource = any> implements HTMLDirective, ViewBehaviorFactory {
     constructor(itemsBinding: Binding, templateBinding: Binding<TSource, SyntheticViewTemplate>, options: RepeatOptions);
     createBehavior(targets: ViewBehaviorTargets): RepeatBehavior<TSource>;
-    createPlaceholder: (index: number) => string;
+    createHTML(add: AddViewBehaviorFactory): string;
+    id: string;
     // (undocumented)
     readonly itemsBinding: Binding;
+    nodeId: string;
     // (undocumented)
     readonly options: RepeatOptions;
     // (undocumented)
@@ -630,11 +658,13 @@ export class Splice {
 }
 
 // @public
-export abstract class StatelessAttachedAttributeDirective<T> extends HTMLDirective implements ViewBehavior {
+export abstract class StatelessAttachedAttributeDirective<T> implements HTMLDirective, ViewBehaviorFactory, ViewBehavior {
     constructor(options: T);
     abstract bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void;
     createBehavior(targets: ViewBehaviorTargets): ViewBehavior;
-    createPlaceholder: (index: number) => string;
+    createHTML(add: AddViewBehaviorFactory): string;
+    id: string;
+    nodeId: string;
     // (undocumented)
     protected options: T;
     abstract unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void;
@@ -704,6 +734,26 @@ export type TrustedTypesPolicy = {
     createHTML(html: string): string;
 };
 
+// Warning: (ae-internal-missing-underscore) The name "TypeDefinition" should be prefixed with an underscore because the declaration is marked as @internal
+//
+// @internal
+export interface TypeDefinition {
+    // (undocumented)
+    type: Function;
+}
+
+// Warning: (ae-internal-missing-underscore) The name "TypeRegistry" should be prefixed with an underscore because the declaration is marked as @internal
+//
+// @internal
+export interface TypeRegistry<TDefinition extends TypeDefinition> {
+    // (undocumented)
+    getByType(key: Function): TDefinition | undefined;
+    // (undocumented)
+    getForInstance(object: any): TDefinition | undefined;
+    // (undocumented)
+    register(definition: TDefinition): boolean;
+}
+
 // @public
 export interface ValueConverter {
     fromView(value: any): any;
@@ -728,7 +778,8 @@ export interface ViewBehavior<TSource = any, TParent = any> {
 // @public
 export interface ViewBehaviorFactory {
     createBehavior(targets: ViewBehaviorTargets): Behavior | ViewBehavior;
-    targetId: string;
+    id: string;
+    nodeId: string;
 }
 
 // @public
@@ -738,9 +789,9 @@ export type ViewBehaviorTargets = {
 
 // @public
 export class ViewTemplate<TSource = any, TParent = any, TContext extends ExecutionContext<TParent> = ExecutionContext> implements ElementViewTemplate<TSource, TParent>, SyntheticViewTemplate<TSource, TParent, TContext> {
-    constructor(html: string | HTMLTemplateElement, directives: ReadonlyArray<HTMLDirective>);
+    constructor(html: string | HTMLTemplateElement, factories: Record<string, ViewBehaviorFactory>);
     create(hostBindingTarget?: Element): HTMLView<TSource, TParent, TContext>;
-    readonly directives: ReadonlyArray<HTMLDirective>;
+    readonly factories: Record<string, ViewBehaviorFactory>;
     readonly html: string | HTMLTemplateElement;
     render(source: TSource, host: Node, hostBindingTarget?: Element, context?: TContext): HTMLView<TSource, TParent, TContext>;
     type: any;
diff --git a/packages/web-components/fast-element/src/components/controller.ts b/packages/web-components/fast-element/src/components/controller.ts
index aeb63d3a276..80ba127329d 100644
--- a/packages/web-components/fast-element/src/components/controller.ts
+++ b/packages/web-components/fast-element/src/components/controller.ts
@@ -482,7 +482,7 @@ export class Controller<
             return controller;
         }
 
-        const definition = FASTElementDefinition.forType(element.constructor);
+        const definition = FASTElementDefinition.getForInstance(element);
 
         if (definition === void 0) {
             throw FAST.error(Message.missingElementDefinition);
diff --git a/packages/web-components/fast-element/src/components/fast-definitions.spec.ts b/packages/web-components/fast-element/src/components/fast-definitions.spec.ts
index 77754ee3682..f77df48fe27 100644
--- a/packages/web-components/fast-element/src/components/fast-definitions.spec.ts
+++ b/packages/web-components/fast-element/src/components/fast-definitions.spec.ts
@@ -4,7 +4,7 @@ import { FASTElementDefinition } from "./fast-definitions";
 import { ElementStyles } from "../styles/element-styles";
 
 describe("FASTElementDefinition", () => {
-    class MyElement {}
+    class MyElement extends HTMLElement {}
 
     context("styles", () => {
         it("can accept a string", () => {
diff --git a/packages/web-components/fast-element/src/components/fast-definitions.ts b/packages/web-components/fast-element/src/components/fast-definitions.ts
index 275dd93b7c6..622fe453a4f 100644
--- a/packages/web-components/fast-element/src/components/fast-definitions.ts
+++ b/packages/web-components/fast-element/src/components/fast-definitions.ts
@@ -1,29 +1,17 @@
-import { isString, KernelServiceId } from "../interfaces.js";
+import { Constructable, isString, KernelServiceId } from "../interfaces.js";
 import { Observable } from "../observation/observable.js";
-import { FAST } from "../platform.js";
+import { createTypeRegistry, FAST, TypeRegistry } from "../platform.js";
 import { ComposableStyles, ElementStyles } from "../styles/element-styles.js";
 import type { ElementViewTemplate } from "../templating/template.js";
 import { AttributeConfiguration, AttributeDefinition } from "./attributes.js";
 
 const defaultShadowOptions: ShadowRootInit = { mode: "open" };
 const defaultElementOptions: ElementDefinitionOptions = {};
-const fastRegistry = FAST.getById(KernelServiceId.elementRegistry, () => {
-    const typeToDefinition = new Map<Function, FASTElementDefinition>();
 
-    return Object.freeze({
-        register(definition: FASTElementDefinition): boolean {
-            if (typeToDefinition.has(definition.type)) {
-                return false;
-            }
-
-            typeToDefinition.set(definition.type, definition);
-            return true;
-        },
-        getByType<TType extends Function>(key: TType): FASTElementDefinition | undefined {
-            return typeToDefinition.get(key);
-        },
-    });
-});
+const fastElementRegistry: TypeRegistry<FASTElementDefinition> = FAST.getById(
+    KernelServiceId.elementRegistry,
+    () => createTypeRegistry<FASTElementDefinition>()
+);
 
 /**
  * Represents metadata configuration for a custom element.
@@ -65,7 +53,9 @@ export interface PartialFASTElementDefinition {
  * Defines metadata for a FASTElement.
  * @public
  */
-export class FASTElementDefinition<TType extends Function = Function> {
+export class FASTElementDefinition<
+    TType extends Constructable<HTMLElement> = Constructable<HTMLElement>
+> {
     private observedAttributes: string[];
 
     /**
@@ -77,7 +67,7 @@ export class FASTElementDefinition<TType extends Function = Function> {
      * Indicates if this element has been defined in at least one registry.
      */
     public get isDefined(): boolean {
-        return !!fastRegistry.getByType(this.type);
+        return !!fastElementRegistry.getByType(this.type);
     }
 
     /**
@@ -184,7 +174,7 @@ export class FASTElementDefinition<TType extends Function = Function> {
     public define(registry: CustomElementRegistry = customElements): this {
         const type = this.type;
 
-        if (fastRegistry.register(this)) {
+        if (fastElementRegistry.register(this)) {
             const attributes = this.attributes;
             const proto = type.prototype;
 
@@ -209,5 +199,11 @@ export class FASTElementDefinition<TType extends Function = Function> {
      * Gets the element definition associated with the specified type.
      * @param type - The custom element type to retrieve the definition for.
      */
-    static readonly forType = fastRegistry.getByType;
+    static readonly getByType = fastElementRegistry.getByType;
+
+    /**
+     * Gets the element definition associated with the instance.
+     * @param instance - The custom element instance to retrieve the definition for.
+     */
+    static readonly getForInstance = fastElementRegistry.getForInstance;
 }
diff --git a/packages/web-components/fast-element/src/components/fast-element.ts b/packages/web-components/fast-element/src/components/fast-element.ts
index 0053801dee8..64ae3dee9fc 100644
--- a/packages/web-components/fast-element/src/components/fast-element.ts
+++ b/packages/web-components/fast-element/src/components/fast-element.ts
@@ -1,3 +1,4 @@
+import type { Constructable } from "../interfaces.js";
 import { Controller } from "./controller.js";
 import {
     FASTElementDefinition,
@@ -121,7 +122,7 @@ export const FASTElement = Object.assign(createFASTElement(HTMLElement), {
      * @param nameOrDef - The name of the element to define or a definition object
      * that describes the element to define.
      */
-    define<TType extends Function>(
+    define<TType extends Constructable<HTMLElement>>(
         type: TType,
         nameOrDef?: string | PartialFASTElementDefinition
     ): TType {
@@ -137,7 +138,7 @@ export const FASTElement = Object.assign(createFASTElement(HTMLElement), {
  */
 export function customElement(nameOrDef: string | PartialFASTElementDefinition) {
     /* eslint-disable-next-line @typescript-eslint/explicit-function-return-type */
-    return function (type: Function) {
+    return function (type: Constructable<HTMLElement>) {
         new FASTElementDefinition(type, nameOrDef).define();
     };
 }
diff --git a/packages/web-components/fast-element/src/platform.ts b/packages/web-components/fast-element/src/platform.ts
index f334c47c9b8..25e04791de4 100644
--- a/packages/web-components/fast-element/src/platform.ts
+++ b/packages/web-components/fast-element/src/platform.ts
@@ -55,3 +55,48 @@ if (FAST.error === void 0) {
  * @internal
  */
 export const emptyArray = Object.freeze([]);
+
+/**
+ * Do not change. Part of shared kernel contract.
+ * @internal
+ */
+export interface TypeDefinition {
+    type: Function;
+}
+
+/**
+ * Do not change. Part of shared kernel contract.
+ * @internal
+ */
+export interface TypeRegistry<TDefinition extends TypeDefinition> {
+    register(definition: TDefinition): boolean;
+    getByType(key: Function): TDefinition | undefined;
+    getForInstance(object: any): TDefinition | undefined;
+}
+
+/**
+ * Do not change. Part of shared kernel contract.
+ * @internal
+ */
+export function createTypeRegistry<TDefinition extends TypeDefinition>(): TypeRegistry<
+    TDefinition
+> {
+    const typeToDefinition = new Map<Function, TDefinition>();
+
+    return Object.freeze({
+        register(definition: TDefinition): boolean {
+            if (typeToDefinition.has(definition.type)) {
+                return false;
+            }
+
+            typeToDefinition.set(definition.type, definition);
+            return true;
+        },
+        getByType<TType extends Function>(key: TType): TDefinition | undefined {
+            return typeToDefinition.get(key);
+        },
+        getForInstance(object: any): TDefinition | undefined {
+            return typeToDefinition.get(object.constructor);
+        },
+    });
+}
diff --git a/packages/web-components/fast-element/src/templating/binding.spec.ts b/packages/web-components/fast-element/src/templating/binding.spec.ts
index 2c02a85e7bd..7bb45456916 100644
--- a/packages/web-components/fast-element/src/templating/binding.spec.ts
+++ b/packages/web-components/fast-element/src/templating/binding.spec.ts
@@ -28,7 +28,7 @@ describe("The HTML binding directive", () => {
 
     function contentBinding(propertyName: keyof Model = "value") {
         const directive = bind(x => x[propertyName]) as HTMLBindingDirective;
-        directive.targetId = 'r';
+        directive.nodeId = 'r';
 
         const node = document.createTextNode(" ");
         const targets = { r: node };
diff --git a/packages/web-components/fast-element/src/templating/binding.ts b/packages/web-components/fast-element/src/templating/binding.ts
index e1df2925b58..55806b3e5de 100644
--- a/packages/web-components/fast-element/src/templating/binding.ts
+++ b/packages/web-components/fast-element/src/templating/binding.ts
@@ -1,5 +1,5 @@
 import { DOM } from "../dom.js";
-import { isString, Message, Mutable } from "../interfaces.js";
+import { isString, Message } from "../interfaces.js";
 import {
     Binding,
     BindingObserver,
@@ -8,11 +8,15 @@ import {
 } from "../observation/observable.js";
 import { FAST } from "../platform.js";
 import {
+    AddViewBehaviorFactory,
     Aspect,
-    AspectedHTMLDirective,
+    Aspected,
+    HTMLDirective,
     ViewBehavior,
+    ViewBehaviorFactory,
     ViewBehaviorTargets,
 } from "./html-directive.js";
+import { Markup } from "./markup.js";
 import type { CaptureType } from "./template.js";
 import type { SyntheticView } from "./view.js";
 
@@ -40,7 +44,7 @@ export const notSupportedBindingType: BindingType = () => {
 /**
  * @alpha
  */
-export type BindingMode = Record<Aspect, BindingType>;
+export type BindingMode = Record<number, BindingType>;
 
 /**
  * @alpha
@@ -85,7 +89,7 @@ function createContentBinding(
         ): void {
             super.unbind(source, context, targets);
 
-            const target = targets[this.directive.targetId] as ContentTarget;
+            const target = targets[this.directive.nodeId] as ContentTarget;
             const view = target.$fastView as ComposableView;
 
             if (view !== void 0 && view.isComposed) {
@@ -177,7 +181,7 @@ function updateTokenListTarget(
     value: any
 ): void {
     const directive = this.directive;
-    const lookup = `${directive.uniqueId}-token-list`;
+    const lookup = `${directive.id}-token-list`;
     const state: TokenListState =
         target[lookup] ?? (target[lookup] = { c: 0, v: Object.create(null) });
     const versions = state.v;
@@ -264,10 +268,10 @@ class TargetUpdateBinding extends BindingBase {
 class OneTimeBinding extends TargetUpdateBinding {
     bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void {
         const directive = this.directive;
-        const target = targets[directive.targetId];
+        const target = targets[directive.nodeId];
         this.updateTarget(
             target,
-            directive.target!,
+            directive.targetAspect!,
             directive.binding(source, context),
             source,
             context
@@ -290,12 +294,12 @@ export function sendSignal(signal: string): void {
 class OnSignalBinding extends TargetUpdateBinding {
     bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void {
         const directive = this.directive;
-        const target = targets[directive.targetId];
+        const target = targets[directive.nodeId];
         const signal = this.getSignal(source, context);
-        const handler = (target[directive.uniqueId] = () => {
+        const handler = (target[directive.id] = () => {
             this.updateTarget(
                 target,
-                directive.target!,
+                directive.targetAspect!,
                 directive.binding(source, context),
                 source,
                 context
@@ -321,8 +325,8 @@ class OnSignalBinding extends TargetUpdateBinding {
 
         if (found && Array.isArray(found)) {
             const directive = this.directive;
-            const target = targets[directive.targetId];
-            const handler = target[directive.uniqueId];
+            const target = targets[directive.nodeId];
+            const handler = target[directive.id];
             const index = found.indexOf(handler);
             if (index !== -1) {
                 found.splice(index, 1);
@@ -348,10 +352,10 @@ class OnChangeBinding extends TargetUpdateBinding {
 
     bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void {
         const directive = this.directive;
-        const target = targets[directive.targetId];
+        const target = targets[directive.nodeId];
         const observer: BindingObserver =
-            target[directive.uniqueId] ??
-            (target[directive.uniqueId] = Observable.binding(
+            target[directive.id] ??
+            (target[directive.id] = Observable.binding(
                 directive.binding,
                 this,
                 this.isBindingVolatile
@@ -363,7 +367,7 @@ class OnChangeBinding extends TargetUpdateBinding {
 
         this.updateTarget(
             target,
-            directive.target!,
+            directive.targetAspect!,
             observer.observe(source, context),
             source,
             context
@@ -371,8 +375,8 @@ class OnChangeBinding extends TargetUpdateBinding {
     }
 
     unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void {
-        const target = targets[this.directive.targetId];
-        const observer = target[this.directive.uniqueId];
+        const target = targets[this.directive.nodeId];
+        const observer = target[this.directive.id];
         observer.disconnect();
         observer.target = null;
         observer.source = null;
@@ -386,7 +390,7 @@ class OnChangeBinding extends TargetUpdateBinding {
         const context = (observer as any).context;
         this.updateTarget(
             target,
-            this.directive.target!,
+            this.directive.targetAspect!,
             observer.observe(source, context!),
             source,
             context
@@ -412,20 +416,24 @@ type FASTEventSource = Node & {
 class EventListener extends BindingBase {
     bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void {
         const directive = this.directive;
-        const target = targets[directive.targetId] as FASTEventSource;
+        const target = targets[directive.nodeId] as FASTEventSource;
         target.$fastSource = source;
         target.$fastContext = context;
-        target.addEventListener(directive.target!, this, directive.options);
+        target.addEventListener(directive.targetAspect!, this, directive.options);
     }
 
     unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void {
-        this.removeEventListener(targets[this.directive.targetId] as FASTEventSource);
+        this.removeEventListener(targets[this.directive.nodeId] as FASTEventSource);
     }
 
     protected removeEventListener(target: FASTEventSource): void {
         target.$fastSource = null;
         target.$fastContext = null;
-        target.removeEventListener(this.directive.target!, this, this.directive.options);
+        target.removeEventListener(
+            this.directive.targetAspect!,
+            this,
+            this.directive.options
+        );
     }
 
     handleEvent(event: Event): void {
@@ -502,73 +510,39 @@ const createInnerHTMLBinding = globalThis.TrustedHTML
 /**
  * @internal
  */
-export class HTMLBindingDirective extends AspectedHTMLDirective {
+export class HTMLBindingDirective
+    implements HTMLDirective, ViewBehaviorFactory, Aspected {
     private factory: BindingBehaviorFactory | null = null;
 
-    public readonly source: string = "";
-    public readonly target: string = "";
-    public readonly aspect: Aspect = Aspect.content;
+    id: string;
+    nodeId: string;
+    sourceAspect: string;
+    targetAspect: string;
+    aspectType: number;
 
-    public constructor(
-        public binding: Binding,
-        public mode: BindingMode,
-        public options: any
-    ) {
-        super();
+    constructor(public binding: Binding, public mode: BindingMode, public options: any) {
+        this.aspectType = Aspect.content;
     }
 
-    public captureSource(value: string): void {
-        (this as Mutable<this>).source = value;
-
-        if (!value) {
-            return;
-        }
-
-        switch (value[0]) {
-            case ":":
-                (this as Mutable<this>).target = value.substring(1);
-                switch (this.target) {
-                    case "innerHTML":
-                        this.binding = createInnerHTMLBinding(this.binding);
-                        (this as Mutable<this>).aspect = Aspect.property;
-                        break;
-                    case "classList":
-                        (this as Mutable<this>).aspect = Aspect.tokenList;
-                        break;
-                    default:
-                        (this as Mutable<this>).aspect = Aspect.property;
-                        break;
-                }
-                break;
-            case "?":
-                (this as Mutable<this>).target = value.substring(1);
-                (this as Mutable<this>).aspect = Aspect.booleanAttribute;
-                break;
-            case "@":
-                (this as Mutable<this>).target = value.substring(1);
-                (this as Mutable<this>).aspect = Aspect.event;
-                break;
-            default:
-                if (value === "class") {
-                    (this as Mutable<this>).target = "className";
-                    (this as Mutable<this>).aspect = Aspect.property;
-                } else {
-                    (this as Mutable<this>).target = value;
-                    (this as Mutable<this>).aspect = Aspect.attribute;
-                }
-                break;
-        }
+    createHTML(add: AddViewBehaviorFactory): string {
+        return Markup.interpolation(add(this));
     }
 
     createBehavior(targets: ViewBehaviorTargets): ViewBehavior {
         if (this.factory == null) {
-            this.factory = this.mode[this.aspect](this);
+            if (this.targetAspect === "innerHTML") {
+                this.binding = createInnerHTMLBinding(this.binding);
+            }
+
+            this.factory = this.mode[this.aspectType](this);
         }
 
         return this.factory.createBehavior(targets);
     }
 }
 
+HTMLDirective.define(HTMLBindingDirective, { aspected: true });
+
 /**
  * @alpha
  */
diff --git a/packages/web-components/fast-element/src/templating/children.spec.ts b/packages/web-components/fast-element/src/templating/children.spec.ts
index 390f2029825..a74dbbc8491 100644
--- a/packages/web-components/fast-element/src/templating/children.spec.ts
+++ b/packages/web-components/fast-element/src/templating/children.spec.ts
@@ -43,18 +43,18 @@ describe("The children", () => {
         function createDOM(elementName: string = "div") {
             const host = document.createElement("div");
             const children = createAndAppendChildren(host, elementName);
-            const targetId = 'r';
-            const targets = { [targetId]: host };
+            const nodeId = 'r';
+            const targets = { [nodeId]: host };
 
-            return { host, children, targets, targetId };
+            return { host, children, targets, nodeId };
         }
 
         it("gathers child nodes", () => {
-            const { host, children, targets, targetId } = createDOM();
+            const { host, children, targets, nodeId } = createDOM();
             const behavior = new ChildrenDirective({
                 property: "nodes",
             });
-            behavior.targetId = targetId;
+            behavior.nodeId = nodeId;
             const model = new Model();
 
             behavior.bind(model, ExecutionContext.default, targets);
@@ -63,12 +63,12 @@ describe("The children", () => {
         });
 
         it("gathers child nodes with a filter", () => {
-            const { host, children, targets, targetId } = createDOM("foo-bar");
+            const { host, children, targets, nodeId } = createDOM("foo-bar");
             const behavior = new ChildrenDirective({
                 property: "nodes",
                 filter: elements("foo-bar"),
             });
-            behavior.targetId = targetId;
+            behavior.nodeId = nodeId;
             const model = new Model();
 
             behavior.bind(model, ExecutionContext.default, targets);
@@ -77,11 +77,11 @@ describe("The children", () => {
         });
 
         it("updates child nodes when they change", async () => {
-            const { host, children, targets, targetId } = createDOM("foo-bar");
+            const { host, children, targets, nodeId } = createDOM("foo-bar");
             const behavior = new ChildrenDirective({
                 property: "nodes",
             });
-            behavior.targetId = targetId;
+            behavior.nodeId = nodeId;
             const model = new Model();
 
             behavior.bind(model, ExecutionContext.default, targets);
@@ -96,12 +96,12 @@ describe("The children", () => {
         });
 
         it("updates child nodes when they change with a filter", async () => {
-            const { host, children, targets, targetId } = createDOM("foo-bar");
+            const { host, children, targets, nodeId } = createDOM("foo-bar");
             const behavior = new ChildrenDirective({
                 property: "nodes",
                 filter: elements("foo-bar"),
             });
-            behavior.targetId = targetId;
+            behavior.nodeId = nodeId;
             const model = new Model();
 
             behavior.bind(model, ExecutionContext.default, targets);
@@ -116,7 +116,7 @@ describe("The children", () => {
         });
 
         it("updates subtree nodes when they change with a selector", async () => {
-            const { host, children, targets, targetId } = createDOM("foo-bar");
+            const { host, children, targets, nodeId } = createDOM("foo-bar");
             const subtreeElement = "foo-bar-baz";
             const subtreeChildren: HTMLElement[] = [];
 
@@ -133,7 +133,7 @@ describe("The children", () => {
                 subtree: true,
                 selector: subtreeElement,
             });
-            behavior.targetId = targetId;
+            behavior.nodeId = nodeId;
 
             const model = new Model();
 
@@ -157,11 +157,11 @@ describe("The children", () => {
         });
 
         it("clears and unwatches when unbound", async () => {
-            const { host, children, targets, targetId } = createDOM("foo-bar");
+            const { host, children, targets, nodeId } = createDOM("foo-bar");
             const behavior = new ChildrenDirective({
                 property: "nodes",
             });
-            behavior.targetId = targetId;
+            behavior.nodeId = nodeId;
             const model = new Model();
 
             behavior.bind(model, ExecutionContext.default, targets);
diff --git a/packages/web-components/fast-element/src/templating/children.ts b/packages/web-components/fast-element/src/templating/children.ts
index ba6d113bbbd..ea5767224e0 100644
--- a/packages/web-components/fast-element/src/templating/children.ts
+++ b/packages/web-components/fast-element/src/templating/children.ts
@@ -1,4 +1,5 @@
 import { isString } from "../interfaces.js";
+import { HTMLDirective } from "./html-directive.js";
 import { NodeBehaviorOptions, NodeObservationDirective } from "./node-observation.js";
 import type { CaptureType } from "./template.js";
 
@@ -59,8 +60,7 @@ export class ChildrenDirective extends NodeObservationDirective<
      */
     observe(target: any): void {
         const observer =
-            target[this.uniqueId] ??
-            (target[this.uniqueId] = new MutationObserver(this.handleEvent));
+            target[this.id] ?? (target[this.id] = new MutationObserver(this.handleEvent));
         observer.$fastTarget = target;
         observer.observe(target, this.options);
     }
@@ -70,7 +70,7 @@ export class ChildrenDirective extends NodeObservationDirective<
      * @param target - The target to unobserve.
      */
     disconnect(target: any): void {
-        const observer = target[this.uniqueId];
+        const observer = target[this.id];
         observer.$fastTarget = null;
         observer.disconnect();
     }
@@ -94,6 +94,8 @@ export class ChildrenDirective extends NodeObservationDirective<
     };
 }
 
+HTMLDirective.define(ChildrenDirective);
+
 /**
  * A directive that observes the `childNodes` of an element and updates a property
  * whenever they change.
diff --git a/packages/web-components/fast-element/src/templating/compiler.spec.ts b/packages/web-components/fast-element/src/templating/compiler.spec.ts
index 9ed9da516d1..ddc8dea5219 100644
--- a/packages/web-components/fast-element/src/templating/compiler.spec.ts
+++ b/packages/web-components/fast-element/src/templating/compiler.spec.ts
@@ -22,11 +22,24 @@ interface CompilationResultInternals {
 
 describe("The template compiler", () => {
     function compile(html: string, directives: HTMLDirective[]) {
-        return Compiler.compile(html, directives) as any as CompilationResultInternals;
+        const factories: Record<string, ViewBehaviorFactory> = Object.create(null);
+        const ids: string[] = [];
+        let nextId = -1;
+        const add = (factory: ViewBehaviorFactory): string => {
+            const id = `${++nextId}`;
+            ids.push(id);
+            factory.id = id;
+            factories[id] = factory;
+            return id;
+        };
+
+        directives.forEach(x => x.createHTML(add));
+
+        return Compiler.compile(html, factories) as any as CompilationResultInternals;
     }
 
     function inline(index: number) {
-        return Markup.interpolation(index);
+        return Markup.interpolation(`${index}`);
     }
 
     function binding(result = "result") {
@@ -176,7 +189,7 @@ describe("The template compiler", () => {
                     expect(length).to.equal(x.targetIds.length);
 
                     for (let i = 0; i < length; ++i) {
-                        expect(factories[i].targetId).to.equal(
+                        expect(factories[i].nodeId).to.equal(
                             x.targetIds[i]
                         );
                     }
@@ -344,7 +357,7 @@ describe("The template compiler", () => {
                     expect(length).to.equal(x.targetIds.length);
 
                     for (let i = 0; i < length; ++i) {
-                        expect(factories[i].targetId).to.equal(
+                        expect(factories[i].nodeId).to.equal(
                             x.targetIds[i]
                         );
                     }
diff --git a/packages/web-components/fast-element/src/templating/compiler.ts b/packages/web-components/fast-element/src/templating/compiler.ts
index c0eef87ec3a..da7fc347e56 100644
--- a/packages/web-components/fast-element/src/templating/compiler.ts
+++ b/packages/web-components/fast-element/src/templating/compiler.ts
@@ -2,12 +2,8 @@ import { isString, Message, TrustedTypesPolicy } from "../interfaces.js";
 import type { ExecutionContext } from "../observation/observable.js";
 import { FAST } from "../platform.js";
 import { Parser } from "./markup.js";
-import { bind, oneTime } from "./binding.js";
-import type {
-    AspectedHTMLDirective,
-    HTMLDirective,
-    ViewBehaviorFactory,
-} from "./html-directive.js";
+import { bind, HTMLBindingDirective, oneTime } from "./binding.js";
+import { Aspect, Aspected, ViewBehaviorFactory } from "./html-directive.js";
 import type { HTMLTemplateCompilationResult as TemplateCompilationResult } from "./template.js";
 import { HTMLView } from "./view.js";
 
@@ -32,27 +28,27 @@ class CompilationContext<
     TContext extends ExecutionContext<TParent> = ExecutionContext<TParent>
 > implements TemplateCompilationResult<TSource, TParent, TContext> {
     private proto: any = null;
-    private targetIds = new Set<string>();
+    private nodeIds = new Set<string>();
     private descriptors: PropertyDescriptorMap = {};
     public readonly factories: ViewBehaviorFactory[] = [];
 
     constructor(
         public readonly fragment: DocumentFragment,
-        public readonly directives: ReadonlyArray<HTMLDirective>
+        public readonly directives: Record<string, ViewBehaviorFactory>
     ) {}
 
     public addFactory(
         factory: ViewBehaviorFactory,
         parentId: string,
-        targetId: string,
+        nodeId: string,
         targetIndex: number
     ): void {
-        if (!this.targetIds.has(targetId)) {
-            this.targetIds.add(targetId);
-            this.addTargetDescriptor(parentId, targetId, targetIndex);
+        if (!this.nodeIds.has(nodeId)) {
+            this.nodeIds.add(nodeId);
+            this.addTargetDescriptor(parentId, nodeId, targetIndex);
         }
 
-        factory.targetId = targetId;
+        factory.nodeId = nodeId;
         this.factories.push(factory);
     }
 
@@ -108,7 +104,7 @@ class CompilationContext<
         targets.r = fragment;
         targets.h = hostBindingTarget ?? fragment;
 
-        for (const id of this.targetIds) {
+        for (const id of this.nodeIds) {
             targets[id]; // trigger locator
         }
 
@@ -131,12 +127,12 @@ function compileAttributes(
         const attr = attributes[i];
         const attrValue = attr.value;
         const parseResult = Parser.parse(attrValue, directives);
-        let result: HTMLDirective | null = null;
+        let result: ViewBehaviorFactory | null = null;
 
         if (parseResult === null) {
             if (includeBasicValues) {
-                result = bind(() => attrValue, oneTime) as AspectedHTMLDirective;
-                (result as AspectedHTMLDirective).captureSource(attr.name);
+                result = bind(() => attrValue, oneTime) as ViewBehaviorFactory;
+                Aspect.assign((result as any) as Aspected, attr.name);
             }
         } else {
             /* eslint-disable-next-line @typescript-eslint/no-use-before-define */
@@ -246,7 +242,7 @@ function compileNode(
     return next;
 }
 
-function isMarker(node: Node, directives: ReadonlyArray<HTMLDirective>): boolean {
+function isMarker(node: Node, directives: Record<string, ViewBehaviorFactory>): boolean {
     return (
         node &&
         node.nodeType == 8 &&
@@ -265,9 +261,9 @@ export type CompilationStrategy = (
      */
     html: string | HTMLTemplateElement,
     /**
-     * The directives used within the html that is being compiled.
+     * The behavior factories used within the html that is being compiled.
      */
-    directives: readonly HTMLDirective[]
+    factories: Record<string, ViewBehaviorFactory>
 ) => TemplateCompilationResult;
 
 const templateTag = "TEMPLATE";
@@ -313,7 +309,7 @@ export const Compiler = {
         TContext extends ExecutionContext<TParent> = ExecutionContext<TParent>
     >(
         html: string | HTMLTemplateElement,
-        directives: ReadonlyArray<HTMLDirective>
+        directives: Record<string, ViewBehaviorFactory>
     ): TemplateCompilationResult<TSource, TParent, TContext> {
         let template: HTMLTemplateElement;
 
@@ -347,7 +343,7 @@ export const Compiler = {
             // Or if there is only one node and a directive, it means the template's content
             // is *only* the directive. In that case, HTMLView.dispose() misses any nodes inserted by
             // the directive. Inserting a new node ensures proper disposal of nodes added by the directive.
-            (fragment.childNodes.length === 1 && directives.length)
+            (fragment.childNodes.length === 1 && Object.keys(directives).length > 0)
         ) {
             fragment.insertBefore(document.createComment(""), fragment.firstChild);
         }
@@ -372,20 +368,20 @@ export const Compiler = {
      * directives.
      * @returns A single inline directive that aggregates the behavior of all the parts.
      */
-    aggregate(parts: (string | HTMLDirective)[]): HTMLDirective {
+    aggregate(parts: (string | ViewBehaviorFactory)[]): ViewBehaviorFactory {
         if (parts.length === 1) {
-            return parts[0] as HTMLDirective;
+            return parts[0] as ViewBehaviorFactory;
         }
 
-        let source: string | undefined;
+        let sourceAspect: string | undefined;
         const partCount = parts.length;
-        const finalParts = parts.map((x: string | AspectedHTMLDirective) => {
+        const finalParts = parts.map((x: string | ViewBehaviorFactory) => {
             if (isString(x)) {
                 return (): string => x;
             }
 
-            source = x.source || source;
-            return x.binding!;
+            sourceAspect = ((x as any) as Aspected).sourceAspect || sourceAspect;
+            return ((x as any) as Aspected).binding!;
         });
 
         const binding = (scope: unknown, context: ExecutionContext): string => {
@@ -398,8 +394,8 @@ export const Compiler = {
             return output;
         };
 
-        const directive = bind(binding) as AspectedHTMLDirective;
-        directive.captureSource(source!);
+        const directive = bind(binding) as HTMLBindingDirective;
+        Aspect.assign(directive, sourceAspect!);
         return directive;
     },
 };
diff --git a/packages/web-components/fast-element/src/templating/html-directive.ts b/packages/web-components/fast-element/src/templating/html-directive.ts
index a79ee2f0482..918eb7e1b17 100644
--- a/packages/web-components/fast-element/src/templating/html-directive.ts
+++ b/packages/web-components/fast-element/src/templating/html-directive.ts
@@ -1,6 +1,8 @@
+import type { Constructable, Mutable } from "../interfaces.js";
 import type { Behavior } from "../observation/behavior.js";
 import type { Binding, ExecutionContext } from "../observation/observable.js";
-import { Markup, nextId } from "./markup.js";
+import { createTypeRegistry } from "../platform.js";
+import { Markup } from "./markup.js";
 
 /**
  * The target nodes available to a behavior.
@@ -46,130 +48,233 @@ export interface ViewBehavior<TSource = any, TParent = any> {
  * @public
  */
 export interface ViewBehaviorFactory {
+    /**
+     * The unique id of the factory.
+     */
+    id: string;
+
     /**
      * The structural id of the DOM node to which the created behavior will apply.
      */
-    targetId: string;
+    nodeId: string;
 
     /**
      * Creates a behavior.
-     * @param target - The targets available for behaviors to be attached to.
+     * @param targets - The targets available for behaviors to be attached to.
      */
     createBehavior(targets: ViewBehaviorTargets): Behavior | ViewBehavior;
 }
 
+/**
+ * Used to add behavior factories when constructing templates.
+ * @public
+ */
+export type AddViewBehaviorFactory = (factory: ViewBehaviorFactory) => string;
+
 /**
  * Instructs the template engine to apply behavior to a node.
  * @public
  */
-export abstract class HTMLDirective implements ViewBehaviorFactory {
+export interface HTMLDirective {
     /**
-     * The structural id of the directive based on the DOM node
-     * that it applies to.
+     * Creates HTML to be used within a template.
+     * @param add - Can be used to add  behavior factories to a template.
      */
-    public targetId: string = "h";
+    createHTML(add: AddViewBehaviorFactory): string;
+}
 
+/**
+ * Represents metadata configuration for an HTMLDirective.
+ * @public
+ */
+export interface PartialHTMLDirectiveDefinition {
     /**
-     * The unique id of the directive instance.
+     * Indicates whether the directive needs access to template contextual information
+     * such as the sourceAspect, targetAspect, and aspectType.
      */
-    public readonly uniqueId: string = nextId();
+    aspected?: boolean;
+}
 
+/**
+ * Defines metadata for an HTMLDirective.
+ * @public
+ */
+export interface HTMLDirectiveDefinition<
+    TType extends Constructable<HTMLDirective> = Constructable<HTMLDirective>
+> extends Required<PartialHTMLDirectiveDefinition> {
     /**
-     * Creates a placeholder string based on the directive's index within the template.
-     * @param index - The index of the directive within the template.
+     * The type that the definition provides metadata for.
      */
-    public abstract createPlaceholder(index: number): string;
+    readonly type: TType;
+}
 
-    /**
-     * Creates a behavior.
-     * @param targets - The targets available for behaviors to be attached to.
-     */
-    public abstract createBehavior(targets: ViewBehaviorTargets): Behavior | ViewBehavior;
+const registry = createTypeRegistry<HTMLDirectiveDefinition>();
+
+/**
+ * Instructs the template engine to apply behavior to a node.
+ * @public
+ */
+export const HTMLDirective = Object.freeze({
+    getForInstance: registry.getForInstance,
+    getByType: registry.getByType,
+    define<TType extends Constructable<HTMLDirective>>(
+        type: TType,
+        options?: PartialHTMLDirectiveDefinition
+    ): TType {
+        options = options || {};
+        (options as Mutable<HTMLDirectiveDefinition>).type = type;
+        registry.register(options as HTMLDirectiveDefinition);
+        return type;
+    },
+});
+
+/**
+ * Decorator: Defines an HTMLDirective.
+ * @param options - Provides options that specify the directives application.
+ * @public
+ */
+export function htmlDirective(options?: PartialHTMLDirectiveDefinition) {
+    /* eslint-disable-next-line @typescript-eslint/explicit-function-return-type */
+    return function (type: Constructable<HTMLDirective>) {
+        HTMLDirective.define(type, options);
+    };
 }
 
 /**
  * The type of HTML aspect to target.
  * @public
  */
-export enum Aspect {
+export const Aspect = Object.freeze({
+    /**
+     * Not aspected.
+     */
+    none: 0,
+
     /**
      * An attribute.
      */
-    attribute = 0,
+    attribute: 1,
+
     /**
      * A boolean attribute.
      */
-    booleanAttribute = 1,
+    booleanAttribute: 2,
+
     /**
      * A property.
      */
-    property = 2,
+    property: 3,
+
     /**
      * Content
      */
-    content = 3,
+    content: 4,
+
     /**
      * A token list.
      */
-    tokenList = 4,
+    tokenList: 5,
+
     /**
      * An event.
      */
-    event = 5,
-}
+    event: 6,
+
+    /**
+     *
+     * @param directive - The directive to assign the aspect to.
+     * @param value - The value to base the aspect determination on.
+     */
+    assign(directive: Aspected, value: string): void {
+        directive.sourceAspect = value;
+
+        if (!value) {
+            return;
+        }
+
+        switch (value[0]) {
+            case ":":
+                directive.targetAspect = value.substring(1);
+                switch (directive.targetAspect) {
+                    case "innerHTML":
+                        directive.aspectType = Aspect.property;
+                        break;
+                    case "classList":
+                        directive.aspectType = Aspect.tokenList;
+                        break;
+                    default:
+                        directive.aspectType = Aspect.property;
+                        break;
+                }
+                break;
+            case "?":
+                directive.targetAspect = value.substring(1);
+                directive.aspectType = Aspect.booleanAttribute;
+                break;
+            case "@":
+                directive.targetAspect = value.substring(1);
+                directive.aspectType = Aspect.event;
+                break;
+            default:
+                if (value === "class") {
+                    directive.targetAspect = "className";
+                    directive.aspectType = Aspect.property;
+                } else {
+                    directive.targetAspect = value;
+                    directive.aspectType = Aspect.attribute;
+                }
+                break;
+        }
+    },
+});
 
 /**
- * A {@link HTMLDirective} that targets a particular aspect
- * (attribute, property, event, etc.) of a node.
+ * Represents something that applies to a specific aspect of the DOM.
  * @public
  */
-export abstract class AspectedHTMLDirective extends HTMLDirective {
+export interface Aspected {
     /**
-     * The original source aspect exactly as represented in the HTML.
+     * The original source aspect exactly as represented in markup.
      */
-    abstract readonly source: string;
+    sourceAspect: string;
 
     /**
      * The evaluated target aspect, determined after processing the source.
      */
-    abstract readonly target: string;
+    targetAspect: string;
 
     /**
      * The type of aspect to target.
      */
-    abstract readonly aspect: Aspect;
+    aspectType: number;
 
     /**
-     * A binding to apply to the target, if applicable.
+     * A binding if one is associated with the aspect.
      */
-    abstract readonly binding?: Binding;
+    binding?: Binding;
+}
 
+/**
+ * A base class used for attribute directives that don't need internal state.
+ * @public
+ */
+export abstract class StatelessAttachedAttributeDirective<T>
+    implements HTMLDirective, ViewBehaviorFactory, ViewBehavior {
     /**
-     * Captures the original source aspect from HTML.
-     * @param source - The original source aspect.
+     * The unique id of the factory.
      */
-    abstract captureSource(source: string): void;
+    id: string;
 
     /**
-     * Creates a placeholder string based on the directive's index within the template.
-     * @param index - The index of the directive within the template.
+     * The structural id of the DOM node to which the created behavior will apply.
      */
-    public createPlaceholder: (index: number) => string = Markup.interpolation;
-}
+    nodeId: string;
 
-/**
- * A base class used for attribute directives that don't need internal state.
- * @public
- */
-export abstract class StatelessAttachedAttributeDirective<T> extends HTMLDirective
-    implements ViewBehavior {
     /**
      * Creates an instance of RefDirective.
      * @param options - The options to use in configuring the directive.
      */
-    public constructor(protected options: T) {
-        super();
-    }
+    public constructor(protected options: T) {}
 
     /**
      * Creates a behavior.
@@ -185,7 +290,9 @@ export abstract class StatelessAttachedAttributeDirective<T> extends HTMLDirecti
      * @remarks
      * Creates a custom attribute placeholder.
      */
-    public createPlaceholder: (index: number) => string = Markup.attribute;
+    public createHTML(add: AddViewBehaviorFactory): string {
+        return Markup.attribute(add(this));
+    }
 
     /**
      * Bind this behavior to the source.
diff --git a/packages/web-components/fast-element/src/templating/markup.ts b/packages/web-components/fast-element/src/templating/markup.ts
index 71a5b2148ac..34385a9c041 100644
--- a/packages/web-components/fast-element/src/templating/markup.ts
+++ b/packages/web-components/fast-element/src/templating/markup.ts
@@ -1,4 +1,4 @@
-import type { HTMLDirective } from "./html-directive.js";
+import type { ViewBehaviorFactory } from "./html-directive.js";
 
 const marker = `fast-${Math.random().toString(36).substring(2, 8)}`;
 const interpolationStart = `${marker}{`;
@@ -22,7 +22,7 @@ export const Markup = Object.freeze({
      * @remarks
      * Used internally by binding directives.
      */
-    interpolation: (index: number) => `${interpolationStart}${index}${interpolationEnd}`,
+    interpolation: (id: string) => `${interpolationStart}${id}${interpolationEnd}`,
 
     /**
      * Creates a placeholder that manifests itself as an attribute on an
@@ -32,8 +32,8 @@ export const Markup = Object.freeze({
      * @remarks
      * Used internally by attribute directives such as `ref`, `slotted`, and `children`.
      */
-    attribute: (index: number) =>
-        `${nextId()}="${interpolationStart}${index}${interpolationEnd}"`,
+    attribute: (id: string) =>
+        `${nextId()}="${interpolationStart}${id}${interpolationEnd}"`,
 
     /**
      * Creates a placeholder that manifests itself as a marker within the DOM structure.
@@ -41,7 +41,7 @@ export const Markup = Object.freeze({
      * @remarks
      * Used internally by structural directives such as `repeat`.
      */
-    comment: (index: number) => `<!--${interpolationStart}${index}${interpolationEnd}-->`,
+    comment: (id: string) => `<!--${interpolationStart}${id}${interpolationEnd}-->`,
 });
 
 /**
@@ -53,32 +53,32 @@ export const Parser = Object.freeze({
      * Parses text content or HTML attribute content, separating out the static strings
      * from the directives.
      * @param value - The content or attribute string to parse.
-     * @param directives - A list of directives to search for in the string.
+     * @param factories - A list of directives to search for in the string.
      * @returns A heterogeneous array of static strings interspersed with
      * directives or null if no directives are found in the string.
      */
     parse(
         value: string,
-        directives: readonly HTMLDirective[]
-    ): (string | HTMLDirective)[] | null {
+        factories: Record<string, ViewBehaviorFactory>
+    ): (string | ViewBehaviorFactory)[] | null {
         const parts = value.split(interpolationStart);
 
         if (parts.length === 1) {
             return null;
         }
 
-        const result: (string | HTMLDirective)[] = [];
+        const result: (string | ViewBehaviorFactory)[] = [];
 
         for (let i = 0, ii = parts.length; i < ii; ++i) {
             const current = parts[i];
             const index = current.indexOf(interpolationEnd);
-            let literal: string | HTMLDirective;
+            let literal: string | ViewBehaviorFactory;
 
             if (index === -1) {
                 literal = current;
             } else {
-                const directiveIndex = parseInt(current.substring(0, index));
-                result.push(directives[directiveIndex]);
+                const factoryId = current.substring(0, index);
+                result.push(factories[factoryId]);
                 literal = current.substring(index + interpolationEndLength);
             }
 
diff --git a/packages/web-components/fast-element/src/templating/node-observation.ts b/packages/web-components/fast-element/src/templating/node-observation.ts
index 189e4f56107..7951388f783 100644
--- a/packages/web-components/fast-element/src/templating/node-observation.ts
+++ b/packages/web-components/fast-element/src/templating/node-observation.ts
@@ -58,7 +58,7 @@ export abstract class NodeObservationDirective<
      * @param targets - The targets that behaviors in a view can attach to.
      */
     bind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void {
-        const target = targets[this.targetId] as any;
+        const target = targets[this.nodeId] as any;
         target.$fastSource = source;
         this.updateTarget(source, this.computeNodes(target));
         this.observe(target);
@@ -71,7 +71,7 @@ export abstract class NodeObservationDirective<
      * @param targets - The targets that behaviors in a view can attach to.
      */
     unbind(source: any, context: ExecutionContext, targets: ViewBehaviorTargets): void {
-        const target = targets[this.targetId] as any;
+        const target = targets[this.nodeId] as any;
         this.updateTarget(source, emptyArray);
         this.disconnect(target);
         target.$fastSource = null;
diff --git a/packages/web-components/fast-element/src/templating/ref.ts b/packages/web-components/fast-element/src/templating/ref.ts
index af0b647077a..9bb6d5ccdc0 100644
--- a/packages/web-components/fast-element/src/templating/ref.ts
+++ b/packages/web-components/fast-element/src/templating/ref.ts
@@ -1,5 +1,6 @@
 import type { ExecutionContext } from "../observation/observable.js";
 import {
+    HTMLDirective,
     StatelessAttachedAttributeDirective,
     ViewBehaviorTargets,
 } from "./html-directive.js";
@@ -21,7 +22,7 @@ export class RefDirective extends StatelessAttachedAttributeDirective<string> {
         context: ExecutionContext,
         targets: ViewBehaviorTargets
     ): void {
-        source[this.options] = targets[this.targetId];
+        source[this.options] = targets[this.nodeId];
     }
 
     /**
@@ -32,6 +33,8 @@ export class RefDirective extends StatelessAttachedAttributeDirective<string> {
     public unbind(): void {}
 }
 
+HTMLDirective.define(RefDirective);
+
 /**
  * A directive that observes the updates a property with a reference to the element.
  * @param propertyName - The name of the property to assign the reference to.
diff --git a/packages/web-components/fast-element/src/templating/repeat.spec.ts b/packages/web-components/fast-element/src/templating/repeat.spec.ts
index fd0a11afe60..653b33f930e 100644
--- a/packages/web-components/fast-element/src/templating/repeat.spec.ts
+++ b/packages/web-components/fast-element/src/templating/repeat.spec.ts
@@ -9,12 +9,12 @@ describe("The repeat", () => {
     function createLocation() {
         const parent = document.createElement("div");
         const location = document.createComment("");
-        const targetId = 'r';
-        const targets = { [targetId]: location };
+        const nodeId = 'r';
+        const targets = { [nodeId]: location };
 
         parent.appendChild(location);
 
-        return { parent, targets, targetId };
+        return { parent, targets, nodeId };
     }
 
     context("template function", () => {
@@ -29,12 +29,12 @@ describe("The repeat", () => {
 
     context("directive", () => {
         it("creates a RepeatBehavior", () => {
-            const { parent, targets, targetId } = createLocation();
+            const { targets, nodeId } = createLocation();
             const directive = repeat(
                 () => [],
                 html`test`
             ) as RepeatDirective;
-            directive.targetId = targetId;
+            directive.nodeId = nodeId;
 
             const behavior = directive.createBehavior(targets);
 
@@ -97,12 +97,12 @@ describe("The repeat", () => {
 
         zeroThroughTen.forEach(size => {
             it(`renders a template for each item in array of size ${size}`, () => {
-                const { parent, targets, targetId } = createLocation();
+                const { parent, targets, nodeId } = createLocation();
                 const directive = repeat<ViewModel>(
                     x => x.items,
                     itemTemplate
                 ) as RepeatDirective;
-                directive.targetId = targetId;
+                directive.nodeId = nodeId;
 
                 const behavior = directive.createBehavior(targets);
                 const vm = new ViewModel(size);
@@ -115,12 +115,12 @@ describe("The repeat", () => {
 
         zeroThroughTen.forEach(size => {
             it(`renders empty when an array of size ${size} is replaced with an empty array`, async () => {
-                const { parent, targets, targetId } = createLocation();
+                const { parent, targets, nodeId } = createLocation();
                 const directive = repeat<ViewModel>(
                     x => x.items,
                     wrappedItemTemplate
                 ) as RepeatDirective;
-                directive.targetId = targetId;
+                directive.nodeId = nodeId;
                 const behavior = directive.createBehavior(targets);
                 const data = new ViewModel(size);
 
@@ -148,12 +148,12 @@ describe("The repeat", () => {
 
         zeroThroughTen.forEach(size => {
             it(`updates rendered HTML when a new item is pushed into an array of size ${size}`, async () => {
-                const { parent, targets, targetId } = createLocation();
+                const { parent, targets, nodeId } = createLocation();
                 const directive = repeat<ViewModel>(
                     x => x.items,
                     itemTemplate
                 ) as RepeatDirective;
-                directive.targetId = targetId;
+                directive.nodeId = nodeId;
                 const behavior = directive.createBehavior(targets);
                 const vm = new ViewModel(size);
 
@@ -168,12 +168,12 @@ describe("The repeat", () => {
 
         oneThroughTen.forEach(size => {
             it(`updates rendered HTML when a single item is spliced from the end of an array of size ${size}`, async () => {
-                const { parent, targets, targetId } = createLocation();
+                const { parent, targets, nodeId } = createLocation();
                 const directive = repeat<ViewModel>(
                     x => x.items,
                     itemTemplate
                 ) as RepeatDirective;
-                directive.targetId = targetId;
+                directive.nodeId = nodeId;
                 const behavior = directive.createBehavior(targets);
                 const vm = new ViewModel(size);
 
@@ -192,12 +192,12 @@ describe("The repeat", () => {
 
         oneThroughTen.forEach(size => {
             it(`updates rendered HTML when a single item is spliced from the beginning of an array of size ${size}`, async () => {
-                const { parent, targets, targetId } = createLocation();
+                const { parent, targets, nodeId } = createLocation();
                 const directive = repeat<ViewModel>(
                     x => x.items,
                     itemTemplate
                 ) as RepeatDirective;
-                directive.targetId = targetId;
+                directive.nodeId = nodeId;
                 const behavior = directive.createBehavior(targets);
                 const vm = new ViewModel(size);
 
@@ -213,12 +213,12 @@ describe("The repeat", () => {
 
         oneThroughTen.forEach(size => {
             it(`updates rendered HTML when a single item is replaced from the end of an array of size ${size}`, async () => {
-                const { parent, targets, targetId } = createLocation();
+                const { parent, targets, nodeId } = createLocation();
                 const directive = repeat<ViewModel>(
                     x => x.items,
                     itemTemplate
                 ) as RepeatDirective;
-                directive.targetId = targetId;
+                directive.nodeId = nodeId;
                 const behavior = directive.createBehavior(targets);
                 const vm = new ViewModel(size);
 
@@ -237,12 +237,12 @@ describe("The repeat", () => {
 
         oneThroughTen.forEach(size => {
             it(`updates rendered HTML when a single item is replaced from the beginning of an array of size ${size}`, async () => {
-                const { parent, targets, targetId } = createLocation();
+                const { parent, targets, nodeId } = createLocation();
                 const directive = repeat<ViewModel>(
                     x => x.items,
                     itemTemplate
                 ) as RepeatDirective;
-                directive.targetId = targetId;
+                directive.nodeId = nodeId;
                 const behavior = directive.createBehavior(targets);
                 const vm = new ViewModel(size);
 
@@ -260,12 +260,12 @@ describe("The repeat", () => {
 
         oneThroughTen.forEach(size => {
             it(`updates all when the template changes for an array of size ${size}`, async () => {
-                const { parent, targets, targetId } = createLocation();
+                const { parent, targets, nodeId } = createLocation();
                 const directive = repeat<ViewModel>(
                     x => x.items,
                     x => vm.template
                 ) as RepeatDirective;
-                directive.targetId = targetId;
+                directive.nodeId = nodeId;
                 const behavior = directive.createBehavior(targets);
                 const vm = new ViewModel(size);
 
@@ -290,12 +290,12 @@ describe("The repeat", () => {
                     )}
                 `;
 
-                const { parent, targets, targetId } = createLocation();
+                const { parent, targets, nodeId } = createLocation();
                 const directive = repeat<ViewModel>(
                     x => x.items,
                     deepItemTemplate
                 ) as RepeatDirective;
-                directive.targetId = targetId;
+                directive.nodeId = nodeId;
                 const behavior = directive.createBehavior(targets);
                 const vm = new ViewModel(size, true);
 
@@ -312,12 +312,12 @@ describe("The repeat", () => {
 
         oneThroughTen.forEach(size => {
             it(`handles back to back shift operations for arrays of size ${size}`, async () => {
-                const { parent, targets, targetId } = createLocation();
+                const { parent, targets, nodeId } = createLocation();
                 const directive = repeat<ViewModel>(
                     x => x.items,
                     itemTemplate
                 ) as RepeatDirective;
-                directive.targetId = targetId;
+                directive.nodeId = nodeId;
                 const behavior = directive.createBehavior(targets);
                 const vm = new ViewModel(size);
 
@@ -336,12 +336,12 @@ describe("The repeat", () => {
 
         zeroThroughTen.forEach(size => {
             it(`updates rendered HTML when a new item is pushed into an array of size ${size} after it has been unbound and rebound`, async () => {
-                const { parent, targets, targetId } = createLocation();
+                const { parent, targets, nodeId } = createLocation();
                 const directive = repeat<ViewModel>(
                     x => x.items,
                     itemTemplate
                 ) as RepeatDirective;
-                directive.targetId = targetId;
+                directive.nodeId = nodeId;
                 const behavior = directive.createBehavior(targets);
                 const vm = new ViewModel(size);
 
diff --git a/packages/web-components/fast-element/src/templating/repeat.ts b/packages/web-components/fast-element/src/templating/repeat.ts
index 749cd6f22a4..ab2bcb349ee 100644
--- a/packages/web-components/fast-element/src/templating/repeat.ts
+++ b/packages/web-components/fast-element/src/templating/repeat.ts
@@ -14,7 +14,12 @@ import {
 } from "../observation/observable.js";
 import { emptyArray } from "../platform.js";
 import { Markup } from "./markup.js";
-import { HTMLDirective, ViewBehaviorTargets } from "./html-directive.js";
+import {
+    AddViewBehaviorFactory,
+    HTMLDirective,
+    ViewBehaviorFactory,
+    ViewBehaviorTargets,
+} from "./html-directive.js";
 import type {
     CaptureType,
     ChildViewTemplate,
@@ -294,15 +299,28 @@ export class RepeatBehavior<TSource = any> implements Behavior, Subscriber {
  * A directive that configures list rendering.
  * @public
  */
-export class RepeatDirective<TSource = any> extends HTMLDirective {
+export class RepeatDirective<TSource = any>
+    implements HTMLDirective, ViewBehaviorFactory {
     private isItemsBindingVolatile: boolean;
     private isTemplateBindingVolatile: boolean;
 
+    /**
+     * The unique id of the factory.
+     */
+    id: string;
+
+    /**
+     * The structural id of the DOM node to which the created behavior will apply.
+     */
+    nodeId: string;
+
     /**
      * Creates a placeholder string based on the directive's index within the template.
      * @param index - The index of the directive within the template.
      */
-    public createPlaceholder: (index: number) => string = Markup.comment;
+    public createHTML(add: AddViewBehaviorFactory): string {
+        return Markup.comment(add(this));
+    }
 
     /**
      * Creates an instance of RepeatDirective.
@@ -315,7 +333,6 @@ export class RepeatDirective<TSource = any> extends HTMLDirective {
         public readonly templateBinding: Binding<TSource, SyntheticViewTemplate>,
         public readonly options: RepeatOptions
     ) {
-        super();
         enableArrayObservation();
         this.isItemsBindingVolatile = Observable.isVolatileBinding(itemsBinding);
         this.isTemplateBindingVolatile = Observable.isVolatileBinding(templateBinding);
@@ -327,7 +344,7 @@ export class RepeatDirective<TSource = any> extends HTMLDirective {
      */
     public createBehavior(targets: ViewBehaviorTargets): RepeatBehavior<TSource> {
         return new RepeatBehavior<TSource>(
-            targets[this.targetId],
+            targets[this.nodeId],
             this.itemsBinding,
             this.isItemsBindingVolatile,
             this.templateBinding,
@@ -337,6 +354,8 @@ export class RepeatDirective<TSource = any> extends HTMLDirective {
     }
 }
 
+HTMLDirective.define(RepeatDirective);
+
 /**
  * A directive that enables list rendering.
  * @param itemsBinding - The array to render.
diff --git a/packages/web-components/fast-element/src/templating/slotted.spec.ts b/packages/web-components/fast-element/src/templating/slotted.spec.ts
index 89149e53c73..ccdec09d260 100644
--- a/packages/web-components/fast-element/src/templating/slotted.spec.ts
+++ b/packages/web-components/fast-element/src/templating/slotted.spec.ts
@@ -14,11 +14,11 @@ describe("The slotted", () => {
 
     context("directive", () => {
         it("creates a behavior by returning itself", () => {
-            const targetId = 'r';
+            const nodeId = 'r';
             const directive = slotted("test") as SlottedDirective;
-            directive.targetId = targetId;
+            directive.nodeId = nodeId;
             const target = document.createElement("slot");
-            const targets = { [targetId]: target }
+            const targets = { [nodeId]: target }
             const behavior = directive.createBehavior(targets);
 
             expect(behavior).to.equal(directive);
@@ -47,18 +47,18 @@ describe("The slotted", () => {
             const slot = document.createElement("slot");
             const shadowRoot = host.attachShadow({ mode: "open" });
             const children = createAndAppendChildren(host, elementName);
-            const targetId = 'r';
-            const targets = { [targetId]: slot };
+            const nodeId = 'r';
+            const targets = { [nodeId]: slot };
 
             shadowRoot.appendChild(slot);
 
-            return { host, slot, children, targets, targetId };
+            return { host, slot, children, targets, nodeId };
         }
 
         it("gathers nodes from a slot", () => {
-            const { children, targets, targetId } = createDOM();
+            const { children, targets, nodeId } = createDOM();
             const behavior = new SlottedDirective({ property: "nodes" });
-            behavior.targetId = targetId;
+            behavior.nodeId = nodeId;
             const model = new Model();
 
             behavior.bind(model, ExecutionContext.default, targets);
@@ -67,12 +67,12 @@ describe("The slotted", () => {
         });
 
         it("gathers nodes from a slot with a filter", () => {
-            const { targets, targetId, children } = createDOM("foo-bar");
+            const { targets, nodeId, children } = createDOM("foo-bar");
             const behavior = new SlottedDirective({
                 property: "nodes",
                 filter: elements("foo-bar"),
             });
-            behavior.targetId = targetId;
+            behavior.nodeId = nodeId;
             const model = new Model();
 
             behavior.bind(model, ExecutionContext.default, targets);
@@ -81,9 +81,9 @@ describe("The slotted", () => {
         });
 
         it("updates when slotted nodes change", async () => {
-            const { host, slot, children, targets, targetId } = createDOM("foo-bar");
+            const { host, slot, children, targets, nodeId } = createDOM("foo-bar");
             const behavior = new SlottedDirective({ property: "nodes" });
-            behavior.targetId = targetId;
+            behavior.nodeId = nodeId;
             const model = new Model();
 
             behavior.bind(model, ExecutionContext.default, targets);
@@ -98,12 +98,12 @@ describe("The slotted", () => {
         });
 
         it("updates when slotted nodes change with a filter", async () => {
-            const { host, slot, children, targets, targetId } = createDOM("foo-bar");
+            const { host, slot, children, targets, nodeId } = createDOM("foo-bar");
             const behavior = new SlottedDirective({
                 property: "nodes",
                 filter: elements("foo-bar"),
             });
-            behavior.targetId = targetId;
+            behavior.nodeId = nodeId;
             const model = new Model();
 
             behavior.bind(model, ExecutionContext.default, targets);
@@ -118,9 +118,9 @@ describe("The slotted", () => {
         });
 
         it("clears and unwatches when unbound", async () => {
-            const { host, slot, children, targets, targetId } = createDOM("foo-bar");
+            const { host, slot, children, targets, nodeId } = createDOM("foo-bar");
             const behavior = new SlottedDirective({ property: "nodes" });
-            behavior.targetId = targetId;
+            behavior.nodeId = nodeId;
             const model = new Model();
 
             behavior.bind(model, ExecutionContext.default, targets);
diff --git a/packages/web-components/fast-element/src/templating/slotted.ts b/packages/web-components/fast-element/src/templating/slotted.ts
index 9c2a697d749..14436f36829 100644
--- a/packages/web-components/fast-element/src/templating/slotted.ts
+++ b/packages/web-components/fast-element/src/templating/slotted.ts
@@ -1,4 +1,5 @@
 import { isString } from "../interfaces.js";
+import { HTMLDirective } from "./html-directive.js";
 import { NodeBehaviorOptions, NodeObservationDirective } from "./node-observation.js";
 import type { CaptureType } from "./template.js";
 
@@ -49,6 +50,8 @@ export class SlottedDirective extends NodeObservationDirective<SlottedDirectiveO
     }
 }
 
+HTMLDirective.define(SlottedDirective);
+
 /**
  * A directive that observes the `assignedNodes()` of a slot and updates a property
  * whenever they change.
diff --git a/packages/web-components/fast-element/src/templating/template.spec.ts b/packages/web-components/fast-element/src/templating/template.spec.ts
index 3a950fbc214..ac766ddaaff 100644
--- a/packages/web-components/fast-element/src/templating/template.spec.ts
+++ b/packages/web-components/fast-element/src/templating/template.spec.ts
@@ -1,9 +1,11 @@
 import { expect } from "chai";
 import { html, ViewTemplate } from "./template";
-import { Markup } from "./markup";
+import { Markup, Parser } from "./markup";
 import { HTMLBindingDirective } from "./binding";
-import { AspectedHTMLDirective, Aspect, HTMLDirective } from "./html-directive";
-import { bind, Binding, ViewBehaviorTargets } from "..";
+import { Aspect, HTMLDirective, ViewBehaviorFactory, Aspected, htmlDirective, AddViewBehaviorFactory } from "./html-directive";
+import { bind, ViewBehaviorTargets } from "..";
+import { Constructable, isString } from "../interfaces";
+import { ExecutionContext } from "../observation/observable";
 
 describe(`The html tag template helper`, () => {
     it(`transforms a string into a ViewTemplate.`, () => {
@@ -11,13 +13,17 @@ describe(`The html tag template helper`, () => {
         expect(template).instanceOf(ViewTemplate);
     });
 
-    class TestDirective extends HTMLDirective {
+    @htmlDirective()
+    class TestDirective implements HTMLDirective, ViewBehaviorFactory {
+        id: string;
+        nodeId: string;
+
         createBehavior() {
             return {} as any;
         }
 
-        createPlaceholder(index: number) {
-            return Markup.comment(index);
+        createHTML(add: AddViewBehaviorFactory) {
+            return Markup.comment(add(this));
         }
     }
 
@@ -26,6 +32,28 @@ describe(`The html tag template helper`, () => {
         doSomething() {}
     }
 
+    const FAKE = {
+        comment: Markup.comment("0"),
+        interpolation: Markup.interpolation("0")
+    };
+
+    function expectTemplateEquals(template: ViewTemplate, expectedHTML: string) {
+        expect(template).instanceOf(ViewTemplate);
+
+        const parts = Parser.parse(template.html as string, {})!;
+
+        if (parts !== null) {
+            const result = parts.reduce((a, b) => isString(b)
+                ? a + b
+                : a + Markup.interpolation("0")
+            , "");
+
+            expect(result).to.equal(expectedHTML);
+        } else {
+            expect(template.html).to.equal(expectedHTML);
+        }
+    }
+
     const stringValue = "string value";
     const numberValue = 42;
     const interpolationScenarios = [
@@ -53,40 +81,40 @@ describe(`The html tag template helper`, () => {
             type: "number",
             location: "at the beginning",
             template: html`${numberValue} end`,
-            result: `${Markup.interpolation(0)} end`,
+            result: `${FAKE.interpolation} end`,
         },
         {
             type: "number",
             location: "in the middle",
             template: html`beginning ${numberValue} end`,
-            result: `beginning ${Markup.interpolation(0)} end`,
+            result: `beginning ${FAKE.interpolation} end`,
         },
         {
             type: "number",
             location: "at the end",
             template: html`beginning ${numberValue}`,
-            result: `beginning ${Markup.interpolation(0)}`,
+            result: `beginning ${FAKE.interpolation}`,
         },
         // expression interpolation
         {
             type: "expression",
             location: "at the beginning",
             template: html<Model>`${x => x.value} end`,
-            result: `${Markup.interpolation(0)} end`,
+            result: `${FAKE.interpolation} end`,
             expectDirectives: [HTMLBindingDirective],
         },
         {
             type: "expression",
             location: "in the middle",
             template: html<Model>`beginning ${x => x.value} end`,
-            result: `beginning ${Markup.interpolation(0)} end`,
+            result: `beginning ${FAKE.interpolation} end`,
             expectDirectives: [HTMLBindingDirective],
         },
         {
             type: "expression",
             location: "at the end",
             template: html<Model>`beginning ${x => x.value}`,
-            result: `beginning ${Markup.interpolation(0)}`,
+            result: `beginning ${FAKE.interpolation}`,
             expectDirectives: [HTMLBindingDirective],
         },
         // directive interpolation
@@ -94,21 +122,21 @@ describe(`The html tag template helper`, () => {
             type: "directive",
             location: "at the beginning",
             template: html`${new TestDirective()} end`,
-            result: `${Markup.comment(0)} end`,
+            result: `${FAKE.comment} end`,
             expectDirectives: [TestDirective],
         },
         {
             type: "directive",
             location: "in the middle",
             template: html`beginning ${new TestDirective()} end`,
-            result: `beginning ${Markup.comment(0)} end`,
+            result: `beginning ${FAKE.comment} end`,
             expectDirectives: [TestDirective],
         },
         {
             type: "directive",
             location: "at the end",
             template: html`beginning ${new TestDirective()}`,
-            result: `beginning ${Markup.comment(0)}`,
+            result: `beginning ${FAKE.comment}`,
             expectDirectives: [TestDirective],
         },
         // template interpolation
@@ -116,21 +144,21 @@ describe(`The html tag template helper`, () => {
             type: "template",
             location: "at the beginning",
             template: html`${html`sub-template`} end`,
-            result: `${Markup.interpolation(0)} end`,
+            result: `${FAKE.interpolation} end`,
             expectDirectives: [HTMLBindingDirective],
         },
         {
             type: "template",
             location: "in the middle",
             template: html`beginning ${html`sub-template`} end`,
-            result: `beginning ${Markup.interpolation(0)} end`,
+            result: `beginning ${FAKE.interpolation} end`,
             expectDirectives: [HTMLBindingDirective],
         },
         {
             type: "template",
             location: "at the end",
             template: html`beginning ${html`sub-template`}`,
-            result: `beginning ${Markup.interpolation(0)}`,
+            result: `beginning ${FAKE.interpolation}`,
             expectDirectives: [HTMLBindingDirective],
         },
         // mixed interpolation
@@ -138,33 +166,21 @@ describe(`The html tag template helper`, () => {
             type: "mixed, back-to-back string, number, expression, and directive",
             location: "at the beginning",
             template: html<Model>`${stringValue}${numberValue}${x => x.value}${new TestDirective()} end`,
-            result: `${stringValue}${Markup.interpolation(
-                0
-            )}${Markup.interpolation(
-                1
-            )}${Markup.comment(2)} end`,
+            result: `${stringValue}${FAKE.interpolation}${FAKE.interpolation}${FAKE.comment} end`,
             expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective],
         },
         {
             type: "mixed, back-to-back string, number, expression, and directive",
             location: "in the middle",
             template: html<Model>`beginning ${stringValue}${numberValue}${x => x.value}${new TestDirective()} end`,
-            result: `beginning ${stringValue}${Markup.interpolation(
-                0
-            )}${Markup.interpolation(
-                1
-            )}${Markup.comment(2)} end`,
+            result: `beginning ${stringValue}${FAKE.interpolation}${FAKE.interpolation}${FAKE.comment} end`,
             expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective],
         },
         {
             type: "mixed, back-to-back string, number, expression, and directive",
             location: "at the end",
             template: html<Model>`beginning ${stringValue}${numberValue}${x => x.value}${new TestDirective()}`,
-            result: `beginning ${stringValue}${Markup.interpolation(
-                0
-            )}${Markup.interpolation(
-                1
-            )}${Markup.comment(2)}`,
+            result: `beginning ${stringValue}${FAKE.interpolation}${FAKE.interpolation}${FAKE.comment}`,
             expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective],
         },
         {
@@ -172,11 +188,7 @@ describe(`The html tag template helper`, () => {
             location: "at the beginning",
             template: html<Model>`${stringValue}separator${numberValue}separator${x =>
                     x.value}separator${new TestDirective()} end`,
-            result: `${stringValue}separator${Markup.interpolation(
-                0
-            )}separator${Markup.interpolation(
-                1
-            )}separator${Markup.comment(2)} end`,
+            result: `${stringValue}separator${FAKE.interpolation}separator${FAKE.interpolation}separator${FAKE.comment} end`,
             expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective],
         },
         {
@@ -184,11 +196,7 @@ describe(`The html tag template helper`, () => {
             location: "in the middle",
             template: html<Model>`beginning ${stringValue}separator${numberValue}separator${x =>
                     x.value}separator${new TestDirective()} end`,
-            result: `beginning ${stringValue}separator${Markup.interpolation(
-                0
-            )}separator${Markup.interpolation(
-                1
-            )}separator${Markup.comment(2)} end`,
+            result: `beginning ${stringValue}separator${FAKE.interpolation}separator${FAKE.interpolation}separator${FAKE.comment} end`,
             expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective],
         },
         {
@@ -196,164 +204,319 @@ describe(`The html tag template helper`, () => {
             location: "at the end",
             template: html<Model>`beginning ${stringValue}separator${numberValue}separator${x =>
                     x.value}separator${new TestDirective()}`,
-            result: `beginning ${stringValue}separator${Markup.interpolation(
-                0
-            )}separator${Markup.interpolation(
-                1
-            )}separator${Markup.comment(2)}`,
+            result: `beginning ${stringValue}separator${FAKE.interpolation}separator${FAKE.interpolation}separator${FAKE.comment}`,
             expectDirectives: [HTMLBindingDirective, HTMLBindingDirective, TestDirective],
         },
     ];
 
     interpolationScenarios.forEach(x => {
         it(`inserts ${x.type} values ${x.location} of the html`, () => {
-            expect(x.template).instanceOf(ViewTemplate);
-            expect(x.template.html).to.equal(x.result);
+            expectTemplateEquals(x.template, x.result);
 
             if (x.expectDirectives) {
-                x.expectDirectives.forEach((type, index) => {
-                    const directive = x.template.directives[index];
+                x.expectDirectives.forEach(type => {
+                    let found = false;
 
-                    expect(directive).to.be.instanceOf(type);
+                    for (const id in x.template.factories) {
+                        const behaviorFactory = x.template.factories[id];
 
-                    if (directive instanceof HTMLBindingDirective) {
-                        expect(directive.aspect).to.equal(Aspect.content);
+                        if (behaviorFactory instanceof type) {
+                            found = true;
+
+                            if (behaviorFactory instanceof HTMLBindingDirective) {
+                                expect(behaviorFactory.aspectType).to.equal(Aspect.content);
+                            }
+                        }
+
+                        expect(behaviorFactory.id).equals(id);
                     }
+
+                    expect(found).to.be.true;
                 });
             }
         });
     });
 
+    function getFactory<T extends Constructable<ViewBehaviorFactory>>(
+        template: ViewTemplate,
+        type: T
+    ): InstanceType<T> | null {
+        for (const id in template.factories) {
+            const potential = template.factories[id];
+
+            if (potential instanceof type) {
+                return potential as any as InstanceType<T>;
+            }
+        }
+
+        return null;
+    }
+
+    function expectAspect<T extends Constructable<ViewBehaviorFactory>>(
+        template: ViewTemplate,
+        type: T,
+        sourceAspect: string,
+        targetAspect: string,
+        aspectType: number
+    ) {
+        const factory = getFactory(template, type) as ViewBehaviorFactory & Aspected;
+        expect(factory!).to.be.instanceOf(type);
+        expect(factory!.sourceAspect).to.equal(sourceAspect);
+        expect(factory!.targetAspect).to.equal(targetAspect);
+        expect(factory!.aspectType).to.equal(aspectType);
+    }
+
     it(`captures an attribute with an expression`, () => {
         const template = html<Model>`<my-element some-attribute=${x => x.value}></my-element>`;
-        const placeholder = Markup.interpolation(0);
-        const directive = template.directives[0] as HTMLBindingDirective;
 
-        expect(template.html).to.equal(
-            `<my-element some-attribute=${placeholder}></my-element>`
+        expectTemplateEquals(
+            template,
+            `<my-element some-attribute=${FAKE.interpolation}></my-element>`
         );
 
-        expect(directive).to.be.instanceOf(HTMLBindingDirective);
-        expect(directive.source).to.equal("some-attribute");
-        expect(directive.target).to.equal("some-attribute");
-        expect(directive.aspect).to.equal(Aspect.attribute);
+        expectAspect(
+            template,
+            HTMLBindingDirective,
+            "some-attribute",
+            "some-attribute",
+            Aspect.attribute
+        );
     });
 
     it(`captures an attribute with a binding`, () => {
         const template = html<Model>`<my-element some-attribute=${bind(x => x.value)}></my-element>`;
-        const placeholder = Markup.interpolation(0);
-        const directive = template.directives[0] as HTMLBindingDirective;
 
-        expect(template.html).to.equal(
-            `<my-element some-attribute=${placeholder}></my-element>`
+        expectTemplateEquals(
+            template,
+            `<my-element some-attribute=${FAKE.interpolation}></my-element>`
+        );
+
+        expectAspect(
+            template,
+            HTMLBindingDirective,
+            "some-attribute",
+            "some-attribute",
+            Aspect.attribute
+        );
+    });
+
+    it(`captures an attribute with an interpolated string`, () => {
+        const template = html<Model>`<my-element some-attribute=${stringValue}></my-element>`;
+
+        expectTemplateEquals(
+            template,
+            `<my-element some-attribute=${FAKE.interpolation}></my-element>`
+        );
+
+        expectAspect(
+            template,
+            HTMLBindingDirective,
+            "some-attribute",
+            "some-attribute",
+            Aspect.attribute
+        );
+
+        const factory = getFactory(template, HTMLBindingDirective);
+        expect(factory!.binding(null, ExecutionContext.default)).equals(stringValue);
+    });
+
+    it(`captures an attribute with an interpolated number`, () => {
+        const template = html<Model>`<my-element some-attribute=${numberValue}></my-element>`;
+
+        expectTemplateEquals(
+            template,
+            `<my-element some-attribute=${FAKE.interpolation}></my-element>`
+        );
+
+        expectAspect(
+            template,
+            HTMLBindingDirective,
+            "some-attribute",
+            "some-attribute",
+            Aspect.attribute
         );
 
-        expect(directive).to.be.instanceOf(HTMLBindingDirective);
-        expect(directive.source).to.equal("some-attribute");
-        expect(directive.target).to.equal("some-attribute");
-        expect(directive.aspect).to.equal(Aspect.attribute);
+        const factory = getFactory(template, HTMLBindingDirective);
+        expect(factory!.binding(null, ExecutionContext.default)).equals(numberValue);
     });
 
     it(`captures a boolean attribute with an expression`, () => {
         const template = html<Model>`<my-element ?some-attribute=${x => x.value}></my-element>`;
-        const placeholder = Markup.interpolation(0);
-        const directive = template.directives[0] as HTMLBindingDirective;
 
-        expect(template.html).to.equal(
-            `<my-element ?some-attribute=${placeholder}></my-element>`
+        expectTemplateEquals(
+            template,
+            `<my-element ?some-attribute=${FAKE.interpolation}></my-element>`
         );
 
-        expect(directive).to.be.instanceOf(HTMLBindingDirective);
-        expect(directive.source).to.equal("?some-attribute");
-        expect(directive.target).to.equal("some-attribute");
-        expect(directive.aspect).to.equal(Aspect.booleanAttribute);
+        expectAspect(
+            template,
+            HTMLBindingDirective,
+            "?some-attribute",
+            "some-attribute",
+            Aspect.booleanAttribute
+        );
     });
 
     it(`captures a boolean attribute with a binding`, () => {
         const template = html<Model>`<my-element ?some-attribute=${bind(x => x.value)}></my-element>`;
-        const placeholder = Markup.interpolation(0);
-        const directive = template.directives[0] as HTMLBindingDirective;
 
-        expect(template.html).to.equal(
-            `<my-element ?some-attribute=${placeholder}></my-element>`
+        expectTemplateEquals(
+            template,
+            `<my-element ?some-attribute=${FAKE.interpolation}></my-element>`
         );
 
-        expect(directive).to.be.instanceOf(HTMLBindingDirective);
-        expect(directive.source).to.equal("?some-attribute");
-        expect(directive.target).to.equal("some-attribute");
-        expect(directive.aspect).to.equal(Aspect.booleanAttribute);
+        expectAspect(
+            template,
+            HTMLBindingDirective,
+            "?some-attribute",
+            "some-attribute",
+            Aspect.booleanAttribute
+        );
+    });
+
+    it(`captures a boolean attribute with an interpolated boolean`, () => {
+        const template = html<Model>`<my-element ?some-attribute=${true}></my-element>`;
+
+        expectTemplateEquals(
+            template,
+            `<my-element ?some-attribute=${FAKE.interpolation}></my-element>`
+        );
+
+        expectAspect(
+            template,
+            HTMLBindingDirective,
+            "?some-attribute",
+            "some-attribute",
+            Aspect.booleanAttribute
+        );
+
+        const factory = getFactory(template, HTMLBindingDirective);
+        expect(factory!.binding(null, ExecutionContext.default)).equals(true);
     });
 
     it(`captures a case-sensitive property with an expression`, () => {
         const template = html<Model>`<my-element :someAttribute=${x => x.value}></my-element>`;
-        const placeholder = Markup.interpolation(0);
-        const directive = template.directives[0] as HTMLBindingDirective;
 
-        expect(template.html).to.equal(
-            `<my-element :someAttribute=${placeholder}></my-element>`
+        expectTemplateEquals(
+            template,
+            `<my-element :someAttribute=${FAKE.interpolation}></my-element>`
         );
 
-        expect(directive).to.be.instanceOf(HTMLBindingDirective);
-        expect(directive.source).to.equal(":someAttribute");
-        expect(directive.target).to.equal("someAttribute");
-        expect(directive.aspect).to.equal(Aspect.property);
+        expectAspect(
+            template,
+            HTMLBindingDirective,
+            ":someAttribute",
+            "someAttribute",
+            Aspect.property
+        );
     });
 
     it(`captures a case-sensitive property with a binding`, () => {
         const template = html<Model>`<my-element :someAttribute=${bind(x => x.value)}></my-element>`;
-        const placeholder = Markup.interpolation(0);
-        const directive = template.directives[0] as HTMLBindingDirective;
 
-        expect(template.html).to.equal(
-            `<my-element :someAttribute=${placeholder}></my-element>`
+        expectTemplateEquals(
+            template,
+            `<my-element :someAttribute=${FAKE.interpolation}></my-element>`
         );
 
-        expect(directive).to.be.instanceOf(HTMLBindingDirective);
-        expect(directive.source).to.equal(":someAttribute");
-        expect(directive.target).to.equal("someAttribute");
-        expect(directive.aspect).to.equal(Aspect.property);
+        expectAspect(
+            template,
+            HTMLBindingDirective,
+            ":someAttribute",
+            "someAttribute",
+            Aspect.property
+        );
+    });
+
+    it(`captures a case-sensitive property with an interpolated string`, () => {
+        const template = html<Model>`<my-element :someAttribute=${stringValue}></my-element>`;
+
+        expectTemplateEquals(
+            template,
+            `<my-element :someAttribute=${FAKE.interpolation}></my-element>`
+        );
+
+        expectAspect(
+            template,
+            HTMLBindingDirective,
+            ":someAttribute",
+            "someAttribute",
+            Aspect.property
+        );
+
+        const factory = getFactory(template, HTMLBindingDirective);
+        expect(factory!.binding(null, ExecutionContext.default)).equals(stringValue);
+    });
+
+    it(`captures a case-sensitive property with an interpolated number`, () => {
+        const template = html<Model>`<my-element :someAttribute=${numberValue}></my-element>`;
+
+        expectTemplateEquals(
+            template,
+            `<my-element :someAttribute=${FAKE.interpolation}></my-element>`
+        );
+
+        expectAspect(
+            template,
+            HTMLBindingDirective,
+            ":someAttribute",
+            "someAttribute",
+            Aspect.property
+        );
+
+        const factory = getFactory(template, HTMLBindingDirective);
+        expect(factory!.binding(null, ExecutionContext.default)).equals(numberValue);
     });
 
     it(`captures a case-sensitive property with an inline directive`, () => {
-        class TestDirective extends AspectedHTMLDirective {
-            binding: Binding;
-            source: string;
-            target: string;
-            aspect = Aspect.property;
-
-            captureSource(value) {
-                this.source = value;
-            }
+        @htmlDirective({ aspected: true })
+        class TestDirective implements HTMLDirective, Aspected {
+            sourceAspect: string;
+            targetAspect: string;
+            aspectType = Aspect.property;
+            id: string;
+            nodeId: string;
 
             createBehavior(targets: ViewBehaviorTargets) {
                 return { bind() {}, unbind() {} };
             }
+
+            public createHTML(add: AddViewBehaviorFactory): string {
+                return Markup.interpolation(add(this));
+            }
         }
 
         const template = html<Model>`<my-element :someAttribute=${new TestDirective()}></my-element>`;
-        const placeholder = Markup.interpolation(0);
 
-        expect(template.html).to.equal(
-            `<my-element :someAttribute=${placeholder}></my-element>`
+        expectTemplateEquals(
+            template,
+            `<my-element :someAttribute=${FAKE.interpolation}></my-element>`
         );
-        expect((template.directives[0] as TestDirective).source).to.equal(
-            ":someAttribute"
+
+        expectAspect(
+            template,
+            TestDirective,
+            ":someAttribute",
+            "someAttribute",
+            Aspect.property
         );
     });
 
     it(`captures a case-sensitive event when used with an expression`, () => {
         const template = html<Model>`<my-element @someEvent=${x => x.doSomething()}></my-element>`;
-        const placeholder = Markup.interpolation(0);
-        const directive = template.directives[0] as HTMLBindingDirective;
 
-        expect(template.html).to.equal(
-            `<my-element @someEvent=${placeholder}></my-element>`
+        expectTemplateEquals(
+            template,
+            `<my-element @someEvent=${FAKE.interpolation}></my-element>`
         );
 
-        expect(directive).to.be.instanceOf(HTMLBindingDirective);
-        expect(directive.source).to.equal("@someEvent");
-        expect(directive.target).to.equal("someEvent");
-        expect(directive.aspect).to.equal(Aspect.event);
+        expectAspect(
+            template,
+            HTMLBindingDirective,
+            "@someEvent",
+            "someEvent",
+            Aspect.event
+        );
     });
 
     it("should dispose of embedded ViewTemplate when the rendering template contains *only* the embedded template", () => {
diff --git a/packages/web-components/fast-element/src/templating/template.ts b/packages/web-components/fast-element/src/templating/template.ts
index 8c93c89e2a8..1ee73c31511 100644
--- a/packages/web-components/fast-element/src/templating/template.ts
+++ b/packages/web-components/fast-element/src/templating/template.ts
@@ -4,11 +4,18 @@ import {
     ChildContext,
     ExecutionContext,
     ItemContext,
-    RootContext,
 } from "../observation/observable.js";
-import { bind, oneTime } from "./binding.js";
+import { bind, HTMLBindingDirective, oneTime } from "./binding.js";
 import { Compiler } from "./compiler.js";
-import { AspectedHTMLDirective, HTMLDirective } from "./html-directive.js";
+import {
+    AddViewBehaviorFactory,
+    Aspect,
+    Aspected,
+    HTMLDirective,
+    HTMLDirectiveDefinition,
+    ViewBehaviorFactory,
+} from "./html-directive.js";
+import { nextId } from "./markup.js";
 import type { ElementView, HTMLView, SyntheticView } from "./view.js";
 
 /**
@@ -128,19 +135,19 @@ export class ViewTemplate<
     /**
      * The directives that will be connected to placeholders in the html.
      */
-    public readonly directives: ReadonlyArray<HTMLDirective>;
+    public readonly factories: Record<string, ViewBehaviorFactory>;
 
     /**
      * Creates an instance of ViewTemplate.
      * @param html - The html representing what this template will instantiate, including placeholders for directives.
-     * @param directives - The directives that will be connected to placeholders in the html.
+     * @param factories - The directives that will be connected to placeholders in the html.
      */
     public constructor(
         html: string | HTMLTemplateElement,
-        directives: ReadonlyArray<HTMLDirective>
+        factories: Record<string, ViewBehaviorFactory>
     ) {
         this.html = html;
-        this.directives = directives;
+        this.factories = factories;
     }
 
     /**
@@ -151,7 +158,7 @@ export class ViewTemplate<
         if (this.result === null) {
             this.result = Compiler.compile<TSource, TParent, TContext>(
                 this.html,
-                this.directives
+                this.factories
             );
         }
 
@@ -201,6 +208,19 @@ export type TemplateValue<
     TContext extends ExecutionContext<TParent> = ExecutionContext<TParent>
 > = Binding<TSource, any, TContext> | HTMLDirective | CaptureType<TSource>;
 
+function createAspectedHTML(
+    value: HTMLDirective & Aspected,
+    prevString: string,
+    add: AddViewBehaviorFactory
+): string {
+    const match = lastAttributeNameRegex.exec(prevString);
+    if (match !== null) {
+        Aspect.assign(value as Aspected, match[2]);
+    }
+
+    return value.createHTML(add);
+}
+
 /**
  * Transforms a template literal string into a ViewTemplate.
  * @param strings - The string fragments that are interpolated with the values.
@@ -218,42 +238,62 @@ export function html<
     strings: TemplateStringsArray,
     ...values: TemplateValue<TSource, TParent, TContext>[]
 ): ViewTemplate<TSource, TParent> {
-    const directives: HTMLDirective[] = [];
     let html = "";
+    const factories: Record<string, ViewBehaviorFactory> = Object.create(null);
+    const add = (factory: ViewBehaviorFactory): string => {
+        const id = factory.id ?? (factory.id = nextId());
+        factories[id] = factory;
+        return id;
+    };
 
     for (let i = 0, ii = strings.length - 1; i < ii; ++i) {
         const currentString = strings[i];
-        let currentValue = values[i];
+        const currentValue = values[i];
+        let definition: HTMLDirectiveDefinition | undefined;
+
         html += currentString;
 
         if (isFunction(currentValue)) {
-            currentValue = bind(currentValue as Binding);
-        } else if (!isString(currentValue) && !(currentValue instanceof HTMLDirective)) {
-            const capturedValue = currentValue;
-            currentValue = bind(() => capturedValue, oneTime);
-        }
-
-        if (currentValue instanceof HTMLDirective) {
-            if (currentValue instanceof AspectedHTMLDirective) {
-                const match = lastAttributeNameRegex.exec(currentString);
-                if (match !== null) {
-                    currentValue.captureSource(match[2]);
-                }
+            html += createAspectedHTML(
+                bind(currentValue) as HTMLBindingDirective,
+                currentString,
+                add
+            );
+        } else if (isString(currentValue)) {
+            const match = lastAttributeNameRegex.exec(currentString);
+            if (match !== null) {
+                const directive = bind(
+                    () => currentValue,
+                    oneTime
+                ) as HTMLBindingDirective;
+                Aspect.assign(directive, match[2]);
+                html += directive.createHTML(add);
+            } else {
+                html += currentValue;
             }
-
-            // Since not all values are directives, we can't use i
-            // as the index for the placeholder. Instead, we need to
-            // use directives.length to get the next index.
-            html += currentValue.createPlaceholder(directives.length);
-            directives.push(currentValue);
+        } else if ((definition = HTMLDirective.getForInstance(currentValue)) === void 0) {
+            html += createAspectedHTML(
+                bind(() => currentValue, oneTime) as HTMLBindingDirective,
+                currentString,
+                add
+            );
         } else {
-            html += currentValue;
+            if (definition.aspected) {
+                html += createAspectedHTML(
+                    currentValue as HTMLDirective & Aspected,
+                    currentString,
+                    add
+                );
+            } else {
+                html += (currentValue as HTMLDirective).createHTML(add);
+            }
         }
     }
 
-    html += strings[strings.length - 1];
-
-    return new ViewTemplate<TSource, TParent, any>(html, directives);
+    return new ViewTemplate<TSource, TParent, any>(
+        html + strings[strings.length - 1],
+        factories
+    );
 }
 
 /**
diff --git a/packages/web-components/fast-foundation/docs/api-report.md b/packages/web-components/fast-foundation/docs/api-report.md
index 8b7d146c0f6..cce56a555b4 100644
--- a/packages/web-components/fast-foundation/docs/api-report.md
+++ b/packages/web-components/fast-foundation/docs/api-report.md
@@ -923,7 +923,7 @@ export const DesignSystem: Readonly<{
 export interface DesignSystemRegistrationContext {
     readonly elementPrefix: string;
     // @deprecated
-    tryDefineElement(name: string, type: Constructable, callback: ElementDefinitionCallback): void;
+    tryDefineElement(name: string, type: Constructable<HTMLElement>, callback: ElementDefinitionCallback): void;
     tryDefineElement(params: ElementDefinitionParams): void;
 }
 
@@ -1066,13 +1066,13 @@ export interface ElementDefinitionContext {
     readonly name: string;
     readonly shadowRootMode: ShadowRootMode | undefined;
     tagFor(type: Constructable): string;
-    readonly type: Constructable;
+    readonly type: Constructable<HTMLElement>;
     readonly willDefine: boolean;
 }
 
 // @public
 export interface ElementDefinitionParams extends Pick<ElementDefinitionContext, "name" | "type"> {
-    readonly baseClass?: Constructable;
+    readonly baseClass?: Constructable<HTMLElement>;
     callback: ElementDefinitionCallback;
 }
 
@@ -1258,7 +1258,7 @@ export class FoundationElement extends FASTElement {
 // @public
 export interface FoundationElementDefinition {
     readonly attributes?: EagerOrLazyFoundationOption<(AttributeConfiguration | string)[], this>;
-    baseClass?: Constructable;
+    baseClass?: Constructable<HTMLElement>;
     baseName: string;
     readonly elementOptions?: EagerOrLazyFoundationOption<ElementDefinitionOptions, this>;
     readonly shadowOptions?: EagerOrLazyFoundationOption<Partial<ShadowRootInit> | null, this>;
diff --git a/packages/web-components/fast-foundation/src/calendar/calendar.template.ts b/packages/web-components/fast-foundation/src/calendar/calendar.template.ts
index d5002cc49f6..d41976ab6d6 100644
--- a/packages/web-components/fast-foundation/src/calendar/calendar.template.ts
+++ b/packages/web-components/fast-foundation/src/calendar/calendar.template.ts
@@ -46,7 +46,9 @@ export const CalendarTitleTemplate: ViewTemplate<Calendar> = html`
  * @returns - The weekday labels template
  * @public
  */
-export const calendarWeekdayTemplate: (context: ElementDefinitionContext) => ItemViewTemplate = context => {
+export const calendarWeekdayTemplate: (
+    context: ElementDefinitionContext
+) => ItemViewTemplate = context => {
     const cellTag = context.tagFor(DataGridCell);
     return item<WeekdayText>`
         <${cellTag}
diff --git a/packages/web-components/fast-foundation/src/data-grid/data-grid-row.ts b/packages/web-components/fast-foundation/src/data-grid/data-grid-row.ts
index c9e3d925436..31d653217d9 100644
--- a/packages/web-components/fast-foundation/src/data-grid/data-grid-row.ts
+++ b/packages/web-components/fast-foundation/src/data-grid/data-grid-row.ts
@@ -183,7 +183,7 @@ export class DataGridRow extends FoundationElement {
                 { positioning: true }
             );
             this.cellsRepeatBehavior = cellsRepeatDirective.createBehavior({
-                [cellsRepeatDirective.targetId]: this.cellsPlaceholder,
+                [cellsRepeatDirective.nodeId]: this.cellsPlaceholder,
             });
             /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
             this.$fastController.addBehaviors([this.cellsRepeatBehavior!]);
diff --git a/packages/web-components/fast-foundation/src/data-grid/data-grid.template.ts b/packages/web-components/fast-foundation/src/data-grid/data-grid.template.ts
index 7f0489c433d..b55cd0cb1e9 100644
--- a/packages/web-components/fast-foundation/src/data-grid/data-grid.template.ts
+++ b/packages/web-components/fast-foundation/src/data-grid/data-grid.template.ts
@@ -11,7 +11,9 @@ import type { ElementDefinitionContext } from "../design-system/registration-con
 import type { DataGrid } from "./data-grid.js";
 import { DataGridRow } from "./data-grid-row.js";
 
-function createRowItemTemplate(context: ElementDefinitionContext): ItemViewTemplate<any, DataGrid> {
+function createRowItemTemplate(
+    context: ElementDefinitionContext
+): ItemViewTemplate<any, DataGrid> {
     const rowTag = context.tagFor(DataGridRow);
     return item<any, DataGrid>`
     <${rowTag}
diff --git a/packages/web-components/fast-foundation/src/data-grid/data-grid.ts b/packages/web-components/fast-foundation/src/data-grid/data-grid.ts
index 5618db0c4ea..23f9bc2fe82 100644
--- a/packages/web-components/fast-foundation/src/data-grid/data-grid.ts
+++ b/packages/web-components/fast-foundation/src/data-grid/data-grid.ts
@@ -347,7 +347,7 @@ export class DataGrid extends FoundationElement {
             { positioning: true }
         );
         this.rowsRepeatBehavior = rowsRepeatDirective.createBehavior({
-            [rowsRepeatDirective.targetId]: this.rowsPlaceholder,
+            [rowsRepeatDirective.nodeId]: this.rowsPlaceholder,
         });
 
         /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
diff --git a/packages/web-components/fast-foundation/src/design-system/design-system.ts b/packages/web-components/fast-foundation/src/design-system/design-system.ts
index a52e93f022e..989ff5c7ebf 100644
--- a/packages/web-components/fast-foundation/src/design-system/design-system.ts
+++ b/packages/web-components/fast-foundation/src/design-system/design-system.ts
@@ -184,7 +184,7 @@ export const DesignSystem = Object.freeze({
 
 function extractTryDefineElementParams(
     params: string | ElementDefinitionParams,
-    elementDefinitionType?: Constructable,
+    elementDefinitionType?: Constructable<HTMLElement>,
     elementDefinitionCallback?: ElementDefinitionCallback
 ): ElementDefinitionParams {
     if (typeof params === "string") {
@@ -243,7 +243,7 @@ class DefaultDesignSystem implements DesignSystem {
             elementPrefix: this.prefix,
             tryDefineElement(
                 params: string | ElementDefinitionParams,
-                elementDefinitionType?: Constructable,
+                elementDefinitionType?: Constructable<HTMLElement>,
                 elementDefinitionCallback?: ElementDefinitionCallback
             ) {
                 const extractedParams = extractTryDefineElementParams(
@@ -327,7 +327,7 @@ class ElementDefinitionEntry implements ElementDefinitionContext {
     constructor(
         public readonly container: Container,
         public readonly name: string,
-        public readonly type: Constructable,
+        public readonly type: Constructable<HTMLElement>,
         public shadowRootMode: ShadowRootMode | undefined,
         public readonly callback: ElementDefinitionCallback,
         public readonly willDefine: boolean
diff --git a/packages/web-components/fast-foundation/src/design-system/registration-context.ts b/packages/web-components/fast-foundation/src/design-system/registration-context.ts
index 3b6287d6270..6275d4aa3ae 100644
--- a/packages/web-components/fast-foundation/src/design-system/registration-context.ts
+++ b/packages/web-components/fast-foundation/src/design-system/registration-context.ts
@@ -26,7 +26,7 @@ export interface ElementDefinitionContext {
      * The type that will be defined.
      * @public
      */
-    readonly type: Constructable;
+    readonly type: Constructable<HTMLElement>;
 
     /**
      * The dependency injection container associated with the design system.
@@ -85,7 +85,7 @@ export interface ElementDefinitionParams
      * FAST actual base class instance.
      * @public
      */
-    readonly baseClass?: Constructable;
+    readonly baseClass?: Constructable<HTMLElement>;
     /**
      * A callback to invoke if definition will happen.
      * @public
@@ -115,7 +115,7 @@ export interface DesignSystemRegistrationContext {
      */
     tryDefineElement(
         name: string,
-        type: Constructable,
+        type: Constructable<HTMLElement>,
         callback: ElementDefinitionCallback
     ): void;
 
diff --git a/packages/web-components/fast-foundation/src/directives/reflect-attributes.spec.ts b/packages/web-components/fast-foundation/src/directives/reflect-attributes.spec.ts
index e46ec24a50f..bd7e5175acd 100644
--- a/packages/web-components/fast-foundation/src/directives/reflect-attributes.spec.ts
+++ b/packages/web-components/fast-foundation/src/directives/reflect-attributes.spec.ts
@@ -1,10 +1,8 @@
-import { FoundationElement } from "../foundation-element";
-import { html, ref, customElement, DOM } from "@microsoft/fast-element";
-import { fixture } from "../test-utilities/fixture";
+import { html, ref, customElement, DOM, FASTElement } from "@microsoft/fast-element";
+import { fixture, uniqueElementName } from "../test-utilities/fixture";
 import { reflectAttributes } from "./reflect-attributes";
 import { expect } from "chai";
 
-
 const template = html<AttributeReflectionTestElement>`
     <div
         ${ref("a")}
@@ -21,98 +19,104 @@ const template = html<AttributeReflectionTestElement>`
         )}
     ></div>
 `
-const name = "attr-reflection"
+const name = uniqueElementName();
 @customElement({
     name,
     template
 })
-class AttributeReflectionTestElement extends FoundationElement {
+class AttributeReflectionTestElement extends FASTElement {
     public a: HTMLElement;
     public b: HTMLElement;
 }
 
+describe("reflectAttributes", () => {
+    it("should reflect configured attributes that exist on the host after connection", async () => {
+        const { element, connect, disconnect } = await fixture<AttributeReflectionTestElement>(name);
 
-function create() {
-    return document.createElement(name) as AttributeReflectionTestElement;
-}
-
-function connect(el: HTMLElement) {
-    document.body.append(el);
+        element.setAttribute("foo", "bar");
+        await connect();
 
-    return () => {
-        document.body.removeChild(el);
-    }
-}
-describe("reflectAttributes", () => {
-    it("should reflect configured attributes that exist on the host after connection", () => {
-        const el = create();
-        el.setAttribute("foo", "bar");
-        const disconnect = connect(el)
-        expect(el.a.getAttribute("foo")).to.equal("bar");
-        disconnect();
+        expect(element.a.getAttribute("foo")).to.equal("bar");
+        await disconnect();
     });
+
     it("should reflect a configured attribute when set on the host to the directive target", async () => {
-        const el = create();
-        const disconnect = connect(el);
-        el.setAttribute("foo", "bar");
+        const { element, connect, disconnect } = await fixture<AttributeReflectionTestElement>(name);
 
+        await connect();
+        element.setAttribute("foo", "bar");
         await DOM.nextUpdate();
-        expect(el.a.getAttribute("foo")).to.equal("bar");
-        disconnect();
+
+        expect(element.a.getAttribute("foo")).to.equal("bar");
+        await disconnect();
     });
+
     it("should reflect a configured attribute when set on the host to all directive targets", async () => {
-        const el = create();
-        const disconnect = connect(el);
-        el.setAttribute("bar", "bat");
+        const { element, connect, disconnect } = await fixture<AttributeReflectionTestElement>(name);
 
+        await connect();
+        element.setAttribute("bar", "bat");
         await DOM.nextUpdate();
-        expect(el.a.getAttribute("bar")).to.equal("bat");
-        expect(el.b.getAttribute("bar")).to.equal("bat");
-        disconnect();
+
+        expect(element.a.getAttribute("bar")).to.equal("bat");
+        expect(element.b.getAttribute("bar")).to.equal("bat");
+
+        await disconnect();
     });
+
     it("should remove a configured attribute from the directive target when it is removed from the host", async () => {
-        const el = create();
-        const disconnect = connect(el)
-        el.setAttribute("foo", "bar");
+        const { element, connect, disconnect } = await fixture<AttributeReflectionTestElement>(name);
+
+        await connect();
+        element.setAttribute("foo", "bar");
         await DOM.nextUpdate();
 
-        el.removeAttribute("foo");
+        element.removeAttribute("foo");
         await DOM.nextUpdate();
-        expect(el.a.hasAttribute("foo")).to.equal(false);
 
-        disconnect();
+        expect(element.a.hasAttribute("foo")).to.equal(false);
+
+        await disconnect();
     });
+
     it("should remove a configured attribute from all directive targets when it is removed from the host", async () => {
-        const el = create();
-        const disconnect = connect(el)
-        el.setAttribute("bar", "bat");
+        const { element, connect, disconnect } = await fixture<AttributeReflectionTestElement>(name);
+
+        await connect();
+        element.setAttribute("bar", "bat");
         await DOM.nextUpdate();
 
-        el.removeAttribute("bar");
+        element.removeAttribute("bar");
         await DOM.nextUpdate();
-        expect(el.a.hasAttribute("bar")).to.equal(false);
-        expect(el.b.hasAttribute("bar")).to.equal(false);
 
-        disconnect();
+        expect(element.a.hasAttribute("bar")).to.equal(false);
+        expect(element.b.hasAttribute("bar")).to.equal(false);
+
+        await disconnect();
     });
 
     it("should only reflect attributes in the directive configuration to the directive target", async () => {
-        const el = create();
-        const disconnect = connect(el);
-        el.setAttribute("foo", "bar");
+        const { element, connect, disconnect } = await fixture<AttributeReflectionTestElement>(name);
+
+        await connect();
+        element.setAttribute("foo", "bar");
 
         await DOM.nextUpdate();
-        expect(el.b.hasAttribute("foo")).to.equal(false);
-        disconnect();
+        expect(element.b.hasAttribute("foo")).to.equal(false);
+
+        await disconnect();
     });
+
     it("should not reflect attributes thats are not in any directive configuration", async () => {
-        const el = create();
-        const disconnect = connect(el);
-        el.setAttribute("bee", "bar");
+        const { element, connect, disconnect } = await fixture<AttributeReflectionTestElement>(name);
 
+        await connect();
+        element.setAttribute("bee", "bar");
         await DOM.nextUpdate();
-        expect(el.a.hasAttribute("bee")).to.equal(false);
-        expect(el.b.hasAttribute("bee")).to.equal(false);
-        disconnect();
+
+        expect(element.a.hasAttribute("bee")).to.equal(false);
+        expect(element.b.hasAttribute("bee")).to.equal(false);
+
+        await disconnect();
     });
 })
diff --git a/packages/web-components/fast-foundation/src/directives/reflect-attributes.ts b/packages/web-components/fast-foundation/src/directives/reflect-attributes.ts
index 6001f1eefa2..cf2e32d7739 100644
--- a/packages/web-components/fast-foundation/src/directives/reflect-attributes.ts
+++ b/packages/web-components/fast-foundation/src/directives/reflect-attributes.ts
@@ -1,6 +1,7 @@
 import {
     DOM,
     ExecutionContext,
+    HTMLDirective,
     StatelessAttachedAttributeDirective,
     Subscriber,
     SubscriberSet,
@@ -10,65 +11,73 @@ import type { CaptureType } from "@microsoft/fast-element";
 
 const observer = new MutationObserver((mutations: MutationRecord[]) => {
     for (const mutation of mutations) {
-        AttributeReflectionSubscriptionSet.getOrCreateFor(mutation.target).notify(
-            mutation.attributeName
-        );
+        AttributeReflectionSubscriptionSet.getOrCreateFor(
+            mutation.target as HTMLElement
+        ).notify(mutation.attributeName);
     }
 });
-class AttributeReflectionSubscriptionSet extends SubscriberSet {
+
+class AttributeReflectionSubscriptionSet {
     private static subscriberCache: WeakMap<
         any,
         AttributeReflectionSubscriptionSet
     > = new WeakMap();
+
     private watchedAttributes: Set<Readonly<string[]>> = new Set();
+    private subscribers = new SubscriberSet(this);
+
+    constructor(public element: HTMLElement) {
+        AttributeReflectionSubscriptionSet.subscriberCache.set(element, this);
+    }
+
+    public notify(attr: string | null) {
+        this.subscribers.notify(attr);
+    }
+
+    public subscribe(subscriber: Subscriber & ReflectAttributesDirective) {
+        this.subscribers.subscribe(subscriber);
 
-    public subscribe(subscriber: Subscriber & ReflectAttrBehavior) {
-        super.subscribe(subscriber);
         if (!this.watchedAttributes.has(subscriber.attributes)) {
             this.watchedAttributes.add(subscriber.attributes);
             this.observe();
         }
     }
 
-    constructor(source: any) {
-        super(source);
-
-        AttributeReflectionSubscriptionSet.subscriberCache.set(source, this);
-    }
+    public unsubscribe(subscriber: Subscriber & ReflectAttributesDirective) {
+        this.subscribers.unsubscribe(subscriber);
 
-    public unsubscribe(subscriber: Subscriber & ReflectAttrBehavior) {
-        super.unsubscribe(subscriber);
         if (this.watchedAttributes.has(subscriber.attributes)) {
             this.watchedAttributes.delete(subscriber.attributes);
             this.observe();
         }
     }
 
-    public static getOrCreateFor(source: any) {
-        return (
-            this.subscriberCache.get(source) ||
-            new AttributeReflectionSubscriptionSet(source)
-        );
-    }
-
     private observe() {
         const attributeFilter: string[] = [];
+
         for (const attributes of this.watchedAttributes.values()) {
             for (let i = 0; i < attributes.length; i++) {
                 attributeFilter.push(attributes[i]);
             }
         }
 
-        observer.observe(this.subject, { attributeFilter });
+        observer.observe(this.element, { attributeFilter });
+    }
+
+    public static getOrCreateFor(source: HTMLElement) {
+        return (
+            this.subscriberCache.get(source) ||
+            new AttributeReflectionSubscriptionSet(source)
+        );
     }
 }
 
-class ReflectAttrBehavior extends StatelessAttachedAttributeDirective<string[]> {
+class ReflectAttributesDirective extends StatelessAttachedAttributeDirective<string[]> {
     /**
      * The attributes the behavior is reflecting
      */
     public attributes: Readonly<string[]>;
-    private target: HTMLElement;
+
     constructor(attributes: string[]) {
         super(attributes);
         this.attributes = Object.freeze(attributes);
@@ -79,14 +88,15 @@ class ReflectAttrBehavior extends StatelessAttachedAttributeDirective<string[]>
         context: ExecutionContext,
         targets: ViewBehaviorTargets
     ): void {
-        this.target = targets[this.targetId] as HTMLElement;
-        AttributeReflectionSubscriptionSet.getOrCreateFor(source).subscribe(this);
+        const subscription = AttributeReflectionSubscriptionSet.getOrCreateFor(source);
+        subscription[this.id] = targets[this.nodeId];
+        subscription.subscribe(this);
 
         // Reflect any existing attributes because MutationObserver will only
         // handle *changes* to attributes.
         if (source.hasAttributes()) {
             for (let i = 0; i < source.attributes.length; i++) {
-                this.handleChange(source, source.attributes[i].name);
+                this.handleChange(subscription, source.attributes[i].name);
             }
         }
     }
@@ -95,17 +105,21 @@ class ReflectAttrBehavior extends StatelessAttachedAttributeDirective<string[]>
         AttributeReflectionSubscriptionSet.getOrCreateFor(source).unsubscribe(this);
     }
 
-    public handleChange(source: HTMLElement, arg: string): void {
+    public handleChange(source: AttributeReflectionSubscriptionSet, arg: string): void {
         // In cases where two or more ReflectAttrBehavior instances are bound to the same element,
         // they will share a Subscriber implementation. In that case, this handle change can be invoked with
         // attributes an instances doesn't need to reflect. This guards against reflecting attrs
         // that shouldn't be reflected.
         if (this.attributes.includes(arg)) {
-            DOM.setAttribute(this.target, arg, source.getAttribute(arg));
+            const element = source.element as HTMLElement;
+            const target = source[this.id] as HTMLElement;
+            DOM.setAttribute(target, arg, element.getAttribute(arg));
         }
     }
 }
 
+HTMLDirective.define(ReflectAttributesDirective);
+
 /**
  * Reflects attributes from the host element to the target element of the directive.
  * @param attributes - The attributes to reflect
@@ -123,10 +137,5 @@ class ReflectAttrBehavior extends StatelessAttachedAttributeDirective<string[]>
  * ```
  */
 export function reflectAttributes<T = any>(...attributes: string[]): CaptureType<T> {
-    return new ReflectAttrBehavior(attributes);
-    // return new AttachedBehaviorHTMLDirective(
-    //     "fast-reflect-attr",
-    //     ReflectAttrBehavior,
-    //     attributes
-    // );
+    return new ReflectAttributesDirective(attributes);
 }
diff --git a/packages/web-components/fast-foundation/src/foundation-element/foundation-element.ts b/packages/web-components/fast-foundation/src/foundation-element/foundation-element.ts
index c647f9e6507..c539ec20b90 100644
--- a/packages/web-components/fast-foundation/src/foundation-element/foundation-element.ts
+++ b/packages/web-components/fast-foundation/src/foundation-element/foundation-element.ts
@@ -38,7 +38,7 @@ export interface FoundationElementDefinition {
     /**
      * The actual FAST base class of the component if different from the class used to compose.
      */
-    baseClass?: Constructable;
+    baseClass?: Constructable<HTMLElement>;
 
     /**
      * The template to render for the custom element.
diff --git a/packages/web-components/fast-foundation/src/picker/picker.template.ts b/packages/web-components/fast-foundation/src/picker/picker.template.ts
index fad127a983b..15dab7cbbf7 100644
--- a/packages/web-components/fast-foundation/src/picker/picker.template.ts
+++ b/packages/web-components/fast-foundation/src/picker/picker.template.ts
@@ -15,7 +15,9 @@ import { PickerMenuOption } from "./picker-menu-option.js";
 import { PickerList } from "./picker-list.js";
 import { PickerListItem } from "./picker-list-item.js";
 
-function createDefaultListItemTemplate(context: ElementDefinitionContext): ChildViewTemplate {
+function createDefaultListItemTemplate(
+    context: ElementDefinitionContext
+): ChildViewTemplate {
     const pickerListItemTag: string = context.tagFor(PickerListItem);
     return child`
     <${pickerListItemTag}
@@ -26,7 +28,9 @@ function createDefaultListItemTemplate(context: ElementDefinitionContext): Child
     `;
 }
 
-function createDefaultMenuOptionTemplate(context: ElementDefinitionContext): ChildViewTemplate {
+function createDefaultMenuOptionTemplate(
+    context: ElementDefinitionContext
+): ChildViewTemplate {
     const pickerMenuOptionTag: string = context.tagFor(PickerMenuOption);
     return child`
     <${pickerMenuOptionTag}
diff --git a/packages/web-components/fast-foundation/src/picker/picker.ts b/packages/web-components/fast-foundation/src/picker/picker.ts
index db046885c2d..8733f8ffd0e 100644
--- a/packages/web-components/fast-foundation/src/picker/picker.ts
+++ b/packages/web-components/fast-foundation/src/picker/picker.ts
@@ -545,7 +545,7 @@ export class Picker extends FormAssociatedPicker {
             { positioning: true }
         );
         this.itemsRepeatBehavior = itemsRepeatDirective.createBehavior({
-            [itemsRepeatDirective.targetId]: this.itemsPlaceholderElement,
+            [itemsRepeatDirective.nodeId]: this.itemsPlaceholderElement,
         });
 
         this.inputElement.addEventListener("input", this.handleTextInput);
@@ -565,7 +565,7 @@ export class Picker extends FormAssociatedPicker {
             { positioning: true }
         );
         this.optionsRepeatBehavior = optionsRepeatDirective.createBehavior({
-            [optionsRepeatDirective.targetId]: this.optionsPlaceholder,
+            [optionsRepeatDirective.nodeId]: this.optionsPlaceholder,
         });
 
         /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
diff --git a/packages/web-components/fast-foundation/src/test-utilities/fixture.ts b/packages/web-components/fast-foundation/src/test-utilities/fixture.ts
index e6f3472f329..89eb8a0c3d3 100644
--- a/packages/web-components/fast-foundation/src/test-utilities/fixture.ts
+++ b/packages/web-components/fast-foundation/src/test-utilities/fixture.ts
@@ -146,7 +146,7 @@ export async function fixture<TElement = HTMLElement>(
 
     if (typeof templateNameOrRegistry === "string") {
         const html = `<${templateNameOrRegistry}></${templateNameOrRegistry}>`;
-        templateNameOrRegistry = new ViewTemplate(html, []);
+        templateNameOrRegistry = new ViewTemplate(html, {});
     } else if (isElementRegistry(templateNameOrRegistry)) {
         templateNameOrRegistry = [templateNameOrRegistry];
     }
@@ -164,7 +164,7 @@ export async function fixture<TElement = HTMLElement>(
 
         const elementName = `${prefix}-${first.definition.baseName}`;
         const html = `<${elementName}></${elementName}>`;
-        templateNameOrRegistry = new ViewTemplate(html, []);
+        templateNameOrRegistry = new ViewTemplate(html, {});
     }
 
     const view = templateNameOrRegistry.create();
diff --git a/packages/web-components/fast-router/docs/api-report.md b/packages/web-components/fast-router/docs/api-report.md
index 37bbe3b9a26..abcdf9fe6e1 100644
--- a/packages/web-components/fast-router/docs/api-report.md
+++ b/packages/web-components/fast-router/docs/api-report.md
@@ -4,11 +4,14 @@
 
 ```ts
 
+import { AddViewBehaviorFactory } from '@microsoft/fast-element';
+import { Behavior } from '@microsoft/fast-element';
 import { ComposableStyles } from '@microsoft/fast-element';
 import { Constructable } from '@microsoft/fast-element';
 import { ExecutionContext } from '@microsoft/fast-element';
 import { FASTElement } from '@microsoft/fast-element';
 import { HTMLDirective } from '@microsoft/fast-element';
+import { ViewBehaviorTargets } from '@microsoft/fast-element';
 import { ViewTemplate } from '@microsoft/fast-element';
 
 // Warning: (ae-internal-missing-underscore) The name "childRouteParameter" should be prefixed with an underscore because the declaration is marked as @internal
@@ -254,8 +257,10 @@ export type NavigationContributor<TSettings = any> = Partial<Record<Exclude<Navi
     commit?: NavigationCommitPhaseHook<TSettings>;
 };
 
+// Warning: (ae-forgotten-export) The symbol "NavigationContributorDirective" needs to be exported by the entry point index.d.ts
+//
 // @alpha (undocumented)
-export function navigationContributor(options?: ContributorOptions): HTMLDirective;
+export function navigationContributor(options?: ContributorOptions): NavigationContributorDirective;
 
 // @alpha (undocumented)
 export interface NavigationHandler {
diff --git a/packages/web-components/fast-router/src/commands.ts b/packages/web-components/fast-router/src/commands.ts
index 5add86956d1..9b328a1ef8b 100644
--- a/packages/web-components/fast-router/src/commands.ts
+++ b/packages/web-components/fast-router/src/commands.ts
@@ -91,10 +91,10 @@ function factoryFromElementInstance(element: HTMLElement): ViewFactory {
     fragment.appendChild(element);
 
     const factory = navigationContributor();
-    factory.targetId = "h";
+    factory.nodeId = "h";
 
     const view = new HTMLView(fragment, [factory], {
-        [factory.targetId]: element,
+        [factory.nodeId]: element,
     });
 
     return {
@@ -196,7 +196,7 @@ export class Render implements RenderCommand {
                 } else if (typeof element === "function") {
                     // Do not cache it becase the function could return
                     // a different value each time.
-                    let def = FASTElementDefinition.forType(element);
+                    let def = FASTElementDefinition.getByType(element);
 
                     if (def) {
                         factory = factoryFromElementName(def.name);
@@ -208,7 +208,7 @@ export class Render implements RenderCommand {
                         } else if (element instanceof HTMLElement) {
                             factory = factoryFromElementInstance(element);
                         } else {
-                            def = FASTElementDefinition.forType(element as any);
+                            def = FASTElementDefinition.getByType(element);
 
                             if (def) {
                                 factory = factoryFromElementName(def.name);
diff --git a/packages/web-components/fast-router/src/contributors.ts b/packages/web-components/fast-router/src/contributors.ts
index eb9afd35e0d..2aa25ece54a 100644
--- a/packages/web-components/fast-router/src/contributors.ts
+++ b/packages/web-components/fast-router/src/contributors.ts
@@ -1,4 +1,5 @@
 import {
+    AddViewBehaviorFactory,
     Behavior,
     HTMLDirective,
     Markup,
@@ -44,23 +45,26 @@ const defaultOptions: ContributorOptions = {
     parameters: true,
 };
 
-class NavigationContributorDirective extends HTMLDirective {
-    constructor(private options: Required<ContributorOptions>) {
-        super();
-    }
+class NavigationContributorDirective implements HTMLDirective {
+    id: string;
+    nodeId: string;
+
+    constructor(private options: Required<ContributorOptions>) {}
 
-    createPlaceholder(index: number) {
-        return Markup.attribute(index);
+    createHTML(add: AddViewBehaviorFactory) {
+        return Markup.attribute(add(this));
     }
 
     createBehavior(targets: ViewBehaviorTargets) {
         return new NavigationContributorBehavior(
-            targets[this.targetId] as HTMLElement & NavigationContributor,
+            targets[this.nodeId] as HTMLElement & NavigationContributor,
             this.options
         );
     }
 }
 
+HTMLDirective.define(NavigationContributorDirective);
+
 class NavigationContributorBehavior implements Behavior {
     private router: Router | null = null;
 
@@ -95,7 +99,9 @@ class NavigationContributorBehavior implements Behavior {
 /**
  * @alpha
  */
-export function navigationContributor(options?: ContributorOptions): HTMLDirective {
+export function navigationContributor(
+    options?: ContributorOptions
+): NavigationContributorDirective {
     return new NavigationContributorDirective(
         Object.assign({}, defaultOptions, options) as Required<ContributorOptions>
     );
diff --git a/packages/web-components/fast-ssr/src/element-renderer/element-renderer.ts b/packages/web-components/fast-ssr/src/element-renderer/element-renderer.ts
index e3035f62eaf..b31e8cc0ee4 100644
--- a/packages/web-components/fast-ssr/src/element-renderer/element-renderer.ts
+++ b/packages/web-components/fast-ssr/src/element-renderer/element-renderer.ts
@@ -1,15 +1,9 @@
 import { ElementRenderer, RenderInfo } from "@lit-labs/ssr";
-import { Aspect, ExecutionContext, DOM, FASTElement } from "@microsoft/fast-element";
+import { Aspect, DOM, ExecutionContext, FASTElement } from "@microsoft/fast-element";
 import { TemplateRenderer } from "../template-renderer/template-renderer.js";
 import { SSRView } from "../view.js";
 import { StyleRenderer } from "../styles/style-renderer.js";
 
-const prefix = "fast-style";
-let id = 0;
-function nextId(): string {
-    return `${prefix}-${id++}`;
-}
-
 export abstract class FASTElementRenderer extends ElementRenderer {
     /**
      * The element instance represented by the {@link FASTElementRenderer}.
@@ -90,10 +84,10 @@ export abstract class FASTElementRenderer extends ElementRenderer {
     constructor(tagName: string) {
         super(tagName);
 
-        const ctor: typeof FASTElement | null = customElements.get(this.tagName);
+        const ctor = customElements.get(this.tagName);
 
         if (ctor) {
-            this.element = new ctor();
+            this.element = new ctor() as FASTElement;
         } else {
             throw new Error(
                 `FASTElementRenderer was unable to find a constructor for a custom element with the tag name '${tagName}'.`
diff --git a/packages/web-components/fast-ssr/src/exports.ts b/packages/web-components/fast-ssr/src/exports.ts
index b4be9efe7ba..551163081fd 100644
--- a/packages/web-components/fast-ssr/src/exports.ts
+++ b/packages/web-components/fast-ssr/src/exports.ts
@@ -1,5 +1,5 @@
 import { RenderInfo } from "@lit-labs/ssr";
-import { Compiler, ElementStyles, HTMLDirective } from "@microsoft/fast-element";
+import { Compiler, ElementStyles, ViewBehaviorFactory } from "@microsoft/fast-element";
 import { FASTElementRenderer } from "./element-renderer/element-renderer.js";
 import { FASTSSRStyleStrategy } from "./element-renderer/style-strategy.js";
 import {
@@ -7,7 +7,7 @@ import {
     StyleElementStyleRenderer,
     StyleRenderer,
 } from "./styles/style-renderer.js";
-import { defaultFASTDirectiveRenderers } from "./template-renderer/directives.js";
+import { defaultViewBehaviorFactoryRenderers } from "./template-renderer/directives.js";
 import {
     TemplateRenderer,
     TemplateRendererConfiguration,
@@ -16,14 +16,17 @@ import { SSRView } from "./view.js";
 
 export type Configuration = TemplateRendererConfiguration;
 Compiler.setDefaultStrategy(
-    (html: string | HTMLTemplateElement, directives: ReadonlyArray<HTMLDirective>) => {
+    (
+        html: string | HTMLTemplateElement,
+        factories: Record<string, ViewBehaviorFactory>
+    ) => {
         if (typeof html !== "string") {
             throw new Error(
                 "SSR compiler does not support HTMLTemplateElement templates"
             );
         }
 
-        return new SSRView(html, directives) as any;
+        return new SSRView(html, factories) as any;
     }
 );
 
@@ -58,7 +61,9 @@ export default function (
             : new StyleElementStyleRenderer();
     };
 
-    templateRenderer.withDirectiveRenderer(...defaultFASTDirectiveRenderers);
+    templateRenderer.withViewBehaviorFactoryRenderers(
+        ...defaultViewBehaviorFactoryRenderers
+    );
 
     return {
         templateRenderer,
diff --git a/packages/web-components/fast-ssr/src/template-parser/op-codes.ts b/packages/web-components/fast-ssr/src/template-parser/op-codes.ts
index a3b59f8afe1..53921e573fe 100644
--- a/packages/web-components/fast-ssr/src/template-parser/op-codes.ts
+++ b/packages/web-components/fast-ssr/src/template-parser/op-codes.ts
@@ -1,4 +1,4 @@
-import { Aspect, Binding, HTMLDirective } from "@microsoft/fast-element";
+import { Binding, ViewBehaviorFactory } from "@microsoft/fast-element";
 
 /**
  * Allows fast identification of operation types
@@ -9,7 +9,7 @@ export const enum OpType {
     customElementAttributes,
     customElementShadow,
     attributeBinding,
-    directive,
+    viewBehaviorFactory,
     templateElementOpen,
     templateElementClose,
     text,
@@ -59,9 +59,9 @@ export type CustomElementShadowOp = {
 /**
  * Operation to emit static text
  */
-export type DirectiveOp = {
-    type: OpType.directive;
-    directive: HTMLDirective;
+export type ViewBehaviorFactoryOp = {
+    type: OpType.viewBehaviorFactory;
+    factory: ViewBehaviorFactory;
 };
 
 /**
@@ -71,12 +71,7 @@ export type AttributeBindingOp = {
     type: OpType.attributeBinding;
     binding: Binding;
     target: string;
-    aspect:
-        | Aspect.attribute
-        | Aspect.booleanAttribute
-        | Aspect.event
-        | Aspect.property
-        | Aspect.tokenList;
+    aspect: number;
     useCustomElementInstance: boolean;
 };
 
@@ -109,7 +104,7 @@ export type Op =
     | AttributeBindingOp
     | CustomElementOpenOp
     | CustomElementCloseOp
-    | DirectiveOp
+    | ViewBehaviorFactoryOp
     | CustomElementAttributes
     | CustomElementShadowOp
     | TemplateElementOpenOp
diff --git a/packages/web-components/fast-ssr/src/template-parser/template-parser.spec.ts b/packages/web-components/fast-ssr/src/template-parser/template-parser.spec.ts
index 024455426de..323dc95aab0 100644
--- a/packages/web-components/fast-ssr/src/template-parser/template-parser.spec.ts
+++ b/packages/web-components/fast-ssr/src/template-parser/template-parser.spec.ts
@@ -1,22 +1,30 @@
 
 import "../dom-shim.js";
-import { Aspect, customElement, FASTElement, html, ViewTemplate } from "@microsoft/fast-element";
+import { Aspect, customElement, FASTElement, html, ViewBehaviorFactory, ViewTemplate } from "@microsoft/fast-element";
 import { expect, test } from "@playwright/test";
-import { AttributeBindingOp, CustomElementOpenOp, DirectiveOp, OpType, TemplateElementOpenOp, TextOp } from "./op-codes.js";
+import { AttributeBindingOp, CustomElementOpenOp, ViewBehaviorFactoryOp, OpType, TemplateElementOpenOp, TextOp } from "./op-codes.js";
 import { parseTemplateToOpCodes } from "./template-parser.js";
 
 @customElement("hello-world")
 class HelloWorld extends FASTElement {}
 
+function firstFactory(factories: Record<string, ViewBehaviorFactory>) {
+    for (const key in factories) {
+        return factories[key];
+    }
+
+    return null;
+}
+
 test.describe("parseTemplateToOpCodes", () => {
     test("should throw when invoked with a ViewTemplate with a HTMLTemplateElement template", () => {
         expect(() => {
-            parseTemplateToOpCodes(new ViewTemplate(document.createElement("template"), []));
+            parseTemplateToOpCodes(new ViewTemplate(document.createElement("template"), {}));
         }).toThrow();
     });
     test("should not throw when invoked with a ViewTemplate with a string template", () => {
         expect(() => {
-            parseTemplateToOpCodes(new ViewTemplate("", []));
+            parseTemplateToOpCodes(new ViewTemplate("", {}));
         }).not.toThrow();
     });
 
@@ -26,24 +34,24 @@ test.describe("parseTemplateToOpCodes", () => {
     test("should emit doctype, html, head, and body elements as part of text op", () => {
         expect(parseTemplateToOpCodes(html`<!DOCTYPE html><html><head></head><body></body></html>`)).toEqual([{type: OpType.text, value: "<!DOCTYPE html><html><head></head><body></body></html>"}])
     })
-    test("should emit a directive op from a binding", () => {
+    test("should emit a viewBehaviorFactory op from a binding", () => {
             const input = html`${() => "hello world"}`;
-            expect(parseTemplateToOpCodes(input)).toEqual([{ type: OpType.directive, directive: input.directives[0]}])
+            expect(parseTemplateToOpCodes(input)).toEqual([{ type: OpType.viewBehaviorFactory, factory: firstFactory(input.factories)}])
     });
     test("should emit a directive op from a content binding", () => {
             const input = html`Hello ${() => "World"}.`;
 
             const codes = parseTemplateToOpCodes(input);
-            const code = codes[1] as DirectiveOp;
+            const code = codes[1] as ViewBehaviorFactoryOp;
             expect(codes.length).toBe(3);
-            expect(code.type).toBe(OpType.directive);
+            expect(code.type).toBe(OpType.viewBehaviorFactory);
     });
-    test("should sandwich directive ops between text ops when binding native element content", () => {
+    test("should sandwich viewBehaviorFactory ops between text ops when binding native element content", () => {
 
             const input = html`<p>${() => "hello world"}</p>`;
             expect(parseTemplateToOpCodes(input)).toEqual([
                     { type: OpType.text, value: "<p>"},
-                    { type: OpType.directive, directive: input.directives[0]},
+                    { type: OpType.viewBehaviorFactory, factory: firstFactory(input.factories)},
                     { type: OpType.text, value: "</p>"},
             ])
         });
diff --git a/packages/web-components/fast-ssr/src/template-parser/template-parser.ts b/packages/web-components/fast-ssr/src/template-parser/template-parser.ts
index c08ddc46ce8..7ddba5e90e2 100644
--- a/packages/web-components/fast-ssr/src/template-parser/template-parser.ts
+++ b/packages/web-components/fast-ssr/src/template-parser/template-parser.ts
@@ -4,10 +4,10 @@
  */
 import {
     Aspect,
-    AspectedHTMLDirective,
+    Aspected,
     Compiler,
-    HTMLDirective,
     Parser,
+    ViewBehaviorFactory,
     ViewTemplate,
 } from "@microsoft/fast-element";
 import {
@@ -111,14 +111,14 @@ export function parseTemplateToOpCodes(template: ViewTemplate): Op[] {
      */
     const templateString = html;
 
-    const codes = parseStringToOpCodes(templateString, template.directives);
+    const codes = parseStringToOpCodes(templateString, template.factories);
     opCache.set(template, codes);
     return codes;
 }
 
 export function parseStringToOpCodes(
     templateString: string,
-    directives: ReadonlyArray<HTMLDirective>
+    factories: Record<string, ViewBehaviorFactory>
 ): Op[] {
     const nodeTree = parseFragment(templateString, { sourceCodeLocationInfo: true });
 
@@ -159,20 +159,17 @@ export function parseStringToOpCodes(
             dynamic: Map<Attribute, AttributeBindingOp>;
         } = node.attrs.reduce(
             (prev, current) => {
-                const parsed = Parser.parse(current.value, directives);
+                const parsed = Parser.parse(current.value, factories);
                 if (parsed) {
-                    const directive = Compiler.aggregate(parsed);
+                    const factory = Compiler.aggregate(parsed) as ViewBehaviorFactory &
+                        Aspected;
                     // Guard against directives like children, ref, and slotted
-                    if (
-                        directive instanceof AspectedHTMLDirective &&
-                        directive.binding &&
-                        directive.aspect !== Aspect.content
-                    ) {
+                    if (factory.binding && factory.aspectType !== Aspect.content) {
                         prev.dynamic.set(current, {
                             type: OpType.attributeBinding,
-                            binding: directive.binding,
-                            aspect: directive.aspect,
-                            target: directive.target,
+                            binding: factory.binding,
+                            aspect: factory.aspectType,
+                            target: factory.targetAspect,
                             useCustomElementInstance: Boolean(
                                 node.isDefinedCustomElement
                             ),
@@ -227,12 +224,12 @@ export function parseStringToOpCodes(
                 skipTo(location.endOffset);
             } else if (!attributes.static.has(attr.name)) {
                 // Handle interpolated directives like children, ref, and slotted
-                const parsed = Parser.parse(attr.value, directives);
+                const parsed = Parser.parse(attr.value, factories);
                 if (parsed) {
                     const location = node.sourceCodeLocation!.attrs[attr.name];
-                    const directive = Compiler.aggregate(parsed);
+                    const factory = Compiler.aggregate(parsed);
                     flushTo(location.startOffset);
-                    opCodes.push({ type: OpType.directive, directive });
+                    opCodes.push({ type: OpType.viewBehaviorFactory, factory });
                     skipTo(location.endOffset);
                 }
             } else if (node.isDefinedCustomElement) {
@@ -311,7 +308,7 @@ export function parseStringToOpCodes(
                 const parsed = Parser.parse(
                     (node as DefaultTreeCommentNode)?.data ||
                         (node as DefaultTreeTextNode).value,
-                    directives
+                    factories
                 );
 
                 if (parsed) {
@@ -321,8 +318,8 @@ export function parseStringToOpCodes(
                             flush(part);
                         } else {
                             opCodes.push({
-                                type: OpType.directive,
-                                directive: part,
+                                type: OpType.viewBehaviorFactory,
+                                factory: part,
                             });
                         }
                     }
diff --git a/packages/web-components/fast-ssr/src/template-renderer/directives.ts b/packages/web-components/fast-ssr/src/template-renderer/directives.ts
index 4cbf0ba538b..59ab38fccba 100644
--- a/packages/web-components/fast-ssr/src/template-renderer/directives.ts
+++ b/packages/web-components/fast-ssr/src/template-renderer/directives.ts
@@ -7,6 +7,7 @@ import {
     RefDirective,
     RepeatDirective,
     SlottedDirective,
+    ViewBehaviorFactory,
     ViewTemplate,
 } from "@microsoft/fast-element";
 import { TemplateRenderer } from "./template-renderer.js";
@@ -14,22 +15,22 @@ import { TemplateRenderer } from "./template-renderer.js";
 /**
  * Describes an implementation that can render a directive
  */
-export interface DirectiveRenderer<T extends Constructable> {
+export interface ViewBehaviorFactoryRenderer<T extends ViewBehaviorFactory> {
     render(
-        directive: InstanceType<T>,
+        behavior: T,
         renderInfo: RenderInfo,
         source: any,
         renderer: TemplateRenderer,
         context: ExecutionContext
     ): IterableIterator<string>;
-    matcher: T;
+    matcher: Constructable<T>;
 }
 
-export const RepeatDirectiveRenderer: DirectiveRenderer<typeof RepeatDirective> = Object.freeze(
+export const RepeatDirectiveRenderer: ViewBehaviorFactoryRenderer<RepeatDirective> = Object.freeze(
     {
         matcher: RepeatDirective,
         *render(
-            directive: InstanceType<typeof RepeatDirective>,
+            directive: RepeatDirective,
             renderInfo: RenderInfo,
             source: any,
             renderer: TemplateRenderer,
@@ -69,27 +70,27 @@ export const RepeatDirectiveRenderer: DirectiveRenderer<typeof RepeatDirective>
 function* noop() {
     yield "";
 }
-export const ChildrenDirectiveRenderer: DirectiveRenderer<typeof ChildrenDirective> = Object.freeze(
+export const ChildrenDirectiveRenderer: ViewBehaviorFactoryRenderer<ChildrenDirective> = Object.freeze(
     {
         matcher: ChildrenDirective,
         render: noop,
     }
 );
 
-export const RefDirectiveRenderer: DirectiveRenderer<typeof RefDirective> = Object.freeze(
+export const RefDirectiveRenderer: ViewBehaviorFactoryRenderer<RefDirective> = Object.freeze(
     {
         matcher: RefDirective,
         render: noop,
     }
 );
-export const SlottedDirectiveRenderer: DirectiveRenderer<typeof SlottedDirective> = Object.freeze(
+export const SlottedDirectiveRenderer: ViewBehaviorFactoryRenderer<SlottedDirective> = Object.freeze(
     {
         matcher: SlottedDirective,
         render: noop,
     }
 );
 
-export const defaultFASTDirectiveRenderers: DirectiveRenderer<any>[] = [
+export const defaultViewBehaviorFactoryRenderers: ViewBehaviorFactoryRenderer<any>[] = [
     RepeatDirectiveRenderer,
     ChildrenDirectiveRenderer,
     RefDirectiveRenderer,
diff --git a/packages/web-components/fast-ssr/src/template-renderer/template-renderer.ts b/packages/web-components/fast-ssr/src/template-renderer/template-renderer.ts
index e18c880985c..3d2c2663d95 100644
--- a/packages/web-components/fast-ssr/src/template-renderer/template-renderer.ts
+++ b/packages/web-components/fast-ssr/src/template-renderer/template-renderer.ts
@@ -2,8 +2,9 @@ import { RenderInfo } from "@lit-labs/ssr";
 import { getElementRenderer } from "@lit-labs/ssr/lib/element-renderer.js";
 import {
     Aspect,
-    AspectedHTMLDirective,
+    Aspected,
     ExecutionContext,
+    ViewBehaviorFactory,
     ViewTemplate,
 } from "@microsoft/fast-element";
 import { Op, OpType } from "../template-parser/op-codes.js";
@@ -11,7 +12,7 @@ import {
     parseStringToOpCodes,
     parseTemplateToOpCodes,
 } from "../template-parser/template-parser.js";
-import { DirectiveRenderer } from "./directives.js";
+import { ViewBehaviorFactoryRenderer } from "./directives.js";
 
 export type ComponentDOMEmissionMode = "shadow" | "light";
 export interface TemplateRendererConfiguration {
@@ -32,7 +33,10 @@ export interface TemplateRendererConfiguration {
 
 export class TemplateRenderer
     implements Readonly<Required<TemplateRendererConfiguration>> {
-    private directiveRenderers: Map<any, DirectiveRenderer<any>> = new Map();
+    private viewBehaviorFactoryRenderers: Map<
+        any,
+        ViewBehaviorFactoryRenderer<any>
+    > = new Map();
     /**
      * {@inheritDoc TemplateRendererConfiguration.componentDOMEmissionMode}
      */
@@ -61,7 +65,7 @@ export class TemplateRenderer
         const codes =
             template instanceof ViewTemplate
                 ? parseTemplateToOpCodes(template)
-                : parseStringToOpCodes(template, []);
+                : parseStringToOpCodes(template, {});
 
         yield* this.renderOpCodes(codes, renderInfo, source, context);
     }
@@ -84,18 +88,15 @@ export class TemplateRenderer
                 case OpType.text:
                     yield code.value;
                     break;
-                case OpType.directive: {
-                    const { directive } = code;
-                    const ctor = directive.constructor;
-                    if (this.directiveRenderers.has(ctor)) {
-                        yield* this.directiveRenderers
+                case OpType.viewBehaviorFactory: {
+                    const factory = code.factory as ViewBehaviorFactory & Aspected;
+                    const ctor = factory.constructor;
+                    if (this.viewBehaviorFactoryRenderers.has(ctor)) {
+                        yield* this.viewBehaviorFactoryRenderers
                             .get(ctor)!
-                            .render(directive, renderInfo, source, this, context);
-                    } else if (
-                        directive instanceof AspectedHTMLDirective &&
-                        directive.binding
-                    ) {
-                        const result = directive.binding(source, context);
+                            .render(factory, renderInfo, source, this, context);
+                    } else if (factory.aspectType && factory.binding) {
+                        const result = factory.binding(source, context);
 
                         // If the result is a template, render the template
                         if (result instanceof ViewTemplate) {
@@ -103,7 +104,7 @@ export class TemplateRenderer
                         } else if (result === null || result === undefined) {
                             // Don't yield anything if result is null
                             break;
-                        } else if (directive.aspect === Aspect.content) {
+                        } else if (factory.aspectType === Aspect.content) {
                             yield result;
                         } else {
                             // debugging error - we should handle all result cases
@@ -113,7 +114,9 @@ export class TemplateRenderer
                         }
                     } else {
                         // Throw if a SSR directive implementation cannot be found.
-                        throw new Error(`Unable to process HTMLDirective: ${directive}`);
+                        throw new Error(
+                            `Unable to process view behavior factory: ${factory}`
+                        );
                     }
 
                     break;
@@ -255,11 +258,13 @@ export class TemplateRenderer
 
     /**
      * Registers DirectiveRenderers to use when rendering templates.
-     * @param directives - The directive renderers to register
+     * @param renderers - The directive renderers to register
      */
-    public withDirectiveRenderer(...directives: DirectiveRenderer<any>[]): void {
-        for (const renderer of directives) {
-            this.directiveRenderers.set(renderer.matcher, renderer);
+    public withViewBehaviorFactoryRenderers(
+        ...renderers: ViewBehaviorFactoryRenderer<any>[]
+    ): void {
+        for (const renderer of renderers) {
+            this.viewBehaviorFactoryRenderers.set(renderer.matcher, renderer);
         }
     }
 }
diff --git a/packages/web-components/fast-ssr/src/view.ts b/packages/web-components/fast-ssr/src/view.ts
index 95452d4991e..022aecf78fc 100644
--- a/packages/web-components/fast-ssr/src/view.ts
+++ b/packages/web-components/fast-ssr/src/view.ts
@@ -1,4 +1,7 @@
-import { HTMLDirective, HTMLTemplateCompilationResult } from "@microsoft/fast-element";
+import {
+    HTMLTemplateCompilationResult,
+    ViewBehaviorFactory,
+} from "@microsoft/fast-element";
 import { Op, OpType, TemplateElementOpenOp } from "./template-parser/op-codes.js";
 import { parseStringToOpCodes } from "./template-parser/template-parser.js";
 
@@ -10,16 +13,16 @@ import { parseStringToOpCodes } from "./template-parser/template-parser.js";
  */
 export class SSRView {
     public readonly html: string;
-    public readonly directives: ReadonlyArray<HTMLDirective>;
+    public readonly factories: Record<string, ViewBehaviorFactory>;
     public result: HTMLTemplateCompilationResult | null = null;
     public codes: Op[];
     public hostStaticAttributes?: TemplateElementOpenOp["staticAttributes"];
     public hostDynamicAttributes?: TemplateElementOpenOp["dynamicAttributes"];
 
-    constructor(html: string, directives: ReadonlyArray<HTMLDirective>) {
+    constructor(html: string, factories: Record<string, ViewBehaviorFactory>) {
         this.html = html;
-        this.directives = directives;
-        const codes = parseStringToOpCodes(html, directives);
+        this.factories = factories;
+        const codes = parseStringToOpCodes(html, factories);
 
         // Check to see if the root is a template element. We may need to
         // make this more sophisticated in the future because it doesn't quite