Skip to content

Commit ae5649e

Browse files
committed
feat: add ll_replicate_default_ctx as a flag
1 parent 7f189a2 commit ae5649e

File tree

6 files changed

+191
-36
lines changed

6 files changed

+191
-36
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Add through [HACS](https://github.com/custom-components/hacs)
4343
| ll_context | object | **Optional** | An object that can be accessed inside of EtaJS as `context`| `` |
4444
| ll_template_engine| string | **Optional** | Template processor ('etajs', 'jinja2') | 'etajs' |
4545
| ll_card_config | string | **Optional** | Template returning a JSON object, merged to linked card | `` |
46+
| ll_replicate_ctx | boolean | **Optional** | Replicate context values to linked card on first render | `true` |
4647

4748
| Name | Type | Requirement | Description | Default |
4849
| ----------------- | ------- | ------------ | --------------------------------------------------------------------------------------------------------- | ------------------- |

docs_site/create-your-first-template.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ By default, Linked Lovelace uses the EtaJS template engine for all template rend
136136

137137
```yaml
138138
ll_key: jinja2-example
139+
ll_template_engine: jinja2
139140
show_name: true
140141
type: button
141142
tap_action:

rollup.config.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import nodePolyfills from 'rollup-plugin-node-polyfills'
1111

1212

1313

14-
const dev = process.env.ROLLUP_WATCH;
14+
const watch = process.env.ROLLUP_WATCH;
15+
const dev = process.env.NODE_ENV === 'development';
1516

1617
const serveopts = {
1718
contentBase: ['./dist'],
@@ -36,7 +37,7 @@ const plugins = [
3637
// left-hand side can be an absolute path, a path
3738
// relative to the current directory, or the name
3839
// of a module in node_modules
39-
'yaml': [ 'parse' ],
40+
'yaml': ['parse'],
4041
}
4142
}),
4243
typescript({
@@ -48,12 +49,12 @@ const plugins = [
4849
babel({
4950
exclude: ['node_modules/**', 'src/**/*.test.ts'],
5051
}),
51-
dev && serve(serveopts),
52-
!dev && terser(),
52+
watch && serve(serveopts),
53+
!watch && !dev && terser(),
5354
eta({
5455
include: ['**/*.eta', '**/*.html'], // optional, '**/*.eta' by default
5556
exclude: ['**/index.html'], // optional, undefined by default
56-
}),
57+
}),
5758
];
5859

5960
export default [

src/helpers/templates.test.ts

Lines changed: 149 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -770,7 +770,6 @@ describe('[function] updateCardTemplate', () => {
770770
expect(await updateCardTemplate(card, { template })).toStrictEqual({
771771
type: 'template',
772772
ll_template: 'template',
773-
ll_context: {},
774773
number: 3,
775774
ll_keys: { 'number': 'number' },
776775
});
@@ -1382,7 +1381,6 @@ describe('[function] updateCardTemplate v2', () => {
13821381
expect(await updateCardTemplate(card, { template })).toStrictEqual({
13831382
type: 'template',
13841383
ll_template: 'template',
1385-
ll_context: {},
13861384
number: 3,
13871385
ll_keys: { 'number': 'number' },
13881386
});
@@ -1445,6 +1443,115 @@ describe('[function] updateCardTemplate v2', () => {
14451443
expect(oldTemplate).toStrictEqual(template)
14461444
});
14471445

1446+
test('Not Overriding linked LL_Context when ll_replicate_ctx is true', async () => {
1447+
const template: DashboardCard = {
1448+
type: "custom_collapsable-cards",
1449+
ll_key: "test_card",
1450+
ll_context: {
1451+
group: "sensor.tempratures_koelkasten",
1452+
name: "Temperatuur"
1453+
},
1454+
title_card: {
1455+
type: "tile",
1456+
name: "<%= context.name %>",
1457+
entity: "<%= context.group %>"
1458+
},
1459+
};
1460+
const card: DashboardCard = {
1461+
type: "text",
1462+
ll_template: "test_card",
1463+
ll_context: {
1464+
group: "sensor.tempratures_another",
1465+
}
1466+
};
1467+
const oldTemplate = JSON.parse(JSON.stringify(template))
1468+
expect(await updateCardTemplate(card, { [template.ll_key!]: template })).toStrictEqual({
1469+
type: "custom_collapsable-cards",
1470+
ll_template: "test_card",
1471+
ll_context: {
1472+
group: "sensor.tempratures_another",
1473+
name: "Temperatuur"
1474+
},
1475+
title_card: {
1476+
type: "tile",
1477+
name: "Temperatuur",
1478+
entity: "sensor.tempratures_another"
1479+
},
1480+
});
1481+
expect(oldTemplate).toStrictEqual(template)
1482+
});
1483+
1484+
1485+
test('Not Replicating LL_Context when ll_replicate_ctx is false', async () => {
1486+
const template: DashboardCard = {
1487+
type: "custom_collapsable-cards",
1488+
ll_replicate_ctx: false,
1489+
ll_key: "test_card",
1490+
ll_context: {
1491+
group: "sensor.tempratures_koelkasten",
1492+
name: "Temperatuur"
1493+
},
1494+
title_card: {
1495+
type: "tile",
1496+
name: "<%= context.name %>",
1497+
entity: "<%= context.group %>"
1498+
},
1499+
};
1500+
const card: DashboardCard = {
1501+
type: "text",
1502+
ll_template: "test_card"
1503+
};
1504+
const oldTemplate = JSON.parse(JSON.stringify(template))
1505+
expect(await updateCardTemplate(card, { [template.ll_key!]: template })).toStrictEqual({
1506+
type: "custom_collapsable-cards",
1507+
ll_template: "test_card",
1508+
title_card: {
1509+
type: "tile",
1510+
name: "Temperatuur",
1511+
entity: "sensor.tempratures_koelkasten"
1512+
},
1513+
});
1514+
expect(oldTemplate).toStrictEqual(template)
1515+
});
1516+
1517+
test('Not Overriding linked LL_Context when ll_replicate_ctx is false', async () => {
1518+
const template: DashboardCard = {
1519+
type: "custom_collapsable-cards",
1520+
ll_replicate_ctx: false,
1521+
ll_key: "test_card",
1522+
ll_context: {
1523+
group: "sensor.tempratures_koelkasten",
1524+
name: "Temperatuur"
1525+
},
1526+
title_card: {
1527+
type: "tile",
1528+
name: "<%= context.name %>",
1529+
entity: "<%= context.group %>"
1530+
},
1531+
};
1532+
const card: DashboardCard = {
1533+
type: "text",
1534+
ll_template: "test_card",
1535+
ll_context: {
1536+
group: "sensor.tempratures_another",
1537+
},
1538+
};
1539+
const oldTemplate = JSON.parse(JSON.stringify(template))
1540+
expect(await updateCardTemplate(card, { [template.ll_key!]: template })).toStrictEqual({
1541+
type: "custom_collapsable-cards",
1542+
ll_template: "test_card",
1543+
ll_context: {
1544+
group: "sensor.tempratures_another",
1545+
},
1546+
title_card: {
1547+
type: "tile",
1548+
name: "Temperatuur",
1549+
entity: "sensor.tempratures_another"
1550+
},
1551+
});
1552+
expect(oldTemplate).toStrictEqual(template)
1553+
});
1554+
14481555
test('merges ll_card_config complex object into main card config', async () => {
14491556
const template: DashboardCard = {
14501557
type: 'custom:complex-card',
@@ -1508,15 +1615,52 @@ describe('[function] updateCardTemplate v2', () => {
15081615
};
15091616
const spy = jest.spyOn(console, 'error').mockImplementation(() => { });
15101617
const result = await updateCardTemplate(card, { complex_card: template });
1511-
expect(result).toMatchObject({
1618+
expect(result).toStrictEqual({
15121619
type: 'custom:complex-card',
15131620
ll_template: 'complex_card',
1514-
foo: 'bar'
1621+
foo: 'bar',
1622+
ll_error: "Error rendering template 'complex_card': Error: Failed to parse ll_card_config for template 'complex_card': SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)",
1623+
ll_template_card: {
1624+
foo: "bar",
1625+
ll_card_config: "{invalid json}",
1626+
ll_key: "complex_card",
1627+
ll_replicate_ctx: true,
1628+
type: "custom:complex-card",
1629+
}
15151630
});
1516-
expect(result.ll_card_config).toBe('{invalid json}');
1631+
expect(result.ll_template_card.ll_card_config).toBe('{invalid json}');
15171632
expect(spy).toHaveBeenCalledWith(
15181633
new Error("Failed to parse ll_card_config for template 'complex_card': SyntaxError: Expected property name or '}' in JSON at position 1 (line 1 column 2)"),
15191634
);
15201635
spy.mockRestore();
15211636
});
1637+
1638+
1639+
test('linked card error information is removed when error is resolved', async () => {
1640+
const template: DashboardCard = {
1641+
type: 'custom:complex-card',
1642+
ll_key: 'complex_card',
1643+
ll_card_config: '{"foo": "barbaz"}',
1644+
foo: 'bar'
1645+
};
1646+
const card: DashboardCard = {
1647+
type: 'custom:complex-card',
1648+
ll_template: 'complex_card',
1649+
foo: 'bar',
1650+
ll_error: "some error from previous state",
1651+
ll_template_card: {
1652+
foo: "bar",
1653+
ll_card_config: "{invalid json}",
1654+
ll_key: "complex_card",
1655+
ll_replicate_ctx: true,
1656+
type: "custom:complex-card",
1657+
}
1658+
};
1659+
const result = await updateCardTemplate(card, { complex_card: template });
1660+
expect(result).toStrictEqual({
1661+
type: 'custom:complex-card',
1662+
ll_template: 'complex_card',
1663+
foo: 'barbaz',
1664+
});
1665+
});
15221666
});

src/helpers/templates.ts

Lines changed: 32 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -38,15 +38,23 @@ export const updateCardTemplate = async (
3838
}
3939

4040
const llKeys = targetCard.ll_keys;
41-
const llContext = targetCard.ll_context;
41+
const llContext = { ...templateContext };
4242
const llTemplate = targetCard.ll_template;
43-
const templateCard = templateCards[llTemplate];
43+
const templateCard = { ...templateCards[llTemplate] };
44+
45+
if (templateCard.ll_replicate_ctx === undefined) {
46+
templateCard.ll_replicate_ctx = true;
47+
}
4448

4549
let engineType: TemplateEngineType = 'eta';
4650
if (templateCard.ll_template_engine === 'jinja2') engineType = 'jinja2';
4751

4852
// destination card context should override template context
49-
templateContext = { ...(templateCard?.ll_context || {}), ...templateContext };
53+
templateContext = { ...(templateCard.ll_context || {}), ...templateContext };
54+
55+
// Delete last error if it exists
56+
delete targetCard.ll_error;
57+
delete targetCard.ll_template_card;
5058

5159
try {
5260
// Render the template with the context
@@ -68,36 +76,34 @@ export const updateCardTemplate = async (
6876
} catch (e) {
6977
console.error(e);
7078
targetCard.ll_error = `Error rendering template '${llTemplate}': ${e}`;
79+
targetCard.ll_template_card = templateCard;
7180
}
7281

73-
// Set a deterministic key order to avoid noisy diffs
74-
targetCard = {
75-
ll_error: targetCard.ll_error,
76-
ll_template: llTemplate,
77-
ll_context: llContext,
78-
ll_keys: llKeys,
79-
...targetCard
80-
};
82+
// Set special keys
83+
targetCard.ll_template = llTemplate;
84+
targetCard.ll_context = templateCard.ll_replicate_ctx ? templateContext : llContext;
85+
targetCard.ll_keys = llKeys;
8186

8287
// Clean up empty, unused or undefined properties
83-
delete targetCard.ll_key;
84-
85-
if (!targetCard.ll_keys) {
86-
delete targetCard.ll_keys;
87-
}
88+
if (!targetCard.ll_keys || Object.keys(targetCard.ll_keys).length === 0) delete targetCard.ll_keys;
89+
if (!targetCard.ll_context || Object.keys(targetCard.ll_context).length === 0) delete targetCard.ll_context;
8890

89-
if (!targetCard.ll_context) {
90-
delete targetCard.ll_context;
91-
}
92-
93-
if (!targetCard.ll_error) {
94-
delete targetCard.ll_error;
91+
// Clean up other properties that should not be in the final card
92+
delete targetCard.ll_key;
93+
delete targetCard.ll_card_config;
94+
delete targetCard.ll_template_engine;
95+
delete targetCard.ll_replicate_ctx;
96+
97+
// Ensure keys are sorted in a deterministic order to avoid noisy diffs
98+
const orderedKeys = ['ll_template', 'll_context', 'll_keys', 'll_error'].filter(k => k in targetCard);
99+
const otherKeys = Object.keys(targetCard).filter(k => !orderedKeys.includes(k));
100+
const sortedTargetCard: any = {};
101+
for (const key of [...orderedKeys, ...otherKeys]) {
102+
sortedTargetCard[key] = targetCard[key];
95103
}
104+
targetCard = sortedTargetCard as DashboardCard;
96105

97-
98-
targetCard = await handleLLKeys(targetCard, templateCards, templateContext);
99-
100-
return targetCard
106+
return await handleLLKeys(targetCard, templateCards, templateContext);
101107
};
102108

103109
const handleLLKeys = async (targetCard: DashboardCard,

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export interface LLCard extends LovelaceCardConfig {
4141
ll_template?: string
4242
sections?: LovelaceCardConfig[]
4343
ll_context?: Record<string, any>
44+
// A boolean to indicate if the default context values should be replicated in the ll_context of dashboard cards (defaults to true)
45+
ll_replicate_ctx?: boolean
4446
ll_template_engine?: 'eta' | 'jinja2'
4547
// A template string for the card configuration that may return a complex object in JSON, it will
4648
// be merged with the main card config

0 commit comments

Comments
 (0)