diff --git a/models/v4/CBWIREController.cfc b/models/v4/CBWIREController.cfc index c09deefe..270d3481 100644 --- a/models/v4/CBWIREController.cfc +++ b/models/v4/CBWIREController.cfc @@ -20,7 +20,6 @@ component singleton { ._withEvent( getEvent() ) ._withParams( arguments.params ) ._withKey( arguments.key ) - ._withHTTPRequestData( getHTTPRequestData() ) .renderIt(); } @@ -40,11 +39,18 @@ component singleton { comp.snapshot = deserializeJSON( comp.snapshot ); return comp; } ); - - return wirebox.getInstance( "CBWIRERequest@cbwire" ) - .withPayload( payload ) - .withEvent( arguments.event ) - .getResponse(); + // Iterate over each component in the payload and process it + return { + "components": payload.components.map( ( _componentPayload ) => { + // Locate the component and instantiate it. + var componentInstance = createInstance( _componentPayload.snapshot.memo.name ); + // Return the response for this component + return componentInstance + ._withEvent( event ) + ._withIncomingPayload( _componentPayload ) + ._getHTTPResponse( _componentPayload ); + } ) + }; } /** diff --git a/models/v4/CBWIRERequest.cfc b/models/v4/CBWIRERequest.cfc deleted file mode 100644 index 180d1790..00000000 --- a/models/v4/CBWIRERequest.cfc +++ /dev/null @@ -1,49 +0,0 @@ -component accessors="true"{ - - property name="cbwireController" inject="CBWIREController@cbwire"; - - property name="payload"; - - property name="event"; - - /** - * Passes in a payload sent by the browser. - * - * @return void - */ - function withPayload( required struct payload ){ - setPayload( arguments.payload ); - return this; - } - - /** - * Passes in the ColdBox event context. - * - * @return void. - */ - function withEvent( required event ){ - setEvent( arguments.event ); - return this; - } - - - /** - * Returns the response for subsequent requests of components. - * - * @returns struct - */ - function getResponse(){ - return { - "components": getPayload().components.map( ( _componentPayload ) => { - // Locate the component and instantiate it. - var componentInstance = getCBWIREController() - .createInstance( _componentPayload.snapshot.memo.name ); - // Return the response for this component - return componentInstance - ._withEvent( getEvent() ) - ._asSubsequentRequest() - ._getHTTPResponse( _componentPayload ); - } ) - }; - } -} \ No newline at end of file diff --git a/models/v4/Component.cfc b/models/v4/Component.cfc index db39c4ce..5e06a035 100644 --- a/models/v4/Component.cfc +++ b/models/v4/Component.cfc @@ -1,56 +1,48 @@ -component accessors="true" { +component { - property name="wirebox" inject="wirebox"; + property name="_CBWIREController" inject="CBWIREController@cbwire"; + + property name="_wirebox" inject="wirebox"; property name="_id"; - property name="_initialLoad" default="true"; + property name="_parent"; + property name="_initialLoad"; property name="_initialDataProperties"; + property name="_incomingPayload"; property name="_dataPropertyNames"; + property name="_validationResult"; property name="_params"; property name="_key"; property name="_event"; - property name="_httpRequestData"; + property name="_children"; property name="_metaData"; property name="_dispatches"; property name="_cache"; // internal cache for storing data - property name="data"; // Used for backwards compatibility - property name="args"; // Used for backwards compatibility /** * Initializes the component, setting a unique ID if not already set. * This method should be called by any extending component's init method if overridden. * Extending components should invoke super.init() to ensure the base initialization is performed. + * * @return The initialized component instance. */ function init() { - if (len(trim(get_Id())) == 0) { - set_Id(createUUID()); + if ( isNull( variables._id ) ) { + variables._id = createUUID(); } - set_Params( {} ); - set_Key( "" ); - set_HttpRequestData( {} ); - set_Cache( {} ); - set_Dispatches( [] ); - - /* - Create a reference to the variables scope called - 'data' to preserve backwards compatibility with - prior versions of CBWIRE. - */ - //setData( variables ); - - /* - Create a reference to the variables scope called - 'args' to preserve backwards compatibility with - prior versions of CBWIRE. - */ - setArgs( variables ); + + variables._params = {}; + variables._key = ""; + variables._cache = {}; + variables._dispatches = []; + variables._children = {}; + variables._initialLoad = true; /* Cache the component's meta data on initialization for fast access where needed. */ - set_MetaData( getMetaData( this ) ); + variables._metaData = getMetaData( this ); /* Prep our data properties @@ -70,13 +62,36 @@ component accessors="true" { return this; } + /* + ================================================================== + Public API + ================================================================== + */ + include "ComponentPublicAPI.cfm"; + + /* + ================================================================== + Internal API + ================================================================== + */ + /** - * Renders the component's HTML output. - * This method should be overridden by subclasses to implement specific rendering logic. - * If not overridden, this method will simply render the view. + * Returns the id of the component. + * + * @return string */ - function renderIt() { - return view( _getViewPath() ); + function _getId() { + return variables._id; + } + + /** + * Passes a reference to the parent of a child component. + * + * @return Component + */ + function _withParent( parent ) { + variables._parent = arguments.parent; + return this; } /** @@ -86,7 +101,18 @@ component accessors="true" { * */ function _withEvent( event ) { - set_Event( arguments.event ); + variables._event = arguments.event; + return this; + } + + /** + * Passes in incoming payload to the component + * + * @return Component + */ + function _withIncomingPayload( payload ) { + variables._incomingPayload = arguments.payload; + variables._initialLoad = false; return this; } @@ -96,14 +122,14 @@ component accessors="true" { * @return Component */ function _withParams( params ) { - set_Params( arguments.params ); + variables._params = arguments.params; // Fire onMount if it exists if (structKeyExists(this, "onMount")) { onMount( - event=get_Event(), - rc=get_Event().getCollection(), - prc=get_Event().getPrivateCollection(), + event=variables._event, + rc=variables._event.getCollection(), + prc=variables._event.getPrivateCollection(), params=arguments.params ); } else { @@ -116,18 +142,6 @@ component accessors="true" { return this; } - /** - * Passes HTTP request data to the component to be used with onMount. - * This method is useful for passing request data to the component for use in rendering. - * - * @param httpRequestData A struct containing the HTTP request data. - * @return Component - */ - function _withHTTPRequestData( httpRequestData ) { - set_HttpRequestData( arguments.httpRequestData ); - return this; - } - /** * Passes a key to the component to be used to identify the component * on subsequent requests. @@ -135,8 +149,7 @@ component accessors="true" { * @return Component */ function _withKey( key ) { - set_Key( arguments.key ); - + variables._key = arguments.key; return this; } @@ -148,14 +161,9 @@ component accessors="true" { */ function _hydrate( componentPayload ) { // Set our component's id to the incoming memo id - set_Id( arguments.componentPayload.snapshot.memo.id ); - - // Loop over the data and set the variables - arguments.componentPayload.snapshot.data.each( function( key, value ) { - if ( variables.keyExists( key ) ) { - variables[ key ] = value; - } - } ); + variables._id = arguments.componentPayload.snapshot.memo.id; + // Append the incoming data to our component's data + variables.data.append( arguments.componentPayload.snapshot.data, true ); } /** @@ -165,7 +173,7 @@ component accessors="true" { */ function _applyUpdates( updates ) { arguments.updates.each( function( key, value ) { - variables[ key ] = value; + variables.data[ key ] = value; } ); } @@ -177,92 +185,40 @@ component accessors="true" { */ function _applyCalls( calls ) { arguments.calls.each( function( call ) { - invoke( this, call.method, call.params ); + try { + invoke( this, call.method, call.params ); + } catch ( ValidationException e ) { + // silently fail so the component can continue to render + } catch( any e ) { + rethrow; + } } ); } /** - * Renders a specified view by converting dot notation to path notation and appending .cfm if necessary. - * Then, it returns the HTML content. - * - * @param viewPath The dot notation path to the view template to be rendered, without the .cfm extension. - * @param params A struct containing the parameters to be passed to the view template. - * @return The rendered HTML content as a string. - */ - function view( viewPath, params = {} ) { - // Normalize the view path - var normalizedPath = _getNormalizedViewPath( viewPath ); - // Render the view content and trim the result - var trimmedHTML = trim( _renderViewContent( normalizedPath, arguments.params ) ); - // Validate the HTML content to ensure it has a single outer element - _validateSingleOuterElement( trimmedHTML); - // If this is the initial load, encode the snapshot and insert Livewire attributes - if ( get_initialLoad() ) { - // Encode the snapshot for HTML attribute inclusion and process the view content - var snapshotEncoded = _encodeAttribute( serializeJson( _getSnapshot() ) ); - return _insertInitialLivewireAttributes( trimmedHTML, snapshotEncoded, get_Id() ); - } else { - // Return the trimmed HTML content - return _insertSubsequentLivewireAttributes( trimmedHTML ); - } - } - - /** - * Get a instance object from WireBox - * - * @name The mapping name or CFC path or DSL to retrieve - * @initArguments The constructor structure of arguments to passthrough when initializing the instance - * @dsl The DSL string to use to retrieve an instance - * - * @return The requested instance - */ - function getInstance( name, initArguments = {}, dsl ){ - return getWirebox().getInstance( argumentCollection=arguments ); - } - - /** - * Captures a dispatch to be executed later - * by the browser. + * Returns the validation manager if it's available. + * Otherwise throws error. * - * @event string | The event to dispatch. - * @args* | The parameters to pass to the listeners. - * - * @return void - */ - function dispatch( event ) { - // Convert params to an array first - var paramsAsArray = _parseDispatchParams( argumentCollection=arguments ); - // Append the dispatch to our dispatches array - get_Dispatches().append( { "name": arguments.event, "params": paramsAsArray } ); - } - - /** - * Dispatches an event to the current component. - * - * @event string | The event to dispatch. - * @return void + * @return ValidationManager */ - function dispatchSelf( event ) { - // Convert params to an array first - var paramsAsArray = _parseDispatchParams( argumentCollection=arguments ); - // Append the dispatch to our dispatches array - get_Dispatches().append( { "name": arguments.event, "params": paramsAsArray, "self": true } ); - + private function _getValidationManager(){ + try { + return getInstance( dsl="provider:ValidationManager@cbvalidation" ); + } catch ( any e ) { + throw( type="CBWIREException", message="ValidationManager not found. Make sure the 'cbvalidation' module is installed." ); + } } /** - * Dispatches a event to another component + * Returns a struct of cbvalidation constraints. * - * @name string | The component to dispatch to. - * @event string | The method to dispatch. - * - * @return void + * @return struct */ - function dispatchTo( name, event ) { - // Convert params to an array first - var paramsAsArray = _parseDispatchParams( argumentCollection=arguments ); - // Append the dispatch to our dispatches array - get_Dispatches().append( { "name": arguments.event, "params": paramsAsArray, "component": arguments.component } ); + private function _getConstraints(){ + if ( variables.keyExists( "constraints" ) ) { + return variables.constraints; + } + return {}; } /** @@ -320,8 +276,7 @@ component accessors="true" { Check the component's meta data for functions labeled as computed. */ - var meta = get_MetaData(); - + var meta = variables._metaData; /* Handle generated getters and setters for data properties. You see we are also preparing the getters and setters in the init method. @@ -330,16 +285,16 @@ component accessors="true" { */ if ( arguments.missingMethodName.reFindNoCase( "^get[A-Z].*" ) ) { var propertyName = arguments.missingMethodName.reReplaceNoCase( "^get", "" ); - if ( variables.keyExists( propertyName ) ) { - return variables[ propertyName ]; + if ( variables.data.keyExists( propertyName ) ) { + return variables.data[ propertyName ]; } } if ( arguments.missingMethodName.reFindNoCase( "^set[A-Z].*" ) ) { var propertyName = arguments.missingMethodName.reReplaceNoCase( "^set", "" ); // Ensure data property exists before setting it - if ( data.keyExists( propertyName ) ) { - variables[ propertyName ] = arguments.missingMethodArguments[ 1 ]; + if ( variables.data.keyExists( propertyName ) ) { + variables.data[ propertyName ] = arguments.missingMethodArguments[ 1 ]; return; } } @@ -355,7 +310,7 @@ component accessors="true" { * * @return String The generated checksum. */ - private function _generate_checksum() { + private function _generateChecksum() { return "f9f66fa895026e389a10ce006daf3f59afaec8db50cdb60f152af599b32f9192"; var secretKey = "YourSecretKey"; // This key should be securely retrieved return hash(serializeJson(arguments.snapshot) & secretKey, "SHA-256"); @@ -381,7 +336,7 @@ component accessors="true" { * @return String The HTML content with Livewire attributes properly inserted. */ private function _insertInitialLivewireAttributes( html, snapshotEncoded, id ) { - var livewireAttributes = ' wire:snapshot="' & arguments.snapshotEncoded & '" wire:effects="[]" wire:id="Z1Ruz1tGMPXSfw7osBW2"'; + var livewireAttributes = ' wire:snapshot="' & arguments.snapshotEncoded & '" wire:effects="[]" wire:id="#variables._id#"'; // Insert attributes into the opening tag return replaceNoCase( arguments.html, ">", livewireAttributes & ">", "one" ); @@ -395,7 +350,7 @@ component accessors="true" { * @return String The HTML content with Livewire attributes properly inserted. */ private function _insertSubsequentLivewireAttributes( html ) { - var livewireAttributes = " wire:id=""#get_Id()#"""; + var livewireAttributes = " wire:id=""#variables._id#"""; return replaceNoCase( arguments.html, ">", livewireAttributes & ">", "one" ); } @@ -406,23 +361,39 @@ component accessors="true" { * @return The rendered content of the view template. */ private function _renderViewContent( normalizedPath, params = {} ){ - + /* + Create reference to local scope for the method. + */ + var localScope = local; + /* + Take our data properties and make them available as variables + to the view. + */ + variables.data.each( function( key, value ) { + localScope[ key ] = value; + } ); + /* Take any params passed to the view method and make them available as variables within the view template. This allows for dynamic content to be rendered based on the parameters passed to the view method. */ params.each( function( key, value ) { - variables[ key ] = value; + localScope[ key ] = value; } ); - var viewContent = ""; - savecontent variable="viewContent" { + // Auto validate whenever rendering the view + validate(); + + // Provide 'validation.' variable to the view + localScope.validation = variables._validationResult; + + savecontent variable="localScope.viewContent" { // The leading slash in the include path might need to be removed depending on your server setup // or application structure, as cfinclude paths are relative to the application root. include "#arguments.normalizedPath#"; // Dynamically includes the CFML file for processing. } - return viewContent; + return localScope.viewContent; } /** @@ -469,6 +440,10 @@ component accessors="true" { _applyUpdates( arguments.componentPayload.updates ); // Apply any calls _applyCalls( arguments.componentPayload.calls ); + // Re-validate, silently moving along if it fails + try { + validate(); + } catch ( any e ) {} // Return the HTML response var response = { "snapshot": serializeJson( _getSnapshot() ), @@ -480,8 +455,8 @@ component accessors="true" { } }; // Add any dispatches - if ( get_Dispatches().len() ) { - response.effects["dispatches"] = get_Dispatches(); + if ( variables._dispatches.len() ) { + response.effects["dispatches"] = variables._dispatches; } return response; @@ -496,66 +471,10 @@ component accessors="true" { return { "data": _getDataProperties(), "memo": _getMemo(), - "checksum": _generate_checksum() + "checksum": _generateChecksum() }; } - /** - * Indicate that this component is being loaded from subsequent requests. - * - * @return void - */ - function _asSubsequentRequest(){ - set_InitialLoad( false ); - return this; - } - - /** - * Resets a data property to it's initial value. - * Can be used to reset all data properties, a single data property, or an array of data properties. - * - * @return - */ - function reset( property ){ - if ( isNull( arguments.property ) ) { - // Reset all properties - _getDataProperties().each( function( key, value ){ - reset( key ); - } ); - } else if ( isArray( arguments.property ) ) { - // Reset each property in our array individually - arguments.property.each( function( prop ){ - reset( prop ); - } ); - } else { - var initialState = get_initialDataProperties(); - // Reset individual property - variables[ arguments.property ] = initialState[ arguments.property ]; - } - } - - /** - * Resets all data properties except the ones specified. - * - * @return void - */ - function resetExcept( property ){ - if ( isNull( arguments.property ) ) { - throw( type="ResetException", message="Cannot reset a null property." ); - } - - // Reset all properties except what was provided - _getDataProperties().each( function( key, value ){ - if ( isArray( property ) ) { - if ( !arrayFindNoCase( property, arguments.key ) ) { - reset( key ); - } - } else if ( property != key ) { - reset( key ); - } - } ); - } - /** * Generates a computed property that caches the result of the computed method. * @@ -583,16 +502,11 @@ component accessors="true" { /** * Prepare our data properties + * + * @return void */ private function _prepareDataProperties() { - - if ( variables.keyExists( "data" ) ) { - /* - Copy any data properties defined using - 'data.' into our variables scope. - */ - variables.append( data ); - } else { + if ( !variables.keyExists( "data" ) ) { variables.data = {}; } @@ -600,26 +514,24 @@ component accessors="true" { Determine our data property names by inspecting both the data struct and the components property tags. */ - var names = []; - data.each( function( key, value ) { - names.append( key ); - } ); + variables._dataPropertyNames = variables.data.reduce( function( acc, key, value ) { + acc.append( key ); + return acc; + }, [] ); - get_metaData().properties + variables._metaData.properties .filter( function( prop ) { return !prop.keyExists( "inject" ); } ) .each( function( prop ) { - names.append( prop.name ); + variables._dataPropertyNames.append( prop.name ); } ); - set_dataPropertyNames( names ); - /* Capture our initial data properties for use in calls like reset(). */ - set_initialDataProperties( duplicate( _getDataProperties() ) ); + variables._initialDataProperties = duplicate( _getDataProperties() ); } /** @@ -634,7 +546,7 @@ component accessors="true" { For each computed function, generate a computed property that caches the result of the computed function. */ - get_MetaData().functions.filter( function( func ) { + variables._metaData.functions.filter( function( func ) { return structKeyExists(func, "computed"); } ).each( function( func ) { _generateComputedProperty( func.name, this[func.name] ); @@ -664,7 +576,7 @@ component accessors="true" { Determine our data property names by inspecting both the data struct and the components property tags. */ - var dataPropertyNames = get_DataPropertyNames(); + var dataPropertyNames = variables._dataPropertyNames; /* Loop over our data property names and generate @@ -697,11 +609,11 @@ component accessors="true" { * @return struct */ private function _getDataProperties(){ - return get_dataPropertyNames().reduce( function( acc, key, value ) { - if ( isBoolean( variables[ key ] ) && !isNumeric( variables[ key ] ) ) { - acc[ key ] = variables[ key ] ? true : false; + return variables.data.reduce( function( acc, key, value ) { + if ( isBoolean( variables.data[ key ] ) && !isNumeric( variables.data[ key ] ) ) { + acc[ key ] = variables.data[ key ] ? true : false; } else { - acc[ key ] = variables[ key ]; + acc[ key ] = variables.data[ key ]; } return acc; }, {} ); @@ -715,11 +627,11 @@ component accessors="true" { private function _getMemo(){ var name = _getComponentName(); return { - "id":"Z1Ruz1tGMPXSfw7osBW2", + "id":"#variables._id#", "name":name, "path":name, "method":"GET", - "children":[], + "children": variables._children.count() ? variables._children : [], "scripts":[], "assets":[], "errors":[], @@ -734,6 +646,30 @@ component accessors="true" { */ private function _getComponentName(){ - return getMetaData().name.replaceNoCase( "wires.", "", "one" ); + return variables._metaData.name.replaceNoCase( "wires.", "", "one" ); + } + + /** + * Take an incoming rendering and determine the outer component tag. + *
...
would return 'div' + * + * @return string + */ + private function _getComponentTag( rendering ){ + var tag = ""; + var regexMatches = reFindNoCase( "^<([a-zA-Z0-9]+)", arguments.rendering.trim(), 1, true ); + if ( regexMatches.match.len() == 2 ) { + return regexMatches.match[ 2 ]; + } + throw( type="CBWIREException", message="Cannot determine component tag." ); + } + + /** + * Returns a generated key for the component. + * + * @return string + */ + private function _generateWireKey(){ + return variables._id & "-" & variables._children.count(); } } diff --git a/models/v4/ComponentPublicAPI.cfm b/models/v4/ComponentPublicAPI.cfm new file mode 100644 index 00000000..5dba8eef --- /dev/null +++ b/models/v4/ComponentPublicAPI.cfm @@ -0,0 +1,261 @@ + + /** + * Renders the component's HTML output. + * This method should be overridden by subclasses to implement specific rendering logic. + * If not overridden, this method will simply render the view. + */ + function renderIt() { + return view( _getViewPath() ); + } + + /** + * Renders a specified view by converting dot notation to path notation and appending .cfm if necessary. + * Then, it returns the HTML content. + * + * @param viewPath The dot notation path to the view template to be rendered, without the .cfm extension. + * @param params A struct containing the parameters to be passed to the view template. + * @return The rendered HTML content as a string. + */ + function view( viewPath, params = {} ) { + // Normalize the view path + var normalizedPath = _getNormalizedViewPath( viewPath ); + // Render the view content and trim the result + var trimmedHTML = trim( _renderViewContent( normalizedPath, arguments.params ) ); + // Validate the HTML content to ensure it has a single outer element + _validateSingleOuterElement( trimmedHTML); + // If this is the initial load, encode the snapshot and insert Livewire attributes + if ( variables._initialLoad ) { + // Encode the snapshot for HTML attribute inclusion and process the view content + var snapshotEncoded = _encodeAttribute( serializeJson( _getSnapshot() ) ); + return _insertInitialLivewireAttributes( trimmedHTML, snapshotEncoded, variables._id ); + } else { + // Return the trimmed HTML content + return _insertSubsequentLivewireAttributes( trimmedHTML ); + } + } + + /** + * Get a instance object from WireBox + * + * @name The mapping name or CFC path or DSL to retrieve + * @initArguments The constructor structure of arguments to passthrough when initializing the instance + * @dsl The DSL string to use to retrieve an instance + * + * @return The requested instance + */ + function getInstance( name, initArguments = {}, dsl ){ + return variables._wirebox.getInstance( argumentCollection=arguments ); + } + + /** + * Captures a dispatch to be executed later + * by the browser. + * + * @event string | The event to dispatch. + * @args* | The parameters to pass to the listeners. + * + * @return void + */ + function dispatch( event ) { + // Convert params to an array first + var paramsAsArray = _parseDispatchParams( argumentCollection=arguments ); + // Append the dispatch to our dispatches array + variables._dispatches.append( { "name": arguments.event, "params": paramsAsArray } ); + } + + /** + * Dispatches an event to the current component. + * + * @event string | The event to dispatch. + * @return void + */ + function dispatchSelf( event ) { + // Convert params to an array first + var paramsAsArray = _parseDispatchParams( argumentCollection=arguments ); + // Append the dispatch to our dispatches array + variables._dispatches.append( { "name": arguments.event, "params": paramsAsArray, "self": true } ); + + } + + /** + * Dispatches a event to another component + * + * @name string | The component to dispatch to. + * @event string | The method to dispatch. + * + * @return void + */ + function dispatchTo( name, event ) { + // Convert params to an array first + var paramsAsArray = _parseDispatchParams( argumentCollection=arguments ); + // Append the dispatch to our dispatches array + variables._dispatches.append( { "name": arguments.event, "params": paramsAsArray, "component": arguments.component } ); + } + + /** + * Instantiates a CBWIRE component, mounts it, + * and then calls its internal renderIt() method. + * + * This is nearly identical to the wire method defined + * in the CBWIREController component, but it is intended + * to provide the wire() method when including nested components + * and provides tracking of the child. + * + * @param name The name of the component to load. + * @param params The parameters you want mounted initially. Defaults to an empty struct. + * @param key An optional key parameter. Defaults to an empty string. + * + * @return An instance of the specified component after rendering. + */ + function wire(required string name, struct params = {}, string key = "") { + // Generate a key if one is not provided + if ( !arguments.key.len() ) { + arguments.key = _generateWireKey(); + } + + /* + If the parent is loaded from a subsequent request, + check if the child has already been rendered. + */ + if ( !variables._initialLoad ) { + var incomingPayload = get_IncomingPayload(); + var children = incomingPayload.snapshot.memo.children; + // Are we trying to render a child that has already been rendered? + if ( children.keyExists( arguments.key ) ) { + var componentTag = children[ arguments.key ][1]; + var componentId = children[ arguments.key ][2]; + // Re-track the rendered child + variables._children.append( { + "#arguments.key#": [ + componentTag, + componentId + ] + } ); + // We've already rendered this child, so return a stub for it + return "<#componentTag# wire:id=""#componentId#"">"; + } + } + // Instaniate this child component as a new component + var instance = variables._CBWIREController.createInstance(argumentCollection=arguments) + ._withParent( this ) + ._withEvent( variables._event ) + ._withParams( arguments.params ) + ._withKey( arguments.key ); + // Render it out + var rendering = instance.renderIt(); + // Based on the rendering, determine our outer component tag + var componentTag = _getComponentTag( rendering ); + // Track the rendered child + variables._children.append( { + "#arguments.key#": [ + componentTag, + instance._getId() + ] + } ); + + return instance.renderIt(); + } + + /** + * Provides the teleport() method to be used in views. + * + * @selector string | The selector to teleport to. + * + * @return string + */ + function teleport( selector ){ + return ""; + } + + /** + * Provides cbvalidation method to be used in actions and views. + * + * @return ValidationResult + */ + function validate( target, fields, constraints, locale, excludeFields, includeFields, profiles ){ + arguments.target = isNull( arguments.target ) ? _getDataProperties() : arguments.target; + arguments.constraints = isNull( arguments.constraints ) ? _getConstraints() : arguments.constraints; + variables._validationResult = _getValidationManager().validate( argumentCollection = arguments ); + return variables._validationResult; + } + + /** + * Provides cbvalidation method to be used in actions and views, + * throwing an exception if validation fails. + * + * @return ValidationResult + * + * @throws ValidationException + */ + function validateOrFail( target, fields, constraints, locale, excludeFields, includeFields, profiles ){ + arguments.target = isNull( arguments.target ) ? _getDataProperties() : arguments.target; + arguments.constraints = isNull( arguments.constraints ) ? _getConstraints() : arguments.constraints; + _getValidationManager().validateOrFail( argumentCollection = arguments ) + variables._validationResult = _getValidationManager().validate( argumentCollection = arguments ); + return variables._validationResult; + } + + /** + * Returns true if the validation result has errors. + * + * @return boolean + */ + function hasErrors( field ){ + return variables._validationResult.hasErrors( arguments.field ); + } + + /** + * Resets a data property to it's initial value. + * Can be used to reset all data properties, a single data property, or an array of data properties. + * + * @return + */ + function reset( property ){ + if ( isNull( arguments.property ) ) { + // Reset all properties + variables.data.each( function( key, value ){ + reset( key ); + } ); + } else if ( isArray( arguments.property ) ) { + // Reset each property in our array individually + arguments.property.each( function( prop ){ + reset( prop ); + } ); + } else { + var initialState = variables._initialDataProperties; + // Reset individual property + variables.data[ arguments.property ] = initialState[ arguments.property ]; + } + } + + /** + * Resets all data properties except the ones specified. + * + * @return void + */ + function resetExcept( property ){ + if ( isNull( arguments.property ) ) { + throw( type="ResetException", message="Cannot reset a null property." ); + } + + // Reset all properties except what was provided + _getDataProperties().each( function( key, value ){ + if ( isArray( property ) ) { + if ( !arrayFindNoCase( property, arguments.key ) ) { + reset( key ); + } + } else if ( property != key ) { + reset( key ); + } + } ); + } + \ No newline at end of file diff --git a/test-harness/tests/specs/CBWIRE4Spec.cfc b/test-harness/tests/specs/CBWIRE4Spec.cfc index cc510821..19bdbcd5 100644 --- a/test-harness/tests/specs/CBWIRE4Spec.cfc +++ b/test-harness/tests/specs/CBWIRE4Spec.cfc @@ -10,6 +10,7 @@ component extends="coldbox.system.testing.BaseTestCase" { // and prepareMock() is a custom method to mock any dependencies, if necessary. setup(); comp = getInstance("wires.SuperHeroes"); + comp._withEvent( getRequestContext( ) ); prepareMock( comp ); }); @@ -91,7 +92,7 @@ component extends="coldbox.system.testing.BaseTestCase" { expect( snapshot.memo.path ).toBe( "SuperHeroes" ); } ); - it( "computed property can be accessed from view", function() { + fit( "computed property can be accessed from view", function() { comp.addHero( "Iron Man" ); comp.addHero( "Superman" ); var viewContent = comp.view("wires.superheroes" ); @@ -142,6 +143,48 @@ component extends="coldbox.system.testing.BaseTestCase" { expect( comp.numberOfVillians() ).toBe( 0 ); } ); + it( "can render child components", function() { + comp.setShowStats( true ); + var result = comp.view( "wires.superheroes" ); + expect( result ).toInclude( ""super-hero" ); + } ); + + it( "provide teleport and endTeleport methods", function() { + expect( comp.teleport( 'body' ) ).toBe( "" ); + } ); + + it( "can validate()", function() { + var result = comp.validate(); + expect( result ).toBeInstanceOf( "ValidationResult" ); + expect( result.hasErrors() ).toBeTrue(); + } ); + + it("can access validation from view", function() { + var result = comp.validate(); + var viewContent = comp.view("wires.superheroesvalidation"); + expect(viewContent).toInclude("The 'mailingList' has an invalid type, expected type is email"); + }); + + it( "auto validates", function() { + var viewContent = comp.view("wires.superheroesvalidation"); + expect(viewContent).toInclude("The 'mailingList' has an invalid type, expected type is email"); + } ); + + it( "can validateOrFail", function() { + expect(function() { + comp.validateOrFail(); + }).toThrow( type="ValidationException" ); + + comp.setMailingList( "x-men@somedomain.com" ); + expect( comp.validateOrFail() ).toBeInstanceOf( "ValidationResult" ); + } ); + + it( "can hasError( field)", function() { + comp.validate(); + expect( comp.hasErrors( "mailingList" ) ).toBeTrue(); + } ); + xit("throws an exception for a non-existent view", function() { expect(function() { comp.view("nonExistentView.cfm"); diff --git a/test-harness/views/examples/index.cfm b/test-harness/views/examples/index.cfm index ee6bfc95..d0f1bf65 100644 --- a/test-harness/views/examples/index.cfm +++ b/test-harness/views/examples/index.cfm @@ -40,6 +40,9 @@
Parent/Child Component
+
+ Teleport +

Forms

diff --git a/test-harness/wires/ChildComponent.cfc b/test-harness/wires/ChildComponent.cfc new file mode 100644 index 00000000..7f7a3be8 --- /dev/null +++ b/test-harness/wires/ChildComponent.cfc @@ -0,0 +1,3 @@ +component extends="cbwire.models.v4.Component" { + +} \ No newline at end of file diff --git a/test-harness/wires/Counter.cfc b/test-harness/wires/Counter.cfc index 7290ce41..3b4e2bbf 100644 --- a/test-harness/wires/Counter.cfc +++ b/test-harness/wires/Counter.cfc @@ -15,7 +15,7 @@ component extends="cbwire.models.v4.Component" accessors="true" { * Increments the counter by 1. */ function increment() { - variables.count += 1; + data.count += 1; dispatch( "incremented" ); } @@ -23,7 +23,7 @@ component extends="cbwire.models.v4.Component" accessors="true" { * Increments the counter by a specified amount. */ function incrementBy( amount ) { - count += amount; + data.count += amount; dispatch( "incrementedBy", 10 ); } @@ -33,7 +33,7 @@ component extends="cbwire.models.v4.Component" accessors="true" { * @return void */ function incrementDispatchTo() { - count += 1; + data.count += 1; dispatchTo( "anotherComponent", "incremented" ); } @@ -42,7 +42,7 @@ component extends="cbwire.models.v4.Component" accessors="true" { * @return void */ function incrementDispatchSelf() { - count += 1; + data.count += 1; dispatchSelf( "incremented" ); } @@ -57,7 +57,7 @@ component extends="cbwire.models.v4.Component" accessors="true" { * Decrements the counter by 1. */ function decrementWithDataDot(){ - count -= 1; + data.count -= 1; } /** diff --git a/test-harness/wires/FormValidation.cfc b/test-harness/wires/FormValidation.cfc index a5a36074..c3b090b8 100644 --- a/test-harness/wires/FormValidation.cfc +++ b/test-harness/wires/FormValidation.cfc @@ -1,4 +1,4 @@ -component extends="cbwire.models.Component" { +component extends="cbwire.models.v4.Component" { constraints = { "email": { required: true, type: "email" } @@ -11,6 +11,6 @@ component extends="cbwire.models.Component" { function addEmail() { validateOrFail(); - data.success = true; + success = true; } } \ No newline at end of file diff --git a/test-harness/wires/ParentChildComponent.cfc b/test-harness/wires/ParentChildComponent.cfc new file mode 100644 index 00000000..5b51d223 --- /dev/null +++ b/test-harness/wires/ParentChildComponent.cfc @@ -0,0 +1,14 @@ +component extends="cbwire.models.v4.Component" { + data = { + "name": "Grant", + "toggle": true + }; + + function toggleComps() { + toggle = !toggle; + } + + function reload() { + refresh(); + } +} \ No newline at end of file diff --git a/test-harness/wires/ParentChildComponent.cfm b/test-harness/wires/ParentChildComponent.cfm index 58c95959..781b068e 100644 --- a/test-harness/wires/ParentChildComponent.cfm +++ b/test-harness/wires/ParentChildComponent.cfm @@ -1,23 +1,9 @@ - - data = { - "name": "Grant", - "toggle": true - }; - - function toggleComps() { - data.toggle = !data.toggle; - } - - function reload() { - refresh(); - } - -

Parent

Rendered at #now()#
#name#
+
toggle = #toggle#
diff --git a/test-harness/wires/SuperHeroStats.cfc b/test-harness/wires/SuperHeroStats.cfc new file mode 100644 index 00000000..7f7a3be8 --- /dev/null +++ b/test-harness/wires/SuperHeroStats.cfc @@ -0,0 +1,3 @@ +component extends="cbwire.models.v4.Component" { + +} \ No newline at end of file diff --git a/test-harness/wires/SuperHeroes.cfc b/test-harness/wires/SuperHeroes.cfc index 8f0edab5..7901d65f 100644 --- a/test-harness/wires/SuperHeroes.cfc +++ b/test-harness/wires/SuperHeroes.cfc @@ -1,30 +1,36 @@ component extends="cbwire.models.v4.Component" { data = { + "mailinglist": "x-men at marvel.com", "heroes": [], "villians": [], "isMarvel": true, - "isDC": false + "isDC": false, + "showStats": false + }; + + constraints = { + "mailingList": { required: true, type: "email" } }; function defeatVillians(){ - villians = []; + data.villians = []; } function addHero(hero){ - heroes.append(hero); + data.heroes.append(hero); } function addVillian(villian){ - villians.append(villian); + data.villians.append(villian); } function numberOfHeroes() computed { - return heroes.len(); + return data.heroes.len(); } function numberOfVillians() computed { - return villians.len(); + return data.villians.len(); } function calculateStrength() computed { diff --git a/test-harness/wires/Teleport.cfc b/test-harness/wires/Teleport.cfc new file mode 100644 index 00000000..30ce8607 --- /dev/null +++ b/test-harness/wires/Teleport.cfc @@ -0,0 +1,3 @@ +component extends="cbwire.models.v4.Component" { + +} \ No newline at end of file diff --git a/test-harness/wires/TeleportChild.cfc b/test-harness/wires/TeleportChild.cfc new file mode 100644 index 00000000..2152afe1 --- /dev/null +++ b/test-harness/wires/TeleportChild.cfc @@ -0,0 +1,10 @@ +component extends="cbwire.models.v4.Component" { + + data = { + "ran": false + }; + + function runTeleport() { + ran = true; + } +} \ No newline at end of file diff --git a/test-harness/wires/formvalidation.cfm b/test-harness/wires/formvalidation.cfm index 8dd1b0a6..9ab4829d 100644 --- a/test-harness/wires/formvalidation.cfm +++ b/test-harness/wires/formvalidation.cfm @@ -1,15 +1,19 @@
- + - +
Success!
+ +
no success
- - -
#error#
+ + +
+ #error# +
diff --git a/test-harness/wires/superheroes.cfm b/test-harness/wires/superheroes.cfm index 86058253..44c6a4c4 100644 --- a/test-harness/wires/superheroes.cfm +++ b/test-harness/wires/superheroes.cfm @@ -11,5 +11,10 @@ + + + #wire( "SuperHeroStats", {}, "super-hero" )# + #wire( "SuperHeroStats" )# +
\ No newline at end of file diff --git a/test-harness/wires/superheroesvalidation.cfm b/test-harness/wires/superheroesvalidation.cfm new file mode 100644 index 00000000..dc49483d --- /dev/null +++ b/test-harness/wires/superheroesvalidation.cfm @@ -0,0 +1,9 @@ + +
+ + +

#error.getMessage()#

+
+
+
+
\ No newline at end of file diff --git a/test-harness/wires/superherostats.cfm b/test-harness/wires/superherostats.cfm new file mode 100644 index 00000000..182834e3 --- /dev/null +++ b/test-harness/wires/superherostats.cfm @@ -0,0 +1,5 @@ + +
+

Super Hero Status

+
+
\ No newline at end of file diff --git a/test-harness/wires/teleport.cfm b/test-harness/wires/teleport.cfm new file mode 100644 index 00000000..461044c2 --- /dev/null +++ b/test-harness/wires/teleport.cfm @@ -0,0 +1,12 @@ + +
+
Teleport
+ + #wire( "TeleportChild" )# + +

Teleported Data from Child

+
+ +
+
+
\ No newline at end of file diff --git a/test-harness/wires/teleportchild.cfm b/test-harness/wires/teleportchild.cfm new file mode 100644 index 00000000..60bee3da --- /dev/null +++ b/test-harness/wires/teleportchild.cfm @@ -0,0 +1,14 @@ + +
+ + + + #teleport( "##teleport-div" )# +
#now()#
+ #endTeleport()# +
+
+ +
\ No newline at end of file