Menggunakan Typed Object Model CSS yang baru

TL;DR (Ringkasan)

CSS kini memiliki API berbasis objek yang tepat untuk menggunakan nilai di JavaScript.

el.attributeStyleMap.set('padding', CSS.px(42));
const padding = el.attributeStyleMap.get('padding');
console.log(padding.value, padding.unit); // 42, 'px'

Masa-masa menggabungkan string dan bug halus sudah berakhir!

Pengantar

Engage Lama

CSS telah memiliki model objek (CSSOM) selama bertahun-tahun. Bahkan, setiap kali Anda membaca/menetapkan .style di JavaScript, Anda sedang menggunakannya:

// Element styles.
el.style.opacity = 0.3;
typeof el.style.opacity === 'string' // Ugh. A string!?

// Stylesheet rules.
document.styleSheets[0].cssRules[0].style.opacity = 0.3;

OM dengan Jenis CSS Baru

CSS Typed Object Model (Typed OM) baru yang merupakan bagian dari upaya Houdini, memperluas pandangan umum ini dengan menambahkan jenis, metode, dan model objek yang tepat ke nilai CSS. Sebagai ganti string, nilai ditampilkan sebagai objek JavaScript untuk memfasilitasi manipulasi CSS yang berperforma tinggi (dan logis).

Sebagai ganti menggunakan element.style, Anda akan mengakses gaya melalui properti .attributeStyleMap baru untuk elemen dan properti .styleMap untuk aturan stylesheet. Keduanya menampilkan objek StylePropertyMap.

// Element styles.
el.attributeStyleMap.set('opacity', 0.3);
typeof el.attributeStyleMap.get('opacity').value === 'number' // Yay, a number!

// Stylesheet rules.
const stylesheet = document.styleSheets[0];
stylesheet.cssRules[0].styleMap.set('background', 'blue');

Karena StylePropertyMap adalah objek mirip Peta, objek ini mendukung semua kecurigaan biasa (get/set/keys/values/entries), sehingga fleksibel untuk digunakan:

// All 3 of these are equivalent:
el.attributeStyleMap.set('opacity', 0.3);
el.attributeStyleMap.set('opacity', '0.3');
el.attributeStyleMap.set('opacity', CSS.number(0.3)); // see next section
// el.attributeStyleMap.get('opacity').value === 0.3

// StylePropertyMaps are iterable.
for (const [prop, val] of el.attributeStyleMap) {
  console.log(prop, val.value);
}
// → opacity, 0.3

el.attributeStyleMap.has('opacity') // true

el.attributeStyleMap.delete('opacity') // remove opacity.

el.attributeStyleMap.clear(); // remove all styles.

Perhatikan bahwa dalam contoh kedua, opacity disetel ke string ('0.3'), tetapi angka akan kembali saat properti dibaca kembali nanti.

Manfaat

Jadi masalah apa yang coba dipecahkan oleh CSS Typed OM? Dengan melihat contoh di atas (dan di sepanjang artikel ini), Anda mungkin berpendapat bahwa CSS Typed OM jauh lebih panjang daripada model objek lama. Saya setuju.

Sebelum menghapus Typed OM, pertimbangkan beberapa fitur utama yang dibawanya ke tabel:

  • Lebih sedikit bug. misalnya, nilai numerik selalu ditampilkan sebagai angka, bukan string.

    el.style.opacity += 0.1;
    el.style.opacity === '0.30.1' // dragons!
    
  • Operasi aritmetika & konversi satuan. mengonversi antara satuan panjang absolut (mis. px -> cm) dan melakukan penghitungan dasar.

  • Pembulatan & pembulatan nilai. Nilai bulat dan/atau klem OM yang diketik sehingga berada dalam rentang yang dapat diterima untuk sebuah properti.

  • Performa yang lebih baik. Browser harus melakukan lebih sedikit pekerjaan serialisasi dan deserialisasi nilai string. Sekarang, mesin ini menggunakan pemahaman serupa tentang nilai CSS di seluruh JS dan C++. Tab Akins telah menunjukkan beberapa tolok ukur performa awal yang menempatkan Typed OM pada ~30% lebih cepat dalam operasi/dtk jika dibandingkan dengan menggunakan WorkManager dan string lama. Hal ini bisa menjadi signifikan untuk animasi CSS cepat yang menggunakan requestionAnimationFrame(). crbug.com/808933 melacak pekerjaan performa tambahan di Blink.

  • Penanganan error. Metode penguraian baru menghadirkan penanganan error di dunia CSS.

  • "Haruskah saya menggunakan nama atau string CSS ber-cased?" Tidak perlu lagi menebak apakah nama menggunakan camel-case atau string (misalnya el.style.backgroundColor vs el.style['background-color']). Nama properti CSS di Typed OM selalu berupa string, sesuai dengan yang sebenarnya Anda tulis di CSS :)

