Skip to content

Commit

Permalink
Merge pull request #599 from sveltejs/gh-582
Browse files Browse the repository at this point in the history
Better handling of textareas
  • Loading branch information
Rich-Harris authored May 28, 2017
2 parents 3e30b75 + b828fdf commit ecc9a93
Show file tree
Hide file tree
Showing 17 changed files with 224 additions and 56 deletions.
14 changes: 14 additions & 0 deletions src/generators/dom/visitors/Element/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ export default function visitElement ( generator: DomGenerator, block: Block, st
}

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
// *after* its children have been updated
visitAttributesAndAddProps();
Expand Down
2 changes: 1 addition & 1 deletion src/generators/dom/visitors/Element/lookup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ const lookup = {
title: {},
type: { appliesTo: [ 'button', 'input', 'command', 'embed', 'object', 'script', 'source', 'style', 'menu' ] },
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' ] },
wrap: { appliesTo: [ 'textarea' ] }
};
Expand Down
45 changes: 29 additions & 16 deletions src/generators/server-side-rendering/visitors/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,17 @@ const meta = {
':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 ) {
if ( node.name in meta ) {
return meta[ node.name ]( generator, block, node );
Expand All @@ -21,24 +32,22 @@ export default function visitElement ( generator: SsrGenerator, block: Block, no
}

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

node.attributes.forEach( ( attribute: Node ) => {
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 ) {
str += `="` + attribute.value.map( ( chunk: Node ) => {
if ( chunk.type === 'Text' ) {
return chunk.data;
}
if ( attribute.value !== true ) {
str += `="${stringifyAttributeValue( block, attribute.value )}"`;
}

const { snippet } = block.contextualise( chunk.expression );
return '${' + snippet + '}';
}).join( '' ) + `"`;
openingTag += str;
}

openingTag += str;
});

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

generator.append( openingTag );

generator.elementDepth += 1;
if ( node.name === 'textarea' && textareaContents !== undefined ) {
generator.append( textareaContents );
} else {
generator.elementDepth += 1;

node.children.forEach( ( child: Node ) => {
visit( generator, block, child );
});
node.children.forEach( ( child: Node ) => {
visit( generator, block, child );
});

generator.elementDepth -= 1;
generator.elementDepth -= 1;
}

if ( !isVoidElementName( node.name ) ) {
generator.append( `</${node.name}>` );
Expand Down
91 changes: 52 additions & 39 deletions src/parse/state/tag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import { Parser } from '../index';
import { Node } from '../../interfaces';

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

const SELF = ':Self';

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

if ( selfClosing ) {
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 {
// don't push self-closing elements onto the stack
parser.stack.push( element );
Expand Down Expand Up @@ -280,28 +284,66 @@ function readAttribute ( parser: Parser, uniqueNames ) {
}

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 = `'`;
if ( parser.eat( `"` ) ) quoteMark = `"`;
function getShorthandValue ( start: number, name: string ) {
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 = {
start: parser.index,
end: null,
type: 'Text',
data: ''
};

const done = quoteMark ?
char => char === quoteMark :
char => invalidUnquotedAttributeCharacters.test( char );

const chunks = [];

while ( parser.index < parser.template.length ) {
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 ) {
currentChunk.end = index;
chunks.push( currentChunk );
Expand All @@ -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 {
currentChunk.data += parser.template[ parser.index++ ];
}
}

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
}
}];
}
}
8 changes: 8 additions & 0 deletions src/validate/html/validateElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ export default function validateElement ( validator: Validator, node: Node ) {
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 );
}
}
}
});
}

Expand Down
3 changes: 3 additions & 0 deletions test/parser/samples/textarea-children/input.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<textarea>
<p>not actually an element. {{foo}}</p>
</textarea>
44 changes: 44 additions & 0 deletions test/parser/samples/textarea-children/output.json
Original file line number Diff line number Diff line change
@@ -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
}
17 changes: 17 additions & 0 deletions test/runtime/samples/textarea-children/_config.js
Original file line number Diff line number Diff line change
@@ -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` );
}
};
3 changes: 3 additions & 0 deletions test/runtime/samples/textarea-children/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<textarea>
<p>not actually an element. {{foo}}</p>
</textarea>
17 changes: 17 additions & 0 deletions test/runtime/samples/textarea-value/_config.js
Original file line number Diff line number Diff line change
@@ -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' );
}
};
1 change: 1 addition & 0 deletions test/runtime/samples/textarea-value/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<textarea value='{{foo}}'/>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<textarea>
<p>not actually an element. 42</p>
</textarea>
11 changes: 11 additions & 0 deletions test/server-side-rendering/samples/textarea-children/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<textarea>
<p>not actually an element. {{foo}}</p>
</textarea>

<script>
export default {
data () {
return { foo: 42 };
}
};
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<textarea>42</textarea>
9 changes: 9 additions & 0 deletions test/server-side-rendering/samples/textarea-value/main.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<textarea value='{{foo}}'/>

<script>
export default {
data () {
return { foo: 42 };
}
};
</script>
8 changes: 8 additions & 0 deletions test/validator/samples/textarea-value-children/errors.json
Original file line number Diff line number Diff line change
@@ -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
}]
Loading

0 comments on commit ecc9a93

Please sign in to comment.