Skip to content

Commit

Permalink
[feature] style binding (#120)
Browse files Browse the repository at this point in the history
* style binding

* +
  • Loading branch information
lifeart authored May 4, 2024
1 parent b3b70cb commit 77a8644
Show file tree
Hide file tree
Showing 12 changed files with 228 additions and 30 deletions.
4 changes: 4 additions & 0 deletions plugins/converter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,10 @@ describe.each([
expect($t<ASTv1.MustacheStatement>(`{{true}}`)).toEqual(true);
expect($t<ASTv1.MustacheStatement>(`{{false}}`)).toEqual(false);
});
test('support string literals', () => {
expect($t<ASTv1.MustacheStatement>(`{{'true'}}`)).toEqual('"true"');
expect($t<ASTv1.MustacheStatement>(`{{'false'}}`)).toEqual('"false"');
});
test('support null literals', () => {
expect($t<ASTv1.MustacheStatement>(`{{null}}`)).toEqual(null);
});
Expand Down
22 changes: 20 additions & 2 deletions plugins/converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,8 @@ export function convert(
return node.path.value;
} else if (node.path.type === 'SubExpression') {
return `${wrap ? `$:() => ` : ''}${ToJSType(node.path)}`;
} else if (node.path.type === 'StringLiteral') {
return escapeString(node.path.value);
}
return null;
}
Expand Down Expand Up @@ -475,6 +477,22 @@ export function convert(
const children = resolvedChildren(element.children)
.map((el) => ToJSType(el))
.filter((el) => el !== null);

const rawStyleEvents = element.attributes.filter((attr) => {
return attr.name.startsWith('style.');
});
element.attributes = element.attributes.filter((attr) => {
return !rawStyleEvents.includes(attr);
});
const styleEvents = rawStyleEvents.map((attr) => {
const propertyName = attr.name.split('.').pop();
const value = attr.value.type === 'TextNode' ? escapeString(attr.value.chars) : ToJSType(attr.value);
const isPath = typeof value === 'string' ? value.includes('.') : false;
return [
EVENT_TYPE.ON_CREATED,
`$:function($v,$n){$n.style.setProperty('${propertyName}',$v);}.bind(null,${SYMBOLS.$_TO_VALUE}(${isPath?`$:()=>${value}`: value}))`,
];
});
const node = {
tag: element.tag,
selfClosing: element.selfClosing,
Expand All @@ -498,7 +516,7 @@ export function convert(
rawValue,
];
}),
events: element.modifiers
events: [...styleEvents,...element.modifiers
.map((mod) => {
if (mod.path.type !== 'PathExpression') {
return null;
Expand Down Expand Up @@ -540,7 +558,7 @@ export function convert(
];
}
})
.filter((el) => el !== null),
.filter((el) => el !== null)],
children: children,
};
if (children.length === 1 && typeof children[0] === 'string') {
Expand Down
1 change: 1 addition & 0 deletions plugins/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const SYMBOLS = {
DYNAMIC_COMPONENT: '$_dc',
$SLOTS_SYMBOL: '$SLOTS_SYMBOL',
$PROPS_SYMBOL: '$PROPS_SYMBOL',
$_TO_VALUE: '$_TO_VALUE',
$_GET_SLOTS: '$_GET_SLOTS',
$_GET_ARGS: '$_GET_ARGS',
$_GET_FW: '$_GET_FW',
Expand Down
28 changes: 16 additions & 12 deletions plugins/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,28 +158,32 @@ export function transform(
const hasFw = results.some((el) => el.includes('$fw'));
const hasSlots = results.some((el) => el.includes('$slots'));
const slotsResolution = `const $slots = ${SYMBOLS.$_GET_SLOTS}(this, arguments);`;
const maybeFw = `${hasFw ? `const $fw = ${SYMBOLS.$_GET_FW}(this, arguments);` : ''}`;
const maybeSlots = `${hasSlots ? slotsResolution : ''}`;
const declareRoots = `const roots = [${results.join(', ')}];`;
const declareReturn = `return ${SYMBOLS.FINALIZE_COMPONENT}(roots, ${finContext});`;

if (isTemplateTag) {
result = `function () {
${hasFw ? `const $fw = ${SYMBOLS.$_GET_FW}(this, arguments);` : ''}
${maybeFw}
${SYMBOLS.$_GET_ARGS}(this, arguments);
${hasSlots ? slotsResolution : ''}
const roots = [${results.join(', ')}];
return ${SYMBOLS.FINALIZE_COMPONENT}(roots, ${finContext});
${maybeSlots}
${declareRoots}
${declareReturn}
}`;
} else {
result = isClass
? `() => {
${hasSlots ? slotsResolution : ''}
${hasFw ? `const $fw = ${SYMBOLS.$_GET_FW}(this, arguments);` : ''}
const roots = [${results.join(', ')}];
return ${SYMBOLS.FINALIZE_COMPONENT}(roots, ${finContext});
${maybeSlots}
${maybeFw}
${declareRoots}
${declareReturn}
}`
: `(() => {
${hasSlots ? slotsResolution : ''}
${hasFw ? `const $fw = ${SYMBOLS.$_GET_FW}(this, arguments);` : ''}
const roots = [${results.join(', ')}];
return ${SYMBOLS.FINALIZE_COMPONENT}(roots, ${finContext});
${maybeSlots}
${maybeFw}
${declareRoots}
${declareReturn}
})()`;
}

Expand Down
31 changes: 26 additions & 5 deletions src/components/pages/PageOne.gts
Original file line number Diff line number Diff line change
@@ -1,11 +1,32 @@
import { Component } from '@lifeart/gxt';
import { Component, cell } from '@lifeart/gxt';
import { Smile } from './page-one/Smile';
import { Table } from './page-one/Table.gts';

export class PageOne extends Component {
<template>
function Controls() {
const color = cell('red');

const intervalId = setInterval(() => {
color.update(Math.random() > 0.5 ? 'red' : 'blue');
}, 1000);

function onDestroy() {
return () => {
console.log('destroying interval');
clearInterval(intervalId);
};
}

return <template>
<h1><q {{onDestroy}} style.background-color={{color}}>Compilers are the New
Frameworks</q>
- Tom Dale &copy;</h1>
</template>;
}

export function PageOne() {
return <template>
<div class='text-white p-3'>
<h1><q>Compilers are the New Frameworks</q> - Tom Dale &copy;</h1>
<Controls />
<br />

<div>Imagine a world where the robust, mature ecosystems of development
Expand Down Expand Up @@ -38,5 +59,5 @@ export class PageOne extends Component {
efficiency. Get ready to elevate your coding experience!</i>
<br /><br />
<a href='/pageTwo'>Go to page two <Smile /></a></div>
</template>
</template>;
}
2 changes: 1 addition & 1 deletion src/tests/integration/Button-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ module('Integration | Component | Button', function () {
<Button @onClick={{onClick}} data-test-button>DEMO</Button>
</template>,
);
click('[data-test-button]');
await click('[data-test-button]');
});

test('has default type', async function (assert) {
Expand Down
2 changes: 1 addition & 1 deletion src/tests/integration/fn-test.gts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ module('Integration | InternalHelper | fn', function () {
</template>,
);

click('[data-test-button]');
await click('[data-test-button]');
});
});
66 changes: 66 additions & 0 deletions src/tests/integration/functional-component-test.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { module, test } from 'qunit';
import { render, rerender } from '@lifeart/gxt/test-utils';
import { cell } from '@lifeart/gxt';

module('Integration | component | functional', function () {
test('should render text', async function (assert) {
function HelloWolrd() {
return <template>123</template>;
}
await render(<template><HelloWolrd /></template>);
assert.dom().hasText('123');
});
test('should render node', async function (assert) {
function HelloWolrd() {
return <template>
<div>123</div>
</template>;
}
await render(<template><HelloWolrd /></template>);
assert.dom('div').hasText('123');
});
test('support static args', async function (assert) {
function HelloWolrd() {
return <template>
<div>{{@name}}</div>
</template>;
}
await render(<template><HelloWolrd @name={{'123'}} /></template>);
assert.dom('div').hasText('123');
});
test('support static args from functional params', async function (assert) {
const HelloWolrd = ({ name }) => {
return <template>
<div>{{name}}</div>
</template>;
};
await render(<template><HelloWolrd @name={{'123'}} /></template>);
assert.dom('div').hasText('123');
});
test('support dynamic args from functional params', async function (assert) {
const value = cell('123');
const HelloWolrd = ({ name }) => {
return <template>
<div>{{name}}</div>
</template>;
};
await render(<template><HelloWolrd @name={{value}} /></template>);
assert.dom('div').hasText('123');
value.update('321');
await rerender();
assert.dom('div').hasText('321');
});
test('support dynamic args from functional params reference', async function (assert) {
const value = cell('123');
const HelloWolrd = (args) => {
return <template>
<div>{{args.name}}</div>
</template>;
};
await render(<template><HelloWolrd @name={{value.value}} /></template>);
assert.dom('div').hasText('123');
value.update('321');
await rerender();
assert.dom('div').hasText('321');
});
});
69 changes: 69 additions & 0 deletions src/tests/integration/style-test.gts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { module, test } from 'qunit';
import { render, rerender, click } from '@lifeart/gxt/test-utils';
import { cell, Component, tracked } from '@lifeart/gxt';

module('Integration | Interal | style', function () {
test('works with static style binding', async function (assert) {
await render(
<template>
<div style.color={{'green'}}>123</div>
</template>,
);
assert.dom('div').hasStyle({
color: 'rgb(0, 128, 0)',
});
});
test('works with dynamic binding', async function (assert) {
const color = cell('red');
await render(
<template>
<div style.color={{color}}>123</div>
</template>,
);
assert.dom('div').hasStyle({
color: 'rgb(255, 0, 0)',
});
color.update('blue');
await rerender();
assert.dom('div').hasStyle({
color: 'rgb(0, 0, 255)',
});
});
test('works with dynamic binding in class', async function (assert) {
class MyComponent {
@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)',
});
});
test('works with functions', async function (assert) {
const color = cell('red');
const getColor = () => color.value;
await render(
<template>
<div style.color={{getColor}}>123</div>
</template>,
);
assert.dom('div').hasStyle({
color: 'rgb(255, 0, 0)',
});
color.update('blue');
await rerender();
assert.dom('div').hasStyle({
color: 'rgb(0, 0, 255)',
});
});
});
3 changes: 2 additions & 1 deletion src/tests/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ export async function rerender(timeout = 0) {
});
}