Dukungan browser & deteksi fitur

Ketik OM muncul di Chrome 66 dan sedang diterapkan di Firefox. Edge telah menunjukkan tanda-tanda dukungan, tetapi belum menambahkannya ke dasbor platform mereka.

Untuk deteksi fitur, Anda dapat memeriksa apakah salah satu factory numerik CSS.* ditentukan:

if (window.CSS && CSS.number) {
  // Supports CSS Typed OM.
}

Dasar-Dasar API

Mengakses gaya

Nilai terpisah dari unit di OM Berjenis CSS. Mendapatkan gaya akan menampilkan CSSUnitValue yang berisi value dan unit:

el.attributeStyleMap.set('margin-top', CSS.px(10));
// el.attributeStyleMap.set('margin-top', '10px'); // string arg also works.
el.attributeStyleMap.get('margin-top').value  // 10
el.attributeStyleMap.get('margin-top').unit // 'px'

// Use CSSKeyWorldValue for plain text values:
el.attributeStyleMap.set('display', new CSSKeywordValue('initial'));
el.attributeStyleMap.get('display').value // 'initial'
el.attributeStyleMap.get('display').unit // undefined

Gaya yang dihitung

Gaya terkomputasi telah dipindahkan dari API pada window ke metode baru pada HTMLElement, computedStyleMap():

Googlebot Lama

el.style.opacity = 0.5;
window.getComputedStyle(el).opacity === "0.5" // Ugh, more strings!

OM dengan Jenis Baru

el.attributeStyleMap.set('opacity', 0.5);
el.computedStyleMap().get('opacity').value // 0.5

Pembulatan / penjepit nilai

Salah satu fitur menarik dari model objek baru adalah penjepitan dan/atau pembulatan nilai gaya terkomputasi secara otomatis. Sebagai contoh, misalkan Anda mencoba menetapkan opacity ke nilai di luar rentang yang dapat diterima, [0, 1]. OM yang diketik akan membatasi nilai menjadi 1 saat menghitung gaya:

el.attributeStyleMap.set('opacity', 3);
el.attributeStyleMap.get('opacity').value === 3  // val not clamped.
el.computedStyleMap().get('opacity').value === 1 // computed style clamps value.

Demikian pula, menyetel z-index:15.4 akan dibulatkan ke 15 sehingga nilainya tetap bilangan bulat.

el.attributeStyleMap.set('z-index', CSS.number(15.4));
el.attributeStyleMap.get('z-index').value  === 15.4 // val not rounded.
el.computedStyleMap().get('z-index').value === 15   // computed style is rounded.

Nilai numerik CSS

Angka direpresentasikan oleh dua jenis objek CSSNumericValue di Typed OM:

  1. CSSUnitValue - nilai yang berisi satu jenis unit (misalnya "42px").
  2. CSSMathValue - nilai yang berisi lebih dari satu nilai/unit, seperti ekspresi matematika (mis. "calc(56em + 10%)").

Nilai satuan

Nilai numerik sederhana ("50%") diwakili oleh objek CSSUnitValue. Meskipun Anda dapat membuat objek ini secara langsung (new CSSUnitValue(10, 'px')), biasanya Anda akan menggunakan metode factory CSS.*:

const {value, unit} = CSS.number('10');
// value === 10, unit === 'number'

const {value, unit} = CSS.px(42);
// value === 42, unit === 'px'

const {value, unit} = CSS.vw('100');
// value === 100, unit === 'vw'

const {value, unit} = CSS.percent('10');
// value === 10, unit === 'percent'

const {value, unit} = CSS.deg(45);
// value === 45, unit === 'deg'

const {value, unit} = CSS.ms(300);
// value === 300, unit === 'ms'

Lihat spesifikasi untuk daftar lengkap metode CSS.*.

Nilai matematika

