From 3859b74796ad26f4242e1021e73b708545aaed01 Mon Sep 17 00:00:00 2001 From: Jane Chu <7559015+janechu@users.noreply.github.com> Date: Wed, 22 Apr 2026 16:22:59 -0700 Subject: [PATCH 01/14] feat: change default attribute-name-strategy from none to camelCase (#7479) 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. - 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`) - [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. (cherry picked from commit 77cd24b00d199fd4b2d2e42a6f2dce2be96396b5) --- ...-285b7229-8023-4304-83b0-25de34f617ce.json | 7 +++++++ ...-c20e636d-e975-46f4-9e0b-c7b6c8143ef6.json | 7 +++++++ crates/microsoft-fast-build/DESIGN.md | 4 ++-- crates/microsoft-fast-build/src/config.rs | 2 +- crates/microsoft-fast-build/src/wasm.rs | 16 +++++++-------- .../tests/attribute_name_strategy.rs | 12 +++++------ .../tests/custom_elements.rs | 12 ++++++----- packages/fast-build/DESIGN.md | 2 +- packages/fast-build/README.md | 6 +++--- packages/fast-build/bin/fast.js | 6 +++--- .../src/declarative/attribute-map.spec.ts | 20 ++++++++++--------- .../attribute-map/attribute-map.spec.ts | 14 ++++++------- .../extensions/attribute-map/index.html | 2 +- .../extensions/attribute-map/state.json | 2 +- .../extensions/attribute-map/templates.html | 2 +- 15 files changed, 66 insertions(+), 48 deletions(-) create mode 100644 change/@microsoft-fast-build-285b7229-8023-4304-83b0-25de34f617ce.json create mode 100644 change/@microsoft-fast-element-c20e636d-e975-46f4-9e0b-c7b6c8143ef6.json diff --git a/change/@microsoft-fast-build-285b7229-8023-4304-83b0-25de34f617ce.json b/change/@microsoft-fast-build-285b7229-8023-4304-83b0-25de34f617ce.json new file mode 100644 index 00000000000..a60d87b93b4 --- /dev/null +++ b/change/@microsoft-fast-build-285b7229-8023-4304-83b0-25de34f617ce.json @@ -0,0 +1,7 @@ +{ + "type": "minor", + "comment": "feat: change default attribute-name-strategy from none to camelCase", + "packageName": "@microsoft/fast-build", + "email": "7559015+janechu@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/change/@microsoft-fast-element-c20e636d-e975-46f4-9e0b-c7b6c8143ef6.json b/change/@microsoft-fast-element-c20e636d-e975-46f4-9e0b-c7b6c8143ef6.json new file mode 100644 index 00000000000..9ef94540ce3 --- /dev/null +++ b/change/@microsoft-fast-element-c20e636d-e975-46f4-9e0b-c7b6c8143ef6.json @@ -0,0 +1,7 @@ +{ + "type": "prerelease", + "comment": "feat: change default attribute-name-strategy from none to camelCase", + "packageName": "@microsoft/fast-element", + "email": "7559015+janechu@users.noreply.github.com", + "dependentChangeType": "none" +} diff --git a/crates/microsoft-fast-build/DESIGN.md b/crates/microsoft-fast-build/DESIGN.md index e8e85d82fd1..2132eebc3ad 100644 --- a/crates/microsoft-fast-build/DESIGN.md +++ b/crates/microsoft-fast-build/DESIGN.md @@ -267,8 +267,8 @@ Controls how HTML attribute names are mapped to state property names when buildi | Strategy | Behaviour | Example | |----------|-----------|---------| -| `"none"` (default) | Attribute names are lowercased as-is. Dashes are preserved. | `foo-bar` → `foo-bar` → `{{foo-bar}}` | -| `"camelCase"` | Dashed attribute names are converted to camelCase. | `foo-bar` → `fooBar` → `{{fooBar}}` | +| `"camelCase"` (default) | Dashed attribute names are converted to camelCase. | `foo-bar` → `fooBar` → `{{fooBar}}` | +| `"none"` | Attribute names are lowercased as-is. Dashes are preserved. | `foo-bar` → `foo-bar` → `{{foo-bar}}` | The `camelCase` strategy only applies to attributes that are **not** already handled by a specialized conversion: diff --git a/crates/microsoft-fast-build/src/config.rs b/crates/microsoft-fast-build/src/config.rs index ab749e3ea1e..d354acb6d76 100644 --- a/crates/microsoft-fast-build/src/config.rs +++ b/crates/microsoft-fast-build/src/config.rs @@ -8,13 +8,13 @@ pub enum AttributeNameStrategy { /// No conversion — attribute names are lowercased as-is. /// `foo-bar` stays `foo-bar`, matching `{{foo-bar}}` in the template. - #[default] None, /// Convert dashed attribute names to camelCase. /// `foo-bar` becomes `fooBar`, matching `{{fooBar}}` in the template. /// Attributes that are already handled by specialized lookup tables /// (`data-*`, `aria-*`, and HTML global attributes like `tabindex`) /// are unaffected — those always use their standard property names. + #[default] CamelCase, } diff --git a/crates/microsoft-fast-build/src/wasm.rs b/crates/microsoft-fast-build/src/wasm.rs index 742ff8b5c09..4a34dbaffa7 100644 --- a/crates/microsoft-fast-build/src/wasm.rs +++ b/crates/microsoft-fast-build/src/wasm.rs @@ -13,8 +13,8 @@ pub fn render(entry: &str, state: &str) -> Result { /// Render a FAST HTML template with custom element templates and a JSON state string. /// `templates_json` is a JSON object mapping element names to their HTML template strings, /// e.g. `{"my-button": ""}`. -/// `attribute_name_strategy` controls attribute-to-property mapping: `"none"` (default) -/// or `"camelCase"`. Pass an empty string for the default. +/// `attribute_name_strategy` controls attribute-to-property mapping: `"camelCase"` (default) +/// or `"none"`. Pass an empty string for the default. /// Returns the rendered HTML or throws a JavaScript error. #[wasm_bindgen] pub fn render_with_templates(entry: &str, templates_json: &str, state: &str, attribute_name_strategy: &str) -> Result { @@ -32,8 +32,8 @@ pub fn render_with_templates(entry: &str, templates_json: &str, state: &str, att /// output, while non-primitive values (`array`, `object`, `null`) are stripped. /// /// `templates_json` is a JSON object mapping element names to their HTML template strings. -/// `attribute_name_strategy` controls attribute-to-property mapping: `"none"` (default) -/// or `"camelCase"`. Pass an empty string for the default. +/// `attribute_name_strategy` controls attribute-to-property mapping: `"camelCase"` (default) +/// or `"none"`. Pass an empty string for the default. /// Returns the rendered HTML or throws a JavaScript error. #[wasm_bindgen] pub fn render_entry_with_templates(entry: &str, templates_json: &str, state: &str, attribute_name_strategy: &str) -> Result { @@ -102,12 +102,12 @@ fn parse_templates_map(templates_json: &str) -> Result, } /// Build an `Option` from the strategy string. -/// Returns `None` for `""` or `"none"` (use defaults), `Some(config)` otherwise. +/// Returns `None` for `""` or `"camelCase"` (use defaults), `Some(config)` for `"none"`. fn build_config(strategy: &str) -> Result, JsValue> { match strategy { - "" | "none" => Ok(None), - "camelCase" => Ok(Some( - RenderConfig::new().with_attribute_name_strategy(AttributeNameStrategy::CamelCase), + "" | "camelCase" => Ok(None), + "none" => Ok(Some( + RenderConfig::new().with_attribute_name_strategy(AttributeNameStrategy::None), )), _ => Err(JsValue::from_str(&format!( "Invalid attribute-name-strategy '{}': expected 'none' or 'camelCase'", diff --git a/crates/microsoft-fast-build/tests/attribute_name_strategy.rs b/crates/microsoft-fast-build/tests/attribute_name_strategy.rs index 50854020e16..ae29b5dd22c 100644 --- a/crates/microsoft-fast-build/tests/attribute_name_strategy.rs +++ b/crates/microsoft-fast-build/tests/attribute_name_strategy.rs @@ -118,24 +118,24 @@ fn test_none_strategy_multi_dashed_attr_preserved() { assert!(result.contains("42"), "none strategy should preserve dashes: {result}"); } -// ── default config matches none strategy ────────────────────────────────────── +// ── default config matches camelCase strategy ───────────────────────────────── #[test] -fn test_default_config_matches_none() { - let locator = make_locator(&[("my-el", "{{foo-bar}}")]); +fn test_default_config_matches_camel_case() { + let locator = make_locator(&[("my-el", "{{fooBar}}")]); let result_default = render_with_locator( r#""#, &empty_root(), &locator, None, ).unwrap(); - let result_none = render_with_locator( + let result_camel = render_with_locator( r#""#, &empty_root(), &locator, - Some(&none_config()), + Some(&camel_config()), ).unwrap(); - assert_eq!(result_default, result_none, "default should match none strategy"); + assert_eq!(result_default, result_camel, "default should match camelCase strategy"); } // ── camelCase with binding resolution ───────────────────────────────────────── diff --git a/crates/microsoft-fast-build/tests/custom_elements.rs b/crates/microsoft-fast-build/tests/custom_elements.rs index 32e791de185..1cc09074069 100644 --- a/crates/microsoft-fast-build/tests/custom_elements.rs +++ b/crates/microsoft-fast-build/tests/custom_elements.rs @@ -1,6 +1,6 @@ mod common; use common::{make_locator, empty_root}; -use microsoft_fast_build::{render_template, render_with_locator, render_template_with_locator, render_entry_template_with_locator, Locator, RenderError}; +use microsoft_fast_build::{render_template, render_with_locator, render_template_with_locator, render_entry_template_with_locator, Locator, RenderError, RenderConfig, AttributeNameStrategy}; // ── attribute → state mapping ───────────────────────────────────────────────── @@ -218,26 +218,28 @@ fn test_locator_name_from_f_template_attribute_not_file_stem() { #[test] fn test_custom_element_kebab_attr_hyphens_preserved() { - // kebab-case attr names are lowercased; hyphens are preserved + // kebab-case attr names are lowercased; with explicit none strategy, hyphens are preserved let locator = make_locator(&[("my-el", "{{selected-user-id}}")]); + let none_config = RenderConfig::new().with_attribute_name_strategy(AttributeNameStrategy::None); let result = render_template_with_locator( r#""#, "{}", &locator, - None, + Some(&none_config), ).unwrap(); assert!(result.contains("42"), "kebab attr resolved: {result}"); } #[test] fn test_custom_element_multi_word_kebab_attrs() { - // multiple kebab-case attrs are lowercased and passed to the child scope as-is + // multiple kebab-case attrs are lowercased; with explicit none strategy, passed to the child scope as-is let locator = make_locator(&[("my-el", "

{{show-details}}

{{enable-continue}}

")]); + let none_config = RenderConfig::new().with_attribute_name_strategy(AttributeNameStrategy::None); let result = render_template_with_locator( r#""#, "{}", &locator, - None, + Some(&none_config), ).unwrap(); assert!(result.contains("true"), "show-details: {result}"); assert!(result.contains("false"), "enable-continue: {result}"); diff --git a/packages/fast-build/DESIGN.md b/packages/fast-build/DESIGN.md index 01c54945e2e..193c004120c 100644 --- a/packages/fast-build/DESIGN.md +++ b/packages/fast-build/DESIGN.md @@ -148,7 +148,7 @@ Three WASM functions are used: | Function | Used when | |----------|-----------| | `wasm.render(entry, state)` | No custom element templates | -| `wasm.render_entry_with_templates(entry, templatesJson, state, strategy)` | At least one template was loaded. `strategy` is `"none"` or `"camelCase"`. | +| `wasm.render_entry_with_templates(entry, templatesJson, state, strategy)` | At least one template was loaded. `strategy` is `"camelCase"` or `"none"`. | | `wasm.parse_f_templates(html)` | Parsing `` elements from each matched HTML file | `templatesJson` is a JSON-stringified object mapping element names to their raw inner template strings (the content extracted from `