Skip to content

Commit

Permalink
feat(props): add merge, mergeUnless and mergeIf methods
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Aug 19, 2023
1 parent 844d2da commit 16786a6
Show file tree
Hide file tree
Showing 3 changed files with 317 additions and 92 deletions.
38 changes: 34 additions & 4 deletions src/component/props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,50 @@ export class ComponentProps {
}

/**
* Define defaults for the props values.
* Merge defaults with the props
*
* - All other attributes will be overwritten when defined in props
* - Classes will be merged together.
*/
defaults(values: Record<string, any>) {
merge(values: Record<string, any>) {
if (values.class && this.#values['class']) {
const classes = { ...values.class, ...this.#values.class }
return new ComponentProps({ ...values, ...this.#values, class: classes })
const classesSet: Set<any> = new Set()
;(Array.isArray(values.class) ? values.class : [values]).forEach((item) => {
classesSet.add(item)
})
;(Array.isArray(this.#values['class'])
? this.#values['class']
: [this.#values['class']]
).forEach((item) => {
classesSet.add(item)
})

return new ComponentProps({ ...values, ...this.#values, class: Array.from(classesSet) })
}

return new ComponentProps({ ...values, ...this.#values })
}

/**
* Merge defaults with the props, if the given condition is truthy
*/
mergeIf(conditional: any, values: Record<string, any>) {
if (conditional) {
return this.merge(values)
}
return this
}

/**
* Merge defaults with the props, if the given condition is falsy
*/
mergeUnless(conditional: any, values: Record<string, any>) {
if (!conditional) {
return this.merge(values)
}
return this
}

/**
* Converts props to HTML attributes
*/
Expand Down
189 changes: 101 additions & 88 deletions tests/props.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,173 +8,186 @@
*/

import { test } from '@japa/runner'
import { Props } from '../src/migrate/props.js'
import { ComponentProps } from '../src/component/props.js'

test.group('Props', () => {
test.group('ComponentProps', () => {
test('get all props', ({ assert }) => {
const props = new Props({ title: 'Hello' })
const props = new ComponentProps({ title: 'Hello' })
assert.deepEqual(props.all(), { title: 'Hello' })
})

test('get value for a given key', ({ assert }) => {
const props = new Props({ title: 'Hello' })
const props = new ComponentProps({ title: 'Hello' })
assert.equal(props.get('title'), 'Hello')
})

test('cherry pick values from the props', ({ assert }) => {
const props = new Props({ title: 'Hello', label: 'Hello world', actionText: 'Confirm' })
const props = new ComponentProps({
title: 'Hello',
label: 'Hello world',
actionText: 'Confirm',
})

assert.deepEqual(props.only(['label', 'actionText']), {
assert.deepEqual(props.only(['label', 'actionText']).all(), {
label: 'Hello world',
actionText: 'Confirm',
})
})

test('get values except for the defined keys from the props', ({ assert }) => {
const props = new Props({ title: 'Hello', label: 'Hello world', actionText: 'Confirm' })
const props = new ComponentProps({
title: 'Hello',
label: 'Hello world',
actionText: 'Confirm',
})

assert.deepEqual(props.except(['label', 'actionText']), {
assert.deepEqual(props.except(['label', 'actionText']).all(), {
title: 'Hello',
})
})

test('serialize props to html attributes', ({ assert }) => {
const props = new Props({
const props = new ComponentProps({
class: ['foo', 'bar'],
onclick: 'foo = bar',
})
assert.equal(props.serialize().value, ' class="foo bar" onclick="foo = bar"')
assert.equal(props.toAttrs().value, 'class="foo bar" onclick="foo = bar"')
})

test('serialize by merging custom properties', ({ assert }) => {
const props = new Props({
const props = new ComponentProps({
class: ['foo', 'bar'],
onclick: 'foo = bar',
})
assert.equal(props.serialize({ id: '1' }).value, ' class="foo bar" onclick="foo = bar" id="1"')
assert.equal(
props.merge({ id: '1' }).toAttrs().value,
'id="1" class="foo bar" onclick="foo = bar"'
)
})

test('serialize specific keys to html attributes', ({ assert }) => {
const props = new Props({
const props = new ComponentProps({
class: ['foo', 'bar'],
onclick: 'foo = bar',
})
assert.equal(props.serializeOnly(['class']).value, ' class="foo bar"')
assert.equal(props.only(['class']).toAttrs().value, 'class="foo bar"')
})

test('serialize specific keys to merge custom properties', ({ assert }) => {
const props = new Props({
test('serialize specific keys and merge custom properties', ({ assert }) => {
const props = new ComponentProps({
class: ['foo', 'bar'],
onclick: 'foo = bar',
})
assert.equal(props.serializeOnly(['class'], { id: '1' }).value, ' class="foo bar" id="1"')
assert.equal(props.only(['class']).merge({ id: '1' }).toAttrs().value, 'id="1" class="foo bar"')
})

test('serialize all except defined keys to html attributes', ({ assert }) => {
const props = new Props({
const props = new ComponentProps({
class: ['foo', 'bar'],
onclick: 'foo = bar',
})

assert.equal(props.serializeExcept(['class']).value, ' onclick="foo = bar"')
assert.equal(props.except(['class']).toAttrs().value, 'onclick="foo = bar"')
})

test('serialize specific keys to merge custom properties', ({ assert }) => {
const props = new Props({
test('serialize specific keys and merge custom properties', ({ assert }) => {
const props = new ComponentProps({
class: ['foo', 'bar'],
onclick: 'foo = bar',
})

assert.equal(props.serializeExcept(['class'], { id: '1' }).value, ' onclick="foo = bar" id="1"')
assert.equal(
props.except(['class']).merge({ id: '1' }).toAttrs().value,
'id="1" onclick="foo = bar"'
)
})

test('copy state properties to the props class', ({ assert }) => {
const props = new Props({
test('merge default and user supplied classes', ({ assert }) => {
const props = new ComponentProps({
class: ['foo', 'bar'],
onclick: 'foo = bar',
})

// @ts-expect-error
assert.deepEqual(props['class'], ['foo', 'bar'])
// @ts-expect-error
assert.deepEqual(props['onclick'], 'foo = bar')
})

test('access nested state properties from the props instance', ({ assert }) => {
const props = new Props({
user: {
name: 'virk',
},
})

// @ts-expect-error
assert.equal(props['user']['name'], 'virk')
})

test('do not raise error when state is undefined', () => {
new Props(undefined)
})

test('do not raise error when state is null', () => {
new Props(null)
assert.equal(
props
.except(['onclick'])
.merge({ class: ['foo', 'baz'] })
.toAttrs().value,
'class="foo baz bar"'
)
})

test('give preference to inline merge object', ({ assert }) => {
const props = new Props({
class: ['foo', 'bar'],
test('merge default and user supplied classes as object', ({ assert }) => {
const props = new ComponentProps({
class: [
'foo',
{
'input-error': false,
'input-disabled': true,
'input-large': false,
'input-medium': true,
'input-rounded': true,
},
],
onclick: 'foo = bar',
})

assert.equal(
props.serializeExcept(['onclick'], { class: ['foo', 'baz'] }).value,
' class="foo baz"'
props
.except(['onclick'])
.merge({ class: ['foo', 'input-error'] })
.toAttrs().value,
'class="foo input-error input-disabled input-medium input-rounded"'
)
})

test('give preference to user props', ({ assert }) => {
const props = new Props({
class: ['foo', 'bar'],
test('mergeUnless a conditional is true', ({ assert }) => {
const props = new ComponentProps({
class: [
'foo',
{
'input-error': false,
'input-disabled': true,
'input-large': false,
'input-medium': true,
'input-rounded': true,
},
],
ignoreExistingClasses: true,
onclick: 'foo = bar',
})

assert.equal(
props.serializeExcept(['onclick'], { class: ['foo', 'baz'] }, false).value,
' class="foo bar"'
props
.except(['onclick', 'ignoreExistingClasses'])
.mergeUnless(props.get('ignoreExistingClasses'), { class: ['foo', 'input-error'] })
.toAttrs().value,
'class="foo input-disabled input-medium input-rounded"'
)
})

test('merge class and className props', ({ assert }) => {
const props = new Props({
class: ['foo', 'bar'],
className: ['baz'],
})

assert.equal(props.serialize().value, ' class="foo bar baz"')
})

test('use classname when no class is defined', ({ assert }) => {
const props = new Props({
className: ['baz'],
})

assert.equal(props.serialize().value, ' class="baz"')
})

test('merge class and className when class is defined as a string', ({ assert }) => {
const props = new Props({
class: 'foo bar',
className: ['baz'],
})

assert.equal(props.serialize().value, ' class="foo bar baz"')
})

test('merge class and className when className is defined as a string', ({ assert }) => {
const props = new Props({
class: 'foo bar',
className: 'baz boom',
test('mergeIf a conditional is true', ({ assert }) => {
const props = new ComponentProps({
class: [
'foo',
{
'input-error': false,
'input-disabled': true,
'input-large': false,
'input-medium': true,
'input-rounded': true,
},
],
applyDefaults: true,
onclick: 'foo = bar',
})

assert.equal(props.serialize().value, ' class="foo bar baz boom"')
assert.equal(
props
.except(['onclick', 'applyDefaults'])
.mergeIf(props.get('applyDefaults'), { class: ['foo', 'input-error'] })
.toAttrs().value,
'class="foo input-error input-disabled input-medium input-rounded"'
)
})
})
Loading

0 comments on commit 16786a6

Please sign in to comment.