Skip to content

Commit 77cd24b

Browse files
authored
feat: change default attribute-name-strategy from none to camelCase (#7479)
# Pull Request ## 📖 Description Changes the default `attribute-name-strategy` from `"none"` to `"camelCase"` across the FAST monorepo. This applies to: - **`microsoft-fast-build` Rust crate**: The `AttributeNameStrategy` enum's `#[default]` is moved from `None` to `CamelCase` - **`@microsoft/fast-build` CLI**: Default strategy passed to the WASM renderer and CLI help text updated - **`@microsoft/fast-html` AttributeMap**: Default fallback changed from `"none"` to `"camelCase"` With this change, dashed HTML attribute names on custom elements (e.g. `foo-bar`) are now converted to camelCase state keys (e.g. `fooBar`) by default. The `"none"` strategy remains available as an explicit opt-in. ## 📑 Test Plan - All existing Rust tests pass (`cargo test`) - All Playwright tests pass (`npm run test`) - All fixtures rebuilt successfully (`npm run build:fixtures`) - Biome check passes (`npm run biome:check`) - Change files validated (`npm run checkchange`) ## ✅ Checklist ### General - [x] I have included a change request file using `$ npm run change` - [ ] I have added tests for my changes. - [x] I have tested my changes. - [x] I have updated the project documentation to reflect my changes. - [x] I have read the [CONTRIBUTING](https://github.com/microsoft/fast/blob/main/CONTRIBUTING.md) documentation and followed the [standards](https://github.com/microsoft/fast/blob/main/CODE_OF_CONDUCT.md#our-standards) for this project.
1 parent 97fa037 commit 77cd24b

20 files changed

Lines changed: 94 additions & 77 deletions
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "feat: change default attribute-name-strategy from none to camelCase",
4+
"packageName": "@microsoft/fast-build",
5+
"email": "7559015+janechu@users.noreply.github.com",
6+
"dependentChangeType": "none"
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "prerelease",
3+
"comment": "feat: change default attribute-name-strategy from none to camelCase",
4+
"packageName": "@microsoft/fast-html",
5+
"email": "7559015+janechu@users.noreply.github.com",
6+
"dependentChangeType": "none"
7+
}

crates/microsoft-fast-build/DESIGN.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,8 @@ Controls how HTML attribute names are mapped to state property names when buildi
267267

268268
| Strategy | Behaviour | Example |
269269
|----------|-----------|---------|
270-
| `"none"` (default) | Attribute names are lowercased as-is. Dashes are preserved. | `foo-bar``foo-bar``{{foo-bar}}` |
271-
| `"camelCase"` | Dashed attribute names are converted to camelCase. | `foo-bar``fooBar``{{fooBar}}` |
270+
| `"camelCase"` (default) | Dashed attribute names are converted to camelCase. | `foo-bar``fooBar``{{fooBar}}` |
271+
| `"none"` | Attribute names are lowercased as-is. Dashes are preserved. | `foo-bar``foo-bar``{{foo-bar}}` |
272272

273273
The `camelCase` strategy only applies to attributes that are **not** already handled by a specialized conversion:
274274

crates/microsoft-fast-build/src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@
88
pub enum AttributeNameStrategy {
99
/// No conversion — attribute names are lowercased as-is.
1010
/// `foo-bar` stays `foo-bar`, matching `{{foo-bar}}` in the template.
11-
#[default]
1211
None,
1312
/// Convert dashed attribute names to camelCase.
1413
/// `foo-bar` becomes `fooBar`, matching `{{fooBar}}` in the template.
1514
/// Attributes that are already handled by specialized lookup tables
1615
/// (`data-*`, `aria-*`, and HTML global attributes like `tabindex`)
1716
/// are unaffected — those always use their standard property names.
17+
#[default]
1818
CamelCase,
1919
}
2020

