Menggunakan Typed Object Model CSS yang baru

TL;DR

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

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

Era penggabungan string dan bug halus telah berakhir.

Pengantar

CSSOM Lama

CSS telah memiliki model objek (CSSOM) selama bertahun-tahun. Sebenarnya, setiap kali Anda membaca/menetapkan .style di JavaScript, Anda 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;

Typed OM CSS Baru

CSS Typed Object Model (Typed OM) baru, yang merupakan bagian dari upaya Houdini, memperluas pandangan dunia ini dengan menambahkan jenis, metode, dan model objek yang tepat ke nilai CSS. Alih-alih string, nilai ditampilkan sebagai objek JavaScript untuk memfasilitasi manipulasi CSS yang berperforma (dan masuk akal).

Daripada 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 seperti Map, objek ini mendukung semua suspek 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 pada contoh kedua, opacity ditetapkan ke string ('0.3'), tetapi angka akan muncul kembali saat properti dibaca kembali nanti.

Manfaat

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

Sebelum Anda mengabaikan OM yang Diketik, pertimbangkan beberapa fitur utama yang ditawarkannya:

  • 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 aritmatika & konversi satuan. Mengonversi antara satuan panjang absolut (misalnya, px -> cm) dan melakukan matematika dasar.

  • Pembatasan & pembulatan nilai. OM yang diketik membulatkan dan/atau mengapit nilai sehingga berada dalam rentang yang dapat diterima untuk suatu properti.

  • Performa yang lebih baik. Browser harus melakukan lebih sedikit pekerjaan untuk membuat serial dan mendeserialisasi nilai string. Sekarang, mesin menggunakan pemahaman yang serupa tentang nilai CSS di seluruh JS dan C++. Tab Atkins telah menunjukkan beberapa tolok ukur performa awal yang menempatkan Typed OM ~30% lebih cepat dalam operasi/detik jika dibandingkan dengan penggunaan CSSOM dan string lama. Hal ini dapat signifikan untuk animasi CSS cepat 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 dengan huruf camel case?" Tidak perlu lagi menebak apakah nama ditulis dalam camel case atau string (misalnya, el.style.backgroundColor vs. el.style['background-color']). Nama properti CSS di Typed OM selalu berupa string, yang cocok dengan apa yang sebenarnya Anda tulis di CSS :)

Dukungan browser & deteksi fitur

OM yang diketik tersedia di Chrome 66 dan sedang diterapkan di Firefox. Edge telah menunjukkan 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 CSS Typed OM. 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 dikomputasi

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

CSSOM Lama

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

OM yang Diketik Baru

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

Pembatasan / pembulatan nilai

Salah satu fitur menarik dari model objek baru adalah pembatasan dan/atau pembulatan otomatis nilai gaya yang dihitung. Sebagai contoh, misalkan Anda mencoba menyetel opacity ke nilai di luar rentang yang dapat diterima, [0, 1]. OM yang diketik mengepaskan nilai ke 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 membulatkan ke 15 sehingga nilainya tetap berupa 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 ditampilkan 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 (misalnya, "calc(56em + 10%)").

Nilai unit

Nilai numerik sederhana ("50%") direpresentasikan oleh objek CSSUnitValue. Meskipun Anda dapat membuat objek ini secara langsung (new CSSUnitValue(10, 'px')) , sebagian besar waktu 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 merepresentasikan ekspresi matematika dan biasanya berisi lebih dari satu nilai/unit. Contoh umumnya adalah membuat ekspresi calc() CSS, tetapi ada 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 menjadi sedikit membingungkan. Berikut 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 paling berguna dari CSS Typed OM adalah Anda dapat melakukan 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 satuan panjang 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

Equality

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). Misalnya, 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. Objek ini memiliki properti boolean untuk membedakan transformasi 2D dan 3D, serta metode .toMatrix() untuk menampilkan representasi DOMMatrix dari 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: membuat animasi 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. Daripada menyentuh DOM atau membaca kembali nilai pada setiap frame (misalnya, tidak ada box.style.transform=`rotate(0,0,1,${newAngle}deg)`), animasi didorong dengan memperbarui objek data CSSTransformValue yang mendasarinya, sehingga meningkatkan performa.

Demo

Di bawah, 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

CSS var() menjadi objek CSSVariableReferenceValue di Typed OM. 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 kustom, ada beberapa langkah 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 diwakili 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 parsing ke platform web. Artinya, Anda akhirnya dapat mengurai nilai CSS secara terprogram, sebelum mencoba menggunakannya. Kemampuan baru ini berpotensi menyelamatkan Anda dari bug awal dan CSS yang salah format.

Mengurai 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)'

Mengurai 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 menerima nilai transform ini:

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

Kesimpulan

Akhirnya kita memiliki model objek yang diupdate untuk CSS. Bekerja dengan string tidak pernah terasa tepat bagi saya. CSS Typed OM API agak panjang, tetapi semoga menghasilkan lebih sedikit bug dan kode yang lebih berperforma di masa mendatang.