Objek CSSMathValue mewakili ekspresi matematika dan biasanya berisi lebih dari satu nilai/unit. Contoh umumnya adalah membuat ekspresi calc() CSS, tetapi ada beberapa metode untuk semua fungsi CSS: calc(), min(), max().

new CSSMathSum(CSS.vw(100), CSS.px(-10)).toString(); // "calc(100vw + -10px)"

new CSSMathNegate(CSS.px(42)).toString() // "calc(-42px)"

new CSSMathInvert(CSS.s(10)).toString() // "calc(1 / 10s)"

new CSSMathProduct(CSS.deg(90), CSS.number(Math.PI/180)).toString();
// "calc(90deg * 0.0174533)"

new CSSMathMin(CSS.percent(80), CSS.px(12)).toString(); // "min(80%, 12px)"

new CSSMathMax(CSS.percent(80), CSS.px(12)).toString(); // "max(80%, 12px)"

Ekspresi bertingkat

Menggunakan fungsi matematika untuk membuat nilai yang lebih kompleks akan agak membingungkan. Berikut ini beberapa contoh untuk membantu Anda memulai. saya telah menambahkan indentasi ekstra untuk membuatnya lebih mudah dibaca.

calc(1px - 2 * 3em) akan dibuat sebagai:

new CSSMathSum(
  CSS.px(1),
  new CSSMathNegate(
    new CSSMathProduct(2, CSS.em(3))
  )
);

calc(1px + 2px + 3px) akan dibuat sebagai:

new CSSMathSum(CSS.px(1), CSS.px(2), CSS.px(3));

calc(calc(1px + 2px) + 3px) akan dibuat sebagai:

new CSSMathSum(
  new CSSMathSum(CSS.px(1), CSS.px(2)),
  CSS.px(3)
);

Operasi aritmetika

Salah satu fitur OM Berjenis CSS yang paling berguna adalah Anda dapat menjalankan operasi matematika pada objek CSSUnitValue.

Operasi dasar

Operasi dasar (add/sub/mul/div/min/max) didukung:

CSS.deg(45).mul(2) // {value: 90, unit: "deg"}

CSS.percent(50).max(CSS.vw(50)).toString() // "max(50%, 50vw)"

// Can Pass CSSUnitValue:
CSS.px(1).add(CSS.px(2)) // {value: 3, unit: "px"}

// multiple values:
CSS.s(1).sub(CSS.ms(200), CSS.ms(300)).toString() // "calc(1s + -200ms + -300ms)"

// or pass a `CSSMathSum`:
const sum = new CSSMathSum(CSS.percent(100), CSS.px(20)));
CSS.vw(100).add(sum).toString() // "calc(100vw + (100% + 20px))"

Konversi

Satuan panjang absolut dapat dikonversi ke panjang satuan lainnya:

// Convert px to other absolute/physical lengths.
el.attributeStyleMap.set('width', '500px');
const width = el.attributeStyleMap.get('width');
width.to('mm'); // CSSUnitValue {value: 132.29166666666669, unit: "mm"}
width.to('cm'); // CSSUnitValue {value: 13.229166666666668, unit: "cm"}
width.to('in'); // CSSUnitValue {value: 5.208333333333333, unit: "in"}

CSS.deg(200).to('rad').value // 3.49066...
CSS.s(2).to('ms').value // 2000

Persamaan

const width = CSS.px(200);
CSS.px(200).equals(width) // true

const rads = CSS.deg(180).to('rad');
CSS.deg(180).equals(rads.to('deg')) // true

Nilai transformasi CSS

Transformasi CSS dibuat dengan CSSTransformValue dan meneruskan array nilai transformasi (misalnya CSSRotate, CSScale, CSSSkew, CSSSkewX, CSSSkewY). Sebagai contoh, Anda ingin membuat ulang CSS ini:

transform: rotateZ(45deg) scale(0.5) translate3d(10px,10px,10px);

Diterjemahkan ke dalam Typed OM:

const transform =  new CSSTransformValue([
  new CSSRotate(CSS.deg(45)),
  new CSSScale(CSS.number(0.5), CSS.number(0.5)),
  new CSSTranslate(CSS.px(10), CSS.px(10), CSS.px(10))
]);

Selain panjangnya (lolz!), CSSTransformValue memiliki beberapa fitur keren. Class ini memiliki properti boolean untuk membedakan transformasi 2D dan 3D serta metode .toMatrix() untuk menampilkan representasi DOMMatrix transformasi:

