LogoLogo
HomeDiscourseBlogDiscord
  • Introduction
  • Introduction
    • Quick start
    • Aurelia for new developers
    • Hello world
      • Creating your first app
      • Your first component - part 1: the view model
      • Your first component - part 2: the view
      • Running our app
      • Next steps
  • Templates
    • Template Syntax
      • Attribute binding
      • Event binding
      • Text interpolation
      • Template promises
      • Template references
      • Template variables
      • Globals
    • Custom attributes
    • Value converters (pipes)
    • Binding behaviors
    • Form Inputs
    • CSS classes and styling
    • Conditional Rendering
    • List Rendering
    • Lambda Expressions
    • Local templates (inline templates)
    • SVG
  • Components
    • Component basics
    • Component lifecycles
    • Bindable properties
    • Styling components
    • Slotted content
    • Scope and context
    • CustomElement API
    • Template compilation
      • processContent
      • Extending templating syntax
      • Modifying template parsing with AttributePattern
      • Extending binding language
      • Using the template compiler
      • Attribute mapping
  • Getting to know Aurelia
    • Routing
      • @aurelia/router
        • Getting Started
        • Creating Routes
        • Routing Lifecycle
        • Viewports
        • Navigating
        • Route hooks
        • Router animation
        • Route Events
        • Router Tutorial
        • Router Recipes
      • @aurelia/router-lite
        • Getting started
        • Router configuration
        • Configuring routes
        • Viewports
        • Navigating
        • Lifecycle hooks
        • Router hooks
        • Router events
        • Navigation model
        • Current route
        • Transition plan
    • App configuration and startup
    • Enhance
    • Template controllers
    • Understanding synchronous binding
    • Dynamic composition
    • Portalling elements
    • Observation
      • Observing property changes with @observable
      • Effect observation
      • HTML observation
      • Using observerLocator
    • Watching data
    • Dependency injection (DI)
    • App Tasks
    • Task Queue
    • Event Aggregator
  • Developer Guides
    • Animation
    • Testing
      • Overview
      • Testing attributes
      • Testing components
      • Testing value converters
      • Working with the fluent API
      • Stubs, mocks & spies
    • Logging
    • Building plugins
    • Web Components
    • UI virtualization
    • Errors
      • Kernel Errors
      • Template Compiler Errors
      • Dialog Errors
      • Runtime HTML Errors
    • Bundlers
    • Recipes
      • Apollo GraphQL integration
      • Auth0 integration
      • Containerizing Aurelia apps with Docker
      • Cordova/Phonegap integration
      • CSS-in-JS with Emotion
      • DOM style injection
      • Firebase integration
      • Markdown integration
      • Multi root
      • Progress Web Apps (PWA's)
      • Securing an app
      • SignalR integration
      • Strongly-typed templates
      • TailwindCSS integration
      • WebSockets Integration
      • Web Workers Integration
    • Playground
      • Binding & Templating
      • Custom Attributes
        • Binding to Element Size
      • Integration
        • Microsoft FAST
        • Ionic
    • Migrating to Aurelia 2
      • For plugin authors
      • Side-by-side comparison
    • Cheat Sheet
  • Aurelia Packages
    • Validation
      • Validation Tutorial
      • Plugin Configuration
      • Defining & Customizing Rules
      • Architecture
      • Tagging Rules
      • Model Based Validation
      • Validation Controller
      • Validate Binding Behavior
      • Displaying Errors
      • I18n Internationalization
      • Migration Guide & Breaking Changes
    • i18n Internationalization
    • Fetch Client
      • Overview
      • Setup and Configuration
      • Response types
      • Working with forms
      • Intercepting responses & requests
      • Advanced
    • Event Aggregator
    • State
    • Store
      • Configuration and Setup
      • Middleware
    • Dialog
  • Tutorials
    • Building a ChatGPT inspired app
    • Building a realtime cryptocurrency price tracker
    • Building a todo application
    • Building a weather application
    • Building a widget-based dashboard
    • React inside Aurelia
    • Svelte inside Aurelia
    • Synthetic view
    • Vue inside Aurelia
  • Community Contribution
    • Joining the community
    • Code of conduct
    • Contributor guide
    • Building and testing aurelia
    • Writing documentation
    • Translating documentation
Powered by GitBook
On this page
  • Table of Contents
  • Core Methods
  • CustomElement.for
  • CustomElement.define
  • CustomElement.getDefinition
  • CustomElement.find
  • CustomElement.isType
  • Metadata Methods
  • CustomElement.annotate
  • CustomElement.getAnnotation
  • Utility Methods
  • CustomElement.generateName
  • CustomElement.generateType
  • CustomElement.createInjectable
  • CustomElement.keyFrom
  • Decorators
  • @customElement Decorator
  • @useShadowDOM Decorator
  • @containerless Decorator
  • @capture Decorator
  • @processContent Decorator
  • Definition Objects
  • PartialCustomElementDefinition
  • CustomElementDefinition
  • Best Practices
  • 1. Use Decorators Over Direct API Calls
  • 2. Type Your Controllers
  • 3. Handle Errors Gracefully
  • 4. Leverage Definition Objects for Complex Elements

Was this helpful?

Export as PDF
  1. Components

CustomElement API

The CustomElement resource is a fundamental concept in Aurelia 2, providing the core functionality for creating encapsulated and reusable components. This comprehensive guide covers all aspects of the CustomElement API, including methods, decorators, and configuration options.

Table of Contents

  • Core Methods

    • CustomElement.for

    • CustomElement.define

    • CustomElement.getDefinition

    • CustomElement.find

    • CustomElement.isType

  • Metadata Methods

    • CustomElement.annotate

    • CustomElement.getAnnotation

  • Utility Methods

    • CustomElement.generateName

    • CustomElement.generateType

    • CustomElement.createInjectable

    • CustomElement.keyFrom

  • Decorators

    • @customElement

    • @useShadowDOM

    • @containerless

    • @capture

    • @processContent

  • Definition Objects

    • PartialCustomElementDefinition

    • CustomElementDefinition

Core Methods

CustomElement.for

Retrieves the Aurelia controller associated with a DOM node. The controller provides access to the element's view model, lifecycle methods, and other properties.

Method Signatures

// Get controller for the current node
CustomElement.for<T>(node: Node): ICustomElementController<T>

// Get controller with optional flag (returns null if not found)
CustomElement.for<T>(node: Node, opts: { optional: true }): ICustomElementController<T> | null

// Search parent nodes for a controller
CustomElement.for<T>(node: Node, opts: { searchParents: true }): ICustomElementController<T>

// Get controller for a named custom element
CustomElement.for<T>(node: Node, opts: { name: string }): ICustomElementController<T> | undefined

// Get controller for a named custom element, searching parents
CustomElement.for<T>(node: Node, opts: { name: string; searchParents: true }): ICustomElementController<T> | undefined

Parameters

  • node: Node - The DOM node for which to retrieve the controller

  • opts?: object - Optional configuration object with the following properties:

    • optional?: boolean - If true, returns null instead of throwing when no controller is found

    • searchParents?: boolean - If true, searches parent nodes (including containerless elements) for a controller

    • name?: string - If provided, only returns controllers for custom elements with this specific name

Examples

import { CustomElement, ILogger } from 'aurelia';

// Basic usage - get controller for current node
const myElement = document.querySelector('.my-custom-element');
try {
  const controller = CustomElement.for(myElement);
  // You can inject ILogger in your classes for proper logging
  this.logger?.info('View model:', controller.viewModel);
  this.logger?.info('Element state:', controller.state);
} catch (error) {
  this.logger?.error('The provided node does not host a custom element.', error);
}

// Safe retrieval without throwing errors
const optionalController = CustomElement.for(myElement, { optional: true });
if (optionalController) {
  // Controller found and available for use
  optionalController.viewModel.someMethod();
} else {
  // No controller found, handle gracefully
  this.logger?.info('Node is not a custom element');
}

// Search parent hierarchy for any custom element controller
const someInnerElement = document.querySelector('.some-inner-element');
const parentController = CustomElement.for(someInnerElement, { searchParents: true });
// parentController is the closest controller up the DOM tree

// Get controller for a specific named custom element
const namedController = CustomElement.for(myElement, { name: 'my-custom-element' });
if (namedController) {
  // Found a controller for the specific element type
} else {
  // The node is not hosting the named custom element type
}

// Search parents for a specific named custom element
const namedParentController = CustomElement.for(someInnerElement, {
  name: 'my-custom-element',
  searchParents: true
});

// Access view model properties and methods
const controller = CustomElement.for(myElement);
const viewModel = controller.viewModel;
viewModel.myProperty = 'new value';
viewModel.myMethod();

// Access lifecycle state
this.logger?.info('Current state:', controller.state);
this.logger?.info('Is activated:', controller.isActive);

CustomElement.define

Registers a class as a custom element in Aurelia. This method can be called directly or is used internally by the @customElement decorator.

Method Signatures

// Define with name and class
CustomElement.define<T>(name: string, Type: Constructable<T>): CustomElementType<T>

// Define with definition object and class
CustomElement.define<T>(def: PartialCustomElementDefinition, Type: Constructable<T>): CustomElementType<T>

// Define with definition object only (generates type)
CustomElement.define<T>(def: PartialCustomElementDefinition): CustomElementType<T>

Parameters

  • nameOrDef: string | PartialCustomElementDefinition - Either the element name or a complete definition object

  • Type?: Constructable - The class containing the element's logic (optional when using definition object)

Examples

import { CustomElement } from 'aurelia';

// Basic definition with name and class
class MyCustomElement {
  public message = 'Hello, World!';

  public greet() {
    alert(this.message);
  }
}

CustomElement.define('my-custom-element', MyCustomElement);

// Definition with complete configuration object
const definition = {
  name: 'advanced-element',
  template: '<template><h1>${title}</h1><div class="content"><au-slot></au-slot></div></template>',
  bindables: ['title', 'size'],
  shadowOptions: { mode: 'open' },
  containerless: false,
  capture: true,
  dependencies: []
};

class AdvancedElement {
  public title = '';
  public size = 'medium';
}

CustomElement.define(definition, AdvancedElement);

// Definition without explicit type (generates anonymous class)
const simpleDefinition = {
  name: 'simple-element',
  template: '<template><p>${text}</p></template>',
  bindables: ['text']
};

const SimpleElementType = CustomElement.define(simpleDefinition);

// Note: Using @customElement decorator is preferred over calling define directly
@customElement('my-element')
class MyElement {
  // This is equivalent to calling CustomElement.define('my-element', MyElement)
}

CustomElement.getDefinition

Retrieves the CustomElementDefinition for a custom element class, providing access to all metadata about the element.

Method Signature

CustomElement.getDefinition<T>(Type: Constructable<T>): CustomElementDefinition<T>

Parameters

  • Type: Constructable - The custom element class

Return Value

Returns a CustomElementDefinition object containing all metadata about the custom element.

Example

import { CustomElement, ILogger } from 'aurelia';

@customElement({
  name: 'my-element',
  template: '<template>${message}</template>',
  bindables: ['message']
})
class MyElement {
  public message = '';

  constructor(private logger: ILogger) {}

  public logDefinitionInfo() {
    const definition = CustomElement.getDefinition(MyElement);

    this.logger.info('Element name:', definition.name); // 'my-element'
    this.logger.info('Template:', definition.template);
    this.logger.info('Bindables:', definition.bindables);
    this.logger.info('Is containerless:', definition.containerless);
    this.logger.info('Shadow options:', definition.shadowOptions);
    this.logger.info('Dependencies:', definition.dependencies);
    this.logger.info('Aliases:', definition.aliases);
    this.logger.info('Capture mode:', definition.capture);
  }
}

CustomElement.find

Searches for a custom element definition by name within a specific container's registry.

Method Signature

CustomElement.find(container: IContainer, name: string): CustomElementDefinition | null

Parameters

  • container: IContainer - The dependency injection container to search in

  • name: string - The name of the custom element to find

Return Value

Returns the CustomElementDefinition if found, or null if no element with the specified name is registered.

Example

import { CustomElement, IContainer, ILogger } from 'aurelia';

// In a custom service or component
class MyService {
  constructor(private container: IContainer, private logger: ILogger) {}

  public checkElementExists(elementName: string): boolean {
    const definition = CustomElement.find(this.container, elementName);
    return definition !== null;
  }

  public getElementTemplate(elementName: string): string | null {
    const definition = CustomElement.find(this.container, elementName);
    return definition?.template as string || null;
  }
}

// Usage in template compiler or dynamic composition
class SomeComponent {
  constructor(private logger: ILogger) {}

  public checkDynamicElement(container: IContainer) {
    const definition = CustomElement.find(container, 'my-dynamic-element');
    if (definition) {
      // Element is registered and available for use
      this.logger.info('Found element:', definition.name);
    } else {
      // Element not found in current container
      this.logger.warn('Element not registered');
    }
  }
}

CustomElement.isType

Checks whether a given value is a custom element type (class decorated with @customElement or defined via CustomElement.define).

Method Signature

CustomElement.isType<T>(value: T): value is CustomElementType<T>

Parameters

  • value: any - The value to check

Return Value

Returns true if the value is a custom element type, false otherwise.

Example

import { CustomElement, customElement, ILogger } from 'aurelia';

@customElement('my-element')
class MyElement {}

class RegularClass {}

// Service class that performs type checking
class TypeCheckingService {
  constructor(private logger: ILogger) {}

  public demonstrateTypeChecking() {
    // Type checking
    this.logger.info('MyElement is custom element type:', CustomElement.isType(MyElement)); // true
    this.logger.info('RegularClass is custom element type:', CustomElement.isType(RegularClass)); // false
    this.logger.info('String is custom element type:', CustomElement.isType('string')); // false
    this.logger.info('Number is custom element type:', CustomElement.isType(42)); // false
  }

  // Usage in dynamic scenarios
  public processComponent(component: unknown) {
    if (CustomElement.isType(component)) {
      // Safe to use as custom element
      const definition = CustomElement.getDefinition(component);
      this.logger.info('Processing element:', definition.name);
    } else {
      this.logger.info('Not a custom element type');
    }
  }
}

Metadata Methods

CustomElement.annotate

Attaches metadata to a custom element class. This is typically used internally by decorators and the framework.

Method Signature

CustomElement.annotate<K extends keyof PartialCustomElementDefinition>(
  Type: Constructable,
  prop: K,
  value: PartialCustomElementDefinition[K]
): void

Parameters

  • Type: Constructable - The custom element class to annotate

  • prop: string - The property key for the annotation

  • value: any - The value to associate with the property

Example

import { CustomElement } from 'aurelia';

class MyElement {}

// Manually annotate the class (decorators do this automatically)
CustomElement.annotate(MyElement, 'template', '<template>${message}</template>');
CustomElement.annotate(MyElement, 'bindables', ['message']);
CustomElement.annotate(MyElement, 'containerless', true);

CustomElement.getAnnotation

Retrieves metadata that was previously attached to a custom element class.

Method Signature

CustomElement.getAnnotation<K extends keyof PartialCustomElementDefinition>(
  Type: Constructable,
  prop: K
): PartialCustomElementDefinition[K] | undefined

Parameters

  • Type: Constructable - The custom element class

  • prop: string - The property key to retrieve

Return Value

Returns the annotation value, or undefined if not found.

Example

import { CustomElement, customElement, ILogger } from 'aurelia';

@customElement({
  name: 'annotated-element',
  template: '<template>${content}</template>'
})
class AnnotatedElement {
  constructor(private logger: ILogger) {}

  public logAnnotations() {
    // Retrieve annotations
    const template = CustomElement.getAnnotation(AnnotatedElement, 'template');
    const bindables = CustomElement.getAnnotation(AnnotatedElement, 'bindables');

    this.logger.info('Template:', template);
    this.logger.info('Bindables:', bindables);
  }
}

Utility Methods

CustomElement.generateName

Generates a unique name for a custom element, useful for anonymous or dynamically created elements.

Method Signature

CustomElement.generateName(): string

Return Value

A string representing a unique name (typically in the format unnamed-{number}).

Example

import { CustomElement } from 'aurelia';

// Generate unique names for dynamic elements
const uniqueName1 = CustomElement.generateName(); // 'unnamed-1'
const uniqueName2 = CustomElement.generateName(); // 'unnamed-2'

// Use with dynamic element creation
class DynamicElement {
  public data = '';
}

const DynamicElementType = CustomElement.define(uniqueName1, DynamicElement);

CustomElement.generateType

Dynamically generates a CustomElementType with a given name and prototype properties.

Method Signature

CustomElement.generateType<P extends object = object>(
  name: string,
  proto?: P
): CustomElementType<Constructable<P>>

Parameters

  • name: string - The name for the generated type

  • proto?: object - An optional object containing properties and methods to add to the prototype

Return Value

A CustomElementType that can be used with CustomElement.define.

Example

import { CustomElement } from 'aurelia';

// Generate a type with custom properties and methods
const DynamicElement = CustomElement.generateType('dynamic-element', {
  message: 'Hello from Dynamic Element!',
  count: 0,

  increment() {
    this.count++;
  },

  showMessage() {
    alert(`${this.message} Count: ${this.count}`);
  }
});

// Define the generated type
CustomElement.define('dynamic-element', DynamicElement);

// Usage in templates: <dynamic-element></dynamic-element>

CustomElement.createInjectable

Creates an InjectableToken for dependency injection scenarios.

Method Signature

CustomElement.createInjectable<T = any>(): InterfaceSymbol<T>

Return Value

An InterfaceSymbol that can be used as a dependency injection token.

Example

import { CustomElement } from 'aurelia';

// Create injectable tokens for custom scenarios
const MyServiceToken = CustomElement.createInjectable<MyService>();

// Use in dependency injection
class MyElement {
  constructor(@MyServiceToken private service: MyService) {}
}

CustomElement.keyFrom

Generates the registry key used internally to store and retrieve custom element definitions.

Method Signature

CustomElement.keyFrom(name: string): string

Parameters

  • name: string - The custom element name

Return Value

A string representing the internal registry key.

Example

import { CustomElement, ILogger } from 'aurelia';

class KeyGeneratorService {
  constructor(private logger: ILogger) {}

  public demonstrateKeyGeneration() {
    const key = CustomElement.keyFrom('my-element');
    this.logger.info(key); // 'au:ce:my-element' (internal format)

    // Used internally for container registration/lookup
    const hasElement = container.has(CustomElement.keyFrom('my-element'));
  }
}

Decorators

@customElement Decorator

The primary decorator for marking a class as a custom element.

Syntax

@customElement(name: string)
@customElement(definition: PartialCustomElementDefinition)

Examples

import { customElement } from 'aurelia';

// Simple name-based definition
@customElement('hello-world')
class HelloWorld {
  public message = 'Hello, World!';
}

// Full definition object
@customElement({
  name: 'advanced-component',
  template: `
    <template>
      <h1>\${title}</h1>
      <div class="content">
        <au-slot></au-slot>
      </div>
    </template>
  `,
  bindables: ['title', 'theme'],
  shadowOptions: { mode: 'open' },
  dependencies: []
})
class AdvancedComponent {
  public title = '';
  public theme = 'light';
}

@useShadowDOM Decorator

Enables Shadow DOM for the custom element.

Syntax

@useShadowDOM(options?: { mode: 'open' | 'closed' })

Example

import { customElement, useShadowDOM } from 'aurelia';

@customElement('shadow-element')
@useShadowDOM({ mode: 'open' })
class ShadowElement {
  // This element will render in Shadow DOM
}

// Or with default mode (open)
@customElement('shadow-element-simple')
@useShadowDOM()
class ShadowElementSimple {}

@containerless Decorator

Renders the custom element without its element container.

Syntax

@containerless()
@containerless(target: Constructable, context: ClassDecoratorContext)

Example

import { customElement, containerless } from 'aurelia';

@customElement('invisible-wrapper')
@containerless()
class InvisibleWrapper {
  // This element won't create its own DOM node
  // Only its content will be rendered
}

// Usage: <invisible-wrapper>content</invisible-wrapper>
// Renders: content (without the wrapper element)

@capture Decorator

Enables capturing of all attributes and bindings that are not explicitly defined as bindables or template controllers.

Syntax

@capture()
@capture(filter: (attr: string) => boolean)

Example

import { customElement, capture } from 'aurelia';

@customElement('flexible-element')
@capture() // Capture all unrecognized attributes
class FlexibleElement {
  // Any attribute not defined as bindable will be captured
}

@customElement('filtered-element')
@capture((attrName) => attrName.startsWith('data-'))
class FilteredElement {
  // Only capture attributes that start with 'data-'
}

@processContent Decorator

Defines a hook that processes the element's content before compilation.

Syntax

@processContent(hook: ProcessContentHook)
@processContent(methodName: string | symbol)
@processContent() // Decorator for static method

Example

import { customElement, processContent, IPlatform } from 'aurelia';

@customElement('content-processor')
class ContentProcessor {
  @processContent()
  static processContent(node: HTMLElement, platform: IPlatform): boolean | void {
    // Modify the element's content before compilation
    const children = Array.from(node.children);
    children.forEach(child => {
      if (child.tagName === 'SPECIAL') {
        child.setAttribute('processed', 'true');
      }
    });
    return true; // Continue with normal compilation
  }
}

// Or reference a method by name
@customElement('named-processor')
@processContent('customProcessor')
class NamedProcessor {
  static customProcessor(node: HTMLElement, platform: IPlatform): boolean | void {
    // Process content
    return true;
  }
}

Definition Objects

PartialCustomElementDefinition

An object that describes a custom element's configuration. All properties are optional.

Properties

interface PartialCustomElementDefinition {
  name?: string;                    // Element name (kebab-case)
  template?: string | Node | null;  // HTML template
  bindables?: string[] | object;    // Bindable properties
  dependencies?: any[];             // Required dependencies
  aliases?: string[];               // Alternative names
  containerless?: boolean;          // Render without container
  shadowOptions?: { mode: 'open' | 'closed' } | null; // Shadow DOM options
  hasSlots?: boolean;              // Has <au-slot> elements
  capture?: boolean | ((attr: string) => boolean); // Capture unbound attributes
  enhance?: boolean;               // Enhance existing DOM
  instructions?: any[][];          // Template instructions
  surrogates?: any[];             // Surrogate instructions
  needsCompile?: boolean;         // Requires compilation
  injectable?: any;               // DI token
  watches?: any[];               // Property watchers
  strict?: boolean;              // Strict binding mode
  processContent?: Function;     // Content processing hook
}

Example

const elementDefinition: PartialCustomElementDefinition = {
  name: 'my-component',
  template: `
    <template>
      <h1>\${title}</h1>
      <p class="description">\${description}</p>
      <div class="actions">
        <au-slot name="actions"></au-slot>
      </div>
    </template>
  `,
  bindables: ['title', 'description'],
  shadowOptions: { mode: 'open' },
  containerless: false,
  hasSlots: true,
  capture: false,
  dependencies: [],
  aliases: ['my-comp']
};

CustomElementDefinition

The complete, resolved definition of a custom element (read-only).

Key Properties

interface CustomElementDefinition {
  readonly Type: CustomElementType;      // The element class
  readonly name: string;                 // Element name
  readonly template: string | Node | null; // Compiled template
  readonly bindables: Record<string, BindableDefinition>; // Resolved bindables
  readonly aliases: string[];            // Alternative names
  readonly key: string;                  // Registry key
  readonly containerless: boolean;       // Container rendering mode
  readonly shadowOptions: { mode: 'open' | 'closed' } | null; // Shadow DOM
  readonly hasSlots: boolean;           // Contains slots
  readonly capture: boolean | Function; // Attribute capturing
  readonly enhance: boolean;            // DOM enhancement mode
  readonly dependencies: any[];         // Required dependencies
  readonly instructions: any[][];       // Template instructions
  readonly surrogates: any[];          // Surrogate instructions
  readonly needsCompile: boolean;      // Compilation requirement
  readonly watches: any[];             // Property watchers
  readonly strict: boolean | undefined; // Strict binding mode
  readonly processContent: Function | null; // Content processor
}

Best Practices

1. Use Decorators Over Direct API Calls

// Preferred
@customElement('my-element')
class MyElement {}

// Avoid unless in dynamic scenarios
CustomElement.define('my-element', MyElement);

2. Type Your Controllers

interface MyElementViewModel {
  title: string;
  count: number;
  increment(): void;
}

const controller = CustomElement.for<MyElementViewModel>(element);
controller.viewModel.increment(); // Fully typed

3. Handle Errors Gracefully

// Use optional flag when controller might not exist
const controller = CustomElement.for(element, { optional: true });
if (controller) {
  // Safe to use
} else {
  // Handle missing controller
}

4. Leverage Definition Objects for Complex Elements

@customElement({
  name: 'complex-element',
  template: complexTemplate,
  shadowOptions: { mode: 'open' },
  bindables: ['data', 'config'],
  dependencies: [SomeService, AnotherDependency]
})
class ComplexElement {}
PreviousScope and contextNextTemplate compilation

Last updated 4 days ago

Was this helpful?