Skip to content

Commit

Permalink
feat(runtime): patchClass implementation (#6)
Browse files Browse the repository at this point in the history
* fix: Fix some linter issues

* docs: Add the commit message convention docs

* feat(runtime): patchClass implemented, that allows changing the node CSS class dynamically

* feat(runtime): New getAttribute() member in the NSVElement class

* test(runtime): Tests for the patchClass function

* chore: typo
  • Loading branch information
rigor789 authored Mar 5, 2020
1 parent 356c0c3 commit d89a274
Show file tree
Hide file tree
Showing 6 changed files with 254 additions and 15 deletions.
36 changes: 23 additions & 13 deletions apps/test/app/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const app = createApp({
text: 'Hello World: ' + this.counter,
textAlignment: 'center',
verticalAlignment: 'middle',
class: this.labelClass,
row,
col
}
Expand All @@ -55,26 +56,34 @@ const app = createApp({
]
)
return h('Frame', [
h('Page', [
h(
'GridLayout',
{
rows: '*, *'
},
[
h('ContentView', [content()]),
h('ContentView', { row: 1 }, [content()])
]
)
])
h(
'Page',
{
css: '.red { color: red }, .green { color: green }'
},
[
h(
'GridLayout',
{
rows: '*, *'
},
[
h('ContentView', [content()]),
h('ContentView', { row: 1 }, [content()])
]
)
]
)
])
},
setup() {
const counter = ref(0)
const p = ref(0)
const labelClass = ref('red')
useInterval(() => {
counter.value++
p.value++
labelClass.value = labelClass.value === 'red' ? 'green' : 'red'
if (p.value > 3) {
p.value = 0
}
Expand All @@ -87,7 +96,8 @@ const app = createApp({
})
return {
counter,
p
p,
labelClass
}
}
}).mount()
207 changes: 207 additions & 0 deletions packages/runtime/__tests__/modules/class.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
// https://github.com/vuejs/vue/blob/dev/test/unit/features/directives/class.spec.js

import { h, render, defineComponent } from '../../src'
import { NSVElement } from '../../src/nodes'

type ClassItem = {
value: string | object | string[]
}

function assertClass(assertions: Array<Array<any>>) {
const root = new NSVElement('StackLayout')
const dynamic = { value: '' }
const wrapper = () => h('StackLayout', { class: ['foo', dynamic.value] })

for (const [input, expected] of assertions) {
if (typeof input === 'function') {
input(dynamic.value)
} else {
dynamic.value = input
}

render(wrapper(), root)
const element = <NSVElement>root.firstChild!
expect(element.getAttribute('class')).toBe(expected)
}
}

describe('class', () => {
test('plain string', () => {
assertClass([
['bar', 'foo bar'],
['baz qux', 'foo baz qux'],
['qux', 'foo qux'],
[undefined, 'foo']
])
})

test('object value', () => {
assertClass([
[{ bar: true, baz: false }, 'foo bar'],
[{ baz: true }, 'foo baz'],
[null, 'foo'],
[{ 'bar baz': true, qux: false }, 'foo bar baz'],
[{ qux: true }, 'foo qux']
])
})

test('array value', () => {
assertClass([
[['bar', 'baz'], 'foo bar baz'],
[['qux', 'baz'], 'foo qux baz'],
[['w', 'x y z'], 'foo w x y z'],
[undefined, 'foo'],
[['bar'], 'foo bar'],
[(val: Array<any>) => val.push('baz'), 'foo bar baz']
])
})

test('array of mixed values', () => {
assertClass([
[['x', { y: true, z: true }], 'foo x y z'],
[['x', { y: true, z: false }], 'foo x y'],
[['f', { z: true }], 'foo f z'],
[['l', 'f', { n: true, z: true }], 'foo l f n z'],
[['x', {}], 'foo x'],
[undefined, 'foo']
])
})

test('class merge between parent and child', () => {
const root = new NSVElement('StackLayout')

const childClass: ClassItem = { value: 'd' }
const child = {
props: {},
render: () => h('StackLayout', { class: ['c', childClass.value] })
}

const parentClass: ClassItem = { value: 'b' }
const parent = {
props: {},
render: () => h(child, { class: ['a', parentClass.value] })
}

render(h(parent), root)
expect((<NSVElement>root.firstChild!).getAttribute('class')).toBe('c d a b')

parentClass.value = 'e'
// the `foo` here is just for forcing parent to be updated
// (otherwise it's skipped since its props never change)
render(h(parent, { foo: 1 }), root)
expect((<NSVElement>root.firstChild!).getAttribute('class')).toBe('c d a e')

parentClass.value = 'f'
render(h(parent, { foo: 2 }), root)
expect((<NSVElement>root.firstChild!).getAttribute('class')).toBe('c d a f')

parentClass.value = { foo: true }
childClass.value = ['bar', 'baz']
render(h(parent, { foo: 3 }), root)
expect((<NSVElement>root.firstChild!).getAttribute('class')).toBe(
'c bar baz a foo'
)
})

test('class merge between multiple nested components sharing same element', () => {
const component1 = defineComponent({
props: {},
render() {
return this.$slots.default!()[0]
}
})

const component2 = defineComponent({
props: {},
render() {
return this.$slots.default!()[0]
}
})

const component3 = defineComponent({
props: {},
render() {
return h(
'StackLayout',
{
class: 'staticClass'
},
[this.$slots.default!()]
)
}
})

const root = new NSVElement('StackLayout')
const componentClass1 = { value: 'componentClass1' }
const componentClass2 = { value: 'componentClass2' }
const componentClass3 = { value: 'componentClass3' }

const wrapper = () =>
h(component1, { class: componentClass1.value }, () => [
h(component2, { class: componentClass2.value }, () => [
h(component3, { class: componentClass3.value }, () => ['some text'])
])
])

render(wrapper(), root)
expect((<NSVElement>root.firstChild!).getAttribute('class')).toBe(
'staticClass componentClass3 componentClass2 componentClass1'
)

componentClass1.value = 'c1'
render(wrapper(), root)
expect((<NSVElement>root.firstChild!).getAttribute('class')).toBe(
'staticClass componentClass3 componentClass2 c1'
)

componentClass2.value = 'c2'
render(wrapper(), root)
expect((<NSVElement>root.firstChild!).getAttribute('class')).toBe(
'staticClass componentClass3 c2 c1'
)

componentClass3.value = 'c3'
render(wrapper(), root)
expect((<NSVElement>root.firstChild!).getAttribute('class')).toBe(
'staticClass c3 c2 c1'
)
})

test('deep update', () => {
const root = new NSVElement('StackLayout')
const test = {
a: true,
b: false
}

const wrapper = () => h('StackLayout', { class: test })
render(wrapper(), root)
expect((<NSVElement>root.firstChild!).getAttribute('class')).toBe('a')

test.b = true
render(wrapper(), root)
expect((<NSVElement>root.firstChild!).getAttribute('class')).toBe('a b')
})

// a vdom patch edge case where the user has several un-keyed elements of the
// same tag next to each other, and toggling them.
test('properly remove staticClass for toggling un-keyed children', () => {
const root = new NSVElement('StackLayout')
const ok = { value: true }
const wrapper = () =>
h('StackLayout', [
ok.value ? h('StackLayout', { class: 'a' }) : h('StackLayout')
])

render(wrapper(), root)
expect(
(<NSVElement>root.firstChild!.firstChild!).getAttribute('class')
).toBe('a')

ok.value = false
render(wrapper(), root)
expect(
(<NSVElement>root.firstChild!.firstChild!).getAttribute('class')
).toBe('')
})
})
10 changes: 10 additions & 0 deletions packages/runtime/src/modules/class.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { INSVElement } from '../nodes'

// compiler should normalize class + :class bindings on the same element
// into a single binding ['staticClass', dynamic]
export function patchClass(el: INSVElement, value: string | null) {
if (value == null) {
value = ''
}
el.setAttribute('class', value)
}
6 changes: 6 additions & 0 deletions packages/runtime/src/nodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export interface INSVElement extends INSVNode {

nativeView: (ViewBase | LayoutBase) & { [ELEMENT_REF]: INSVElement }

getAttribute(name: string): string

setAttribute(name: string, value: any): void

insertBefore(el: INSVNode, anchor?: INSVNode | null): void
Expand Down Expand Up @@ -129,6 +131,10 @@ export class NSVElement extends NSVNode implements INSVElement {
this.nativeView.removeEventListener(event, handler)
}

getAttribute(name: string): string {
return this.nativeView[name]
}

setAttribute(name: string, value: any) {
this.nativeView.set(name, value)
}
Expand Down
6 changes: 4 additions & 2 deletions packages/runtime/src/patchProp.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { isAndroid, isIOS } from '@nativescript/core/platform'
import { INSVElement } from './nodes'
import { isOn } from '@vue/shared'
import { patchClass } from './modules/class'
import { patchEvent } from './modules/events'
import { isAndroidKey, isIOSKey } from './runtimeHelpers'

Expand All @@ -23,11 +24,12 @@ export function patchProp(
switch (key) {
// special
case 'class':
// todo
// TODO
console.log('->patchProp+Class')
patchClass(el, nextValue)
break
case 'style':
// todo
// TODO
console.log('->patchProp+Style')
break
case 'modelValue':
Expand Down
4 changes: 4 additions & 0 deletions tests/ns-mocks/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,8 @@ export class NSView {
) {
delete this.eventListener[eventNames]
}

public set(name: string, value: any) {
;(this as any)[name] = value
}
}

0 comments on commit d89a274

Please sign in to comment.