Merge pull request #599 from sveltejs/gh-582

Better handling of textareas
pull/606/head
Rich Harris 8 years ago committed by GitHub
commit ecc9a9352c

@ -107,6 +107,20 @@ export default function visitElement ( generator: DomGenerator, block: Block, st
} }
if ( node.name !== 'select' ) { if ( node.name !== 'select' ) {
if ( node.name === 'textarea' ) {
// this is an egregious hack, but it's the easiest way to get <textarea>
// children treated the same way as a value attribute
if ( node.children.length > 0 ) {
node.attributes.push({
type: 'Attribute',
name: 'value',
value: node.children
});
node.children = [];
}
}
// <select> value attributes are an annoying special case — it must be handled // <select> value attributes are an annoying special case — it must be handled
// *after* its children have been updated // *after* its children have been updated
visitAttributesAndAddProps(); visitAttributesAndAddProps();

@ -109,7 +109,7 @@ const lookup = {
title: {}, title: {},
type: { appliesTo: [ 'button', 'input', 'command', 'embed', 'object', 'script', 'source', 'style', 'menu' ] }, type: { appliesTo: [ 'button', 'input', 'command', 'embed', 'object', 'script', 'source', 'style', 'menu' ] },
usemap: { propertyName: 'useMap', appliesTo: [ 'img', 'input', 'object' ] }, usemap: { propertyName: 'useMap', appliesTo: [ 'img', 'input', 'object' ] },
value: { appliesTo: [ 'button', 'option', 'input', 'li', 'meter', 'progress', 'param', 'select' ] }, value: { appliesTo: [ 'button', 'option', 'input', 'li', 'meter', 'progress', 'param', 'select', 'textarea' ] },
width: { appliesTo: [ 'canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video' ] }, width: { appliesTo: [ 'canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video' ] },
wrap: { appliesTo: [ 'textarea' ] } wrap: { appliesTo: [ 'textarea' ] }
}; };

@ -10,6 +10,17 @@ const meta = {
':Window': visitWindow ':Window': visitWindow
}; };
function stringifyAttributeValue ( block: Block, chunks: Node[] ) {
return chunks.map( ( chunk: Node ) => {
if ( chunk.type === 'Text' ) {
return chunk.data;
}
const { snippet } = block.contextualise( chunk.expression );
return '${' + snippet + '}';
}).join( '' )
}
export default function visitElement ( generator: SsrGenerator, block: Block, node: Node ) { export default function visitElement ( generator: SsrGenerator, block: Block, node: Node ) {
if ( node.name in meta ) { if ( node.name in meta ) {
return meta[ node.name ]( generator, block, node ); return meta[ node.name ]( generator, block, node );
@ -21,24 +32,22 @@ export default function visitElement ( generator: SsrGenerator, block: Block, no
} }
let openingTag = `<${node.name}`; let openingTag = `<${node.name}`;
let textareaContents; // awkward special case
node.attributes.forEach( ( attribute: Node ) => { node.attributes.forEach( ( attribute: Node ) => {
if ( attribute.type !== 'Attribute' ) return; if ( attribute.type !== 'Attribute' ) return;
let str = ` ${attribute.name}`; if ( attribute.name === 'value' && node.name === 'textarea' ) {
textareaContents = stringifyAttributeValue( block, attribute.value );
} else {
let str = ` ${attribute.name}`;
if ( attribute.value !== true ) { if ( attribute.value !== true ) {
str += `="` + attribute.value.map( ( chunk: Node ) => { str += `="${stringifyAttributeValue( block, attribute.value )}"`;
if ( chunk.type === 'Text' ) { }
return chunk.data;
}
const { snippet } = block.contextualise( chunk.expression ); openingTag += str;
return '${' + snippet + '}';
}).join( '' ) + `"`;
} }
openingTag += str;
}); });
if ( generator.cssId && !generator.elementDepth ) { if ( generator.cssId && !generator.elementDepth ) {
@ -49,13 +58,17 @@ export default function visitElement ( generator: SsrGenerator, block: Block, no
generator.append( openingTag ); generator.append( openingTag );
generator.elementDepth += 1; if ( node.name === 'textarea' && textareaContents !== undefined ) {
generator.append( textareaContents );
} else {
generator.elementDepth += 1;
node.children.forEach( ( child: Node ) => { node.children.forEach( ( child: Node ) => {
visit( generator, block, child ); visit( generator, block, child );
}); });
generator.elementDepth -= 1; generator.elementDepth -= 1;
}
if ( !isVoidElementName( node.name ) ) { if ( !isVoidElementName( node.name ) ) {
generator.append( `</${node.name}>` ); generator.append( `</${node.name}>` );

@ -9,7 +9,6 @@ import { Parser } from '../index';
import { Node } from '../../interfaces'; import { Node } from '../../interfaces';
const validTagName = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/; const validTagName = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
const invalidUnquotedAttributeCharacters = /[\s"'=<>\/`]/;
const SELF = ':Self'; const SELF = ':Self';
@ -181,6 +180,11 @@ export default function tag ( parser: Parser ) {
if ( selfClosing ) { if ( selfClosing ) {
element.end = parser.index; element.end = parser.index;
} else if ( name === 'textarea' ) {
// special case
element.children = readSequence( parser, () => parser.template.slice( parser.index, parser.index + 11 ) === '</textarea>' );
parser.read( /<\/textarea>/ );
element.end = parser.index;
} else { } else {
// don't push self-closing elements onto the stack // don't push self-closing elements onto the stack
parser.stack.push( element ); parser.stack.push( element );
@ -280,11 +284,41 @@ function readAttribute ( parser: Parser, uniqueNames ) {
} }
function readAttributeValue ( parser: Parser ) { function readAttributeValue ( parser: Parser ) {
let quoteMark; const quoteMark = (
parser.eat( `'` ) ? `'` :
parser.eat( `"` ) ? `"` :
null
);
const regex = (
quoteMark === `'` ? /'/ :
quoteMark === `"` ? /"/ :
/[\s"'=<>\/`]/
);
const value = readSequence( parser, () => regex.test( parser.template[ parser.index ] ) );
if ( quoteMark ) parser.index += 1;
return value;
}
if ( parser.eat( `'` ) ) quoteMark = `'`; function getShorthandValue ( start: number, name: string ) {
if ( parser.eat( `"` ) ) quoteMark = `"`; const end = start + name.length;
return [{
type: 'AttributeShorthand',
start,
end,
expression: {
type: 'Identifier',
start,
end,
name
}
}];
}
function readSequence ( parser: Parser, done: () => boolean ) {
let currentChunk: Node = { let currentChunk: Node = {
start: parser.index, start: parser.index,
end: null, end: null,
@ -292,16 +326,24 @@ function readAttributeValue ( parser: Parser ) {
data: '' data: ''
}; };
const done = quoteMark ?
char => char === quoteMark :
char => invalidUnquotedAttributeCharacters.test( char );
const chunks = []; const chunks = [];
while ( parser.index < parser.template.length ) { while ( parser.index < parser.template.length ) {
const index = parser.index; const index = parser.index;
if ( parser.eat( '{{' ) ) { if ( done() ) {
currentChunk.end = parser.index;
if ( currentChunk.data ) chunks.push( currentChunk );
chunks.forEach( chunk => {
if ( chunk.type === 'Text' ) chunk.data = decodeCharacterReferences( chunk.data );
});
return chunks;
}
else if ( parser.eat( '{{' ) ) {
if ( currentChunk.data ) { if ( currentChunk.data ) {
currentChunk.end = index; currentChunk.end = index;
chunks.push( currentChunk ); chunks.push( currentChunk );
@ -328,39 +370,10 @@ function readAttributeValue ( parser: Parser ) {
}; };
} }
else if ( done( parser.template[ parser.index ] ) ) {
currentChunk.end = parser.index;
if ( quoteMark ) parser.index += 1;
if ( currentChunk.data ) chunks.push( currentChunk );
chunks.forEach( chunk => {
if ( chunk.type === 'Text' ) chunk.data = decodeCharacterReferences( chunk.data );
});
return chunks;
}
else { else {
currentChunk.data += parser.template[ parser.index++ ]; currentChunk.data += parser.template[ parser.index++ ];
} }
} }
parser.error( `Unexpected end of input` ); parser.error( `Unexpected end of input` );
} }
function getShorthandValue ( start: number, name: string ) {
const end = start + name.length;
return [{
type: 'AttributeShorthand',
start,
end,
expression: {
type: 'Identifier',
start,
end,
name
}
}];
}

@ -77,6 +77,14 @@ export default function validateElement ( validator: Validator, node: Node ) {
validator.error( `Missing transition '${attribute.name}'`, attribute.start ); validator.error( `Missing transition '${attribute.name}'`, attribute.start );
} }
} }
else if ( attribute.type === 'Attribute' ) {
if ( attribute.name === 'value' && node.name === 'textarea' ) {
if ( node.children.length ) {
validator.error( `A <textarea> can have either a value attribute or (equivalently) child content, but not both`, attribute.start );
}
}
}
}); });
} }

@ -0,0 +1,3 @@
<textarea>
<p>not actually an element. {{foo}}</p>
</textarea>

@ -0,0 +1,44 @@
{
"hash": 3618147195,
"html": {
"start": 0,
"end": 63,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 63,
"type": "Element",
"name": "textarea",
"attributes": [],
"children": [
{
"start": 10,
"end": 40,
"type": "Text",
"data": "\n\t<p>not actually an element. "
},
{
"start": 40,
"end": 47,
"type": "MustacheTag",
"expression": {
"type": "Identifier",
"start": 42,
"end": 45,
"name": "foo"
}
},
{
"start": 47,
"end": 52,
"type": "Text",
"data": "</p>\n"
}
]
}
]
},
"css": null,
"js": null
}

@ -0,0 +1,17 @@
export default {
'skip-ssr': true, // SSR behaviour is awkwardly different
data: {
foo: 42
},
html: `<textarea></textarea>`,
test ( assert, component, target ) {
const textarea = target.querySelector( 'textarea' );
assert.strictEqual( textarea.value, `\n\t<p>not actually an element. 42</p>\n` );
component.set({ foo: 43 });
assert.strictEqual( textarea.value, `\n\t<p>not actually an element. 43</p>\n` );
}
};

@ -0,0 +1,3 @@
<textarea>
<p>not actually an element. {{foo}}</p>
</textarea>

@ -0,0 +1,17 @@
export default {
'skip-ssr': true, // SSR behaviour is awkwardly different
data: {
foo: 42
},
html: `<textarea></textarea>`,
test ( assert, component, target ) {
const textarea = target.querySelector( 'textarea' );
assert.strictEqual( textarea.value, '42' );
component.set({ foo: 43 });
assert.strictEqual( textarea.value, '43' );
}
};

@ -0,0 +1 @@
<textarea value='{{foo}}'/>

@ -0,0 +1,3 @@
<textarea>
<p>not actually an element. 42</p>
</textarea>

@ -0,0 +1,11 @@
<textarea>
<p>not actually an element. {{foo}}</p>
</textarea>
<script>
export default {
data () {
return { foo: 42 };
}
};
</script>

@ -0,0 +1,9 @@
<textarea value='{{foo}}'/>
<script>
export default {
data () {
return { foo: 42 };
}
};
</script>

@ -0,0 +1,8 @@
[{
"message": "A <textarea> can have either a value attribute or (equivalently) child content, but not both",
"loc": {
"line": 1,
"column": 10
},
"pos": 10
}]

@ -0,0 +1,3 @@
<textarea value='{{foo}}'>
some illegal text
</textarea>
Loading…
Cancel
Save