crates/microsoft-fast-build/src/wasm.rs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ pub fn render(entry: &str, state: &str) -> Result<String, JsValue> {
1313
/// Render a FAST HTML template with custom element templates and a JSON state string.
1414
/// `templates_json` is a JSON object mapping element names to their HTML template strings,
1515
/// e.g. `{"my-button": "<template>...</template>"}`.
16-
/// `attribute_name_strategy` controls attribute-to-property mapping: `"none"` (default)
17-
/// or `"camelCase"`. Pass an empty string for the default.
16+
/// `attribute_name_strategy` controls attribute-to-property mapping: `"camelCase"` (default)
17+
/// or `"none"`. Pass an empty string for the default.
1818
/// Returns the rendered HTML or throws a JavaScript error.
1919
#[wasm_bindgen]
2020
pub fn render_with_templates(entry: &str, templates_json: &str, state: &str, attribute_name_strategy: &str) -> Result<String, JsValue> {
@@ -32,8 +32,8 @@ pub fn render_with_templates(entry: &str, templates_json: &str, state: &str, att
3232
/// output, while non-primitive values (`array`, `object`, `null`) are stripped.
3333
///
3434
/// `templates_json` is a JSON object mapping element names to their HTML template strings.
35-
/// `attribute_name_strategy` controls attribute-to-property mapping: `"none"` (default)
36-
/// or `"camelCase"`. Pass an empty string for the default.
35+
/// `attribute_name_strategy` controls attribute-to-property mapping: `"camelCase"` (default)
36+
/// or `"none"`. Pass an empty string for the default.
3737
/// Returns the rendered HTML or throws a JavaScript error.
3838
#[wasm_bindgen]
3939
pub fn render_entry_with_templates(entry: &str, templates_json: &str, state: &str, attribute_name_strategy: &str) -> Result<String, JsValue> {
@@ -102,12 +102,12 @@ fn parse_templates_map(templates_json: &str) -> Result<HashMap<String, String>,
102102
}
103103

104104
/// Build an `Option<RenderConfig>` from the strategy string.
105-
/// Returns `None` for `""` or `"none"` (use defaults), `Some(config)` otherwise.
105+
/// Returns `None` for `""` or `"camelCase"` (use defaults), `Some(config)` for `"none"`.
106106
fn build_config(strategy: &str) -> Result<Option<RenderConfig>, JsValue> {
107107
match strategy {
108-
"" | "none" => Ok(None),
109-
"camelCase" => Ok(Some(
110-
RenderConfig::new().with_attribute_name_strategy(AttributeNameStrategy::CamelCase),
108+
"" | "camelCase" => Ok(None),
109+
"none" => Ok(Some(
110+
RenderConfig::new().with_attribute_name_strategy(AttributeNameStrategy::None),
111111
)),
112112
_ => Err(JsValue::from_str(&format!(
113113
"Invalid attribute-name-strategy '{}': expected 'none' or 'camelCase'",

crates/microsoft-fast-build/tests/attribute_name_strategy.rs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,24 +118,24 @@ fn test_none_strategy_multi_dashed_attr_preserved() {
118118
assert!(result.contains("42"), "none strategy should preserve dashes: {result}");
119119
}
120120

121-
// ── default config matches none strategy ──────────────────────────────────────
121+
// ── default config matches camelCase strategy ─────────────────────────────────
122122

123123
#[test]
124-
fn test_default_config_matches_none() {
125-
let locator = make_locator(&[("my-el", "<span>{{foo-bar}}</span>")]);
124+
fn test_default_config_matches_camel_case() {
125+
let locator = make_locator(&[("my-el", "<span>{{fooBar}}</span>")]);
126126
let result_default = render_with_locator(
127127
r#"<my-el foo-bar="hello"></my-el>"#,
128128
&empty_root(),
129129
&locator,
130130
None,
131131
).unwrap();
132-
let result_none = render_with_locator(
132+
let result_camel = render_with_locator(
133133
r#"<my-el foo-bar="hello"></my-el>"#,
134134
&empty_root(),
135135
&locator,
136-
Some(&none_config()),
136+
Some(&camel_config()),
137137
).unwrap();
138-
assert_eq!(result_default, result_none, "default should match none strategy");
138+
assert_eq!(result_default, result_camel, "default should match camelCase strategy");
139139
}
140140

141141
// ── camelCase with binding resolution ─────────────────────────────────────────

crates/microsoft-fast-build/tests/custom_elements.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
mod common;
22
use common::{make_locator, empty_root};
3-
use microsoft_fast_build::{render_template, render_with_locator, render_template_with_locator, render_entry_template_with_locator, Locator, RenderError};
3+
use microsoft_fast_build::{render_template, render_with_locator, render_template_with_locator, render_entry_template_with_locator, Locator, RenderError, RenderConfig, AttributeNameStrategy};
44

55
// ── attribute → state mapping ─────────────────────────────────────────────────
66

@@ -218,26 +218,28 @@ fn test_locator_name_from_f_template_attribute_not_file_stem() {
218218

219219
#[test]
220220
fn test_custom_element_kebab_attr_hyphens_preserved() {
221-
// kebab-case attr names are lowercased; hyphens are preserved
221+
// kebab-case attr names are lowercased; with explicit none strategy, hyphens are preserved
222222
let locator = make_locator(&[("my-el", "<span>{{selected-user-id}}</span>")]);
223+
let none_config = RenderConfig::new().with_attribute_name_strategy(AttributeNameStrategy::None);
223224
let result = render_template_with_locator(
224225
r#"<my-el selected-user-id="42"></my-el>"#,
225226
"{}",
226227
&locator,
227-
None,
228+
Some(&none_config),
228229
).unwrap();
229230
assert!(result.contains("42"), "kebab attr resolved: {result}");
230231
}
231232

232233
#[test]
233234
fn test_custom_element_multi_word_kebab_attrs() {
234-
// multiple kebab-case attrs are lowercased and passed to the child scope as-is
235+
// multiple kebab-case attrs are lowercased; with explicit none strategy, passed to the child scope as-is
235236
let locator = make_locator(&[("my-el", "<p>{{show-details}}</p><p>{{enable-continue}}</p>")]);
237+
let none_config = RenderConfig::new().with_attribute_name_strategy(AttributeNameStrategy::None);
236238
let result = render_template_with_locator(
237239
r#"<my-el show-details="true" enable-continue="false"></my-el>"#,
238240
"{}",
239241
&locator,
240-
None,
242+
Some(&none_config),
241243
).unwrap();
242244
assert!(result.contains("true"), "show-details: {result}");
243245
assert!(result.contains("false"), "enable-continue: {result}");

packages/fast-build/DESIGN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ Three WASM functions are used:
148148
| Function | Used when |
149149
|----------|-----------|
150150
| `wasm.render(entry, state)` | No custom element templates |
151-
| `wasm.render_entry_with_templates(entry, templatesJson, state, strategy)` | At least one template was loaded. `strategy` is `"none"` or `"camelCase"`. |
151+
| `wasm.render_entry_with_templates(entry, templatesJson, state, strategy)` | At least one template was loaded. `strategy` is `"camelCase"` or `"none"`. |
152152
| `wasm.parse_f_templates(html)` | Parsing `<f-template>` elements from each matched HTML file |
153153

154154
`templatesJson` is a JSON-stringified object mapping element names to their raw inner template strings (the content extracted from `<template>` inside `<f-template>`). The WASM renderer uses this map to resolve custom element tags and inject Declarative Shadow DOM.

packages/fast-build/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ fast build [options]
3939
| `--state="<path>"` | `state.json` | JSON file containing the template state |
4040
| `--output="<path>"` | `output.html` | Where to write the rendered HTML |
4141
| `--templates="<glob>"` | _(none)_ | Glob pattern(s) for custom element template HTML files. Separate multiple patterns with commas. A warning is printed if not provided or if no files match a pattern. |
42-
| `--attribute-name-strategy="<strategy>"` | `none` | Strategy for mapping HTML attribute names to state property names on custom elements. `"none"` preserves dashes (e.g. `foo-bar``foo-bar`). `"camelCase"` converts dashes to camelCase (e.g. `foo-bar``fooBar`). See [Attribute name strategy](#attribute-name-strategy). |
42+
| `--attribute-name-strategy="<strategy>"` | `camelCase` | Strategy for mapping HTML attribute names to state property names on custom elements. `"camelCase"` converts dashes to camelCase (e.g. `foo-bar``fooBar`). `"none"` preserves dashes (e.g. `foo-bar``foo-bar`). See [Attribute name strategy](#attribute-name-strategy). |
4343
| `--config="<path>"` | `fast-build.config.json` | Path to a JSON configuration file. If omitted, `fast-build.config.json` in the current directory is used when present. CLI arguments take precedence over config values. See [Configuration file](#configuration-file). |
4444

4545
### Example
@@ -113,8 +113,8 @@ The `--attribute-name-strategy` option controls how HTML attribute names on cust
113113

114114
| Strategy | Behaviour | Template binding |
115115
|---|---|---|
116-
| `none` (default) | Attribute names lowercased as-is, dashes preserved | `foo-bar``{{foo-bar}}` |
117-
| `camelCase` | Dashed attribute names converted to camelCase | `foo-bar``{{fooBar}}` |
116+
| `camelCase` (default) | Dashed attribute names converted to camelCase | `foo-bar``{{fooBar}}` |
117+
| `none` | Attribute names lowercased as-is, dashes preserved | `foo-bar``{{foo-bar}}` |
118118

119119
The `camelCase` strategy only affects "plain" custom element attributes. It does **not** change:
120120
- `data-*` attributes (always use `dataset.*` grouping)

packages/fast-build/bin/fast.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ async function runBuild(args) {
335335
let rendered;
336336
if (Object.keys(templatesMap).length > 0) {
337337
rendered = wasm.render_entry_with_templates(
338-
entryContent, JSON.stringify(templatesMap), stateContent, attributeNameStrategy || "none"
338+
entryContent, JSON.stringify(templatesMap), stateContent, attributeNameStrategy || ""
339339
);
340340
} else {
341341
rendered = wasm.render(entryContent, stateContent);
@@ -357,9 +357,9 @@ async function main() {
357357
' --output="output.html" Output file path (default: output.html)\n' +
358358
' --entry="index.html" Entry HTML template file (default: index.html)\n' +
359359
' --state="state.json" State JSON file (default: state.json)\n' +
360-
' --attribute-name-strategy="none"\n' +
360+
' --attribute-name-strategy="camelCase"\n' +
361361
' Strategy for mapping attribute names to property names.\n' +
362-
' "none" (default) or "camelCase".\n' +
362+
' "camelCase" (default) or "none".\n' +
363363
' --config="<path>" Path to a fast-build config JSON file.\n' +
364364
' Defaults to "fast-build.config.json" in the\n' +
365365
' current directory if it exists. File paths in\n' +

0 commit comments

Comments
 (0)