-
Notifications
You must be signed in to change notification settings - Fork 27.2k
refactor(compiler): support passing @content blocks as functions
#69087
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -7,48 +7,52 @@ | |||||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import * as html from '../ml_parser/ast'; | ||||||||||||||||||||||||||||||||||||||||
| import {ParseError} from '../parse_util'; | ||||||||||||||||||||||||||||||||||||||||
| import {ParseError, ParseSourceSpan} from '../parse_util'; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| import * as t from './r3_ast'; | ||||||||||||||||||||||||||||||||||||||||
| import {IDENTIFIER_PATTERN} from './util'; | ||||||||||||||||||||||||||||||||||||||||
| import {IDENTIFIER_PATTERN, LET_PATTERN} from './util'; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** Creates a content block from an HTML AST node. */ | ||||||||||||||||||||||||||||||||||||||||
| export function createContentBlock( | ||||||||||||||||||||||||||||||||||||||||
| ast: html.Block, | ||||||||||||||||||||||||||||||||||||||||
| visitor: html.Visitor, | ||||||||||||||||||||||||||||||||||||||||
| ): {node: t.ContentBlock | null; errors: ParseError[]} { | ||||||||||||||||||||||||||||||||||||||||
| const errors: ParseError[] = []; | ||||||||||||||||||||||||||||||||||||||||
| if (ast.parameters.length !== 1) { | ||||||||||||||||||||||||||||||||||||||||
| if (ast.parameters.length < 1 || ast.parameters.length > 2) { | ||||||||||||||||||||||||||||||||||||||||
| errors.push( | ||||||||||||||||||||||||||||||||||||||||
| new ParseError(ast.startSourceSpan, '@content block must have exactly one parameter'), | ||||||||||||||||||||||||||||||||||||||||
| new ParseError( | ||||||||||||||||||||||||||||||||||||||||
| ast.startSourceSpan, | ||||||||||||||||||||||||||||||||||||||||
| '@content block must have one or two parameters, e.g. ' + | ||||||||||||||||||||||||||||||||||||||||
| '"@content(header)" or "@content(items; let item, index)"', | ||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| return {node: null, errors}; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const param = ast.parameters[0]; | ||||||||||||||||||||||||||||||||||||||||
| let expr = param.expression.trim(); | ||||||||||||||||||||||||||||||||||||||||
| if (expr.startsWith('(') && expr.endsWith(')')) { | ||||||||||||||||||||||||||||||||||||||||
| expr = expr.slice(1, -1).trim(); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const parts = expr.split(',').map((p) => p.trim()); | ||||||||||||||||||||||||||||||||||||||||
| if (parts.length !== 1 || parts[0] === '') { | ||||||||||||||||||||||||||||||||||||||||
| const nameParam = ast.parameters[0]; | ||||||||||||||||||||||||||||||||||||||||
| const name = nameParam.expression.trim(); | ||||||||||||||||||||||||||||||||||||||||
| if (name.includes(',')) { | ||||||||||||||||||||||||||||||||||||||||
| errors.push( | ||||||||||||||||||||||||||||||||||||||||
| new ParseError(ast.startSourceSpan, '@content block must have exactly one parameter'), | ||||||||||||||||||||||||||||||||||||||||
| new ParseError(ast.startSourceSpan, '@content block must have exactly one name parameter'), | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| return {node: null, errors}; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const name = parts[0]; | ||||||||||||||||||||||||||||||||||||||||
| if (!IDENTIFIER_PATTERN.test(name)) { | ||||||||||||||||||||||||||||||||||||||||
| errors.push( | ||||||||||||||||||||||||||||||||||||||||
| new ParseError(param.sourceSpan, '@content name must be a valid JavaScript identifier'), | ||||||||||||||||||||||||||||||||||||||||
| new ParseError(nameParam.sourceSpan, '@content name must be a valid JavaScript identifier'), | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| return {node: null, errors}; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const variables = parseContentBlockVariables(ast, errors); | ||||||||||||||||||||||||||||||||||||||||
| if (variables === null) { | ||||||||||||||||||||||||||||||||||||||||
| return {node: null, errors}; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const node = new t.ContentBlock( | ||||||||||||||||||||||||||||||||||||||||
| name, | ||||||||||||||||||||||||||||||||||||||||
| variables, | ||||||||||||||||||||||||||||||||||||||||
| html.visitAll(visitor, ast.children, ast.children), | ||||||||||||||||||||||||||||||||||||||||
| ast.nameSpan, | ||||||||||||||||||||||||||||||||||||||||
| ast.sourceSpan, | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -58,3 +62,81 @@ export function createContentBlock( | |||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| return {node, errors}; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| /** Parses the variables of a content block. */ | ||||||||||||||||||||||||||||||||||||||||
| function parseContentBlockVariables(ast: html.Block, errors: ParseError[]): t.Variable[] | null { | ||||||||||||||||||||||||||||||||||||||||
| const variables: t.Variable[] = []; | ||||||||||||||||||||||||||||||||||||||||
| if (ast.parameters.length < 2) { | ||||||||||||||||||||||||||||||||||||||||
| return variables; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const varsParam = ast.parameters[1]; | ||||||||||||||||||||||||||||||||||||||||
| const varsExpr = varsParam.expression.trim(); | ||||||||||||||||||||||||||||||||||||||||
| const letMatch = varsExpr.match(LET_PATTERN); | ||||||||||||||||||||||||||||||||||||||||
| if (!letMatch) { | ||||||||||||||||||||||||||||||||||||||||
| errors.push( | ||||||||||||||||||||||||||||||||||||||||
| new ParseError( | ||||||||||||||||||||||||||||||||||||||||
| varsParam.sourceSpan, | ||||||||||||||||||||||||||||||||||||||||
| '@content block variables must start with "let", e.g. "let item, index"', | ||||||||||||||||||||||||||||||||||||||||
| ), | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ✨ Currently, if a user attempts to use destructuring syntax (e.g. While we may want to revisit supporting destructuring or other advanced syntaxes in the future, explicitly guarding against them now with a simple regex check on the entire variables string before splitting allows us to throw a clear error.
Suggested change
|
||||||||||||||||||||||||||||||||||||||||
| const varNames = letMatch[1].split(',').map((v) => v.trim()); | ||||||||||||||||||||||||||||||||||||||||
| const variablesRawString = letMatch[1]; | ||||||||||||||||||||||||||||||||||||||||
| const variablesStartOffset = varsParam.expression.indexOf(variablesRawString); | ||||||||||||||||||||||||||||||||||||||||
| const variablesStartLocation = varsParam.sourceSpan.start.moveBy(variablesStartOffset); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| let searchIndex = 0; | ||||||||||||||||||||||||||||||||||||||||
| for (let varName of varNames) { | ||||||||||||||||||||||||||||||||||||||||
| if (varName === '') { | ||||||||||||||||||||||||||||||||||||||||
| errors.push(new ParseError(varsParam.sourceSpan, 'Invalid variable name in @content block')); | ||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| let varSpan: ParseSourceSpan; | ||||||||||||||||||||||||||||||||||||||||
| const index = variablesRawString.indexOf(varName, searchIndex); | ||||||||||||||||||||||||||||||||||||||||
| const varStart = variablesStartLocation.moveBy(index); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (varName.includes('=')) { | ||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AGENT: In
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably minor since this is invalid syntax anyways. |
||||||||||||||||||||||||||||||||||||||||
| const eqIndex = varName.indexOf('='); | ||||||||||||||||||||||||||||||||||||||||
| const namePart = varName.substring(0, eqIndex).trim(); | ||||||||||||||||||||||||||||||||||||||||
| const fullVarSpan = new ParseSourceSpan(varStart, varStart.moveBy(varName.length)); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| errors.push( | ||||||||||||||||||||||||||||||||||||||||
| new ParseError(fullVarSpan, `@content block variables cannot be assigned a value`), | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| varName = namePart; | ||||||||||||||||||||||||||||||||||||||||
| varSpan = new ParseSourceSpan(varStart, varStart.moveBy(varName.length)); | ||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||
| varSpan = new ParseSourceSpan(varStart, varStart.moveBy(varName.length)); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (!IDENTIFIER_PATTERN.test(varName)) { | ||||||||||||||||||||||||||||||||||||||||
| errors.push( | ||||||||||||||||||||||||||||||||||||||||
| new ParseError(varSpan, `Variable name "${varName}" must be a valid JavaScript identifier`), | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| searchIndex = index + varName.length; | ||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (variables.some((v) => v.name === varName)) { | ||||||||||||||||||||||||||||||||||||||||
| errors.push( | ||||||||||||||||||||||||||||||||||||||||
| new ParseError(varSpan, `Duplicate variable name "${varName}" in @content block`), | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| searchIndex = index + varName.length; | ||||||||||||||||||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // @content block variables cannot be assigned an explicit value in the template | ||||||||||||||||||||||||||||||||||||||||
| // (e.g. "let item = value"). Instead, they are assigned an argument of the calling render function | ||||||||||||||||||||||||||||||||||||||||
| // based on their positional index. For example if we have a @content block like | ||||||||||||||||||||||||||||||||||||||||
| // "@content(items; let item, index)" the render function for that block will be called like | ||||||||||||||||||||||||||||||||||||||||
| // "render(items, ctx[0], ctx[1])". | ||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AGENT nit: The name of the block (items) is actually not passed as the first parameter to the render function. probably only address this if you're making other changes |
||||||||||||||||||||||||||||||||||||||||
| variables.push(new t.Variable(varName, '', varSpan, varSpan)); | ||||||||||||||||||||||||||||||||||||||||
| searchIndex = index + varName.length; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| return variables; | ||||||||||||||||||||||||||||||||||||||||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How exactly does this work? How would the foreign component communicate a change to the values of the parameters and when would that be written to the DOM as part of the update pass?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes are communicated through reactivity–if you want to provide mutable props, you pass a signal/function instead of a value. It's up to the foreign component to reactively track those reads and schedule updates accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
angular/packages/compiler/src/template/pipeline/src/ingest.ts
Lines 312 to 317 in 6bde84f
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check out the reactivity tests for an example of this working end to end.