Skip to content

Instantly share code, notes, and snippets.

@lifeart

lifeart/2048.gts Secret

Last active June 28, 2024 08:12
Show Gist options
  • Save lifeart/25e1b0d11cd67a5e29059b18ff22dc7c to your computer and use it in GitHub Desktop.
Save lifeart/25e1b0d11cd67a5e29059b18ff22dc7c to your computer and use it in GitHub Desktop.
autocode
import { Component, cellFor, tracked } from '@lifeart/gxt';
const GRID_SIZE = 4;
class Cell extends Component<{
Args: {
value: number;
};
}> {
get color() {
switch (this.args.value) {
case 2:
return 'bg-yellow-300';
case 4:
return 'bg-yellow-400';
case 8:
return 'bg-yellow-500';
case 16:
return 'bg-orange-400';
case 32:
return 'bg-orange-500';
case 64:
return 'bg-orange-600';
case 128:
return 'bg-red-400';
case 256:
return 'bg-red-500';
case 512:
return 'bg-red-600';
case 1024:
return 'bg-purple-400';
case 2048:
return 'bg-purple-500';
default:
return 'bg-gray-200';
}
}
<template>
<div
class='w-16 h-16 flex items-center justify-center text-2xl font-bold rounded transition-all duration-300 ease-in-out
{{this.color}}'
>
{{@value}}
</div>
</template>
}
export class Game2048 extends Component {
@tracked grid: { value: number }[][] = [];
constructor(args: {}) {
super(args);
this.initGrid();
this.generateNewTile();
this.generateNewTile();
}
initGrid() {
this.grid = [];
for (let i = 0; i < GRID_SIZE; i++) {
this.grid[i] = [];
for (let j = 0; j < GRID_SIZE; j++) {
const cell = {
value: 0,
};
cellFor(cell, 'value');
this.grid[i][j] = cell;
}
}
}
generateNewTile() {
const emptyCells = [];
for (let i = 0; i < GRID_SIZE; i++) {
for (let j = 0; j < GRID_SIZE; j++) {
if (this.grid[i][j].value === 0) {
emptyCells.push({ x: i, y: j });
}
}
}
if (emptyCells.length > 0) {
const randomIndex = Math.floor(Math.random() * emptyCells.length);
const { x, y } = emptyCells[randomIndex];
this.grid[x][y].value = Math.random() < 0.9 ? 2 : 4;
}
// this.grid = [...this.grid];
}
handleKeyDown = (e: KeyboardEvent) => {
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
this.move(e.key);
}
};
move(direction: string) {
let moved = false;
switch (direction) {
case 'ArrowUp':
moved = this.moveUp();
break;
case 'ArrowDown':
moved = this.moveDown();
break;
case 'ArrowLeft':
moved = this.moveLeft();
break;
case 'ArrowRight':
moved = this.moveRight();
break;
}
if (moved) {
this.generateNewTile();
}
}
moveUp() {
let moved = false;
for (let j = 0; j < GRID_SIZE; j++) {
for (let i = 1; i < GRID_SIZE; i++) {
if (this.grid[i][j].value !== 0) {
let k = i;
while (k > 0 && this.grid[k - 1][j].value === 0) {
this.grid[k - 1][j].value = this.grid[k][j].value;
this.grid[k][j].value = 0;
k--;
moved = true;
}
if (k > 0 && this.grid[k - 1][j].value === this.grid[k][j].value) {
this.grid[k - 1][j].value *= 2;
this.grid[k][j].value = 0;
moved = true;
}
}
}
}
return moved;
}
moveDown() {
let moved = false;
for (let j = 0; j < GRID_SIZE; j++) {
for (let i = GRID_SIZE - 2; i >= 0; i--) {
if (this.grid[i][j].value !== 0) {
let k = i;
while (k < GRID_SIZE - 1 && this.grid[k + 1][j].value === 0) {
this.grid[k + 1][j].value = this.grid[k][j].value;
this.grid[k][j].value = 0;
k++;
moved = true;
}
if (
k < GRID_SIZE - 1 &&
this.grid[k + 1][j].value === this.grid[k][j].value
) {
this.grid[k + 1][j].value *= 2;
this.grid[k][j].value = 0;
moved = true;
}
}
}
}
return moved;
}
moveLeft() {
let moved = false;
for (let i = 0; i < GRID_SIZE; i++) {
for (let j = 1; j < GRID_SIZE; j++) {
if (this.grid[i][j].value !== 0) {
let k = j;
while (k > 0 && this.grid[i][k - 1].value === 0) {
this.grid[i][k - 1].value = this.grid[i][k].value;
this.grid[i][k].value = 0;
k--;
moved = true;
}
if (k > 0 && this.grid[i][k - 1].value === this.grid[i][k].value) {
this.grid[i][k - 1].value *= 2;
this.grid[i][k].value = 0;
moved = true;
}
}
}
}
return moved;
}
moveRight() {
let moved = false;
for (let i = 0; i < GRID_SIZE; i++) {
for (let j = GRID_SIZE - 2; j >= 0; j--) {
if (this.grid[i][j].value !== 0) {
let k = j;
while (k < GRID_SIZE - 1 && this.grid[i][k + 1].value === 0) {
this.grid[i][k + 1].value = this.grid[i][k].value;
this.grid[i][k].value = 0;
k++;
moved = true;
}
if (
k < GRID_SIZE - 1 &&
this.grid[i][k + 1].value === this.grid[i][k].value
) {
this.grid[i][k + 1].value *= 2;
this.grid[i][k].value = 0;
moved = true;
}
}
}
}
return moved;
}
get gridForRender() {
console.log('gridForRender');
console.table(this.grid);
return this.grid;
}
<template>
<div
class='container mx-auto p-4'
tabindex='0'
{{on 'keydown' this.handleKeyDown}}
>
<h1 class='text-4xl font-bold mb-4'>2048</h1>
<div class='grid grid-rows-4 gap-4'>
{{#each this.gridForRender as |row|}}
<div class='flex gap-4'>
{{#each row as |cell|}}
<Cell @value={{cell.value}} />
{{/each}}
</div>
{{/each}}
</div>
</div>
</template>
}
import { Component, tracked, cellFor } from '@lifeart/gxt';
const events = [
{
id: 1,
title: 'Summer Music Festival',
minPrice: 45,
imageUrl: 'https://via.placeholder.com/300x200',
description: 'Enjoy a weekend of live music, food, and fun!',
},
{
id: 2,
title: 'Art Exhibition Opening',
minPrice: 20,
imageUrl: 'https://via.placeholder.com/300x200',
description: 'Explore the latest works from emerging artists.',
},
{
id: 3,
title: 'Tech Conference',
minPrice: 150,
imageUrl: 'https://via.placeholder.com/300x200',
description: 'Learn from industry experts and network with peers.',
},
// Add more event data as needed
];
export class EventCard extends Component<{
Args: {
event: {
id: number;
title: string;
minPrice: number;
imageUrl: string;
description: string;
detailedDescription: string; // Add a field for detailed description
};
onToggleFavorite: (eventId: number) => void;
};
}> {
@tracked isFavorite = false;
constructor(args: any) {
super(args);
this.isFavorite = !!localStorage.getItem(`event-${this.args.event.id}`);
}
toggleFavorite = () => {
this.isFavorite = !this.isFavorite;
if (this.isFavorite) {
localStorage.setItem(`event-${this.args.event.id}`, 'true');
} else {
localStorage.removeItem(`event-${this.args.event.id}`);
}
this.args.onToggleFavorite(this.args.event.id);
};
<template>
<div class='bg-white rounded-lg shadow p-4 mb-4 relative'>
<img
src={{@event.imageUrl}}
alt={{@event.title}}
class='w-full h-48 object-cover mb-2 rounded-t-lg'
/>
<h2 class='text-lg font-medium'>{{@event.title}}</h2>
<p class='text-gray-600 mb-2'>From ${{@event.minPrice}}</p>
<p class='text-sm text-gray-700 line-clamp-3'>{{@event.description}}</p>
<button
class='bg-blue-500 text-white px-3 py-2 rounded mt-2'
{{on 'click' this.toggleFavorite}}
>
{{if this.isFavorite 'Remove Favorite' 'Add to Favorites'}}
</button>
</div>
</template>
}
export class EventDialog extends Component<{
Args: {
event: {
title: string;
detailedDescription: string;
imageUrl: string;
minPrice: number;
};
isOpen: boolean;
onClose: () => void;
};
Element: HTMLDivElement;
}> {
handleClick = (event: Event) => {
// Only close if the click is outside the dialog content
if (event.target === this.element) {
this.args.onClose();
}
};
<template>
<div
class='fixed inset-0 z-10 flex items-center justify-center bg-black bg-opacity-50'
{{on 'click' this.handleClick}}
...attributes
>
<div
class='bg-white w-full max-w-md rounded-lg overflow-hidden shadow-lg'
>
<img
src={{@event.imageUrl}}
alt={{@event.title}}
class='w-full h-48 object-cover'
/>
<div class='p-4'>
<h2 class='text-xl font-medium mb-2'>{{@event.title}}</h2>
<p class='text-gray-700 mb-4'>{{@event.detailedDescription}}</p>
<p class='text-gray-600 mb-2'>From ${{@event.minPrice}}</p>
<button
class='bg-green-500 text-white px-4 py-2 rounded'
>Purchase</button>
<button
class='bg-gray-300 text-gray-700 px-4 py-2 rounded ml-2'
{{on 'click' @onClose}}
>Close</button>
</div>
</div>
</div>
</template>
}
export class Panel extends Component {
@tracked events = events.map((event) => {
cellFor(event, 'isFavorite');
event.isFavorite = !!localStorage.getItem(`event-${event.id}`);
return event;
});
@tracked selectedEvent: {
id: number;
title: string;
minPrice: number;
imageUrl: string;
description: string;
detailedDescription: string; // Add a field for detailed description
} | null = null;
@tracked showDialog = false;
toggleFavorite = (eventId: number) => {
const event = this.events.find((e) => e.id === eventId);
if (event) {
event.isFavorite = !event.isFavorite;
if (event.isFavorite) {
localStorage.setItem(`event-${eventId}`, 'true');
} else {
localStorage.removeItem(`event-${eventId}`);
}
}
};
openDialog = (event: {
id: number;
title: string;
minPrice: number;
imageUrl: string;
description: string;
detailedDescription: string;
}) => {
this.selectedEvent = event;
this.showDialog = true;
};
closeDialog = () => {
this.showDialog = false;
};
<template>
<div class='bg-gray-100 p-4'>
<h1 class='text-2xl font-bold mb-4'>Upcoming Events</h1>
<div class='grid grid-cols-1 gap-4'>
{{#each this.events as |event|}}
<EventCard
@event={{event}}
@onToggleFavorite={{this.toggleFavorite}}
/>
<button
class='bg-blue-500 text-white px-3 py-2 rounded mt-2'
{{on 'click' (fn this.openDialog event)}}
>View</button>
{{/each}}
</div>
{{#if this.showDialog}}
<EventDialog
@event={{this.selectedEvent}}
@isOpen={{this.showDialog}}
@onClose={{this.closeDialog}}
/>
{{/if}}
</div>
</template>
}
import { Component, cellFor, tracked } from '@lifeart/gxt';
type Note = {
id: string;
content: string;
x: number;
y: number;
};
type AppSignature = {
Args: {};
Element: HTMLDivElement;
Blocks: {};
};
function px(value: number) {
return `${value}px`;
}
export class Panel extends Component<AppSignature> {
@tracked notes: Note[] = [];
@tracked newNoteContent = '';
@tracked draggingNote: Note | null = null;
@tracked offsetX = 0;
@tracked offsetY = 0;
get nextNoteId() {
return crypto.randomUUID();
}
addNote = () => {
const newNote = {
id: this.nextNoteId,
content: this.newNoteContent,
x: 100,
y: 100,
};
cellFor(newNote, 'x');
cellFor(newNote, 'y');
cellFor(newNote, 'content');
this.notes = [...this.notes, newNote];
this.newNoteContent = '';
};
updateNoteContent = (id: string, newContent: string) => {
this.notes.find((note) => note.id === id)!.content = newContent;
};
updateNewNoteContent = (e: Event) => {
this.newNoteContent = (e.target as HTMLInputElement).value;
};
handleNoteMouseDown = (note: Note, e: MouseEvent) => {
console.log('handleNoteMouseDown', note);
this.draggingNote = note;
this.offsetX = e.clientX - note.x;
this.offsetY = e.clientY - note.y;
};
handleMouseMove = (e: MouseEvent) => {
if (this.draggingNote) {
this.draggingNote.x = e.clientX - this.offsetX;
this.draggingNote.y = e.clientY - this.offsetY;
}
};
handleMouseUp = () => {
this.draggingNote = null;
};
<template>
<div
class='app'
{{on 'mousemove' this.handleMouseMove}}
{{on 'mouseup' this.handleMouseUp}}
>
<div class='input-area'>
<input
type='text'
placeholder='Enter note content'
value={{this.newNoteContent}}
{{on 'input' this.updateNewNoteContent}}
/>
<button {{on 'click' this.addNote}}>Add Note</button>
</div>
{{#each this.notes as |note|}}
<div
class='note'
style.top={{px note.y}}
style.left={{px note.x}}
{{on 'mousedown' (fn this.handleNoteMouseDown note)}}
>
<textarea
{{on 'input' (fn this.updateNoteContent note.id)}}
>{{note.content}}</textarea>
</div>
{{/each}}
</div>
<style>
.app { width: 100vw; height: 100vh; position: relative; } .input-area {
padding: 20px; background-color: #f5f5f5; border-bottom: 1px solid #ddd; }
.note { position: absolute; width: 200px; background-color: yellow;
padding: 10px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0, 0, 0,
0.1); resize: both; overflow: auto; } .note textarea { width: 100%;
height: 100px; border: none; resize: none; background-color: transparent;
}</style>
</template>
}

Hello LLM! Here is a concept of framework I would like you to to help me with:

Imports and Module Syntax:

Components, functions, and other utilities are imported from the framework and other modules.

Example: import { Component, tracked } from '@lifeart/gxt';

Component Definition:

Components are defined as ES6 classes extending the Component base class. Components can have tracked properties, methods, and a <template> block. Components has .gts extensions. It mean GlimmerTypeScript. Scope inside tag is not JavaScript, it's handlebars-like syntax. Scope outside tag is latest TypeScript, you may use any language features. Bultin helpers could not be imported from '@lifeart/gxt', it's in global namespace.

Example:

import { Component, tracked } from '@lifeart/gxt';
export class MyComponent extends Component {
  @tracked myProperty = 'initialValue';

  myMethod() {
    // method logic
  }

  <template>
    <!-- template content -->
  </template>
}

Component signature:

type ButtonSignature = {
  Args: { // list of arguments
    onClick?: () => void; 
  };
  Element: HTMLButtonElement; // type of node we use `...attributes` on
  Blocks: {
    default: []; // slot name, {{yield}} is equal to {{yield to="default"}}`, params if we have params in slot, we could expose it in type definition: `{{yield this.name this.age}}` as `Blocks: { default: [string, number] }`
  };
};


export class Button extends Component<ButtonSignature> {
    <template>
     ... here is template
    </template>
}

Reactivity

Reactivity driven by getter, setter access.

Primary way is to use @tracked JavaScript decorator inside classes. For example, if we have class, with property age and this property should be reactive, we need to mark it with decorator: class MyObject { @age = 12 }.

Note, if value has it's own fields, this field is not reactive by default. You need to create class to make it reactive.

  class Form {
     @tracked name = '';
     @tracked age = 0;
  }
  class MyObject {
     @tracked form = new Form();
  }

If we need to create alot of objects, it's simplier to use cellFor method to make existing property on object reactive. If we have POJO (plain javascript objects) and planning to mutate some of it's properties, we could use function, named cellFor (could be imported from '@lifeart/gxt') - it accept object as first argument and propertyName as second argument and patching descriptors for this object, making it reactive. Example:

const myObject = { name: 'Kate' };
// to make 'name' reactive, we do:
cellFor(myObject, 'name');
// that's all

Avoid components arguments destructuring, because it's breaking reactivity. Use getter if you need to fallback some argument or extend it. Also, do not do:

class MyComponent extends Component {
   component = this.args.component ?? 'div';
}

instead do:

class MyComponent extends Component {
   get component() { return this.args.component ?? 'div'; }
}

Example:

class MyButton extends Component<{
 Args: {
 type?: string;
}
}> {
   get type() {
      return this.args.type ?? 'button';
    }
}

Templates

Templates are defined within a <template> block inside the component class. Handlebars-like syntax is used for data binding and control flow. Templates has lisp-like fn combination syntax, where we have fn param1 param2 (fn2 param3 param4). Dynamic properties in template wrapped in {{ prop }} if used standalone. For example, non-block if looks like this: {{if isTrue ifTrue ifFalse}} @ symbol in template is alias for this.args and should be used if we use arguments passed to component inside template. For example: if we invoke MyComponent with @name param, like this: <MyComponent @name="Jake" /> and inside MyComponent we have template looking like <h1>Hello, {{@name}} </h1>. It renders: <h1> Hello, Jake</h1>.

Example:

<template>
  <div>{{this.myProperty}}</div>
  {{#if this.condition}}
    <span>Condition is true</span>
  {{/if}}
</template>

Element references.

If you need to get element access in logic, you need to create modifier - function, invoked before DOM node is appended, and bind node to class property. This function may return destructor. Here is example:

class MyComponent extends Component {
   @tracked
   nodeRef: null | HTMLDIVElement = null;
   onNodeCreated = (node: HTMLDIVElement) => {
       this.nodeRef = node;
       return () => {
          this.nodeRef = null;
     }
   }
}

If you need to use computed keys in templates, like {{this.foo[this.bar]}} you need to create getter instead: Example:

export class MyComponent extends Component {
   get dynamicPart() {
      return this.foo[this.bar];
   }
}

Slots (AKA Blocks)

Render slots (children). To render nested components or other elements inside component. We use {{yield}} symbol. It's equal to props.children in react or equal to default slot in web-components. For example:

class ComponentWithSlot extends Component<{
  Blocks: {
default: []
}
}> {
   <template><div>{{yield}}</div></template>
}

we could render as: <ComponentWithSlot> slot internals going here </ComponentWithSlot> and it will looks like: <div> slot internals going here </div>

Blocks support positional params:

For example:

class ComponentWithSlot extends Component<{
  Blocks: {
default: [string]
}
}> {
   <template><div>{{yield "Hi"}}</div></template>
}

we could render as: <ComponentWithSlot as |prefix|>{{prefix}} slot internals going here </ComponentWithSlot> and it will looks like: <div>Hi slot internals going here </div>

Also, it's possible to have multiple slots. You need to give them a name, for example:

<template>
{{yield to="header"}}
{{yield to="body"}}
{{yield to="footer"}}
</template>

Consumer of this component could render content like this:

<MyComponent>
<:header>content inside header slot</:header>
<:body>content inside body slot</:body>
<:footer>content inside footer slot</:footer>
</MyComponent>

In addition, in component with slot you may check if it's rendered with given slot or without, using has-block helper, like:

{{#if (has-block 'header')}}
{{yield to="header"}}
{{else}}
DEFAULT HEADER
{{/if}}

Also, has-block-params helper available, allowing to check if slot (block) invoked with params or not. Note, we could not count amount of params is invoked. We could just check if it's invoked with some params or not.

{{#if (has-block-params 'header')}}
 Block with params, invoked like `<:header as |param1 param2|> {{ param1 }} {{param2}} </:header>`
{{else}}
 Block without params, invoked like `<:header> some block content </:header>`
{{/if}}

Component invocation in template accept html attributes and html properties, it differs from arguments.

For example “checked” in html is property, and “data-test” is attribute.

Arguments

Arguments - is list of properties for data flow. Arguments starting from @, attributes and properties - not (just like generic html node attrs). Use the @ symbol to denote component arguments and attributes. Inside component template we put …attributes property on needed DOM node as anchor for forwarded DOM attributes and properties.

<MyButton type=“submit” />

And inside MyButton component:

class MyButton extends Component<{
 Element: HTMLButtonElement
}> {

<template>
<button …attributes >

</template>

}

Note, we could not access attributes or properties from javascript or spread it.

argument names (starting from @), attributes and properties could not have dynamic names, it’s always static.

class attribute if used on element with …attributes - merging values. Other external attributes override internal.

Helpers

Helper is a function, it's executed during initial rendering and re-rendering (if params changed). It's has positional and hash params. Special helpers, here is list of builtin helpers: and, or, not, array, hash, fn, eq. You don't need to import this helpers, because it's available in global scope (in execution environment). Note, hash helper use key=value pars and represent POJO Here is example:

{{hash key=value key1=value2}}

If we use helper for composition, we could to this:

{{array (hash key=value) (hash key=value2)}}

If you need to concat values, prefer creating getter property with logic like this:

{
  get classNames() {
     return [this.foo, this.bar].join(' ');
   }
}
<div class={{this.classNames}}></div>

If you see that path is used as html node name, prefer element helper. (like this.tagName or this.elementName or item.tag, etc); If it's just plain html tag name, do not wrap it in elment. Note, element helper creating new HTML node, it's not resolving any value or binding it.

Instead of writing: <this.tagName ...attributes> internal </this.tagName>, do:

{{#let (element this.tagName) as |node|}}
  <node ...attributes>internal</node>
{{/let}}

Note, do not use capitalize variables, representing html tag names. do node instead of Node (not uppercased). You may use let block to create closures inside template. It support multiple arguments.

Component Composition:

Components can be nested and composed together. Pass arguments and use blocks to yield content. Example:

<ParentComponent @arg={{this.value}}>
  <ChildComponent @anotherArg={{this.otherValue}} someAttribute="name" someProperty={{true}} />
</ParentComponent>

Iterative Rendering:

Use {{#each}} for rendering lists. Example:

<ul>
  {{#each this.items as |item|}}
    <li>{{item.name}}</li>
  {{/each}}
</ul>

Event Handling:

Event handlers are bound using the {{on "eventName" fnToExecute}} syntax. Note, do not use JavaScript syntax for function binding in template, use lisp-like, you may specify only path to function or combine params using fn function. Do not do this: {{on 'input' (fn (e) => { this.newTodoText = e.target.value })}} - this is invalid syntax. Example: <button {{on 'click' this.handleClick}}>Click Me</button> If we need to bind additional argument to function, we could use fn helper to do it: Example: <button {{on 'click' (fn this.handleClick "button")}}>Click Me</button> To not loose context of function inside class component, we need to define it as arrow function.

Note, if you use on modifier with function, firs argument of this function will be Event. If you bind additional arguments, using fn function. Event will be last argument. Do not use fn if you don't bind additional arguments to function.

Example:

export class MyComponent extends Component {
  handleClick = (e: Event) => {
    alert('Hello, looks like you clicked button!');
  }
 <template>
<button {{on 'click' this.handleClick}}>Click Me</button>
</template>
}

Style bindings:

You may use style property to bind style string. Also, you could manage atomic style fields using this notiation:

function px(value: number) {
   return `${value}px`;  
}
<template>
<div style.width="100px" style.max-height={{px this.maxHeight}} ></div>
</template>

Invalid syntax:

Do not use patterns like this: <div style={{ viewTransitionName: 'player' }} > - you cannot use object notation to specify style attribute value. Style attribute could accept only strings, you may consider using this approach: <div style.viewTransitionName="player" >. Notation like: {{hash top={{note.y}} left={{note.x}}}} - is incorrect and not allowed to nest mustache, correct way to use it - {{hash top=note.y left=note.x}}. {{#if starpodConfig.platforms?.spotify}} - do not use optional chaining operators in templates, all paths optional-chained by default. {{this.args.date.toISOString()}} - do not try to invoke paths as functions, create helper if needed and use it. <div style="transform: translate({{this.args.col * 100}}%, {{this.args.row * 100}}%)"></div> - do not use js expressions inside {{ scope, prefer to create getters: <div style="transform: translate({{this.colScale}}%, {{this.rowScale}}%)"></div> {{#if this.value !== 0}}{{this.value}}{{/if}} - is incorrect notation, replace it with {{#if this.value}}{{this.value}}{{/if}} <{{this.component}} ... - tag name could be dynamic only using element helper ref={{on 'create' this.onNodeCreated}} - it's not allowed to use modifiers as values for some properties. class={{this.c.base[this.style]}} - prefer to use getter to manage computed values in template. Components and html tags do not have ref property. If you need reference to node, use modifier. on modifier support only valid html events. Instead of having something like this:

<div {{on 'create' this.onInputCreated}} >...</div>

do this:

<div {{this.onInputCreated}} >...</div>

Paths is optional chained by default. No need to do this:

{{#if this.args.value}}
      {{this.args.value}}
    {{/if}}

Instead do this: {{@value}} If you need to call helper with arguments, do not use fn helper for arguments binding. Instead of {{(fn this.getTransform this.x @y)}} do: {{this.getTransform this.x @y}} If you need to get iteration index while using each, instead of this: {{#each row as |cell, colIndex|}} do this: {{#each row as |cell colIndex|}}. To check if array is empty inside {{#if block, instead of {{#if this.array}} do {{#if this.array.length}}

Testing:

We use qunit and qunit dom for unit and integration test. Simple test looks has gts file extension and looks like this:

 import { module, test } from 'qunit';
import { render, rerender, click } from '@lifeart/gxt/test-utils';
import { cell } from '@lifeart/gxt';

module('Integration | Internal | Cell', function () {
  test('could render cell as text primitive', async function (assert) {
    const value = cell('foo');
    await render(<template>{{value}}</template>);

    assert.dom().hasText('foo', 'cell value is rendered');
    value.update('bar');
    await rerender();
    assert.dom().hasText('bar', 'cell value is updated');
  });
});

in additional, test may contain component classes:

  test('works with dynamic binding in class', async function (assert) {
  class MyComponent extends Component {
    @tracked color = 'red';
    onClick = () => {
      this.color = 'blue';
    };
    <template>
      <div style.color={{this.color}}>123</div>
      <button type='button' {{on 'click' this.onClick}}>change color</button>
    </template>
  }
  await render(<template><MyComponent /></template>);
  assert.dom('div').hasStyle({
    color: 'rgb(255, 0, 0)',
  });
  await click('button');
  assert.dom('div').hasStyle({
    color: 'rgb(0, 0, 255)',
  });
});

Final notes: If you need to conditionally apply class, create getter for it. There is no math and equal symbols allowed inside {{. If you trying to access arguments in template, prefer @argName notation instead of this.args.argName. Ensure all args used in component class and in template has type signatures. Do not declare Blocks signature if no yields used in template. Do not use builtin helpers outside template scope. Do no declare Element signature if no ...attributes used in template. Do not use JS expressions or notation inside {{ }} blocks in scope. If you see parts if template enclosed in variables, extract them to independent components. Components don't have lifecycle methods, if you need to do some append/destroy logic, create modifier and attach it to stable html element in template. If you adding event listeners outside of template, do not forget do remove it. If additional styles needed, you may use tag <style> inside <template> to add it. You need to convert to .gxt this formats: .jsx, .tsx, .vue, .svelte, .astro, .html. Converted files should have namespace, starting with @, for example: @/components/ComponentName.gts. It's fine to have multiple component classes in one file. If you use each, and inside it more than one element, consider creating new component for internals.

Quest:

I need to to write / convert code following rules above. If you asked to create new code, use tailwind css for styling. If you not clear on how to write something in correct way - ask me. Do not try to breakdown details. Just print output code as result of writing / converting.

import { Component, tracked, cellFor } from '@lifeart/gxt';
const products = [
{
id: 1,
name: 'Stylish Sneakers',
price: 79.99,
imageUrl: 'https://via.placeholder.com/200x150', // Replace with actual image URLs
},
{
id: 2,
name: 'Trendy T-shirt',
price: 24.99,
imageUrl: 'https://via.placeholder.com/200x150',
},
{
id: 3,
name: 'Elegant Dress',
price: 129.99,
imageUrl: 'https://via.placeholder.com/200x150',
},
{
id: 4,
name: 'Classic Jeans',
price: 59.99,
imageUrl: 'https://via.placeholder.com/200x150',
},
];
export class Panel extends Component {
@tracked products = products.map((product) => {
cellFor(product, 'quantity');
product.quantity = 0;
return product;
});
incrementQuantity = (productId: number) => {
const product = this.products.find((p) => p.id === productId);
if (product) {
product.quantity++;
}
};
decrementQuantity = (productId: number) => {
const product = this.products.find((p) => p.id === productId);
if (product && product.quantity > 0) {
product.quantity--;
}
};
<template>
<div class='bg-gray-100 p-4'>
<h1 class='text-2xl font-bold mb-4'>Shop Our Latest</h1>
<div class='grid grid-cols-1 md:grid-cols-2 gap-4'>
{{#each this.products as |product|}}
<div class='bg-white rounded-lg shadow p-4'>
<img
src={{product.imageUrl}}
alt={{product.name}}
class='w-full h-48 object-cover mb-2'
/>
<h2 class='text-lg font-medium'>{{product.name}}</h2>
<p class='text-gray-600'>${{product.price}}</p>
<div class='flex items-center mt-2'>
<button
{{on 'click' (fn this.decrementQuantity product.id)}}
class='bg-gray-200 px-2 py-1 rounded'
>-</button>
<span class='mx-2'>{{product.quantity}}</span>
<button
{{on 'click' (fn this.incrementQuantity product.id)}}
class='bg-blue-500 text-white px-2 py-1 rounded'
>+</button>
</div>
</div>
{{/each}}
</div>
</div>
</template>
}
import { Component, tracked } from '@lifeart/gxt';
type Todo = {
id: number;
text: string;
completed: boolean;
};
export class TodoApp extends Component {
@tracked todos: Todo[] = [
{ id: 1, text: 'Learn Glimmer', completed: false },
{ id: 2, text: 'Build something awesome', completed: false },
];
@tracked newTodoText = '';
get pendingTodosCount() {
return this.todos.filter((todo) => !todo.completed).length;
}
addTodo = () => {
if (this.newTodoText.trim() !== '') {
const newTodo: Todo = {
id: Date.now(),
text: this.newTodoText,
completed: false,
};
this.todos = [...this.todos, newTodo];
this.newTodoText = '';
}
};
toggleTodo = (todo: Todo) => {
const updatedTodos = this.todos.map((t) =>
t.id === todo.id ? { ...t, completed: !t.completed } : t,
);
this.todos = updatedTodos;
};
removeTodo = (todo: Todo) => {
this.todos = this.todos.filter((t) => t.id !== todo.id);
};
onInput = (e: Event) => {
this.newTodoText = (e.target as HTMLInputElement).value;
};
<template>
<div class='container mx-auto p-4 text-white'>
<h1 class='text-3xl font-bold mb-4'>Todo List</h1>
<div class='flex mb-4'>
<input
type='text'
class='border rounded px-3 py-2 mr-2 flex-grow'
placeholder='Add a new todo...'
value={{this.newTodoText}}
{{on 'input' this.onInput}}
/>
<button
class='bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded'
{{on 'click' this.addTodo}}
>
Add Todo
</button>
</div>
<ul>
{{#each this.todos as |todo|}}
<li class='flex items-center py-2'>
<input
type='checkbox'
checked={{todo.completed}}
{{on 'change' (fn this.toggleTodo todo)}}
/>
<span class='ml-2 {{if todo.completed "line-through"}}'>
{{todo.text}}
</span>
<button
class='ml-auto text-red-500 hover:text-red-700'
{{on 'click' (fn this.removeTodo todo)}}
>
Remove
</button>
</li>
{{/each}}
</ul>
{{#if this.pendingTodosCount}}
<p class='mt-4'>
You have
{{this.pendingTodosCount}}
pending todos.
</p>
{{/if}}
</div>
</template>
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment