8
0
Просмотр исходного кода

Merge pull request #42 from ckeditor/t/27

Support for binding styles to view model props
Aleksander Nowodzinski 9 лет назад
Родитель
Сommit
b6a97a2589
2 измененных файлов с 215 добавлено и 8 удалено
  1. 92 8
      packages/ckeditor5-ui/src/template.js
  2. 123 0
      packages/ckeditor5-ui/tests/template.js

+ 92 - 8
packages/ckeditor5-ui/src/template.js

@@ -23,7 +23,10 @@ const bindIfSymbol = Symbol( 'bindIf' );
  *		new Template( {
  *			tag: 'p',
  *			attributes: {
- *				class: 'foo'
+ *				class: 'foo',
+ *				style: {
+ *					backgroundColor: 'yellow'
+ *				}
  *			},
  *			children: [
  *				'A paragraph.'
@@ -32,7 +35,7 @@ const bindIfSymbol = Symbol( 'bindIf' );
  *
  * will render the following HTMLElement:
  *
- *		<p class="foo">A paragraph.</p>
+ *		<p class="foo" style="background-color: yellow;">A paragraph.</p>
  *
  * See {@link ui.TemplateDefinition} to know more about templates and see complex examples.
  *
@@ -368,8 +371,8 @@ export default class Template {
 	 */
 	_renderText( valueSchemaOrText, textNode = document.createTextNode( '' ) ) {
 		// Check if this Text Node is bound to Observable. Cases:
-		//		{ text: [ Template.bind.to( ... ) ] }
-		//		{ text: [ 'foo', Template.bind.to( ... ), ... ] }
+		//		{ text: [ Template.bind( ... ).to( ... ) ] }
+		//		{ text: [ 'foo', Template.bind( ... ).to( ... ), ... ] }
 		if ( hasBinding( valueSchemaOrText.text ) ) {
 			this._bindToObservable( valueSchemaOrText.text, textNode, getTextUpdater( textNode ) );
 		}
@@ -403,9 +406,9 @@ export default class Template {
 			attrNs = attrValue[ 0 ].ns || null;
 
 			// Activate binding if one is found. Cases:
-			// 		{ class: [ Template.bind.to( ... ) ] }
-			// 		{ class: [ 'bar', Template.bind.to( ... ), 'baz' ] }
-			// 		{ class: { ns: 'abc', value: Template.bind.to( ... ) } }
+			// 		{ class: [ Template.bind( ... ).to( ... ) ] }
+			// 		{ class: [ 'bar', Template.bind( ... ).to( ... ), 'baz' ] }
+			// 		{ class: { ns: 'abc', value: Template.bind( ... ).to( ... ) } }
 			if ( hasBinding( attrValue ) ) {
 				// Normalize attributes with additional data like namespace:
 				//		{ class: { ns: 'abc', value: [ ... ] } }
@@ -416,7 +419,16 @@ export default class Template {
 				);
 			}
 
-			// Otherwise simply set the attribute.
+			// Style attribute could be an Object so it needs to be parsed in a specific way.
+			//		style: {
+			//			width: '100px',
+			//			height: Template.bind( ... ).to( ... )
+			//		}
+			else if ( attrName == 'style' && typeof attrValue[ 0 ] !== 'string' ) {
+				this._renderStyleAttribute( attrValue[ 0 ], el );
+			}
+
+			// Otherwise simply set the static attribute.
 			// 		{ class: [ 'foo' ] }
 			// 		{ class: [ 'all', 'are', 'static' ] }
 			// 		{ class: [ { ns: 'abc', value: [ 'foo' ] } ] }
@@ -434,6 +446,52 @@ export default class Template {
 		}
 	}
 
+	/**
+	 * Renders `style` attribute.
+	 *
+	 * Style attribute is an {Object} with static values:
+	 *
+	 *		attributes: {
+	 * 			style: {
+	 * 				color: 'red'
+	 * 			}
+	 * 		}
+	 *
+	 * or values bound to {@link ui.Model} properties:
+	 *
+	 *		attributes: {
+	 * 			style: {
+	 * 				color: bind.to( ... )
+	 * 			}
+	 * 		}
+	 *
+	 * Note: `style` attribute is rendered without setting namespace. It does not seem to be
+	 * needed.
+	 *
+	 * @private
+	 * @param {ui.TemplateDefinition.attributes.styles} styles Styles definition.
+	 * @param {HTMLElement} el Element which is rendered.
+	 */
+	_renderStyleAttribute( styles, el ) {
+		for ( let styleName in styles ) {
+			const styleValue = styles[ styleName ];
+
+			// style: {
+			//	color: bind.to( 'attribute' )
+			// }
+			if ( hasBinding( styleValue ) ) {
+				this._bindToObservable( [ styleValue ], el, getStyleUpdater( el, styleName ) );
+			}
+
+			// style: {
+			//	color: 'red'
+			// }
+			else {
+				el.style[ styleName ] = styleValue;
+			}
+		}
+	}
+
 	/**
 	 * Recursively renders element children from definition by
 	 * calling {@link ui.Template#_renderElement}.
@@ -639,6 +697,25 @@ function getAttributeUpdater( el, attrName, ns = null ) {
 	};
 }
 
+// Returns an object consisting of `set` and `remove` functions, which
+// can be used in the context of CSSStyleDeclaration to set or remove a style.
+// @see ui.View#_bindToObservable
+//
+// @param {Node} node DOM Node to be modified.
+// @param {String} styleName Name of the style to be modified.
+// @returns {Object}
+function getStyleUpdater( el, styleName ) {
+	return {
+		set( value ) {
+			el.style[ styleName ] = value;
+		},
+
+		remove() {
+			el.style[ styleName ] = null;
+		}
+	};
+}
+
 // Clones definition of the template.
 //
 // @param {ui.TemplateDefinition} def
@@ -908,6 +985,7 @@ function extendTemplateDefinition( def, extDef ) {
  *			attributes: {
  *				class: {@link ui.TemplateValueSchema},
  *				id: {@link ui.TemplateValueSchema},
+ *				style: {@link ui.TemplateValueSchema}
  *				...
  *			},
  *			on: {
@@ -955,6 +1033,12 @@ function extendTemplateDefinition( def, extDef ) {
  *						bind.if( 'baz', 'value-when-true' )
  *						'static-text'
  *					]
+ *				},
+ *
+ *				// Object literal schema, specific for styles.
+ *				style: {
+ *					color: 'red',
+ *					backgroundColor: bind.to( 'qux', () => { ... } )
  *				}
  *			}
  *		} );

+ 123 - 0
packages/ckeditor5-ui/tests/template.js

@@ -315,6 +315,129 @@ describe( 'Template', () => {
 			observable.foo = 'baz';
 			expect( el.firstChild.textContent ).to.equal( 'baz static' );
 		} );
+
+		describe( 'style attribute', () => {
+			let observable, emitter, bind;
+
+			beforeEach( () => {
+				observable = new Model( {
+					width: '10px',
+					backgroundColor: 'yellow'
+				} );
+
+				emitter = Object.create( EmitterMixin );
+				bind = Template.bind( observable, emitter );
+			} );
+
+			it( 'renders as a static value', () => {
+				setElement( {
+					tag: 'p',
+					attributes: {
+						style: 'color: red'
+					}
+				} );
+
+				expect( el.outerHTML ).to.equal( '<p style="color: red"></p>' );
+			} );
+
+			it( 'renders as a static value (Array of values)', () => {
+				setElement( {
+					tag: 'p',
+					attributes: {
+						style: [ 'color: red;', 'display: block;' ]
+					}
+				} );
+
+				expect( el.outerHTML ).to.equal( '<p style="color: red; display: block;"></p>' );
+			} );
+
+			it( 'renders as a value bound to the model', () => {
+				setElement( {
+					tag: 'p',
+					attributes: {
+						style: bind.to( 'width', w => `width: ${ w }` )
+					}
+				} );
+
+				expect( el.outerHTML ).to.equal( '<p style="width: 10px"></p>' );
+
+				observable.width = '1em';
+
+				expect( el.outerHTML ).to.equal( '<p style="width: 1em"></p>' );
+			} );
+
+			it( 'renders as a value bound to the model (Array of bindings)', () => {
+				setElement( {
+					tag: 'p',
+					attributes: {
+						style: [
+							bind.to( 'width', w => `width: ${ w };` ),
+							bind.to( 'backgroundColor', c => `background-color: ${ c };` )
+						]
+					}
+				} );
+
+				expect( el.outerHTML ).to.equal( '<p style="width: 10px; background-color: yellow;"></p>' );
+
+				observable.width = '1em';
+
+				expect( el.outerHTML ).to.equal( '<p style="width: 1em; background-color: yellow;"></p>' );
+			} );
+
+			describe( 'object', () => {
+				it( 'renders with static and bound attributes', () => {
+					setElement( {
+						tag: 'p',
+						attributes: {
+							style: {
+								width: bind.to( 'width' ),
+								height: '10px',
+								backgroundColor: bind.to( 'backgroundColor' )
+							}
+						}
+					} );
+
+					expect( el.outerHTML ).to.equal( '<p style="width: 10px; height: 10px; background-color: yellow;"></p>' );
+
+					observable.width = '20px';
+					observable.backgroundColor = 'green';
+
+					expect( el.outerHTML ).to.equal( '<p style="width: 20px; height: 10px; background-color: green;"></p>' );
+				} );
+
+				it( 'renders with empty string attributes', () => {
+					setElement( {
+						tag: 'p',
+						attributes: {
+							style: {
+								width: '',
+								backgroundColor: bind.to( 'backgroundColor' )
+							}
+						}
+					} );
+
+					expect( el.outerHTML ).to.be.equal( '<p style="background-color: yellow;"></p>' );
+
+					observable.backgroundColor = '';
+
+					expect( el.outerHTML ).to.be.equal( '<p></p>' );
+				} );
+
+				it( 'renders with falsy values', () => {
+					setElement( {
+						tag: 'p',
+						attributes: {
+							style: {
+								width: null,
+								height: false
+							}
+						}
+					} );
+
+					expect( el.outerHTML ).to.be.equal( '<p></p>' );
+				} );
+			} );
+		} );
 	} );
 
 	describe( 'apply', () => {