new CSSTranslate(CSS.px(10), CSS.px(10)).is2D // true
new CSSTranslate(CSS.px(10), CSS.px(10), CSS.px(10)).is2D // false
new CSSTranslate(CSS.px(10), CSS.px(10)).toMatrix() // DOMMatrix

Contoh: menganimasikan kubus

Mari kita lihat contoh praktis penggunaan transformasi. Kita akan menggunakan transformasi JavaScript dan CSS untuk menganimasikan kubus.

const rotate = new CSSRotate(0, 0, 1, CSS.deg(0));
const transform = new CSSTransformValue([rotate]);

const box = document.querySelector('#box');
box.attributeStyleMap.set('transform', transform);

(function draw() {
  requestAnimationFrame(draw);
  transform[0].angle.value += 5; // Update the transform's angle.
  // rotate.angle.value += 5; // Or, update the CSSRotate object directly.
  box.attributeStyleMap.set('transform', transform); // commit it.
})();

Perhatikan bahwa:

  1. Nilai numerik berarti kita dapat menambah sudut secara langsung menggunakan matematika.
  2. Alih-alih menyentuh DOM atau membaca kembali nilai pada setiap frame (misalnya, tanpa box.style.transform=`rotate(0,0,1,${newAngle}deg)`), animasi didorong dengan memperbarui objek data CSSTransformValue yang mendasarinya, sehingga meningkatkan performa.

Demo

Di bawah ini, Anda akan melihat kubus merah jika browser Anda mendukung Typed OM. Kubus mulai berputar saat Anda mengarahkan kursor ke atasnya. Animasi ini didukung oleh CSS Typed OM! 🤘

Nilai properti kustom CSS

var() CSS menjadi objek CSSVariableReferenceValue di OM yang Diketik. Nilainya diuraikan menjadi CSSUnparsedValue karena dapat mengambil jenis apa pun (px, %, em, rgba(), dll.).

const foo = new CSSVariableReferenceValue('--foo');
// foo.variable === '--foo'

// Fallback values:
const padding = new CSSVariableReferenceValue(
    '--default-padding', new CSSUnparsedValue(['8px']));
// padding.variable === '--default-padding'
// padding.fallback instanceof CSSUnparsedValue === true
// padding.fallback[0] === '8px'

Jika Anda ingin mendapatkan nilai properti khusus, ada sedikit pekerjaan yang harus dilakukan:

<style>
  body {
    --foo: 10px;
  }
</style>
<script>
  const styles = document.querySelector('style');
  const foo = styles.sheet.cssRules[0].styleMap.get('--foo').trim();
  console.log(CSSNumericValue.parse(foo).value); // 10
</script>

Nilai posisi

Properti CSS yang mengambil posisi x/y yang dipisahkan spasi seperti object-position direpresentasikan oleh objek CSSPositionValue.

const position = new CSSPositionValue(CSS.px(5), CSS.px(10));
el.attributeStyleMap.set('object-position', position);

console.log(position.x.value, position.y.value);
// → 5, 10

Mengurai nilai

Typed OM memperkenalkan metode penguraian ke platform web. Artinya, Anda akhirnya dapat mengurai nilai CSS secara terprogram, sebelum mencoba menggunakannya. Kemampuan baru ini berpotensi menyelamatkan nyawa untuk menangkap bug awal dan format CSS yang salah.

Uraikan gaya lengkap:

const css = CSSStyleValue.parse(
    'transform', 'translate3d(10px,10px,0) scale(0.5)');
// → css instanceof CSSTransformValue === true
// → css.toString() === 'translate3d(10px, 10px, 0) scale(0.5)'

Uraikan nilai ke dalam CSSUnitValue:

CSSNumericValue.parse('42.0px') // {value: 42, unit: 'px'}

// But it's easier to use the factory functions:
CSS.px(42.0) // '42px'

Penanganan error

Contoh - periksa apakah parser CSS akan puas dengan nilai transform ini:

try {
  const css = CSSStyleValue.parse('transform', 'translate4d(bogus value)');
  // use css
} catch (err) {
  console.err(err);
}

Kesimpulan

Senang rasanya bisa memiliki model objek yang diperbarui untuk CSS. Bekerja dengan {i>string<i} tidak terasa tepat bagi saya. CSS Typed OM API sedikit panjang, tetapi semoga menghasilkan lebih sedikit bug dan kode yang lebih berperforma tinggi nantinya.