From 833ec62f1a8a16f0affa86339199345ddee80e35 Mon Sep 17 00:00:00 2001 From: Dmitryi Kunets <1mickeyfrost1@gmail.com> Date: Sun, 24 May 2020 17:42:44 +0300 Subject: [PATCH] add 'attrs' support --- src/constructors/styled.js | 14 ++- src/models/StyledComponent.js | 37 +++++++- src/test/attrs.test.js | 168 ++++++++++++++++++++++++++++++++++ 3 files changed, 212 insertions(+), 7 deletions(-) create mode 100644 src/test/attrs.test.js diff --git a/src/constructors/styled.js b/src/constructors/styled.js index e00a935..6802a69 100644 --- a/src/constructors/styled.js +++ b/src/constructors/styled.js @@ -3,13 +3,21 @@ import domElements from '../utils/domElements' import isValidElementType from '../utils/isValidElementType' export default (createStyledComponent) => { - const styled = (tagName, props = {}) => { + const styled = (tagName, props = {}, options = {}) => { if (!isValidElementType(tagName)) { throw new Error(tagName + ' is not allowed for styled tag type.') } - return (cssRules, ...interpolations) => ( - createStyledComponent(tagName, css(cssRules, ...interpolations), props) + + const templateFunction = (cssRules, ...interpolations) => ( + createStyledComponent(tagName, css(cssRules, ...interpolations), props, options) ) + + templateFunction.attrs = attrs => styled(tagName, props, { + ...options, + attrs: Array.prototype.concat(options.attrs, attrs).filter(Boolean) + }) + + return templateFunction } domElements.forEach((domElement) => { diff --git a/src/models/StyledComponent.js b/src/models/StyledComponent.js index a7d28a2..e7dd13c 100644 --- a/src/models/StyledComponent.js +++ b/src/models/StyledComponent.js @@ -3,7 +3,10 @@ import normalizeProps from '../utils/normalizeProps' import isVueComponent from '../utils/isVueComponent' export default (ComponentStyle) => { - const createStyledComponent = (target, rules, props) => { + const createStyledComponent = (target, rules, props, options) => { + const { + attrs = [] + } = options const componentStyle = new ComponentStyle(rules) // handle array-declaration props @@ -46,6 +49,7 @@ export default (ComponentStyle) => { class: [this.generatedClassName], props: this.$props, domProps: { + ...this.attrs, value: this.localValue }, on: { @@ -68,11 +72,36 @@ export default (ComponentStyle) => { }, computed: { generatedClassName () { - const componentProps = { theme: this.theme, ...this.$props } + const { context, attrs } = this + const componentProps = { ...context, ...attrs } return this.generateAndInjectStyles(componentProps) }, theme () { return this.$theme() + }, + context () { + return { + theme: this.theme, + ...this.$props + } + }, + attrs () { + const resolvedAttrs = {} + const { context } = this + + attrs.forEach((attrDef) => { + let resolvedAttrDef = attrDef + + if (typeof resolvedAttrDef === 'function') { + resolvedAttrDef = resolvedAttrDef(context) + } + + for (const key in resolvedAttrDef) { + context[key] = resolvedAttrs[key] = resolvedAttrDef[key] + } + }) + + return resolvedAttrs } }, watch: { @@ -85,10 +114,10 @@ export default (ComponentStyle) => { }, extend (cssRules, ...interpolations) { const extendedRules = css(cssRules, ...interpolations) - return createStyledComponent(target, rules.concat(extendedRules), props) + return createStyledComponent(target, rules.concat(extendedRules), props, options) }, withComponent (newTarget) { - return createStyledComponent(newTarget, rules, props) + return createStyledComponent(newTarget, rules, props, options) } } diff --git a/src/test/attrs.test.js b/src/test/attrs.test.js new file mode 100644 index 0000000..c26d524 --- /dev/null +++ b/src/test/attrs.test.js @@ -0,0 +1,168 @@ +import Vue from 'vue' +import expect from 'expect' + +import { resetStyled, expectCSSMatches } from './utils' + +let styled + +describe('"attrs" feature', () => { + beforeEach(() => { + styled = resetStyled() + }) + + it('should add html attributes to an element', () => { + const Component = styled('img', {}).attrs({ src: 'image.jpg' })` + width: 50; + ` + const vm = new Vue(Component).$mount() + expect(vm._vnode.data.domProps).toEqual({ src: 'image.jpg' }) + }) + + it('should add several html attributes to an element', () => { + const Component = styled('img', {}).attrs({ src: 'image.jpg', alt: 'Test image' })` + width: 50; + ` + const vm = new Vue(Component).$mount() + expect(vm._vnode.data.domProps).toEqual({ src: 'image.jpg', alt: 'Test image' }) + }) + + it('should work as expected with empty attributes object provided', () => { + const Component = styled('img', {}).attrs({})` + width: 50; + ` + const vm = new Vue(Component).$mount() + expectCSSMatches('.a {width: 50;}') + }) + + it('should work as expected with null attributes object provided', () => { + const Component = styled('img', {}).attrs(null)` + width: 50; + ` + const vm = new Vue(Component).$mount() + expectCSSMatches('.a {width: 50;}') + expect(vm._vnode.data.domProps).toEqual({}) + }) + + it('should work as expected without attributes provided', () => { + const Component = styled('img')` + width: 50; + ` + const vm = new Vue(Component).$mount() + expectCSSMatches('.a {width: 50;}') + expect(vm._vnode.data.domProps).toEqual({}) + }) + + it('should work with a function as a parameter of of the method', () => { + const Component = styled('img', {}).attrs(() => ({ + src: 'image.jpg', + alt: 'Test image', + height: '50' + }))` + width: 50; + ` + const vm = new Vue(Component).$mount() + expect(vm._vnode.data.domProps).toEqual({ src: 'image.jpg', alt: 'Test image', height: '50' }) + }) + + it('should work with multiple attrs method call', () => { + const Component = styled('img', {}) + .attrs(() => ({ + src: 'image.jpg', + alt: 'Test image' + })) + .attrs({ + height: '50' + })` + width: 50; + ` + const vm = new Vue(Component).$mount() + expect(vm._vnode.data.domProps).toEqual({ src: 'image.jpg', alt: 'Test image', height: '50' }) + }) + + it('should access to all previous attribute properties', () => { + const Component = styled('img', {}) + .attrs(() => ({ + src: 'image', + alt: 'My test image' + })) + .attrs((props) => ({ + src: props.src + '.jpg', + height: 5 * 10 + }))` + width: 50; + ` + const vm = new Vue(Component).$mount() + expect(vm._vnode.data.domProps).toEqual({ src: 'image.jpg', alt: 'My test image', height: 50 }) + }) + + it('should override attribute properties', () => { + const Component = styled('img', {}) + .attrs(() => ({ + src: 'image.jpg', + alt: 'Test image', + height: '20' + })) + .attrs({ + height: '50' + })` + width: 50; + ` + const vm = new Vue(Component).$mount() + expect(vm._vnode.data.domProps).toEqual({ src: 'image.jpg', alt: 'Test image', height: '50' }) + }) + + it('should access to component props', () => { + const Component = styled('img', { propsHeight: Number }) + .attrs((props) => ({ + src: 'image.jpg', + alt: 'Test image', + height: props.propsHeight * 2 + }))` + width: 50; + ` + + const vm = new Vue({ + render: function (h) { + return h(Component, { + props: { + propsHeight: 20 + }, + }) + } + }).$mount() + + expect(vm.$children[0]._vnode.data.domProps).toEqual({ src: 'image.jpg', alt: 'Test image', height: 40 }) + }) + + it('attributes should be reactive', () => { + const Component = styled('img', { propsHeight: Number }) + .attrs((props) => ({ + src: 'image.jpg', + alt: 'Test image', + height: props.propsHeight * 2 + }))` + width: 50; + ` + + const vm = new Vue({ + render: function (h) { + const self = this + return h(Component, { + props: { + propsHeight: self.dataHeight + }, + }) + }, + data: () => ({ + dataHeight: 20 + }) + }).$mount() + + expect(vm.$children[0]._vnode.data.domProps).toEqual({ src: 'image.jpg', alt: 'Test image', height: 40 }) + + vm.dataHeight = 90 + setTimeout(() => { // $nextTick + expect(vm.$children[0]._vnode.data.domProps).toEqual({ src: 'image.jpg', alt: 'Test image', height: 180 }) + }, 0) + }) +})