Component basics
Components are the building blocks for custom functionality in yuor theme. Each component is defined in src/components/, by a class which use the @Component() decorator and which extends ThemeComponent.
Each component may also optionally include a template and styles.
- MyComponent.ts
- MyComponent.liquid
- MyComponent.scss
src/components/MyComponent.tstypescriptimport {Component ,Prop ,Ref } from '@eastsideco/salvo-ts';import {ThemeComponent } from '@/theme/ThemeComponent';@Component export classMyComponent extendsThemeComponent {}
src/components/MyComponent.tstypescriptimport {Component ,Prop ,Ref } from '@eastsideco/salvo-ts';import {ThemeComponent } from '@/theme/ThemeComponent';@Component export classMyComponent extendsThemeComponent {}
src/components/MyComponent.liquidhtml<my-component></my-component>
src/components/MyComponent.liquidhtml<my-component></my-component>
src/components/MyComponent.scssscssmy-component {background: #CCC;border: #333;color: #000;}
src/components/MyComponent.scssscssmy-component {background: #CCC;border: #333;color: #000;}
This example component could be used anywhere in a theme as <my-component>.
The @Component decorator is attached to the class declaration following it. Most decorators provided by SALVO-TS also accept optional parameters if you call them like a function (i.e. @Component({ ... })).
Notice how the component name is formed of two words - in Typescript it's written in PascalCase, and in HTML its referred to using an element with the same name in kebab-case. These conventions are how the framework identifies and loads your component automatically.
Props
Props are essentially the inputs which your component can accept. For example, a "quantity spinner" component may have min, max, and currentValue props.
Props are defined on the component class and can be passed on the component element:
src/components/MyComponent.tstypescriptimport {Component ,Prop ,Ref } from '@eastsideco/salvo-ts';import {ThemeComponent } from '@/theme/ThemeComponent';@Component classQuantitySpinner extendsThemeComponent {@Prop currentValue !: number;// …}
src/components/MyComponent.tstypescriptimport {Component ,Prop ,Ref } from '@eastsideco/salvo-ts';import {ThemeComponent } from '@/theme/ThemeComponent';@Component classQuantitySpinner extendsThemeComponent {@Prop currentValue !: number;// …}
src/components/MyComponent.liquidhtml<quantity-spinnercurrent-value="1"></quantity-spinner>
src/components/MyComponent.liquidhtml<quantity-spinnercurrent-value="1"></quantity-spinner>
Defining props
Props are defined as part of the component class as normal class properties, marked up with the @Prop decorator:
typescript@Component export default classMyComponent extendsThemeComponent {// Basic definition@Prop propName !: string;// You can override the default attribute name@Prop ({attr : 'value' })currentValue !: string;// Props are required by default, unless you specify a default.@Prop ({default : 'user' })customerName !: string;@Prop ({default : null })customerEmail !: string|null;// Props can have (almost) any type@Prop name !: string;@Prop count !: number;@Prop countries !: string[];@Prop customer !:Customer ;}
typescript@Component export default classMyComponent extendsThemeComponent {// Basic definition@Prop propName !: string;// You can override the default attribute name@Prop ({attr : 'value' })currentValue !: string;// Props are required by default, unless you specify a default.@Prop ({default : 'user' })customerName !: string;@Prop ({default : null })customerEmail !: string|null;// Props can have (almost) any type@Prop name !: string;@Prop count !: number;@Prop countries !: string[];@Prop customer !:Customer ;}
Props are defined with the non-null assertion operator (!), because they're automatically populated by the framework, and therefore won't be initialized in the constructor normally.
Passing props
Props can be passed in on component elements as HTML attributes.
html<my-componentprop-name="test"customer-name="{{ customer.name|escape }}"countries="["GB", "FR", "DE"]"customer="{{ customer|json|escape }}"></my-component>
html<my-componentprop-name="test"customer-name="{{ customer.name|escape }}"countries="["GB", "FR", "DE"]"customer="{{ customer|json|escape }}"></my-component>
The attribute name for a given prop will by default be the same name converted to kebab-case, unless you specify otherwise in the prop arguments:
| Prop definition | Attribute name |
|---|---|
|
|
|
|
|
|
Simple props like strings can be passed directly in the attribute value, whereas complex props like objects need to be JSON-encoded:
| Prop definition | Attribute value |
|---|---|
|
|
|
|
|
|
Complex prop values need to be JSON-encoded first - for objects which already exist, you can usually pass them in directly using Liquid's |json and |escape filters:
ts@Prop product !:Shopify .Liquid .Product ;
ts@Prop product !:Shopify .Liquid .Product ;
html<my-componentproduct="{{ product|json|escape }}"></my-component>
html<my-componentproduct="{{ product|json|escape }}"></my-component>
For custom objects, you will need to construct the JSON first - the easiest way to do this often using {% capture %}:
ts@Prop example !: {key : string;value : number;};
ts@Prop example !: {key : string;value : number;};
html{% capture value %}{"key": "abc","value": 42}{% endcapture %}<my-componentexample="{{ value|escape }}"></my-component>
html{% capture value %}{"key": "abc","value": 42}{% endcapture %}<my-componentexample="{{ value|escape }}"></my-component>
You must always ensure that prop values are properly escaped. This means using the |json filter when constructing JSON objects, and using the |escape filter when passing unsafe values to attributes.
Custom classes as props
SALVO-TS also supports using custom classes for props. This allows you to pass JSON into a component, and then access the prop as a properly-initialized class instance.
Classes used in this way should implement SalvoSerializable so they have control over how they are parsed. Otherwise, the framework will open populate basic attribute (essentially running Object.assign(new CustomClass(), JSON.parse(attributeValue))).
typescriptimport {SalvoSerializable ,staticImplements } from '@eastsideco/salvo-ts';// Data which will be passed to the propinterfaceJsonData {foo : string;bar : 'enabled'|'disabled';}// The type that we want the prop to instantiate to@staticImplements <SalvoSerializable <JsonData >>()classMyComplexObject {foo : string;isBar : boolean = false;constructor(foo : string) {this.foo =foo ;}// Function which takes the data passed to the prop and returns and instance of this classstaticfromSalvoPropValue (value :JsonData ):MyComplexObject {letobj = newMyComplexObject (value .foo );obj .isBar =value .bar == 'enabled';returnobj ;}// Function which converts the current state back into the attribute valuetoSalvoPropValue ():JsonData {return {foo : this.foo ,bar : this.isBar ? 'enabled' : 'disabled'};}}
typescriptimport {SalvoSerializable ,staticImplements } from '@eastsideco/salvo-ts';// Data which will be passed to the propinterfaceJsonData {foo : string;bar : 'enabled'|'disabled';}// The type that we want the prop to instantiate to@staticImplements <SalvoSerializable <JsonData >>()classMyComplexObject {foo : string;isBar : boolean = false;constructor(foo : string) {this.foo =foo ;}// Function which takes the data passed to the prop and returns and instance of this classstaticfromSalvoPropValue (value :JsonData ):MyComplexObject {letobj = newMyComplexObject (value .foo );obj .isBar =value .bar == 'enabled';returnobj ;}// Function which converts the current state back into the attribute valuetoSalvoPropValue ():JsonData {return {foo : this.foo ,bar : this.isBar ? 'enabled' : 'disabled'};}}
typescript@Component export classMyComponent extendsThemeComponent {@Prop example !:MyComplexObject ;}
typescript@Component export classMyComponent extendsThemeComponent {@Prop example !:MyComplexObject ;}
html{% capture value %}{"foo": "lorem ipsum","bar": "enabled"}{% endcapture %}<my-componentexample="{{ value|escape }}"></my-component>
html{% capture value %}{"foo": "lorem ipsum","bar": "enabled"}{% endcapture %}<my-componentexample="{{ value|escape }}"></my-component>
Attribute reflection
Changes to props can be automatically reflected back to the prop attribute using attribute reflection.
If attribute reflection is enabled then:
- When the prop changes, the associated attribute will be updated with the new value.
- When the attribute changes, the value will be re-parsed and updated on the prop.
Simple props (i.e. number, string, boolean) are reflected by default. Complex props (i.e. objects) are not reflected by default.
These defaults can by overridden using the reflectAttr argument in the prop decorator:
typescript// Don't reflect this simple prop@Prop ({reflectAttr : false })score !: number;// Do reflect this complex prop@Prop ({reflectAttr : true })productData !:Shopify .Liquid .Product ;
typescript// Don't reflect this simple prop@Prop ({reflectAttr : false })score !: number;// Do reflect this complex prop@Prop ({reflectAttr : true })productData !:Shopify .Liquid .Product ;
Refs
Refs allow you to store references to child elements which appear inside of your component.
Refs are defined on the component class and identified by [data-ref] attributes on child elements.
src/components/MyComponent.tstypescriptimport {Component ,Prop ,Ref } from '@eastsideco/salvo-ts';import {ThemeComponent } from '@/theme/ThemeComponent';@Component export default classMyComponent extendsThemeComponent {@Ref greeting !:HTMLElement ;}
src/components/MyComponent.tstypescriptimport {Component ,Prop ,Ref } from '@eastsideco/salvo-ts';import {ThemeComponent } from '@/theme/ThemeComponent';@Component export default classMyComponent extendsThemeComponent {@Ref greeting !:HTMLElement ;}
src/components/MyComponent.liquidhtml<my-component><span data-ref="greeting">Hello there!</span></my-component>
src/components/MyComponent.liquidhtml<my-component><span data-ref="greeting">Hello there!</span></my-component>
Defining refs
Refs are defined as part of the component class as normal class properties, marked up with the @Ref decorator:
typescript@Component export default classMyComponent extendsThemeComponent {// Basic definition@Ref nameSpan !:HTMLElement ;// You can specify the specific type of element to benefit from better type checking@Ref nameInput !:HTMLInputElement ;// (deprecated) You can override the selector@Ref ({selector : '[data-ref="foo"]' })foobar !:HTMLElement ;}
typescript@Component export default classMyComponent extendsThemeComponent {// Basic definition@Ref nameSpan !:HTMLElement ;// You can specify the specific type of element to benefit from better type checking@Ref nameInput !:HTMLInputElement ;// (deprecated) You can override the selector@Ref ({selector : '[data-ref="foo"]' })foobar !:HTMLElement ;}
Passing refs
Refs are identified using [data-ref] attributes, added to the child element:
html<my-component><div data-ref="foo"></div><p>What is your name?<input data-ref="nameInput"/></p><p>Hello, <span data-ref="nameSpan"></span></p></my-component>
html<my-component><div data-ref="foo"></div><p>What is your name?<input data-ref="nameInput"/></p><p>Hello, <span data-ref="nameSpan"></span></p></my-component>
Other components can also be used as refs, which allows composite components to be created:
html<product-form>…<product-option-picker data-ref="options">…</product-option-picker><quantity-spinner data-ref="quantity">…</quantity-spinner></product-form>
html<product-form>…<product-option-picker data-ref="options">…</product-option-picker><quantity-spinner data-ref="quantity">…</quantity-spinner></product-form>
src/components/ProductForm.tstypescript@Component export classProductForm extendsThemeComponent {@Ref quantity !:QuantitySpinner ;@Ref options !:ProductOptionPicker ;asyncaddToCart () {constvariant = this.options .selectedVariant ; // Access child component props directlyconstquantity = this.quantity .val if (variant ) {await this.$cart .addItem ({id :variant .id ,quantity ,});}}}
src/components/ProductForm.tstypescript@Component export classProductForm extendsThemeComponent {@Ref quantity !:QuantitySpinner ;@Ref options !:ProductOptionPicker ;asyncaddToCart () {constvariant = this.options .selectedVariant ; // Access child component props directlyconstquantity = this.quantity .val if (variant ) {await this.$cart .addItem ({id :variant .id ,quantity ,});}}}
Optional refs
By default refs are required, but optional refs can be defined by passing the required argument to the ref decorator:
ts@Component export classProductForm extendsThemeComponent {// This element only exists if the reviews setting is turned on@Ref ({required : false })reviews !:ReviewStars |null;logReviewScore () {// Using an optional ref without checking it for null will result in an errorthis.Object is possibly 'null'.2531Object is possibly 'null'.$log .debug ('review score', this.reviews .score );}}
ts@Component export classProductForm extendsThemeComponent {// This element only exists if the reviews setting is turned on@Ref ({required : false })reviews !:ReviewStars |null;logReviewScore () {// Using an optional ref without checking it for null will result in an errorthis.Object is possibly 'null'.2531Object is possibly 'null'.$log .debug ('review score', this.reviews .score );}}
Array refs
Array refs allow you to capture multiple instances of an element in a single ref. For example, all of the slides in slideshow component.
html<product-option-buttons>{%- for option in options -%}<button data-ref="button">{{ option.value }}</button>{%- endfor -%}</product-option-buttons>
html<product-option-buttons>{%- for option in options -%}<button data-ref="button">{{ option.value }}</button>{%- endfor -%}</product-option-buttons>
ts@Component export classProductOptionButtons extendsThemeComponent {@Ref buttons !:HTMLButtonElement [];}
ts@Component export classProductOptionButtons extendsThemeComponent {@Ref buttons !:HTMLButtonElement [];}
Array refs are not inherently optional - if you want to allow zero matches, pass the required: false argument to the ref decorator.
Component decorator
The component decorator allows you to specify an alternative name for the component element, which may be useful when the generated name would be cumbersome:
- Original
- With custom name
ts@Component export classVATInput extendsThemeComponent {}
ts@Component export classVATInput extendsThemeComponent {}
html<v-a-t-input></v-a-t-input>
html<v-a-t-input></v-a-t-input>
ts@Component ({selector : 'vat-input',})export classVATInput extendsThemeComponent {}
ts@Component ({selector : 'vat-input',})export classVATInput extendsThemeComponent {}
html<vat-input></vat-input>
html<vat-input></vat-input>