export function click(selector: string) {
export async function click(selector: string) {
const element = getDocument()
.getElementById('ember-testing')!
.querySelector(selector);
Expand All @@ -93,6 +93,7 @@ export function click(selector: string) {
cancelable: true,
});
element!.dispatchEvent(event);
await rerender();
}

export function step(message: string) {
Expand Down
15 changes: 14 additions & 1 deletion src/utils/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,16 @@ const $_className = 'className';
let unstableWrapperId: number = 0;
let ROOT: Component<any> | null = null;

export function $_TO_VALUE(reference: unknown) {
if (isPrimitive(reference)) {
return reference;
} else if (isTagLike(reference)) {
return reference;
} else {
return resolveRenderable(reference as Function);
}
}

export function $_componentHelper(params: any, hash: any) {
const componentFn = params.shift();

Expand Down Expand Up @@ -712,7 +722,10 @@ function _component(
}
}
// @ts-expect-error construct signature
const instance = new (comp as unknown as Component<any>)(args, fw);
let instance = comp.prototype === undefined ? comp(args, fw) : new (comp as unknown as Component<any>)(args, fw);
if (typeof instance === 'function') {
instance = new instance(args, fw);
}
// todo - fix typings here
if ($template in instance) {
const result = (
Expand Down
15 changes: 8 additions & 7 deletions src/utils/reactive.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,15 @@ export class MergedCell {
return this.fn();
}

let $tracker = tracker();
try {
currentTracker = tracker();
setTracker($tracker)
return this.fn();
} finally {
bindAllCellsToTag(currentTracker!, this);
this.isConst = currentTracker!.size === 0;
this.relatedCells = currentTracker;
currentTracker = null;
bindAllCellsToTag($tracker, this);
this.isConst = $tracker.size === 0;
this.relatedCells = $tracker;
setTracker(null)
}
}
}
Expand Down Expand Up @@ -328,9 +329,9 @@ export function cell<T>(value: T, debugName?: string) {

export function inNewTrackingFrame(callback: () => void) {
const existingTracker = currentTracker;
currentTracker = null;
setTracker(null);
callback();
currentTracker = existingTracker;
setTracker(existingTracker);
}

export function getTracker() {
Expand Down

0 comments on commit 77a8644

Please sign in to comment.