ember-ast-helpers
Version:
Utility belt to level-up your Ember AST transforms
158 lines (157 loc) • 8.46 kB
TypeScript
import { BuildAttrContent } from './html';
import { NodeVisitor, AST, Syntax } from '@glimmer/syntax';
export { default as interpolateProperties } from './build-time-component/interpolate-properties';
export declare type BuildTimeComponentOptions = {
tagName: string;
classNames: string[];
classNameBindings: string[];
attributeBindings: string[];
contentVisitor?: NodeVisitor;
[key: string]: any;
};
export declare type BuildTimeComponentNode = AST.MustacheStatement | AST.BlockStatement;
/**
* This is supposed to be the main abstraction used by most people to achieve most of their works
* Only when they want to do something extra the can override methods and do it themselves.
*
* It has some basic behaviour by default to remind how "real" ember components work, but very little.
* Namely, the `class` property is automatically bound to `class` attribute in the resulting HTMLElement.
* Also, if on initialization the user passes `classNames`, the classes in that array will be concatenated
* with the value passed to `class`.
* The user can also pass default values for the properties the component doesn't receive on invocation.
*
* That object has two main properties to help working with this abstraction useful.
*
* - `classNameBindings`: Identical behavior to the one in Ember components
* - `attributeBindings`: Almost identical behaviour to the one in Ember components, with one enhancements.
* Some attributes are expected to have regular values (p.e. the `title` attribute must have a string),
* so `{{my-component title=username}}` compiles to `<div title={{username}}></div>`.
* However, there is properties that are expected to be boolean that when converted to attributes
* should have other values. That is why you can pass `attributeBindings: ['isDisabled:disabled:no']`
* You will notice that in regular Ember components, the items in attribute bindings only have one `:`
* dividing propertyName and attributeName. If you put two semicolons dividing the string in three parts
* the third part will be used for the truthy value, generating in the example above `<div disabled={{if disabled 'no'}}></div>`
*
* More example usages:
*
* let component = new BuildTimeComponent(node); // creates the component
* component.toNode(); // generates the element with the right markup
*
* let soldier = new BuildTimeComponent(node, {
* classNameBindings: ['active:is-deployed:reservist'],
* attributeBindings: ['title', 'url:href', 'ariaHidden:aria-hidden:true']
* });
*/
export default class BuildTimeComponent {
private syntax;
private _defaultTagName;
private _defaultClassNames;
private _defaultClassNameBindings;
private _defaultAttributeBindings;
private _defaultPositionalParams;
private _contentVisitor?;
private _attrs?;
node: BuildTimeComponentNode;
options: Partial<BuildTimeComponentOptions>;
[key: string]: any;
constructor(syntax: Syntax, node: BuildTimeComponentNode, options?: Partial<BuildTimeComponentOptions>);
tagName: string;
readonly invocationAttrs: {
[key: string]: AST.Literal | AST.PathExpression | AST.SubExpression;
};
attributeBindings: string[];
classNames: string[];
classNameBindings: string[];
positionalParams: string[];
contentVisitor: NodeVisitor | undefined;
layout(args: TemplateStringsArray): void;
classContent(): BuildAttrContent | undefined;
/**
* Attribute bindings have this format: `<propName>:<attrName>:<truthyValue>`.
*
* These bindings can be of two types, boolean or regular.
*
* Boolean:
* - `attributeBinding: ['active:aria-active:on:off']`
* when true => `<div aria-active="on">`
* when false => `<div aria-active="off">`
* when dynamic => `<div aria-active={{if active 'on' 'off'}}>`
* - `attributeBinding: ['active:aria-active:on']`
* when true => `<div aria-active="on">`
* when false => `<div>`
* when dynamic => `<div aria-active={{if active 'on'}}>`
* - `attributeBinding: ['active:aria-active']` but we can determine statically that `active` is
* expected to be a boolean
* when true => `<div aria-active="true">`
* when false => `<div>`
* when dynamic => `<div aria-active={{if active 'true'}}>`
* - `attributeBinding: ['active']` but we can determine statically that `active` is expected
* to be a boolean:
* when true => `<div active="true">`
* when false => `<div>`
* when dynamic => `<div active={{if active 'true'}}>`
*
* Regular:
* - `attributeBinding: ['title']` and we can't determine that title is a boolean in compile time
* When the value is static => `<div title="some text">`
* When the value is dynamic => `<div title={{title}}>`
*/
readonly elementAttrs: AST.AttrNode[];
readonly elementModifiers: AST.ElementModifierStatement[];
readonly elementChildren: AST.Statement[];
toElement(): AST.ElementNode | AST.Statement[];
/**
* There is two possible kinds of classNameBindings: boolean or regular
*
* Boolean bindings are those that must be interpreted by the truthyness or falsyness of the
* property they are bound to.
*
* A bindings is deemed boolean when either of this conditions is met:
* - If the binding definition contains truthy or falsy values, it always considered boolean,
* regardless of the type of value on that property. E.g: `classNameBindings: ['enabled:on:off']`
*
* - If the binding has no truthy/falsy values but its property has been initialized to a boolean
* value, then it's reasonably safe that the developer expects it to be a boolean. In that case,
* just like Ember.Component does, the truthy value will be the dasherized name of the property,
* and when false, it won't have a false value.
* E.g. `new BuildTimeComponent(node, { classNameBindings: ['isActive'], isActive: true })` will
* generate `<div class="is-active"></div>`. If the component is invoked with a dynamic value
* on that property (`{{my-foo isActive=condition}}`) it generates `<div class={{if condition "is-active"}}></div>`
*
* - If the binding doesn't have truthy/falsy values, and the property hasn't been initialized to
* a boolean value, but the invocation passed the property as a boolean literal, it's also
* considered a boolean.
* E.g. `new BuildTimeComponent(node, { classNameBindings: ['isActive']})` invoked with
* `{{my-foo isActive=true}}` will generate `<div class="is-active"></div>`
*
* Regular bindings are simpler than that. If the value is just added to the class. If we can
* determine the value in compile time, it will generate `<div class="a b c propValue"></div>`,
* and if it can't, it will be interpolated `<div class="a b c {{prop}}"></div>`
*/
_applyClassNameBindings(content: AST.AttrNode['value'] | undefined): AST.AttrNode['value'] | undefined;
_analyzeBinding(binding: string, {propertyAlias}?: {
propertyAlias?: boolean;
}): {
isBooleanBinding: boolean;
computedValue: any;
staticValue: any;
invocationValue: AST.PathExpression | AST.SubExpression | AST.StringLiteral | AST.BooleanLiteral | AST.NumberLiteral | AST.UndefinedLiteral | AST.NullLiteral | undefined;
propName: string;
attrName: string | undefined;
truthyValue: string;
falsyValue: string;
};
_getPropertyValues(propName: string): {
invocationValue?: AST.PathExpression | AST.SubExpression | AST.StringLiteral | AST.BooleanLiteral | AST.NumberLiteral | AST.UndefinedLiteral | AST.NullLiteral | undefined;
computedValue?: any;
staticValue?: any;
};
_getPropertyValue(propName: string): any;
_transformElementChildren(node: AST.ElementNode): void;
_transformElementAttributes(node: AST.ElementNode): void;
_transformMustache(node: AST.MustacheStatement): string | AST.StringLiteral | AST.NumberLiteral | undefined;
_transformMustacheParams(node: AST.MustacheStatement): void;
_transformMustachePairs(node: AST.MustacheStatement): void;
_replaceYield(node: AST.MustacheStatement): AST.Statement[] | null | undefined;
_transformMustacheInCollection(siblings: AST.Statement[], i: number): boolean;
}