├── .github └── workflows │ └── test.yml ├── .gitignore ├── .project ├── .smalltalk.ston ├── Dockerfile ├── LICENSE ├── README.md ├── build-examples.sh ├── load-project.st ├── src ├── .properties ├── BaselineOfLiveWeb │ ├── BaselineOfLiveWeb.class.st │ └── package.st ├── LiveWeb-Bulma │ ├── LWBulma.class.st │ ├── LWBulmaBlock.class.st │ ├── LWBulmaBox.class.st │ ├── LWBulmaButton.class.st │ ├── LWBulmaColumns.class.st │ ├── LWBulmaComponent.class.st │ ├── LWBulmaPage.class.st │ ├── LWBulmaTabs.class.st │ ├── LWBulmaTile.class.st │ ├── ManifestLiveWebBulma.class.st │ └── package.st ├── LiveWeb-Core-Tests │ ├── HTMLRendererTest.class.st │ ├── LWComponentTest.class.st │ ├── LWContainerTest.class.st │ ├── LWMockContext.class.st │ ├── LWTemplateTest.class.st │ └── package.st ├── LiveWeb-Core │ ├── BlockClosure.extension.st │ ├── Choice.class.st │ ├── Dictionary.extension.st │ ├── FileStream.class.st │ ├── HTMLRenderer.class.st │ ├── LWAttributeRenderer.class.st │ ├── LWBlockContainer.class.st │ ├── LWComponent.class.st │ ├── LWContainer.class.st │ ├── LWContext.class.st │ ├── LWCustomElement.class.st │ ├── LWCustomElementSlot.class.st │ ├── LWCustomEvent.class.st │ ├── LWExportJS.class.st │ ├── LWFragmentContainer.class.st │ ├── LWLogEvent.class.st │ ├── LWPage.class.st │ ├── LWPageConnection.class.st │ ├── LWPartsComponent.class.st │ ├── LWPushState.class.st │ ├── LWPushStateBase.class.st │ ├── LWPushStateRoute.class.st │ ├── LWPushStateRouter.class.st │ ├── LWScriptCallback.class.st │ ├── LWScriptHelper.class.st │ ├── LWSingleContainer.class.st │ ├── LWSwitch.class.st │ ├── LWTemplate.class.st │ ├── LWTemplateHTMLRenderer.class.st │ ├── LWWindowListener.class.st │ ├── ManifestLiveWebCore.class.st │ ├── String.extension.st │ └── package.st ├── LiveWeb-Developer-Tests │ ├── LWDevHTMLCompilerTest.class.st │ └── package.st ├── LiveWeb-Developer │ ├── LWDevClassExamplesView.class.st │ ├── LWDevClassMethodsView.class.st │ ├── LWDevClassView.class.st │ ├── LWDevHTMLCompiler.class.st │ ├── LWDevListing.class.st │ ├── LWDevMain.class.st │ ├── LWDevMethodView.class.st │ ├── LWDevPackageMenu.class.st │ ├── LWDevPackageView.class.st │ ├── LWDevPage.class.st │ ├── LWDevSpotter.class.st │ ├── LWDevStyles.class.st │ ├── ManifestLiveWebDeveloper.class.st │ └── package.st ├── LiveWeb-Examples-Tests │ ├── LWExamplesTest.class.st │ └── package.st ├── LiveWeb-Examples │ ├── DemoEntity.class.st │ ├── LWAnalogClock.class.st │ ├── LWClock.class.st │ ├── LWClockExample.class.st │ ├── LWCounter.class.st │ ├── LWCrudCreateState.class.st │ ├── LWCrudEditState.class.st │ ├── LWCrudExample.class.st │ ├── LWCrudListingState.class.st │ ├── LWCrudPage.class.st │ ├── LWCrudRouter.class.st │ ├── LWCrudState.class.st │ ├── LWDigitalClock.class.st │ ├── LWExampleMain.class.st │ ├── LWExampleMenu.class.st │ ├── LWExamplePage.class.st │ ├── LWExampleStyles.class.st │ ├── LWExamples.class.st │ ├── LWMultiCounter.class.st │ ├── LWTodoExample.class.st │ ├── LWTodoItem.class.st │ ├── LWTreeExample.class.st │ ├── LWTreeNode.class.st │ ├── LWTypeAheadExample.class.st │ ├── LWWordle.class.st │ ├── LWWordleGuess.class.st │ ├── LWWordleKeyboard.class.st │ ├── ManifestLiveWebExamples.class.st │ └── package.st ├── LiveWeb-Forms │ ├── LWBaseEditor.class.st │ ├── LWBooleanEditor.class.st │ ├── LWCollectionEditor.class.st │ ├── LWDateAndTimeEditor.class.st │ ├── LWDateEditor.class.st │ ├── LWDependentEditor.class.st │ ├── LWEditForm.class.st │ ├── LWFloatEditor.class.st │ ├── LWFormComponent.class.st │ ├── LWFormError.class.st │ ├── LWFormMessages.class.st │ ├── LWIntegerEditor.class.st │ ├── LWNumericEditor.class.st │ ├── LWScaledDecimalEditor.class.st │ ├── LWStringEditor.class.st │ ├── LWTimeEditor.class.st │ ├── LWTypeAhead.class.st │ ├── LWValidationError.class.st │ ├── LWValueTooHighError.class.st │ ├── LWValueTooLowError.class.st │ └── package.st ├── LiveWeb-Presenter │ ├── LWPresenter.class.st │ ├── LWPresenterStyles.class.st │ ├── LWTwoColumnListing.class.st │ └── package.st ├── LiveWeb-ReStore │ ├── Class.extension.st │ ├── ManifestLiveWebReStore.class.st │ ├── Object.extension.st │ ├── SSWDBDependentWrapper.extension.st │ ├── SSWDBOwnedCollectionSpec.extension.st │ ├── SSWDBScaledDecimalWithInfo.extension.st │ ├── SSWDBStringWithInfo.extension.st │ └── package.st ├── LiveWeb-Schjerfbeck │ ├── ManifestLiveWebSchjerfbeck.class.st │ ├── SkBoolean.class.st │ ├── SkChoice.class.st │ ├── SkEntity.class.st │ ├── SkGroup.class.st │ ├── SkModel.class.st │ ├── SkMulti.class.st │ ├── SkNumber.class.st │ ├── SkText.class.st │ ├── SkVisitor.class.st │ └── package.st ├── LiveWeb-Shoelace │ ├── ManifestLiveWebShoelace.class.st │ ├── SlAlert.class.st │ ├── SlBadge.class.st │ ├── SlButton.class.st │ ├── SlChangeEvent.class.st │ ├── SlCheckbox.class.st │ ├── SlCheckboxEvent.class.st │ ├── SlClickEvent.class.st │ ├── SlDialog.class.st │ ├── SlElement.class.st │ ├── SlFormField.class.st │ ├── SlHTMLRenderer.class.st │ ├── SlIcon.class.st │ ├── SlInput.class.st │ ├── SlInputEvent.class.st │ ├── SlLWPage.class.st │ ├── SlMenu.class.st │ ├── SlMenuItem.class.st │ ├── SlOption.class.st │ ├── SlSelect.class.st │ ├── SlSelectEvent.class.st │ ├── SlSplitPanel.class.st │ ├── SlTag.class.st │ └── package.st ├── LiveWeb-Styling-Tests │ ├── LWStyleBuilderTest.class.st │ ├── LWStyleTest.class.st │ ├── LWStylesheetProviderTest.class.st │ ├── LWTestsheet.class.st │ └── package.st └── LiveWeb-Styling │ ├── Array.extension.st │ ├── CSSBinaryExpr.class.st │ ├── CSSExpr.class.st │ ├── CSSFnExpr.class.st │ ├── CSSValue.class.st │ ├── CSSVar.class.st │ ├── LWClassAttribute.class.st │ ├── LWStyle.class.st │ ├── LWStyleAttributeRenderer.class.st │ ├── LWStyleBuilder.class.st │ ├── LWStyledComponent.class.st │ ├── LWStyledPage.class.st │ ├── LWStylesheetComponent.class.st │ ├── LWStylesheetProvider.class.st │ ├── ManifestLiveWebStyling.class.st │ ├── Number.extension.st │ ├── String.extension.st │ ├── Symbol.extension.st │ └── package.st └── start.st /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: [push] 3 | jobs: 4 | build: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v2 8 | - name: Prepare java 9 | uses: actions/setup-java@v3 10 | with: 11 | distribution: 'zulu' 12 | java-version: '17' 13 | 14 | - name: Install clojure tools 15 | uses: DeLaGuardo/setup-clojure@12.2 16 | with: 17 | cli: 1.11.1.1149 18 | 19 | - uses: hpi-swa/setup-smalltalkCI@v1 20 | id: smalltalkci 21 | with: 22 | smalltalk-image: 'Pharo64-stable' 23 | - run: smalltalkci -s ${{ steps.smalltalkci.outputs.smalltalk-image }} 24 | shell: bash 25 | timeout-minutes: 15 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | playwright/.clj-kondo 2 | playwright/.cpcache 3 | playwright/.* 4 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | { 2 | 'srcDirectory' : 'src' 3 | } -------------------------------------------------------------------------------- /.smalltalk.ston: -------------------------------------------------------------------------------- 1 | SmalltalkCISpec { 2 | #loading : [ 3 | SCIMetacelloLoadSpec { 4 | #baseline : 'LiveWeb', 5 | #directory: 'src', 6 | #load: [ #fulltests ], 7 | #platforms : [ #pharo ] 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build in 2 stages to minimize the size of the final docker image 2 | # See https://docs.docker.com/develop/develop-images/multistage-build/ 3 | 4 | # Stage 1: Load project 5 | FROM basmalltalk/pharo:9.0-image AS loader 6 | COPY load-project.st ./ 7 | USER root 8 | RUN pharo Pharo.image load-project.st --save --quit 9 | 10 | # Stage 2: Copy the resulting Pharo.image with our project loaded 11 | # into a new docker image with just the vm 12 | FROM basmalltalk/pharo:9.0 13 | WORKDIR /app 14 | COPY --from=loader /opt/pharo/Pharo.image ./ 15 | COPY --from=loader /opt/pharo/Pharo.changes ./ 16 | COPY --from=loader /opt/pharo/Pharo*.sources ./ 17 | COPY start.st ./ 18 | 19 | USER root 20 | RUN chown pharo:users /app -R 21 | 22 | USER pharo 23 | CMD [ "pharo", "Pharo.image", "start.st" ] 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tatu Tarvainen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LiveWeb 2 | 3 | ![test workflow](https://github.com/tatut/LiveWeb/actions/workflows/test.yml/badge.svg) 4 | 5 | LiveWeb is a server side rendered web application framework for Smalltalk. 6 | It is based on Zinc HTTP server and WebSockets. 7 | 8 | The components live on the server and can send updates to clients through the web socket. 9 | 10 | 11 | ## Quick start guide 12 | 13 | ### Try docker examples 14 | 15 | You can try the examples in a docker with 16 | 17 | `docker run -p 8080:8080 antitadex/liveweb-examples` 18 | 19 | and opening examples: `http://localhost:8080/examples/clock` (and `counter`, `wordle`). 20 | 21 | ### Installation 22 | 23 | ```smalltalk 24 | Metacello new 25 | repository: 'github://tatut/LiveWeb/src'; 26 | baseline: 'LiveWeb'; 27 | load. 28 | ``` 29 | 30 | ### Creating a page and component. 31 | 32 | The main endpoint is an instance of `LWPage` subclass. The page will create the head (optional) and 33 | body (required) components for each page render. The page will register itself with a random UUID 34 | so the client can connect to it. 35 | 36 | Creating a simple counter component. 37 | ```smalltalk 38 | LWPage subclass: #CounterPage. 39 | 40 | CounterPage >> body: args [ 41 | ^ CounterComponent new 42 | ] 43 | ``` 44 | 45 | The create the component. 46 | ```smalltalk 47 | LWComponent subclass: #CounterComponent 48 | instanceVariableNames: 'counter'. 49 | 50 | CounterComponent >> initialize [ 51 | super initialize. 52 | counter := 0 53 | ] 54 | 55 | CounterComponent >> renderOn: h [ 56 | h div: { #id->'myBody' } with: [ 57 | h button: { #onclick -> [ self counter: counter - 1] } with: '-'; 58 | div: counter asString; 59 | button: { #onclick -> [ self counter: counter + 1] } with: '+' 60 | ] 61 | ] 62 | 63 | CounterComponent >> counter: newValue [ 64 | counter := newValue. 65 | self changed "this causes the component to rerender" 66 | ] 67 | ``` 68 | 69 | Then you need to register the page into the Zinc server delegate. 70 | ```smalltalk 71 | ZnServer default delegate 72 | map: #counter 73 | to: [:req | CounterPage new value: req ]. 74 | ``` 75 | 76 | See the `LiveWeb-Examples` package for more elaborate examples and 77 | examples of using the styling package. -------------------------------------------------------------------------------- /build-examples.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | docker build --platform linux/amd64 . -t liveweb/examples 3 | docker tag liveweb/examples antitadex/liveweb-examples 4 | docker push antitadex/liveweb-examples 5 | -------------------------------------------------------------------------------- /load-project.st: -------------------------------------------------------------------------------- 1 | Metacello new 2 | repository: 'github://tatut/LiveWeb/src'; 3 | baseline: 'LiveWeb'; 4 | load: #(examples). 5 | -------------------------------------------------------------------------------- /src/.properties: -------------------------------------------------------------------------------- 1 | { 2 | #format : #tonel 3 | } -------------------------------------------------------------------------------- /src/BaselineOfLiveWeb/BaselineOfLiveWeb.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #BaselineOfLiveWeb, 3 | #superclass : #BaselineOf, 4 | #category : #BaselineOfLiveWeb 5 | } 6 | 7 | { #category : #baselines } 8 | BaselineOfLiveWeb >> baseline: spec [ 9 | 10 | spec for: #common do: [ 11 | self reStore: spec. 12 | spec 13 | package: 'Zinc-WebSocket-Core' with: [ spec repository: 'github://svenvc/zinc' ]; 14 | package: 'LiveWeb-Core' with: [ spec requires: #('Zinc-WebSocket-Core') ]; 15 | package: 'LiveWeb-Styling' with: [ spec requires: #('LiveWeb-Core') ]; 16 | package: 'LiveWeb-Core-Tests' with: [ spec requires: #('LiveWeb-Core') ]; 17 | package: 'LiveWeb-Styling-Tests' with: [ spec requires: #('LiveWeb-Core' 'LiveWeb-Styling') ]; 18 | package: 'LiveWeb-Bulma' with: [ spec requires: #('LiveWeb-Core') ]; 19 | package: 'LiveWeb-Examples' with: [ spec requires: #('LiveWeb-Core' 'LiveWeb-Styling' 'LiveWeb-Forms' 'LiveWeb-Bulma' 'LiveWeb-ReStore') ]; 20 | package: 'Pows-Core' with: [ spec repository: 'github://tatut/pharo-Pows' ]; 21 | package: 'LiveWeb-Examples-Tests' with: [ spec requires: #('LiveWeb-Examples' 'Pows-Core') ]; 22 | package: 'LiveWeb-Shoelace' with: [ spec requires: #('LiveWeb-Core') ]; 23 | package: 'LiveWeb-Developer' with: [ spec requires: #('LiveWeb-Core' 'LiveWeb-Shoelace') ]; 24 | package: 'LiveWeb-Developer-Tests' with: [ spec requires: #('LiveWeb-Developer') ]; 25 | package: 'LiveWeb-Forms' with: [ spec requires: #('LiveWeb-Core' 'LiveWeb-Styling') ]; 26 | package: 'LiveWeb-ReStore' with: [ spec requires: #('ReStore') ]; 27 | group: 'default' with: #(core tests); 28 | group: 'core' with: #('LiveWeb-Core' 'LiveWeb-Styling'); 29 | group: 'developer' with: #(core 'LiveWeb-Developer'); 30 | group: 'tests' with: #('LiveWeb-Core-Tests' 'LiveWeb-Styling-Tests' 'LiveWeb-Developer-Tests'); 31 | group: 'examples' with: #('core' 'LiveWeb-Examples'); 32 | group: 'fulltests' with: #(core tests 'LiveWeb-Examples' 'LiveWeb-Examples-Tests') 33 | 34 | ] 35 | ] 36 | 37 | { #category : #accessing } 38 | BaselineOfLiveWeb >> reStore: spec [ 39 | spec 40 | baseline: 'ReStore' 41 | with: [ 42 | spec 43 | repository: 'github://rko281/ReStoreForPharo'; 44 | loads: 'Examples' 45 | ] 46 | ] 47 | -------------------------------------------------------------------------------- /src/BaselineOfLiveWeb/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #BaselineOfLiveWeb } 2 | -------------------------------------------------------------------------------- /src/LiveWeb-Bulma/LWBulma.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I provide a convenient method interface to creating many of the 3 | Bulma LiveWeb components. 4 | " 5 | Class { 6 | #name : #LWBulma, 7 | #superclass : #Object, 8 | #category : #'LiveWeb-Bulma' 9 | } 10 | 11 | { #category : #accessing } 12 | LWBulma >> block [ 13 | ^ LWContainer new containerElement: #div; containerAttributes: { #class -> 'block' } 14 | ] 15 | 16 | { #category : #accessing } 17 | LWBulma >> box [ 18 | ^ LWContainer new containerElement: #div; containerAttributes: { #class -> 'box' } 19 | ] 20 | 21 | { #category : #'accessing - structure variables' } 22 | LWBulma >> button [ 23 | ^ LWBulmaButton new 24 | 25 | ] 26 | 27 | { #category : #api } 28 | LWBulma >> columns [ 29 | "Create a Bulma columns container" 30 | ^ LWBulmaColumns new 31 | ] 32 | 33 | { #category : #'as yet unclassified' } 34 | LWBulma >> flexCol [ 35 | ^ LWContainer new containerElement: #div; containerAttributes: { #class -> 'is-flex is-flex-direction-column' }; yourself 36 | 37 | ] 38 | 39 | { #category : #'as yet unclassified' } 40 | LWBulma >> flexRow [ 41 | ^ LWContainer new containerElement: #div; containerAttributes: { #class -> 'is-flex is-flex-direction-row' }; yourself 42 | 43 | ] 44 | 45 | { #category : #accessing } 46 | LWBulma >> tabs [ 47 | ^ LWBulmaTabs new 48 | ] 49 | 50 | { #category : #'as yet unclassified' } 51 | LWBulma >> tile [ 52 | ^ LWBulmaTile new 53 | ] 54 | -------------------------------------------------------------------------------- /src/LiveWeb-Bulma/LWBulmaBlock.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #LWBulmaBlock, 3 | #superclass : #LWContainer, 4 | #category : #'LiveWeb-Bulma' 5 | } 6 | 7 | { #category : #initialization } 8 | LWBulmaBlock >> initialize [ 9 | super initialize. 10 | containerElement := #div. 11 | containerAttributes := { #class -> 'block' } 12 | ] 13 | -------------------------------------------------------------------------------- /src/LiveWeb-Bulma/LWBulmaBox.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #LWBulmaBox, 3 | #superclass : #LWContainer, 4 | #category : #'LiveWeb-Bulma' 5 | } 6 | 7 | { #category : #initialization } 8 | LWBulmaBox >> initialize [ 9 | super initialize. 10 | containerElement := #div. 11 | containerAttributes := { #class -> 'box' } 12 | ] 13 | -------------------------------------------------------------------------------- /src/LiveWeb-Bulma/LWBulmaButton.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am a Bulma button component. 3 | 4 | I have content and an onclick action. 5 | My look can be tweaked with the styling methods like #bePrimary and #beOutlined. 6 | 7 | " 8 | Class { 9 | #name : #LWBulmaButton, 10 | #superclass : #LWContainer, 11 | #instVars : [ 12 | 'styles', 13 | 'onclick' 14 | ], 15 | #category : #'LiveWeb-Bulma' 16 | } 17 | 18 | { #category : #styling } 19 | LWBulmaButton >> beBlack [ 20 | self style: 'is-black' 21 | ] 22 | 23 | { #category : #styling } 24 | LWBulmaButton >> beDanger [ 25 | self style: 'is-danger' 26 | ] 27 | 28 | { #category : #styling } 29 | LWBulmaButton >> beDark [ 30 | self style: 'is-dark' 31 | ] 32 | 33 | { #category : #styling } 34 | LWBulmaButton >> beFullWidth [ 35 | self style: 'is-fullwidth' 36 | ] 37 | 38 | { #category : #styling } 39 | LWBulmaButton >> beGhost [ 40 | self style: 'is-ghost' 41 | ] 42 | 43 | { #category : #styling } 44 | LWBulmaButton >> beInfo [ 45 | self style: 'is-info' 46 | ] 47 | 48 | { #category : #styling } 49 | LWBulmaButton >> beInverted [ 50 | self style: 'is-inverted' 51 | ] 52 | 53 | { #category : #styling } 54 | LWBulmaButton >> beLarge [ 55 | self style: 'is-large'. 56 | ] 57 | 58 | { #category : #styling } 59 | LWBulmaButton >> beLight [ 60 | self style: 'is-light' 61 | ] 62 | 63 | { #category : #styling } 64 | LWBulmaButton >> beLink [ 65 | self style: 'is-link' 66 | ] 67 | 68 | { #category : #styling } 69 | LWBulmaButton >> beLoading [ 70 | self style: 'is-loading' 71 | ] 72 | 73 | { #category : #styling } 74 | LWBulmaButton >> beMedium [ 75 | self style: 'is-medium'. 76 | ] 77 | 78 | { #category : #styling } 79 | LWBulmaButton >> beNormal [ 80 | self style: 'is-normal'. 81 | ] 82 | 83 | { #category : #styling } 84 | LWBulmaButton >> beOutlined [ 85 | self style: 'is-outlined' 86 | ] 87 | 88 | { #category : #styling } 89 | LWBulmaButton >> bePrimary [ 90 | self style: 'is-primary' 91 | ] 92 | 93 | { #category : #styling } 94 | LWBulmaButton >> beRounded [ 95 | self style: 'is-rounded' 96 | ] 97 | 98 | { #category : #styling } 99 | LWBulmaButton >> beSmall [ 100 | self style: 'is-small'. 101 | ] 102 | 103 | { #category : #styling } 104 | LWBulmaButton >> beStatic [ 105 | self style: 'is-static' 106 | ] 107 | 108 | { #category : #styling } 109 | LWBulmaButton >> beSuccess [ 110 | self style: 'is-success' 111 | ] 112 | 113 | { #category : #styling } 114 | LWBulmaButton >> beText [ 115 | self style: 'is-text' 116 | ] 117 | 118 | { #category : #styling } 119 | LWBulmaButton >> beWarning [ 120 | self style: 'is-warning' 121 | ] 122 | 123 | { #category : #styling } 124 | LWBulmaButton >> beWhite [ 125 | self style: 'is-white' 126 | ] 127 | 128 | { #category : #initialization } 129 | LWBulmaButton >> initialize [ 130 | super initialize. 131 | styles := #('button') asOrderedCollection 132 | ] 133 | 134 | { #category : #accessing } 135 | LWBulmaButton >> onclick: aBlock [ 136 | onclick := aBlock. 137 | ] 138 | 139 | { #category : #rendering } 140 | LWBulmaButton >> renderOn: h [ 141 | h button: { #onclick -> onclick. 142 | #class -> (' ' join: styles) } 143 | with: [ self renderChildren: h ] 144 | 145 | ] 146 | 147 | { #category : #accessing } 148 | LWBulmaButton >> style: styleToAdd [ 149 | styles add: styleToAdd 150 | ] 151 | 152 | { #category : #styling } 153 | LWBulmaButton >> toggleLoading [ 154 | (styles includes: 'is-loading') 155 | ifTrue: [ styles remove: 'is-loading' ] 156 | ifFalse: [ styles add: 'is-loading' ] 157 | ] 158 | -------------------------------------------------------------------------------- /src/LiveWeb-Bulma/LWBulmaColumns.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am a Bulma columns layout, add components to me using the methods 3 | like #oneFifth: etc or a size directly using #add:sized:. 4 | 5 | See https://bulma.io/documentation/columns/sizes/ 6 | 7 | " 8 | Class { 9 | #name : #LWBulmaColumns, 10 | #superclass : #LWComponent, 11 | #instVars : [ 12 | 'columns' 13 | ], 14 | #category : #'LiveWeb-Bulma' 15 | } 16 | 17 | { #category : #'as yet unclassified' } 18 | LWBulmaColumns >> add: aComponent sized: size [ 19 | "Add any Bulma size definition, like 12 column " 20 | columns add: aComponent -> ('is-', size asString) 21 | ] 22 | 23 | { #category : #'as yet unclassified' } 24 | LWBulmaColumns >> auto: aComponent [ 25 | "Add an auto-sized column component" 26 | columns add: aComponent -> ''. 27 | ] 28 | 29 | { #category : #accessing } 30 | LWBulmaColumns >> children [ 31 | ^ ReadStream on: (columns collect: #key) 32 | 33 | ] 34 | 35 | { #category : #'as yet unclassified' } 36 | LWBulmaColumns >> fourFifths: aComponent [ 37 | "Add component that takes four fifths of space" 38 | columns add: aComponent -> 'is-four-fifths' 39 | ] 40 | 41 | { #category : #'as yet unclassified' } 42 | LWBulmaColumns >> full: aComponent [ 43 | "Add component that takes all space" 44 | columns add: aComponent -> 'is-full' 45 | ] 46 | 47 | { #category : #'as yet unclassified' } 48 | LWBulmaColumns >> half: aComponent [ 49 | columns add: aComponent -> 'is-half' 50 | ] 51 | 52 | { #category : #initialization } 53 | LWBulmaColumns >> initialize [ 54 | super initialize. 55 | columns := OrderedCollection new. 56 | ] 57 | 58 | { #category : #'as yet unclassified' } 59 | LWBulmaColumns >> oneFifth: aComponent [ 60 | "Add component that takes one fifth of space" 61 | columns add: aComponent -> 'is-one-fifth' 62 | ] 63 | 64 | { #category : #'as yet unclassified' } 65 | LWBulmaColumns >> oneQuarter: aComponent [ 66 | columns add: aComponent -> 'is-one-quarter' 67 | ] 68 | 69 | { #category : #rendering } 70 | LWBulmaColumns >> renderOn: h [ 71 | h div: { #class -> 'columns' } with: [ 72 | columns do: [ :c | 73 | h div: { #class -> ('column {1}' format: {c value}) } with: [ c key render: h ] 74 | ] 75 | ] 76 | ] 77 | 78 | { #category : #'as yet unclassified' } 79 | LWBulmaColumns >> threeFifths: aComponent [ 80 | "Add component that takes four fifths of space" 81 | columns add: aComponent -> 'is-three-fifths' 82 | ] 83 | 84 | { #category : #'as yet unclassified' } 85 | LWBulmaColumns >> threeQuarters: aComponent [ 86 | columns add: aComponent -> 'is-three-quarters' 87 | ] 88 | 89 | { #category : #'as yet unclassified' } 90 | LWBulmaColumns >> twoFifths: aComponent [ 91 | "Add component that takes four fifths of space" 92 | columns add: aComponent -> 'is-two-fifths' 93 | ] 94 | 95 | { #category : #'as yet unclassified' } 96 | LWBulmaColumns >> twoThirds: aComponent [ 97 | columns add: aComponent -> 'is-two-thirds' 98 | ] 99 | -------------------------------------------------------------------------------- /src/LiveWeb-Bulma/LWBulmaComponent.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am a base class for components that build from Bulma components. 3 | Instead of the raw HTML rendering #renderOn:, subclasses will override 4 | the method #view: which is given an LWBulma builder instance. 5 | 6 | This makes it easy to build components in a fluid way without needing 7 | raw HTML. 8 | " 9 | Class { 10 | #name : #LWBulmaComponent, 11 | #superclass : #LWComponent, 12 | #instVars : [ 13 | 'root' 14 | ], 15 | #category : #'LiveWeb-Bulma' 16 | } 17 | 18 | { #category : #accessing } 19 | LWBulmaComponent >> children [ 20 | ^ Generator on: [ :g | root ifNotNil: [ g yield: root ] ]. 21 | 22 | ] 23 | 24 | { #category : #'component lifecycle' } 25 | LWBulmaComponent >> mount [ 26 | root := self view: LWBulma new 27 | ] 28 | 29 | { #category : #rendering } 30 | LWBulmaComponent >> renderOn: h [ 31 | root render: h 32 | ] 33 | 34 | { #category : #accessing } 35 | LWBulmaComponent >> view: anLWBulma [ 36 | "Build the root view of this component using the given builder." 37 | self subclassResponsibility 38 | ] 39 | -------------------------------------------------------------------------------- /src/LiveWeb-Bulma/LWBulmaPage.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am an LWPage that includes Bulma CSS in the header. 3 | " 4 | Class { 5 | #name : #LWBulmaPage, 6 | #superclass : #LWPage, 7 | #category : #'LiveWeb-Bulma' 8 | } 9 | 10 | { #category : #'as yet unclassified' } 11 | LWBulmaPage >> bulmaURL [ 12 | ^ 'https://cdn.jsdelivr.net/npm/bulma@0.9.4/css/bulma.min.css' 13 | 14 | ] 15 | 16 | { #category : #'API - accessing' } 17 | LWBulmaPage >> head: _args [ 18 | ^ LWBlockContainer new block: [ :h | 19 | h link: { #rel -> 'stylesheet'. #href -> self bulmaURL }; 20 | meta: { #name -> 'viewport'. #content -> 'width=device-width, initial-scale=1' } 21 | ] 22 | ] 23 | -------------------------------------------------------------------------------- /src/LiveWeb-Bulma/LWBulmaTabs.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #LWBulmaTabs, 3 | #superclass : #LWComponent, 4 | #instVars : [ 5 | 'tabs', 6 | 'styles', 7 | 'active', 8 | 'onchange' 9 | ], 10 | #category : #'LiveWeb-Bulma' 11 | } 12 | 13 | { #category : #convenience } 14 | LWBulmaTabs >> activate: tab [ 15 | active := tab value. 16 | onchange value: tab value. 17 | self changed. 18 | ] 19 | 20 | { #category : #accessing } 21 | LWBulmaTabs >> active: activeTabValue [ 22 | active := activeTabValue 23 | ] 24 | 25 | { #category : #'as yet unclassified' } 26 | LWBulmaTabs >> beCentered [ 27 | styles add: 'is-centered' 28 | ] 29 | 30 | { #category : #styling } 31 | LWBulmaTabs >> beLarge [ 32 | styles add: 'is-large' 33 | ] 34 | 35 | { #category : #styling } 36 | LWBulmaTabs >> beMedium [ 37 | styles add: 'is-medium' 38 | ] 39 | 40 | { #category : #'as yet unclassified' } 41 | LWBulmaTabs >> beRight [ 42 | styles add: 'is-right' 43 | ] 44 | 45 | { #category : #styling } 46 | LWBulmaTabs >> beSmall [ 47 | styles add: 'is-small' 48 | ] 49 | 50 | { #category : #initialization } 51 | LWBulmaTabs >> initialize [ 52 | super initialize. 53 | styles := #('tabs') asOrderedCollection. 54 | tabs := OrderedCollection new. 55 | ] 56 | 57 | { #category : #accessing } 58 | LWBulmaTabs >> onchange: aBlock [ 59 | "Set the block to call with the selected tab component." 60 | onchange := aBlock 61 | ] 62 | 63 | { #category : #rendering } 64 | LWBulmaTabs >> renderActiveTab: h [ 65 | | tab | 66 | tab := tabs detect: [ :t | t key = active ]. 67 | tab render: h 68 | ] 69 | 70 | { #category : #rendering } 71 | LWBulmaTabs >> renderOn: h [ 72 | h div: [ 73 | "Render tab bar" 74 | self renderTabbar: h. 75 | "renderActiveTab: h" 76 | ] 77 | ] 78 | 79 | { #category : #rendering } 80 | LWBulmaTabs >> renderTabbar: h [ 81 | ^ h div: { (#class -> (' ' join: styles)) } with: [ 82 | h ul: [ 83 | tabs do: [ :tab | 84 | h 85 | li: { (#class -> (tab value = active 86 | ifTrue: [ 'is-active' ] 87 | ifFalse: [ '' ])). 88 | #onclick -> [ self activate: tab ] } 89 | with: [ h a: tab key ] ] ] ] 90 | ] 91 | 92 | { #category : #accessing } 93 | LWBulmaTabs >> tab: nameToValueAssociation [ 94 | tabs add: nameToValueAssociation. 95 | active ifNil: [ active := nameToValueAssociation key ]. 96 | self changed 97 | ] 98 | -------------------------------------------------------------------------------- /src/LiveWeb-Bulma/LWBulmaTile.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #LWBulmaTile, 3 | #superclass : #LWContainer, 4 | #instVars : [ 5 | 'styles' 6 | ], 7 | #category : #'LiveWeb-Bulma' 8 | } 9 | 10 | { #category : #'as yet unclassified' } 11 | LWBulmaTile >> beAncestor [ 12 | styles add: 'is-ancestor' 13 | ] 14 | 15 | { #category : #'as yet unclassified' } 16 | LWBulmaTile >> beChild [ 17 | styles add: 'is-child' 18 | ] 19 | 20 | { #category : #'as yet unclassified' } 21 | LWBulmaTile >> beVertical [ 22 | styles add: 'is-vertical' 23 | ] 24 | 25 | { #category : #initialization } 26 | LWBulmaTile >> initialize [ 27 | super initialize. 28 | styles := #('tile') asOrderedCollection. 29 | ] 30 | 31 | { #category : #rendering } 32 | LWBulmaTile >> renderOn: h [ 33 | h div: { #class -> (' ' join: styles) } with: [ self renderChildren: h ] 34 | ] 35 | 36 | { #category : #'as yet unclassified' } 37 | LWBulmaTile >> size: size [ 38 | self assert: [ size >= 1 & size <= 12 ]. 39 | styles add: ('is-{1}' format: { size }) 40 | ] 41 | 42 | { #category : #accessing } 43 | LWBulmaTile >> style: style [ 44 | styles add: style. 45 | 46 | ] 47 | -------------------------------------------------------------------------------- /src/LiveWeb-Bulma/ManifestLiveWebBulma.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I contain LiveWeb components that use the Bulma CSS framework. 3 | 4 | Bulma provides a nice look and feel without needing to do styles. 5 | " 6 | Class { 7 | #name : #ManifestLiveWebBulma, 8 | #superclass : #PackageManifest, 9 | #category : #'LiveWeb-Bulma-Manifest' 10 | } 11 | -------------------------------------------------------------------------------- /src/LiveWeb-Bulma/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #'LiveWeb-Bulma' } 2 | -------------------------------------------------------------------------------- /src/LiveWeb-Core-Tests/HTMLRendererTest.class.st: -------------------------------------------------------------------------------- 1 | " 2 | A HTMLRendererTest is a test class for testing the behavior of HTMLRenderer 3 | " 4 | Class { 5 | #name : #HTMLRendererTest, 6 | #superclass : #TestCase, 7 | #category : #'LiveWeb-Core-Tests' 8 | } 9 | 10 | { #category : #'instance creation' } 11 | HTMLRendererTest >> html: block [ 12 | ^ String streamContents: [:out | block value: (HTMLRenderer on: out) ] 13 | ] 14 | 15 | { #category : #tests } 16 | HTMLRendererTest >> testAttributes [ 17 | "attributes can be an array of associations" 18 | self assert: (self html: [:h | 19 | h div: { #id -> 'foo' . #style -> 'display: none;' } 20 | with: 'content' 21 | ]) 22 | equals: ''. 23 | 24 | "or a dictionary" 25 | self assert: (self html: [:h | 26 | h div: ({ #class -> 'foo' } asDictionary) with: 'content' 27 | ]) 28 | equals: '
content
' 29 | ] 30 | 31 | { #category : #tests } 32 | HTMLRendererTest >> testBooleanAttribute [ 33 | self assert: (self html: [:h | h div: [ h input: { #checked->true }; input: { #checked->false } ]]) 34 | equals: '
' 35 | ] 36 | 37 | { #category : #tests } 38 | HTMLRendererTest >> testComponentId [ 39 | "content block without arguments (use the same renderer)" 40 | self assert: (self html: [:h | 41 | h id: 42. 42 | h div: 'hello' 43 | ]) 44 | equals: '
hello
'. 45 | 46 | self assert: (self html: [:h | 47 | h id: 123. 48 | h div: [ h div: 'inner' ]]) 49 | equals: '
inner
'. 50 | 51 | self assert: (self html: [:h | 52 | h id: 0. 53 | h div: { #id -> 'foobar' } with: 'content']) 54 | equals: '
content
'. 55 | 56 | ] 57 | 58 | { #category : #tests } 59 | HTMLRendererTest >> testContentBlock [ 60 | "content block without arguments (use the same renderer)" 61 | self assert: (self html: [:h | 62 | h ul: [ 63 | 1 to: 3 do: [ :i | 64 | h li: { #class -> (i even ifTrue: 'even' ifFalse: 'odd') } 65 | with: i 66 | ]] 67 | ]) 68 | equals: ''. 69 | 70 | "content block with 1 argument (the renderer)" 71 | self assert: (self html: [:h | 72 | h div: [ :r | r a: { #href -> 'http://example.com' } with: 'click' ] 73 | ]) 74 | equals: '
click
' 75 | ] 76 | 77 | { #category : #tests } 78 | HTMLRendererTest >> testEmptyTags [ 79 | self assert: (self html: [:h | 80 | h div: 'Hello'. 81 | h hr. 82 | h div: 'world' 83 | ]) 84 | equals: '
Hello

world
' 85 | ] 86 | 87 | { #category : #tests } 88 | HTMLRendererTest >> testEscapeHtml [ 89 | | esc | 90 | esc := [ :txt | HTMLRenderer new escapeHtml: txt ]. 91 | self assert: (esc value: '') 92 | equals: '<script>alert("evil")</script>' 93 | ] 94 | 95 | { #category : #tests } 96 | HTMLRendererTest >> testOnlyAttrs [ 97 | self assert: (self html: [:h | 98 | h div: { #id->'hello' } 99 | ]) 100 | equals: '
' 101 | ] 102 | 103 | { #category : #tests } 104 | HTMLRendererTest >> testSimpleHtmlContent [ 105 | self assert: (self html: [:h | 106 | h div: 'Hello' 107 | ]) 108 | equals: '
Hello
' 109 | ] 110 | -------------------------------------------------------------------------------- /src/LiveWeb-Core-Tests/LWComponentTest.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am the base class for component tests. I setup a mock context that can 3 | be used to render components as strings. 4 | " 5 | Class { 6 | #name : #LWComponentTest, 7 | #superclass : #TestCase, 8 | #instVars : [ 9 | 'ctx', 10 | 'component' 11 | ], 12 | #category : #'LiveWeb-Core-Tests' 13 | } 14 | 15 | { #category : #accessing } 16 | LWComponentTest >> patches [ 17 | ^ self patches: component 18 | ] 19 | 20 | { #category : #accessing } 21 | LWComponentTest >> patches: forComponent [ 22 | ^ctx patches at: forComponent id 23 | ] 24 | 25 | { #category : #rendering } 26 | LWComponentTest >> render: c [ 27 | "Render component to string" 28 | c inContext: ctx. 29 | component := c. 30 | ^ String streamContents: [ :out | 31 | c render: (HTMLRenderer on: out) 32 | ] 33 | 34 | ] 35 | 36 | { #category : #running } 37 | LWComponentTest >> setUp [ 38 | super setUp. 39 | ctx := LWMockContext new. 40 | ] 41 | -------------------------------------------------------------------------------- /src/LiveWeb-Core-Tests/LWContainerTest.class.st: -------------------------------------------------------------------------------- 1 | " 2 | A LWContainerTest is a test class for testing the behavior of LWContainer 3 | " 4 | Class { 5 | #name : #LWContainerTest, 6 | #superclass : #LWComponentTest, 7 | #category : #'LiveWeb-Core-Tests' 8 | } 9 | 10 | { #category : #tests } 11 | LWContainerTest >> testAddAttribute [ 12 | | c | 13 | c := LWContainer new containerElement: #div; containerAttributes: { #class -> 'foo' }. 14 | c attr: #style -> 'display: flex;'. 15 | self assert: (self render: c) equals: '
' 16 | 17 | ] 18 | 19 | { #category : #tests } 20 | LWContainerTest >> testMountedAttributeChange [ 21 | self assert: (self render: LWContainer new) equals: ''. 22 | component attr: 'data-foo' -> 420. 23 | self assert: self patches last equals: '@' -> { 'data-foo' -> 420 } asDictionary 24 | ] 25 | 26 | { #category : #tests } 27 | LWContainerTest >> testUpdateAttribute [ 28 | | c | 29 | c := LWContainer new containerElement: #div; containerAttributes: { #class -> 'foo' }. 30 | c attr: #class update: [ :foo | foo, ' bar' ]. 31 | self assert: (self render: c) equals: '
' 32 | ] 33 | -------------------------------------------------------------------------------- /src/LiveWeb-Core-Tests/LWMockContext.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #LWMockContext, 3 | #superclass : #Object, 4 | #instVars : [ 5 | 'nextId', 6 | 'patches' 7 | ], 8 | #category : #'LiveWeb-Core-Tests' 9 | } 10 | 11 | { #category : #adding } 12 | LWMockContext >> addPatchesTo: addable for: componentId [ 13 | "Set object to add patches to for the given component id. 14 | Addable must be an object that undestands #add: message". 15 | self patches at: componentId put: addable 16 | ] 17 | 18 | { #category : #accessing } 19 | LWMockContext >> patches [ 20 | patches ifNil: [ patches := Dictionary new ]. 21 | ^ patches 22 | ] 23 | 24 | { #category : #registering } 25 | LWMockContext >> registerComponent: c [ 26 | | id | 27 | nextId ifNil: [ nextId := 0 ]. 28 | id := nextId. 29 | nextId := nextId + 1. 30 | self addPatchesTo: OrderedCollection new for: id. 31 | ^ id 32 | ] 33 | 34 | { #category : #'client control' } 35 | LWMockContext >> send: type for: componentId with: content [ 36 | (self patches at: componentId ifAbsent: [ ^ self ]) 37 | add: type -> content 38 | ] 39 | -------------------------------------------------------------------------------- /src/LiveWeb-Core-Tests/LWTemplateTest.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #LWTemplateTest, 3 | #superclass : #TestCase, 4 | #category : #'LiveWeb-Core-Tests' 5 | } 6 | 7 | { #category : #tests } 8 | LWTemplateTest >> testTemplate [ 9 | LWTemplate new template: [ :h | h div: [ h templateSlot: #first ] ]. 10 | 11 | ] 12 | -------------------------------------------------------------------------------- /src/LiveWeb-Core-Tests/package.st: -------------------------------------------------------------------------------- 1 | Package { #name : #'LiveWeb-Core-Tests' } 2 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/BlockClosure.extension.st: -------------------------------------------------------------------------------- 1 | Extension { #name : #BlockClosure } 2 | 3 | { #category : #'*LiveWeb-Core' } 4 | BlockClosure >> asLWComponent [ 5 | ^ LWBlockContainer new block: self 6 | ] 7 | 8 | { #category : #'*LiveWeb-Core' } 9 | BlockClosure >> asLWScriptCallback [ 10 | ^ LWScriptCallback new callback: self; jsParams: #() 11 | ] 12 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/Choice.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #Choice, 3 | #superclass : #Dictionary, 4 | #category : #'LiveWeb-Core' 5 | } 6 | 7 | { #category : #convenience } 8 | Choice >> ? of [ 9 | ^ self at: of 10 | ] 11 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/Dictionary.extension.st: -------------------------------------------------------------------------------- 1 | Extension { #name : #Dictionary } 2 | 3 | { #category : #'*LiveWeb-Core' } 4 | Dictionary >> asPushStateJSON [ 5 | ^ self 6 | 7 | ] 8 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/FileStream.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am required by ZincHTTPComponents to be able to load 3 | " 4 | Class { 5 | #name : #FileStream, 6 | #superclass : #Object, 7 | #category : #'LiveWeb-Core' 8 | } 9 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWAttributeRenderer.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am the base class for hooks that change how attributes are rendered. 3 | " 4 | Class { 5 | #name : #LWAttributeRenderer, 6 | #superclass : #Object, 7 | #instVars : [ 8 | 'parent' 9 | ], 10 | #category : #'LiveWeb-Core' 11 | } 12 | 13 | { #category : #accessing } 14 | LWAttributeRenderer >> parent: anObject [ 15 | 16 | parent := anObject 17 | ] 18 | 19 | { #category : #rendering } 20 | LWAttributeRenderer >> render: attributeEntry [ 21 | "Render an attribute value to string, may do some special handling. 22 | Returns a new association with value processed. 23 | Default implementation just returns attribute mapping as is." 24 | ^ attributeEntry 25 | ] 26 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWBlockContainer.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am a component that renders a single block as a component. 3 | This is convenient to simply swap some content by setting the block. 4 | 5 | The block being rendered must output one root element which will consume 6 | the component id. 7 | 8 | If the block is nil, a placeholder script will be output. 9 | " 10 | Class { 11 | #name : #LWBlockContainer, 12 | #superclass : #LWComponent, 13 | #instVars : [ 14 | 'block' 15 | ], 16 | #category : #'LiveWeb-Core' 17 | } 18 | 19 | { #category : #accessing } 20 | LWBlockContainer >> block: aBlock [ 21 | block := aBlock. 22 | self changed. 23 | ] 24 | 25 | { #category : #rendering } 26 | LWBlockContainer >> renderOn: h [ 27 | block 28 | ifNil: [ h script: { #type -> 'liveweb/placeholder' } with: '' ] 29 | ifNotNil: [ block value: h ]. 30 | ] 31 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWContainer.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am an component that manages an ordered collection of child components. 3 | " 4 | Class { 5 | #name : #LWContainer, 6 | #superclass : #LWComponent, 7 | #instVars : [ 8 | 'children', 9 | 'containerElement', 10 | 'containerAttributes' 11 | ], 12 | #category : #'LiveWeb-Core' 13 | } 14 | 15 | { #category : #'as yet unclassified' } 16 | LWContainer class >> flex: direction [ 17 | "Convenience to create a div with flex direction." 18 | ^ self new 19 | containerElement: #div; 20 | containerAttributes: { #style -> ('display: flex; flex-direction: ', direction asString) } 21 | 22 | ] 23 | 24 | { #category : #'instance creation' } 25 | LWContainer class >> with: componentArray [ 26 | | c | 27 | c := self new. 28 | componentArray do: [ :child | c add: child ]. 29 | ^ c 30 | ] 31 | 32 | { #category : #adding } 33 | LWContainer >> add: aChildComponent [ 34 | ^ self add: aChildComponent beforeIndex: children size + 1 35 | ] 36 | 37 | { #category : #adding } 38 | LWContainer >> add: aChildComponent beforeIndex: idx [ 39 | "add child component to this component" 40 | children ifNil: [ children := OrderedCollection new ]. 41 | ctx ifNotNil: [ aChildComponent inContext: ctx ]. 42 | children add: aChildComponent beforeIndex: idx. 43 | mounted ifTrue: [ ctx component: self childAdded: aChildComponent at: idx ]. 44 | ^ aChildComponent 45 | ] 46 | 47 | { #category : #'as yet unclassified' } 48 | LWContainer >> attr: nameValueAssociation [ 49 | "Set the container attribute. Overwriting it if it exists." 50 | self attr: nameValueAssociation key update: [ nameValueAssociation value ] 51 | ] 52 | 53 | { #category : #'as yet unclassified' } 54 | LWContainer >> attr: name update: block [ 55 | "Update attribute container attribute. Creating it if it does not exist. 56 | If the attribute doesn't exist, the update block is called with an empty string. 57 | If the component is mounted, the new attribute will be patched in." 58 | | attrs newVal | 59 | attrs := containerAttributes asOrderedCollection. 60 | attrs detect: [ :a | a name = name ] 61 | ifFound: [ :assoc | newVal := (block cull: assoc value). assoc value: newVal ] 62 | ifNone: [ newVal := (block cull: ''). attrs add: name -> newVal ]. 63 | containerAttributes := attrs. 64 | self send: '@' with: { name -> newVal } asDictionary. 65 | 66 | ] 67 | 68 | { #category : #accessing } 69 | LWContainer >> childCount [ 70 | ^ children ifNil: 0 ifNotNil: [ children size ] 71 | ] 72 | 73 | { #category : #accessing } 74 | LWContainer >> children [ 75 | ^ ReadStream on: children 76 | ] 77 | 78 | { #category : #enumerating } 79 | LWContainer >> childrenDo: aBlock [ 80 | "run block with each child" 81 | children ifNotNil: [ 82 | children do: [ :child | aBlock value: child ]]. 83 | 84 | ] 85 | 86 | { #category : #accessing } 87 | LWContainer >> containerAttributes: anObject [ 88 | 89 | containerAttributes := anObject 90 | ] 91 | 92 | { #category : #accessing } 93 | LWContainer >> containerElement: anObject [ 94 | 95 | containerElement := anObject 96 | ] 97 | 98 | { #category : #initialization } 99 | LWContainer >> initialize [ 100 | super initialize. 101 | children := OrderedCollection new. 102 | containerElement := #span. 103 | containerAttributes := #(). 104 | 105 | ] 106 | 107 | { #category : #rendering } 108 | LWContainer >> renderChildren: h [ 109 | self children do: [ :c | c render: h ] 110 | ] 111 | 112 | { #category : #rendering } 113 | LWContainer >> renderOn: h [ 114 | h render: containerElement 115 | attrs: containerAttributes 116 | with: [ self renderChildren: h ] 117 | ] 118 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWCustomElementSlot.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #LWCustomElementSlot, 3 | #superclass : #LWSingleContainer, 4 | #instVars : [ 5 | 'slot' 6 | ], 7 | #category : #'LiveWeb-Core' 8 | } 9 | 10 | { #category : #initialization } 11 | LWCustomElementSlot >> initialize [ 12 | super initialize. 13 | containerElement := #div. 14 | ] 15 | 16 | { #category : #accessing } 17 | LWCustomElementSlot >> slot: slotName [ 18 | containerAttributes := { #slot -> slotName }. 19 | 20 | ] 21 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWCustomEvent.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am an event type that can be listened to on a LWCustomElement. 3 | 4 | Subclass me to specialize the event type and JavaScript objects. 5 | " 6 | Class { 7 | #name : #LWCustomEvent, 8 | #superclass : #SmallDictionary, 9 | #category : #'LiveWeb-Core' 10 | } 11 | 12 | { #category : #serialization } 13 | LWCustomEvent class >> eventFields [ 14 | "Return an array of associations mapping instance variables to JS code needed to extract the value from event. 15 | 16 | for example: ^ { #value -> 'e.target.value' } 17 | " 18 | self subclassResponsibility 19 | ] 20 | 21 | { #category : #'instance creation' } 22 | LWCustomEvent class >> fromArray: arr [ 23 | | e | 24 | e := self new. 25 | self eventFields doWithIndex: [ :f :i | 26 | e at: f key put: (arr at: i) 27 | ]. 28 | ^ e 29 | ] 30 | 31 | { #category : #accessing } 32 | LWCustomEvent class >> type [ 33 | "Return the JS event type to add listeners for. For example 'change'" 34 | self subclassResponsibility 35 | ] 36 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWExportJS.class.st: -------------------------------------------------------------------------------- 1 | Class { 2 | #name : #LWExportJS, 3 | #superclass : #LWComponent, 4 | #instVars : [ 5 | 'export' 6 | ], 7 | #category : #'LiveWeb-Core' 8 | } 9 | 10 | { #category : #accessing } 11 | LWExportJS >> export: nameToCallbackAssociation [ 12 | export add: nameToCallbackAssociation. 13 | ] 14 | 15 | { #category : #initialization } 16 | LWExportJS >> initialize [ 17 | super initialize. 18 | export := OrderedCollection new. 19 | ] 20 | 21 | { #category : #rendering } 22 | LWExportJS >> renderOn: h [ 23 | h script: [ 24 | export do: [ :e | 25 | | name cb cbId argNames | 26 | name := e key. 27 | cb := e value. 28 | argNames := ',' join: ((1 to: cb numArgs) collect: [ :i | 'a{1}' format: { i } ]). 29 | cbId := ctx registerCallback: cb for: self. 30 | h raw: 'function '; raw: name asString; raw: '('; raw: argNames; 31 | raw: '){_lws('; raw: cbId asString; raw: ',['; raw: argNames; raw: '])} '. 32 | ] 33 | ] 34 | ] 35 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWFragmentContainer.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am a raw fragment container that has no parent element. 3 | It just renders all children as is. 4 | 5 | This component cannot be rerendered, only the individual children can. 6 | " 7 | Class { 8 | #name : #LWFragmentContainer, 9 | #superclass : #LWContainer, 10 | #instVars : [ 11 | 'rendered' 12 | ], 13 | #category : #'LiveWeb-Core' 14 | } 15 | 16 | { #category : #adding } 17 | LWFragmentContainer >> add: aComponentOrString [ 18 | aComponentOrString isString 19 | ifTrue: [ super add: 20 | (LWBlockContainer new block: [:h | h raw: aComponentOrString ]) ] 21 | ifFalse: [ super add: aComponentOrString ]. 22 | ^ aComponentOrString 23 | ] 24 | 25 | { #category : #initialization } 26 | LWFragmentContainer >> initialize [ 27 | super initialize. 28 | rendered := false. 29 | ] 30 | 31 | { #category : #rendering } 32 | LWFragmentContainer >> renderOn: h [ 33 | self assert: [ rendered not ]. 34 | rendered := true. 35 | self renderChildren: h. 36 | ] 37 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWLogEvent.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am a log event for framework events in LiveWeb. 3 | I provide utility methods for sending info, warn or error level messages. 4 | 5 | " 6 | Class { 7 | #name : #LWLogEvent, 8 | #superclass : #ZnLogEvent, 9 | #instVars : [ 10 | 'level', 11 | 'message' 12 | ], 13 | #category : #'LiveWeb-Core' 14 | } 15 | 16 | { #category : #'accessing - structure variables' } 17 | LWLogEvent class >> debug: message [ 18 | self emitLevel: #DEBUG message: message. 19 | ] 20 | 21 | { #category : #'as yet unclassified' } 22 | LWLogEvent class >> emitLevel: level message: message [ 23 | self new level: level; message: message; emit 24 | ] 25 | 26 | { #category : #'accessing - structure variables' } 27 | LWLogEvent class >> error: message [ 28 | self emitLevel: #ERROR message: message. 29 | ] 30 | 31 | { #category : #'accessing - structure variables' } 32 | LWLogEvent class >> info: message [ 33 | self emitLevel: #INFO message: message. 34 | ] 35 | 36 | { #category : #'as yet unclassified' } 37 | LWLogEvent class >> logToStdout [ 38 | self stopLoggingToStdout. 39 | ZnLogEvent announcer when: ZnLogEvent do: [ :e | 40 | | msg | 41 | msg := String streamContents: [:out | e printOn: out]. 42 | Stdio stdout << msg; crlf; flush. 43 | ] 44 | ] 45 | 46 | { #category : #'as yet unclassified' } 47 | LWLogEvent class >> stopLoggingToStdout [ 48 | ZnLogEvent announcer unsubscribe: self. 49 | ] 50 | 51 | { #category : #'accessing - structure variables' } 52 | LWLogEvent class >> warn: message [ 53 | self emitLevel: #WARN message: message. 54 | ] 55 | 56 | { #category : #accessing } 57 | LWLogEvent >> level: anObject [ 58 | 59 | level := anObject 60 | ] 61 | 62 | { #category : #accessing } 63 | LWLogEvent >> message: anObject [ 64 | 65 | message := anObject 66 | ] 67 | 68 | { #category : #printing } 69 | LWLogEvent >> printContentsOn: stream [ 70 | stream 71 | << ' ['; 72 | << level asString; 73 | << '] '; 74 | << message asString. 75 | 76 | ] 77 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWPartsComponent.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am a component that has multiple render methods, which each make up a child component. 3 | This makes it convenient to have separately updatable child components, while keeping the rendering 4 | in the same class. 5 | 6 | When initialized, I will create a dictionary of children that map selector to a newly created 7 | LWBlockContainer component. Each child's render must output a single element. 8 | 9 | Each child component render method must have an pragma attached. 10 | 11 | There is no need to implement children method, it is automatic. 12 | 13 | " 14 | Class { 15 | #name : #LWPartsComponent, 16 | #superclass : #LWComponent, 17 | #instVars : [ 18 | 'parts' 19 | ], 20 | #category : #'LiveWeb-Core' 21 | } 22 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWPushState.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am a component that renders scripts needed to support push state in the browser. 3 | " 4 | Class { 5 | #name : #LWPushState, 6 | #superclass : #LWComponent, 7 | #instVars : [ 8 | 'page' 9 | ], 10 | #classInstVars : [ 11 | 'page' 12 | ], 13 | #category : #'LiveWeb-Core' 14 | } 15 | 16 | { #category : #'instance creation' } 17 | LWPushState class >> on: page [ 18 | ^ self new page: page; yourself 19 | ] 20 | 21 | { #category : #accessing } 22 | LWPushState >> page: aPage [ 23 | page := aPage 24 | ] 25 | 26 | { #category : #rendering } 27 | LWPushState >> renderOn: h [ 28 | | cb initial | 29 | cb := ctx registerCallback: [ :state | page pushStateChanged: state ] for: self. 30 | initial := STONJSON toString: page initialPushState asPushStateJSON. 31 | h script: [ 32 | h raw: ('window.onload=function(){_lw.enablePushState(<1s>, <2s>)}' expandMacrosWith: initial with: cb asString) 33 | ] 34 | ] 35 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWPushStateBase.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am the abstract base class for page state parsed from a push state route. 3 | I can turn the state back into a route (for sending navigation to browser) 4 | and JSON state. 5 | 6 | Subclasses must have instance variables with accessors for all the path 7 | segments. For example the path '/customers/:customerId/orders/:orderId' 8 | must have customerId and orderId instance variables with the regular getter 9 | and setter patterns. 10 | " 11 | Class { 12 | #name : #LWPushStateBase, 13 | #superclass : #Object, 14 | #instVars : [ 15 | 'matchedRoute' 16 | ], 17 | #category : #'LiveWeb-Core' 18 | } 19 | 20 | { #category : #converting } 21 | LWPushStateBase >> asPushStateJSON [ 22 | "return STONable push state" 23 | ^ (matchedRoute fieldSegments 24 | collect: [ :s | s -> (self perform: s) ]) asDictionary 25 | at: '_route' put: matchedRoute id; yourself 26 | 27 | ] 28 | 29 | { #category : #converting } 30 | LWPushStateBase >> asRoute [ 31 | "Answer with the route path" 32 | ^ String streamContents: [ :out | 33 | matchedRoute segments do: [ :seg | 34 | out << $/. 35 | seg isNotEmpty ifTrue: [ 36 | seg first = $: 37 | ifTrue: [ 38 | out << (self perform: (seg allButFirst asSymbol)) asString ] 39 | ifFalse: [ 40 | out << seg ] 41 | ] 42 | ] 43 | ] 44 | ] 45 | 46 | { #category : #converting } 47 | LWPushStateBase >> fromPushStateJSON: aDictionary [ 48 | "read fields from JSON dictionary" 49 | matchedRoute fieldSegments do: [ :s | 50 | | setter | 51 | setter := (s,':') asSymbol. 52 | self perform: setter with: (aDictionary at: s)] 53 | 54 | ] 55 | 56 | { #category : #accessing } 57 | LWPushStateBase >> matchedRoute [ 58 | 59 | ^ matchedRoute 60 | ] 61 | 62 | { #category : #accessing } 63 | LWPushStateBase >> matchedRoute: anObject [ 64 | 65 | matchedRoute := anObject 66 | ] 67 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWPushStateRoute.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am a route definition in a push state router. 3 | I can match against an input route and construct a push state object. 4 | 5 | " 6 | Class { 7 | #name : #LWPushStateRoute, 8 | #superclass : #Object, 9 | #instVars : [ 10 | 'segments', 11 | 'stateClass' 12 | ], 13 | #category : #'LiveWeb-Core' 14 | } 15 | 16 | { #category : #'as yet unclassified' } 17 | LWPushStateRoute >> fieldSegments [ 18 | "answer with accessors for segments that are fields to extract from the path." 19 | ^ segments select: [ :s | 20 | s isEmpty ifTrue: false ifFalse: [ s first = $: ]] 21 | thenCollect: [ :s | s allButFirst asSymbol ] 22 | ] 23 | 24 | { #category : #accessing } 25 | LWPushStateRoute >> id [ 26 | "answer with the id of this route" 27 | ^ ('/' join: segments) hash printStringBase: 64 28 | ] 29 | 30 | { #category : #accessing } 31 | LWPushStateRoute >> match: inputSegments [ 32 | | match | 33 | segments size ~= inputSegments size ifTrue: [ ^ nil ]. 34 | 35 | match := stateClass new. 36 | match matchedRoute: self. 37 | 38 | segments with: inputSegments do: [ :expected :received | 39 | | param | 40 | param := expected isEmpty ifTrue: false ifFalse: [ expected first = $: ]. 41 | param ifTrue: [ 42 | match perform: (expected allButFirst,':') asSymbol with: received ] 43 | ifFalse: [ 44 | expected ~= received ifTrue: [ ^ nil ] ] 45 | ]. 46 | ^ match 47 | 48 | ] 49 | 50 | { #category : #accessing } 51 | LWPushStateRoute >> segments [ 52 | 53 | ^ segments 54 | ] 55 | 56 | { #category : #accessing } 57 | LWPushStateRoute >> segments: anObject [ 58 | 59 | segments := anObject 60 | ] 61 | 62 | { #category : #accessing } 63 | LWPushStateRoute >> stateClass [ 64 | 65 | ^ stateClass 66 | ] 67 | 68 | { #category : #accessing } 69 | LWPushStateRoute >> stateClass: anObject [ 70 | 71 | stateClass := anObject 72 | ] 73 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWPushStateRouter.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I handle push state routing and transform between route and push state. 3 | I can generate state from a route and vice versa. 4 | 5 | " 6 | Class { 7 | #name : #LWPushStateRouter, 8 | #superclass : #Object, 9 | #instVars : [ 10 | 'routes' 11 | ], 12 | #category : #'LiveWeb-Core' 13 | } 14 | 15 | { #category : #'instance creation' } 16 | LWPushStateRouter >> fromPushStateJSON: aDictionary [ 17 | "Parse state from JSON received from the browser. 18 | Matches the route by id." 19 | | id route | 20 | id := aDictionary at: #_route. 21 | route := (routes detect: [ :r | r id = id ]). 22 | ^ route stateClass new 23 | matchedRoute: route; 24 | fromPushStateJSON: aDictionary; yourself 25 | ] 26 | 27 | { #category : #initialization } 28 | LWPushStateRouter >> initialize [ 29 | routes := OrderedCollection new. 30 | ] 31 | 32 | { #category : #'instance creation' } 33 | LWPushStateRouter >> match: aZnUri [ 34 | "Parse state from route. Answer with the parsed state." 35 | | segments | 36 | segments := aZnUri segments 37 | ifNil: {''} 38 | ifNotNil: [ :segs | segs select: [:s | s size > 0 ]]. 39 | routes do: [ :r | 40 | | match | 41 | match := r match: segments. 42 | match ifNotNil: [ ^ match ] 43 | ]. 44 | ^ nil 45 | ] 46 | 47 | { #category : #'instance creation' } 48 | LWPushStateRouter >> matchState: aPushState [ 49 | "Create push state that matches the given push state template. 50 | Considers routes that have the same state class. 51 | If there are multiple candidates, considers the set of non-nil instance variables." 52 | | candidates setVariables route | 53 | candidates := routes select: [ :r | r stateClass = aPushState class ]. 54 | candidates isEmpty ifTrue: [ Error signal: 'No matching route for push state template.' ]. 55 | candidates size = 1 ifTrue: [ ^ aPushState copy matchedRoute: candidates first; yourself ]. 56 | 57 | "we have multiple candidates, need to consider the instance variables" 58 | setVariables := Set new. 59 | aPushState class instanceVariables do: [ :v | (v read: aPushState) ifNotNil: [ setVariables add: v name ] ]. 60 | route := candidates 61 | detect: [ :r | r fieldSegments asSet = setVariables] 62 | ifNone: [ Error signal: 'No matching route candidate for push state template' ]. 63 | ^ aPushState copy matchedRoute: route; yourself 64 | ] 65 | 66 | { #category : #'as yet unclassified' } 67 | LWPushStateRouter >> route: aPathString as: aStateClass [ 68 | routes add: (LWPushStateRoute new 69 | segments: (self routeSegments: aPathString); 70 | stateClass: aStateClass; 71 | yourself) 72 | ] 73 | 74 | { #category : #'as yet unclassified' } 75 | LWPushStateRouter >> routeSegments: aRouteString [ 76 | | segments | 77 | segments := aRouteString splitOn: $/. 78 | segments first = '' ifTrue: [ segments := segments allButFirst ]. 79 | ^ segments 80 | 81 | ] 82 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWScriptCallback.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I represent a callback from browser to server that has arguments that are evaluated on JS side. 3 | An callback can also have a JS evaluated condition. 4 | Or a debounce millisecond. 5 | 6 | 7 | 8 | " 9 | Class { 10 | #name : #LWScriptCallback, 11 | #superclass : #Object, 12 | #instVars : [ 13 | 'callback', 14 | 'condition', 15 | 'jsParams', 16 | 'debounceMs', 17 | 'preventDefault', 18 | 'thenJS' 19 | ], 20 | #category : #'LiveWeb-Core' 21 | } 22 | 23 | { #category : #'as yet unclassified' } 24 | LWScriptCallback >> afterSend: id [ 25 | thenJS ifNil: [ ^ 'null' ]. 26 | 27 | ] 28 | 29 | { #category : #'as yet unclassified' } 30 | LWScriptCallback >> afterSend: js forComponent: id [ 31 | thenJS ifNil: [ ^ nil ]. 32 | js << ',(function(){'; 33 | << thenJS; 34 | << '}).bind(_lw.get("'; 35 | << id asString; 36 | << '"))' 37 | 38 | 39 | ] 40 | 41 | { #category : #converting } 42 | LWScriptCallback >> asJS: cb forComponent: id [ 43 | ^ String streamContents: [ :js | 44 | preventDefault ifTrue: [ js << 'window.event.preventDefault();' ]. 45 | condition ifNotNil: [ js << 'if(!('; << condition; << ')) return;' ]. 46 | js << '_lws('; << cb asString; << ',['; 47 | << (',' join: jsParams); 48 | << '],'; 49 | << (debounceMs ifNil: [ '0' ] ifNotNil: [ debounceMs asString]). 50 | self afterSend: js forComponent: id. 51 | js << ')' ] 52 | ] 53 | 54 | { #category : #converting } 55 | LWScriptCallback >> asLWScriptCallback [ 56 | ^ self 57 | 58 | ] 59 | 60 | { #category : #accessing } 61 | LWScriptCallback >> callback [ 62 | 63 | ^ callback 64 | ] 65 | 66 | { #category : #accessing } 67 | LWScriptCallback >> callback: anObject [ 68 | 69 | callback := anObject 70 | ] 71 | 72 | { #category : #accessing } 73 | LWScriptCallback >> condition [ 74 | 75 | ^ condition 76 | ] 77 | 78 | { #category : #accessing } 79 | LWScriptCallback >> condition: anObject [ 80 | 81 | condition := anObject 82 | ] 83 | 84 | { #category : #accessing } 85 | LWScriptCallback >> debounceMs [ 86 | 87 | ^ debounceMs 88 | ] 89 | 90 | { #category : #accessing } 91 | LWScriptCallback >> debounceMs: anObject [ 92 | 93 | debounceMs := anObject 94 | ] 95 | 96 | { #category : #initialization } 97 | LWScriptCallback >> initialize [ 98 | preventDefault := false. 99 | jsParams := #(). 100 | ] 101 | 102 | { #category : #accessing } 103 | LWScriptCallback >> jsParams [ 104 | 105 | ^ jsParams 106 | ] 107 | 108 | { #category : #accessing } 109 | LWScriptCallback >> jsParams: anObject [ 110 | 111 | jsParams := anObject 112 | ] 113 | 114 | { #category : #accessing } 115 | LWScriptCallback >> preventDefault: prevent [ 116 | preventDefault := prevent 117 | ] 118 | 119 | { #category : #accessing } 120 | LWScriptCallback >> thenJS: aScript [ 121 | "Set JS code to run immediately after sending the callback. 122 | JavaScript 'this' is bound to the current component." 123 | thenJS := aScript 124 | ] 125 | 126 | { #category : #evaluating } 127 | LWScriptCallback >> valueWithArguments: args [ 128 | ^ callback valueWithArguments: args 129 | ] 130 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWSingleContainer.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am a container that contains a single child. This is useful to hold one 3 | selected component to show (for example a selected tab content). 4 | 5 | When a child is set, the current child (if any) is unmounted and cleaned up. 6 | " 7 | Class { 8 | #name : #LWSingleContainer, 9 | #superclass : #LWComponent, 10 | #instVars : [ 11 | 'child', 12 | 'containerElement', 13 | 'containerAttributes' 14 | ], 15 | #category : #'LiveWeb-Core' 16 | } 17 | 18 | { #category : #accessing } 19 | LWSingleContainer >> child [ 20 | ^ child 21 | ] 22 | 23 | { #category : #accessing } 24 | LWSingleContainer >> child: aChildComponent [ 25 | "Set the new child component" 26 | child ifNotNil: [ ctx cleanup: child unmount: true ]. 27 | child := aChildComponent. 28 | ctx ifNotNil: [ child ifNotNil: [ child inContext: ctx ] ]. 29 | self changed. 30 | 31 | ] 32 | 33 | { #category : #accessing } 34 | LWSingleContainer >> children [ 35 | ^ Generator on: [ :yield | 36 | child ifNotNil: [ yield value: child ] 37 | ] 38 | ] 39 | 40 | { #category : #accessing } 41 | LWSingleContainer >> containerAttributes: anObject [ 42 | 43 | containerAttributes := anObject 44 | ] 45 | 46 | { #category : #accessing } 47 | LWSingleContainer >> containerElement: anObject [ 48 | 49 | containerElement := anObject 50 | ] 51 | 52 | { #category : #testing } 53 | LWSingleContainer >> hasChild [ 54 | ^ child isNil not 55 | ] 56 | 57 | { #category : #initialization } 58 | LWSingleContainer >> initialize [ 59 | super initialize. 60 | containerElement := #span. 61 | containerAttributes := Array empty. 62 | ] 63 | 64 | { #category : #rendering } 65 | LWSingleContainer >> renderOn: h [ 66 | child 67 | ifNil: [ h script: { #type -> 'liveweb/placeholder' } with: '' ] 68 | ifNotNil: [ 69 | h render: containerElement attrs: containerAttributes with: [ child render: h ] 70 | ] 71 | ] 72 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWSwitch.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I am a simple switch component with one child. 3 | When the switch is ON, the child is rendered. 4 | When the switch is OFF, an invisible script placeholder is rendered. 5 | " 6 | Class { 7 | #name : #LWSwitch, 8 | #superclass : #LWComponent, 9 | #instVars : [ 10 | 'child', 11 | 'on' 12 | ], 13 | #category : #'LiveWeb-Core' 14 | } 15 | 16 | { #category : #accessing } 17 | LWSwitch >> child [ 18 | 19 | ^ child 20 | ] 21 | 22 | { #category : #accessing } 23 | LWSwitch >> child: anObject [ 24 | 25 | child := anObject 26 | ] 27 | 28 | { #category : #accessing } 29 | LWSwitch >> children [ 30 | ^ ReadStream on: { child } 31 | ] 32 | 33 | { #category : #initialization } 34 | LWSwitch >> initialize [ 35 | super initialize. 36 | on := false. 37 | ] 38 | 39 | { #category : #testing } 40 | LWSwitch >> isOn [ 41 | "is the switch on?" 42 | ^ on 43 | 44 | ] 45 | 46 | { #category : #accessing } 47 | LWSwitch >> off [ 48 | "set the switch to off (hiding the component)" 49 | on := false. 50 | child isMounted ifTrue: [ ctx cleanup: child unmount: true ]. 51 | self changed. 52 | ] 53 | 54 | { #category : #accessing } 55 | LWSwitch >> on [ 56 | "set the switch to on (showing the component)" 57 | on := true. 58 | self changed. 59 | ] 60 | 61 | { #category : #rendering } 62 | LWSwitch >> renderOn: h [ 63 | on ifTrue: [ h span: [ child render: h ] ] 64 | ifFalse: [ h script: { #type -> 'liveweb/placeholder' } with: '' ] 65 | ] 66 | 67 | { #category : #operations } 68 | LWSwitch >> toggle [ 69 | "If switch is OFF, turn it ON... and vice versa." 70 | on ifTrue: [ self off ] ifFalse: [ self on ] 71 | 72 | ] 73 | -------------------------------------------------------------------------------- /src/LiveWeb-Core/LWTemplate.class.st: -------------------------------------------------------------------------------- 1 | " 2 | I render an HTML