Skip to content

Commit ecc9a93

Browse files
authored
Merge pull request #599 from sveltejs/gh-582
Better handling of textareas
2 parents 3e30b75 + b828fdf commit ecc9a93

File tree

17 files changed

+224
-56
lines changed

17 files changed

+224
-56
lines changed

src/generators/dom/visitors/Element/Element.ts

+14
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,20 @@ export default function visitElement ( generator: DomGenerator, block: Block, st
107107
}
108108

109109
if ( node.name !== 'select' ) {
110+
if ( node.name === 'textarea' ) {
111+
// this is an egregious hack, but it's the easiest way to get <textarea>
112+
// children treated the same way as a value attribute
113+
if ( node.children.length > 0 ) {
114+
node.attributes.push({
115+
type: 'Attribute',
116+
name: 'value',
117+
value: node.children
118+
});
119+
120+
node.children = [];
121+
}
122+
}
123+
110124
// <select> value attributes are an annoying special case — it must be handled
111125
// *after* its children have been updated
112126
visitAttributesAndAddProps();

src/generators/dom/visitors/Element/lookup.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ const lookup = {
109109
title: {},
110110
type: { appliesTo: [ 'button', 'input', 'command', 'embed', 'object', 'script', 'source', 'style', 'menu' ] },
111111
usemap: { propertyName: 'useMap', appliesTo: [ 'img', 'input', 'object' ] },
112-
value: { appliesTo: [ 'button', 'option', 'input', 'li', 'meter', 'progress', 'param', 'select' ] },
112+
value: { appliesTo: [ 'button', 'option', 'input', 'li', 'meter', 'progress', 'param', 'select', 'textarea' ] },
113113
width: { appliesTo: [ 'canvas', 'embed', 'iframe', 'img', 'input', 'object', 'video' ] },
114114
wrap: { appliesTo: [ 'textarea' ] }
115115
};

src/generators/server-side-rendering/visitors/Element.ts

+29-16
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@ const meta = {
1010
':Window': visitWindow
1111
};
1212

13+
function stringifyAttributeValue ( block: Block, chunks: Node[] ) {
14+
return chunks.map( ( chunk: Node ) => {
15+
if ( chunk.type === 'Text' ) {
16+
return chunk.data;
17+
}
18+
19+
const { snippet } = block.contextualise( chunk.expression );
20+
return '${' + snippet + '}';
21+
}).join( '' )
22+
}
23+
1324
export default function visitElement ( generator: SsrGenerator, block: Block, node: Node ) {
1425
if ( node.name in meta ) {
1526
return meta[ node.name ]( generator, block, node );
@@ -21,24 +32,22 @@ export default function visitElement ( generator: SsrGenerator, block: Block, no
2132
}
2233

2334
let openingTag = `<${node.name}`;
35+
let textareaContents; // awkward special case
2436

2537
node.attributes.forEach( ( attribute: Node ) => {
2638
if ( attribute.type !== 'Attribute' ) return;
2739

28-
let str = ` ${attribute.name}`;
40+
if ( attribute.name === 'value' && node.name === 'textarea' ) {
41+
textareaContents = stringifyAttributeValue( block, attribute.value );
42+
} else {
43+
let str = ` ${attribute.name}`;
2944

30-
if ( attribute.value !== true ) {
31-
str += `="` + attribute.value.map( ( chunk: Node ) => {
32-
if ( chunk.type === 'Text' ) {
33-
return chunk.data;
34-
}
45+
if ( attribute.value !== true ) {
46+
str += `="${stringifyAttributeValue( block, attribute.value )}"`;
47+
}
3548

36-
const { snippet } = block.contextualise( chunk.expression );
37-
return '${' + snippet + '}';
38-
}).join( '' ) + `"`;
49+
openingTag += str;
3950
}
40-
41-
openingTag += str;
4251
});
4352

4453
if ( generator.cssId && !generator.elementDepth ) {
@@ -49,13 +58,17 @@ export default function visitElement ( generator: SsrGenerator, block: Block, no
4958

5059
generator.append( openingTag );
5160

52-
generator.elementDepth += 1;
61+
if ( node.name === 'textarea' && textareaContents !== undefined ) {
62+
generator.append( textareaContents );
63+
} else {
64+
generator.elementDepth += 1;
5365

54-
node.children.forEach( ( child: Node ) => {
55-
visit( generator, block, child );
56-
});
66+
node.children.forEach( ( child: Node ) => {
67+
visit( generator, block, child );
68+
});
5769

58-
generator.elementDepth -= 1;
70+
generator.elementDepth -= 1;
71+
}
5972

6073
if ( !isVoidElementName( node.name ) ) {
6174
generator.append( `</${node.name}>` );

src/parse/state/tag.ts

+52-39
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import { Parser } from '../index';
99
import { Node } from '../../interfaces';
1010

1111
const validTagName = /^\!?[a-zA-Z]{1,}:?[a-zA-Z0-9\-]*/;
12-
const invalidUnquotedAttributeCharacters = /[\s"'=<>\/`]/;
1312

1413
const SELF = ':Self';
1514

@@ -181,6 +180,11 @@ export default function tag ( parser: Parser ) {
181180

182181
if ( selfClosing ) {
183182
element.end = parser.index;
183+
} else if ( name === 'textarea' ) {
184+
// special case
185+
element.children = readSequence( parser, () => parser.template.slice( parser.index, parser.index + 11 ) === '</textarea>' );
186+
parser.read( /<\/textarea>/ );
187+
element.end = parser.index;
184188
} else {
185189
// don't push self-closing elements onto the stack
186190
parser.stack.push( element );
@@ -280,28 +284,66 @@ function readAttribute ( parser: Parser, uniqueNames ) {
280284
}
281285

282286
function readAttributeValue ( parser: Parser ) {
283-
let quoteMark;
287+
const quoteMark = (
288+
parser.eat( `'` ) ? `'` :
289+
parser.eat( `"` ) ? `"` :
290+
null
291+
);
292+
293+
const regex = (
294+
quoteMark === `'` ? /'/ :
295+
quoteMark === `"` ? /"/ :
296+
/[\s"'=<>\/`]/
297+
);
298+
299+
const value = readSequence( parser, () => regex.test( parser.template[ parser.index ] ) );
300+
301+
if ( quoteMark ) parser.index += 1;
302+
return value;
303+
}
284304

285-
if ( parser.eat( `'` ) ) quoteMark = `'`;
286-
if ( parser.eat( `"` ) ) quoteMark = `"`;
305+
function getShorthandValue ( start: number, name: string ) {
306+
const end = start + name.length;
287307

308+
return [{
309+
type: 'AttributeShorthand',
310+
start,
311+
end,
312+
expression: {
313+
type: 'Identifier',
314+
start,
315+
end,
316+
name
317+
}
318+
}];
319+
}
320+
321+
function readSequence ( parser: Parser, done: () => boolean ) {
288322
let currentChunk: Node = {
289323
start: parser.index,
290324
end: null,
291325
type: 'Text',
292326
data: ''
293327
};
294328

295-
const done = quoteMark ?
296-
char => char === quoteMark :
297-
char => invalidUnquotedAttributeCharacters.test( char );
298-
299329
const chunks = [];
300330

301331
while ( parser.index < parser.template.length ) {
302332
const index = parser.index;
303333

304-
if ( parser.eat( '{{' ) ) {
334+
if ( done() ) {
335+
currentChunk.end = parser.index;
336+
337+
if ( currentChunk.data ) chunks.push( currentChunk );
338+
339+
chunks.forEach( chunk => {
340+
if ( chunk.type === 'Text' ) chunk.data = decodeCharacterReferences( chunk.data );
341+
});
342+
343+
return chunks;
344+
}
345+
346+
else if ( parser.eat( '{{' ) ) {
305347
if ( currentChunk.data ) {
306348
currentChunk.end = index;
307349
chunks.push( currentChunk );
@@ -328,39 +370,10 @@ function readAttributeValue ( parser: Parser ) {
328370
};
329371
}
330372

331-
else if ( done( parser.template[ parser.index ] ) ) {
332-
currentChunk.end = parser.index;
333-
if ( quoteMark ) parser.index += 1;
334-
335-
if ( currentChunk.data ) chunks.push( currentChunk );
336-
337-
chunks.forEach( chunk => {
338-
if ( chunk.type === 'Text' ) chunk.data = decodeCharacterReferences( chunk.data );
339-
});
340-
341-
return chunks;
342-
}
343-
344373
else {
345374
currentChunk.data += parser.template[ parser.index++ ];
346375
}
347376
}
348377

349378
parser.error( `Unexpected end of input` );
350-
}
351-
352-
function getShorthandValue ( start: number, name: string ) {
353-
const end = start + name.length;
354-
355-
return [{
356-
type: 'AttributeShorthand',
357-
start,
358-
end,
359-
expression: {
360-
type: 'Identifier',
361-
start,
362-
end,
363-
name
364-
}
365-
}];
366-
}
379+
}

src/validate/html/validateElement.ts

+8
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,14 @@ export default function validateElement ( validator: Validator, node: Node ) {
7777
validator.error( `Missing transition '${attribute.name}'`, attribute.start );
7878
}
7979
}
80+
81+
else if ( attribute.type === 'Attribute' ) {
82+
if ( attribute.name === 'value' && node.name === 'textarea' ) {
83+
if ( node.children.length ) {
84+
validator.error( `A <textarea> can have either a value attribute or (equivalently) child content, but not both`, attribute.start );
85+
}
86+
}
87+
}
8088
});
8189
}
8290

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<textarea>
2+
<p>not actually an element. {{foo}}</p>
3+
</textarea>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"hash": 3618147195,
3+
"html": {
4+
"start": 0,
5+
"end": 63,
6+
"type": "Fragment",
7+
"children": [
8+
{
9+
"start": 0,
10+
"end": 63,
11+
"type": "Element",
12+
"name": "textarea",
13+
"attributes": [],
14+
"children": [
15+
{
16+
"start": 10,
17+
"end": 40,
18+
"type": "Text",
19+
"data": "\n\t<p>not actually an element. "
20+
},
21+
{
22+
"start": 40,
23+
"end": 47,
24+
"type": "MustacheTag",
25+
"expression": {
26+
"type": "Identifier",
27+
"start": 42,
28+
"end": 45,
29+
"name": "foo"
30+
}
31+
},
32+
{
33+
"start": 47,
34+
"end": 52,
35+
"type": "Text",
36+
"data": "</p>\n"
37+
}
38+
]
39+
}
40+
]
41+
},
42+
"css": null,
43+
"js": null
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export default {
2+
'skip-ssr': true, // SSR behaviour is awkwardly different
3+
4+
data: {
5+
foo: 42
6+
},
7+
8+
html: `<textarea></textarea>`,
9+
10+
test ( assert, component, target ) {
11+
const textarea = target.querySelector( 'textarea' );
12+
assert.strictEqual( textarea.value, `\n\t<p>not actually an element. 42</p>\n` );
13+
14+
component.set({ foo: 43 });
15+
assert.strictEqual( textarea.value, `\n\t<p>not actually an element. 43</p>\n` );
16+
}
17+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<textarea>
2+
<p>not actually an element. {{foo}}</p>
3+
</textarea>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export default {
2+
'skip-ssr': true, // SSR behaviour is awkwardly different
3+
4+
data: {
5+
foo: 42
6+
},
7+
8+
html: `<textarea></textarea>`,
9+
10+
test ( assert, component, target ) {
11+
const textarea = target.querySelector( 'textarea' );
12+
assert.strictEqual( textarea.value, '42' );
13+
14+
component.set({ foo: 43 });
15+
assert.strictEqual( textarea.value, '43' );
16+
}
17+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<textarea value='{{foo}}'/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<textarea>
2+
<p>not actually an element. 42</p>
3+
</textarea>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<textarea>
2+
<p>not actually an element. {{foo}}</p>
3+
</textarea>
4+
5+
<script>
6+
export default {
7+
data () {
8+
return { foo: 42 };
9+
}
10+
};
11+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<textarea>42</textarea>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<textarea value='{{foo}}'/>
2+
3+
<script>
4+
export default {
5+
data () {
6+
return { foo: 42 };
7+
}
8+
};
9+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[{
2+
"message": "A <textarea> can have either a value attribute or (equivalently) child content, but not both",
3+
"loc": {
4+
"line": 1,
5+
"column": 10
6+
},
7+
"pos": 10
8+
}]

0 commit comments

Comments
 (0)