UNPKG

ember-ast-helpers

Version:

Utility belt to level-up your Ember AST transforms

158 lines (157 loc) 8.46 kB
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; }